import { z } from "zod"; import { AgentHarnessSchema, AgentProviderSchema } from "../schemas/agent.js"; import { CredentialGateKindSchema, CredentialGateScopeSchema, CredentialLaneSchema } from "../schemas/secrets.js"; // Requests a turn raises to ask the person something mid-turn; each pauses the turn until `POST /agent/reply` answers it. // Shapes shared by the daemon that raises them, the browser that draws them, and the transcript that records them // (transcript.ts). // One interactive question the agent asks via the `ask` tool (mirrors AskUserQuestion's input shape). export const AskOptionSchema = z.object({ label: z.string().describe("The choice, in a few words."), description: z.string().describe("What picking it means."), preview: z.string().optional().describe("Something to look at while deciding: a mock-up, a snippet, a layout."), }); export type AskOption = z.infer; export const AskQuestionSchema = z.object({ question: z.string().describe("What the agent is asking."), header: z.string().describe("A short label for the question."), multiSelect: z.boolean().describe("Whether more than one answer can be picked."), options: z.array(AskOptionSchema).describe("The choices offered. A free-text answer is always possible as well."), }); export type AskQuestion = z.infer; // The command a request is holding, as data rather than prose. `spans` is carried from the classifier's own match (never // re-derived), offset into `text` after truncation. export const ProgramAskSchema = z.object({ text: z.string().describe("What would run."), language: z.enum(["bash", "javascript"]).describe("Which of the two backends it is written for, named as the grammar that colours it."), truncated: z .boolean() .describe( "Whether this is an excerpt of a longer program, so the request can say so instead of ending mid-word. An excerpt always carries the flagged fragment: the beginning, then a window around the fragment, with any skipped middle written into the text as a bracketed count.", ), spans: z .array(z.object({ start: z.number().int().nonnegative(), end: z.number().int().nonnegative() })) .describe( "Which fragments of the text the pattern match fired on: every matched class's, or, under the hard rule, only the class the title names. Offsets into text, in order, never overlapping.", ), }); export type ProgramAsk = z.infer; // What a child agent runs on: who serves it and how. The child-agent gate's request shows it, and the owner's allow may // carry a replacement (the permission reply's `child`, plan-limits.ts). export const ChildRunSchema = z.object({ provider: AgentProviderSchema.describe("Which provider serves it."), model: z.string().min(1).describe("Which of that provider's models."), harness: AgentHarnessSchema.optional().describe("Which agentic loop runs it. Absent is the provider's own."), account: z .string() .optional() .describe("Which of that provider's connected accounts pays for it. Absent is whichever has the most room when it starts."), effort: z.string().optional().describe("How hard it thinks, where the model offers a choice. Absent is the model's own default."), thinking: z.boolean().optional().describe("Whether it reasons before it answers, where the model offers the choice."), fast: z.boolean().optional().describe("Whether it asks for the faster rate, at the higher price."), }); export type ChildRun = z.infer; // A move a parent makes on a child agent that the owner's rules may hold for a yes. export const ChildMoveSchema = z.enum(["spawn", "send", "answer"]); export type ChildMove = z.infer; // The child a held request is about, as the daemon's own record of the call has it, never as the agent describes it. export const ChildAgentAskSchema = ChildRunSchema.extend({ move: ChildMoveSchema.describe("What the parent asks to do: start a new child, say something to one it started, or answer one's question."), child: z.string().optional().describe("The child's id, for one that already exists."), task: z.string().optional().describe("What the child is for, in a line."), message: z .string() .optional() .describe( "What the parent would say to it: the prompt a described start opens on, the message it sends, or the answers it gives. Clipped for the request, with an ellipsis where it was cut.", ), on: z.string().optional().describe('Which machine it runs on, when one is named: a runner, or "here" for this sandbox.'), proposed: ChildRunSchema.optional().describe( "What the agent asked to start it on, when the owner started it on something else instead. Present only once the request has settled that way.", ), }); export type ChildAgentAsk = z.infer; // One per-tool permission prompt (SDK's canUseTool, surfaced as a request); the daemon passes the bridge's own rendered // strings through unchanged. `alwaysLabel` is present only when the SDK offered a rule to persist. export const PermissionAskSchema = z.object({ toolName: z.string().describe("Which tool it wants to use."), // The whole prompt sentence, exactly as the runtime words it, when the bridge rendered one. title: z.string().optional().describe("The whole question, as a sentence, exactly as the runtime words it."), // Short noun phrase for the allow button ("Read file"). displayName: z.string().optional().describe("A short phrase for the button, such as read file."), description: z.string().optional().describe("More about what it is asking for."), // Why the prompt fired ('rule' | 'mode' | 'classifier' | …), shown as the request's muted subline. reason: z.string().optional().describe("Why it is asking at all: a rule, the current mode, something that looked risky."), // The file the request is about, when it is about one, workspace-root-relative. path: z.string().optional().describe("Which file it concerns, when it concerns one."), alwaysLabel: z .string() .optional() .describe( "The wording for an always-allow answer. Present only when there is something an always could actually remember; without it the only answers are once and no.", ), alwaysAsks: z .literal(true) .optional() .describe( "This request asks every time: an owner's hard rule, a sandbox restart other conversations would feel, or a change only the owner may make. Allow everything in this conversation is not offered on it and never answers it.", ), program: ProgramAskSchema.optional().describe( "The program this request is holding, when the request is about one. Present on a command gate's request and absent on every other permission ask.", ), child: ChildAgentAskSchema.optional().describe( "The subagent this request would start or reach, and what it runs on. Present on the request to start or reach a subagent and absent on every other permission ask.", ), // The judge's own sentence, present only when the title can't say it; never written by the agent being gated. explain: z .string() .optional() .describe( "One plain sentence saying what the program does and why it is being asked about, where the title says something else. Written by the judge that read your safety policy, never by the agent being gated.", ), }); export type PermissionAsk = z.infer; // One missing capability asked for; `request`/`name` are resolved by the daemon from the catalog it validates against. The // model contributes only `why`, so the request can't misrepresent what's being connected. export const CapabilityOfferSchema = z.object({ // The catalog request being asked for, and how the catalog itself titles it ("Notion", "GitHub", "Docker"). entry: z.string().describe("Which catalog entry is being asked for."), name: z.string().describe("What it is called, as the catalogue titles it rather than as the agent named it."), // The agent's one-line case for connecting it, the only prose on the request that is the model's. why: z.string().optional().describe("The agent's case for connecting it, and the only words on this request that are the agent's."), }); export type CapabilityOffer = z.infer; // One outbound USDC payment offered; every number is the daemon's own arithmetic over the endpoint's challenge and the // wallet's ledger. The model contributes only `why`, so the price can't be misquoted. export const PaymentOfferSchema = z.object({ // The paid resource, as the endpoint's challenge stated it. url: z.string().describe("What is being paid for."), description: z.string().optional().describe("What the endpoint says it is."), // Where the money goes, verbatim off the challenge: recipient address, CAIP-2 network, token contract. payTo: z.string().describe("Where the money goes, taken verbatim from the endpoint's own demand."), network: z.string().describe("On which network."), asset: z.string().describe("In which token."), // The token's display name ("USDC"), dollar-pegged, so every amount below reads as USD. assetName: z.string().describe("That token's name. It is pegged to the dollar, which is what lets every amount here read as dollars."), // The exact price; the x402 exact scheme has no ranges, so this is the whole spend, not a ceiling. amountUsd: z.string().describe("The exact price. Not a ceiling: this scheme has no ranges, so this is the whole spend."), // The wallet's meter as the daemon's ledger states it, what "spent today / cap" renders from. spentTodayUsd: z.string().describe("What has already gone out today."), dailyCapUsd: z.string().describe("What may go out in a day."), // The agent's one-line case for paying, the only prose on the request that is the model's. why: z.string().optional().describe("The agent's case for paying, and the only words on this request that are the agent's."), }); export type PaymentOffer = z.infer; // One gated credential asked for; every field but `why` is the daemon's own, off the gate policy and the exit that // would spend it. `approvers` are shown since the request addresses a named list, not the owner. export const CredentialOfferSchema = z.object({ // The gate's subject: a secret's reference name (`DATABASE_URL`) or a capability id (`reddit`). subject: z.string().describe("Which credential is being asked for."), kind: CredentialGateKindSchema, lane: CredentialLaneSchema, // Where it would go, in the reader's terms; reference-form, never a value, so it can be shown safely. detail: z .string() .optional() .describe("Where it would go: the start of the command, the site, or what is being mounted. Never a value: the command still reads as a reference at this point."), // The agent's one-line case, the only prose on the request that is the model's. why: z.string().optional().describe("The agent's case for using it, and the only words on this request that are the agent's."), approvers: z.array(z.string()).describe("Who may release it. A click from anyone else is refused and leaves the request standing."), scope: CredentialGateScopeSchema, }); export type CredentialOffer = z.infer; // One provider-advertised slash command (ACP's available_commands, or a Claude Code session's supportedCommands). // `hint` is the argument placeholder the popover shows after the name. export const AgentCommandSchema = z.object({ name: z.string().describe("What to type, without the leading slash."), description: z.string().describe("What it does."), hint: z.string().optional().describe("What its argument should look like, shown after the name."), }); export type AgentCommand = z.infer; // GET /agent/commands: which provider's last-published list to read; absent means claude, matching AgentTurn. export const AgentCommandsQuerySchema = z.object({ agent: AgentProviderSchema.optional().describe("Whose commands to read. Leave it out for Claude."), }); export const AgentCommandsSchema = z.object({ commands: z.array(AgentCommandSchema).describe("The shortcut commands, as the provider last published them."), }); // One TodoWrite/Task checklist item, surfaced live so the UI shows the agent's plan-of-work. export const TodoItemSchema = z.object({ content: z.string().describe("The item, as the agent wrote it."), status: z.enum(["pending", "in_progress", "completed"]).describe("Where it is."), activeForm: z .string() .optional() .describe("How to phrase it while it is happening, so a screen can say what the agent is doing rather than what it plans to do."), }); export type TodoItem = z.infer; // Context-window fill for a conversation: tokens the latest request sent vs. the model's window, so the UI can warn // near auto-compaction. Per-conversation, unlike the account-wide usage. export const ContextUsageSchema = z.object({ tokens: z.number().describe("How much the latest request sent, all told."), contextWindow: z.number().describe("How much the model can hold. The gap between these two is how close the conversation is to being compacted."), // Both absent unless the turn's last request actually used the prompt cache; a provider whose TTL we cannot ground // reports neither rather than a guessed pair. cachedAt: z .number() .optional() .describe( "When that request last touched the provider's prompt cache, in milliseconds. The cache's clock runs from here, since a read refreshes it as a write does.", ), cacheTtlMs: z.number().optional().describe("How long that cache entry lives from `cachedAt`, in milliseconds."), }); export type ContextUsage = z.infer; // ACP-aligned tool taxonomy (Agent Client Protocol's ToolKind, verbatim): what a tool call does, driving the request icon // regardless of which backend named the tool. export const ToolKindSchema = z.enum(["read", "edit", "delete", "move", "search", "execute", "think", "fetch", "other"]); export type ToolKind = z.infer; export const ToolCallStatusSchema = z.enum(["pending", "in_progress", "completed", "failed"]); export type ToolCallStatus = z.infer; // A file a tool call touches. Workspace-root-relative, forward-slash; adapters normalize from the turn's cwd. `line` is // 1-based. export const ToolCallLocationSchema = z.object({ path: z.string().describe("The file, as a workspace path, whatever directory the tool was run from."), line: z.number().optional().describe("Which line, counting from one."), }); export type ToolCallLocation = z.infer; // Structured tool output (ACP's ToolCallContent, verbatim); `diff` is hunk-level for edits, whole-file for Write. // `image` is a workspace path, not bytes, so it stays openable without bloating the stream. export const ToolCallContentSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("text").describe("Plain output."), text: z.string().describe("What the tool said."), }), z.object({ type: z.literal("diff").describe("A change to a file."), path: z.string().describe("Which file, as a workspace path."), oldText: z.string().optional().describe("What was there. Absent for a new file, or where the previous contents are not known."), newText: z.string().describe("What is there now."), truncated: z.boolean().optional().describe("One of the two sides was too large to send whole."), }), z.object({ type: z.literal("image").describe("A picture the tool produced."), path: z .string() .describe( "Where it is, as a workspace path. A path rather than the bytes, because the workspace already serves it, sending it inline would bloat every stored record, and this way the picture stays openable afterwards.", ), }), ]); export type ToolCallContent = z.infer; // The document a parked request is asking about, carried by value: the bytes are already in hand, and a restored // transcript has no workspace to fetch a path from. `path` rides along for an overflow. export const RequestDocumentSchema = z.object({ path: z.string().describe("Where it lives, as a workspace path."), title: z.string().describe("What it is called: its opening heading, or its file name."), markdown: z.string().describe("The document itself."), truncated: z.boolean().optional().describe("It was clipped at the wire cap; the file on disk has more."), plan: z.boolean().optional().describe("It is one of the CLI's plan files, written to be approved rather than merely read."), }); export type RequestDocument = z.infer; // A page the agent showed in the chat (its `show_page`/`ask_page` tools): a self-contained HTML document the daemon // stored under the workspace's records, drawn inline in a sealed frame. The row carries where it lives, not its bytes, // so a long chat full of charts stays a light record; the file is the page as it was shown, never rewritten after. export const PAGE_MIN_HEIGHT = 80; export const PAGE_MAX_HEIGHT = 2000; export const PAGE_TITLE_MAX = 200; export const PageSchema = z.object({ id: z.string().min(1).describe("The page's own id, which a later `replaces` names to redraw it in place."), title: z.string().max(PAGE_TITLE_MAX).describe("What it is, in a few words."), path: z.string().min(1).describe("Where the page is stored, as a workspace path."), height: z .number() .int() .min(PAGE_MIN_HEIGHT) .max(PAGE_MAX_HEIGHT) .optional() .describe("The tallest the agent wants its frame, in pixels; the page scrolls inside it beyond that. Absent, the frame fits the page."), measured: z .number() .int() .positive() .optional() .describe("The page's height at the chat's column width, as the sandbox's own browser laid it out, so its room is held before it loads."), source: z.string().optional().describe("The workspace file the page was built from, when the agent showed a file rather than writing the page inline."), revision: z.number().int().nonnegative().optional().describe("How many times the agent redrew this page; each redraw is a new file at a new path."), superseded: z .boolean() .optional() .describe("A later redraw of this page stands further down the conversation, so this one is drawn folded to a line."), app: z .object({ server: z.string().min(1).describe("The connected MCP server whose app it is, by its capability id."), tool: z.string().min(1).describe("The tool whose call the app shows."), }) .optional() .describe("Present when the page is an MCP server's own app (an MCP Apps `ui://` resource), shown for one of its tool calls."), }); export type Page = z.infer; // What a page sent back is capped at: an answer, not a document. export const PAGE_VALUE_MAX = 64_000; // One request's fields, spelled once: the raising frame, the parked-card journal entry, and the transcript row all share // this shape rather than declaring it three times. const REQUEST_ID = z.string().describe("What to send back when you answer."); export const planRequest = { requestId: REQUEST_ID, text: z.string().describe("The plan itself."), // Present when the plan prose points at a document; absent when the text already is the whole plan. document: RequestDocumentSchema.optional().describe("The write-up this plan refers to, when the plan itself is a pointer to one."), }; export const questionRequest = { requestId: REQUEST_ID, questions: z.array(AskQuestionSchema).describe("What it wants to know."), document: RequestDocumentSchema.optional().describe("The document this turn wrote and is asking about, so the choice can be read beside it."), }; export const permissionRequest = { requestId: REQUEST_ID }; // The agent's browser is parked on something it can't clear itself (captcha, password, phone check). `session` names it // on /browsers; `account` says whose login is stuck. export const browserHelpRequest = { requestId: z.string(), session: z.string(), account: z.string(), message: z.string(), }; // The agent's terminal is sitting at a prompt it can't answer (OTP, security-key touch, confirm). `session` names it on // the terminal panel, same division as the browser request. export const terminalHelpRequest = { requestId: z.string(), session: z.string(), message: z.string(), }; // A page the agent showed to be answered (`ask_page`): the turn waits until the page sends its answer back. export const pageAskRequest = { requestId: REQUEST_ID, page: PageSchema.describe("The page to answer on."), }; export const capabilityOfferRequest = { requestId: z.string(), offer: CapabilityOfferSchema }; export const paymentOfferRequest = { requestId: z.string(), offer: PaymentOfferSchema }; export const credentialOfferRequest = { requestId: z.string(), offer: CredentialOfferSchema }; // How an accepted capability offer ended, shared by the frame that reports it and the record that keeps it, so a // reopened receipt matches the live request. export const CapabilityOutcomeSchema = z.object({ outcome: z.enum(["connected", "unfinished"]), id: z.string().optional(), }); export type CapabilityOutcome = z.infer; export const PaymentReceiptSchema = z.object({ outcome: z.enum(["paid", "failed"]), amountUsd: z.string(), transaction: z.string().optional(), network: z.string().optional(), }); export type PaymentReceipt = z.infer; // Who released a gated credential, or that someone refused it. `released` carries the approver's verified identity, not // what the click claimed. export const CredentialReceiptSchema = z.object({ outcome: z.enum(["released", "refused"]), approvedBy: z.string().optional(), }); export type CredentialReceipt = z.infer; // The requests a turn journal can restore verbatim after a daemon restart, raised by the turn itself or by a gate it // called. `browser_help`/`terminal_help` are excluded: their Chromium/waiting command dies with the container, so those // can only be reported, not restored. A gate's offer comes back as a card whose answer is told to a turn of its own, // since the call that raised it died with the daemon: a yes is the agent's to act on by asking again. export const PlanRequestSchema = z.object({ kind: z.literal("plan").describe("The agent has written a plan and is waiting for a yes."), ...planRequest, }); export const QuestionRequestSchema = z.object({ kind: z.literal("question").describe("The agent has asked you something and is waiting."), ...questionRequest, }); export const PermissionRequestSchema = PermissionAskSchema.extend({ kind: z.literal("permission").describe("The agent wants to use a tool it needs permission for."), ...permissionRequest, }); export const PageAskRequestSchema = z.object({ kind: z.literal("page_ask").describe("The agent has shown a page to be answered on and is waiting."), ...pageAskRequest, }); export const CapabilityOfferRequestSchema = z.object({ kind: z.literal("capability_offer").describe("The agent asks for something to be connected."), ...capabilityOfferRequest, }); export const PaymentOfferRequestSchema = z.object({ kind: z.literal("payment_offer").describe("The agent asks to pay for something."), ...paymentOfferRequest, }); export const CredentialOfferRequestSchema = z.object({ kind: z.literal("credential_offer").describe("The agent asks a named person to release a credential."), ...credentialOfferRequest, }); export const ParkedRequestSchema = z.discriminatedUnion("kind", [ PlanRequestSchema, QuestionRequestSchema, PermissionRequestSchema, PageAskRequestSchema, CapabilityOfferRequestSchema, PaymentOfferRequestSchema, CredentialOfferRequestSchema, ]); // Whether a frame is a card the journal keeps for a restart to put back; a record, so a kind added above fails to compile // until it is named here. const RESTORABLE = { plan: true, question: true, permission: true, page_ask: true, capability_offer: true, payment_offer: true, credential_offer: true, } as const satisfies Record; export const isRestorableRequest = (event: { readonly kind: string }): event is ParkedRequest => Object.hasOwn(RESTORABLE, event.kind); export type ParkedRequest = z.infer;