import { z } from "zod"; // Every live surface the sandbox's one global panel can show: tmux sessions, plus the agent's browser (not a terminal, // but the same question, what is running and can I look at it). // shell: a session the user opened. // panel: a repo's dev-server session, started via Start; running:false means untracked. // agent: the agent's Bash commands; running:false once every pane is a finished command's dead one. // job: work the sandbox runs to completion rather than keeps, so it stops being a tab once it ends: the terminal // runner's flows (capability add, infra check) and the process manager's one-shot runs (install, a project's checks, // a scaffold), whose lingering shell is what the `panel` kind above would otherwise read as an idle dev server. // process: a managed background process, shown read-only, never as a killable tab. export const TerminalSessionSchema = z.object({ name: z.string().describe("Its id, and what the close route takes."), label: z.string().optional().describe("What to call it on screen."), kind: z .enum(["shell", "panel", "agent", "job", "process"]) .describe( "What sort of thing it is: a terminal somebody opened, a repository's dev server, where an agent's commands run, a job the sandbox started, or a background process that is watched rather than typed into.", ), running: z .boolean() .describe("Whether it is alive. A finished one-shot job leaves a dead shell behind, which reads as false and is how it gets swept up."), activityAt: z.number().describe("When it last produced output, in milliseconds. Zero means it did not say, which is unknown rather than 1970."), exitCode: z.number().optional().describe("How the last thing in it ended. Absent while that pane is still alive."), // pane_current_command; absent at a prompt. Distinct from running, which only says the session is alive. command: z .string() .optional() .describe( "What is running in it right now. Absent when it is sitting at a prompt. Not a second spelling of whether it is alive: this says whether anything is happening, which is what a close button should ask about before it ends something.", ), extensionId: z.string().optional().describe("Which extension declared this process, when one did."), processName: z .string() .optional() .describe("Which of that extension's processes it is, which together with the id above addresses its start and stop routes."), // Agent waiting on a person (OTP, key touch, confirm); settled via POST /agent/reply. Present only while open. help: z .object({ requestId: z.string().describe("What to send back when you answer, through the agent reply route."), message: z.string().describe("What the agent needs, in its own words."), requestedAt: z.number().describe("When it asked, in milliseconds."), }) .optional() .describe("The agent has stopped at something only a person can clear, and is waiting at this terminal. Present only while it is waiting."), }); export const TerminalsListSchema = z.object({ sessions: z .array(TerminalSessionSchema) .describe("Every live surface the sandbox is holding, in one list, because the question they all answer is the same one."), }); export type TerminalsList = z.infer; export const TerminalNameParamSchema = z.object({ name: z.string().describe("Which terminal.") }); // Pane history as plain text: the browser cannot read tmux's alternate-screen scrollback any other way. `lines` bounds // how far back to ask; `truncated` marks a request-bound stop, not history's end. export const TerminalScrollbackQuerySchema = z.object({ name: z.string().describe("Which terminal."), lines: z.coerce.number().min(1).max(100_000).default(20_000).describe("How far back to ask for. Clamped to the history that actually exists."), }); export const TerminalScrollbackSchema = z.object({ name: z.string().describe("Which terminal this is from."), // Oldest line first, wrapped lines rejoined so a copied URL or path comes back whole. text: z.string().describe("The history, oldest line first, with wrapped lines rejoined so a copied address or path comes back whole."), lines: z.number().describe("How many lines you got."), truncated: z.boolean().describe("It stopped because you asked for that many, not because the history ran out."), }); export type TerminalScrollback = z.infer; // The Chromium the agent drives via @playwright/mcp, listed apart from terminals since it holds several pages at once, // not one stream. `active` is the page the agent last touched. export const BrowserPageSchema = z.object({ id: z .string() .describe( "Stable for the life of the page, which is what lets a tab survive a refresh of this list. Its address changes as the agent navigates and its position changes when a sibling closes.", ), // The page's title; absent mid-navigation, when a tab still needs to render. title: z.string().optional().describe("The page's title. Absent mid-navigation, which is exactly when a tab still has to be drawn."), url: z.string().describe("Where it is."), // The page the agent last drove; for a finished session, the one it ended on. Exactly one page has it. active: z.boolean().describe("The one the agent last touched, or for a finished session, the one it ended on. Exactly one page has this."), }); export const BrowserSessionSchema = z.object({ name: z.string().describe("Its id, and what the close route takes."), // The pill's text: the active page's title, else its host, else which browser this is. label: z.string().describe("What to call it on screen: the open page's title, or its site, or which browser this is."), // Which MCP server drives it: the credential-free browser, or a signed-in capability's id. server: z .string() .describe( "Which browser drives it: the credential-free one, or a signed-in account's. The difference between a throwaway page and one logged in as you, which is worth saying out loud.", ), // False once the Chromium is gone; a finished session still lists briefly with the pages it had. running: z .boolean() .describe("Whether it is still open. A closed one is listed for a while with the pages it had, as the record of where the agent went."), activityAt: z.number().describe("When it last did anything, in milliseconds."), // When the Chromium closed; absent while running, for the "closed 20m ago" line. finishedAt: z.number().optional().describe("When it closed, in milliseconds. Absent while it is open."), // Agent stuck on a captcha, password or phone check; settled via POST /agent/reply. Present only while open. help: z .object({ requestId: z.string().describe("What to send back when you answer, through the agent reply route."), message: z.string().describe("What the agent needs, in its own words."), requestedAt: z.number().describe("When it asked, in milliseconds."), }) .optional() .describe( "The agent has hit something only a person can clear: a captcha, a password it does not hold, a check on your phone. Present only while it is waiting.", ), // A JavaScript dialog a page has open; held by the daemon for whoever answers first, the owner over the live view // or the agent through its tool. Present only while open. dialog: z .object({ pageId: z.string().describe("Which page opened it."), kind: z.enum(["alert", "confirm", "prompt", "beforeunload"]).describe("What it asks: an alert wants dismissing, a confirm a yes or no, a prompt a text."), message: z.string().describe("What the page says."), defaultValue: z.string().optional().describe("A prompt's prefilled answer."), }) .optional() .describe("A dialog a page has open and is waiting on. You or the agent answers it; present only while it is open."), pages: z .array(BrowserPageSchema) .describe("Every page it has open. A browser holds several at once, which is the reason it is listed apart from the terminals."), // Opened from the browser view by the person, not by an agent's tool; absent on an agent's and from an older daemon. own: z .boolean() .optional() .describe("Your own window, opened from the browser view rather than by an agent. Yours to use whenever it is open; no agent drives it."), // The conversation whose agent opened it; the client reads that agent's turn to know whether it is driving now. owner: z .string() .optional() .describe( "The conversation whose agent opened it. While that conversation has a turn running the agent may act in this window at any moment; once it has not, the window is yours to use.", ), }); export type BrowserPage = z.infer; export type BrowserSession = z.infer; export const BrowsersListSchema = z.object({ sessions: z.array(BrowserSessionSchema).describe("Every browser the agents have running, open or recently closed."), }); export type BrowsersList = z.infer; export const BrowserNameParamSchema = z.object({ name: z.string().describe("Which browser.") }); export const BrowserPageParamSchema = z.object({ name: z.string().describe("Which browser."), pageId: z.string().describe("Which of its tabs: a page id from the roster."), }); export const OpenBrowserInputSchema = z.object({ url: z.string().optional().describe("Where the new tab goes. Absent opens a blank tab, the way a browser's own new tab does."), }); export type OpenBrowserInput = z.infer; export const OpenBrowserResultSchema = z.object({ name: z.string().describe("The window it opened in: what the browser view, the live picture and the close route take."), pageId: z.string().optional().describe("The tab it opened, once the window has it. Absent when the tab had not appeared in time."), }); export type OpenBrowserResult = z.infer; // One window on the sandbox's desktop, as its window manager lists it. The id is what /system/desktop-view takes as // `window` to stream that window alone, and what the agent's own desktop tools name it by. export const DesktopWindowSchema = z.object({ id: z.string().describe("The window, as the desktop's window manager names it. Open a picture of it alone by passing this to the desktop view."), app: z.string().describe("The program it belongs to, as the system names it (`xterm`, `Code`), not what a person calls it."), title: z.string().describe("What its title bar says. Empty for a window that has none."), focused: z.boolean().describe("Whether it has the keyboard: typing on the desktop goes to this window."), }); export type DesktopWindow = z.infer; // The sandbox's own desktop (desktop/agent-desktop.ts): one screen, so a state rather than a roster. `windows` is what // tells an empty desktop from a broken picture of one, and what puts its tile on the rail; `list` is what each of them // is, for a tab that shows one window alone. export const DesktopStateSchema = z.object({ running: z .boolean() .describe("Whether the desktop is up. It starts the first time anyone looks at it, you or an agent, and stays up until the sandbox restarts."), display: z.string().optional().describe("The X display it is, present while it runs: a program started with DISPLAY set to this opens its window there."), windows: z .number() .int() .nonnegative() .optional() .describe("How many windows are open on it. None is an empty desktop. Absent while it is not running, or when it has no window manager to ask."), list: z .array(DesktopWindowSchema) .optional() .describe("Each window open on it, absent when it is not running or has no window manager to ask."), }); export type DesktopState = z.infer; // A subagent is any agent another agent started, and every surface shows one the same way whichever mechanism started // it. The kind names the mechanism, never a second sort of thing: // subagent: in-process, the runtime's own Agent/Task tool, tracked via SubagentStart/Stop hooks and task_* messages // joined on toolUseId; it works in its parent's turn and tree, on its parent's provider. // spawned: a full agent on any provider, started through the daemon's spawn door (agent/subagents/children.ts) as a // conversation of its own, in its own worktree; the daemon reports its life directly, and it can outlive its parent's // turn, be steered, and land its own work. // id is the spawning tool call's id for an in-process one, its own conversation id for a spawned one. The kind changes // only where its record is read from and what else can be done with it, never how it looks. // `linked`: a task handed to another of the owner's sandboxes over a link; it runs and lands there, never in this checkout. export const SubagentKindSchema = z.enum(["subagent", "spawned", "linked"]); export type SubagentKind = z.infer; // pending/running/blocked/paused are live, the rest terminal. Uses the SDK's own task vocabulary rather than AgentStatus. // `blocked` is one addition: a spawned child's question/permission/plan card raises it. `paused` the other: a spawned // child whose turn stopped at a wall the sandbox re-runs it past by itself, its report still to come. export const SubagentStatusSchema = z.enum(["pending", "running", "blocked", "completed", "failed", "killed", "paused"]); export type SubagentStatus = z.infer; // Whether anything checked what the subagent did, computed from its edits against the checks that ran after them; works // on every provider. Absent means the daemon saw no tool calls from it. // verified / failing: names the check that spoke, so a targeted test is never read as the whole suite. // unproven: it changed code and nothing checked it. // no-code: it edited nothing, the honest answer for a research subagent, not to be read as approval. export const SubagentVerificationSchema = z.object({ state: z .enum(["verified", "unproven", "failing", "no-code"]) .describe( "Whether anything proved its work: a check passed after its last edit, it changed code and nothing checked it, a check ran and failed, or it changed no code at all.", ), paths: z.array(z.string()).optional().describe("The code files it changed, most recent last. The first few; the record holds the rest."), check: z .string() .optional() .describe( "The command that spoke: the one that cleared it, or the one that failed. Named rather than summarised, so a targeted test is not read as the whole suite.", ), }); export type SubagentVerification = z.infer; // Where the Claude runtime keeps an in-process subagent's own record: beside the session it ran in, under the runtime's // own id for it. The daemon pairs the two while the subagent runs and writes them onto its delegation's card, so the // subagent's whole run reads back from that record after a restart, when nothing in memory still knows where it is. export const SubagentSdkRecordSchema = z.object({ sessionId: z.string().describe("The provider session the subagent ran in."), agentId: z.string().describe("The runtime's own id for the subagent, which names its record beside that session."), }); export type SubagentSdkRecord = z.infer; export const SubagentSessionSchema = z.object({ id: z .string() .describe( "The id of the tool call that started it (an SDK child) or the child's own conversation id (a spawned one); either way both sides already hold it, so a card links to its subagent with the id it has and the subagent points back the same way.", ), kind: SubagentKindSchema.describe( "How it was started: in-process by the runtime's own Agent/Task tool, spawned by the daemon as a conversation of its own, on any provider, or handed to another of the owner's sandboxes over a link, where it runs and lands. It changes where its record is read from and what else can be done with it, never what it is.", ), // The conversation whose turn started it; how a card links back to the chat it belongs to. conversationId: z.string().describe("The conversation whose turn started it, and the way back to the chat it belongs to."), // Subagent type (Explore, general-purpose) or a child's provider label; description is the one-line ask. agentType: z.string().optional().describe("What kind of subagent it is."), description: z.string().optional().describe("What it was asked to do, in one line."), model: z .string() .optional() .describe( "Which model it runs on: the exact id its provider served once its own record says so, where the call that started it named only an alias or nothing.", ), effort: z .string() .optional() .describe("How hard it was told to think: its own definition's tier, else the one it inherited from its parent's turn."), // Which provider serves a spawned child; an SDK subagent implies its own (its parent's). provider: z.string().optional().describe("Which provider serves it, for a subagent spawned across providers."), // How deep in the spawn tree; 1 means the turn itself started it. A subagent can itself delegate further. spawnDepth: z .number() .optional() .describe( "How deep in the chain it sits, where one means the turn itself started it. A subagent can start subagents, and a flat list that could not say so would read as though the turn started all of them.", ), // True when the parent kept working instead of waiting for it to finish. background: z .boolean() .optional() .describe( "The parent carried on working instead of waiting for it. Such a subagent is otherwise invisible until its result lands, sometimes minutes later.", ), status: SubagentStatusSchema.describe( "How it is going. Blocked means it needs an answer, which a parent and an operator act on differently from it simply working.", ), startedAt: z.number().describe("When it started, in milliseconds."), endedAt: z.number().optional().describe("When it finished, in milliseconds. Absent while it works."), activityAt: z.number().describe("When it last did anything, in milliseconds."), // What it has spent, its own count; a parent's cost and the sum of its children's are different numbers. tokens: z .number() .optional() .describe("What it has spent. Its own, so a parent's cost and the sum of its subagents' are two different true numbers."), toolUses: z.number().optional().describe("How many tools it has used."), lastTool: z.string().optional().describe("The last one it reached for."), // Its report: the last assistant message or task summary, what it concluded without opening the transcript. summary: z .string() .optional() .describe("Its report: what it concluded, without opening its record. The question a finished subagent gets read for."), error: z.string().optional().describe("Why it failed, when it did."), // Filled once the subagent ends; a standing read mid-work would judge a job not yet done. verification: SubagentVerificationSchema.optional().describe("Whether anything proved the work its report describes."), // A `linked` one's far end: where its conversation lives, so a row opens it there rather than here. linked: z .object({ link: z.string().describe("The link card it went through: the agent's name for that sandbox."), sandbox: z.string().describe("What that sandbox is called."), sandboxId: z.string().optional().describe("That sandbox's id on the owner's account, when the card knows it."), conversationId: z.string().describe("The conversation it opened there."), }) .optional() .describe("For a task handed to another sandbox: which one, and the conversation it runs in there."), }); export type SubagentSession = z.infer; export const SubagentsListSchema = z.object({ sessions: z .array(SubagentSessionSchema) .describe( "The subagents conversations the caller can see have started, in-process and spawned alike: every one still working, and the most recent that have settled. Working ones first, then the most recently active.", ), }); export type SubagentsList = z.infer;