// agents: the conversation fleet import { LandingDeliverySchema } from "./project-delivery.js"; import { AgentNextSchema } from "./agent-next.js"; import { z } from "zod"; import { AgentHarnessSchema, AgentOriginSchema, AgentProviderSchema, ConversationQueueSchema, ForkedFromSchema, FreshSessionSchema } from "./agent.js"; import { LoopStateSchema } from "./loops.js"; import { LimitPolicySchema, RetryPolicySchema, RoomPolicySchema, TurnBreakPolicySchema, TurnBreakSchema } from "./turn-break.js"; import { KeepWarmSchema } from "./keep-warm.js"; import { AgentNeedSchema } from "./needs.js"; import { EMOJI_MAX_LENGTH, isSingleEmoji } from "../text/emoji.js"; import { ProgramAskSchema } from "../events/requests.js"; import { ReviewTallySchema, TurnProofSchema } from "./work-assurance.js"; import { AgentReactionSchema, SessionOwnerSchema } from "./agent-people.js"; export { AgentReactionSchema, ReactorSchema, SessionOwnerSchema, type AgentReaction, type Reactor, type SessionOwner } from "./agent-people.js"; // A fleet agent is any conversation with a registry entry, keyed by conversationId. Isolated ones own a git worktree // (branch agent/); workspace conversations have none, but both share one status/activity/cost lifecycle. // idle/running/awaiting: turn lifecycle (awaiting = paused on a plan or question) // ready/landed/conflict: land-flow outcomes (ready = clean completion held on the branch, auto-land off) // error: terminal turn failure // interrupted: the daemon died under the turn (crash, OOM, rebuild); without it such a turn rehydrates as idle and its // parked question disappears // stopping/stopped: the two halves of a user's Stop, since a hard-cancel is not instant (`stopping` the moment the // abort lands, `stopped` once the turn's generator has unwound) // dismissing: the same window for waving away a parked question; settles in Finished rather than Attention, since // nothing is left half-done // `stopped` is its own value, never `error` (a deliberate stop is not a failure) or `interrupted` (which allows an // automatic re-run) // resuming: the turn was killed by something the daemon is already undoing (a credential re-mint, an outage backoff); // never persisted, since a daemon restart forgets the resume and the turn's own last-written ending becomes true again export const AgentStatusSchema = z.enum([ "idle", "running", "awaiting", "stopping", "dismissing", "stopped", "resuming", "landing", "ready", "landed", "conflict", "error", "interrupted", ]); export type AgentStatus = z.infer; // The card's live activity snippet: the last tool the agent used (with its target) and the in-progress todo. export const AgentActivitySchema = z.object({ tool: z.string().optional().describe("The last tool it reached for."), target: z.string().optional().describe("What it reached for that tool with: a file, a command, a URL."), todo: z.string().optional().describe("The item on its own list that it is working through."), }); export type AgentActivity = z.infer; // Where the agent is on its own checklist right now, unlike UnfinishedWorkSchema's account of where a turn stopped. // Counts items, never segments: how a rim or a bar divides them is the reader's business. export const AgentChecklistSchema = z.object({ done: z.number().describe("Items it has completed."), total: z.number().describe("Items on the list. Never zero: a conversation that kept no list carries no clause at all."), }); export type AgentChecklist = z.infer; // Which "needs you" flags are raised, the fleet badge aggregates these across all agents. export const AgentAttentionSchema = z.object({ plan: z.boolean().describe("It has proposed a plan and is waiting for a yes."), question: z.boolean().describe("It has asked you something."), permission: z.boolean().describe("It wants to use a tool it needs permission for."), // A missing capability on a card from before needs (docs/architecture/needs.md); lets the lane say "setup needed" rather than a generic // pause. capability: z.boolean().describe("It needs something connected that is not connected yet."), // A gated credential parked on a named person's click; the one pause the board's reader may not be able to clear themselves. credential: z .boolean() .describe("It is waiting for a named person to release a credential. The one pause that may not be yours to clear, whatever your role."), conflict: z.boolean().describe("Its work cannot be merged without somebody resolving a clash."), // A need outlives its turn (docs/architecture/needs.md), so this can stand on a card that is not running at all. need: z .boolean() .optional() .describe("It asked a person for something it still needs: a connection, a secret, wider reach, a tool. Absent from a daemon older than needs."), }); export type AgentAttention = z.infer; // What the last turn left open, measured at the moment it ended, unlike AgentAttentionSchema's live parked waits. // Derived from finish frames every harness sends, never self-reported; absent for a turn that closed its list and // passed its checks. export const UnfinishedWorkSchema = z.object({ // Epoch ms the turn ended; not `updatedAt`, which moves on a land or rename without the work moving. at: z.number().describe("When the turn that left this ended, in milliseconds."), // The agent's own checklist as of that turn's last word on it; `open`/`total` count items, `next` is the // in-progress or first pending one. steps: z .object({ open: z.number().describe("Items on it that were never completed."), total: z.number().describe("Items on the whole list."), next: z.string().optional().describe("The one it would have done next: what it was working through, or the first still waiting."), }) .optional() .describe("The agent's own checklist where that turn left it. Absent for a conversation that kept no list."), // Name of the `turn.ending` check still failing when the turn ended, from when a failing one left the work unfinished. // Nothing writes it now, and it stays so entries written before still read. check: z .string() .optional() .describe( "The end-of-turn check that was still failing when the turn ended, by name. No longer written: what a check finds as a turn ends is said back to the model and never leaves its work unfinished.", ), }); export type UnfinishedWork = z.infer; // What has looked at a conversation's work (its last turn's own checks, its latest code review): work-assurance.ts. export * from "./work-assurance.js"; // How many entries each of a reach's lists carries; past it, the list's `…More` count says how many were left out. export const TURN_REACH_LIST_MAX = 10; // Where the last turn's work went when it went somewhere other than its own branch, read off what the turn touched and // never asked of the model. Lists only the exceptions: a turn whose work all lives on its branch leaves no reach at all. export const TurnReachSchema = z.object({ at: z.number().describe("When the turn that left this ended, in milliseconds."), live: z .array( z.object({ path: z .string() .describe( "What it wrote, as it named it: relative to the workspace for the sandbox's shared state, absolute for the owner's own checkout.", ), extension: z .string() .optional() .describe("The extension, by its manifest id, when what it changed is the installed copy of one. Absent for anything else."), }), ) .max(TURN_REACH_LIST_MAX) .optional() .describe( "Files it changed in place where every conversation reads them, outside its own branch: an installed extension's copy, the sandbox's shared state, the owner's own checkout. Live the moment they were written, never reviewed, and an installed extension's next update replaces them.", ), liveMore: z.number().optional().describe("How many more such files there were beyond the ones listed. Absent when all are listed."), stranded: z .array( z.object({ dir: z.string().describe("The clone, as a folder relative to its own copy of the workspace."), uncommitted: z.number().describe("Files changed in it and never committed."), unpushed: z.number().describe("Commits in it its upstream does not have, or that no remote has when it follows none."), }), ) .max(TURN_REACH_LIST_MAX) .optional() .describe( "Repositories it cloned inside its own copy and left work in. A clone of its own never rides its land, so what is in one stays there.", ), strandedMore: z.number().optional().describe("How many more such clones there were beyond the ones listed. Absent when all are listed."), published: z .array( z.object({ dir: z.string().optional().describe("The folder it pushed from, as the command named it. Absent for its own working folder."), remote: z.string().optional().describe("The remote it pushed to, as the command named it. Absent when the command named none."), branch: z.string().optional().describe("The branch it pushed to, as the command named it. Absent when the command named none."), command: z.string().describe("The push itself, trimmed to one line."), }), ) .max(TURN_REACH_LIST_MAX) .optional() .describe("Pushes it made with git that went through: work it sent out of the sandbox itself, never through a land."), publishedMore: z.number().optional().describe("How many more such pushes there were beyond the ones listed. Absent when all are listed."), }); export type TurnReach = z.infer; // A landing's commit message: subject plus the two trailer sentences a changelog repo gets. One schema, not three // duplicated fields, since both the live roster and the review carrier must read the same shape and can't disagree. export const LandedMessageSchema = z.object({ // Written from the landed diff, not the opening prompt: a session that drifts from its title still needs an accurate subject. subject: z .string() .describe( "One line saying what the merged work did, read off the code rather than off the opening request. A conversation that asks for an audit and then spends four turns fixing what it found needs a subject about the fixes.", ), // Usually absent by design: the model omits it rather than inventing a note for an unremarkable change. note: z .string() .optional() .describe( "The same change said to somebody who uses the product, for a repository that keeps a changelog. Usually absent, because most changes are not ones a user would notice.", ), // Nearly always absent (removals only); required and mechanically guaranteed when a landing shrinks a wire-contract lock. breaking: z .string() .optional() .describe("What this change takes away, for anything already relying on it. Nearly always absent: it is for removals, not for additions."), // Present only when the conversation said why a test it weakened should stay weaker; the push gate reads it as the // weakening's declaration. testNote: z .string() .optional() .describe("Why a test this change weakened is meant to be weaker, in the conversation's own words. Nearly always absent."), // Present only when the conversation declared an exception a check should grant this change (`Allow: — `); // the push reads it off the commit. allows: z .array(z.string()) .optional() .describe("Exceptions the conversation declared for this change, each ` — ` in its own words. Nearly always absent."), }); export type LandedMessage = z.infer; // One model's turn in the drafting walk (agent/role-model.ts tries connected models in order), in spend order: // asking: in flight, `ms` absent // answered: wrote the sentence, in `ms` // refused: failed or declined, in `ms`, with `reason` // skipped: not asked, since it refused recently and the walk stepped over it export const LandedMessageStepSchema = z.object({ provider: z.string().min(1).describe("Which provider was asked."), model: z.string().min(1).describe("Which of its models."), status: z .enum(["asking", "answered", "refused", "skipped"]) .describe("How this one went. Skipped means it was not asked at all, because it refused a few minutes ago and the walk stepped over it."), // Epoch ms it started; client-measures an in-flight step's ticking display. Absent for `skipped`, which cost no time. at: z.number().optional().describe("When it started being asked, in milliseconds. Absent for one that was skipped, which cost no time."), ms: z.number().optional().describe("How long it took. Absent while it is still being asked."), reason: z.string().optional().describe("Why it refused, in its own words."), }); export type LandedMessageStep = z.infer; // Full account of one landing's commit message being drafted: started, which models were asked, how each went, and how // it ended. `outcome` absent means still running; `written` puts the sentence on the card, `failed` carries a one-line // `reason`. export const LandedMessageDraftSchema = z.object({ startedAt: z.number().describe("When the drafting began, in milliseconds."), steps: z .array(LandedMessageStepSchema) .describe( "Each model that was asked, in the order they were spent, so the list is the timeline. Empty with no outcome means the diff is still being read.", ), outcome: z.enum(["written", "failed"]).optional().describe("How it ended. Absent means it is still going."), reason: z .string() .optional() .describe("The one-line account of a failure, for a screen with one line to spend. The steps carry each model's own words."), finishedAt: z.number().optional().describe("When it ended, in milliseconds."), }); export type LandedMessageDraft = z.infer; // Why a path would not land: // workspace: your own uncommitted edits are on that path // diverged: the main tree's committed content moved under the agent since it branched; nothing of yours is at risk // binary: git cannot three-way merge the file at all // Declared here rather than beside the conflict report below, since the roster carries the causes too: a card cannot // offer the press that clears a refusal without knowing whose refusal it is. export const LandConflictReasonSchema = z.enum(["workspace", "diverged", "binary"]); export type LandConflictReason = z.infer; export const AgentSummarySchema = z.object({ id: z.string().describe("The conversation id, which is how every other call addresses it."), sessionId: z.string().optional().describe("The provider session behind the last turn. It is retired whenever the model or account changes."), title: z.string().optional().describe("What to call it: the first prompt cut to one line, unless somebody renamed it."), titleAction: z .string() .optional() .describe( "One word for the kind of work the naming pass read in it (fix, audit, redesign). Never part of the displayed name: a board tints and glyphs a card by it. Absent for a title nothing named an action for.", ), status: AgentStatusSchema.describe( "What it is doing. Stopping and stopped are the two halves of somebody pressing stop, because a cancel is not instant; dismissing is the same window for a question waved away, which ends the turn too but owes the user nothing; resuming means the sandbox is already putting right whatever killed the turn; landing means its work is being carried into the workspace right now, and nothing may act on its branch until that settles.", ), // Sentence the last turn died on; carried since `error` status alone isn't an answer, especially for an unwatched run. failure: z .string() .optional() .describe( "Why the last turn failed, in the words it died on. Absent unless it did, and cleared the moment it runs again. Carried here because the word error on its own is not an answer, least of all for a run nobody was watching.", ), // The error frame's own code, so the board can distinguish e.g. a rate limit from a crash the way chat already does. failureCode: z .string() .optional() .describe( "Which kind of failure it was, as the turn's own error frame coded it. Absent for a failure nothing could classify, which reads as the plain red line it is.", ), // Who worded `failure`, as the turn's error frame said (its `own`): a card leads with those words only when they are // the sandbox's own sentence, as the chat's notice does (failureView), and with its kind's line otherwise. failureOwn: z .boolean() .optional() .describe( "Whether `failure` is the sandbox's own sentence rather than the provider's or the harness's words. Absent for theirs, and for an ending recorded before it was said.", ), // Epoch seconds the spent allowance reopens; absent means genuinely unknown (Grok, Cursor) — never guess one. limitResetsAt: z.number().optional().describe("When the spent allowance reopens, in epoch seconds. Absent when the provider publishes no instant."), limitHeld: z.boolean().optional().describe("Whether the refused turn is held whole, so sending again re-runs it instead of appending to it."), // What a sandbox older than `next` (before 1.343.0) said in its place, read so the editor can derive `next` from it // (agentStatus-standing.ts, withNext): the hosted editor meets such a sandbox for as long as it stays off the release // that dropped them, which carried a Breaking-Note and so is never taken by itself. No sandbox sends them since. limitScheduled: z.boolean().optional().describe("Sent only by sandboxes older than `next`: whether the held turn was booked to go again by itself."), limitMoving: z.string().optional().describe("Sent only by sandboxes older than `next`: the account a booked move was taking the held turn to."), limitNextAt: z.number().optional().describe("Sent only by sandboxes older than `next`: when the booked re-run went, in epoch seconds."), // The one answer to "does it go by itself, and when" (agent-next.ts), derived from what it holds every time it is read. next: AgentNextSchema.optional().describe( "What carries it on next with nobody pressing anything, and when: a held turn re-run, a message held for memory, or messages booked for later. Absent when only a press moves it.", ), provider: AgentProviderSchema.describe("Which model provider it runs on."), harness: AgentHarnessSchema.describe("Which agentic loop it runs on."), // Latched with the conversation, so the card can say where work runs without asking anything else. runner: z.string().optional().describe("The runner this conversation runs on. Absent means this sandbox."), // Both latched at the first turn, so a board scoped to one project can say which conversations are its own. startIn: z.string().optional().describe("Which folder it opened in, relative to the workspace root. Absent means the root."), actsAs: z.string().optional().describe("Which persona its first turn acted as. Absent for an ordinary chat."), lastActsAs: z .string() .optional() .describe( "Which persona its last turn acted as: each turn runs as the persona it names, so this is who the conversation speaks as now, and what a view groups it under. Absent for an ordinary chat.", ), // Per-conversation, so opening it restores its own choices rather than another tab's; `fast` here is what was asked // for, not what was served. model: z .string() .optional() .describe( "What its last turn ran with. Kept per conversation so opening it restores the choices made in it, rather than whatever some other tab last picked.", ), effort: z.string().optional().describe("How hard that turn was told to think."), thinking: z.boolean().optional().describe("Whether that turn showed its reasoning."), fast: z.boolean().optional().describe("Whether that turn asked for higher speed. What was asked for, not what was served."), account: z.string().optional().describe("Which connected account paid for it."), // The worktree branch (agent/); absent for a non-isolated (main-tree) conversation. branch: z.string().optional().describe("The branch its private copy works on. Absent for a conversation that works directly in the shared tree."), // Per-agent override of the sandbox-wide `autoLand`; absent means inherit, the common case that keeps the toggle // meaningful. autoLand: z .boolean() .optional() .describe( "This conversation's own answer to whether its work merges automatically. Absent means it follows the sandbox-wide setting, which is the common case.", ), // This conversation's own answer to each ending's one question, written by the in-chat control (not the settings // row) so one late-night click can't arm every agent; absent inherits the sandbox-wide policy. limitPolicy: LimitPolicySchema.optional(), outagePolicy: RetryPolicySchema.optional(), stopPolicy: RetryPolicySchema.optional(), memoryPolicy: RoomPolicySchema.optional(), // A collaborator's ask to land (collaborators can't merge themselves); cleared by whichever merge or discard // answers it. landRequested: z .object({ email: z.string().describe("Who asked."), name: z.string().optional().describe("Their display name."), at: z.number().describe("When they asked, in milliseconds."), }) .optional() .describe( "A collaborator has asked a maintainer to merge this work. Cleared by whichever merge or discard answers it. Absent means nobody is waiting.", ), // Beside `landRequested` because both are marks people left on the card rather than anything the agent did. Ordered // by when each emoji was first used, so a chip never jumps out from under the cursor when a second person presses // the same one. reactions: z .array(AgentReactionSchema) .optional() .describe( "What people have marked this conversation with, one entry per emoji, in the order the emoji were first used. Absent means nobody has marked it, which is most conversations.", ), // The card's provenance line when an outside message opened the conversation; absent means a person started it. origin: AgentOriginSchema.optional().describe( "Where the conversation came from when nobody typed it: a chat mention, a visitor's message, a webhook. Absent means a person started it.", ), // The other provenance line: who asked for the first turn directly (vs `origin`, which names the automation); // latched, never rewritten by a later turn. startedBy: z .string() .optional() .describe( "Who asked for the first turn, as the sandbox verified it: a member's email, token: