/** * ONE turn, in both venues — the object a caller holds while it runs, and the * assembly that runs it. * * `chat()` and `run()` are the same turn wearing two contexts: a present user's * conversation and an unattended run differ in venue, in presence, and in * whether the answer has a declared shape — and in nothing else. So there is one * {@link runTurn} here rather than one per lane. `session()`/`respond()` are NOT * this lane: they hand back a streaming `Response` and BLOCK on an approval, and * they are untouched. * * DRAINER OF RECORD. The turn executes and persists whether or not anyone reads * `events` and whether or not anyone awaits it: `await response.text()` below is * what fires the stream's `onFinish`, and `onFinish` is what writes the * transcript and the audit row. `events` is an INDEPENDENT tap on the same run — * a turn nobody taps still happens, in full. * * A PARK ENDS THE TURN. Every turn here runs `interactive: false`, so a call the * guard wants a person for refuses on the spot instead of blocking on the 90s * approval waiter: `await turn` answers `interrupted` in the time the turn took, * never in a minute and a half. Presence is the CTX's and is untouched by that * flag (`ToolListingContext`), so a chat turn still runs `presence: "present"` * and still sees the whole present-user tool surface. */ import { type ApprovalRequest, type Decisions, type FilesAdapter, type Guard, type Harness, type Json, type RunContext, type SeatModels, type Skill, type ThreadId, type ToolCall, type ToolOutcome, type ToolRegistry, type TurnId, type TurnResult as CoreTurnResult } from "../core/index.js"; import type { VendoGuard } from "../guard/index.js"; import { type HarnessRuntimeDeps, type UsageTotals } from "../harnesses/index.js"; import { type VendoStore } from "../store/index.js"; import { type FlexibleSchema, type LanguageModel } from "ai"; import type { MemoryAdapter } from "./memory.js"; import type { SystemPromptHook } from "./prompt.js"; /** The union, with the AGENTS-level `Turn` bound into the arm that resumes. * Core owns the definition; this only names the loop core cannot. */ export type TurnResult = CoreTurnResult>; /** * A turn in flight. * * Returned rather than awaited so `threadId` and `turnId` are readable * immediately — show them, hand the thread back on the next call, or join the * turn's audit rows — and so `events` can be read while the turn is still going. */ export interface Turn extends PromiseLike> { readonly threadId: ThreadId; readonly turnId: TurnId; /** Read ONCE, while the turn runs: nothing is kept for a reader that never * attaches, and a second reader alongside the first throws. */ readonly events: AsyncIterable; } /** What a caller can watch a turn do while it runs. The harness's own vocabulary * for what it SAYS (`text`/`status`/`error`, straight off the runtime's * `observe` tap) plus the two things it DOES, off the same bridge rails the * result is assembled from. */ export type RunEvent = { type: "text"; delta: string; } | { type: "status"; label: string; } | { type: "error"; message: string; } | { type: "tool-call"; id: string; tool: string; args: Json; } | { type: "tool-result"; id: string; tool: string; outcome: ToolOutcome["status"]; }; export interface ChatOptions { /** Whose turn this is — the subject every grant, workspace and audit row is * scoped to. Unset, the agent talks as itself. */ as?: string; /** Server-trust identity facts, model-visible (`[User]`). */ user?: Record; /** Guard/tools context: functions run at check-time, data survives parking. */ context?: Record; /** Present-user auth forwarding — the request's own headers. Per call, never * bound: request-lifetime authority does not outlive the request. */ headers?: Record | Headers; /** Continue a conversation this subject already owns instead of starting one. * Omit it for a new thread; see {@link openingThread} for why passing an * explicit `undefined` is refused. */ threadId?: string; signal?: AbortSignal; } /** The composition ONE turn runs on. */ export interface TurnDeps { /** The brain, with its knobs already bound. */ harness: Harness; store: VendoStore; guard: Guard; /** Where workspace blobs land; unset → the store's own rows. */ files?: FilesAdapter; /** Projected into the read-only `/host/skills` mount, as in a session. */ skills?: readonly Skill[]; /** Per-user memory. Declared here because `resolveSystem` below reads it off * this object to fill `[Memory]`: undeclared, the block worked only as long * as the caller happened to pass a wider object. */ memory?: MemoryAdapter; /** The host's prompt block. */ instructions?: string; /** * The turn's system prompt, for a composition that already has one. Handed this * package's own assembly (`instructions`, the ctx's situation data, and the * guard's directions); a returned string is used verbatim, `undefined` is that * default — never a promptless turn. * * It exists because the prompt is VENUE-GATED and carries the guard's * directions, so it needs the ctx: the umbrella assembles a chat turn's brief * per turn, and an away firing that thought with a different brief than a chat * turn would be a second agent wearing the same name. */ system?: SystemPromptHook; /** The seats a harness that does NOT bring its own brain reads (`vendo()`). */ models?: SeatModels; liveTurn?: HarnessRuntimeDeps["liveTurn"]; } /** What `agent()` composed, as {@link startTurn} needs it: the agent's own name * and tool surface, the two gates a turn clears before it opens anything, and * the guard in its full form — deciding an approval on resume is a * `VendoGuard` verb. */ export interface AgentDeps extends TurnDeps { /** Attribution when the caller names no subject of their own. */ name: string; /** * WHICH agent this is — `agent({ name })`, carried by the composition rather * than read off {@link AgentDeps.name}, which is a label whoever assembles * these deps fills in. * * It rides every turn's ctx onto the rows that turn parks, and it is the axis * `turns.list`/`turns.resume` filter on (interruptions.ts). Two agents over * one store share one approvals collection — `serve({ agents: [a, b] })` is * exactly that — and a park named only the subject, the thread and the turn, * so a person's yes to `ops` dispatched `support`'s same-named tool and * `support.turns.list()` returned `ops`' turn verbatim. * * Optional because a composition assembled by hand names no agent; absent, * its parked turns belong to nobody and neither face offers them. */ agent?: string; /** The agent's guard-bound registry — the turn's whole tool surface. */ tools: ToolRegistry; guard: VendoGuard; /** Awaited before the turn opens anything — `agent()`'s model check, so a turn * with no model fails for the same reason `respond()` does, and writes no * thread on the way. */ assertModel?: () => Promise; /** A loopback door still binding its port, exactly as `createSession` awaits * it (session.ts). Without this a `claudeCode()` turn can start while the * door's origin is still undefined, and the box dials a URL that is not * there yet. */ doorReady?: Promise; } /** * The thread this call MEANT. * * Omitted is a new conversation. Present and explicitly `undefined` is a * mistake, and it is refused rather than quietly forked: `{ threadId: * thread?.id }` on a value that was not there is the one way a caller loses a * conversation without ever being told. Detected with `in`, because `undefined` * and absent are the two cases that have to be told apart. * * It THROWS where a foreign id rejects, and the difference is the point: a * malformed call is wrong before anything opens, an unowned thread is a lookup * that failed. */ export declare function openingThread(options: { threadId?: string; }): { threadId: ThreadId; reopen: boolean; }; /** One entry per call the harness attempted, in order, each carrying the last * thing known about it. */ export interface RecordedCall { call: ToolCall; outcome: ToolOutcome["status"]; } /** What ONE turn left behind, before either face shapes it: {@link startTurn} * reads it into a {@link TurnResult}, `awayRunner` into core's * `AgentRunReport`. */ export interface TurnRecord { /** The assistant's OWN words for this turn, in full — never a narrowing. */ text: string; toolCalls: RecordedCall[]; usage: UsageTotals; /** Every approval this turn parked, in the order the guard minted them. */ parked: ApprovalRequest[]; /** Something outside the turn called time on it. */ stopped?: "aborted" | "maxToolCalls"; /** The turn broke; the harness's own sentence for it when it gave one. */ failed?: { message?: string; }; /** Present only when `output` was asked for AND the model filled it in. */ output?: unknown; } /** What one turn needs beyond the composition it runs on. */ export interface TurnInput { prompt: string; /** The whole tool surface for this turn — guard-bound already. */ tools: ToolRegistry; ctx: RunContext; threadId: ThreadId; turnId: TurnId; /** The id came from the caller: reopen it (ownership-checked) rather than mint. */ reopen: boolean; maxToolCalls: number; signal?: AbortSignal; output?: FlexibleSchema; emit?: (event: RunEvent) => void; /** Interruptions a caller answered, settled before the model thinks again. The * guard rides along because deciding an approval is a `VendoGuard` verb while * a turn's composition is typed on core's narrower `Guard`. */ resume?: { guard: VendoGuard; parked: readonly ApprovalRequest[]; decisions: Decisions; }; } /** What a caller with no budget gets. The automations engine always passes its * own (50), so this only bounds a host driving a turn directly. */ export declare const DEFAULT_MAX_TOOL_CALLS = 20; /** Past its TTL, the ask is dead whether or not a sweep has been by. Nothing in * this package sweeps (the umbrella's `compose-sweep.ts` is the only caller of * `sweepExpiredApprovals`), so expiry is decided where it is read: a turn a * host composed without a sweeper still keeps the seven-day promise, and it * keeps it with a sentence instead of a silence. */ export declare const expired: (request: ApprovalRequest, ttlMs: number, at: number) => boolean; /** ONE harness turn — everything both venues share. */ export declare function runTurn(deps: TurnDeps, input: TurnInput): Promise; /** * Start one turn and hand back the object it runs behind. * * The gates a turn clears before it opens anything are here, in the one place a * turn begins: the model check, and a door still binding its port — * `createSession` awaits the same two. */ export declare function startTurn(deps: AgentDeps, input: Omit): Turn; /** * `agent.chat(message)` — one turn of a conversation, for code that wants the * answer rather than a stream. * * No output schema by design: a chat turn's answer is what the assistant SAID. * `run()` is the lane with a declared shape. */ export declare function startChat(deps: AgentDeps, message: string, options?: ChatOptions): Turn;