The main agent had no way to give a specialist a piece of work and keep
track of it. message_bot is a fire-and-forget peer message whose work stays
hidden from the user, and handoff_to_bot ends the main bot's turn and only
works between members of one group. Delegation now runs the specialist's
work where the user follows the main agent and brings the result back so
the main agent can close the task.
- delegate_task({bot, title, brief, deadline?}) is offered to the main bot
only and refused unless the caller leads the main conversation. It
resolves the specialist by id or name across the user's spaces, creates a
DelegatedTask and posts a task card in the main conversation.
- A specialist from the main conversation's space joins it and works there
in a run with the new trigger "delegation", which the peer filters do not
hide, so its progress, asks and approvals appear in the main conversation
with the specialist as sender. A specialist from another space works in
its own chat; its written result is posted into the main conversation as
its message.
- The executor mirrors the run on its task (running, waiting, done, failed,
cancelled) after the run starts and when each attempt ends, and updates
the card in place through thread.message.updated. Done and failed tasks
wake the main bot in the main conversation with the result. The job
reconciler settles tasks whose run ended without the executor (stop,
clear, archive, deletion, crash).
- tasks.list({threadId?, status?}) returns the caller's tasks for the
current space.
- The task card shows who, what and status with Lingui labels; the brief
opens on demand. The message_bot card no longer shows the recipient's
unrelated overall status or hard-coded German copy.
- The scripted runtime understands "delegate to the bot named X: ..." and
the task wake prompt; activity rows name delegation runs by task title.
Tests: unit tests for the prompts, status mapping, card, reducer, tool
selection and scripted runtime; a Postgres suite for same-space, group,
other-space, ask and answer, failure, stop and replay; tasks.list in the
authorization suite; and an end-to-end spec for the whole flow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
838 lines
29 KiB
TypeScript
838 lines
29 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 {
|
|
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,
|
|
IntegrationCatalogResultSchema,
|
|
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, ProductEventSchema } from "./events.js";
|
|
import { Id, IsoDate } from "./ids.js";
|
|
import {
|
|
IntegrationProviderConfigSchema,
|
|
IntegrationSetupStateSchema,
|
|
} from "./integration-settings.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";
|
|
|
|
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),
|
|
},
|
|
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 })),
|
|
},
|
|
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.object({
|
|
baseUrl: z.string(),
|
|
apiKey: z.string().optional(),
|
|
}),
|
|
)
|
|
.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) })),
|
|
},
|
|
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),
|
|
duplicate: oc.input(botId).output(BotSchema),
|
|
reorder: oc.input(ReorderBotsInput).output(z.object({ ok: z.literal(true) })),
|
|
update: oc.input(UpdateBotInput).output(BotSchema),
|
|
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) })),
|
|
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: {
|
|
/** Seed the first-run greeting into the bot's thread (focus card is separate). */
|
|
start: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
/** Post the focus choice card when the thread is still idle. */
|
|
promptFocus: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
/** Answer the focus choice; posts the app cards. Does not rename the bot. */
|
|
choose: oc
|
|
.input(z.object({ botId: Id, optionId: z.string() }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
/** Dismiss the unanswered focus card without choosing an option. */
|
|
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) })),
|
|
scan: oc.input(z.object({ botId: Id.optional() })).output(z.object({ scanId: z.string() })),
|
|
scanStatus: oc.input(z.object({ scanId: z.string() })).output(
|
|
z.object({
|
|
status: z.enum(["scanning", "connecting", "building", "briefing", "complete", "failed"]),
|
|
progress: z.number(),
|
|
message: z.string(),
|
|
result: z.any().optional(),
|
|
}),
|
|
),
|
|
},
|
|
integrationSetup: {
|
|
get: oc.output(IntegrationSetupStateSchema),
|
|
save: oc.input(IntegrationProviderConfigSchema).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
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: z.string().min(8).max(512) }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
unregisterPush: oc.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)),
|
|
},
|
|
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;
|