cynarAI 54658ee689
feat: autopilot progress, composer start and morning summary (#146)
The autopilot's start line counts its steps and folds open to the latest
checkpoints, and its result reads as a card. The composer's plus menu
starts an autopilot with the draft as the order and a choice of until
done, tomorrow morning or a time. Today lists the autopilots that run,
wait or ended within a day, and their step runs no longer show one by
one. Autopilot runs send no completion or failure push; only a pause
notifies.


Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-05 04:45:49 +02:00

999 lines
36 KiB
TypeScript

import { eventIterator, oc } from "@orpc/contract";
import * as z from "zod";
import { AiConsentQuerySchema, AiConsentStatusSchema } from "./ai-consent.js";
import { ATTACHMENT_MAX_BASE64_LENGTH, ATTACHMENT_MAX_COUNT } from "./attachments.js";
import { AutopilotRecentSchema, AutopilotSchema, AutopilotStartInputSchema } from "./autopilot.js";
import { CardActSchema } from "./cards.js";
import {
CheckInSchema,
COACH_TITLE_MAX_LENGTH,
CoachRhythmSchema,
CoachTodaySchema,
CreateGoalInput,
CreateHabitInput,
GoalSchema,
HabitSchema,
ListCheckInsInput,
ListGoalsInput,
ListHabitsInput,
LogCheckInInput,
UpdateGoalInput,
UpdateHabitInput,
} from "./coach.js";
import {
ActionApprovalRuleSchema,
ActionAutoReviewSettingsSchema,
AgentSecretInputSchema,
AgentSecretSchema,
AgentSkillCatalogEntrySchema,
AgentSkillSchema,
AppBootstrapSchema,
ArtifactSchema,
ArtifactWithContentSchema,
AvatarStyleSchema,
BotMcpServerSchema,
BotSchema,
BotSectionSchema,
CapabilityInstallSchema,
ComputerModeSchema,
ComputerReleaseReasonSchema,
ComputerStatusSchema,
ComputerUpdateSchema,
ConnectionCatalogItemSchema,
ConnectionSchema,
CreateAgentSkillInput,
CreateBotInput,
CreateGroupInput,
CreateRoutineInput,
CreateScratchpadItemInput,
DelegatedTaskSchema,
DeploymentSettingsSchema,
ExportManifestSchema,
ExternalConversationPolicySchema,
GroupDetailSchema,
GroupSchema,
ImportBotInput,
IntegrationCatalogResultSchema,
MainConversationRefSchema,
McpServerConfigInput,
McpServerSchema,
MemoryDocumentSchema,
MemoryScopeSchema,
MeSchema,
MessagingAgentConnectionSchema,
MessagingChannelMembershipSchema,
MessagingLinkedIdentitySchema,
MessagingStatusSchema,
ModelCatalogEntrySchema,
ModelConnectInputSchema,
ModelCredentialSchema,
ModelOAuthBeginSchema,
REPLY_QUOTE_MAX_LENGTH,
ReorderBotsInput,
RoutineSchema,
ScratchpadItemSchema,
ScratchpadItemStatusSchema,
ServerUpdateCheckSchema,
ServerUpdateRequestSchema,
ServerUpdateRunSchema,
ServerUpdateStatusSchema,
SkillPlaybookSchema,
SpaceMemoryConfigSchema,
SpaceNavigationSchema,
SpaceSchema,
TaughtSkillSchema,
TeachRecordingEventSchema,
ThreadMessagePageSchema,
ThreadSnapshotSchema,
UpdateAgentSkillInput,
UpdateBotInput,
UpdateExternalConversationPolicyInput,
UpdateGroupInput,
UsageRecordSchema,
VoiceCatalogEntrySchema,
VoiceCredentialSchema,
VoiceInfoSchema,
VoiceStatusSchema,
} from "./domain.js";
import { DelegatedTaskStatusSchema, OnboardingStepSchema, ProductEventSchema } from "./events.js";
import { Id, IsoDate } from "./ids.js";
import { InboxListOutputSchema } from "./inbox.js";
import {
IntegrationProviderConfigSchema,
IntegrationSetupStateSchema,
} from "./integration-settings.js";
import {
MediaCapabilitiesSchema,
MediaJobSnapshotSchema,
MediaJobStartInputSchema,
MediaJobStartOutputSchema,
} from "./media-jobs.js";
import { MediaSettingsUpdateSchema, MediaSettingsViewSchema } from "./media-settings.js";
import { ModelTierMappingSchema, SetModelTierInput } from "./model-tiers.js";
import {
ExpoPushTokenSchema,
NotificationSettingsSchema,
UpdateNotificationSettingsInput,
WebPushConfigSchema,
WebPushEndpointSchema,
WebPushSubscriptionInput,
} from "./notifications.js";
import {
OnboardingScanApplyInputSchema,
OnboardingScanApplyResultSchema,
OnboardingScanStatusSchema,
OnboardingScanTargetSchema,
} from "./onboarding-scan.js";
import {
CreateProjectGoalInput,
ProjectGoalSchema,
UpdateProjectGoalInput,
} from "./project-goals.js";
import { ProjectSessionsOutputSchema } from "./project-sessions.js";
import { MessageReactionSchema } from "./reactions.js";
import { RunsListOutputSchema } from "./runs.js";
import { SearchQueryOutputSchema } from "./search.js";
import {
ForgetUserFactsInput,
UpdateUserProfileInput,
UserProfileStateSchema,
} from "./user-profile.js";
const botId = z.object({ botId: Id });
const groupId = z.object({ groupId: Id });
const threadTarget = z
.object({
botId: Id.optional(),
groupId: Id.optional(),
})
.superRefine((input, ctx) => {
const hasBot = Boolean(input.botId);
const hasGroup = Boolean(input.groupId);
if (hasBot === hasGroup) {
ctx.addIssue({
code: "custom",
message: "Provide exactly one of botId or groupId",
path: ["botId"],
});
}
});
const structuredMentionTarget = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("bot"), id: Id }),
z.object({ kind: z.literal("group"), id: Id }),
z.object({ kind: z.literal("routine"), id: Id }),
z.object({ kind: z.literal("connector"), id: Id }),
]);
const threadSendInput = threadTarget
.safeExtend({
text: z.string().optional(),
artifactIds: z.array(Id).max(ATTACHMENT_MAX_COUNT).optional(),
/** Bare bot ids (legacy) or typed mention chips from the composer. */
mentions: z
.array(z.union([Id, structuredMentionTarget]))
.max(64)
.optional(),
replyToMessageId: Id.optional(),
replyQuote: z.string().trim().min(1).max(REPLY_QUOTE_MAX_LENGTH).optional(),
clientNonce: z.string().min(1).max(200).optional(),
})
.superRefine((input, ctx) => {
const text = input.text?.trim() ?? "";
const artifactIds = input.artifactIds ?? [];
if (!text && artifactIds.length === 0) {
ctx.addIssue({
code: "custom",
message: "Provide text or at least one attachment",
path: ["text"],
});
}
if (input.replyQuote && !input.replyToMessageId) {
ctx.addIssue({
code: "custom",
message: "replyQuote requires replyToMessageId",
path: ["replyQuote"],
});
}
});
export const appContract = {
aiConsent: {
status: oc.input(AiConsentQuerySchema).output(AiConsentStatusSchema),
allow: oc
.input(
z.object({
scope: z.string(),
version: z.string(),
keys: z.array(z.string()).min(1).max(200),
}),
)
.output(AiConsentStatusSchema),
revoke: oc.input(z.object({ key: z.string().nullable() })).output(AiConsentStatusSchema),
},
health: oc.output(z.object({ ok: z.literal(true), version: z.string() })),
me: oc.output(MeSchema),
preferences: {
update: oc.input(z.object({ avatarStyle: AvatarStyleSchema })).output(MeSchema),
},
/** What the user's bots know about the user, across spaces. */
profile: {
get: oc.output(UserProfileStateSchema),
update: oc.input(UpdateUserProfileInput).output(UserProfileStateSchema),
forget: oc.input(ForgetUserFactsInput).output(UserProfileStateSchema),
},
/** The user's goals, habits, check-ins and check-in times; owner-only, across spaces. */
coach: {
today: oc.output(CoachTodaySchema),
goals: {
list: oc.input(ListGoalsInput).output(z.array(GoalSchema)),
create: oc.input(CreateGoalInput).output(GoalSchema),
update: oc.input(UpdateGoalInput).output(GoalSchema),
remove: oc.input(z.object({ goalId: Id })).output(z.object({ ok: z.literal(true) })),
},
habits: {
list: oc.input(ListHabitsInput).output(z.array(HabitSchema)),
create: oc.input(CreateHabitInput).output(HabitSchema),
update: oc.input(UpdateHabitInput).output(HabitSchema),
remove: oc.input(z.object({ habitId: Id })).output(z.object({ ok: z.literal(true) })),
},
checkIns: {
log: oc.input(LogCheckInInput).output(CheckInSchema),
list: oc.input(ListCheckInsInput).output(z.array(CheckInSchema)),
},
rhythm: {
get: oc.output(CoachRhythmSchema),
set: oc.input(CoachRhythmSchema).output(CoachRhythmSchema),
},
},
spaces: {
list: oc.output(SpaceNavigationSchema),
create: oc
.input(
z.object({
name: z.string().trim().min(1).max(60),
workspacePath: z.string().trim().min(1).max(500).optional(),
}),
)
.output(SpaceSchema),
remove: oc
.input(z.object({ spaceId: Id }))
.output(z.object({ ok: z.literal(true), activeSpaceId: Id })),
/** The space's main conversation, created with its main bot when the user first opens it. */
ensureMain: oc.input(z.object({ spaceId: Id })).output(MainConversationRefSchema),
},
bootstrap: oc.input(z.object({ botId: Id.optional() })).output(AppBootstrapSchema),
deployment: {
get: oc.output(DeploymentSettingsSchema),
update: oc
.input(
z.object({
signupsEnabled: z.boolean().optional(),
signupAllowlist: z.array(z.string()).optional(),
computerHost: z.enum(["docker", "this-mac"]).nullable().optional(),
}),
)
.output(DeploymentSettingsSchema),
},
/**
* Deployment-owner product updates. When the Compose updater sidecar is reachable, these proxy
* to its `/state` `/plan` `/apply` contract. Rollback stays on the sidecar for ops only and is
* not exposed here. Never git-fetch from the API process.
*/
updater: {
status: oc.output(ServerUpdateStatusSchema),
check: oc.input(ServerUpdateRequestSchema).output(ServerUpdateCheckSchema),
apply: oc.input(ServerUpdateRequestSchema).output(ServerUpdateRunSchema),
},
models: {
list: oc.output(z.array(ModelCatalogEntrySchema)),
credentials: oc.output(z.array(ModelCredentialSchema)),
connect: oc.input(ModelConnectInputSchema).output(ModelCredentialSchema),
probeOpenAiCompatible: oc
.input(
z.union([
z.object({
baseUrl: z.string(),
apiKey: z.string().optional(),
}),
// A connected server, probed with its stored address and key.
z.object({ credentialId: Id }),
]),
)
.output(z.object({ models: z.array(z.string()) })),
beginOAuth: oc
.input(
z.object({
provider: z.string(),
label: z.string().optional(),
modelId: z.string().optional(),
}),
)
.output(ModelOAuthBeginSchema),
submitOAuthCode: oc
.input(z.object({ loginId: z.string(), code: z.string().trim().min(1).max(8_192) }))
.output(z.object({ ok: z.literal(true) })),
completeOAuth: oc
.input(z.object({ loginId: z.string() }))
.output(
z.discriminatedUnion("status", [
z.object({ status: z.literal("pending") }),
z.object({ status: z.literal("ready") }),
z.object({ status: z.literal("error"), error: z.string() }),
]),
),
finishOAuth: oc.input(z.object({ loginId: z.string() })).output(ModelCredentialSchema),
cancelOAuth: oc
.input(z.object({ loginId: z.string() }))
.output(z.object({ ok: z.literal(true) })),
setDefault: oc
.input(z.object({ provider: z.string(), modelId: z.string() }))
.output(z.object({ ok: z.literal(true) })),
tiers: {
list: oc.output(z.array(ModelTierMappingSchema)),
set: oc.input(SetModelTierInput).output(z.array(ModelTierMappingSchema)),
},
},
bots: {
list: oc.output(z.array(BotSchema)),
listArchived: oc.output(z.array(BotSchema)),
get: oc.input(botId).output(BotSchema),
create: oc.input(CreateBotInput).output(BotSchema),
/** The user's main bot, created with its main conversation in the home space when missing. */
ensureMain: oc.output(BotSchema),
duplicate: oc.input(botId).output(BotSchema),
/** A new bot restored from an `export.bot` manifest. */
import: oc.input(ImportBotInput).output(BotSchema),
reorder: oc.input(ReorderBotsInput).output(z.object({ ok: z.literal(true) })),
update: oc.input(UpdateBotInput).output(BotSchema),
/** Builtin tool names this bot's allowlist can choose from. */
builtinTools: oc.input(botId).output(z.array(z.string())),
setComputer: oc.input(z.object({ botId: Id, mode: ComputerModeSchema })).output(BotSchema),
archive: oc.input(botId).output(z.object({ ok: z.literal(true) })),
restore: oc.input(botId).output(z.object({ ok: z.literal(true) })),
remove: oc
.input(z.object({ botId: Id, deleteMemories: z.boolean().default(false) }))
.output(z.object({ ok: z.literal(true) })),
rotateWebhookSecret: oc.input(botId).output(
z.object({
secret: z.string(),
path: z.string(),
webhookConfigured: z.literal(true),
}),
),
},
groups: {
create: oc.input(CreateGroupInput).output(GroupSchema),
list: oc.output(z.array(GroupSchema)),
listArchived: oc.output(z.array(GroupSchema)),
get: oc.input(groupId).output(GroupDetailSchema),
listSessions: oc.input(groupId).output(ProjectSessionsOutputSchema),
duplicate: oc.input(groupId).output(GroupSchema),
update: oc.input(UpdateGroupInput).output(GroupSchema),
archive: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
restore: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
remove: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
},
botSections: {
list: oc.output(z.array(BotSectionSchema)),
create: oc
.input(threadTarget.safeExtend({ name: z.string().trim().min(1).max(60) }))
.output(BotSectionSchema),
update: oc
.input(z.object({ sectionId: Id, name: z.string().trim().min(1).max(60) }))
.output(BotSectionSchema),
},
projectGoals: {
list: oc
.input(
z.object({
sectionId: Id,
groupId: Id.optional(),
seedDefaults: z.boolean().optional(),
}),
)
.output(z.array(ProjectGoalSchema)),
create: oc.input(CreateProjectGoalInput).output(ProjectGoalSchema),
update: oc.input(UpdateProjectGoalInput).output(ProjectGoalSchema),
},
threads: {
head: oc.input(threadTarget).output(
z.object({
threadId: Id,
cursor: z.number().int().min(-1),
}),
),
get: oc.input(threadTarget).output(ThreadSnapshotSchema),
messages: oc
.input(
threadTarget.safeExtend({
before: z.number().int().nonnegative().optional(),
includePeerRuns: z.boolean().optional(),
includePeerReceipts: z.boolean().optional(),
around: z
.object({
messageId: Id.optional(),
seq: z.number().int().nonnegative().optional(),
})
.optional(),
}),
)
.output(ThreadMessagePageSchema),
subscribe: oc
.input(threadTarget.safeExtend({ cursor: z.number().int().min(-1) }))
.output(eventIterator(ProductEventSchema)),
send: oc.input(threadSendInput).output(
z.object({
taskId: Id,
runId: Id,
seq: z.number().int(),
runIds: z.array(Id).optional(),
}),
),
react: oc
.input(
threadTarget.safeExtend({
messageId: Id,
reaction: MessageReactionSchema,
clientNonce: z.string().min(1).max(200),
}),
)
.output(z.object({ ok: z.literal(true) })),
stop: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
followUp: oc
.input(threadTarget.safeExtend({ text: z.string().min(1) }))
.output(z.object({ ok: z.literal(true) })),
clear: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
answer: oc
.input(
threadTarget.safeExtend({
runId: Id,
messageId: Id,
answer: z.string().min(1),
}),
)
.output(z.object({ ok: z.literal(true) })),
/** A tap on an interactive card: the backend applies it and may answer the bot. */
act: oc
.input(
threadTarget.safeExtend({
messageId: Id,
cardId: z.string().min(1).max(40),
act: CardActSchema,
clientNonce: z.string().min(1).max(200),
}),
)
.output(z.object({ ok: z.literal(true) })),
markRead: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
markUnread: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
},
computer: {
status: oc.input(botId).output(ComputerStatusSchema),
boot: oc.input(botId).output(ComputerStatusSchema),
stop: oc.input(botId).output(ComputerStatusSchema),
recover: oc.input(botId).output(ComputerUpdateSchema),
reset: oc.input(botId).output(ComputerStatusSchema),
update: oc.input(botId).output(ComputerUpdateSchema),
updates: oc.output(z.array(ComputerUpdateSchema)),
releaseInterrupted: oc
.input(z.object({ id: Id, workersStopped: z.literal(true) }))
.output(z.object({ ok: z.literal(true) })),
dismissUpdate: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
takeover: oc.input(botId).output(z.object({ leaseId: Id, expiresAt: z.string() })),
release: oc
.input(
z.object({
botId: Id,
reason: ComputerReleaseReasonSchema.optional(),
}),
)
.output(z.object({ ok: z.literal(true) })),
input: oc
.input(
z.object({
botId: Id,
kind: z.enum(["key", "pointer", "clipboard", "scroll"]),
payload: z.record(z.string(), z.unknown()),
}),
)
.output(z.object({ ok: z.literal(true) })),
files: oc
.input(z.object({ botId: Id, path: z.string().default("/") }))
.output(
z.array(z.object({ path: z.string(), kind: z.enum(["file", "dir"]), size: z.number() })),
),
readFile: oc
.input(z.object({ botId: Id, path: z.string() }))
.output(z.object({ path: z.string(), content: z.string() })),
screenUrl: oc.input(botId).output(z.object({ url: z.string().nullable() })),
heartbeat: oc.input(botId).output(z.object({ ok: z.literal(true) })),
},
memory: {
list: oc
.input(z.object({ botId: Id.optional(), scope: z.enum(["bot", "user"]).optional() }))
.output(z.array(MemoryDocumentSchema)),
update: oc
.input(z.object({ documentId: Id, content: z.string() }))
.output(MemoryDocumentSchema),
exportMarkdown: oc.input(z.object({ botId: Id.optional() })).output(z.string()),
providerConfig: oc.output(SpaceMemoryConfigSchema.nullable()),
connectProvider: oc
.input(
z.object({
provider: z.string().min(1),
settings: z.record(z.string(), z.string()),
credentials: z.record(z.string(), z.string()),
defaultMemoryScope: MemoryScopeSchema.default("isolated"),
}),
)
.output(SpaceMemoryConfigSchema),
setDefaultScope: oc
.input(z.object({ defaultMemoryScope: MemoryScopeSchema }))
.output(SpaceMemoryConfigSchema),
disconnectProvider: oc.output(z.object({ ok: z.literal(true) })),
},
routines: {
list: oc.input(botId).output(z.array(RoutineSchema)),
create: oc.input(CreateRoutineInput).output(RoutineSchema),
update: oc
.input(
z
.object({
routineId: Id,
name: z.string().optional(),
prompt: z.string().optional(),
crons: z.array(z.string().min(1)).optional(),
timezone: z.string().optional(),
active: z.boolean().optional(),
notify: z.boolean().optional(),
webhookEnabled: z.boolean().optional(),
githubEnabled: z.boolean().optional(),
messageProvider: z
.string()
.min(1)
.max(50)
.regex(/^[a-z0-9._-]+$/i)
.nullable()
.optional(),
/** ISO datetime to arm a never-run one-shot. */
runAt: IsoDate.optional(),
})
.superRefine((value, ctx) => {
if (
value.crons &&
value.crons.length === 0 &&
value.webhookEnabled === false &&
value.githubEnabled === false &&
value.messageProvider === null
) {
ctx.addIssue({
code: "custom",
message: "Add a schedule, webhook, GitHub, or message trigger",
path: ["crons"],
});
}
}),
)
.output(RoutineSchema),
remove: oc.input(z.object({ routineId: Id })).output(z.object({ ok: z.literal(true) })),
testRun: oc
.input(
z.object({
routineId: Id,
clientNonce: z.string().min(1).max(200).optional(),
}),
)
.output(z.object({ runId: Id })),
},
scratchpad: {
list: oc
.input(
z.object({
botId: Id,
status: ScratchpadItemStatusSchema.optional(),
includeDone: z.boolean().optional(),
}),
)
.output(z.array(ScratchpadItemSchema)),
create: oc.input(CreateScratchpadItemInput).output(ScratchpadItemSchema),
update: oc
.input(
z.object({
itemId: Id,
title: z.string().min(1).max(200).optional(),
status: ScratchpadItemStatusSchema.optional(),
notes: z.string().max(4_000).optional(),
}),
)
.output(ScratchpadItemSchema),
remove: oc.input(z.object({ itemId: Id })).output(z.object({ ok: z.literal(true) })),
},
skills: {
list: oc.input(botId).output(z.array(TaughtSkillSchema)),
get: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
start: oc
.input(z.object({ botId: Id, goal: z.string().min(1).max(4000) }))
.output(TaughtSkillSchema),
appendEvent: oc
.input(z.object({ skillId: Id, event: TeachRecordingEventSchema }))
.output(TaughtSkillSchema),
snapshot: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
stop: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
updateDraft: oc
.input(
z.object({
skillId: Id,
name: z.string().optional(),
playbook: SkillPlaybookSchema,
}),
)
.output(TaughtSkillSchema),
save: oc
.input(z.object({ skillId: Id, name: z.string().optional() }))
.output(TaughtSkillSchema),
testRun: oc
.input(z.object({ skillId: Id, prompt: z.string().optional() }))
.output(z.object({ runId: Id })),
remove: oc.input(z.object({ skillId: Id })).output(z.object({ ok: z.literal(true) })),
},
/** Claude Agent Skills (SKILL.md recipes) shared across assistants (not taught/demo skills). Pi already understands this format; we persist and inject them. */
agentSkills: {
list: oc.output(z.array(AgentSkillCatalogEntrySchema)),
get: oc
.input(
z
.object({ skillId: Id.optional(), name: z.string().min(1).max(80).optional() })
.superRefine((input, ctx) => {
if (!input.skillId && !input.name?.trim()) {
ctx.addIssue({
code: "custom",
message: "Provide skillId or name",
path: ["skillId"],
});
}
}),
)
.output(AgentSkillSchema),
create: oc.input(CreateAgentSkillInput).output(AgentSkillSchema),
update: oc.input(UpdateAgentSkillInput).output(AgentSkillSchema),
remove: oc.input(z.object({ skillId: Id })).output(z.object({ ok: z.literal(true) })),
},
capabilities: {
list: oc.output(z.array(CapabilityInstallSchema)),
catalogSearch: oc
.input(
z.object({
query: z.string().trim().max(253).default(""),
usePublicCatalog: z.boolean().default(false),
}),
)
.output(
z.object({
enabled: z.boolean(),
results: z.array(IntegrationCatalogResultSchema),
}),
),
install: oc
.input(
z.object({
kind: z.enum(["skill", "plugin", "mcp", "api", "graphql"]),
name: z.string().min(1).max(120),
source: z.string().min(1).max(2048),
config: z.record(z.string(), z.unknown()).default({}),
credential: z.string().max(16_384).optional(),
}),
)
.output(CapabilityInstallSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
mcp: {
servers: {
list: oc.output(z.array(McpServerSchema)),
create: oc.input(McpServerConfigInput).output(McpServerSchema),
update: oc
.input(
z.union([
z.object({ id: Id, config: McpServerConfigInput }),
z.object({ id: Id, secret: z.string().min(1).max(16384) }),
]),
)
.output(McpServerSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
assignments: {
list: oc.input(botId).output(z.array(BotMcpServerSchema)),
all: oc.output(z.array(BotMcpServerSchema)),
approve: oc.input(z.object({ botId: Id, serverId: Id })).output(BotMcpServerSchema),
replace: oc
.input(
z.object({
botId: Id,
assignments: z.array(
z.object({
serverId: Id,
allowAllTools: z.boolean().default(true),
allowedTools: z.array(z.string().min(1).max(200)).max(500).default([]),
}),
),
}),
)
.output(z.array(BotMcpServerSchema)),
},
oauth: {
begin: oc.input(z.object({ serverId: Id, redirectUri: z.string().url() })).output(
z.discriminatedUnion("status", [
z.object({
status: z.literal("authorization_required"),
sessionId: Id,
authorizationUrl: z.string().url(),
}),
z.object({
status: z.enum(["already_connected", "authorization_not_requested"]),
}),
]),
),
complete: oc
.input(z.object({ sessionId: Id, code: z.string().min(1), state: z.string().min(1) }))
.output(z.object({ ok: z.literal(true) })),
disconnect: oc.input(z.object({ serverId: Id })).output(z.object({ ok: z.literal(true) })),
},
},
onboarding: {
/** Checks that the caller may onboard this bot's thread. Posts nothing. */
start: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
/**
* Posts the first card: for the main bot the first onboarding question, once per user;
* for another bot its focus card while the thread is still idle.
*/
promptFocus: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
/**
* Answers the pending card and posts what follows it: the focus card's app suggestions,
* then the next onboarding question. A stale answer (another step is pending) is ignored.
*/
choose: oc
.input(
z.object({
botId: Id,
step: OnboardingStepSchema.optional(),
optionId: z.string(),
/** The answer in the user's words, with `optionId` "_text"; only the goal card takes one. */
text: z.string().trim().min(1).max(COACH_TITLE_MAX_LENGTH).optional(),
/** This device could still turn on push notifications. */
canEnablePush: z.boolean().optional(),
}),
)
.output(z.object({ ok: z.literal(true) })),
/** Dismisses the pending card; an onboarding question counts as skipped. */
dismissFocus: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
/** Flip an app_connect card to connected after authorization completes. */
appConnected: oc
.input(
z.object({ botId: Id, provider: z.string(), connectorId: z.string().default("composio") }),
)
.output(z.object({ ok: z.literal(true) })),
/** The folder a scan would read; null when this account cannot scan, so the step is skipped. */
scanTarget: oc.output(OnboardingScanTargetSchema),
/** Start scanning that folder. Only called after the user agreed. */
scan: oc.output(z.object({ scanId: z.string() })),
scanStatus: oc.input(z.object({ scanId: z.string() })).output(OnboardingScanStatusSchema),
/** Create a project space per chosen project and save the chosen facts. */
applyScan: oc.input(OnboardingScanApplyInputSchema).output(OnboardingScanApplyResultSchema),
},
integrationSetup: {
get: oc.output(IntegrationSetupStateSchema),
save: oc.input(IntegrationProviderConfigSchema).output(z.object({ ok: z.literal(true) })),
},
media: {
/** Deployment-wide media backends; deployment owner only. */
settings: {
get: oc.output(MediaSettingsViewSchema),
update: oc.input(MediaSettingsUpdateSchema).output(MediaSettingsViewSchema),
},
/** With a job id, what the job's space can run (a card's follow-ups). */
capabilities: oc
.input(z.object({ jobId: Id.optional() }).optional())
.output(MediaCapabilitiesSchema),
jobs: {
start: oc.input(MediaJobStartInputSchema).output(MediaJobStartOutputSchema),
get: oc.input(z.object({ jobId: Id })).output(MediaJobSnapshotSchema),
cancel: oc.input(z.object({ jobId: Id })).output(MediaJobSnapshotSchema),
},
},
connections: {
catalog: oc
.input(z.object({ query: z.string().optional(), connectorId: z.string().optional() }))
.output(z.array(ConnectionCatalogItemSchema)),
list: oc.output(z.array(ConnectionSchema)),
begin: oc
.input(
z.object({
connectorId: z.string().default("composio"),
provider: z.string(),
displayName: z.string(),
}),
)
.output(z.object({ connectionId: Id, authorizationUrl: z.string().nullable() })),
complete: oc
.input(z.object({ connectionId: Id, code: z.string().optional() }))
.output(ConnectionSchema),
rename: oc
.input(z.object({ connectionId: Id, displayName: z.string().trim().min(1).max(80) }))
.output(ConnectionSchema),
revoke: oc.input(z.object({ connectionId: Id })).output(z.object({ ok: z.literal(true) })),
/** Tools the connected provider exposes. Read-only; no per-tool allowlist yet. */
tools: oc.input(z.object({ connectorId: z.string(), provider: z.string() })).output(
z.array(
z.object({
name: z.string(),
description: z.string(),
}),
),
),
},
/** External messaging surface: link state, group channels, agent connections. */
messaging: {
status: oc.output(MessagingStatusSchema),
link: {
/** Issue a short-lived code the user sends to the line from a chat app. */
start: oc
.input(z.object({ botId: Id }))
.output(z.object({ code: z.string(), expiresAt: z.string() })),
},
identities: {
setBot: oc
.input(z.object({ identityId: Id, botId: Id }))
.output(MessagingLinkedIdentitySchema),
unlink: oc.input(z.object({ identityId: Id })).output(z.object({ ok: z.literal(true) })),
},
channels: {
list: oc.output(z.array(MessagingChannelMembershipSchema)),
// Addressed by membership, not channel: one user can have two linked
// chat apps in the same group, and each answers for its own agent.
respond: oc
.input(z.object({ membershipId: Id, accept: z.boolean() }))
.output(MessagingChannelMembershipSchema),
leave: oc.input(z.object({ membershipId: Id })).output(z.object({ ok: z.literal(true) })),
},
connections: {
list: oc.output(z.array(MessagingAgentConnectionSchema)),
respond: oc
.input(z.object({ connectionId: Id, accept: z.boolean() }))
.output(MessagingAgentConnectionSchema),
revoke: oc.input(z.object({ connectionId: Id })).output(z.object({ ok: z.literal(true) })),
},
},
approvalRules: {
list: oc.output(z.array(ActionApprovalRuleSchema)),
set: oc
.input(
z.object({
effect: z.enum(["always_allow", "require_approval"]),
matchKind: z.enum(["tool", "connector", "category"]),
matchValue: z.string().min(1),
}),
)
.output(ActionApprovalRuleSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
autoReview: {
get: oc.output(ActionAutoReviewSettingsSchema),
set: oc.input(z.object({ enabled: z.boolean() })).output(ActionAutoReviewSettingsSchema),
},
artifacts: {
list: oc.input(botId).output(z.array(ArtifactSchema)),
create: oc
.input(
threadTarget.and(
z.object({
name: z.string().min(1).max(255),
mimeType: z.string().min(1),
contentBase64: z.string().min(1).max(ATTACHMENT_MAX_BASE64_LENGTH),
}),
),
)
.output(ArtifactSchema),
get: oc.input(threadTarget.and(z.object({ artifactId: Id }))).output(ArtifactWithContentSchema),
},
usage: {
list: oc.output(z.array(UsageRecordSchema)),
summary: oc.output(
z.object({
inputTokens: z.number(),
outputTokens: z.number(),
runs: z.number(),
}),
),
},
export: {
bot: oc.input(botId).output(ExportManifestSchema),
},
notifications: {
registerPush: oc
.input(z.object({ token: ExpoPushTokenSchema }))
.output(z.object({ ok: z.literal(true) })),
unregisterPush: oc.output(z.object({ ok: z.literal(true) })),
settings: {
get: oc.output(NotificationSettingsSchema),
update: oc.input(UpdateNotificationSettingsInput).output(NotificationSettingsSchema),
},
webPush: {
config: oc.output(WebPushConfigSchema),
subscribe: oc.input(WebPushSubscriptionInput).output(z.object({ ok: z.literal(true) })),
unsubscribe: oc
.input(z.object({ endpoint: WebPushEndpointSchema }))
.output(z.object({ ok: z.literal(true) })),
},
},
search: {
query: oc.input(z.object({ q: z.string().max(200) })).output(SearchQueryOutputSchema),
},
runs: {
list: oc.input(z.object({ filter: z.enum(["active", "recent"]) })).output(RunsListOutputSchema),
},
tasks: {
list: oc
.input(z.object({ threadId: Id.optional(), status: DelegatedTaskStatusSchema.optional() }))
.output(z.array(DelegatedTaskSchema)),
},
/** Asks, approvals, takeovers and unread threads outside the main conversation, in all spaces. */
inbox: {
list: oc.output(InboxListOutputSchema),
},
/** The thread's open autopilot: start one, stop it, or let a paused one go on. */
autopilot: {
get: oc.input(threadTarget).output(AutopilotSchema.nullable()),
start: oc
.input(threadTarget.safeExtend(AutopilotStartInputSchema.shape))
.output(AutopilotSchema),
stop: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
resume: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
/** The user's autopilots in this space that run, wait, or ended within the last day. */
recent: oc.output(z.object({ items: z.array(AutopilotRecentSchema) })),
},
voice: {
catalog: oc.output(z.array(VoiceCatalogEntrySchema)),
status: oc.output(VoiceStatusSchema),
credentials: oc.output(z.array(VoiceCredentialSchema)),
connect: oc
.input(
z.object({
provider: z.string(),
apiKey: z.string().min(8),
voiceId: z.string().max(120).optional(),
}),
)
.output(VoiceCredentialSchema),
disconnect: oc
.input(z.object({ provider: z.string().min(1) }))
.output(z.object({ ok: z.literal(true) })),
setVoice: oc
.input(z.object({ voiceId: z.string().min(1).max(120), provider: z.string().optional() }))
.output(VoiceStatusSchema),
voices: oc
.input(z.object({ provider: z.string().optional() }))
.output(z.array(VoiceInfoSchema)),
prepare: oc
.input(
z.object({
text: z.string().max(20000),
voiceId: z.string().max(120).optional(),
botId: Id.optional(),
}),
)
.output(z.object({ ready: z.boolean(), utterances: z.array(z.string()) })),
},
externalConversations: {
updatePolicy: oc
.input(UpdateExternalConversationPolicyInput)
.output(ExternalConversationPolicySchema),
},
agentSecrets: {
list: oc.output(z.array(AgentSecretSchema)),
put: oc.input(AgentSecretInputSchema).output(AgentSecretSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
};
export type AppContract = typeof appContract;