import type { TextBlock } from '../types/blocks.js'; import type { ContextEvidenceState } from '../types/context-evidence.js'; import type { FileEventRecord } from '../types/file-event-record.js'; import type { Message } from '../types/messages.js'; import type { Provider, Usage } from '../types/provider.js'; import type { SessionWriter } from '../types/session.js'; import type { TokenCounter } from '../types/token-counter.js'; import type { Tool } from '../types/tool.js'; import { ConversationState } from './conversation-state.js'; import type { RunEnv } from './run-env.js'; export interface TodoItem { id: string; content: string; status: 'pending' | 'in_progress' | 'completed'; activeForm?: string | undefined; /** When promoted from a plan item, stores the plan item's id. */ promotedFromPlan?: string | undefined; /** When promoted from a task, stores the task's id. */ promotedFromTask?: string | undefined; } export interface RunOptions { signal?: AbortSignal | undefined; model?: string | undefined; executionStrategy?: 'parallel' | 'sequential' | 'smart' | undefined; maxIterations?: number | undefined; /** * Enable autonomous continue for this specific run. When true, the agent * loop re-runs on `[continue]`/`[next step]`/`[proceed]` markers or * `continue_to_next_iteration()` tool calls instead of returning. * Overrides `AgentInit.autonomousContinue` for this call only. */ autonomousContinue?: boolean | undefined; } export interface ContextInit { systemPrompt: TextBlock[]; provider: Provider; session: SessionWriter; signal: AbortSignal; tokenCounter: TokenCounter; cwd: string; projectRoot: string; /** Mutable working directory. Defaults to `cwd`. Must stay within `projectRoot`. */ workingDir?: string | undefined; /** * When false, file tools and `setWorkingDir()` are confined to `projectRoot`. * Defaults to `false` (restrictive) when omitted so directly-constructed * contexts (tests, embedded callers) keep the safe behavior; the runtime * passes the config-derived value (default `true` — permissive) explicitly. */ allowOutsideProjectRoot?: boolean | undefined; model: string; tools?: Tool[] | undefined; /** Agent id performing this run (e.g. 'leader', 'executor', 'tech-stack'). */ agentId?: string | undefined; /** Human-readable agent name. */ agentName?: string | undefined; /** * Session-level trace ID for correlating storage events with agent * iterations in observability pipelines. Stored on the SessionWriter * so that storage operations can emit it in `storage.*` events. * When set, the Context constructor propagates it to * `session.traceId` automatically. */ traceId?: string | undefined; } /** * L1-A: `Context` is the live agent-run object. Its read-only environment * shape is exposed by the `RunEnv` interface (every field below the * conversation state) and its mutable shape by `ConversationState` (the * `state` accessor). New code should declare the narrower type at its * parameter — pass `ctx` for it. Existing tools that accept `Context` * still work because `Context` structurally satisfies both. * * The single source of truth for the project directory is `projectRoot`. * All tools (read/write/bash/exec) resolve paths relative to this. * Sessions, config, memory, and logs are also stored under this root. * * There IS a mutable `workingDir` (separate from `projectRoot`) that can be * changed at runtime via `setWorkingDir()`. It starts as `cwd` and allows * the agent and user to navigate within the project without spawning a new * process. All changes must stay inside `projectRoot`. */ export declare class Context implements RunEnv { messages: Message[]; todos: TodoItem[]; /** * Files whose content the **user / model has explicitly seen** via the * `read` tool (or an edit's auto-read, which surfaces the content to the * model). This is the set the permission policy's write-smart-bypass * (step 7) checks — writing a file the model has already read is treated * as "no new content to approve". It must NEVER contain files only touched * by `edit`/`write`, otherwise the model could repeatedly overwrite a file * whose content the user never saw (P1 #1, before-release.md). * * Tool-driven mutations record via `writtenFiles` + `recordRead(..., 'write')` * so mtime tracking still works without widening the bypass. */ readFiles: Set; /** * Files written by `edit`/`write` in this session. Tracked for observability * and to keep `readFiles` (the permission-bypass source of truth) clean. * `recordRead(path, mtime, 'write')` adds here instead of `readFiles`. */ writtenFiles: Set; fileMtimes: Map; /** * sha-256 (hex) of file content at the last recorded read/write, when the * recording tool had the content in hand. Used by `edit` as the authoritative * staleness arbiter: mtime comparison has a 2 s tolerance window on Windows * (FAT/NTFS granularity) during which an external modification is invisible, * and conversely a bare `touch` bumps mtime without changing content. Hash * equality resolves both cases exactly. Entries are dropped whenever a * hash-less `recordRead` observes a *different* mtime (content may have * changed under us — fall back to the mtime heuristic rather than trust a * stale hash). */ fileHashes: Map; /** * Structured side-effect records accumulated during the current run * (P2 #5). Populated by `recordSideEffect()` — read by /diag for an * in-memory audit trail without parsing the JSONL file. Cleared by * `clearFileTracking()` alongside read/written-file tracking. */ sideEffects: import('../types/side-effect.js').SideEffect[]; /** * Tracked file events for the current session. Populated by * `recordFileEvent()` — used for in-memory audit and real-time * subscription (EventBus `file.event`). Also persisted to session * JSONL as `file_event` events for durable storage. */ fileEvents: FileEventRecord[]; contextEvidence: ContextEvidenceState; systemPrompt: TextBlock[]; provider: Provider; session: SessionWriter; signal: AbortSignal; tokenCounter: TokenCounter; cwd: string; projectRoot: string; /** Mutable working directory — starts as `cwd`. Change via `setWorkingDir()`. */ workingDir: string; /** * When true, file tools (via `_util.ts`) and `setWorkingDir()` reject paths * outside `projectRoot`. When false, those boundary checks are bypassed so * tools may reach paths outside the project (still gated by permission * tiers). Mutable so `/settings` can toggle it live on the running session. */ allowOutsideProjectRoot: boolean; model: string; tools: Tool[]; meta: Record; /** Agent id performing this run (e.g. 'leader', 'executor'). */ agentId: string; /** Human-readable agent name. */ agentName: string; /** * Current kanban task ID, set by the agent/coordinator when working * on a specific kanban task. Tools use this via `recordFileEvent()` * to associate file operations with the active task. */ currentKanbanTaskId: string | undefined; /** * Current kanban board ID, paired with `currentKanbanTaskId`. */ currentKanbanBoardId: string | undefined; /** * Session-level trace ID for correlating storage events with agent * iterations. Stored here and also propagated to `session.traceId` * so storage operations can include it in `storage.*` events. */ traceId: string | undefined; /** Callbacks fired when `setWorkingDir()` changes the working directory. */ private _onWorkingDirChanged; /** * Set to true when the conversation gains new tool_use or tool_result * blocks — the only time repairToolUseAdjacency() can find new issues. * buildAndRunRequestPipeline() checks this flag to skip an O(n) scan * on iterations where no tool content was added (pure text responses). */ toolAdjacencyDirty: boolean; /** * Pending PostToolUse hook context text accumulated during tool execution * when `contextAs === 'separate'`. Instead of appending a standalone user * message (which would break tool-use/tool-result adjacency), the tool * executor stores the text here and agent-tools merges it as a leading * text block in the same user message that carries the tool_results. * Cleared after being consumed by agent-tools. */ pendingPostToolContext: string | undefined; /** * H1: pre-computed total-request token estimate from the most recent * `estimateRequestTokens()` call in the agent loop's pre-flight step. * The middleware that decides when to compact, the `emitContextPct` * helper that drives the live context-fill bar, and the pre-flight * itself all need this number; previously each one walked the same * messages/system/tools arrays independently. Stashing it here lets * the three call sites share a single compute per iteration. * * The value is the **uncalibrated** total. Callers that want the * calibrated number apply the per-(provider,model) ratio themselves. */ lastRequestTokens: number | undefined; /** * The provider's **authoritative** prompt-token count from the most recent * response — `effectiveInputTokens(usage)` = `input + cacheRead + cacheWrite`. * This is a REAL number, not an estimate. Paired with * `meta.realAnchorMsgCount` (the `messages.length` of the request that * produced it), it anchors the live context figure: the true count of * everything sent last turn, plus only an estimate of the messages appended * since. Undefined before the first response. See `realAnchoredInputTokens`. */ lastRealInputTokens: number | undefined; constructor(init: ContextInit); /** * Observable wrapper over the mutable conversation state. Lazy so * subsystems that don't subscribe pay nothing. Mutations made directly * on `ctx.messages` / `ctx.todos` are still visible through this * wrapper's read API (it holds a reference, not a copy) but only * mutations that go through `state.appendMessage()` etc. fire * `onChange`. New code should prefer the wrapper API. */ private _state; private readonly _conversationJournalQueue; private _conversationJournalBytes; private _conversationJournalDrain; private static readonly CONVERSATION_JOURNAL_MAX_EVENTS; private static readonly CONVERSATION_JOURNAL_MAX_BYTES; /** Wait until every exact conversation-state event queued so far is in the writer buffer. */ flushConversationJournal(): Promise; private conversationJournalBytes; private enqueueConversationJournal; private startConversationJournalDrain; get state(): ConversationState; /** * Register a teardown hook tied to the current run's abort signal. * Hooks registered before a run starts are stored and fired when the * next run ends; there is no immediate fire when no run is active. * * **Scope:** these hooks fire on the **whole agent run's** abort, not on * an individual tool call. For per-tool teardown of resources owned by * the tool author (child processes, handles), prefer `Tool.cleanup` — * see its JSDoc for the full rule. * * For hooks that must survive across run boundaries (mailbox heartbeat, * awareness polling, HQ publisher), prefer `registerAgentHook` instead. */ private abortHooks; registerAbortHook(fn: () => void | Promise): () => void; drainAbortHooks(): Promise; /** * Register a teardown hook that persists across individual run boundaries. * These hooks are NOT drained by `drainAbortHooks()` (which fires on every * run end). They are only released by `drainAgentHooks()`, intended to be * called during Agent teardown / process shutdown. * * Used for long-lived resources such as the mailbox heartbeat timer, * awareness polling interval, HQ publisher connection, and auto-compaction * timer — resources that must survive from the first run to the last. */ private agentHooks; registerAgentHook(fn: () => void | Promise): () => void; drainAgentHooks(): Promise; /** * Record that a file's content was seen / mtime was observed. * * `source` controls which tracking set is populated — and therefore whether * the permission policy's write-smart-bypass (step 7) will auto-approve a * subsequent `write` to this path: * * - `'user'` (default): the model/user saw the content (via `read`, or an * edit's auto-read that surfaced it). Adds to `readFiles` → bypass applies. * - `'write'`: a tool wrote the file (`edit`/`write`) and is recording the * new mtime so subsequent edits detect external modification. Adds to * `writtenFiles` only — the bypass does NOT apply, because the user never * approved the new content (P1 #1, before-release.md). * * `fileMtimes` is updated in both cases so mtime-based staleness checks work. * * `contentHash` (sha-256 hex of the exact content seen) is optional so * existing callers keep working. When provided it is stored in `fileHashes` * and becomes the authoritative staleness arbiter for later edits. When * omitted, a previously stored hash survives only if the observed mtime is * unchanged — a different mtime with no fresh hash means the content may * have changed, so the stale hash is dropped and staleness checks fall back * to mtime comparison. */ recordRead(absPath: string, mtimeMs: number, source?: 'user' | 'write', contentHash?: string): void; /** Clear accumulated file-read metadata after compaction or at boundaries * where stale read history could cause tools to skip legitimate re-reads. * The agent re-populates this naturally on the next file access. */ clearFileTracking(): void; /** * Record a structured side effect for the audit trail (P2 #5). * Called by tools that perform non-filesystem mutations (bash, install, * fetch) so /diag and session replay can show what the agent did beyond * file edits. * * Unlike recordFileChange(), this does NOT support undo — it is purely * for observability and audit. The event is appended to the session * JSONL fire-and-forget; errors are swallowed so recording never blocks * tool execution. */ recordSideEffect(sideEffect: import('../types/side-effect.js').SideEffect): void; /** * Set the current kanban task context for subsequent file operations. * Tools call this (or the agent loop sets it) so that `recordFileEvent()` * can associate operations with the active kanban task. * * Pass `undefined` for both to clear the task association. */ setCurrentKanbanTask(taskId: string | undefined, boardId?: string | undefined): void; /** * Record a comprehensive file event for the audit trail. * Persists the event as a `file_event` session JSONL event. * Consumers that need `file.event` EventBus events should subscribe * at the executor level (see `ToolExecutor`'s auto-emission). * * The `scope` field is auto-derived from `currentKanbanTaskId`: * - When a kanban task is set, scope = `'task'` and `taskId`/`boardId` * are populated from `currentKanbanTaskId`/`currentKanbanBoardId`. * - Otherwise scope = `'session'`. * * Fire-and-forget: errors are swallowed so recording never blocks * tool execution. */ recordFileEvent(input: { operation: 'create' | 'read' | 'update' | 'delete' | 'rename'; filePath: string; absPath: string; toolName: string; toolUseId: string; durationMs?: number | undefined; fileSize?: number | undefined; lines?: number | undefined; bytes?: number | undefined; }): void; /** * True if the model/user has explicitly seen this file's content via `read` * (or an edit auto-read). Tool-only writes (`source: 'write'`) do NOT count * — this is the source of truth for the permission policy's write bypass. */ hasRead(absPath: string): boolean; /** True if `edit`/`write` wrote this file in the current session. */ hasWritten(absPath: string): boolean; lastReadMtime(absPath: string): number | undefined; /** sha-256 (hex) of the content at the last recorded read/write, if the * recording tool supplied one. See `fileHashes` for drop semantics. */ lastReadHash(absPath: string): string | undefined; /** * Change the working directory for path resolution. Resolves relative paths * against `projectRoot` and validates the result is within the project root. * Fires all registered `onWorkingDirChanged` callbacks. * Returns the resolved absolute path. */ setWorkingDir(dir: string): string; /** * Register a callback that fires when the working directory changes. * Returns an unsubscribe function. Callbacks are fired synchronously * inside `setWorkingDir()` — errors in callbacks are swallowed so one * bad listener doesn't prevent others from executing. */ onWorkingDirChanged(cb: (newDir: string, oldDir: string) => void): () => void; usage(): Usage; } //# sourceMappingURL=context.d.ts.map