import type { SessionTokenBudgetSummary } from '../../store/budget/index.js'; import type { ActivityStatus, ActivityType } from '../activity/index.js'; import type { BaseAgentResult } from '../agent/base.js'; import type { CostInfo, PlatformError, TokenUsage } from '../common/index.js'; import type { CheckpointId, ToolCallSummary } from '../hitl/index.js'; import type { ActivityId, MessageId, PlanId, PluginId, SandboxId, SessionId, TaskId, ToolUseId, TurnId } from '../ids/index.js'; import type { Message } from '../message/index.js'; import type { PlanStep } from '../plan/index.js'; import type { PluginHookEvent, PluginHookResult } from '../plugin/index.js'; import type { TaskStatus } from '../task/index.js'; import type { ToolResultView } from '../tool/presentation.js'; import type { CancelCause } from './cancel-cause.js'; import type { FencingToken } from './durable.js'; import type { Lineage } from './lineage.js'; import type { MessageStopReason, StopReason } from './stop-reason.js'; import type { SubSessionKind } from './sub-session.js'; import type { Origin, TurnBudgetBinding, TurnConfigSnapshot, TurnSettlement } from './turn.js'; /** * Live events of a session: the stream a host listens to while a turn runs. * * The model is session → turn → message. Every event names the session it * belongs to (`sessionId`) and, when it happened inside a turn, the turn * (`turnId`). Events that can only happen inside a turn declare `turnId` * required; the ones a host can cause between turns (a manual compaction, a * background job exiting, a policy swap, a session hook, task and sandbox * bookkeeping) declare it optional, and an absent `turnId` means "outside any * turn". * * Every event whose type {@link isEphemeralEvent} does not name is also * appended to the session log as a record of the same `type`, with its * payload minus `sessionId`, `turnId` and `lineage` (those live on the record * envelope, or follow from `session_started.parent`). The record schema is in * `./records.ts` and `docs/sdk/session-log.md`. */ export interface SessionEventEnvelope { /** Session record schema version; see `SESSION_RECORD_SCHEMA_VERSION`. */ v?: 1; /** * Delegation linkage, present on events relayed from a child session to a * listener of its parent. `depth` is 0 at the root session. */ lineage?: Lineage; /** * This event's position in its session's log, from 1. * * Present means recorded: the writer appends the record first and only then * hands the event to the live stream, so a `seq` a consumer sees is one the * log contains, and it works as a reconnect cursor. Absent means not * recoverable: an ephemeral event, an event whose append failed (still * delivered, because losing the news of a failure is worse than delivering * it without a cursor), or an event relayed from a child session, whose * position is in the child's own log. * * Per session, not per stream: a listener that also receives child events * keeps one cursor per `sessionId`. */ seq?: number; /** * The lease fence the session log was written under (the record's `gen`). * A takeover raises it, which makes a cursor from before the takeover * detectable rather than silently wrong. */ generation?: FencingToken; } type CoreSessionEvent = { type: 'tool_calls_admitted'; sessionId: SessionId; turnId: TurnId; kind: 'initialize' | 'batch' | 'nested' | 'retry'; /** Newly reserved attempts; zero only for ledger initialization. */ count: number; /** Cumulative reserved attempts, including abandoned reservations. */ used: number; limit: number; } /** * A turn began. The prompt that opened it is the `message` record named by * `userMessageId`, appended right after this one and inside the turn. */ | { type: 'turn_started'; sessionId: SessionId; turnId: TurnId; userMessageId: MessageId; systemPrompt?: string; config: TurnConfigSnapshot; /** Which protocol and which caller-side ids opened the turn. */ origin?: Origin; /** The token ledger this turn spends from, keyed by the root turn. */ budget?: TurnBudgetBinding; } | { type: 'iteration_started'; sessionId: SessionId; turnId: TurnId; iteration: number; } /** * Who answers when this turn asks a human, changed mid-turn. * * The policy used to be a closure captured at `query()` start, so * changing it meant ending the turn. Now it is a value a host can swap — * and a swap that left no trace would be the worst version of that: an * incident review would see approvals with no way to tell which rule * granted them. * * Names, not handlers. A durable log cannot hold a function, and * `[Function (anonymous)]` is what a log says when somebody tries. */ | { type: 'approval_policy_changed'; sessionId: SessionId; turnId?: TurnId; from: string; to: string; reason: string; } /** * What the model was actually asked, when it changed. * * `turn_started` records a system prompt once, and tool schemas never * reached the transcript at all — yet the effective envelope changes * mid-turn: `prepareStep` rewrites the system text, narrows the tool * list, or swaps the model, and a step's skills ride an ephemeral * trailing system message. So a transcript showed one prompt and a turn * that had asked several different questions. * * Emitted only when the tuple DIFFERS from the last one recorded. The * common case — nothing changed — costs one hash and no event, because * a per-iteration copy of an unchanged system prompt is the fastest way * to make a durable log too large to read. */ | { type: 'request_envelope'; sessionId: SessionId; turnId: TurnId; iteration: number; model: string; /** Leading system messages plus this step's preamble, concatenated. */ systemPrompt: string; toolNames: readonly string[]; /** * Stable hash over the sorted tool schemas. A name list cannot see * a tool whose SCHEMA changed while its name did not — which is * the change most likely to alter what the model does and least * likely to be noticed. */ toolSchemaDigest: string; } | { type: 'iteration_completed'; sessionId: SessionId; turnId: TurnId; iteration: number; hasToolCalls: boolean; } /** * What a compaction pass removed, recorded before it is gone. * * Emitted BEFORE the context is replaced, and appended to the session log * with the pass's `compaction` record, so the shed messages stay in the log * for audit, search and undo even though the next request no longer * carries them. The fold that builds the context reads the `compaction` * record (its summary and kept message ids), not this event. * * Carries whole message bodies, tool output included, which is why both * external wire mappers decline it. */ | { type: 'compaction_shed'; sessionId: SessionId; turnId?: TurnId; iteration: number; /** Exactly the messages the pass removed, in their original order. */ messages: Message[]; /** Automatic threshold, provider rejection, or a host-requested pass. */ reason: 'threshold' | 'overflow' | 'manual'; } | { type: 'compaction_completed'; sessionId: SessionId; turnId?: TurnId; iteration: number; /** Messages before and after the pass. */ messagesBefore: number; messagesAfter: number; /** Context size in tokens before and after. */ tokensBefore: number; tokensAfter: number; /** Whether `tokensBefore` came from the provider or a heuristic. */ measuredBy: 'provider' | 'estimate'; /** The window the trigger measured against, and where it came from. */ contextWindowTokens: number; windowSource: 'config' | 'provider' | 'model-table' | 'default'; /** * False when the pass could not get the context below * `resetThreshold` — the trigger is still armed, and a host may * want to surface that the turn is running tight. */ reachedResetThreshold?: boolean; } /** * Oversized tool results were emptied instead of the history being * summarized. * * This is the most common context-relief path and it was the only one * that emitted nothing. It edits the conversation the model sees — * `tool_result` bodies are replaced, irrecoverably — so a host reading * the session log saw results it no longer has and no record of why. * The two summarization outcomes were both on the wire; the cheap one * that runs far more often was not. * * Emitted on BOTH branches. `reliefWasEnough: false` means the clear * happened and was insufficient, so a full summarization followed and a * `compaction_completed` is coming — the history took two edits, not * one, and a reader that only saw the second would misattribute the * first. */ | { type: 'compaction_tool_results_cleared'; sessionId: SessionId; turnId?: TurnId; iteration: number; /** How many `tool_result` bodies were emptied. */ clearedCount: number; /** Assistant narrations cut to their first sentence by the salience pass, when it ran. */ stubbedCount?: number; /** Characters removed, summed across those results. */ charsReclaimed: number; /** `charsReclaimed` as tokens, by the same estimate the trigger uses. */ reclaimedTokens: number; /** * Whether the clear alone brought the context back under * `triggerThreshold`. `false` means summarization ran afterwards. */ reliefWasEnough: boolean; } /** * A compaction pass ran and shed nothing, so the history is unchanged. * * A shed that did not happen is exactly as consequential as one that did, * and until this existed only one of them was on the wire. The three * decline paths all reached a log line — and a host that silences its * logger, which every command-line entry point does, made a failed * compaction invisible to the user, to the host AND to the model. The turn * then continued at full context toward a provider rejection several turns * later that named none of this. * * The history is guaranteed untouched on every one of these: the reducer's * result is installed whole or not at all, so there is no partial state to * reason about. That is the property that makes reporting sufficient and a * repair unnecessary. */ | { type: 'compaction_failed'; sessionId: SessionId; turnId?: TurnId; iteration: number; /** * Which decline path was taken. These want different responses, so * a single "it failed" would put the reader back where the silence * did: * * - `reducer_threw` — the reducer raised. Usually a bug or a failed * model call inside a summarising reducer; the next pass may work. * - `shed_nothing` — it returned no fewer messages than it was * given. The history is already at its floor, or the reducer's * own threshold disagrees with the trigger's, and every later * pass will decline identically. * - `split_tool_pair` — its result separated a `tool_use` from its * `tool_result`, so it was refused wholesale rather than sent to * a provider that rejects the pairing. A reducer bug, and one * `findSafeTrimIndex` exists to prevent. */ cause: 'reducer_threw' | 'shed_nothing' | 'split_tool_pair'; /** Unchanged, and stated so a reader need not infer it. */ messages: number; /** Present only for `reducer_threw`. */ error?: string; } | { type: 'tool_executing'; sessionId: SessionId; turnId: TurnId; toolUseId: ToolUseId; toolName: string; input: unknown; /** * Present when another TOOL dispatched this call, rather than the * model. * * `run_code` is the reason: a program it runs calls tools in a * loop, and those calls went through `registry.execute` directly — * so they reached the permission gate and reached the event stream * not at all. A turn whose transcript showed one `run_code` call and * nothing about the eleven writes it performed is a transcript * that cannot be audited. * * Named rather than merely present, and this is the load-bearing * part: without it a consumer counting tool calls double-counts — * the parent AND each child — and a consumer rendering a timeline * draws eleven siblings where there is one call with eleven * children. */ via?: { readonly tool: string; readonly toolUseId: ToolUseId; /** The code runtime's request id, when that was the dispatch source. */ readonly runtimeToolCallId?: string; }; } /** * A tool saying how far along it is. * * Ephemeral — never appended to the session log, like `text_delta`. It is * for a host rendering a live view, not part of the conversation, and a * chatty tool must not be able to bloat the durable record. * * Tools get a deadline of up to two minutes by default, so before this * a build, a test run or a long fetch was simply silent for its whole * duration: the host could show that a tool had started and then nothing * at all until it either finished or timed out. The model never sees * these; they answer "is it still working?", which is a question only a * human asks. It is latest state rather than a lossless log: the executor * bounds each UTF-8 message and coalesces intermediate updates when the * live consumer falls behind. Complete output belongs to the terminal * `tool_completed` result. */ | { type: 'tool_progress'; sessionId: SessionId; turnId: TurnId; toolUseId: ToolUseId; toolName: string; /** Human-readable, e.g. "compiled 40/120 files". */ message: string; /** Optional completion in [0,1] when the tool genuinely knows it. */ fraction?: number; } /** Provider-executed activity, retained as evidence rather than a local tool request. */ | { type: 'hosted_tool'; sessionId: SessionId; turnId: TurnId; iteration: number; tool: NonNullable; } /** * A model call failed transiently and is being retried after a backoff. * * Answers the same question `tool_progress` answers — "is it still * working?" — for the other half of a turn's wall clock. With the default * policy, or a server-directed delay up to the cap, a turn can sit silent * for the better part of a minute between `iteration_started` and the * next event. A host saw literally nothing and no keepalive, so a * backoff was indistinguishable from a hang and a watchdog would cancel * a turn that was about to succeed. * * Emitted before the sleep, so the delay it names is the one still * ahead. */ | { type: 'provider_retry'; sessionId: SessionId; turnId: TurnId; iteration: number; /** 1-based attempt that just failed. */ attempt: number; maxRetries: number; delayMs: number; /** Classified failure code, as the boundary classifier reports it. */ code: string; status?: number; /** The delay came from the server's own `Retry-After`. */ serverDirected: boolean; } /** * A member of the provider chain could not serve, and a later member has * taken over. The turn continues from where it stopped. * * This event is the feature's honesty. A chain that swapped silently would * produce a turn that succeeded while quietly not doing what the operator * asked — served by a provider they did not choose, at a price and a * quality they did not agree to, with nothing in the transcript saying so. * A host is expected to SHOW this, not log it. * * Emitted at the moment of the swap, before the replacement request runs. */ | { type: 'provider_fallback'; sessionId: SessionId; turnId: TurnId; iteration: number; /** 0-based position in the chain, as the host declared it. */ fromIndex: number; fromProviderId: string; fromModel?: string; toIndex: number; toProviderId: string; toModel?: string; /** Classified failure code, as the boundary classifier reports it. */ code: string; status?: number; /** The classified failure's own sentence. */ reason: string; } | { type: 'tool_completed'; /** Bounded result view; omitted when output was overridden or truncated. */ presentation?: ToolResultView; sessionId: SessionId; turnId: TurnId; toolUseId: ToolUseId; toolName: string; result: string; isError: boolean; /** See {@link tool_executing}'s `via`. Carried on both, so a * consumer can pair them without holding the start event. */ via?: { readonly tool: string; readonly toolUseId: ToolUseId; readonly runtimeToolCallId?: string; }; /** * Wall-clock the tool took. Computed since the first version of * the executor but only ever logged; a host asking "which tool * was slow" had to reconstruct it from event timestamps. */ durationMs?: number; /** * Size of the tool's output BEFORE the model-visible budget was * applied, so a host can report "returned 2.1 MB" even though * `result` is a preview. */ outputLength?: number; /** True when `result` is a preview rather than the whole output. */ outputTruncated?: boolean; /** Where the full output was written, when it was spilled. */ outputSpillPath?: string; /** SHA-256 of the chunk manifest captured with the retained text. */ outputSpillIntegrity?: string; } /** * A tool asked the user a question and the turn is parked on it. * * The question used to park through the raw handler under a synthetic * checkpoint id that was never written, so a remote host could not * observe it at all — the in-process callback was the only channel, and * a tool review with the same shape had an event, a bridge mapping and * a durable record. This is that surface, for the other kind of park. */ | { type: 'user_question_asked'; sessionId: SessionId; turnId: TurnId; checkpointId: CheckpointId; /** The asking `tool_use_id`, so an answer can be matched back. */ questionId: string; question: string; } /** * The question was resolved. * * `answered: false` covers a decline and a non-response. Distinguished * because the asking tool refuses to invent consent from either, and a * host rendering the card needs the same distinction. */ | { type: 'user_question_answered'; sessionId: SessionId; turnId: TurnId; checkpointId: CheckpointId; /** * Which question, when the resolution named one. * * Its sibling `user_question_asked` carries this and the answer * did not, so a client that keyed on the question id — the * natural key, since it is what routes an answer back on resume * — could not match the two halves without also having stored * the checkpoint id. Absent when the pause was resolved without * an answer. */ questionId?: string; answered: boolean; } | { type: 'tool_review_requested'; sessionId: SessionId; turnId: TurnId; toolCalls: ToolCallSummary[]; iteration: number; } | { type: 'tool_review_completed'; sessionId: SessionId; turnId: TurnId; decision: 'approved' | 'modified' | 'rejected'; } | { type: 'checkpoint_created'; sessionId: SessionId; turnId: TurnId; checkpointId: CheckpointId; iteration: number; } /** * The turn parked at a checkpoint. This ends a segment of the turn and is * NOT terminal: the turn stays the session's active turn until * `resumeSession` continues it (same `turnId`) or `abandonTurn` closes it. */ | { type: 'turn_paused'; budget?: SessionTokenBudgetSummary; sessionId: SessionId; turnId: TurnId; checkpointId: CheckpointId; reason: string; /** * The same structured failure projection a terminal `turn_failed` * carries. A pause is a different verdict, not a less informative one: * the retryability and any provider-directed delay are what let a host * decide when and how to resume this checkpoint. */ failure?: PlatformError; /** First-hand driver classification, when the provider produced one. */ providerError?: import('../provider/error.js').ProviderErrorInfo; /** Curated operator copy, absent when no catalog rule matched. */ explanation?: { id: string; message: string; hint: string; }; } /** A paused turn continues from its checkpoint, under the same `turnId`. */ | { type: 'turn_resuming'; sessionId: SessionId; turnId: TurnId; fromCheckpointId: CheckpointId; /** The decision whose answer released the park, when one did. */ resolvedDecisionId?: string; } /** * A guardrail blocked or rewrote the turn's input or answer. * * Emitted so a host can show WHY a turn refused, and — for a rewrite — * so a consumer that already rendered `text_delta` events knows the * text it displayed has been corrected. */ | { type: 'guardrail_triggered'; sessionId: SessionId; turnId: TurnId; stage: 'input' | 'output'; action: 'block' | 'rewrite'; guardrail?: string; reason?: string; } /** * A background job this session (or the owner it runs under) started has * ended. The model learns it from a notice on its next tool result; * the host learns it from this, whether or not a turn is running. */ | { type: 'background_job_exited'; sessionId: SessionId; turnId?: TurnId; jobId: string; command: string; status: 'exited' | 'killed'; exitCode?: number; signal?: string; } /** * What the turn learned was written to the host's memory store: its * decisions, discoveries and failures as one entry a later session can * search for. Emitted only when a host asked (`consolidateInto`) and * the turn had something to say. */ | { type: 'memory_consolidated'; sessionId: SessionId; turnId?: TurnId; memoryId: string; title: string; decisions: number; discoveries: number; failures: number; } /** * The turn reached its end without throwing. * * `completed` is not `succeeded`. A turn stopped by its token budget, its * timeout, its iteration cap, a cancellation or a blocking output guardrail * all arrive here — `turn_failed` is emitted only from the throw path — so a * consumer that treated this event as success reported one for a turn whose * answer was refused. `stopReason` and `settlement.status` separate them. * * `result` is the authoritative answer, after guardrail, review, * outstanding-work and structured-output overrides. When it differs from the * text of the turn's last assistant message, a `message_replaced` record is * appended first, so every fold of the log shows this answer and never the * raw one (`settlement.resultSource` says which override applied). */ | { type: 'turn_completed'; budget?: SessionTokenBudgetSummary; sessionId: SessionId; turnId: TurnId; result: string; stopReason?: StopReason; /** * Present only on a cancellation whose origin was recorded. Absent * is a real answer: a cancellation nobody attributed is not a user * cancellation, and defaulting to one would put a confident wrong * value where an honest gap belongs. */ cancelCause?: CancelCause; settlement: TurnSettlement; } /** * The turn failed. Terminal, like `turn_completed`. * * `failure.code` names machine-readable causes the session log itself * produces: `interrupted` (closed by `beginTurn({ abandonInterrupted })` * after a crash) and `abandoned` (closed by `abandonTurn`). * * `error` is the flattened message, kept for every consumer that only * ever rendered a string. `failure` is the structured projection, and * it is the point: namzu already classifies at the provider boundary — * over status, errno, `Retry-After` and the whole cause chain — so a * fully-populated error genuinely arrived here and was flattened one * line later, discarding `code`, `status`, `retryAfterMs`, `retryable` * and `details`. * * The damage was self-inflicted downstream: one consumer substring- * matched the flattened message to decide whether an error had * occurred, and the iteration loop re-ran the classifier to recover * structure that had already been computed upstream. */ | { type: 'turn_failed'; budget?: SessionTokenBudgetSummary; sessionId: SessionId; turnId: TurnId; error: string; failure?: PlatformError; /** * The driver's own classification, when it produced one. Carried * beside `failure` rather than folded into it: this is the * provider's first-hand statement, and a consumer deciding whether * to retry reads it directly. */ providerError?: import('../provider/error.js').ProviderErrorInfo; /** * Operator-facing explanation, when a catalog rule claims this * failure: a stable `id` to grep for, and `hint` saying what to * change. Absent when no rule matched — inventing advice for an * uncharacterised failure is worse than saying nothing, because * it sends the reader somewhere specific and wrong. */ explanation?: { id: string; message: string; hint: string; }; settlement: TurnSettlement; } | { type: 'capability_warning'; sessionId: SessionId; turnId?: TurnId; capability: 'tools' | 'vision' | 'documents'; /** Present when the mismatch was produced after a tool executed. */ contentSource?: 'tool-result'; providerId: string; message: string; } /** * Provider-invalid tool history was repaired before the first model call. * * Counts, not content: a host can surface and audit the rewrite without * copying tool output or conversation secrets into its event channel. * `fresh-history` names caller-supplied history. `abandoned-checkpoint` * excludes any incomplete turn still owned by a durable pending/recovered * resume plan; that turn is completed by its authority path instead. */ | { type: 'message_history_repaired'; sessionId: SessionId; turnId: TurnId; source: 'fresh-history' | 'abandoned-checkpoint' | 'provider-rejected-image'; duplicateToolResultsRemoved: number; orphanedToolResultsRemoved: number; syntheticToolResultsInserted: number; /** Exact number of durable image occurrences withheld from later requests. */ providerRejectedImagesSuppressed?: number; } | { type: 'token_usage_updated'; /** Aggregate tree spend, distinct from this turn's own usage. */ budget?: SessionTokenBudgetSummary; sessionId: SessionId; turnId: TurnId; usage: TokenUsage; cost: CostInfo; /** * How large the CONTEXT is right now, and how large it may get. * * These are a different quantity from `usage` beside them and the * distinction is the whole reason they are named this explicitly. * `usage` is CUMULATIVE SPEND over the turn: prompt plus completion * tokens summed across every turn, monotonically increasing, and * untouched by compaction. `contextTokens` is the size of the * conversation being sent right now, which falls when a compaction * sheds. * * Dividing the first by a context window is a category error, and * it is one this estate shipped: a host did exactly that, so its * indicator climbed toward full on any long session no matter how much * room the conversation actually had — most wrong precisely when * someone needed it most. The numbers are here so nobody has to * reach for the wrong one, and named so reaching for it is a * visible mistake rather than a plausible guess. * * `contextMeasuredBy` says whether the provider counted the prompt * or we estimated it, and `windowSource` where the ceiling came * from. A fraction of two numbers is only as honest as the weaker * of them, and a surface rendering these owes a reader the same * distinction rather than presenting an estimate as a measurement. * * Absent when the turn has no compaction configuration, because * nothing then resolves a window and inventing one would be the * guess this exists to replace. * * This is a state snapshot, not merely a receipt for a model call. * It is also emitted immediately after an automatic context edit: * cumulative `usage`/`cost` may be unchanged (or may include a * verifier call) while `contextTokens` falls to its post-edit * estimate. Hosts therefore need not wait for another provider * response to learn that compaction made room. */ contextTokens?: number; contextMeasuredBy?: 'provider' | 'estimate'; contextWindowTokens?: number; windowSource?: 'config' | 'provider' | 'model-table' | 'default'; } | { type: 'activity_created'; sessionId: SessionId; turnId: TurnId; activityId: ActivityId; activityType: ActivityType; description: string; } | { type: 'activity_updated'; sessionId: SessionId; turnId: TurnId; activityId: ActivityId; status: ActivityStatus; output?: unknown; error?: string; } | { type: 'plan_ready'; sessionId: SessionId; turnId: TurnId; planId: PlanId; title: string; steps: PlanStep[]; summary?: string; } | { type: 'plan_approved'; sessionId: SessionId; turnId: TurnId; planId: PlanId; } | { type: 'plan_rejected'; sessionId: SessionId; turnId: TurnId; planId: PlanId; reason?: string; } | { type: 'plan_step_updated'; sessionId: SessionId; turnId: TurnId; planId: PlanId; stepId: string; status: PlanStep['status']; } /** * The plan is over, and it went the way it was supposed to. * * The plan events used to stop before the outcome: `plan_ready`, * `plan_approved`, `plan_rejected` and `plan_step_updated` all reached the * wire, and the two terminal ones were folded into a bare `break` in the * translator. So a host watching the stream saw the steps report and then * silence — it could tell a plan had been approved and never that it * closed, which leaves a plan rendered as in-flight forever. * * Found by the first live end-to-end run rather than by a test, and the * reason is worth keeping: the tests read the outcome off `PlanManager` * through `onContextCreated`, so they proved the plan settled without ever * asking whether a consumer of the EVENT STREAM could see it. */ | { type: 'plan_completed'; sessionId: SessionId; turnId: TurnId; planId: PlanId; } /** * The plan is over and it did not finish. * * `reason` is the text handed to `failPlan`, which used to be discarded — * an event that says "failed" without saying why puts the reader back * where the missing event did. */ | { type: 'plan_failed'; sessionId: SessionId; turnId: TurnId; planId: PlanId; reason?: string; } | { type: 'agent_pending'; sessionId: SessionId; turnId: TurnId; taskId: TaskId; parentAgentId: string; childAgentId: string; depth: number; /** Approved plan edge carried while the blocking tool is still live. */ planId?: string; planStepId?: string; /** * How the host that delegated this child wants it GROUPED on screen — * a shared label over a set of related delegations, typically one * operator-visible piece of work several children are doing together. * * These fields are display annotations only; they do not create * dependencies, barriers, or serial execution. Nothing in the kernel * reads them: admission, ordering and concurrency come from the * scheduler and from {@link planId}/{@link planStepId}, which is the * field pair that DOES carry correlation a host may act on. A reader * who infers execution structure from a label here has inferred it * from a caption. * * Absent unless the delegating host supplied them, which is the * normal case — a host that groups nothing sends nothing, and a * consumer written before these existed reads the same event it * always did. * * They ride this event rather than staying in the delegating * process's memory for REACH: a consumer watching from outside * that process — another listener, or an SSE client — can rebuild * the same picture instead of seeing an undifferentiated list of * children. * * Reach is not durability, and this event buys only the first. * Like every delegation lifecycle event, it is handed straight to * a host's listener and never enters a session log — which is what * the absent `seq` on this variant says, and what the `seq` doc * above spells out. A label here is therefore written nowhere by * the kernel and does not survive a restart of the host that chose * it; a host wanting the grouping to outlive its process records it * from the listener. */ workflow?: string; /** * Display group WITHIN {@link workflow} — a stage of that work, as * the delegating host labelled it. Display-only on the same terms as * {@link workflow}: it creates no dependencies, barriers or serial * execution, and two children naming the same phase are not thereby * sequenced or synchronised. */ phase?: string; /** * Longer text explaining {@link phase}, for a surface that has room * to show it. Display-only on the same terms as {@link workflow}. */ phaseDetail?: string; /** * Where {@link phase} sits in the host's intended DISPLAY order, * zero-based. Display-only on the same terms as {@link workflow}: it * orders a list on a screen and orders nothing that runs. Children in * one phase are expected to carry the same value; a consumer that * sees two disagree should keep the first rather than resequence, * because nothing here is authoritative enough to arbitrate. */ phaseOrder?: number; } | { type: 'agent_completed'; sessionId: SessionId; turnId: TurnId; taskId: TaskId; result: BaseAgentResult; } | { type: 'agent_failed'; sessionId: SessionId; turnId: TurnId; taskId: TaskId; error: string; } | { type: 'agent_canceled'; sessionId: SessionId; turnId: TurnId; taskId: TaskId; /** Same value the child's own `turn_completed` carries, so the two * sides of one cancellation agree rather than being correlated * by timing. */ cancelCause?: CancelCause; } | { type: 'task_created'; sessionId: SessionId; turnId?: TurnId; taskId: TaskId; subject: string; status: TaskStatus; /** * What this unit waits on, and who claims it. * * The store maintains a full dependency graph — `blocks` and * `blockedBy` are mirrored on both ends, written under a lock, and * deadlock-avoided — and none of it reached the wire. So a host * could show a flat list of units and nothing about their order, * while the model was already maintaining the order. * * Absent rather than empty when the unit depends on nothing, so a * reader can tell "no dependencies" from an emitter that predates * these fields. */ blockedBy?: readonly TaskId[]; owner?: string; } | { type: 'task_updated'; sessionId: SessionId; turnId?: TurnId; taskId: TaskId; subject: string; status: TaskStatus; owner?: string; /** See `task_created`. Carried on updates because an edge can be added later. */ blockedBy?: readonly TaskId[]; } | { type: 'plugin_hook_executing'; sessionId: SessionId; turnId?: TurnId; pluginId: PluginId; hookEvent: PluginHookEvent; } | { type: 'plugin_hook_completed'; sessionId: SessionId; turnId?: TurnId; pluginId: PluginId; hookEvent: PluginHookEvent; result: PluginHookResult; } | { type: 'sandbox_created'; sessionId: SessionId; turnId?: TurnId; sandboxId: SandboxId; environment: string; } | { type: 'sandbox_exec'; sessionId: SessionId; turnId?: TurnId; sandboxId: SandboxId; command: string; exitCode: number; durationMs: number; } | { type: 'sandbox_destroyed'; sessionId: SessionId; turnId?: TurnId; sandboxId: SandboxId; } | { type: 'message_started'; sessionId: SessionId; turnId: TurnId; iteration: number; messageId: MessageId; } /** * The model began emitting a reasoning block. * * Without these, extended thinking looked to a streaming UI like a * multi-second stall with no events at all — the turn was working, and * the host had no way to say so. */ | { type: 'reasoning_started'; sessionId: SessionId; turnId: TurnId; iteration: number; messageId: MessageId; blockIndex: number; reasoningType: 'thinking' | 'redacted_thinking'; } /** Ephemeral — never appended to the session log, like `text_delta`. */ | { type: 'reasoning_delta'; sessionId: SessionId; turnId: TurnId; iteration: number; messageId: MessageId; blockIndex: number; text: string; } | { type: 'reasoning_completed'; sessionId: SessionId; turnId: TurnId; iteration: number; messageId: MessageId; blockIndex: number; /** Present only when the provider returned readable thinking. */ text?: string; /** True when the block carried a signature that must be replayed. */ signed: boolean; } | { type: 'text_delta'; sessionId: SessionId; turnId: TurnId; iteration: number; messageId: MessageId; text: string; textPart?: Omit; } | { type: 'message_completed'; sessionId: SessionId; turnId: TurnId; iteration: number; messageId: MessageId; stopReason: MessageStopReason; usage?: TokenUsage; /** * Settled assistant text. When the provider supplies public text * phases, this selects explicit final-answer items; concatenating * raw deltas would also include intermediate commentary. All items * remain available in textParts. Without phases, ordinary delta * concatenation is unchanged. Stop reason still determines whether * this message finished, was cancelled or hit an output limit. */ content?: string; /** Ordered original items; content selects explicit final answers if supplied. */ textParts?: readonly import('../message/index.js').AssistantTextPart[]; } | { type: 'tool_input_started'; sessionId: SessionId; turnId: TurnId; iteration: number; messageId: MessageId; toolUseId: ToolUseId; toolName: string; } | { type: 'tool_input_delta'; sessionId: SessionId; turnId: TurnId; toolUseId: ToolUseId; partialJson: string; } | { type: 'tool_input_completed'; sessionId: SessionId; turnId: TurnId; toolUseId: ToolUseId; input: unknown; /** * True when the provider stream ended before the tool JSON * arguments closed. `input` stays a sanitized object so public * consumers never receive internal recovery sentinels. */ inputTruncated?: boolean; }; /** * A child session was created to do delegated work. Appended to the PARENT * session's log, inside the parent turn whose tool call spawned it. */ export interface ChildSessionSpawnedEvent { type: 'child_session_spawned'; sessionId: SessionId; turnId: TurnId; childSessionId: SessionId; /** The parent's tool call that spawned the child. */ toolCallId: ToolUseId; kind: SubSessionKind; description: string; /** The child's log, relative to the parent's session directory: `subagents/.jsonl`. */ path: string; /** * Display grouping a host asked for, for example one operator-visible piece * of work several children do together. Batches are derived from these * annotations; nothing executes differently because of them. */ batch?: { batchId: string; name: string; phase?: string; }; /** The token-budget account the child's turns spend from. */ budgetAccountId?: string; } /** * The child session appended a message. * * A child can outlive the parent turn that spawned it (`Turn.abandonedTaskIds` * names such workers), so `turnId` is the spawning turn only while that turn * is still open, and absent once it has closed. It never names a later turn: * the child belongs to the turn in its `child_session_spawned` record. */ export interface ChildSessionMessagedEvent { type: 'child_session_messaged'; sessionId: SessionId; /** The spawning turn while it is open; absent after it closed. */ turnId?: TurnId; childSessionId: SessionId; messageId: MessageId; } /** * The child session went idle: its current turn ended and nothing is queued. * `turnId` follows the rule on {@link ChildSessionMessagedEvent}. */ export interface ChildSessionIdledEvent { type: 'child_session_idled'; sessionId: SessionId; /** The spawning turn while it is open; absent after it closed. */ turnId?: TurnId; childSessionId: SessionId; } export type ChildSessionLifecycleEvent = ChildSessionSpawnedEvent | ChildSessionMessagedEvent | ChildSessionIdledEvent; /** * Discriminated union of every live session event: 62 type literals. * * `type` is the sole discriminator for exhaustive switches; envelope fields * are additive and never take part in discrimination. */ export type SessionEvent = (CoreSessionEvent & SessionEventEnvelope) | (ChildSessionLifecycleEvent & SessionEventEnvelope); export type { CoreSessionEvent }; export type SessionEventType = SessionEvent['type']; export type SessionEventListener = (event: SessionEvent) => void | Promise; /** * Event types whose volume makes durable persistence wasteful. They reach the * live stream and never the session log: the completed message, reasoning * block and tool input carry the same content once. */ export declare const EPHEMERAL_EVENT_TYPES: ReadonlySet; export declare function isEphemeralEvent(event: { readonly type: string; }): boolean; /** The event types a session log records: every literal except the ephemeral four. */ export type PersistedSessionEventType = Exclude; //# sourceMappingURL=events.d.ts.map