import { z } from "zod"; import { ShareDetailSchema } from "../schemas/share.js"; import { TurnErrandSchema, TurnSpeakerSchema } from "../schemas/speaker.js"; import { SubagentKindSchema, SubagentSdkRecordSchema, SubagentStatusSchema, SubagentVerificationSchema } from "../schemas/terminal.js"; import { NeedSchema } from "../schemas/needs.js"; import { HeldEndingSchema, RetryLadderSchema } from "../schemas/turn-break.js"; import { HandoffOfferSchema } from "../schemas/providers/handoff.js"; import { NoticeCodeSchema } from "./sandbox-notice.js"; import { TranscriptAgentNoticeSchema } from "./agent-ui.js"; import { ReviewFindingsSchema } from "./review.js"; import type { ToolCallContent, ToolCallLocation, ToolCallStatus, ToolKind } from "./requests.js"; import { browserHelpRequest, capabilityOfferRequest, CapabilityOutcomeSchema, credentialOfferRequest, CredentialReceiptSchema, pageAskRequest, PageSchema, paymentOfferRequest, PaymentReceiptSchema, PermissionAskSchema, permissionRequest, planRequest, questionRequest, terminalHelpRequest, TodoItemSchema, ToolCallContentSchema, ToolCallLocationSchema, ToolCallStatusSchema, ToolKindSchema, } from "./requests.js"; // A conversation as recorded and replayed: the rows, the cards they carry, and the patches that change them while a // turn runs. One shape for the live row and the recorded one, since they're the same row. // Transcript cards. // Status enums for a parked card, settled by the fold from the reply that released it (request-status.ts), riding the row // rather than the reply. `cancelled` means nobody answered, not a decision; `pending` is still parked. export const PlanStatusSchema = z.enum(["pending", "approved", "rejected", "cancelled"]); export type PlanStatus = z.infer; export const QuestionStatusSchema = z.enum(["pending", "answered", "cancelled"]); export type QuestionStatus = z.infer; // `everything`: allowed by a conversation-wide yes, the one given on this card or one already standing. export const PermissionStatusSchema = z.enum(["pending", "allowed", "always", "everything", "denied", "cancelled"]); export type PermissionStatus = z.infer; export const HelpStatusSchema = z.enum(["pending", "helped", "declined", "cancelled"]); export type HelpStatus = z.infer; export const OfferStatusSchema = z.enum(["pending", "approved", "skipped", "cancelled"]); export type OfferStatus = z.infer; // A yes settles the decision, not the ask: the card moves to `connecting` until the outcome frame resolves it. export const CapabilityOfferStatusSchema = z.enum(["pending", "connecting", "skipped", "cancelled"]); export type CapabilityOfferStatus = z.infer; export const TranscriptPlanSchema = z.object({ ...planRequest, status: PlanStatusSchema.describe("Where the decision stands.") }); export type TranscriptPlan = z.infer; export const TranscriptQuestionSchema = z.object({ ...questionRequest, status: QuestionStatusSchema.describe("Where the answer stands."), answers: z .record(z.string(), z.array(z.string())) .optional() .describe("What was chosen, keyed by the question, with the chosen labels or the user's own words."), attachments: z .record(z.string(), z.array(z.string())) .optional() .describe("Files the user attached to their own-words answer, keyed by the question, as workspace-relative paths."), }); export type TranscriptQuestion = z.infer; // `explain`, the judge's sentence, arrives via PermissionAskSchema at raise time; nothing patches it in after. export const TranscriptPermissionSchema = PermissionAskSchema.extend({ ...permissionRequest, status: PermissionStatusSchema.describe("Where the decision stands."), }); export type TranscriptPermission = z.infer; export const TranscriptBrowserHelpSchema = z.object({ ...browserHelpRequest, status: HelpStatusSchema.describe("How the hand-over ended.") }); export type TranscriptBrowserHelp = z.infer; export const TranscriptTerminalHelpSchema = z.object({ ...terminalHelpRequest, status: HelpStatusSchema.describe("How the hand-over ended.") }); export type TranscriptTerminalHelp = z.infer; export const TranscriptCapabilityOfferSchema = z.object({ ...capabilityOfferRequest, status: CapabilityOfferStatusSchema.describe("Where the decision stands."), outcome: CapabilityOutcomeSchema.optional().describe("How an accepted ask's setup ended (the capability_outcome frame)."), }); export type TranscriptCapabilityOffer = z.infer; export const TranscriptPaymentOfferSchema = z.object({ ...paymentOfferRequest, status: OfferStatusSchema.describe("Where the decision stands."), receipt: PaymentReceiptSchema.optional().describe("How the approved payment ended (the payment_receipt frame)."), }); export type TranscriptPaymentOffer = z.infer; export const TranscriptCredentialOfferSchema = z.object({ ...credentialOfferRequest, status: OfferStatusSchema.describe("Where the decision stands."), receipt: CredentialReceiptSchema.optional().describe("Who released it, or that somebody refused (the credential_receipt frame)."), }); export type TranscriptCredentialOffer = z.infer; // A page asked to be answered, and what it sent back: the value as the page gave it, JSON text, which is also exactly // what the agent was handed. export const TranscriptPageAskSchema = z.object({ ...pageAskRequest, status: QuestionStatusSchema.describe("Where the answer stands."), value: z.string().optional().describe("What the page sent back, as JSON text."), }); export type TranscriptPageAsk = z.infer; // One row shape across live folding, storage and replay; a reopened chat redraws the transcript, not paraphrases it. A // subagent's own calls nest under the card that spawned them (z.lazy, self-referential). export const TranscriptToolSchema: z.ZodType = z.lazy(() => z.object({ id: z.string().describe("The call's id."), name: z.string().describe("Which tool."), category: ToolKindSchema.describe( "What kind of thing it does: read, edit, delete, move, search, run, think, fetch. Named the same way whatever the backend called the tool.", ), status: ToolCallStatusSchema.describe("How it went."), target: z.string().optional().describe("What it acted on, in one line: a file, a command, an address."), locations: z.array(ToolCallLocationSchema).optional().describe("The files it touched."), content: z.array(ToolCallContentSchema).optional().describe("What it produced: text, a change to a file, or a picture."), children: z .array(TranscriptToolSchema) .optional() .describe( "Calls a delegated subagent made, nested under the call that started it, so a reopened conversation redraws the delegation rather than collapsing it into one result.", ), nested: z .number() .int() .nonnegative() .optional() .describe( "How many calls sit under this one, present in place of `children` when they were left behind. A transcript page does that, since a settled delegation draws collapsed; ask for the call's own children to fill it in.", ), thinking: z.string().optional().describe("What the agent was reasoning about around this call."), subagent: TranscriptSubagentSchema.optional().describe( "The helper this call started, as the daemon's registry sees it: what it is, how it is going, what it has spent. What a card can say about a backgrounded child whose result is minutes away.", ), input: z .string() .optional() .describe( "What the call was given, as JSON text cut to a few kilobytes, on a call whose input names no one-line target: an MCP tool's arguments, a delegation's prompt. Read afterwards to learn what was asked when the card alone cannot say.", ), startedAt: z.number().optional().describe("When the call was seen, in milliseconds. Absent on calls recorded before cards were stamped."), endedAt: z .number() .optional() .describe("When its result arrived, in milliseconds; absent while it runs, and on a call whose result never came."), exitCode: z.number().int().optional().describe("A command's exit code, where the runtime reports one as a number rather than in its output."), }), ); // The subagent a call started, on that call's card whichever mechanism started it (transcript-fold.ts places it). // Identifying fields arrive once; status/spend/activity replace on each update. export const TranscriptSubagentSchema = z.object({ id: z .string() .optional() .describe( "Its own id, where that is not the card's: a spawned subagent is named by its own conversation, which is what the roster, `wait` and its own chat call it. Absent, the card's id is its id, as it is for one the runtime started in-process.", ), kind: SubagentKindSchema, agentType: z.string().optional(), description: z.string().optional(), model: z.string().optional(), provider: z.string().optional(), background: z.boolean().optional(), status: SubagentStatusSchema, tokens: z.number().optional(), toolUses: z.number().optional(), lastTool: z.string().optional(), summary: z.string().optional(), error: z.string().optional(), verification: SubagentVerificationSchema.optional(), sdk: SubagentSdkRecordSchema.optional().describe( "Where the Claude runtime keeps this in-process subagent's own record, so its whole run reads back once nothing in memory remembers it. Absent for every other kind, and until the daemon has paired it.", ), }); export type TranscriptSubagent = z.infer; // Mutable, unlike most of this file: the fold settles a card in place when its result lands turns later, saving a // second pass. export interface TranscriptTool { id: string; name: string; category: ToolKind; status: ToolCallStatus; target?: string | undefined; locations?: ToolCallLocation[] | undefined; content?: ToolCallContent[] | undefined; children?: TranscriptTool[] | undefined; // How many calls sit under this one when `children` is not carried: a transcript page leaves a delegation's run // behind, since it only draws once the card is opened. nested?: number | undefined; thinking?: string | undefined; subagent?: TranscriptSubagent | undefined; input?: string | undefined; startedAt?: number | undefined; endedAt?: number | undefined; exitCode?: number | undefined; } // Every ending that wakes a conversation off a condition watch; composing and parsing the wake is watch-wake.ts. export const WatchOutcomeSchema = z.enum(["met", "timeout", "restart-expired", "broken"]); export type WatchOutcome = z.infer; // A condition watch waking the conversation, as the row carries it. The wake arrives as an ordinary turn prompt that // nobody typed, so the row keeps that prompt verbatim rather than a summary of it. export const TranscriptWatchWakeSchema = z.object({ outcome: WatchOutcomeSchema.describe("How the watch ended: the condition held, the deadline passed, or a restart cut it short."), note: z.string().describe("The agent's own line on what it was waiting for."), elapsed: z .string() .describe("How long the watch stood, already worded ('43m'): carried rather than recomputed, since the arming instant is not on the row."), sent: z.string().describe("The whole prompt the model was woken with, disclosed under the row."), }); export type TranscriptWatchWake = z.infer; // A need's answer that reached this conversation as a prompt (need-wake.ts); the row keeps the prompt and names the need. export const TranscriptNeedWakeSchema = z.object({ outcome: z.enum(["met", "declined"]).describe("How the need ended: a person gave it, or said no."), title: z.string().describe("The need, as its card leads with it."), id: z.string().describe("The need's handle, which its live card is keyed by."), sent: z.string().describe("The whole prompt the model received, disclosed under the row."), }); export type TranscriptNeedWake = z.infer; // Another agent's words that reached this conversation as a prompt; the row keeps the prompt and names the sender. export const TranscriptAgentWordsSchema = z.object({ kind: z.enum(["peer", "child"]).describe("Who sent it: another conversation in the workspace, or a subagent this one started."), from: z.string().describe("The sending conversation's id."), title: z.string().optional().describe("The sender's title, when it had one."), failed: z.boolean().optional().describe("A child's report on a turn that failed rather than finished."), sent: z.string().describe("The whole prompt the model received, disclosed under the row."), }); export type TranscriptAgentWords = z.infer; // A command the agent left running past its own call; how it is doing now is live state on the card (AgentSummary.jobs). export const TranscriptBackgroundJobSchema = z.object({ id: z.string().describe("The daemon's handle for the job, which its live state on the conversation's card is keyed by."), label: z.string().describe("What the job is, in the agent's own words when it gave any, else its command on one line."), command: z.string().describe("The command as the agent wrote it, folded to one line."), startedAt: z.number().describe("When it started, in milliseconds."), }); export type TranscriptBackgroundJob = z.infer; // One note the daemon put before a user's message: the model reads `text`, the chat draws `title` on a row that opens // to it. Shared by the live frame and the restored transcript, so it reads the same either way. export const TurnNoteSchema = z.object({ title: z.string().describe("The one line a reader sees, on a row that opens to the text below."), text: z.string().describe("The note itself, which is also exactly what the model was told."), }); export type TurnNote = z.infer; // End-of-turn accounting (assistant rows only, the last bubble of a turn): what the turn cost, attached where // the answer ended so a reader can see what each exchange spent. export const TranscriptUsageSchema = z.object({ costUsd: z.number().optional(), inputTokens: z.number().optional(), outputTokens: z.number().optional(), durationMs: z.number().optional(), numTurns: z.number().optional(), }); export type TranscriptUsage = z.infer; // One row; each block of the agent's prose is its own, with the tool cards that block introduced, so a turn redraws as // it actually unfolded rather than one bubble with everything hanging off the end. export const TranscriptRowSchema = z.object({ // `notice` is neither speaker: something that happened to the turn (a refusal, a landed delta, a stop). role: z .enum(["user", "assistant", "notice"]) .describe( "Who said it. A notice is neither side: it is something that happened to the turn, recorded so a reopened conversation can say it. Without those, a turn a provider refused ends on the user's message and reads as broken.", ), text: z.string().describe("The words."), // The row's own name, minted once where the row is made (the live fold, or the record for a row written outside a // run) and carried on every copy: the live head's, each patch's, the record's, a client's mirror. Positions move // under a rewind and contents grow while a run streams, so this is how a reader says "this row" across all of them. rowId: z .string() .optional() .describe( "The row's own id: the same on the live turn's copy, every change to it, the record, and a reader's own copy, so a reader updates the row it already holds instead of matching it by content. Rows recorded before rows had ids are named by their position (`@12`). Absent from a sandbox older than ids.", ), // Which run produced this row, carried on every copy of it: the live head's, the record's, a client's mirror. // It is how a client says "I am already showing this run" without comparing content, which cannot answer that // while the run's last row is still growing. run: z .string() .optional() .describe( "The run that produced this row. Present on everything a turn produced, absent on rows written outside one. A client draws a run's rows over whatever it already holds for that run, which is what this identifies; content cannot, because the last row of a live run keeps growing.", ), // The turn's send time (user rows only), not settlement; the agent's rows carry `openedAt` instead. sentAt: z .number() .optional() .describe("When it was sent, in milliseconds. On the user's rows only; the agent's own rows and notices carry `openedAt`."), // Stamped by the daemon as it folds the frame that opens the row, since the frames themselves carry no clock. openedAt: z .number() .optional() .describe( "When an assistant bubble or a notice opened, in milliseconds, as the daemon folded the frame that started it. Absent on user rows, which carry `sentAt`, and on rows recorded before rows were stamped.", ), // A position moves under a rewind and a message's words can repeat, so this is the one name a message keeps. messageId: z .string() .optional() .describe( "The message's own id, on the rows of messages sent to the agent: what its sender named it, or what the sandbox did. A rewind names the message by it, and the same id sent again is recognised rather than delivered twice.", ), // Uploaded files only (user rows); a path already @-mentioned inline is not drawn again as a chip. attachments: z.array(z.string()).optional().describe("Files attached to this message, as workspace paths."), // Never stored: looked up each read from the daemon's rewind points, matching what's still there. checkpointId: z .string() .optional() .describe( "The saved point this message can be rewound to. Looked up on each read rather than stored, so what is offered is exactly what is still there to go back to.", ), rewindIndex: z .number() .int() .nonnegative() .optional() .describe("This message's position in the conversation's record, which is how a rewind names it. Present only beside a checkpoint."), thinking: z.string().optional().describe("What the agent was reasoning about."), tools: z.array(TranscriptToolSchema).optional().describe("The tool calls this part of the turn made."), todos: z.array(TodoItemSchema).optional().describe("The agent's task checklist, as of this bubble."), usage: TranscriptUsageSchema.optional().describe("What the turn cost, on the bubble its answer ended in."), // The daemon's preamble notes for this message; on the message, not its own row, since they were part of it. notes: z .array(TurnNoteSchema) .optional() .describe( "What the sandbox added to this message before the model saw it. Carried on the message rather than as rows of their own, because they genuinely were part of what was sent.", ), // Who spoke a message row, as the sandbox verified it (schemas/speaker.ts), and what a composed one is for: a reader // shows the sandbox's own words as the sandbox's, never as the owner's. Absent on rows written before rows said so, // which are read by their opening (events/errands.ts). speaker: TurnSpeakerSchema.optional().describe("Who sent this message, as the sandbox verified it. Absent where it does not say."), errand: TurnErrandSchema.optional().describe( "What this message is for, when the sandbox or the app composed it rather than a person typing it. Absent on a person's own words.", ), // The user wrote this as the agent, no turn behind it; marked for a human reader, never surfaced to the agent. placed: z .boolean() .optional() .describe( "A person wrote this in the agent's voice, with no turn behind it. Marked for the human re-reading the conversation months later, so their own words do not pass as the agent's. The agent itself never sees the mark.", ), // The follow-up this notice offers, by name; the chat decides what it does, and whether it stands. `sendAnyway` sends // the held message; `sandboxMemory` offers that and a raise of the sandbox's memory; `sendAgain` sends a turn the // sandbox kept after a refusal no raise would clear; `switchModel` opens the model list for a turn a model's refusal // stopped partway, so the same conversation carries on with another. noticeAction: z .enum(["landHold", "depsInstall", "watchStop", "sandboxMemory", "sendAnyway", "sendAgain", "switchModel"]) .optional() .describe("A one-press follow-up this notice offers, by name. The chat decides what it does and whether it still applies."), // Where a refused turn's words wait decides what the notice's press does: the conversation's queue, or the sandbox. sandboxHeld: z .boolean() .optional() .describe( "The sandbox kept this refused turn whole, its message still above, so the notice's press runs that turn again instead of letting the conversation's queue go, which never held these words.", ), // An unfinished wait this notice describes, by name; whether it's still running is live state, not stored here. noticeWait: z .enum(["credentialRenewal", "chatRoute", "watch"]) .optional() .describe("The wait this notice describes, by name, so a reader can say whether it is still on."), // Which one, for a wait whose kind can have several in flight at once; without it two armed watches settle together. noticeWaitId: z.string().optional().describe("Which instance of the wait this notice names, for a kind that can have several running at once."), // The sandbox's own notice by name, beside its English `text` (sandbox-notice.ts); absent on every other row, and // on the notices written before rows carried it, which a reader draws from `text` as it always did. noticeCode: NoticeCodeSchema.optional().describe( "Which of the sandbox's own notices this row is, and the facts it was worded from, so a reader can say it in the reader's own language. The text stays the English sentence.", ), // A notice a runtime's own extension or loop said (events/agent-ui.ts), the words in `text`; absent on every other row. agentNotice: TranscriptAgentNoticeSchema.optional().describe( "This notice was said by the agent's runtime or one of its extensions rather than by the sandbox: how loud, and who said it.", ), // At most one card per row; a card closes its bubble. One field per kind, so a reader reaches it by name. plan: TranscriptPlanSchema.optional().describe("The plan this row asked approval for, and the answer."), question: TranscriptQuestionSchema.optional().describe("The questions this row asked, and the picks that answered them."), permission: TranscriptPermissionSchema.optional().describe("The tool this row asked permission for, and the decision."), browserHelp: TranscriptBrowserHelpSchema.optional().describe("The browser hand-over this row asked for, and how it ended."), terminalHelp: TranscriptTerminalHelpSchema.optional().describe("The terminal hand-over this row asked for, and how it ended."), capabilityOffer: TranscriptCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."), paymentOffer: TranscriptPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."), watchWake: TranscriptWatchWakeSchema.optional().describe("The condition watch that woke this conversation, and the prompt it was woken with."), // A need is not a parked card: it holds no turn open, outlives the one that raised it, and its live state is the // needs store's, keyed by `need.id`. What rides here is the need as it was raised, for a reader with no store. need: NeedSchema.optional().describe( "Something the agent asked a person for, as it was when raised. Its live state (answered, met) is read by its id, since it outlives the turn.", ), needWake: TranscriptNeedWakeSchema.optional().describe("The answered need that reached this conversation, and the prompt it came as."), agentWords: TranscriptAgentWordsSchema.optional().describe( "Another agent's words that reached this conversation, whose they are, and the prompt they came as.", ), backgroundJob: TranscriptBackgroundJobSchema.optional().describe("The background job this row marks the start of."), // Not a card: a page parks nothing, it is part of what the agent said. On the bubble whose tool call showed it. page: PageSchema.optional().describe("A page the agent showed here, drawn inline."), // Not a card either: a review's findings are what the agent said, drawn as one block on the bubble that reported them. review: ReviewFindingsSchema.optional().describe("A code review's findings the agent reported here, drawn inline."), pageAsk: TranscriptPageAskSchema.optional().describe("A page the agent asked to be answered on, and what it sent back."), credentialOffer: TranscriptCredentialOfferSchema.optional().describe( "The gated credential this row asked to use, who may release it, and who did.", ), }); export type TranscriptRow = z.infer; // Every card field, as one list, for readers asking whether a row holds a card at all. export const REQUEST_FIELDS = [ "plan", "question", "permission", "browserHelp", "terminalHelp", "capabilityOffer", "paymentOffer", "credentialOffer", "pageAsk", ] as const; export type RequestField = (typeof REQUEST_FIELDS)[number]; export type TranscriptRequests = Pick; // Whether a row holds a card at all, answered or not. export const holdsRequest = (row: TranscriptRequests): boolean => REQUEST_FIELDS.some((field) => row[field] !== undefined); // Whether a row is holding the turn open on a card nobody has answered. export const isAwaitingDecision = (row: TranscriptRequests): boolean => REQUEST_FIELDS.some((field) => row[field]?.status === "pending"); // The row a patch changes, by its own id, beside the index older readers count from. const patchRowId = z .string() .optional() .describe("The id of the row this changes. A reader holding a row by that id changes it there, wherever it sits; `index` is for one that does not."); // One change to a run's rows, so a client keeps rows rather than frames. Prose/thinking append to a row or a card; a // `tool` card replaces its own fields by id, never its nested calls or thinking, which arrive by their own patches; // everything else replaces its row; `drop` removes a row that opened and never wrote. Each names its row twice: by // `index` from where the run's rows start, and by `rowId`, which needs no counting and survives a row dropped above it. export const TranscriptPatchSchema = z.discriminatedUnion("op", [ z.object({ op: z.literal("append").describe("A new row at the end."), row: TranscriptRowSchema }), z.object({ op: z.literal("replace").describe("This row, whole, in place of the one at that index."), index: z.number().int().nonnegative(), rowId: patchRowId, row: TranscriptRowSchema, }), z.object({ op: z.literal("drop").describe("The row at that index is gone: it was opened and never written into."), index: z.number().int().nonnegative(), rowId: patchRowId, }), z.object({ op: z.literal("text").describe("More of the agent's prose, onto that row's text."), index: z.number().int().nonnegative(), rowId: patchRowId, text: z.string(), }), z.object({ op: z.literal("thinking").describe("More of the agent's reasoning, onto that row's thinking."), index: z.number().int().nonnegative(), rowId: patchRowId, text: z.string(), }), z.object({ op: z.literal("toolThinking").describe("More of a delegated subagent's reasoning, onto the thinking of the card that started it."), index: z.number().int().nonnegative(), rowId: patchRowId, id: z.string().describe("The card's id, matched wherever it nests."), text: z.string(), }), z.object({ op: z .literal("tool") .describe( "A tool card's own fields: new, or the latest state of one already there, matched by id wherever it nests. Carries no `children` or `thinking`; a card already there keeps its own.", ), index: z.number().int().nonnegative(), rowId: patchRowId, tool: TranscriptToolSchema, parent: z.string().optional().describe("The card this one nests under, when it is a delegated subagent's own call."), }), ]); export type TranscriptPatch = z.infer; export const SessionTranscriptSchema = z.object({ messages: z .array(TranscriptRowSchema) .describe( "The conversation, in order. Each block of the agent's prose is its own message with the tools that block introduced, which is what reproduces the way it actually unfolded.", ), }); // How a turn that left work behind ended, folded straight into pickup state (chat/pickUp.ts). `held` makes a press // re-run the turn instead of appending a message; `scheduled` means it's already coming back. export const TurnEndingSchema = z.object({ reason: HeldEndingSchema.describe( "Which ending left the work here: a Stop or a daemon killed under the turn, a spent usage allowance, a provider that refused it, or the provider's safety classifier stopping it partway.", ), resetsAt: z .number() .optional() .describe( "When the spent allowance reopens, in epoch seconds. Absent for every ending that names no instant, and for a provider that publishes none.", ), held: z .object({ ran: z .boolean() .describe("Whether the held turn got anywhere before it was refused, which is a different sentence from one refused at the door."), contextTokens: z .number() .optional() .describe( "How much context a press that keeps the session re-reads once, on this account at the reset or carried to another. Absent when no usage frame measured it.", ), handoffTokens: z .number() .optional() .describe( "What a press that opens a fresh session pays instead: the capped record plus the sandbox's measured brief, counted at the failure.", ), moving: z .string() .optional() .describe( "The account the owner's policy is already moving this turn to, when it is; the surface then reports the move rather than offering a press.", ), handoff: HandoffOfferSchema.optional().describe( "How the held turn can continue once its cache is cold: carry the session, trim it, or summarise it, which one the sandbox suggests, and which one a person picked. Absent where there is nothing to carry.", ), }) .optional() .describe("Present when the daemon still holds the refused turn whole, so a press re-runs it rather than appending a message after it."), scheduled: z .boolean() .optional() .describe( "Whether something other than the user is already booked to send this turn again, so the surface reports the wait instead of offering a press.", ), // `resetsAt` is the provider's fact about the allowance; this is the daemon's own appointment, and the two differ // whenever a ladder rung falls short of the reset, or an ending with no allowance at all is armed. nextAt: z .number() .optional() .describe( "When the booked send actually fires, in epoch seconds. Present only with `scheduled`; absent for a booking that fires on the next pass, which is 'now' to a reader.", ), retries: RetryLadderSchema.optional().describe( "How many automatic re-runs a stopped turn has already had, of how many. Absent before its first; equal counts mean the ladder is spent and only a press sends it again.", ), }); export type TurnEnding = z.infer; // A delegation's own run, fetched when its card opens; empty when the record no longer holds it. export const AgentToolChildrenSchema = z.object({ children: z.array(TranscriptToolSchema).describe("The calls the delegated agent made, in the order it made them."), }); // The whole of a published conversation, baked into the page since nothing else may still be running when it opens. The // same TranscriptRow rows the app replays, filtered to the chosen detail, pictures rewritten to published copies. export const SharePayloadSchema = z.object({ title: z.string(), // When the snapshot was taken, not when the conversation happened. sharedAt: z.number(), detail: ShareDetailSchema, messages: z.array(TranscriptRowSchema), }); export type SharePayload = z.infer;