Claude d3689b88fc
feat(onboarding): show the onboarding cards and check-in names in the user's language
The onboarding cards, the closing line and the coach routine names were English server text
for every user. The server keeps the state and an English fallback; the web now words them
through Lingui, so there is one translation source.

- Choice cards: the web says the question, the option labels and the placeholder from the
  step and option ids, and keeps the server text for anything it does not know (quiet-hour
  ranges are already written in the user's locale). Another bot's focus card is recognized
  by its option ids. Mobile and agent history keep the server text.
- The "Got it. … are a good place to start." line is gone: the app cards speak for
  themselves, and with no cards nothing is said.
- The closing line becomes an intake_note block (note "done", nextCheckInAt, English text).
  The web words it with the first check-in in the user's clock; mobile blockText and
  blocksToAgentHistoryText read its text through their generic fallback.
- Routines carry coachKey. The routine list and editor show a check-in under its label in
  the UI language until the user renames it; saving an untouched label keeps the stored
  name. The English names move to contracts (COACH_ROUTINE_NAMES); prompts stay English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 14:51:59 +00:00

1390 lines
45 KiB
TypeScript

import * as z from "zod";
import { CoachRoutineKeySchema } from "./coach.js";
import { DelegatedTaskStatusSchema, ThreadMessageSchema } from "./events.js";
import { Id, MemoryScope, RunStatus, SandboxKind } from "./ids.js";
import { McpHeadersSchema, McpRemoteEndpointSchema, McpTransportSchema } from "./mcp.js";
export const ComputerModeSchema = z.enum(["team", "dedicated"]);
export type ComputerMode = z.infer<typeof ComputerModeSchema>;
export const MemoryScopeSchema = z.enum(["isolated", "shared"]);
export type MemoryScopeValue = z.infer<typeof MemoryScopeSchema>;
export const AvatarStyleSchema = z.enum(["robot", "organic"]);
export type AvatarStyle = z.infer<typeof AvatarStyleSchema>;
export const ThinkingLevelSchema = z.enum([
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
]);
export type ThinkingLevel = z.infer<typeof ThinkingLevelSchema>;
export const AGENT_SECRET_NAME_PATTERN = /^[A-Z_][A-Z0-9_]{0,63}$/;
export const AgentSecretSchema = z.object({
id: Id,
name: z.string().regex(AGENT_SECRET_NAME_PATTERN),
createdAt: z.string(),
updatedAt: z.string(),
});
export type AgentSecret = z.infer<typeof AgentSecretSchema>;
export const AgentSecretInputSchema = z.object({
name: z.string().trim().regex(AGENT_SECRET_NAME_PATTERN),
value: z.string().min(1).max(16_384),
});
export type AgentSecretInput = z.infer<typeof AgentSecretInputSchema>;
/** "main" is the user's single lead agent; every other bot is a "specialist". */
export const BOT_KINDS = ["main", "specialist"] as const;
export const BotKindSchema = z.enum(BOT_KINDS);
export type BotKind = z.infer<typeof BotKindSchema>;
/** Model tiers a bot or a single task can ask for; each maps to a connected model per space. */
export const MODEL_TIERS = ["fast", "standard", "code"] as const;
export const ModelTierSchema = z.enum(MODEL_TIERS);
export type ModelTier = z.infer<typeof ModelTierSchema>;
export const BotSchema = z.object({
id: Id,
spaceId: Id,
name: z.string(),
title: z.string(),
description: z.string(),
instructions: z.string(),
color: z.string(),
notifyOnFinish: z.boolean(),
pinned: z.boolean(),
sectionId: Id.nullable(),
archivedAt: z.string().nullable(),
unread: z.boolean(),
parentBotId: Id.nullable(),
memoryScope: MemoryScopeSchema.nullable(),
threadId: Id,
preview: z.string(),
status: z.string(),
computerMode: ComputerModeSchema,
updatedAt: z.string(),
createdAt: z.string(),
voiceId: z.string().nullable(),
autoSpeak: z.boolean(),
modelProvider: z.string().nullable(),
modelId: z.string().nullable(),
thinkingLevel: ThinkingLevelSchema.nullable(),
teamChatAmbientEnabled: z.boolean(),
teamChatRules: z.string(),
webhookConfigured: z.boolean(),
/** Present when created with an idempotency key (e.g. onboarding:first). */
spawnKey: z.string().nullable(),
kind: BotKindSchema,
/** Capability tags used to route delegated work. */
tags: z.array(z.string()),
/** When true, only allowedBuiltinTools are exposed to this bot's runs. */
restrictBuiltinTools: z.boolean(),
allowedBuiltinTools: z.array(z.string()),
modelTier: ModelTierSchema.nullable(),
});
export type Bot = z.infer<typeof BotSchema>;
export const ReorderBotsInput = z.object({
botIds: z
.array(Id)
.min(1)
.refine((ids) => new Set(ids).size === ids.length, { error: "botIds must be distinct" }),
});
export type ReorderBotsInput = z.infer<typeof ReorderBotsInput>;
export const GroupMemberSchema = z.object({
botId: Id,
name: z.string(),
color: z.string(),
status: z.string().optional(),
});
export type GroupMember = z.infer<typeof GroupMemberSchema>;
/** Selected-text excerpt carried by a reply; capped so a quote stays a quote. */
export const REPLY_QUOTE_MAX_LENGTH = 2_000;
export const GROUP_MEMBER_MIN = 2;
export const GROUP_MEMBER_MAX = 6;
/** "main" is the user's main conversation, led by the main bot; it has no member limits. */
export const GROUP_KINDS = ["group", "main"] as const;
export const GroupKindSchema = z.enum(GROUP_KINDS);
export type GroupKind = z.infer<typeof GroupKindSchema>;
export const GroupSchema = z.object({
id: Id,
spaceId: Id,
name: z.string(),
kind: GroupKindSchema,
leadBotId: Id.nullable(),
pinned: z.boolean(),
sectionId: Id.nullable(),
archivedAt: z.string().nullable(),
members: z.array(GroupMemberSchema),
threadId: Id,
preview: z.string(),
unread: z.boolean(),
updatedAt: z.string(),
createdAt: z.string(),
});
export type Group = z.infer<typeof GroupSchema>;
const GroupBotIds = z
.array(Id)
.min(GROUP_MEMBER_MIN)
.max(GROUP_MEMBER_MAX)
.refine((ids) => new Set(ids).size === ids.length, { error: "botIds must be distinct" });
export const CreateGroupInput = z.object({
name: z.string().trim().min(1).max(80),
botIds: GroupBotIds,
});
export type CreateGroupInput = z.infer<typeof CreateGroupInput>;
export const UpdateGroupInput = z.object({
groupId: Id,
name: z.string().trim().min(1).max(80).optional(),
botIds: GroupBotIds.optional(),
pinned: z.boolean().optional(),
sectionId: Id.nullable().optional(),
});
export type UpdateGroupInput = z.infer<typeof UpdateGroupInput>;
export const GroupDetailSchema = GroupSchema.extend({
messages: z.array(ThreadMessageSchema).optional(),
});
export type GroupDetail = z.infer<typeof GroupDetailSchema>;
export const BotSectionSchema = z.object({
id: Id,
name: z.string(),
position: z.number().int().nonnegative(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type BotSection = z.infer<typeof BotSectionSchema>;
/**
* A space is a real privacy boundary, not just sidebar organization.
* Bots are included so clients can keep every space visible without granting
* those bots access to the currently active space.
*/
export const SpaceBotSchema = BotSchema.pick({
id: true,
spaceId: true,
kind: true,
name: true,
title: true,
color: true,
notifyOnFinish: true,
pinned: true,
sectionId: true,
unread: true,
preview: true,
status: true,
updatedAt: true,
});
export type SpaceBot = z.infer<typeof SpaceBotSchema>;
export const SpaceGroupSchema = GroupSchema.pick({
id: true,
spaceId: true,
kind: true,
name: true,
pinned: true,
sectionId: true,
members: true,
preview: true,
unread: true,
updatedAt: true,
});
export type SpaceGroup = z.infer<typeof SpaceGroupSchema>;
export const TEAM_CHAT_RULES_MAX_LENGTH = 4000;
export const AutomatedSenderPolicyModeSchema = z.enum(["ignore", "rollup", "action", "user"]);
export type AutomatedSenderPolicyMode = z.infer<typeof AutomatedSenderPolicyModeSchema>;
export const AutomatedSenderPolicySchema = z
.object({
name: z.string().trim().min(1).max(120),
mode: AutomatedSenderPolicyModeSchema,
rollupHours: z.number().int().min(1).max(720).optional(),
})
.superRefine((policy, ctx) => {
if (policy.mode === "rollup" && policy.rollupHours === undefined) {
ctx.addIssue({
code: "custom",
message: "Rollup policies require a frequency",
path: ["rollupHours"],
});
}
});
export type AutomatedSenderPolicy = z.infer<typeof AutomatedSenderPolicySchema>;
export const AutomatedSenderPoliciesSchema = z
.record(z.string().trim().min(1).max(200), AutomatedSenderPolicySchema)
.refine((policies) => Object.keys(policies).length <= 50, {
message: "At most 50 automated sender policies are allowed",
});
export type AutomatedSenderPolicies = z.infer<typeof AutomatedSenderPoliciesSchema>;
export const AutomatedSenderSchema = z.object({
id: z.string().min(1).max(200),
name: z.string().min(1).max(120),
});
export type AutomatedSender = z.infer<typeof AutomatedSenderSchema>;
export const ExternalConversationPolicySchema = z.object({
teamChatAmbientEnabled: z.boolean().nullable(),
teamChatRules: z.string().max(TEAM_CHAT_RULES_MAX_LENGTH).nullable(),
automatedSenderPolicies: AutomatedSenderPoliciesSchema,
});
export type ExternalConversationPolicy = z.infer<typeof ExternalConversationPolicySchema>;
export const UpdateExternalConversationPolicyInput = ExternalConversationPolicySchema.extend({
externalConversationId: Id,
});
export type UpdateExternalConversationPolicyInput = z.infer<
typeof UpdateExternalConversationPolicyInput
>;
export const ExternalConversationSchema = z.object({
id: Id,
spaceId: Id,
botId: Id,
provider: z.string(),
displayName: z.string().nullable(),
participantNames: z.array(z.string()),
teamChatAmbientEnabled: z.boolean().nullable(),
teamChatRules: z.string().nullable(),
automatedSenderPolicies: AutomatedSenderPoliciesSchema,
automatedSenders: z.array(AutomatedSenderSchema),
threadId: Id,
preview: z.string(),
unread: z.boolean(),
updatedAt: z.string(),
});
export type ExternalConversation = z.infer<typeof ExternalConversationSchema>;
export const SpaceGitLinkSchema = z.object({
remoteUrl: z.string(),
defaultBranch: z.string(),
headCommit: z.string().nullable(),
dirty: z.boolean(),
dirtyPaths: z.array(z.string()),
});
export type SpaceGitLink = z.infer<typeof SpaceGitLinkSchema>;
export const SpaceWorkspaceSchema = z.object({
path: z.string(),
git: SpaceGitLinkSchema.nullable(),
});
export type SpaceWorkspace = z.infer<typeof SpaceWorkspaceSchema>;
export const SpaceSchema = z.object({
id: Id,
name: z.string(),
isDefault: z.boolean(),
/** True when the space has any bot or group, including archived. */
hasContent: z.boolean(),
/** True only when the current member may delete this non-default space. */
canDelete: z.boolean().optional(),
workspace: SpaceWorkspaceSchema.nullable().optional(),
bots: z.array(SpaceBotSchema),
groups: z.array(SpaceGroupSchema),
externalConversations: z.array(ExternalConversationSchema),
botSections: z.array(BotSectionSchema),
});
export type Space = z.infer<typeof SpaceSchema>;
/** Where the user's main bot and main conversation live; the space may differ from the current one. */
export const MainConversationRefSchema = z.object({
botId: Id,
groupId: Id,
spaceId: Id,
});
export type MainConversationRef = z.infer<typeof MainConversationRefSchema>;
export const SpaceNavigationSchema = z.object({
current: z.object({
id: Id,
name: z.string(),
bots: z.array(BotSchema),
groups: z.array(GroupSchema),
externalConversations: z.array(ExternalConversationSchema),
botSections: z.array(BotSectionSchema),
}),
spaces: z.array(SpaceSchema),
/** Null until onboarding has created the main bot. */
main: MainConversationRefSchema.nullable(),
});
export type SpaceNavigation = z.infer<typeof SpaceNavigationSchema>;
export const BOT_NAME_MAX_LENGTH = 80;
export const BOT_TITLE_MAX_LENGTH = 500;
export const BOT_DESCRIPTION_MAX_LENGTH = 4000;
export const BOT_INSTRUCTIONS_MAX_LENGTH = 20000;
export const BOT_TAG_MAX_LENGTH = 40;
export const BOT_TAGS_MAX = 20;
/** Upper bound for an allowlist; the router checks each name against the builtin tools. */
export const BOT_ALLOWED_TOOLS_MAX = 100;
const distinct = (values: string[]) => new Set(values).size === values.length;
const BotTags = z
.array(z.string().trim().min(1).max(BOT_TAG_MAX_LENGTH))
.max(BOT_TAGS_MAX)
.refine(distinct, { error: "tags must be distinct" });
/** Trims tags and drops empty or repeated ones, within the limits the bot inputs enforce. */
export function normalizeBotTags(values: readonly string[]): string[] {
const tags = values.map((value) => value.trim().slice(0, BOT_TAG_MAX_LENGTH).trim());
return [...new Set(tags.filter(Boolean))].slice(0, BOT_TAGS_MAX);
}
const BotAllowedBuiltinTools = z
.array(z.string().min(1).max(64))
.max(BOT_ALLOWED_TOOLS_MAX)
.refine(distinct, { error: "allowedBuiltinTools must be distinct" });
export const CreateBotInput = z.object({
name: z.string().trim().min(1).max(BOT_NAME_MAX_LENGTH),
title: z.string().max(BOT_TITLE_MAX_LENGTH).default(""),
description: z.string().max(BOT_DESCRIPTION_MAX_LENGTH).default(""),
instructions: z.string().max(BOT_INSTRUCTIONS_MAX_LENGTH).default(""),
notifyOnFinish: z.boolean().default(true),
color: z.string().optional(),
computerMode: ComputerModeSchema.default("team"),
/** Idempotency key within a space (unique with spaceId). */
spawnKey: z.string().trim().min(1).max(120).optional(),
tags: BotTags.optional(),
restrictBuiltinTools: z.boolean().optional(),
allowedBuiltinTools: BotAllowedBuiltinTools.optional(),
});
export type CreateBotInput = z.infer<typeof CreateBotInput>;
export function normalizeCreateBotProfile(
input: Pick<CreateBotInput, "name" | "title" | "description">,
) {
const description = input.description.trim();
return {
name: input.name.trim().slice(0, BOT_NAME_MAX_LENGTH),
title: input.title.trim().slice(0, BOT_TITLE_MAX_LENGTH),
description: description.slice(0, BOT_DESCRIPTION_MAX_LENGTH),
instructions: description.slice(0, BOT_INSTRUCTIONS_MAX_LENGTH),
};
}
export const UpdateBotInput = z
.object({
botId: Id,
name: z.string().trim().min(1).max(BOT_NAME_MAX_LENGTH).optional(),
title: z.string().trim().max(BOT_TITLE_MAX_LENGTH).optional(),
description: z.string().trim().max(BOT_DESCRIPTION_MAX_LENGTH).optional(),
instructions: z.string().trim().max(BOT_INSTRUCTIONS_MAX_LENGTH).optional(),
notifyOnFinish: z.boolean().optional(),
color: z.string().optional(),
pinned: z.boolean().optional(),
memoryScope: MemoryScopeSchema.nullable().optional(),
sectionId: Id.nullable().optional(),
voiceId: z.string().max(120).nullable().optional(),
autoSpeak: z.boolean().optional(),
modelProvider: z.string().trim().min(1).max(80).nullable().optional(),
modelId: z.string().trim().min(1).max(200).nullable().optional(),
thinkingLevel: ThinkingLevelSchema.nullable().optional(),
modelTier: ModelTierSchema.nullable().optional(),
teamChatAmbientEnabled: z.boolean().optional(),
teamChatRules: z.string().max(TEAM_CHAT_RULES_MAX_LENGTH).optional(),
tags: BotTags.optional(),
restrictBuiltinTools: z.boolean().optional(),
allowedBuiltinTools: BotAllowedBuiltinTools.optional(),
})
.superRefine((value, ctx) => {
// A bot runs on a pinned model or a tier; setting one clears the other.
if (value.modelTier && (value.modelProvider || value.modelId)) {
ctx.addIssue({
code: "custom",
message: "Choose a model or a tier, not both",
path: ["modelTier"],
});
}
const providerProvided = value.modelProvider !== undefined;
const modelProvided = value.modelId !== undefined;
if (!providerProvided && !modelProvided) return;
// Reject partial shapes like `{ modelId: null }` (provider omitted) so a
// clear cannot succeed without updating both persisted fields.
if (providerProvided !== modelProvided) {
ctx.addIssue({
code: "custom",
message: "Model provider and model id must both be set or both cleared",
path: ["modelId"],
});
return;
}
const bothNull = value.modelProvider === null && value.modelId === null;
const bothSet = Boolean(value.modelProvider) && Boolean(value.modelId);
if (!bothNull && !bothSet) {
ctx.addIssue({
code: "custom",
message: "Model provider and model id must both be set or both cleared",
path: ["modelId"],
});
}
});
export const RoutineSchema = z.object({
id: Id,
botId: Id,
name: z.string(),
prompt: z.string(),
crons: z.array(z.string()),
timezone: z.string(),
active: z.boolean(),
notify: z.boolean(),
webhookEnabled: z.boolean(),
githubEnabled: z.boolean(),
messageProvider: z
.string()
.min(1)
.max(50)
.regex(/^[a-z0-9._-]+$/i)
.nullable(),
lastRunAt: z.string().nullable(),
nextRunAt: z.string().nullable(),
createdAt: z.string(),
/** Set on the main bot's check-in routines. */
coachKey: CoachRoutineKeySchema.nullable(),
});
export type Routine = z.infer<typeof RoutineSchema>;
export const CreateRoutineInput = z
.object({
botId: Id,
name: z.string().min(1).max(80),
prompt: z.string().min(1),
crons: z.array(z.string().min(1)).default([]),
timezone: z.string().default("UTC"),
notify: z.boolean().default(true),
active: z.boolean().default(false),
webhookEnabled: z.boolean().default(false),
githubEnabled: z.boolean().default(false),
messageProvider: z
.string()
.min(1)
.max(50)
.regex(/^[a-z0-9._-]+$/i)
.nullable()
.default(null),
})
.superRefine((value, ctx) => {
if (
value.crons.length === 0 &&
!value.webhookEnabled &&
!value.githubEnabled &&
!value.messageProvider
) {
ctx.addIssue({
code: "custom",
message: "Add a schedule, webhook, GitHub, or message trigger",
path: ["crons"],
});
}
});
export const ScratchpadItemStatusSchema = z.enum(["open", "parked", "done"]);
export type ScratchpadItemStatus = z.infer<typeof ScratchpadItemStatusSchema>;
export const ScratchpadItemSchema = z.object({
id: Id,
botId: Id,
title: z.string(),
status: ScratchpadItemStatusSchema,
notes: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type ScratchpadItem = z.infer<typeof ScratchpadItemSchema>;
export const CreateScratchpadItemInput = z.object({
botId: Id,
title: z.string().min(1).max(200),
status: ScratchpadItemStatusSchema.default("open"),
notes: z.string().max(4_000).default(""),
});
export const TaughtSkillStatusSchema = z.enum(["recording", "drafting", "draft", "saved"]);
export type TaughtSkillStatus = z.infer<typeof TaughtSkillStatusSchema>;
export const SkillPlaybookSchema = z.object({
whenToUse: z.string(),
inputs: z.array(z.string()),
steps: z.array(z.string()),
howToCheck: z.string(),
whatToReturn: z.string(),
approvalBoundaries: z.string(),
failureHandling: z.string(),
});
export type SkillPlaybook = z.infer<typeof SkillPlaybookSchema>;
export const TeachRecordingEventSchema = z.object({
at: z.string(),
kind: z.enum(["pointer", "key", "clipboard", "snapshot", "scroll"]),
x: z.number().optional(),
y: z.number().optional(),
button: z.string().optional(),
type: z.string().optional(),
key: z.string().optional(),
text: z.string().optional(),
summary: z.string().optional(),
});
export type TeachRecordingEvent = z.infer<typeof TeachRecordingEventSchema>;
export const TeachSnapshotSchema = z.object({
at: z.string(),
summary: z.string(),
hash: z.string().optional(),
});
export type TeachSnapshot = z.infer<typeof TeachSnapshotSchema>;
export const TeachRecordingSchema = z.object({
events: z.array(TeachRecordingEventSchema),
snapshots: z.array(TeachSnapshotSchema),
controlLeaseId: z.string().optional(),
});
export type TeachRecording = z.infer<typeof TeachRecordingSchema>;
export const TaughtSkillSchema = z.object({
id: Id,
botId: Id,
name: z.string(),
goal: z.string(),
status: TaughtSkillStatusSchema,
playbook: SkillPlaybookSchema,
recording: TeachRecordingSchema,
startedAt: z.string().nullable(),
expiresAt: z.string().nullable(),
stoppedAt: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type TaughtSkill = z.infer<typeof TaughtSkillSchema>;
export const AgentSkillSourceSchema = z.enum(["user", "builtin", "plugin"]);
export type AgentSkillSource = z.infer<typeof AgentSkillSourceSchema>;
export const AgentSkillSchema = z.object({
id: Id,
name: z.string(),
description: z.string(),
content: z.string(),
source: AgentSkillSourceSchema,
readOnly: z.boolean(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type AgentSkill = z.infer<typeof AgentSkillSchema>;
export const AgentSkillCatalogEntrySchema = AgentSkillSchema.pick({
id: true,
name: true,
description: true,
source: true,
readOnly: true,
});
export type AgentSkillCatalogEntry = z.infer<typeof AgentSkillCatalogEntrySchema>;
export const CreateAgentSkillInput = z
.object({
content: z.string().min(1).max(100_000).optional(),
name: z.string().min(1).max(80).optional(),
description: z.string().min(1).max(2000).optional(),
body: z.string().max(100_000).optional(),
})
.superRefine((input, ctx) => {
if (input.content?.trim()) return;
if (!input.name?.trim() || !input.description?.trim()) {
ctx.addIssue({
code: "custom",
message: "Provide content (SKILL.md) or name + description (+ optional body)",
path: ["content"],
});
}
});
export const UpdateAgentSkillInput = z
.object({
skillId: Id,
content: z.string().min(1).max(100_000).optional(),
name: z.string().min(1).max(80).optional(),
description: z.string().min(1).max(2000).optional(),
body: z.string().max(100_000).optional(),
})
.superRefine((input, ctx) => {
if (
input.content === undefined &&
input.name === undefined &&
input.description === undefined &&
input.body === undefined
) {
ctx.addIssue({
code: "custom",
message: "Provide at least one field to update",
path: ["content"],
});
}
});
export const MemoryDocumentSchema = z.object({
id: Id,
scope: MemoryScope,
botId: Id.nullable(),
path: z.string(),
content: z.string(),
revision: z.number().int(),
updatedAt: z.string(),
});
export type MemoryDocument = z.infer<typeof MemoryDocumentSchema>;
export const ConnectionSchema = z.object({
id: Id,
connectorId: z.string(),
provider: z.string(),
displayName: z.string(),
status: z.enum(["pending", "connected", "revoked", "error"]),
capabilities: z.array(z.string()),
createdAt: z.string(),
});
export type Connection = z.infer<typeof ConnectionSchema>;
export const ConnectionCatalogItemSchema = z.object({
connectorId: z.string(),
slug: z.string(),
name: z.string(),
logo: z.string().nullable(),
connected: z.boolean(),
noAuth: z.boolean(),
});
export type ConnectionCatalogItem = z.infer<typeof ConnectionCatalogItemSchema>;
export const ActionApprovalRuleSchema = z.object({
id: Id,
effect: z.enum(["always_allow", "require_approval"]),
matchKind: z.enum(["tool", "connector", "category"]),
matchValue: z.string(),
createdAt: z.string(),
});
export type ActionApprovalRule = z.infer<typeof ActionApprovalRuleSchema>;
export const ActionAutoReviewSettingsSchema = z.object({
enabled: z.boolean(),
checkerAvailable: z.boolean(),
});
export type ActionAutoReviewSettings = z.infer<typeof ActionAutoReviewSettingsSchema>;
export const CapabilityInstallSchema = z.object({
id: Id,
kind: z.enum(["skill", "plugin", "mcp", "api", "graphql", "connection"]),
name: z.string(),
source: z.string(),
version: z.string().nullable(),
digest: z.string().nullable(),
secretConfigured: z.boolean(),
config: z.record(z.string(), z.unknown()),
createdAt: z.string(),
});
export type CapabilityInstall = z.infer<typeof CapabilityInstallSchema>;
export const IntegrationCatalogSurfaceSchema = z.object({
kind: z.enum(["mcp", "openapi", "graphql", "cli"]),
slug: z.string(),
source: z.string().nullable(),
auth: z
.object({
type: z.enum(["none", "bearer", "header"]),
headerName: z.string().nullable(),
note: z.string().nullable(),
})
.nullable(),
});
export type IntegrationCatalogSurface = z.infer<typeof IntegrationCatalogSurfaceSchema>;
export const IntegrationCatalogResultSchema = z.object({
domain: z.string(),
name: z.string(),
description: z.string(),
pageUrl: z.string().nullable(),
surfaces: z.array(IntegrationCatalogSurfaceSchema),
});
export type IntegrationCatalogResult = z.infer<typeof IntegrationCatalogResultSchema>;
export type { McpTransport } from "./mcp.js";
const McpServerBaseInput = z.object({
slug: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/),
name: z.string().trim().min(1).max(120),
description: z.string().max(2000).default(""),
enabled: z.boolean().default(true),
/** Update-only: drop the stored static credential (secret/env/headers).
* OAuth state survives so a connected server stays connected. */
clearCredential: z.boolean().optional(),
});
export const McpServerConfigInput = z.discriminatedUnion("transport", [
McpServerBaseInput.extend({
transport: z.literal("streamable_http"),
endpoint: McpRemoteEndpointSchema,
headers: McpHeadersSchema.default({}),
secret: z.string().max(16384).optional(),
}),
McpServerBaseInput.extend({
transport: z.literal("sse"),
endpoint: McpRemoteEndpointSchema,
headers: McpHeadersSchema.default({}),
secret: z.string().max(16384).optional(),
}),
McpServerBaseInput.extend({
transport: z.literal("stdio"),
command: z.string().min(1).max(512),
args: z.array(z.string().max(2048)).max(64).default([]),
env: z
.record(z.string().regex(/^[A-Z_][A-Z0-9_]*$/), z.string().max(4096))
.superRefine((value, ctx) => {
if (Object.keys(value).length > 32) {
ctx.addIssue({ code: "custom", message: "At most 32 environment variables are allowed" });
}
})
.default({}),
secret: z.string().max(16384).optional(),
}),
]);
export type McpServerConfigInput = z.infer<typeof McpServerConfigInput>;
export const McpServerSchema = z.object({
id: Id,
spaceId: Id,
slug: z.string(),
name: z.string(),
description: z.string(),
transport: McpTransportSchema,
endpoint: z.string().url().nullable(),
command: z.string().nullable(),
args: z.array(z.string()),
envKeys: z.array(z.string()),
headerKeys: z.array(z.string()),
hasSecret: z.boolean(),
oauthStatus: z.enum(["none", "connected", "reconnect"]),
enabled: z.boolean(),
revision: z.number().int().positive(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type McpServer = z.infer<typeof McpServerSchema>;
export const BotMcpServerSchema = z.object({
id: Id,
botId: Id,
serverId: Id,
allowAllTools: z.boolean(),
allowedTools: z.array(z.string().min(1).max(200)),
createdAt: z.string(),
updatedAt: z.string(),
});
export type BotMcpServer = z.infer<typeof BotMcpServerSchema>;
export const ArtifactSchema = z.object({
id: Id,
botId: Id.nullable(),
groupId: Id.nullable(),
runId: Id.nullable(),
name: z.string(),
mimeType: z.string(),
size: z.number().int(),
createdAt: z.string(),
});
export const ArtifactWithContentSchema = ArtifactSchema.extend({
contentBase64: z.string(),
});
export type ArtifactWithContent = z.infer<typeof ArtifactWithContentSchema>;
export const UsageRecordSchema = z.object({
id: Id,
botId: Id.nullable(),
runId: Id.nullable(),
provider: z.string(),
model: z.string(),
inputTokens: z.number().int(),
outputTokens: z.number().int(),
createdAt: z.string(),
});
export const COMPUTER_UPDATE_STAGES = [
"preparing",
"saving",
"recreating",
"restoring",
"reconnecting",
] as const;
export const ComputerUpdateSchema = z.object({
canReleaseReservation: z.boolean().optional(),
action: z.enum(["update", "recover"]),
id: Id,
botId: Id,
name: z.string(),
mode: ComputerModeSchema,
status: z.enum(["queued", "running", "interrupted", "completed", "failed"]),
stage: z.enum(COMPUTER_UPDATE_STAGES),
});
export type ComputerUpdate = z.infer<typeof ComputerUpdateSchema>;
export const ComputerStatusSchema = z.object({
botId: Id,
mode: ComputerModeSchema,
kind: SandboxKind,
state: z.enum(["stopped", "booting", "running", "suspended", "error"]),
controlHolder: z.enum(["bot", "user", "none"]),
controlBotId: Id.nullable(),
takeoverRequested: z.boolean(),
screenAvailable: z.boolean(),
screenWidth: z.number().int().positive(),
screenHeight: z.number().int().positive(),
homeRevision: z.string().nullable(),
busyBotName: z.string().nullable(),
canUpdate: z.boolean(),
});
export type ComputerStatus = z.infer<typeof ComputerStatusSchema>;
export const ComputerReleaseReasonSchema = z.enum(["done", "skipped"]);
export type ComputerReleaseReason = z.infer<typeof ComputerReleaseReasonSchema>;
export const MessagingLinkedIdentitySchema = z.object({
id: Id,
provider: z.string(),
address: z.string(),
botId: Id,
botName: z.string(),
});
export type MessagingLinkedIdentity = z.infer<typeof MessagingLinkedIdentitySchema>;
export const MessagingStatusSchema = z.object({
enabled: z.boolean(),
/** Messaging platforms mounted on this deployment (sendblue, slack, …). */
providers: z.array(z.string()),
/** True when unknown senders auto-provision their own accounts. */
openSignup: z.boolean(),
/** The caller's linked chat apps, one entry per (provider, address). */
identities: z.array(MessagingLinkedIdentitySchema),
});
export type MessagingStatus = z.infer<typeof MessagingStatusSchema>;
export const MessagingChannelMembershipSchema = z.object({
/** One row per linked identity: the same group can hold two of the caller's. */
id: Id,
channelId: Id,
/** Which of the caller's linked chat apps this membership belongs to. */
identityId: Id,
provider: z.string(),
name: z.string().nullable(),
status: z.enum(["invited", "approved", "declined", "left"]),
memberCount: z.number().int().nonnegative(),
});
export type MessagingChannelMembership = z.infer<typeof MessagingChannelMembershipSchema>;
export const MessagingAgentConnectionSchema = z.object({
id: Id,
peerBotName: z.string(),
peerOwnerLabel: z.string(),
status: z.enum(["pending", "approved", "declined", "revoked"]),
/** true when the caller's bot is the target (only the target can respond). */
incoming: z.boolean(),
});
export type MessagingAgentConnection = z.infer<typeof MessagingAgentConnectionSchema>;
export const RunSchema = z.object({
id: Id,
botId: Id,
threadId: Id,
taskId: Id,
status: RunStatus,
trigger: z.enum([
"user",
"routine",
"resume",
"follow_up",
"reaction",
"spawn",
"skill",
"bot_message",
"webhook",
"messaging",
"cloud_agent",
"delegation",
]),
routineId: Id.nullable(),
modelProvider: z.string().nullable(),
modelId: z.string().nullable(),
error: z.string().nullable(),
startedAt: z.string().nullable(),
completedAt: z.string().nullable(),
createdAt: z.string(),
});
export type Run = z.infer<typeof RunSchema>;
export const ThreadMessagePageSchema = z.object({
threadId: Id,
messages: z.array(ThreadMessageSchema),
olderCursor: z.number().int().nonnegative().nullable(),
});
export type ThreadMessagePage = z.infer<typeof ThreadMessagePageSchema>;
export const ThreadSnapshotSchema = z.object({
threadId: Id,
cursor: z.number().int().min(-1),
messages: z.array(ThreadMessageSchema),
olderCursor: z.number().int().nonnegative().nullable(),
botId: Id.optional(),
groupId: Id.optional(),
groupName: z.string().optional(),
members: z.array(GroupMemberSchema).optional(),
/** The member that answers when nobody is addressed (the main conversation's main bot). */
leadBotId: Id.optional(),
run: RunSchema.nullable(),
activeRuns: z.array(RunSchema).optional(),
computer: ComputerStatusSchema.optional(),
});
export type ThreadSnapshot = z.infer<typeof ThreadSnapshotSchema>;
/** Default maximum number of completion tokens for an OpenAI-compatible connection. */
export const DEFAULT_MODEL_MAX_TOKENS = 4_096;
/** Largest completion-token limit exposed by model settings. */
export const MAX_MODEL_MAX_TOKENS = 131_072;
/** Default context window for an OpenAI-compatible connection. */
export const DEFAULT_MODEL_CONTEXT_WINDOW = 32_768;
/** Largest context window exposed by model settings. */
export const MAX_MODEL_CONTEXT_WINDOW = 1_048_576;
/** Parse the optional per-connection image limit entered in model settings. */
export function parseModelMaxImagesPerPrompt(
value: string,
supportsImages = true,
): number | undefined {
if (!supportsImages) return undefined;
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 1000 ? parsed : undefined;
}
/** Parse the optional completion-token limit entered in model settings. */
export function parseModelMaxTokens(value: string): number | undefined {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 1 && parsed <= MAX_MODEL_MAX_TOKENS
? parsed
: undefined;
}
/** Parse the optional context-window limit entered in model settings. */
export function parseModelContextWindow(value: string): number | undefined {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 1 && parsed <= MAX_MODEL_CONTEXT_WINDOW
? parsed
: undefined;
}
export const ModelCredentialSchema = z.object({
id: Id,
provider: z.string(),
label: z.string(),
hasKey: z.boolean(),
isDefault: z.boolean(),
baseUrl: z.string().optional(),
modelId: z.string().optional(),
reasoning: z.boolean().optional(),
thinkingLevel: ThinkingLevelSchema.nullable().optional(),
maxTokens: z.number().int().min(1).max(MAX_MODEL_MAX_TOKENS).optional(),
contextWindow: z.number().int().min(1).max(MAX_MODEL_CONTEXT_WINDOW).optional(),
supportsImages: z.boolean().optional(),
maxImagesPerPrompt: z.number().int().min(1).max(1000).optional(),
thinkingLevels: z.array(ThinkingLevelSchema).optional(),
});
export type ModelCredential = z.infer<typeof ModelCredentialSchema>;
export const OPENAI_COMPATIBLE_PROVIDER_ID = "openai-compatible";
export const ModelConnectInputSchema = z
.object({
provider: z.string(),
apiKey: z.string().optional(),
baseUrl: z.string().optional(),
label: z.string().optional(),
modelId: z.string().optional(),
reasoning: z.boolean().optional(),
thinkingLevel: ThinkingLevelSchema.nullable().optional(),
maxTokens: z.number().int().min(1).max(MAX_MODEL_MAX_TOKENS).optional(),
contextWindow: z.number().int().min(1).max(MAX_MODEL_CONTEXT_WINDOW).optional(),
supportsImages: z.boolean().optional(),
maxImagesPerPrompt: z.number().int().min(1).max(1000).nullable().optional(),
})
.superRefine((value, ctx) => {
if (
value.maxTokens !== undefined &&
value.contextWindow !== undefined &&
value.maxTokens > value.contextWindow
) {
ctx.addIssue({
code: "custom",
message: "Maximum output tokens cannot exceed the context limit",
path: ["maxTokens"],
});
}
if (value.provider === OPENAI_COMPATIBLE_PROVIDER_ID) {
if (!value.baseUrl?.trim()) {
ctx.addIssue({
code: "custom",
message: "Base URL is required for OpenAI-compatible models",
path: ["baseUrl"],
});
}
if (!value.modelId?.trim()) {
ctx.addIssue({
code: "custom",
message: "Model id is required for OpenAI-compatible models",
path: ["modelId"],
});
}
return;
}
if (!value.apiKey || value.apiKey.trim().length < 1) {
ctx.addIssue({
code: "custom",
message: "API key must contain at least 8 characters",
path: ["apiKey"],
});
}
});
export type ModelConnectInput = z.infer<typeof ModelConnectInputSchema>;
export const ModelOAuthSignInModeSchema = z.enum(["device-code", "auth-url"]);
export type ModelOAuthSignInMode = z.infer<typeof ModelOAuthSignInModeSchema>;
const ModelOAuthBeginBaseSchema = z.object({
loginId: z.string(),
provider: z.string(),
verificationUri: z
.string()
.url()
.refine((value) => value.startsWith("https://"), "Expected an HTTPS authorization URL"),
expiresInSeconds: z.number().int().positive(),
});
export const ModelOAuthBeginSchema = z.discriminatedUnion("mode", [
ModelOAuthBeginBaseSchema.extend({
mode: z.literal("device-code"),
userCode: z.string().min(1),
}),
ModelOAuthBeginBaseSchema.extend({ mode: z.literal("auth-url") }),
]);
export type ModelOAuthBegin = z.infer<typeof ModelOAuthBeginSchema>;
export const SpaceMemoryConfigSchema = z.object({
provider: z.string(),
settings: z.record(z.string(), z.string()),
defaultMemoryScope: MemoryScopeSchema,
updatedAt: z.string(),
});
export type SpaceMemoryConfig = z.infer<typeof SpaceMemoryConfigSchema>;
export const ModelCatalogEntrySchema = z.object({
provider: z.string(),
providerName: z.string().optional(),
id: z.string(),
label: z.string(),
billing: z.string(),
auth: z.enum(["api-key", "oauth", "both"]).optional(),
oauthLabel: z.string().optional(),
authHint: z.string().optional(),
subscription: z.boolean().optional(),
signIn: ModelOAuthSignInModeSchema.optional(),
reasoning: z.boolean().optional(),
thinkingLevels: z.array(ThinkingLevelSchema).optional(),
/** Catalog stand-in so a provider appears before the user enters a real model id. */
placeholder: z.boolean().optional(),
});
export type ModelCatalogEntry = z.infer<typeof ModelCatalogEntrySchema>;
export const VoiceCatalogEntrySchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
transcribe: z.boolean(),
});
export type VoiceCatalogEntry = z.infer<typeof VoiceCatalogEntrySchema>;
export const VoiceInfoSchema = z.object({
id: z.string(),
label: z.string(),
description: z.string().optional(),
});
export type VoiceInfo = z.infer<typeof VoiceInfoSchema>;
export const VoiceCredentialSchema = z.object({
id: Id,
provider: z.string(),
hasKey: z.boolean(),
isDefault: z.boolean(),
voiceId: z.string(),
transcribe: z.boolean(),
});
export type VoiceCredential = z.infer<typeof VoiceCredentialSchema>;
export const VoiceStatusSchema = z.object({
configured: z.boolean(),
ready: z.boolean(),
transcribe: z.boolean(),
provider: z.string().nullable(),
voiceId: z.string(),
});
export type VoiceStatus = z.infer<typeof VoiceStatusSchema>;
export const DeploymentSettingsSchema = z.object({
ownerUserId: Id.nullable(),
signupsEnabled: z.boolean(),
signupAllowlist: z.array(z.string()),
hasDeploymentModelCredential: z.boolean(),
defaultProvider: z.string().nullable(),
defaultModel: z.string().nullable(),
computerHost: z.enum(["docker", "this-mac"]).nullable(),
canChooseHostComputer: z.boolean(),
sandboxProvider: z.string(),
});
export const ServerUpdateSourceSchema = z.object({
repoUrl: z.string().max(400),
branch: z.string().max(200),
official: z.boolean(),
});
export type ServerUpdateSource = z.infer<typeof ServerUpdateSourceSchema>;
export const ServerUpdateStepSchema = z.object({
id: z.string().max(40),
label: z.string().max(200),
ok: z.boolean(),
exitCode: z.number().int().nullable(),
output: z.string().max(8_001),
});
/** How an update reaches new code: published images, a build on the server, or a git checkout. */
export const ServerUpdateStrategySchema = z.enum(["pull", "build", "checkout"]);
export type ServerUpdateStrategy = z.infer<typeof ServerUpdateStrategySchema>;
/** `sidecar` is the Compose deployment; `checkout` is a supervised source install. */
export const ServerUpdateModeSchema = z.enum(["sidecar", "checkout", "unavailable"]);
export type ServerUpdateMode = z.infer<typeof ServerUpdateModeSchema>;
/**
* What Settings should render for a deployment owner.
* `sidecar`: Check / Update / Rollback through the updater sidecar.
* `compose`: Compose install without a reachable sidecar; show host pull/up commands.
* `source`: git checkout / `pnpm dev`; show terminal commands, never a fake Apply.
*/
export const ServerUpdateInstallKindSchema = z.enum(["sidecar", "compose", "source"]);
export type ServerUpdateInstallKind = z.infer<typeof ServerUpdateInstallKindSchema>;
export const ServerUpdateRunSchema = z.object({
startedAt: z.iso.datetime(),
finishedAt: z.iso.datetime().nullable(),
ok: z.boolean(),
fromCommit: z.string().nullable(),
toCommit: z.string().nullable(),
fromTag: z.string().nullable(),
toTag: z.string().nullable(),
strategy: ServerUpdateStrategySchema.nullable(),
repoUrl: z.string().max(400),
branch: z.string().max(200),
/**
* `recreated` means the updater sidecar replaced the containers and no restart is owed.
* `supervised` means the process exited and its supervisor is bringing it back.
*/
restart: z.enum(["recreated", "supervised", "manual", "not-required"]),
restartAdvice: z.string(),
error: z.string().nullable(),
steps: z.array(ServerUpdateStepSchema),
});
export type ServerUpdateRun = z.infer<typeof ServerUpdateRunSchema>;
export const ServerUpdateStatusSchema = z.object({
supported: z.boolean(),
unsupportedReason: z.string().nullable(),
installKind: ServerUpdateInstallKindSchema,
/** Host commands to run when `installKind` is not `sidecar`. Empty when the sidecar applies. */
manualCommands: z.array(z.string()),
mode: ServerUpdateModeSchema,
strategy: ServerUpdateStrategySchema.nullable(),
strategyNote: z.string().nullable(),
version: z.string(),
revision: z.string().nullable(),
commit: z.string().nullable(),
branch: z.string().nullable(),
remoteUrl: z.string().nullable(),
dirty: z.boolean(),
dirtyPaths: z.array(z.string()),
image: z.string().nullable(),
imageTag: z.string().nullable(),
previousImageTag: z.string().nullable(),
canRollback: z.boolean(),
source: ServerUpdateSourceSchema,
officialRepoUrl: z.string(),
restartSupervisor: z.enum(["systemd", "pm2", "declared", "none"]),
restartAdvice: z.string(),
running: z.boolean(),
lastRun: ServerUpdateRunSchema.nullable(),
});
export type ServerUpdateStatus = z.infer<typeof ServerUpdateStatusSchema>;
export const ServerUpdateCheckSchema = z.object({
status: z.enum(["unavailable", "dirty", "up-to-date", "available"]),
reason: z.string().nullable(),
changed: z.array(z.string()),
commit: z.string().nullable(),
targetCommit: z.string().nullable(),
targetTag: z.string().nullable(),
behindBy: z.number().int().nonnegative(),
});
export type ServerUpdateCheck = z.infer<typeof ServerUpdateCheckSchema>;
export const ServerUpdateRequestSchema = z.object({
repoUrl: z.string().max(400).optional(),
branch: z.string().max(200).optional(),
});
export type ServerUpdateRequest = z.infer<typeof ServerUpdateRequestSchema>;
export const MeSchema = z.object({
userId: Id,
email: z.string().email(),
name: z.string(),
spaceId: Id,
isDeploymentOwner: z.boolean(),
needsModel: z.boolean(),
defaultProvider: z.string().nullable(),
defaultModel: z.string().nullable(),
computerHost: z.enum(["docker", "this-mac"]).nullable(),
canChooseHostComputer: z.boolean(),
sandboxProvider: z.string(),
avatarStyle: AvatarStyleSchema,
});
export type Me = z.infer<typeof MeSchema>;
export const AppBootstrapSchema = z.object({
me: MeSchema,
bots: z.array(BotSchema),
groups: z.array(GroupSchema),
botSections: z.array(BotSectionSchema),
archivedBots: z.array(BotSchema),
archivedGroups: z.array(GroupSchema),
thread: ThreadSnapshotSchema.nullable(),
routines: z.array(RoutineSchema),
spaces: z.array(SpaceSchema),
main: MainConversationRefSchema.nullable(),
});
export type AppBootstrap = z.infer<typeof AppBootstrapSchema>;
export const ExportManifestSchema = z.object({
version: z.literal(1),
exportedAt: z.string(),
bot: BotSchema.pick({ name: true, title: true, description: true, instructions: true }),
memory: z.array(z.object({ path: z.string(), content: z.string() })),
routines: z.array(RoutineSchema.pick({ name: true, prompt: true, crons: true, timezone: true })),
files: z.array(z.object({ path: z.string(), content: z.string() })),
history: z.array(ThreadMessageSchema),
});
export type ExportManifest = z.infer<typeof ExportManifestSchema>;
/** Categories of durable knowledge about the user. */
export const USER_FACT_CATEGORIES = [
"identity",
"work",
"goals",
"people",
"health",
"values",
"habits",
"preferences",
"tools",
] as const;
export const UserFactCategorySchema = z.enum(USER_FACT_CATEGORIES);
export type UserFactCategory = z.infer<typeof UserFactCategorySchema>;
/** Facts in these categories need the user's confirmation before agents may use them. */
export const SENSITIVE_USER_FACT_CATEGORIES = [
"health",
"values",
] as const satisfies readonly UserFactCategory[];
export const USER_FACT_SOURCES = [
"onboarding",
"user",
"conversation",
"consolidation",
"connector",
] as const;
export const UserFactSourceSchema = z.enum(USER_FACT_SOURCES);
export type UserFactSource = z.infer<typeof UserFactSourceSchema>;
/** "pending" facts wait for the user's confirmation and are never shown to agents. */
export const USER_FACT_STATUSES = ["active", "pending"] as const;
export const UserFactStatusSchema = z.enum(USER_FACT_STATUSES);
export type UserFactStatus = z.infer<typeof UserFactStatusSchema>;
export const UserProfileSchema = z.object({
timeZone: z.string().nullable(),
locale: z.string().nullable(),
language: z.string().nullable(),
communicationStyle: z.string(),
summary: z.string(),
consolidatedAt: z.string().nullable(),
updatedAt: z.string().nullable(),
});
export type UserProfile = z.infer<typeof UserProfileSchema>;
export const UserFactSchema = z.object({
id: Id,
category: UserFactCategorySchema,
value: z.string(),
confidence: z.number().min(0).max(1),
source: UserFactSourceSchema,
status: UserFactStatusSchema,
sensitive: z.boolean(),
/** False when the fact is limited to spaceIds. */
allSpaces: z.boolean(),
spaceIds: z.array(Id),
sourceBotId: Id.nullable(),
sourceThreadId: Id.nullable(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type UserFact = z.infer<typeof UserFactSchema>;
export const DelegatedTaskSchema = z.object({
id: Id,
threadId: Id,
requesterBotId: Id,
assigneeBotId: Id,
assigneeSpaceId: Id,
title: z.string(),
brief: z.string(),
status: DelegatedTaskStatusSchema,
deadlineAt: z.string().nullable(),
result: z.string().nullable(),
runId: Id.nullable(),
createdAt: z.string(),
updatedAt: z.string(),
completedAt: z.string().nullable(),
});
export type DelegatedTask = z.infer<typeof DelegatedTaskSchema>;