@immediately-run/sdk 0.69.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var agentLoop_exports = {};
20
20
  __export(agentLoop_exports, {
21
21
  COMPACTION_MARKER: () => COMPACTION_MARKER,
22
+ HOST_CONTEXT_OVERFLOW_CODE: () => HOST_CONTEXT_OVERFLOW_CODE,
22
23
  NUDGE_TEXT: () => NUDGE_TEXT,
23
24
  compactTranscript: () => compactTranscript,
24
25
  detectStall: () => detectStall,
@@ -109,10 +110,16 @@ function dropBlocks(m, kind) {
109
110
  }
110
111
  const dropImages = (m) => dropBlocks(m, "image");
111
112
  const dropReasoning = (m) => dropBlocks(m, "reasoning");
113
+ const HOST_CONTEXT_OVERFLOW_CODE = "context-too-large";
112
114
  function isContextOverflow(e) {
113
115
  const msg = (e?.message ?? String(e)).toLowerCase();
114
116
  const code = String(e?.code ?? "").toLowerCase();
115
- return code.includes("context_length") || code.includes("context-length") || /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg);
117
+ return (
118
+ // The host's own typed code, matched EXACTLY: the host is the one place that
119
+ // decides what counts as an overflow (R3-588) — a relay `too-large` it did NOT
120
+ // translate must not sneak in as a substring of some message.
121
+ code === HOST_CONTEXT_OVERFLOW_CODE || code.includes("context_length") || code.includes("context-length") || /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg)
122
+ );
116
123
  }
117
124
  const TRUNCATED_RETRY_TEXT = "That turn was cut off at the token limit mid tool-call, so the call was NOT executed. Emit a smaller step: fewer/shorter tool calls, or a smaller file write.";
118
125
  const cacheCounterFields = (cacheReadTokens, cacheWriteTokens) => ({
@@ -289,6 +296,7 @@ async function runAgent(opts) {
289
296
  // Annotate the CommonJS export names for ESM import in node:
290
297
  0 && (module.exports = {
291
298
  COMPACTION_MARKER,
299
+ HOST_CONTEXT_OVERFLOW_CODE,
292
300
  NUDGE_TEXT,
293
301
  compactTranscript,
294
302
  detectStall,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/agentLoop.ts"],"sourcesContent":["// Provider-agnostic agentic tool-use loop — PORTED from agent-demo `src/lib/agentLoop.ts`\n// (GROVE_AGENT_SPEC §7: embedded agents REUSE this seam rather than reimplementing it;\n// the SDK is its shared home so every app's agent loop is the same exercised machinery).\n// Renames for the SDK's flat export surface: `ChatMessage`→`AgentMessage`, `Role`→`AgentRole`.\n// Provider-agnostic agentic tool-use loop (LLM_AND_AGENTS_SPEC §3.3). The loop is\n// the heart of the in-browser coding agent: send the conversation + tool list to a\n// ModelClient, execute any tool calls the model emits, append the results, and\n// repeat until the model stops, a spend budget is hit, or a large safety-stop is\n// reached. The ModelClient seam keeps the loop independent of any one provider\n// (host `chat()` impl: chatModelClient.ts).\n//\n// Confinement (G12/T24) is NOT enforced here — it falls out of the capability\n// model: the `tools` handed to the model ARE the app's grant-filtered §5.5\n// catalog (agentTools.ts), and `execute` routes through the host's gated\n// `invoke()`, so an off-catalog/hallucinated tool returns `forbidden` at the host.\n//\n// R3-220 (AHG-1) adds the machinery that lets the loop run LONG enough to build a\n// real app: token accounting (from the provider `usage` delta), automatic context\n// COMPACTION when the window fills, a truncated-tool-call guard, and a spend budget\n// replacing the old fixed 12-turn cap. All of it is inert unless a `contextWindow`\n// is supplied, so a caller that passes none behaves exactly as before.\n//\n// PREFIX STABILITY IS LOAD-BEARING (R3-336). The loop's contribution to prompt caching\n// is structural, not a parameter: `system` and `tools` are fixed for a run and are sent\n// BYTE-IDENTICALLY on every turn, while everything that changes is appended to\n// `messages`. That is what the host's cache breakpoints key on. Rebuilding the system\n// prompt per turn — re-stamping a date, re-ordering the tool list — would cost nothing\n// visible and silently turn every cache read into a cache write, so it is asserted in\n// the tests rather than left as a convention.\n\nimport { anySignal, steerWireText, INTERRUPTED_TURN_TEXT, type SteerMessage, type SteerSource } from './agentSteering';\nimport type { PauseSource } from './agentPause';\n\nexport type TextBlock = { type: 'text'; text: string };\n/**\n * An image the model can look at (R3-339). `data` is base64 with no `data:` prefix,\n * matching the SDK `ContentPart` the transport already accepts.\n *\n * Carried as its OWN block rather than stuffed inside a `tool_result`, because a tool\n * result's content is a string on the wire — the loop appends the image to the same\n * user message that carries the results, which is the shape both host adapters map.\n */\nexport type ImageBlock = { type: 'image'; mimeType: string; data: string };\n/**\n * A block of the model's own reasoning (R3-335).\n *\n * Kept in the message sequence rather than rendered and thrown away, for two reasons:\n * the user needs to see what the model is doing during the long stretches compaction\n * now makes possible, and some providers REQUIRE the block echoed back — with its\n * `signature` — for the following turn of a tool-use chain to stay valid. A loop that\n * drops them is quietly lossy in a way that shows up as degraded output, not an error.\n *\n * `redactedData` carries provider-redacted reasoning: opaque bytes with no readable\n * text, which still have to be replayed in place. Never render it.\n */\nexport type ReasoningBlock = {\n type: 'reasoning';\n text: string;\n signature?: string;\n redactedData?: string;\n};\nexport type ToolUseBlock = { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> };\nexport type ToolResultBlock = { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean };\nexport type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock | ReasoningBlock;\n\n/** A tool the model may call: name, description, and a JSON Schema for its input\n * (`input_schema`, the Anthropic wire name — {@link createChatModelClient} maps it to the\n * chat slot's `ToolDef`). */\nexport interface AgentTool {\n name: string;\n description: string;\n input_schema: Record<string, unknown>;\n}\n\nexport type AgentRole = 'user' | 'assistant';\nexport interface AgentMessage {\n role: AgentRole;\n content: ContentBlock[];\n}\n\n/** Provider-reported token counts for one turn (R3-220). `inputTokens` is the size\n * of everything the provider processed this turn; `outputTokens` is what it\n * generated. Absent when the provider emits no `usage` delta. */\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n /** R3-336 — prompt-cache counters, present only where the provider reports them.\n * ABSENT is not zero: it means this provider says nothing about caching, which is a\n * different fact from \"nothing was cached\", and conflating them would turn a\n * measurement into a guess. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n}\n\n/** One model turn: the assistant's emitted blocks + why it stopped (+ usage). */\nexport interface ModelResponse {\n content: (TextBlock | ToolUseBlock | ReasoningBlock)[];\n /** Anthropic stop_reason: 'end_turn' | 'tool_use' | 'max_tokens' | 'refusal' | … */\n stopReason: string;\n /** Provider token counts for this turn, when reported (R3-220 accounting). */\n usage?: TokenUsage;\n}\n\n/** The provider seam — one model turn. Implemented by `chatModelClient.ts` over\n * the host `chat()` slot; faked in tests. When the client streams, it calls\n * `onTextDelta` with each token slice as it arrives (the assembled turn is still\n * returned whole); a non-streaming client simply never calls it. */\nexport interface ModelClient {\n createMessage(req: {\n system?: string;\n messages: AgentMessage[];\n tools: AgentTool[];\n /** Called with incremental assistant-text slices during a streamed turn. */\n onTextDelta?: (text: string) => void;\n /** R3-335: incremental REASONING slices, for a live thinking surface. Never called\n * by a provider that does not emit reasoning. */\n onReasoningDelta?: (text: string) => void;\n /** R3-224: aborts the in-flight turn — the host stops the upstream provider\n * request and stops billing, not just the app-side stream (§3.3). */\n signal?: AbortSignal;\n }): Promise<ModelResponse>;\n}\n\n/** Executes one tool call, returning a string result (and whether it errored —\n * a `forbidden`/failed call comes back as `is_error` so the model can adapt). */\nexport type ToolExecutor = (name: string, input: Record<string, unknown>) => Promise<ToolOutcome>;\n\n/** What one tool call produced. `images` (R3-339) is how a tool hands the model\n * something to LOOK at; `content` still carries the text the model reads. */\nexport interface ToolOutcome {\n content: string;\n isError?: boolean;\n images?: ImageBlock[];\n}\n\n/** Why a no-tool-call turn looked like a stall rather than a genuine finish. */\nexport type StallReason = 'empty' | 'announced-no-call';\n\n/** Optional UI hooks so a panel can render the loop as it runs. */\nexport interface AgentEvents {\n /** A streamed token slice of the in-flight assistant turn (live preview). */\n onAssistantDelta?(text: string): void;\n /** The complete assistant text for a turn, once the turn is in. */\n onAssistantText?(text: string): void;\n onToolUse?(name: string, input: Record<string, unknown>): void;\n onToolResult?(name: string, result: ToolOutcome): void;\n /** Fired when the loop nudges a STALLED turn (the model ended without a tool\n * call despite empty or \"I'll do X\" intent text) back into action, so a panel\n * can show \"nudging the model to continue\" rather than a silent stall. */\n onNudge?(reason: StallReason): void;\n /** Fired after every turn with the running context size + window (R3-220\n * loop-observability). `contextTokens` is provider-reported when available, else\n * a char/4 estimate. */\n onUsage?(usage: {\n contextTokens: number;\n window?: number;\n spentTokens: number;\n /** R3-336 — cumulative cache reads/writes across the run, on providers that report\n * them. Surfacing this is what makes the caching claim verifiable rather than\n * believed; `undefined` means the provider reported nothing. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }): void;\n /** Fired when the loop compacts the transcript to stay under the context window;\n * `summarizedCount` is how many older messages were folded into the summary.\n *\n * R3-336: a compaction invalidates the conversation-prefix cache it rewrote — the\n * durable system+tools prefix survives it — so the next turn pays one prefix\n * re-write. `cacheReadTokens`/`cacheWriteTokens` are the run totals AT the\n * compaction, which is what lets the cost curve across it be read off rather than\n * assumed (exit 2). */\n onCompact?(info: { summarizedCount: number; cacheReadTokens?: number; cacheWriteTokens?: number }): void;\n /** Fired when the loop stops because the token/spend budget was exhausted. */\n onBudgetStop?(info: { spentTokens: number; tokenBudget: number }): void;\n /** Fired when a turn was truncated (`max_tokens`) while emitting tool calls, so\n * the partial calls were failed-and-re-prompted rather than executed (R3-220 F3). */\n onTruncatedToolCall?(): void;\n /** R3-335: a streamed slice of the model's reasoning, for a live thinking surface. */\n onReasoningDelta?(text: string): void;\n /** R3-335: the complete reasoning block for a turn, once the turn is in. */\n onReasoning?(block: ReasoningBlock): void;\n /** R3-333: the loop applied the user's mid-run correction(s). `interrupted` is\n * true when an `interrupt`-mode steer cut an in-flight model turn short (as\n * opposed to being applied at an ordinary turn boundary). */\n onSteer?(info: { messages: SteerMessage[]; interrupted: boolean }): void;\n /** R3-562: the loop reached a turn boundary while its region was hidden and stopped\n * advancing. A surface can say \"paused — this view is hidden\" instead of looking hung. */\n onPause?(info: { turn: number }): void;\n /** R3-562: the region was revealed (or the run was stopped) and the loop resumed. */\n onResume?(info: { turn: number }): void;\n}\n\nexport interface RunAgentOptions {\n client: ModelClient;\n tools: AgentTool[];\n execute: ToolExecutor;\n system?: string;\n /** Prior turns of this conversation, replayed before the new prompt so a\n * follow-up has context (the conversation stage seeds this from the store). */\n history?: AgentMessage[];\n /** The user's instruction that kicks off the loop. */\n prompt: string;\n /** Large safety-stop on model turns (default 100). No longer the primary bound —\n * a long task is bounded by `tokenBudget` + compaction; this just backstops a\n * pathological loop the budget/compaction somehow miss. */\n maxTurns?: number;\n /** Max consecutive \"you announced work but emitted no tool call\" nudges before\n * the loop gives up (default 1). GLM-over-OpenRouter intermittently ends a turn\n * with future-tense intent (\"I'll read the files…\") or an EMPTY turn right after\n * a tool error — no tool call, a silent stall (tutorial findings §2). One nudge\n * recovers most of these; the cap keeps a genuinely-finished model (which answers\n * the nudge with another call-free turn) from looping, and the budget resets on\n * any turn that DID call a tool, so a long task's later stall is still covered.\n * Set 0 to disable the backstop. */\n maxNudges?: number;\n // ---- R3-220 accounting / compaction (all inert unless `contextWindow` is set) ----\n /** The resolved provider's context window (`describeChat().features.maxContextTokens`).\n * Compaction is disabled when this is absent/0 — the loop then behaves as before. */\n contextWindow?: number;\n /** Headroom left below the window before compacting (default: 25% of the window). */\n reserveTokens?: number;\n /** Recent messages kept verbatim across a compaction (default 8). */\n keepRecentTurns?: number;\n /** Cumulative token budget (input+output across turns). When exceeded the loop\n * stops — the runaway-cost guard that replaces the raw 12-turn cap. Off when unset. */\n tokenBudget?: number;\n /** Max consecutive truncated-tool-call re-prompts before giving up (default 2). */\n maxTruncationRetries?: number;\n /** R3-224 (§3.3): the stop button. When it fires the loop stops between turns AND\n * aborts the in-flight model turn (the host tears down the upstream provider\n * request and stops billing) — not merely the between-turn loop. The transcript so\n * far is returned; an abort is a clean stop, never a thrown error. */\n signal?: AbortSignal;\n /** R3-333: the mid-run steering queue. The loop drains it at every turn boundary\n * and folds each correction in as a `user` message, so the human can redirect a\n * run without restarting it and paying for the transcript again. Its `interrupt`\n * signal aborts the in-flight MODEL turn only — never a tool batch, which must\n * keep every `tool_use` paired with a `tool_result`. Absent ⇒ the loop behaves\n * exactly as before. */\n steering?: SteerSource;\n /**\n * R3-562 (AGENT_RUN_DURABILITY_SPEC §7 R-ARD-20a): pause the run while nobody can see\n * or stop it — the host has hidden this app's region but kept the frame mounted.\n *\n * Read at the TURN BOUNDARY only, so every `tool_use` still has its `tool_result` when\n * the loop stops advancing. Nothing is torn down and nothing is injected: the run\n * simply does not start its next turn until the region is revealed, then continues\n * with no repair pass and no resume gate. Omitted ⇒ the loop never pauses, exactly as\n * before.\n */\n pause?: PauseSource;\n events?: AgentEvents;\n}\n\nconst textOf = (blocks: { type: string; text?: string }[]): string =>\n blocks\n .filter((b): b is TextBlock => b.type === 'text')\n .map((b) => b.text)\n .join('');\n\n// Terminal stops we must NOT nudge past. Only `max_tokens` survives the SDK→loop\n// mapping distinctly (chatModelClient `mapStop`: 'length'→'max_tokens', while\n// 'end'/'filtered'→'end_turn' and 'tool'→'tool_use'); a truncated turn is a\n// token-budget problem a nudge can't fix. An empty give-up after a tool error\n// arrives as 'end_turn', so it stays nudgeable.\nconst TERMINAL_STOPS = new Set(['max_tokens', 'refusal']);\n\n// Future-tense intent to ACT (\"I'll read…\", \"let me create…\", \"next I'll edit…\").\nconst INTENT_RE =\n /\\b(i'?ll|i will|i'?m going to|going to|let me|let's|now,? i(?:'?ll| will)?|next,? i(?:'?ll| will)?)\\b[\\s\\S]{0,80}?\\b(read|write|edit|creat|add|updat|modif|regist|check|look|call|run|search|grep|list|open|fetch|inspect|review|explor|implement|fix|appl)/i;\n// A wrap-up marker → treat the turn as a genuine finish, never nudge.\nconst DONE_RE =\n /\\b(done|complete|finished|all set|no (?:further|more) (?:changes|steps)|i(?:'| ha)ve (?:creat|add|updat|made|written|regist|edit|implement|fix|appli)|here'?s (?:a |the )?summ|to summ|in summ)/i;\n\n/**\n * Classify a NO-tool-call turn as a stall (nudge-worthy) vs a genuine finish.\n * GLM-over-OpenRouter intermittently (a) writes \"I'll read the files…\" then ends\n * with no call, or (b) returns an EMPTY turn after a tool error — both silent\n * give-ups (tutorial findings §2). Conservative on purpose: a real wrap-up (a\n * summary, \"Done\", \"I've created…\") returns null so the loop never nudges a\n * finished agent. Empty text is always a stall (there is nothing a finished agent\n * would say with zero words).\n */\nexport function detectStall(text: string): StallReason | null {\n const t = text.trim();\n if (!t) return 'empty';\n if (DONE_RE.test(t)) return null;\n if (INTENT_RE.test(t)) return 'announced-no-call';\n return null;\n}\n\n// The single follow-up we inject to break a stall. Directive, short, and honest\n// about the two outcomes so a genuinely-finished model just confirms and stops\n// (→ another call-free turn, which the nudge cap then lets terminate). Exported so\n// the transcript renderer can recognise the injected turn and show it as a \"nudge\"\n// row (not a user message) when a persisted conversation is replayed.\nexport const NUDGE_TEXT =\n \"You ended your turn without calling a tool. If the task is already complete, say so plainly in one line and stop. Otherwise don't just describe the next step — emit the tool call now.\";\n\n// ---- R3-220 token accounting + compaction ----------------------------------------\n\n/** Rough token estimate (~4 chars/token) over a message array, used only when the\n * provider reports no `usage` delta. Conservative by design (over- not under-counts\n * by treating structured blocks as their JSON length). */\nexport function estimateTokens(messages: AgentMessage[]): number {\n let chars = 0;\n for (const m of messages) {\n for (const b of m.content) {\n if (b.type === 'text') chars += b.text.length;\n else if (b.type === 'tool_use') chars += JSON.stringify(b.input).length + b.name.length;\n else if (b.type === 'tool_result') chars += b.content.length;\n // R3-339: an image is large and MUST be accounted for, or it escapes exactly the\n // budget the accounting exists to enforce. base64 is ~4/3 of the bytes, and the\n // provider bills tokens per pixel area — the base64 length is the honest local\n // proxy for \"this is big\", and over-counting is the safe direction.\n else if (b.type === 'image') chars += b.data.length;\n // R3-335: reasoning occupies the window like anything else. Not counting it would\n // let a thinking model overrun the context the accounting exists to protect.\n else if (b.type === 'reasoning') chars += b.text.length + (b.redactedData?.length ?? 0);\n }\n }\n return Math.ceil(chars / 4);\n}\n\n/** Should the loop compact now? True once the running context passes\n * `window − reserveTokens`. Disabled (false) when there is no window. */\nexport function shouldCompact(contextTokens: number, window: number | undefined, reserveTokens: number): boolean {\n if (!window || window <= 0) return false;\n return contextTokens > window - reserveTokens;\n}\n\n/** Prefix marking a `user` message as a compaction summary (not a real user turn),\n * so the transcript renderer shows a \"compacted N turns\" affordance on replay. */\nexport const COMPACTION_MARKER = '␟[compacted-context]\\n';\n\nconst SUMMARY_SYSTEM =\n 'You are compacting a coding-agent transcript to fit the context window. Produce a ' +\n 'DENSE structured summary under these exact headings: Goal / Constraints / Progress / ' +\n 'Decisions / Next Steps / Critical Context. PRESERVE VERBATIM every file path, symbol/' +\n 'identifier, and error string that later steps will need — do not paraphrase them. Be ' +\n 'terse everywhere else. Output only the summary.';\nconst SUMMARY_INSTRUCTION =\n 'Summarize everything above into the structured block. Keep exact paths, symbols, and ' +\n 'error strings verbatim so work can continue from the summary alone.';\n\n/** Compact `messages` by folding the older head into a structured summary and keeping\n * a verbatim recent tail. The tail is snapped to start at an `assistant` message so a\n * `tool_use`/`tool_result` pair is never split (which would malform the next request).\n * The taint tier is NOT modelled on messages (it is run-scoped host state, R-ASG-2):\n * this is a pure content transform over the SAME session — it starts no new external\n * read — so it cannot launder taint (F6). Returns the original array unchanged when\n * there is nothing safe to summarize. */\nexport async function compactTranscript(\n messages: AgentMessage[],\n client: ModelClient,\n keepRecentTurns: number,\n): Promise<{ messages: AgentMessage[]; summarizedCount: number }> {\n if (messages.length <= keepRecentTurns + 1) return { messages, summarizedCount: 0 };\n\n // Snap the tail boundary to an assistant message so tool_use/tool_result pairs stay\n // together and the summary (a `user` turn) is followed by an `assistant` turn.\n // Prefer the first assistant at/after the keep-recent boundary; fall back to the\n // last assistant in the transcript so the tail is always well-formed.\n const boundary = Math.max(1, messages.length - keepRecentTurns);\n let tailStart = -1;\n for (let i = boundary; i < messages.length; i++) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n if (tailStart === -1) {\n for (let i = messages.length - 1; i >= 1; i--) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n }\n if (tailStart <= 0) return { messages, summarizedCount: 0 };\n\n const head = messages.slice(0, tailStart);\n // Compaction DROPS both image parts (R3-339) and reasoning (R3-335) from the kept\n // tail, each by an explicit rule — an implicit answer here is what corrupts a\n // transcript quietly.\n //\n // IMAGES: the largest and least summarisable thing in a transcript, and the summary\n // the head folds into is TEXT. The `tool_result` that named the image stays, so the\n // model still knows it looked at `assets/mock.png` and what it concluded; it simply\n // cannot look again without re-reading the file, which it can do.\n //\n // REASONING: only ever required by the turn that FOLLOWS it, and compaction rewrites\n // at a turn boundary — so nothing after it is mid-chain and nothing needs the block\n // replayed. Keeping them would spend the window on its most disposable content.\n const tail = messages.map(dropImages).map(dropReasoning).slice(tailStart);\n\n // Ask the model to summarize the head. Append the instruction to the final head\n // message when it is a `user` turn (avoids introducing consecutive user turns).\n const reqMessages: AgentMessage[] = head.map((m) => ({ role: m.role, content: [...m.content] }));\n const lastMsg = reqMessages[reqMessages.length - 1];\n if (lastMsg && lastMsg.role === 'user') {\n lastMsg.content = [...lastMsg.content, { type: 'text', text: SUMMARY_INSTRUCTION }];\n } else {\n reqMessages.push({ role: 'user', content: [{ type: 'text', text: SUMMARY_INSTRUCTION }] });\n }\n\n let summaryText = '(summary unavailable)';\n try {\n const res = await client.createMessage({ system: SUMMARY_SYSTEM, messages: reqMessages, tools: [] });\n summaryText = textOf(res.content).trim() || summaryText;\n } catch {\n // Summarization itself failed — keep the original transcript (caller will retry\n // or hit the safety-stop). Better a longer context than a lost transcript.\n return { messages, summarizedCount: 0 };\n }\n\n const summaryMsg: AgentMessage = {\n role: 'user',\n content: [{ type: 'text', text: COMPACTION_MARKER + summaryText }],\n };\n return { messages: [summaryMsg, ...tail], summarizedCount: head.length };\n}\n\n/** Strip blocks of one kind from a message, keeping everything else in order. A message\n * left with no content at all keeps a single empty text block so the role sequence\n * stays well-formed (a content-less message is rejected by most providers). */\nfunction dropBlocks(m: AgentMessage, kind: 'image' | 'reasoning'): AgentMessage {\n if (!m.content.some((b) => b.type === kind)) return m;\n const kept = m.content.filter((b) => b.type !== kind);\n return { role: m.role, content: kept.length ? kept : [{ type: 'text', text: '' }] };\n}\n\n/** Compaction's image-drop rule (R3-339). */\nconst dropImages = (m: AgentMessage): AgentMessage => dropBlocks(m, 'image');\n/** Compaction's reasoning-drop rule (R3-335). */\nconst dropReasoning = (m: AgentMessage): AgentMessage => dropBlocks(m, 'reasoning');\n\n/** Does this thrown error look like a hard context-window overflow? Used to trigger\n * recover-then-retry compaction (F3/exit-c) rather than a dead loop. */\nexport function isContextOverflow(e: unknown): boolean {\n const msg = ((e as Error)?.message ?? String(e)).toLowerCase();\n const code = String((e as { code?: unknown })?.code ?? '').toLowerCase();\n return (\n code.includes('context_length') ||\n code.includes('context-length') ||\n /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg)\n );\n}\n\n// The user turn injected when a truncated (`max_tokens`) turn emitted tool calls: we\n// fail the partial calls rather than execute them (F3), and tell the model to retry.\nconst TRUNCATED_RETRY_TEXT =\n 'That turn was cut off at the token limit mid tool-call, so the call was NOT executed. ' +\n 'Emit a smaller step: fewer/shorter tool calls, or a smaller file write.';\n\n/** The cumulative cache counters, as the event fields that carry them. A counter no\n * provider has reported yet is OMITTED, never zeroed: \"the provider reports nothing\"\n * and \"the provider cached nothing\" are different facts and the consumer must be able\n * to tell them apart. Three event payloads state this rule; this is the one place it\n * is spelled out. */\nconst cacheCounterFields = (\n cacheReadTokens: number | undefined,\n cacheWriteTokens: number | undefined,\n): { cacheReadTokens?: number; cacheWriteTokens?: number } => ({\n ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),\n ...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),\n});\n\n/**\n * Drive the agent loop to completion. Returns the full message transcript\n * (including the kickoff user turn). Stops when the model returns without tool\n * calls (or a terminal stop reason), when the token budget is exhausted, or when\n * `maxTurns` (a large safety-stop) is reached. With a `contextWindow` set, the loop\n * accounts tokens and compacts automatically so it can run long.\n */\nexport async function runAgent(opts: RunAgentOptions): Promise<AgentMessage[]> {\n const { client, tools, execute, system, prompt, events, signal, steering, pause } = opts;\n const maxTurns = opts.maxTurns ?? 100;\n const maxNudges = opts.maxNudges ?? 1;\n const maxTruncationRetries = opts.maxTruncationRetries ?? 2;\n const window = opts.contextWindow;\n const reserveTokens = opts.reserveTokens ?? (window ? Math.floor(window * 0.25) : 0);\n const keepRecentTurns = opts.keepRecentTurns ?? 8;\n\n let messages: AgentMessage[] = [...(opts.history ?? []), { role: 'user', content: [{ type: 'text', text: prompt }] }];\n\n // Consecutive-stall counter: how many times in a row we've nudged a no-tool-call\n // turn. Reset to 0 by any turn that DOES call a tool, so the budget is per stall\n // *episode*, not per run.\n let nudges = 0;\n let truncationRetries = 0;\n // True when the previous iteration's model turn was cut short by an `interrupt`\n // steer, so the injected correction can be reported as an interruption.\n let interruptedLastTurn = false;\n // Running context size (provider-reported when available) + cumulative spend.\n let contextTokens = 0;\n let spentTokens = 0;\n // R3-336 — cumulative cache accounting. `undefined` until a provider reports\n // something, so \"reports nothing\" stays distinguishable from \"cached nothing\".\n let cacheReadTokens: number | undefined;\n let cacheWriteTokens: number | undefined;\n\n // Compact the transcript and, when anything was actually folded in, adopt the\n // compacted messages, re-estimate the running context, and report it. Returns how\n // many messages were summarized — 0 means \"there was nothing to compact\", which is\n // what the overflow-recovery path below treats as unrecoverable. Both compaction\n // sites (near-window before a request, and hard-overflow recovery) go through here,\n // so they cannot drift on what a compaction updates or reports.\n const compactAndReport = async (): Promise<number> => {\n const { messages: compacted, summarizedCount } = await compactTranscript(messages, client, keepRecentTurns);\n if (summarizedCount > 0) {\n messages = compacted;\n contextTokens = estimateTokens(messages);\n events?.onCompact?.({ summarizedCount, ...cacheCounterFields(cacheReadTokens, cacheWriteTokens) });\n }\n return summarizedCount;\n };\n\n for (let turn = 0; turn < maxTurns; turn++) {\n // R3-224 (§3.3): the stop button, checked between turns. Combined with the\n // per-request `signal` below (which aborts the in-flight upstream turn), this\n // halts \"the loop between tool calls AND aborts the in-flight LLM request\".\n if (signal?.aborted) break;\n\n // R3-562 (§7 R-ARD-20a): if the region is hidden, stop HERE — at the boundary, with\n // the previous turn's tool batch fully paired — and wait for the reveal. Placed after\n // the stop check and before the steer drain so a correction queued while hidden is\n // applied on the way back in, as the very next turn, rather than a turn late.\n if (pause?.isPaused()) {\n events?.onPause?.({ turn });\n await pause.whenResumed(signal);\n events?.onResume?.({ turn });\n // `whenResumed` also resolves on abort, so a run stopped while hidden lands here\n // rather than awaiting a reveal that never comes.\n if (signal?.aborted) break;\n }\n\n // R3-333: apply any queued corrections at the TURN BOUNDARY, before the next\n // request, so the model's very next turn reflects them. Draining here (rather\n // than at the point of arrival) is what makes a steer safe: whatever the loop\n // was doing — streaming a turn, running a tool batch — has finished.\n if (steering) {\n const steers = steering.drain();\n if (steers.length) {\n messages.push({\n role: 'user',\n content: steers.map((m) => ({ type: 'text' as const, text: steerWireText(m) })),\n });\n events?.onSteer?.({ messages: steers, interrupted: interruptedLastTurn });\n }\n interruptedLastTurn = false;\n steering.rearm();\n }\n\n // Compact BEFORE the next request when the running context is near the window.\n if (shouldCompact(contextTokens, window, reserveTokens)) await compactAndReport();\n\n // The in-flight turn is abortable by EITHER verb: STOP (ends the run) or an\n // `interrupt`-mode STEER (ends the turn, keeps the run). They are composed into\n // one per-turn signal, and told apart in the catch by asking which fired.\n const turnAbort = anySignal([signal, steering?.interrupt]);\n // Capture what the model had streamed when a steer cut in, so the interrupted\n // turn is recorded as what actually happened rather than dropped.\n let partialText = '';\n const onTextDelta = (text: string): void => {\n partialText += text;\n events?.onAssistantDelta?.(text);\n };\n const sendTurn = () =>\n client.createMessage({\n system,\n messages,\n tools,\n // R3-333's local `onTextDelta` (it captures the partial text a steer may cut\n // short) — NOT `events.onAssistantDelta` directly.\n onTextDelta,\n // R3-335's reasoning stream rides alongside it.\n onReasoningDelta: events?.onReasoningDelta,\n // R3-333: STOP composed with the steer INTERRUPT, so either verb ends the turn.\n signal: turnAbort.signal,\n });\n let res: ModelResponse;\n try {\n try {\n res = await sendTurn();\n } catch (e) {\n // Recover-then-retry on a hard context-overflow (exit-c): compact once and\n // re-send. If there is nothing to compact, or the retry also overflows, the\n // error propagates — a bounded recovery, never a dead loop.\n if (turnAbort.signal.aborted || !isContextOverflow(e)) throw e;\n if ((await compactAndReport()) === 0) throw e;\n res = await sendTurn();\n }\n } catch (e) {\n // R3-224: a mid-turn abort surfaces as a thrown (Abort/Stream)Error. Treat it\n // as a CLEAN stop — return the transcript so far — not a failure to bubble up.\n if (signal?.aborted) {\n turnAbort.dispose();\n break;\n }\n // R3-333: the SAME thrown abort, but from a steer — the run continues. Record\n // the turn the user cut short (an assistant message, so the transcript keeps\n // strict role alternation and replay shows the interruption where it happened),\n // then loop: the drain at the top of the next iteration injects the correction.\n if (steering?.interrupt.aborted) {\n turnAbort.dispose();\n messages.push({\n role: 'assistant',\n content: [{ type: 'text', text: partialText.trim() || INTERRUPTED_TURN_TEXT }],\n });\n events?.onAssistantText?.(partialText.trim() || INTERRUPTED_TURN_TEXT);\n interruptedLastTurn = true;\n continue;\n }\n turnAbort.dispose();\n throw e;\n }\n turnAbort.dispose();\n\n // Token accounting (R3-220): prefer the provider `usage`, else estimate. `turnCost`\n // is what this turn billed (input + output); `contextTokens` is the current window\n // occupancy (drives compaction); `spentTokens` is cumulative run spend (input is\n // re-billed every turn, so summing turnCost is the true cost signal).\n const turnCost = res.usage\n ? res.usage.inputTokens + res.usage.outputTokens\n : estimateTokens(messages) + Math.ceil(textOf(res.content).length / 4);\n contextTokens = turnCost;\n spentTokens += turnCost;\n if (res.usage?.cacheReadTokens !== undefined) {\n cacheReadTokens = (cacheReadTokens ?? 0) + res.usage.cacheReadTokens;\n }\n if (res.usage?.cacheWriteTokens !== undefined) {\n cacheWriteTokens = (cacheWriteTokens ?? 0) + res.usage.cacheWriteTokens;\n }\n events?.onUsage?.({\n contextTokens,\n window,\n spentTokens,\n ...cacheCounterFields(cacheReadTokens, cacheWriteTokens),\n });\n\n const assistantText = textOf(res.content);\n if (assistantText) events?.onAssistantText?.(assistantText);\n // R3-335: reasoning stays IN the message sequence — a provider that requires the\n // block echoed back gets it from `messages`, not from a side channel.\n for (const b of res.content) if (b.type === 'reasoning') events?.onReasoning?.(b);\n messages.push({ role: 'assistant', content: res.content });\n\n const toolUses = res.content.filter((b): b is ToolUseBlock => b.type === 'tool_use');\n\n // Truncated-tool-call guard (F3): a `max_tokens` turn that emitted tool calls\n // was cut off mid-call, so its args may be partial. Do NOT execute them — fail\n // each with an error tool_result (keeps the conversation well-formed) and\n // re-prompt for a smaller step, bounded by maxTruncationRetries.\n if (res.stopReason === 'max_tokens' && toolUses.length > 0) {\n events?.onTruncatedToolCall?.();\n const failed: ContentBlock[] = toolUses.map((c) => ({\n type: 'tool_result',\n tool_use_id: c.id,\n content: 'tool call truncated by the token limit — not executed',\n is_error: true,\n }));\n failed.push({ type: 'text', text: TRUNCATED_RETRY_TEXT });\n messages.push({ role: 'user', content: failed });\n if (++truncationRetries > maxTruncationRetries) break;\n continue;\n }\n truncationRetries = 0;\n\n if (toolUses.length === 0) {\n // No tool calls. Usually the model is genuinely done — but GLM/OpenRouter\n // intermittently ends with \"I'll read the files…\" or an empty turn after a\n // tool error and no call (findings §2). Nudge such a STALL back into action\n // once (per episode), respecting terminal stops and a real wrap-up.\n const stall = TERMINAL_STOPS.has(res.stopReason) ? null : detectStall(assistantText);\n if (stall && nudges < maxNudges) {\n nudges++;\n events?.onNudge?.(stall);\n messages.push({ role: 'user', content: [{ type: 'text', text: NUDGE_TEXT }] });\n continue;\n }\n // R3-333 follow-up: the model is done, but the user queued something while it\n // was working. Continue rather than end — the drain at the top of the next\n // iteration turns the queued message into the next turn's prompt. This is the\n // difference between a follow-up and a restart.\n if (steering?.hasPending()) continue;\n break;\n }\n\n nudges = 0; // a productive turn clears the stall budget\n\n const results: ToolResultBlock[] = [];\n // R3-339 — image parts produced by tools this turn. They ride in the SAME user\n // message as the results (after them), because a `tool_result`'s content is a string\n // on the wire; this is the shape both host adapters map to their provider.\n const images: ImageBlock[] = [];\n for (const call of toolUses) {\n events?.onToolUse?.(call.name, call.input);\n let outcome: ToolOutcome;\n try {\n outcome = await execute(call.name, call.input);\n } catch (e) {\n // A thrown executor error (e.g. host `forbidden`) becomes an error\n // tool_result so the model sees the gate's verdict and can adapt.\n const code = (e as { code?: string })?.code;\n const msg = (e as Error)?.message ?? String(e);\n outcome = { content: code ? `${code}: ${msg}` : msg, isError: true };\n }\n events?.onToolResult?.(call.name, outcome);\n results.push({\n type: 'tool_result',\n tool_use_id: call.id,\n content: outcome.content,\n is_error: outcome.isError,\n });\n if (outcome.images?.length) images.push(...outcome.images);\n }\n messages.push({ role: 'user', content: [...results, ...images] });\n\n // Runaway-cost guard: stop once cumulative spend passes the budget (the token/\n // spend bound that replaces the old raw turn cap). Compaction keeps a single\n // request small; this bounds the whole run.\n if (opts.tokenBudget && spentTokens >= opts.tokenBudget) {\n events?.onBudgetStop?.({ spentTokens, tokenBudget: opts.tokenBudget });\n break;\n }\n }\n\n return messages;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/agentLoop.ts"],"sourcesContent":["// Provider-agnostic agentic tool-use loop — PORTED from agent-demo `src/lib/agentLoop.ts`\n// (GROVE_AGENT_SPEC §7: embedded agents REUSE this seam rather than reimplementing it;\n// the SDK is its shared home so every app's agent loop is the same exercised machinery).\n// Renames for the SDK's flat export surface: `ChatMessage`→`AgentMessage`, `Role`→`AgentRole`.\n// Provider-agnostic agentic tool-use loop (LLM_AND_AGENTS_SPEC §3.3). The loop is\n// the heart of the in-browser coding agent: send the conversation + tool list to a\n// ModelClient, execute any tool calls the model emits, append the results, and\n// repeat until the model stops, a spend budget is hit, or a large safety-stop is\n// reached. The ModelClient seam keeps the loop independent of any one provider\n// (host `chat()` impl: chatModelClient.ts).\n//\n// Confinement (G12/T24) is NOT enforced here — it falls out of the capability\n// model: the `tools` handed to the model ARE the app's grant-filtered §5.5\n// catalog (agentTools.ts), and `execute` routes through the host's gated\n// `invoke()`, so an off-catalog/hallucinated tool returns `forbidden` at the host.\n//\n// R3-220 (AHG-1) adds the machinery that lets the loop run LONG enough to build a\n// real app: token accounting (from the provider `usage` delta), automatic context\n// COMPACTION when the window fills, a truncated-tool-call guard, and a spend budget\n// replacing the old fixed 12-turn cap. All of it is inert unless a `contextWindow`\n// is supplied, so a caller that passes none behaves exactly as before.\n//\n// PREFIX STABILITY IS LOAD-BEARING (R3-336). The loop's contribution to prompt caching\n// is structural, not a parameter: `system` and `tools` are fixed for a run and are sent\n// BYTE-IDENTICALLY on every turn, while everything that changes is appended to\n// `messages`. That is what the host's cache breakpoints key on. Rebuilding the system\n// prompt per turn — re-stamping a date, re-ordering the tool list — would cost nothing\n// visible and silently turn every cache read into a cache write, so it is asserted in\n// the tests rather than left as a convention.\n\nimport { anySignal, steerWireText, INTERRUPTED_TURN_TEXT, type SteerMessage, type SteerSource } from './agentSteering';\nimport type { PauseSource } from './agentPause';\n\nexport type TextBlock = { type: 'text'; text: string };\n/**\n * An image the model can look at (R3-339). `data` is base64 with no `data:` prefix,\n * matching the SDK `ContentPart` the transport already accepts.\n *\n * Carried as its OWN block rather than stuffed inside a `tool_result`, because a tool\n * result's content is a string on the wire — the loop appends the image to the same\n * user message that carries the results, which is the shape both host adapters map.\n */\nexport type ImageBlock = { type: 'image'; mimeType: string; data: string };\n/**\n * A block of the model's own reasoning (R3-335).\n *\n * Kept in the message sequence rather than rendered and thrown away, for two reasons:\n * the user needs to see what the model is doing during the long stretches compaction\n * now makes possible, and some providers REQUIRE the block echoed back — with its\n * `signature` — for the following turn of a tool-use chain to stay valid. A loop that\n * drops them is quietly lossy in a way that shows up as degraded output, not an error.\n *\n * `redactedData` carries provider-redacted reasoning: opaque bytes with no readable\n * text, which still have to be replayed in place. Never render it.\n */\nexport type ReasoningBlock = {\n type: 'reasoning';\n text: string;\n signature?: string;\n redactedData?: string;\n};\nexport type ToolUseBlock = { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> };\nexport type ToolResultBlock = { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean };\nexport type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock | ReasoningBlock;\n\n/** A tool the model may call: name, description, and a JSON Schema for its input\n * (`input_schema`, the Anthropic wire name — {@link createChatModelClient} maps it to the\n * chat slot's `ToolDef`). */\nexport interface AgentTool {\n name: string;\n description: string;\n input_schema: Record<string, unknown>;\n}\n\nexport type AgentRole = 'user' | 'assistant';\nexport interface AgentMessage {\n role: AgentRole;\n content: ContentBlock[];\n}\n\n/** Provider-reported token counts for one turn (R3-220). `inputTokens` is the size\n * of everything the provider processed this turn; `outputTokens` is what it\n * generated. Absent when the provider emits no `usage` delta. */\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n /** R3-336 — prompt-cache counters, present only where the provider reports them.\n * ABSENT is not zero: it means this provider says nothing about caching, which is a\n * different fact from \"nothing was cached\", and conflating them would turn a\n * measurement into a guess. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n}\n\n/** One model turn: the assistant's emitted blocks + why it stopped (+ usage). */\nexport interface ModelResponse {\n content: (TextBlock | ToolUseBlock | ReasoningBlock)[];\n /** Anthropic stop_reason: 'end_turn' | 'tool_use' | 'max_tokens' | 'refusal' | … */\n stopReason: string;\n /** Provider token counts for this turn, when reported (R3-220 accounting). */\n usage?: TokenUsage;\n}\n\n/** The provider seam — one model turn. Implemented by `chatModelClient.ts` over\n * the host `chat()` slot; faked in tests. When the client streams, it calls\n * `onTextDelta` with each token slice as it arrives (the assembled turn is still\n * returned whole); a non-streaming client simply never calls it. */\nexport interface ModelClient {\n createMessage(req: {\n system?: string;\n messages: AgentMessage[];\n tools: AgentTool[];\n /** Called with incremental assistant-text slices during a streamed turn. */\n onTextDelta?: (text: string) => void;\n /** R3-335: incremental REASONING slices, for a live thinking surface. Never called\n * by a provider that does not emit reasoning. */\n onReasoningDelta?: (text: string) => void;\n /** R3-224: aborts the in-flight turn — the host stops the upstream provider\n * request and stops billing, not just the app-side stream (§3.3). */\n signal?: AbortSignal;\n }): Promise<ModelResponse>;\n}\n\n/** Executes one tool call, returning a string result (and whether it errored —\n * a `forbidden`/failed call comes back as `is_error` so the model can adapt). */\nexport type ToolExecutor = (name: string, input: Record<string, unknown>) => Promise<ToolOutcome>;\n\n/** What one tool call produced. `images` (R3-339) is how a tool hands the model\n * something to LOOK at; `content` still carries the text the model reads. */\nexport interface ToolOutcome {\n content: string;\n isError?: boolean;\n images?: ImageBlock[];\n}\n\n/** Why a no-tool-call turn looked like a stall rather than a genuine finish. */\nexport type StallReason = 'empty' | 'announced-no-call';\n\n/** Optional UI hooks so a panel can render the loop as it runs. */\nexport interface AgentEvents {\n /** A streamed token slice of the in-flight assistant turn (live preview). */\n onAssistantDelta?(text: string): void;\n /** The complete assistant text for a turn, once the turn is in. */\n onAssistantText?(text: string): void;\n onToolUse?(name: string, input: Record<string, unknown>): void;\n onToolResult?(name: string, result: ToolOutcome): void;\n /** Fired when the loop nudges a STALLED turn (the model ended without a tool\n * call despite empty or \"I'll do X\" intent text) back into action, so a panel\n * can show \"nudging the model to continue\" rather than a silent stall. */\n onNudge?(reason: StallReason): void;\n /** Fired after every turn with the running context size + window (R3-220\n * loop-observability). `contextTokens` is provider-reported when available, else\n * a char/4 estimate. */\n onUsage?(usage: {\n contextTokens: number;\n window?: number;\n spentTokens: number;\n /** R3-336 — cumulative cache reads/writes across the run, on providers that report\n * them. Surfacing this is what makes the caching claim verifiable rather than\n * believed; `undefined` means the provider reported nothing. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }): void;\n /** Fired when the loop compacts the transcript to stay under the context window;\n * `summarizedCount` is how many older messages were folded into the summary.\n *\n * R3-336: a compaction invalidates the conversation-prefix cache it rewrote — the\n * durable system+tools prefix survives it — so the next turn pays one prefix\n * re-write. `cacheReadTokens`/`cacheWriteTokens` are the run totals AT the\n * compaction, which is what lets the cost curve across it be read off rather than\n * assumed (exit 2). */\n onCompact?(info: { summarizedCount: number; cacheReadTokens?: number; cacheWriteTokens?: number }): void;\n /** Fired when the loop stops because the token/spend budget was exhausted. */\n onBudgetStop?(info: { spentTokens: number; tokenBudget: number }): void;\n /** Fired when a turn was truncated (`max_tokens`) while emitting tool calls, so\n * the partial calls were failed-and-re-prompted rather than executed (R3-220 F3). */\n onTruncatedToolCall?(): void;\n /** R3-335: a streamed slice of the model's reasoning, for a live thinking surface. */\n onReasoningDelta?(text: string): void;\n /** R3-335: the complete reasoning block for a turn, once the turn is in. */\n onReasoning?(block: ReasoningBlock): void;\n /** R3-333: the loop applied the user's mid-run correction(s). `interrupted` is\n * true when an `interrupt`-mode steer cut an in-flight model turn short (as\n * opposed to being applied at an ordinary turn boundary). */\n onSteer?(info: { messages: SteerMessage[]; interrupted: boolean }): void;\n /** R3-562: the loop reached a turn boundary while its region was hidden and stopped\n * advancing. A surface can say \"paused — this view is hidden\" instead of looking hung. */\n onPause?(info: { turn: number }): void;\n /** R3-562: the region was revealed (or the run was stopped) and the loop resumed. */\n onResume?(info: { turn: number }): void;\n}\n\nexport interface RunAgentOptions {\n client: ModelClient;\n tools: AgentTool[];\n execute: ToolExecutor;\n system?: string;\n /** Prior turns of this conversation, replayed before the new prompt so a\n * follow-up has context (the conversation stage seeds this from the store). */\n history?: AgentMessage[];\n /** The user's instruction that kicks off the loop. */\n prompt: string;\n /** Large safety-stop on model turns (default 100). No longer the primary bound —\n * a long task is bounded by `tokenBudget` + compaction; this just backstops a\n * pathological loop the budget/compaction somehow miss. */\n maxTurns?: number;\n /** Max consecutive \"you announced work but emitted no tool call\" nudges before\n * the loop gives up (default 1). GLM-over-OpenRouter intermittently ends a turn\n * with future-tense intent (\"I'll read the files…\") or an EMPTY turn right after\n * a tool error — no tool call, a silent stall (tutorial findings §2). One nudge\n * recovers most of these; the cap keeps a genuinely-finished model (which answers\n * the nudge with another call-free turn) from looping, and the budget resets on\n * any turn that DID call a tool, so a long task's later stall is still covered.\n * Set 0 to disable the backstop. */\n maxNudges?: number;\n // ---- R3-220 accounting / compaction (all inert unless `contextWindow` is set) ----\n /** The resolved provider's context window (`describeChat().features.maxContextTokens`).\n * Compaction is disabled when this is absent/0 — the loop then behaves as before. */\n contextWindow?: number;\n /** Headroom left below the window before compacting (default: 25% of the window). */\n reserveTokens?: number;\n /** Recent messages kept verbatim across a compaction (default 8). */\n keepRecentTurns?: number;\n /** Cumulative token budget (input+output across turns). When exceeded the loop\n * stops — the runaway-cost guard that replaces the raw 12-turn cap. Off when unset. */\n tokenBudget?: number;\n /** Max consecutive truncated-tool-call re-prompts before giving up (default 2). */\n maxTruncationRetries?: number;\n /** R3-224 (§3.3): the stop button. When it fires the loop stops between turns AND\n * aborts the in-flight model turn (the host tears down the upstream provider\n * request and stops billing) — not merely the between-turn loop. The transcript so\n * far is returned; an abort is a clean stop, never a thrown error. */\n signal?: AbortSignal;\n /** R3-333: the mid-run steering queue. The loop drains it at every turn boundary\n * and folds each correction in as a `user` message, so the human can redirect a\n * run without restarting it and paying for the transcript again. Its `interrupt`\n * signal aborts the in-flight MODEL turn only — never a tool batch, which must\n * keep every `tool_use` paired with a `tool_result`. Absent ⇒ the loop behaves\n * exactly as before. */\n steering?: SteerSource;\n /**\n * R3-562 (AGENT_RUN_DURABILITY_SPEC §7 R-ARD-20a): pause the run while nobody can see\n * or stop it — the host has hidden this app's region but kept the frame mounted.\n *\n * Read at the TURN BOUNDARY only, so every `tool_use` still has its `tool_result` when\n * the loop stops advancing. Nothing is torn down and nothing is injected: the run\n * simply does not start its next turn until the region is revealed, then continues\n * with no repair pass and no resume gate. Omitted ⇒ the loop never pauses, exactly as\n * before.\n */\n pause?: PauseSource;\n events?: AgentEvents;\n}\n\nconst textOf = (blocks: { type: string; text?: string }[]): string =>\n blocks\n .filter((b): b is TextBlock => b.type === 'text')\n .map((b) => b.text)\n .join('');\n\n// Terminal stops we must NOT nudge past. Only `max_tokens` survives the SDK→loop\n// mapping distinctly (chatModelClient `mapStop`: 'length'→'max_tokens', while\n// 'end'/'filtered'→'end_turn' and 'tool'→'tool_use'); a truncated turn is a\n// token-budget problem a nudge can't fix. An empty give-up after a tool error\n// arrives as 'end_turn', so it stays nudgeable.\nconst TERMINAL_STOPS = new Set(['max_tokens', 'refusal']);\n\n// Future-tense intent to ACT (\"I'll read…\", \"let me create…\", \"next I'll edit…\").\nconst INTENT_RE =\n /\\b(i'?ll|i will|i'?m going to|going to|let me|let's|now,? i(?:'?ll| will)?|next,? i(?:'?ll| will)?)\\b[\\s\\S]{0,80}?\\b(read|write|edit|creat|add|updat|modif|regist|check|look|call|run|search|grep|list|open|fetch|inspect|review|explor|implement|fix|appl)/i;\n// A wrap-up marker → treat the turn as a genuine finish, never nudge.\nconst DONE_RE =\n /\\b(done|complete|finished|all set|no (?:further|more) (?:changes|steps)|i(?:'| ha)ve (?:creat|add|updat|made|written|regist|edit|implement|fix|appli)|here'?s (?:a |the )?summ|to summ|in summ)/i;\n\n/**\n * Classify a NO-tool-call turn as a stall (nudge-worthy) vs a genuine finish.\n * GLM-over-OpenRouter intermittently (a) writes \"I'll read the files…\" then ends\n * with no call, or (b) returns an EMPTY turn after a tool error — both silent\n * give-ups (tutorial findings §2). Conservative on purpose: a real wrap-up (a\n * summary, \"Done\", \"I've created…\") returns null so the loop never nudges a\n * finished agent. Empty text is always a stall (there is nothing a finished agent\n * would say with zero words).\n */\nexport function detectStall(text: string): StallReason | null {\n const t = text.trim();\n if (!t) return 'empty';\n if (DONE_RE.test(t)) return null;\n if (INTENT_RE.test(t)) return 'announced-no-call';\n return null;\n}\n\n// The single follow-up we inject to break a stall. Directive, short, and honest\n// about the two outcomes so a genuinely-finished model just confirms and stops\n// (→ another call-free turn, which the nudge cap then lets terminate). Exported so\n// the transcript renderer can recognise the injected turn and show it as a \"nudge\"\n// row (not a user message) when a persisted conversation is replayed.\nexport const NUDGE_TEXT =\n \"You ended your turn without calling a tool. If the task is already complete, say so plainly in one line and stop. Otherwise don't just describe the next step — emit the tool call now.\";\n\n// ---- R3-220 token accounting + compaction ----------------------------------------\n\n/** Rough token estimate (~4 chars/token) over a message array, used only when the\n * provider reports no `usage` delta. Conservative by design (over- not under-counts\n * by treating structured blocks as their JSON length). */\nexport function estimateTokens(messages: AgentMessage[]): number {\n let chars = 0;\n for (const m of messages) {\n for (const b of m.content) {\n if (b.type === 'text') chars += b.text.length;\n else if (b.type === 'tool_use') chars += JSON.stringify(b.input).length + b.name.length;\n else if (b.type === 'tool_result') chars += b.content.length;\n // R3-339: an image is large and MUST be accounted for, or it escapes exactly the\n // budget the accounting exists to enforce. base64 is ~4/3 of the bytes, and the\n // provider bills tokens per pixel area — the base64 length is the honest local\n // proxy for \"this is big\", and over-counting is the safe direction.\n else if (b.type === 'image') chars += b.data.length;\n // R3-335: reasoning occupies the window like anything else. Not counting it would\n // let a thinking model overrun the context the accounting exists to protect.\n else if (b.type === 'reasoning') chars += b.text.length + (b.redactedData?.length ?? 0);\n }\n }\n return Math.ceil(chars / 4);\n}\n\n/** Should the loop compact now? True once the running context passes\n * `window − reserveTokens`. Disabled (false) when there is no window. */\nexport function shouldCompact(contextTokens: number, window: number | undefined, reserveTokens: number): boolean {\n if (!window || window <= 0) return false;\n return contextTokens > window - reserveTokens;\n}\n\n/** Prefix marking a `user` message as a compaction summary (not a real user turn),\n * so the transcript renderer shows a \"compacted N turns\" affordance on replay. */\nexport const COMPACTION_MARKER = '␟[compacted-context]\\n';\n\nconst SUMMARY_SYSTEM =\n 'You are compacting a coding-agent transcript to fit the context window. Produce a ' +\n 'DENSE structured summary under these exact headings: Goal / Constraints / Progress / ' +\n 'Decisions / Next Steps / Critical Context. PRESERVE VERBATIM every file path, symbol/' +\n 'identifier, and error string that later steps will need — do not paraphrase them. Be ' +\n 'terse everywhere else. Output only the summary.';\nconst SUMMARY_INSTRUCTION =\n 'Summarize everything above into the structured block. Keep exact paths, symbols, and ' +\n 'error strings verbatim so work can continue from the summary alone.';\n\n/** Compact `messages` by folding the older head into a structured summary and keeping\n * a verbatim recent tail. The tail is snapped to start at an `assistant` message so a\n * `tool_use`/`tool_result` pair is never split (which would malform the next request).\n * The taint tier is NOT modelled on messages (it is run-scoped host state, R-ASG-2):\n * this is a pure content transform over the SAME session — it starts no new external\n * read — so it cannot launder taint (F6). Returns the original array unchanged when\n * there is nothing safe to summarize. */\nexport async function compactTranscript(\n messages: AgentMessage[],\n client: ModelClient,\n keepRecentTurns: number,\n): Promise<{ messages: AgentMessage[]; summarizedCount: number }> {\n if (messages.length <= keepRecentTurns + 1) return { messages, summarizedCount: 0 };\n\n // Snap the tail boundary to an assistant message so tool_use/tool_result pairs stay\n // together and the summary (a `user` turn) is followed by an `assistant` turn.\n // Prefer the first assistant at/after the keep-recent boundary; fall back to the\n // last assistant in the transcript so the tail is always well-formed.\n const boundary = Math.max(1, messages.length - keepRecentTurns);\n let tailStart = -1;\n for (let i = boundary; i < messages.length; i++) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n if (tailStart === -1) {\n for (let i = messages.length - 1; i >= 1; i--) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n }\n if (tailStart <= 0) return { messages, summarizedCount: 0 };\n\n const head = messages.slice(0, tailStart);\n // Compaction DROPS both image parts (R3-339) and reasoning (R3-335) from the kept\n // tail, each by an explicit rule — an implicit answer here is what corrupts a\n // transcript quietly.\n //\n // IMAGES: the largest and least summarisable thing in a transcript, and the summary\n // the head folds into is TEXT. The `tool_result` that named the image stays, so the\n // model still knows it looked at `assets/mock.png` and what it concluded; it simply\n // cannot look again without re-reading the file, which it can do.\n //\n // REASONING: only ever required by the turn that FOLLOWS it, and compaction rewrites\n // at a turn boundary — so nothing after it is mid-chain and nothing needs the block\n // replayed. Keeping them would spend the window on its most disposable content.\n const tail = messages.map(dropImages).map(dropReasoning).slice(tailStart);\n\n // Ask the model to summarize the head. Append the instruction to the final head\n // message when it is a `user` turn (avoids introducing consecutive user turns).\n const reqMessages: AgentMessage[] = head.map((m) => ({ role: m.role, content: [...m.content] }));\n const lastMsg = reqMessages[reqMessages.length - 1];\n if (lastMsg && lastMsg.role === 'user') {\n lastMsg.content = [...lastMsg.content, { type: 'text', text: SUMMARY_INSTRUCTION }];\n } else {\n reqMessages.push({ role: 'user', content: [{ type: 'text', text: SUMMARY_INSTRUCTION }] });\n }\n\n let summaryText = '(summary unavailable)';\n try {\n const res = await client.createMessage({ system: SUMMARY_SYSTEM, messages: reqMessages, tools: [] });\n summaryText = textOf(res.content).trim() || summaryText;\n } catch {\n // Summarization itself failed — keep the original transcript (caller will retry\n // or hit the safety-stop). Better a longer context than a lost transcript.\n return { messages, summarizedCount: 0 };\n }\n\n const summaryMsg: AgentMessage = {\n role: 'user',\n content: [{ type: 'text', text: COMPACTION_MARKER + summaryText }],\n };\n return { messages: [summaryMsg, ...tail], summarizedCount: head.length };\n}\n\n/** Strip blocks of one kind from a message, keeping everything else in order. A message\n * left with no content at all keeps a single empty text block so the role sequence\n * stays well-formed (a content-less message is rejected by most providers). */\nfunction dropBlocks(m: AgentMessage, kind: 'image' | 'reasoning'): AgentMessage {\n if (!m.content.some((b) => b.type === kind)) return m;\n const kept = m.content.filter((b) => b.type !== kind);\n return { role: m.role, content: kept.length ? kept : [{ type: 'text', text: '' }] };\n}\n\n/** Compaction's image-drop rule (R3-339). */\nconst dropImages = (m: AgentMessage): AgentMessage => dropBlocks(m, 'image');\n/** Compaction's reasoning-drop rule (R3-335). */\nconst dropReasoning = (m: AgentMessage): AgentMessage => dropBlocks(m, 'reasoning');\n\n/**\n * The host's typed code for \"the conversation no longer fits\" — produced by site-main's\n * `PROVIDER_ERROR_CODES` vocabulary (`src/editor/llm/providerErrors.ts`): the host maps\n * BOTH a provider's own context overflow AND the relay's bound refusal to this one code.\n * The SDK cannot import the host's vocabulary, so the literal has exactly one home HERE,\n * pointed at its producer.\n */\nexport const HOST_CONTEXT_OVERFLOW_CODE = 'context-too-large';\n\n/** Does this thrown error look like a hard context-window overflow? Used to trigger\n * recover-then-retry compaction (F3/exit-c) rather than a dead loop. */\nexport function isContextOverflow(e: unknown): boolean {\n const msg = ((e as Error)?.message ?? String(e)).toLowerCase();\n const code = String((e as { code?: unknown })?.code ?? '').toLowerCase();\n return (\n // The host's own typed code, matched EXACTLY: the host is the one place that\n // decides what counts as an overflow (R3-588) — a relay `too-large` it did NOT\n // translate must not sneak in as a substring of some message.\n code === HOST_CONTEXT_OVERFLOW_CODE ||\n code.includes('context_length') ||\n code.includes('context-length') ||\n /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg)\n );\n}\n\n// The user turn injected when a truncated (`max_tokens`) turn emitted tool calls: we\n// fail the partial calls rather than execute them (F3), and tell the model to retry.\nconst TRUNCATED_RETRY_TEXT =\n 'That turn was cut off at the token limit mid tool-call, so the call was NOT executed. ' +\n 'Emit a smaller step: fewer/shorter tool calls, or a smaller file write.';\n\n/** The cumulative cache counters, as the event fields that carry them. A counter no\n * provider has reported yet is OMITTED, never zeroed: \"the provider reports nothing\"\n * and \"the provider cached nothing\" are different facts and the consumer must be able\n * to tell them apart. Three event payloads state this rule; this is the one place it\n * is spelled out. */\nconst cacheCounterFields = (\n cacheReadTokens: number | undefined,\n cacheWriteTokens: number | undefined,\n): { cacheReadTokens?: number; cacheWriteTokens?: number } => ({\n ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),\n ...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),\n});\n\n/**\n * Drive the agent loop to completion. Returns the full message transcript\n * (including the kickoff user turn). Stops when the model returns without tool\n * calls (or a terminal stop reason), when the token budget is exhausted, or when\n * `maxTurns` (a large safety-stop) is reached. With a `contextWindow` set, the loop\n * accounts tokens and compacts automatically so it can run long.\n */\nexport async function runAgent(opts: RunAgentOptions): Promise<AgentMessage[]> {\n const { client, tools, execute, system, prompt, events, signal, steering, pause } = opts;\n const maxTurns = opts.maxTurns ?? 100;\n const maxNudges = opts.maxNudges ?? 1;\n const maxTruncationRetries = opts.maxTruncationRetries ?? 2;\n const window = opts.contextWindow;\n const reserveTokens = opts.reserveTokens ?? (window ? Math.floor(window * 0.25) : 0);\n const keepRecentTurns = opts.keepRecentTurns ?? 8;\n\n let messages: AgentMessage[] = [...(opts.history ?? []), { role: 'user', content: [{ type: 'text', text: prompt }] }];\n\n // Consecutive-stall counter: how many times in a row we've nudged a no-tool-call\n // turn. Reset to 0 by any turn that DOES call a tool, so the budget is per stall\n // *episode*, not per run.\n let nudges = 0;\n let truncationRetries = 0;\n // True when the previous iteration's model turn was cut short by an `interrupt`\n // steer, so the injected correction can be reported as an interruption.\n let interruptedLastTurn = false;\n // Running context size (provider-reported when available) + cumulative spend.\n let contextTokens = 0;\n let spentTokens = 0;\n // R3-336 — cumulative cache accounting. `undefined` until a provider reports\n // something, so \"reports nothing\" stays distinguishable from \"cached nothing\".\n let cacheReadTokens: number | undefined;\n let cacheWriteTokens: number | undefined;\n\n // Compact the transcript and, when anything was actually folded in, adopt the\n // compacted messages, re-estimate the running context, and report it. Returns how\n // many messages were summarized — 0 means \"there was nothing to compact\", which is\n // what the overflow-recovery path below treats as unrecoverable. Both compaction\n // sites (near-window before a request, and hard-overflow recovery) go through here,\n // so they cannot drift on what a compaction updates or reports.\n const compactAndReport = async (): Promise<number> => {\n const { messages: compacted, summarizedCount } = await compactTranscript(messages, client, keepRecentTurns);\n if (summarizedCount > 0) {\n messages = compacted;\n contextTokens = estimateTokens(messages);\n events?.onCompact?.({ summarizedCount, ...cacheCounterFields(cacheReadTokens, cacheWriteTokens) });\n }\n return summarizedCount;\n };\n\n for (let turn = 0; turn < maxTurns; turn++) {\n // R3-224 (§3.3): the stop button, checked between turns. Combined with the\n // per-request `signal` below (which aborts the in-flight upstream turn), this\n // halts \"the loop between tool calls AND aborts the in-flight LLM request\".\n if (signal?.aborted) break;\n\n // R3-562 (§7 R-ARD-20a): if the region is hidden, stop HERE — at the boundary, with\n // the previous turn's tool batch fully paired — and wait for the reveal. Placed after\n // the stop check and before the steer drain so a correction queued while hidden is\n // applied on the way back in, as the very next turn, rather than a turn late.\n if (pause?.isPaused()) {\n events?.onPause?.({ turn });\n await pause.whenResumed(signal);\n events?.onResume?.({ turn });\n // `whenResumed` also resolves on abort, so a run stopped while hidden lands here\n // rather than awaiting a reveal that never comes.\n if (signal?.aborted) break;\n }\n\n // R3-333: apply any queued corrections at the TURN BOUNDARY, before the next\n // request, so the model's very next turn reflects them. Draining here (rather\n // than at the point of arrival) is what makes a steer safe: whatever the loop\n // was doing — streaming a turn, running a tool batch — has finished.\n if (steering) {\n const steers = steering.drain();\n if (steers.length) {\n messages.push({\n role: 'user',\n content: steers.map((m) => ({ type: 'text' as const, text: steerWireText(m) })),\n });\n events?.onSteer?.({ messages: steers, interrupted: interruptedLastTurn });\n }\n interruptedLastTurn = false;\n steering.rearm();\n }\n\n // Compact BEFORE the next request when the running context is near the window.\n if (shouldCompact(contextTokens, window, reserveTokens)) await compactAndReport();\n\n // The in-flight turn is abortable by EITHER verb: STOP (ends the run) or an\n // `interrupt`-mode STEER (ends the turn, keeps the run). They are composed into\n // one per-turn signal, and told apart in the catch by asking which fired.\n const turnAbort = anySignal([signal, steering?.interrupt]);\n // Capture what the model had streamed when a steer cut in, so the interrupted\n // turn is recorded as what actually happened rather than dropped.\n let partialText = '';\n const onTextDelta = (text: string): void => {\n partialText += text;\n events?.onAssistantDelta?.(text);\n };\n const sendTurn = () =>\n client.createMessage({\n system,\n messages,\n tools,\n // R3-333's local `onTextDelta` (it captures the partial text a steer may cut\n // short) — NOT `events.onAssistantDelta` directly.\n onTextDelta,\n // R3-335's reasoning stream rides alongside it.\n onReasoningDelta: events?.onReasoningDelta,\n // R3-333: STOP composed with the steer INTERRUPT, so either verb ends the turn.\n signal: turnAbort.signal,\n });\n let res: ModelResponse;\n try {\n try {\n res = await sendTurn();\n } catch (e) {\n // Recover-then-retry on a hard context-overflow (exit-c): compact once and\n // re-send. If there is nothing to compact, or the retry also overflows, the\n // error propagates — a bounded recovery, never a dead loop.\n if (turnAbort.signal.aborted || !isContextOverflow(e)) throw e;\n if ((await compactAndReport()) === 0) throw e;\n res = await sendTurn();\n }\n } catch (e) {\n // R3-224: a mid-turn abort surfaces as a thrown (Abort/Stream)Error. Treat it\n // as a CLEAN stop — return the transcript so far — not a failure to bubble up.\n if (signal?.aborted) {\n turnAbort.dispose();\n break;\n }\n // R3-333: the SAME thrown abort, but from a steer — the run continues. Record\n // the turn the user cut short (an assistant message, so the transcript keeps\n // strict role alternation and replay shows the interruption where it happened),\n // then loop: the drain at the top of the next iteration injects the correction.\n if (steering?.interrupt.aborted) {\n turnAbort.dispose();\n messages.push({\n role: 'assistant',\n content: [{ type: 'text', text: partialText.trim() || INTERRUPTED_TURN_TEXT }],\n });\n events?.onAssistantText?.(partialText.trim() || INTERRUPTED_TURN_TEXT);\n interruptedLastTurn = true;\n continue;\n }\n turnAbort.dispose();\n throw e;\n }\n turnAbort.dispose();\n\n // Token accounting (R3-220): prefer the provider `usage`, else estimate. `turnCost`\n // is what this turn billed (input + output); `contextTokens` is the current window\n // occupancy (drives compaction); `spentTokens` is cumulative run spend (input is\n // re-billed every turn, so summing turnCost is the true cost signal).\n const turnCost = res.usage\n ? res.usage.inputTokens + res.usage.outputTokens\n : estimateTokens(messages) + Math.ceil(textOf(res.content).length / 4);\n contextTokens = turnCost;\n spentTokens += turnCost;\n if (res.usage?.cacheReadTokens !== undefined) {\n cacheReadTokens = (cacheReadTokens ?? 0) + res.usage.cacheReadTokens;\n }\n if (res.usage?.cacheWriteTokens !== undefined) {\n cacheWriteTokens = (cacheWriteTokens ?? 0) + res.usage.cacheWriteTokens;\n }\n events?.onUsage?.({\n contextTokens,\n window,\n spentTokens,\n ...cacheCounterFields(cacheReadTokens, cacheWriteTokens),\n });\n\n const assistantText = textOf(res.content);\n if (assistantText) events?.onAssistantText?.(assistantText);\n // R3-335: reasoning stays IN the message sequence — a provider that requires the\n // block echoed back gets it from `messages`, not from a side channel.\n for (const b of res.content) if (b.type === 'reasoning') events?.onReasoning?.(b);\n messages.push({ role: 'assistant', content: res.content });\n\n const toolUses = res.content.filter((b): b is ToolUseBlock => b.type === 'tool_use');\n\n // Truncated-tool-call guard (F3): a `max_tokens` turn that emitted tool calls\n // was cut off mid-call, so its args may be partial. Do NOT execute them — fail\n // each with an error tool_result (keeps the conversation well-formed) and\n // re-prompt for a smaller step, bounded by maxTruncationRetries.\n if (res.stopReason === 'max_tokens' && toolUses.length > 0) {\n events?.onTruncatedToolCall?.();\n const failed: ContentBlock[] = toolUses.map((c) => ({\n type: 'tool_result',\n tool_use_id: c.id,\n content: 'tool call truncated by the token limit — not executed',\n is_error: true,\n }));\n failed.push({ type: 'text', text: TRUNCATED_RETRY_TEXT });\n messages.push({ role: 'user', content: failed });\n if (++truncationRetries > maxTruncationRetries) break;\n continue;\n }\n truncationRetries = 0;\n\n if (toolUses.length === 0) {\n // No tool calls. Usually the model is genuinely done — but GLM/OpenRouter\n // intermittently ends with \"I'll read the files…\" or an empty turn after a\n // tool error and no call (findings §2). Nudge such a STALL back into action\n // once (per episode), respecting terminal stops and a real wrap-up.\n const stall = TERMINAL_STOPS.has(res.stopReason) ? null : detectStall(assistantText);\n if (stall && nudges < maxNudges) {\n nudges++;\n events?.onNudge?.(stall);\n messages.push({ role: 'user', content: [{ type: 'text', text: NUDGE_TEXT }] });\n continue;\n }\n // R3-333 follow-up: the model is done, but the user queued something while it\n // was working. Continue rather than end — the drain at the top of the next\n // iteration turns the queued message into the next turn's prompt. This is the\n // difference between a follow-up and a restart.\n if (steering?.hasPending()) continue;\n break;\n }\n\n nudges = 0; // a productive turn clears the stall budget\n\n const results: ToolResultBlock[] = [];\n // R3-339 — image parts produced by tools this turn. They ride in the SAME user\n // message as the results (after them), because a `tool_result`'s content is a string\n // on the wire; this is the shape both host adapters map to their provider.\n const images: ImageBlock[] = [];\n for (const call of toolUses) {\n events?.onToolUse?.(call.name, call.input);\n let outcome: ToolOutcome;\n try {\n outcome = await execute(call.name, call.input);\n } catch (e) {\n // A thrown executor error (e.g. host `forbidden`) becomes an error\n // tool_result so the model sees the gate's verdict and can adapt.\n const code = (e as { code?: string })?.code;\n const msg = (e as Error)?.message ?? String(e);\n outcome = { content: code ? `${code}: ${msg}` : msg, isError: true };\n }\n events?.onToolResult?.(call.name, outcome);\n results.push({\n type: 'tool_result',\n tool_use_id: call.id,\n content: outcome.content,\n is_error: outcome.isError,\n });\n if (outcome.images?.length) images.push(...outcome.images);\n }\n messages.push({ role: 'user', content: [...results, ...images] });\n\n // Runaway-cost guard: stop once cumulative spend passes the budget (the token/\n // spend bound that replaces the old raw turn cap). Compaction keeps a single\n // request small; this bounds the whole run.\n if (opts.tokenBudget && spentTokens >= opts.tokenBudget) {\n events?.onBudgetStop?.({ spentTokens, tokenBudget: opts.tokenBudget });\n break;\n }\n }\n\n return messages;\n}\n"],"mappings":"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","names":[]}
@@ -273,6 +273,14 @@ declare function compactTranscript(messages: AgentMessage[], client: ModelClient
273
273
  messages: AgentMessage[];
274
274
  summarizedCount: number;
275
275
  }>;
276
+ /**
277
+ * The host's typed code for "the conversation no longer fits" — produced by site-main's
278
+ * `PROVIDER_ERROR_CODES` vocabulary (`src/editor/llm/providerErrors.ts`): the host maps
279
+ * BOTH a provider's own context overflow AND the relay's bound refusal to this one code.
280
+ * The SDK cannot import the host's vocabulary, so the literal has exactly one home HERE,
281
+ * pointed at its producer.
282
+ */
283
+ declare const HOST_CONTEXT_OVERFLOW_CODE = "context-too-large";
276
284
  /** Does this thrown error look like a hard context-window overflow? Used to trigger
277
285
  * recover-then-retry compaction (F3/exit-c) rather than a dead loop. */
278
286
  declare function isContextOverflow(e: unknown): boolean;
@@ -285,4 +293,4 @@ declare function isContextOverflow(e: unknown): boolean;
285
293
  */
286
294
  declare function runAgent(opts: RunAgentOptions): Promise<AgentMessage[]>;
287
295
 
288
- export { type AgentEvents, type AgentMessage, type AgentRole, type AgentTool, COMPACTION_MARKER, type ContentBlock, type ImageBlock, type ModelClient, type ModelResponse, NUDGE_TEXT, type ReasoningBlock, type RunAgentOptions, type StallReason, type TextBlock, type TokenUsage, type ToolExecutor, type ToolOutcome, type ToolResultBlock, type ToolUseBlock, compactTranscript, detectStall, estimateTokens, isContextOverflow, runAgent, shouldCompact };
296
+ export { type AgentEvents, type AgentMessage, type AgentRole, type AgentTool, COMPACTION_MARKER, type ContentBlock, HOST_CONTEXT_OVERFLOW_CODE, type ImageBlock, type ModelClient, type ModelResponse, NUDGE_TEXT, type ReasoningBlock, type RunAgentOptions, type StallReason, type TextBlock, type TokenUsage, type ToolExecutor, type ToolOutcome, type ToolResultBlock, type ToolUseBlock, compactTranscript, detectStall, estimateTokens, isContextOverflow, runAgent, shouldCompact };
@@ -273,6 +273,14 @@ declare function compactTranscript(messages: AgentMessage[], client: ModelClient
273
273
  messages: AgentMessage[];
274
274
  summarizedCount: number;
275
275
  }>;
276
+ /**
277
+ * The host's typed code for "the conversation no longer fits" — produced by site-main's
278
+ * `PROVIDER_ERROR_CODES` vocabulary (`src/editor/llm/providerErrors.ts`): the host maps
279
+ * BOTH a provider's own context overflow AND the relay's bound refusal to this one code.
280
+ * The SDK cannot import the host's vocabulary, so the literal has exactly one home HERE,
281
+ * pointed at its producer.
282
+ */
283
+ declare const HOST_CONTEXT_OVERFLOW_CODE = "context-too-large";
276
284
  /** Does this thrown error look like a hard context-window overflow? Used to trigger
277
285
  * recover-then-retry compaction (F3/exit-c) rather than a dead loop. */
278
286
  declare function isContextOverflow(e: unknown): boolean;
@@ -285,4 +293,4 @@ declare function isContextOverflow(e: unknown): boolean;
285
293
  */
286
294
  declare function runAgent(opts: RunAgentOptions): Promise<AgentMessage[]>;
287
295
 
288
- export { type AgentEvents, type AgentMessage, type AgentRole, type AgentTool, COMPACTION_MARKER, type ContentBlock, type ImageBlock, type ModelClient, type ModelResponse, NUDGE_TEXT, type ReasoningBlock, type RunAgentOptions, type StallReason, type TextBlock, type TokenUsage, type ToolExecutor, type ToolOutcome, type ToolResultBlock, type ToolUseBlock, compactTranscript, detectStall, estimateTokens, isContextOverflow, runAgent, shouldCompact };
296
+ export { type AgentEvents, type AgentMessage, type AgentRole, type AgentTool, COMPACTION_MARKER, type ContentBlock, HOST_CONTEXT_OVERFLOW_CODE, type ImageBlock, type ModelClient, type ModelResponse, NUDGE_TEXT, type ReasoningBlock, type RunAgentOptions, type StallReason, type TextBlock, type TokenUsage, type ToolExecutor, type ToolOutcome, type ToolResultBlock, type ToolUseBlock, compactTranscript, detectStall, estimateTokens, isContextOverflow, runAgent, shouldCompact };
package/dist/agentLoop.js CHANGED
@@ -80,10 +80,16 @@ function dropBlocks(m, kind) {
80
80
  }
81
81
  const dropImages = (m) => dropBlocks(m, "image");
82
82
  const dropReasoning = (m) => dropBlocks(m, "reasoning");
83
+ const HOST_CONTEXT_OVERFLOW_CODE = "context-too-large";
83
84
  function isContextOverflow(e) {
84
85
  const msg = (e?.message ?? String(e)).toLowerCase();
85
86
  const code = String(e?.code ?? "").toLowerCase();
86
- return code.includes("context_length") || code.includes("context-length") || /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg);
87
+ return (
88
+ // The host's own typed code, matched EXACTLY: the host is the one place that
89
+ // decides what counts as an overflow (R3-588) — a relay `too-large` it did NOT
90
+ // translate must not sneak in as a substring of some message.
91
+ code === HOST_CONTEXT_OVERFLOW_CODE || code.includes("context_length") || code.includes("context-length") || /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg)
92
+ );
87
93
  }
88
94
  const TRUNCATED_RETRY_TEXT = "That turn was cut off at the token limit mid tool-call, so the call was NOT executed. Emit a smaller step: fewer/shorter tool calls, or a smaller file write.";
89
95
  const cacheCounterFields = (cacheReadTokens, cacheWriteTokens) => ({
@@ -259,6 +265,7 @@ async function runAgent(opts) {
259
265
  }
260
266
  export {
261
267
  COMPACTION_MARKER,
268
+ HOST_CONTEXT_OVERFLOW_CODE,
262
269
  NUDGE_TEXT,
263
270
  compactTranscript,
264
271
  detectStall,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/agentLoop.ts"],"sourcesContent":["// Provider-agnostic agentic tool-use loop — PORTED from agent-demo `src/lib/agentLoop.ts`\n// (GROVE_AGENT_SPEC §7: embedded agents REUSE this seam rather than reimplementing it;\n// the SDK is its shared home so every app's agent loop is the same exercised machinery).\n// Renames for the SDK's flat export surface: `ChatMessage`→`AgentMessage`, `Role`→`AgentRole`.\n// Provider-agnostic agentic tool-use loop (LLM_AND_AGENTS_SPEC §3.3). The loop is\n// the heart of the in-browser coding agent: send the conversation + tool list to a\n// ModelClient, execute any tool calls the model emits, append the results, and\n// repeat until the model stops, a spend budget is hit, or a large safety-stop is\n// reached. The ModelClient seam keeps the loop independent of any one provider\n// (host `chat()` impl: chatModelClient.ts).\n//\n// Confinement (G12/T24) is NOT enforced here — it falls out of the capability\n// model: the `tools` handed to the model ARE the app's grant-filtered §5.5\n// catalog (agentTools.ts), and `execute` routes through the host's gated\n// `invoke()`, so an off-catalog/hallucinated tool returns `forbidden` at the host.\n//\n// R3-220 (AHG-1) adds the machinery that lets the loop run LONG enough to build a\n// real app: token accounting (from the provider `usage` delta), automatic context\n// COMPACTION when the window fills, a truncated-tool-call guard, and a spend budget\n// replacing the old fixed 12-turn cap. All of it is inert unless a `contextWindow`\n// is supplied, so a caller that passes none behaves exactly as before.\n//\n// PREFIX STABILITY IS LOAD-BEARING (R3-336). The loop's contribution to prompt caching\n// is structural, not a parameter: `system` and `tools` are fixed for a run and are sent\n// BYTE-IDENTICALLY on every turn, while everything that changes is appended to\n// `messages`. That is what the host's cache breakpoints key on. Rebuilding the system\n// prompt per turn — re-stamping a date, re-ordering the tool list — would cost nothing\n// visible and silently turn every cache read into a cache write, so it is asserted in\n// the tests rather than left as a convention.\n\nimport { anySignal, steerWireText, INTERRUPTED_TURN_TEXT, type SteerMessage, type SteerSource } from './agentSteering';\nimport type { PauseSource } from './agentPause';\n\nexport type TextBlock = { type: 'text'; text: string };\n/**\n * An image the model can look at (R3-339). `data` is base64 with no `data:` prefix,\n * matching the SDK `ContentPart` the transport already accepts.\n *\n * Carried as its OWN block rather than stuffed inside a `tool_result`, because a tool\n * result's content is a string on the wire — the loop appends the image to the same\n * user message that carries the results, which is the shape both host adapters map.\n */\nexport type ImageBlock = { type: 'image'; mimeType: string; data: string };\n/**\n * A block of the model's own reasoning (R3-335).\n *\n * Kept in the message sequence rather than rendered and thrown away, for two reasons:\n * the user needs to see what the model is doing during the long stretches compaction\n * now makes possible, and some providers REQUIRE the block echoed back — with its\n * `signature` — for the following turn of a tool-use chain to stay valid. A loop that\n * drops them is quietly lossy in a way that shows up as degraded output, not an error.\n *\n * `redactedData` carries provider-redacted reasoning: opaque bytes with no readable\n * text, which still have to be replayed in place. Never render it.\n */\nexport type ReasoningBlock = {\n type: 'reasoning';\n text: string;\n signature?: string;\n redactedData?: string;\n};\nexport type ToolUseBlock = { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> };\nexport type ToolResultBlock = { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean };\nexport type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock | ReasoningBlock;\n\n/** A tool the model may call: name, description, and a JSON Schema for its input\n * (`input_schema`, the Anthropic wire name — {@link createChatModelClient} maps it to the\n * chat slot's `ToolDef`). */\nexport interface AgentTool {\n name: string;\n description: string;\n input_schema: Record<string, unknown>;\n}\n\nexport type AgentRole = 'user' | 'assistant';\nexport interface AgentMessage {\n role: AgentRole;\n content: ContentBlock[];\n}\n\n/** Provider-reported token counts for one turn (R3-220). `inputTokens` is the size\n * of everything the provider processed this turn; `outputTokens` is what it\n * generated. Absent when the provider emits no `usage` delta. */\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n /** R3-336 — prompt-cache counters, present only where the provider reports them.\n * ABSENT is not zero: it means this provider says nothing about caching, which is a\n * different fact from \"nothing was cached\", and conflating them would turn a\n * measurement into a guess. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n}\n\n/** One model turn: the assistant's emitted blocks + why it stopped (+ usage). */\nexport interface ModelResponse {\n content: (TextBlock | ToolUseBlock | ReasoningBlock)[];\n /** Anthropic stop_reason: 'end_turn' | 'tool_use' | 'max_tokens' | 'refusal' | … */\n stopReason: string;\n /** Provider token counts for this turn, when reported (R3-220 accounting). */\n usage?: TokenUsage;\n}\n\n/** The provider seam — one model turn. Implemented by `chatModelClient.ts` over\n * the host `chat()` slot; faked in tests. When the client streams, it calls\n * `onTextDelta` with each token slice as it arrives (the assembled turn is still\n * returned whole); a non-streaming client simply never calls it. */\nexport interface ModelClient {\n createMessage(req: {\n system?: string;\n messages: AgentMessage[];\n tools: AgentTool[];\n /** Called with incremental assistant-text slices during a streamed turn. */\n onTextDelta?: (text: string) => void;\n /** R3-335: incremental REASONING slices, for a live thinking surface. Never called\n * by a provider that does not emit reasoning. */\n onReasoningDelta?: (text: string) => void;\n /** R3-224: aborts the in-flight turn — the host stops the upstream provider\n * request and stops billing, not just the app-side stream (§3.3). */\n signal?: AbortSignal;\n }): Promise<ModelResponse>;\n}\n\n/** Executes one tool call, returning a string result (and whether it errored —\n * a `forbidden`/failed call comes back as `is_error` so the model can adapt). */\nexport type ToolExecutor = (name: string, input: Record<string, unknown>) => Promise<ToolOutcome>;\n\n/** What one tool call produced. `images` (R3-339) is how a tool hands the model\n * something to LOOK at; `content` still carries the text the model reads. */\nexport interface ToolOutcome {\n content: string;\n isError?: boolean;\n images?: ImageBlock[];\n}\n\n/** Why a no-tool-call turn looked like a stall rather than a genuine finish. */\nexport type StallReason = 'empty' | 'announced-no-call';\n\n/** Optional UI hooks so a panel can render the loop as it runs. */\nexport interface AgentEvents {\n /** A streamed token slice of the in-flight assistant turn (live preview). */\n onAssistantDelta?(text: string): void;\n /** The complete assistant text for a turn, once the turn is in. */\n onAssistantText?(text: string): void;\n onToolUse?(name: string, input: Record<string, unknown>): void;\n onToolResult?(name: string, result: ToolOutcome): void;\n /** Fired when the loop nudges a STALLED turn (the model ended without a tool\n * call despite empty or \"I'll do X\" intent text) back into action, so a panel\n * can show \"nudging the model to continue\" rather than a silent stall. */\n onNudge?(reason: StallReason): void;\n /** Fired after every turn with the running context size + window (R3-220\n * loop-observability). `contextTokens` is provider-reported when available, else\n * a char/4 estimate. */\n onUsage?(usage: {\n contextTokens: number;\n window?: number;\n spentTokens: number;\n /** R3-336 — cumulative cache reads/writes across the run, on providers that report\n * them. Surfacing this is what makes the caching claim verifiable rather than\n * believed; `undefined` means the provider reported nothing. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }): void;\n /** Fired when the loop compacts the transcript to stay under the context window;\n * `summarizedCount` is how many older messages were folded into the summary.\n *\n * R3-336: a compaction invalidates the conversation-prefix cache it rewrote — the\n * durable system+tools prefix survives it — so the next turn pays one prefix\n * re-write. `cacheReadTokens`/`cacheWriteTokens` are the run totals AT the\n * compaction, which is what lets the cost curve across it be read off rather than\n * assumed (exit 2). */\n onCompact?(info: { summarizedCount: number; cacheReadTokens?: number; cacheWriteTokens?: number }): void;\n /** Fired when the loop stops because the token/spend budget was exhausted. */\n onBudgetStop?(info: { spentTokens: number; tokenBudget: number }): void;\n /** Fired when a turn was truncated (`max_tokens`) while emitting tool calls, so\n * the partial calls were failed-and-re-prompted rather than executed (R3-220 F3). */\n onTruncatedToolCall?(): void;\n /** R3-335: a streamed slice of the model's reasoning, for a live thinking surface. */\n onReasoningDelta?(text: string): void;\n /** R3-335: the complete reasoning block for a turn, once the turn is in. */\n onReasoning?(block: ReasoningBlock): void;\n /** R3-333: the loop applied the user's mid-run correction(s). `interrupted` is\n * true when an `interrupt`-mode steer cut an in-flight model turn short (as\n * opposed to being applied at an ordinary turn boundary). */\n onSteer?(info: { messages: SteerMessage[]; interrupted: boolean }): void;\n /** R3-562: the loop reached a turn boundary while its region was hidden and stopped\n * advancing. A surface can say \"paused — this view is hidden\" instead of looking hung. */\n onPause?(info: { turn: number }): void;\n /** R3-562: the region was revealed (or the run was stopped) and the loop resumed. */\n onResume?(info: { turn: number }): void;\n}\n\nexport interface RunAgentOptions {\n client: ModelClient;\n tools: AgentTool[];\n execute: ToolExecutor;\n system?: string;\n /** Prior turns of this conversation, replayed before the new prompt so a\n * follow-up has context (the conversation stage seeds this from the store). */\n history?: AgentMessage[];\n /** The user's instruction that kicks off the loop. */\n prompt: string;\n /** Large safety-stop on model turns (default 100). No longer the primary bound —\n * a long task is bounded by `tokenBudget` + compaction; this just backstops a\n * pathological loop the budget/compaction somehow miss. */\n maxTurns?: number;\n /** Max consecutive \"you announced work but emitted no tool call\" nudges before\n * the loop gives up (default 1). GLM-over-OpenRouter intermittently ends a turn\n * with future-tense intent (\"I'll read the files…\") or an EMPTY turn right after\n * a tool error — no tool call, a silent stall (tutorial findings §2). One nudge\n * recovers most of these; the cap keeps a genuinely-finished model (which answers\n * the nudge with another call-free turn) from looping, and the budget resets on\n * any turn that DID call a tool, so a long task's later stall is still covered.\n * Set 0 to disable the backstop. */\n maxNudges?: number;\n // ---- R3-220 accounting / compaction (all inert unless `contextWindow` is set) ----\n /** The resolved provider's context window (`describeChat().features.maxContextTokens`).\n * Compaction is disabled when this is absent/0 — the loop then behaves as before. */\n contextWindow?: number;\n /** Headroom left below the window before compacting (default: 25% of the window). */\n reserveTokens?: number;\n /** Recent messages kept verbatim across a compaction (default 8). */\n keepRecentTurns?: number;\n /** Cumulative token budget (input+output across turns). When exceeded the loop\n * stops — the runaway-cost guard that replaces the raw 12-turn cap. Off when unset. */\n tokenBudget?: number;\n /** Max consecutive truncated-tool-call re-prompts before giving up (default 2). */\n maxTruncationRetries?: number;\n /** R3-224 (§3.3): the stop button. When it fires the loop stops between turns AND\n * aborts the in-flight model turn (the host tears down the upstream provider\n * request and stops billing) — not merely the between-turn loop. The transcript so\n * far is returned; an abort is a clean stop, never a thrown error. */\n signal?: AbortSignal;\n /** R3-333: the mid-run steering queue. The loop drains it at every turn boundary\n * and folds each correction in as a `user` message, so the human can redirect a\n * run without restarting it and paying for the transcript again. Its `interrupt`\n * signal aborts the in-flight MODEL turn only — never a tool batch, which must\n * keep every `tool_use` paired with a `tool_result`. Absent ⇒ the loop behaves\n * exactly as before. */\n steering?: SteerSource;\n /**\n * R3-562 (AGENT_RUN_DURABILITY_SPEC §7 R-ARD-20a): pause the run while nobody can see\n * or stop it — the host has hidden this app's region but kept the frame mounted.\n *\n * Read at the TURN BOUNDARY only, so every `tool_use` still has its `tool_result` when\n * the loop stops advancing. Nothing is torn down and nothing is injected: the run\n * simply does not start its next turn until the region is revealed, then continues\n * with no repair pass and no resume gate. Omitted ⇒ the loop never pauses, exactly as\n * before.\n */\n pause?: PauseSource;\n events?: AgentEvents;\n}\n\nconst textOf = (blocks: { type: string; text?: string }[]): string =>\n blocks\n .filter((b): b is TextBlock => b.type === 'text')\n .map((b) => b.text)\n .join('');\n\n// Terminal stops we must NOT nudge past. Only `max_tokens` survives the SDK→loop\n// mapping distinctly (chatModelClient `mapStop`: 'length'→'max_tokens', while\n// 'end'/'filtered'→'end_turn' and 'tool'→'tool_use'); a truncated turn is a\n// token-budget problem a nudge can't fix. An empty give-up after a tool error\n// arrives as 'end_turn', so it stays nudgeable.\nconst TERMINAL_STOPS = new Set(['max_tokens', 'refusal']);\n\n// Future-tense intent to ACT (\"I'll read…\", \"let me create…\", \"next I'll edit…\").\nconst INTENT_RE =\n /\\b(i'?ll|i will|i'?m going to|going to|let me|let's|now,? i(?:'?ll| will)?|next,? i(?:'?ll| will)?)\\b[\\s\\S]{0,80}?\\b(read|write|edit|creat|add|updat|modif|regist|check|look|call|run|search|grep|list|open|fetch|inspect|review|explor|implement|fix|appl)/i;\n// A wrap-up marker → treat the turn as a genuine finish, never nudge.\nconst DONE_RE =\n /\\b(done|complete|finished|all set|no (?:further|more) (?:changes|steps)|i(?:'| ha)ve (?:creat|add|updat|made|written|regist|edit|implement|fix|appli)|here'?s (?:a |the )?summ|to summ|in summ)/i;\n\n/**\n * Classify a NO-tool-call turn as a stall (nudge-worthy) vs a genuine finish.\n * GLM-over-OpenRouter intermittently (a) writes \"I'll read the files…\" then ends\n * with no call, or (b) returns an EMPTY turn after a tool error — both silent\n * give-ups (tutorial findings §2). Conservative on purpose: a real wrap-up (a\n * summary, \"Done\", \"I've created…\") returns null so the loop never nudges a\n * finished agent. Empty text is always a stall (there is nothing a finished agent\n * would say with zero words).\n */\nexport function detectStall(text: string): StallReason | null {\n const t = text.trim();\n if (!t) return 'empty';\n if (DONE_RE.test(t)) return null;\n if (INTENT_RE.test(t)) return 'announced-no-call';\n return null;\n}\n\n// The single follow-up we inject to break a stall. Directive, short, and honest\n// about the two outcomes so a genuinely-finished model just confirms and stops\n// (→ another call-free turn, which the nudge cap then lets terminate). Exported so\n// the transcript renderer can recognise the injected turn and show it as a \"nudge\"\n// row (not a user message) when a persisted conversation is replayed.\nexport const NUDGE_TEXT =\n \"You ended your turn without calling a tool. If the task is already complete, say so plainly in one line and stop. Otherwise don't just describe the next step — emit the tool call now.\";\n\n// ---- R3-220 token accounting + compaction ----------------------------------------\n\n/** Rough token estimate (~4 chars/token) over a message array, used only when the\n * provider reports no `usage` delta. Conservative by design (over- not under-counts\n * by treating structured blocks as their JSON length). */\nexport function estimateTokens(messages: AgentMessage[]): number {\n let chars = 0;\n for (const m of messages) {\n for (const b of m.content) {\n if (b.type === 'text') chars += b.text.length;\n else if (b.type === 'tool_use') chars += JSON.stringify(b.input).length + b.name.length;\n else if (b.type === 'tool_result') chars += b.content.length;\n // R3-339: an image is large and MUST be accounted for, or it escapes exactly the\n // budget the accounting exists to enforce. base64 is ~4/3 of the bytes, and the\n // provider bills tokens per pixel area — the base64 length is the honest local\n // proxy for \"this is big\", and over-counting is the safe direction.\n else if (b.type === 'image') chars += b.data.length;\n // R3-335: reasoning occupies the window like anything else. Not counting it would\n // let a thinking model overrun the context the accounting exists to protect.\n else if (b.type === 'reasoning') chars += b.text.length + (b.redactedData?.length ?? 0);\n }\n }\n return Math.ceil(chars / 4);\n}\n\n/** Should the loop compact now? True once the running context passes\n * `window − reserveTokens`. Disabled (false) when there is no window. */\nexport function shouldCompact(contextTokens: number, window: number | undefined, reserveTokens: number): boolean {\n if (!window || window <= 0) return false;\n return contextTokens > window - reserveTokens;\n}\n\n/** Prefix marking a `user` message as a compaction summary (not a real user turn),\n * so the transcript renderer shows a \"compacted N turns\" affordance on replay. */\nexport const COMPACTION_MARKER = '␟[compacted-context]\\n';\n\nconst SUMMARY_SYSTEM =\n 'You are compacting a coding-agent transcript to fit the context window. Produce a ' +\n 'DENSE structured summary under these exact headings: Goal / Constraints / Progress / ' +\n 'Decisions / Next Steps / Critical Context. PRESERVE VERBATIM every file path, symbol/' +\n 'identifier, and error string that later steps will need — do not paraphrase them. Be ' +\n 'terse everywhere else. Output only the summary.';\nconst SUMMARY_INSTRUCTION =\n 'Summarize everything above into the structured block. Keep exact paths, symbols, and ' +\n 'error strings verbatim so work can continue from the summary alone.';\n\n/** Compact `messages` by folding the older head into a structured summary and keeping\n * a verbatim recent tail. The tail is snapped to start at an `assistant` message so a\n * `tool_use`/`tool_result` pair is never split (which would malform the next request).\n * The taint tier is NOT modelled on messages (it is run-scoped host state, R-ASG-2):\n * this is a pure content transform over the SAME session — it starts no new external\n * read — so it cannot launder taint (F6). Returns the original array unchanged when\n * there is nothing safe to summarize. */\nexport async function compactTranscript(\n messages: AgentMessage[],\n client: ModelClient,\n keepRecentTurns: number,\n): Promise<{ messages: AgentMessage[]; summarizedCount: number }> {\n if (messages.length <= keepRecentTurns + 1) return { messages, summarizedCount: 0 };\n\n // Snap the tail boundary to an assistant message so tool_use/tool_result pairs stay\n // together and the summary (a `user` turn) is followed by an `assistant` turn.\n // Prefer the first assistant at/after the keep-recent boundary; fall back to the\n // last assistant in the transcript so the tail is always well-formed.\n const boundary = Math.max(1, messages.length - keepRecentTurns);\n let tailStart = -1;\n for (let i = boundary; i < messages.length; i++) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n if (tailStart === -1) {\n for (let i = messages.length - 1; i >= 1; i--) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n }\n if (tailStart <= 0) return { messages, summarizedCount: 0 };\n\n const head = messages.slice(0, tailStart);\n // Compaction DROPS both image parts (R3-339) and reasoning (R3-335) from the kept\n // tail, each by an explicit rule — an implicit answer here is what corrupts a\n // transcript quietly.\n //\n // IMAGES: the largest and least summarisable thing in a transcript, and the summary\n // the head folds into is TEXT. The `tool_result` that named the image stays, so the\n // model still knows it looked at `assets/mock.png` and what it concluded; it simply\n // cannot look again without re-reading the file, which it can do.\n //\n // REASONING: only ever required by the turn that FOLLOWS it, and compaction rewrites\n // at a turn boundary — so nothing after it is mid-chain and nothing needs the block\n // replayed. Keeping them would spend the window on its most disposable content.\n const tail = messages.map(dropImages).map(dropReasoning).slice(tailStart);\n\n // Ask the model to summarize the head. Append the instruction to the final head\n // message when it is a `user` turn (avoids introducing consecutive user turns).\n const reqMessages: AgentMessage[] = head.map((m) => ({ role: m.role, content: [...m.content] }));\n const lastMsg = reqMessages[reqMessages.length - 1];\n if (lastMsg && lastMsg.role === 'user') {\n lastMsg.content = [...lastMsg.content, { type: 'text', text: SUMMARY_INSTRUCTION }];\n } else {\n reqMessages.push({ role: 'user', content: [{ type: 'text', text: SUMMARY_INSTRUCTION }] });\n }\n\n let summaryText = '(summary unavailable)';\n try {\n const res = await client.createMessage({ system: SUMMARY_SYSTEM, messages: reqMessages, tools: [] });\n summaryText = textOf(res.content).trim() || summaryText;\n } catch {\n // Summarization itself failed — keep the original transcript (caller will retry\n // or hit the safety-stop). Better a longer context than a lost transcript.\n return { messages, summarizedCount: 0 };\n }\n\n const summaryMsg: AgentMessage = {\n role: 'user',\n content: [{ type: 'text', text: COMPACTION_MARKER + summaryText }],\n };\n return { messages: [summaryMsg, ...tail], summarizedCount: head.length };\n}\n\n/** Strip blocks of one kind from a message, keeping everything else in order. A message\n * left with no content at all keeps a single empty text block so the role sequence\n * stays well-formed (a content-less message is rejected by most providers). */\nfunction dropBlocks(m: AgentMessage, kind: 'image' | 'reasoning'): AgentMessage {\n if (!m.content.some((b) => b.type === kind)) return m;\n const kept = m.content.filter((b) => b.type !== kind);\n return { role: m.role, content: kept.length ? kept : [{ type: 'text', text: '' }] };\n}\n\n/** Compaction's image-drop rule (R3-339). */\nconst dropImages = (m: AgentMessage): AgentMessage => dropBlocks(m, 'image');\n/** Compaction's reasoning-drop rule (R3-335). */\nconst dropReasoning = (m: AgentMessage): AgentMessage => dropBlocks(m, 'reasoning');\n\n/** Does this thrown error look like a hard context-window overflow? Used to trigger\n * recover-then-retry compaction (F3/exit-c) rather than a dead loop. */\nexport function isContextOverflow(e: unknown): boolean {\n const msg = ((e as Error)?.message ?? String(e)).toLowerCase();\n const code = String((e as { code?: unknown })?.code ?? '').toLowerCase();\n return (\n code.includes('context_length') ||\n code.includes('context-length') ||\n /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg)\n );\n}\n\n// The user turn injected when a truncated (`max_tokens`) turn emitted tool calls: we\n// fail the partial calls rather than execute them (F3), and tell the model to retry.\nconst TRUNCATED_RETRY_TEXT =\n 'That turn was cut off at the token limit mid tool-call, so the call was NOT executed. ' +\n 'Emit a smaller step: fewer/shorter tool calls, or a smaller file write.';\n\n/** The cumulative cache counters, as the event fields that carry them. A counter no\n * provider has reported yet is OMITTED, never zeroed: \"the provider reports nothing\"\n * and \"the provider cached nothing\" are different facts and the consumer must be able\n * to tell them apart. Three event payloads state this rule; this is the one place it\n * is spelled out. */\nconst cacheCounterFields = (\n cacheReadTokens: number | undefined,\n cacheWriteTokens: number | undefined,\n): { cacheReadTokens?: number; cacheWriteTokens?: number } => ({\n ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),\n ...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),\n});\n\n/**\n * Drive the agent loop to completion. Returns the full message transcript\n * (including the kickoff user turn). Stops when the model returns without tool\n * calls (or a terminal stop reason), when the token budget is exhausted, or when\n * `maxTurns` (a large safety-stop) is reached. With a `contextWindow` set, the loop\n * accounts tokens and compacts automatically so it can run long.\n */\nexport async function runAgent(opts: RunAgentOptions): Promise<AgentMessage[]> {\n const { client, tools, execute, system, prompt, events, signal, steering, pause } = opts;\n const maxTurns = opts.maxTurns ?? 100;\n const maxNudges = opts.maxNudges ?? 1;\n const maxTruncationRetries = opts.maxTruncationRetries ?? 2;\n const window = opts.contextWindow;\n const reserveTokens = opts.reserveTokens ?? (window ? Math.floor(window * 0.25) : 0);\n const keepRecentTurns = opts.keepRecentTurns ?? 8;\n\n let messages: AgentMessage[] = [...(opts.history ?? []), { role: 'user', content: [{ type: 'text', text: prompt }] }];\n\n // Consecutive-stall counter: how many times in a row we've nudged a no-tool-call\n // turn. Reset to 0 by any turn that DOES call a tool, so the budget is per stall\n // *episode*, not per run.\n let nudges = 0;\n let truncationRetries = 0;\n // True when the previous iteration's model turn was cut short by an `interrupt`\n // steer, so the injected correction can be reported as an interruption.\n let interruptedLastTurn = false;\n // Running context size (provider-reported when available) + cumulative spend.\n let contextTokens = 0;\n let spentTokens = 0;\n // R3-336 — cumulative cache accounting. `undefined` until a provider reports\n // something, so \"reports nothing\" stays distinguishable from \"cached nothing\".\n let cacheReadTokens: number | undefined;\n let cacheWriteTokens: number | undefined;\n\n // Compact the transcript and, when anything was actually folded in, adopt the\n // compacted messages, re-estimate the running context, and report it. Returns how\n // many messages were summarized — 0 means \"there was nothing to compact\", which is\n // what the overflow-recovery path below treats as unrecoverable. Both compaction\n // sites (near-window before a request, and hard-overflow recovery) go through here,\n // so they cannot drift on what a compaction updates or reports.\n const compactAndReport = async (): Promise<number> => {\n const { messages: compacted, summarizedCount } = await compactTranscript(messages, client, keepRecentTurns);\n if (summarizedCount > 0) {\n messages = compacted;\n contextTokens = estimateTokens(messages);\n events?.onCompact?.({ summarizedCount, ...cacheCounterFields(cacheReadTokens, cacheWriteTokens) });\n }\n return summarizedCount;\n };\n\n for (let turn = 0; turn < maxTurns; turn++) {\n // R3-224 (§3.3): the stop button, checked between turns. Combined with the\n // per-request `signal` below (which aborts the in-flight upstream turn), this\n // halts \"the loop between tool calls AND aborts the in-flight LLM request\".\n if (signal?.aborted) break;\n\n // R3-562 (§7 R-ARD-20a): if the region is hidden, stop HERE — at the boundary, with\n // the previous turn's tool batch fully paired — and wait for the reveal. Placed after\n // the stop check and before the steer drain so a correction queued while hidden is\n // applied on the way back in, as the very next turn, rather than a turn late.\n if (pause?.isPaused()) {\n events?.onPause?.({ turn });\n await pause.whenResumed(signal);\n events?.onResume?.({ turn });\n // `whenResumed` also resolves on abort, so a run stopped while hidden lands here\n // rather than awaiting a reveal that never comes.\n if (signal?.aborted) break;\n }\n\n // R3-333: apply any queued corrections at the TURN BOUNDARY, before the next\n // request, so the model's very next turn reflects them. Draining here (rather\n // than at the point of arrival) is what makes a steer safe: whatever the loop\n // was doing — streaming a turn, running a tool batch — has finished.\n if (steering) {\n const steers = steering.drain();\n if (steers.length) {\n messages.push({\n role: 'user',\n content: steers.map((m) => ({ type: 'text' as const, text: steerWireText(m) })),\n });\n events?.onSteer?.({ messages: steers, interrupted: interruptedLastTurn });\n }\n interruptedLastTurn = false;\n steering.rearm();\n }\n\n // Compact BEFORE the next request when the running context is near the window.\n if (shouldCompact(contextTokens, window, reserveTokens)) await compactAndReport();\n\n // The in-flight turn is abortable by EITHER verb: STOP (ends the run) or an\n // `interrupt`-mode STEER (ends the turn, keeps the run). They are composed into\n // one per-turn signal, and told apart in the catch by asking which fired.\n const turnAbort = anySignal([signal, steering?.interrupt]);\n // Capture what the model had streamed when a steer cut in, so the interrupted\n // turn is recorded as what actually happened rather than dropped.\n let partialText = '';\n const onTextDelta = (text: string): void => {\n partialText += text;\n events?.onAssistantDelta?.(text);\n };\n const sendTurn = () =>\n client.createMessage({\n system,\n messages,\n tools,\n // R3-333's local `onTextDelta` (it captures the partial text a steer may cut\n // short) — NOT `events.onAssistantDelta` directly.\n onTextDelta,\n // R3-335's reasoning stream rides alongside it.\n onReasoningDelta: events?.onReasoningDelta,\n // R3-333: STOP composed with the steer INTERRUPT, so either verb ends the turn.\n signal: turnAbort.signal,\n });\n let res: ModelResponse;\n try {\n try {\n res = await sendTurn();\n } catch (e) {\n // Recover-then-retry on a hard context-overflow (exit-c): compact once and\n // re-send. If there is nothing to compact, or the retry also overflows, the\n // error propagates — a bounded recovery, never a dead loop.\n if (turnAbort.signal.aborted || !isContextOverflow(e)) throw e;\n if ((await compactAndReport()) === 0) throw e;\n res = await sendTurn();\n }\n } catch (e) {\n // R3-224: a mid-turn abort surfaces as a thrown (Abort/Stream)Error. Treat it\n // as a CLEAN stop — return the transcript so far — not a failure to bubble up.\n if (signal?.aborted) {\n turnAbort.dispose();\n break;\n }\n // R3-333: the SAME thrown abort, but from a steer — the run continues. Record\n // the turn the user cut short (an assistant message, so the transcript keeps\n // strict role alternation and replay shows the interruption where it happened),\n // then loop: the drain at the top of the next iteration injects the correction.\n if (steering?.interrupt.aborted) {\n turnAbort.dispose();\n messages.push({\n role: 'assistant',\n content: [{ type: 'text', text: partialText.trim() || INTERRUPTED_TURN_TEXT }],\n });\n events?.onAssistantText?.(partialText.trim() || INTERRUPTED_TURN_TEXT);\n interruptedLastTurn = true;\n continue;\n }\n turnAbort.dispose();\n throw e;\n }\n turnAbort.dispose();\n\n // Token accounting (R3-220): prefer the provider `usage`, else estimate. `turnCost`\n // is what this turn billed (input + output); `contextTokens` is the current window\n // occupancy (drives compaction); `spentTokens` is cumulative run spend (input is\n // re-billed every turn, so summing turnCost is the true cost signal).\n const turnCost = res.usage\n ? res.usage.inputTokens + res.usage.outputTokens\n : estimateTokens(messages) + Math.ceil(textOf(res.content).length / 4);\n contextTokens = turnCost;\n spentTokens += turnCost;\n if (res.usage?.cacheReadTokens !== undefined) {\n cacheReadTokens = (cacheReadTokens ?? 0) + res.usage.cacheReadTokens;\n }\n if (res.usage?.cacheWriteTokens !== undefined) {\n cacheWriteTokens = (cacheWriteTokens ?? 0) + res.usage.cacheWriteTokens;\n }\n events?.onUsage?.({\n contextTokens,\n window,\n spentTokens,\n ...cacheCounterFields(cacheReadTokens, cacheWriteTokens),\n });\n\n const assistantText = textOf(res.content);\n if (assistantText) events?.onAssistantText?.(assistantText);\n // R3-335: reasoning stays IN the message sequence — a provider that requires the\n // block echoed back gets it from `messages`, not from a side channel.\n for (const b of res.content) if (b.type === 'reasoning') events?.onReasoning?.(b);\n messages.push({ role: 'assistant', content: res.content });\n\n const toolUses = res.content.filter((b): b is ToolUseBlock => b.type === 'tool_use');\n\n // Truncated-tool-call guard (F3): a `max_tokens` turn that emitted tool calls\n // was cut off mid-call, so its args may be partial. Do NOT execute them — fail\n // each with an error tool_result (keeps the conversation well-formed) and\n // re-prompt for a smaller step, bounded by maxTruncationRetries.\n if (res.stopReason === 'max_tokens' && toolUses.length > 0) {\n events?.onTruncatedToolCall?.();\n const failed: ContentBlock[] = toolUses.map((c) => ({\n type: 'tool_result',\n tool_use_id: c.id,\n content: 'tool call truncated by the token limit — not executed',\n is_error: true,\n }));\n failed.push({ type: 'text', text: TRUNCATED_RETRY_TEXT });\n messages.push({ role: 'user', content: failed });\n if (++truncationRetries > maxTruncationRetries) break;\n continue;\n }\n truncationRetries = 0;\n\n if (toolUses.length === 0) {\n // No tool calls. Usually the model is genuinely done — but GLM/OpenRouter\n // intermittently ends with \"I'll read the files…\" or an empty turn after a\n // tool error and no call (findings §2). Nudge such a STALL back into action\n // once (per episode), respecting terminal stops and a real wrap-up.\n const stall = TERMINAL_STOPS.has(res.stopReason) ? null : detectStall(assistantText);\n if (stall && nudges < maxNudges) {\n nudges++;\n events?.onNudge?.(stall);\n messages.push({ role: 'user', content: [{ type: 'text', text: NUDGE_TEXT }] });\n continue;\n }\n // R3-333 follow-up: the model is done, but the user queued something while it\n // was working. Continue rather than end — the drain at the top of the next\n // iteration turns the queued message into the next turn's prompt. This is the\n // difference between a follow-up and a restart.\n if (steering?.hasPending()) continue;\n break;\n }\n\n nudges = 0; // a productive turn clears the stall budget\n\n const results: ToolResultBlock[] = [];\n // R3-339 — image parts produced by tools this turn. They ride in the SAME user\n // message as the results (after them), because a `tool_result`'s content is a string\n // on the wire; this is the shape both host adapters map to their provider.\n const images: ImageBlock[] = [];\n for (const call of toolUses) {\n events?.onToolUse?.(call.name, call.input);\n let outcome: ToolOutcome;\n try {\n outcome = await execute(call.name, call.input);\n } catch (e) {\n // A thrown executor error (e.g. host `forbidden`) becomes an error\n // tool_result so the model sees the gate's verdict and can adapt.\n const code = (e as { code?: string })?.code;\n const msg = (e as Error)?.message ?? String(e);\n outcome = { content: code ? `${code}: ${msg}` : msg, isError: true };\n }\n events?.onToolResult?.(call.name, outcome);\n results.push({\n type: 'tool_result',\n tool_use_id: call.id,\n content: outcome.content,\n is_error: outcome.isError,\n });\n if (outcome.images?.length) images.push(...outcome.images);\n }\n messages.push({ role: 'user', content: [...results, ...images] });\n\n // Runaway-cost guard: stop once cumulative spend passes the budget (the token/\n // spend bound that replaces the old raw turn cap). Compaction keeps a single\n // request small; this bounds the whole run.\n if (opts.tokenBudget && spentTokens >= opts.tokenBudget) {\n events?.onBudgetStop?.({ spentTokens, tokenBudget: opts.tokenBudget });\n break;\n }\n }\n\n return messages;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/agentLoop.ts"],"sourcesContent":["// Provider-agnostic agentic tool-use loop — PORTED from agent-demo `src/lib/agentLoop.ts`\n// (GROVE_AGENT_SPEC §7: embedded agents REUSE this seam rather than reimplementing it;\n// the SDK is its shared home so every app's agent loop is the same exercised machinery).\n// Renames for the SDK's flat export surface: `ChatMessage`→`AgentMessage`, `Role`→`AgentRole`.\n// Provider-agnostic agentic tool-use loop (LLM_AND_AGENTS_SPEC §3.3). The loop is\n// the heart of the in-browser coding agent: send the conversation + tool list to a\n// ModelClient, execute any tool calls the model emits, append the results, and\n// repeat until the model stops, a spend budget is hit, or a large safety-stop is\n// reached. The ModelClient seam keeps the loop independent of any one provider\n// (host `chat()` impl: chatModelClient.ts).\n//\n// Confinement (G12/T24) is NOT enforced here — it falls out of the capability\n// model: the `tools` handed to the model ARE the app's grant-filtered §5.5\n// catalog (agentTools.ts), and `execute` routes through the host's gated\n// `invoke()`, so an off-catalog/hallucinated tool returns `forbidden` at the host.\n//\n// R3-220 (AHG-1) adds the machinery that lets the loop run LONG enough to build a\n// real app: token accounting (from the provider `usage` delta), automatic context\n// COMPACTION when the window fills, a truncated-tool-call guard, and a spend budget\n// replacing the old fixed 12-turn cap. All of it is inert unless a `contextWindow`\n// is supplied, so a caller that passes none behaves exactly as before.\n//\n// PREFIX STABILITY IS LOAD-BEARING (R3-336). The loop's contribution to prompt caching\n// is structural, not a parameter: `system` and `tools` are fixed for a run and are sent\n// BYTE-IDENTICALLY on every turn, while everything that changes is appended to\n// `messages`. That is what the host's cache breakpoints key on. Rebuilding the system\n// prompt per turn — re-stamping a date, re-ordering the tool list — would cost nothing\n// visible and silently turn every cache read into a cache write, so it is asserted in\n// the tests rather than left as a convention.\n\nimport { anySignal, steerWireText, INTERRUPTED_TURN_TEXT, type SteerMessage, type SteerSource } from './agentSteering';\nimport type { PauseSource } from './agentPause';\n\nexport type TextBlock = { type: 'text'; text: string };\n/**\n * An image the model can look at (R3-339). `data` is base64 with no `data:` prefix,\n * matching the SDK `ContentPart` the transport already accepts.\n *\n * Carried as its OWN block rather than stuffed inside a `tool_result`, because a tool\n * result's content is a string on the wire — the loop appends the image to the same\n * user message that carries the results, which is the shape both host adapters map.\n */\nexport type ImageBlock = { type: 'image'; mimeType: string; data: string };\n/**\n * A block of the model's own reasoning (R3-335).\n *\n * Kept in the message sequence rather than rendered and thrown away, for two reasons:\n * the user needs to see what the model is doing during the long stretches compaction\n * now makes possible, and some providers REQUIRE the block echoed back — with its\n * `signature` — for the following turn of a tool-use chain to stay valid. A loop that\n * drops them is quietly lossy in a way that shows up as degraded output, not an error.\n *\n * `redactedData` carries provider-redacted reasoning: opaque bytes with no readable\n * text, which still have to be replayed in place. Never render it.\n */\nexport type ReasoningBlock = {\n type: 'reasoning';\n text: string;\n signature?: string;\n redactedData?: string;\n};\nexport type ToolUseBlock = { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> };\nexport type ToolResultBlock = { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean };\nexport type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock | ReasoningBlock;\n\n/** A tool the model may call: name, description, and a JSON Schema for its input\n * (`input_schema`, the Anthropic wire name — {@link createChatModelClient} maps it to the\n * chat slot's `ToolDef`). */\nexport interface AgentTool {\n name: string;\n description: string;\n input_schema: Record<string, unknown>;\n}\n\nexport type AgentRole = 'user' | 'assistant';\nexport interface AgentMessage {\n role: AgentRole;\n content: ContentBlock[];\n}\n\n/** Provider-reported token counts for one turn (R3-220). `inputTokens` is the size\n * of everything the provider processed this turn; `outputTokens` is what it\n * generated. Absent when the provider emits no `usage` delta. */\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n /** R3-336 — prompt-cache counters, present only where the provider reports them.\n * ABSENT is not zero: it means this provider says nothing about caching, which is a\n * different fact from \"nothing was cached\", and conflating them would turn a\n * measurement into a guess. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n}\n\n/** One model turn: the assistant's emitted blocks + why it stopped (+ usage). */\nexport interface ModelResponse {\n content: (TextBlock | ToolUseBlock | ReasoningBlock)[];\n /** Anthropic stop_reason: 'end_turn' | 'tool_use' | 'max_tokens' | 'refusal' | … */\n stopReason: string;\n /** Provider token counts for this turn, when reported (R3-220 accounting). */\n usage?: TokenUsage;\n}\n\n/** The provider seam — one model turn. Implemented by `chatModelClient.ts` over\n * the host `chat()` slot; faked in tests. When the client streams, it calls\n * `onTextDelta` with each token slice as it arrives (the assembled turn is still\n * returned whole); a non-streaming client simply never calls it. */\nexport interface ModelClient {\n createMessage(req: {\n system?: string;\n messages: AgentMessage[];\n tools: AgentTool[];\n /** Called with incremental assistant-text slices during a streamed turn. */\n onTextDelta?: (text: string) => void;\n /** R3-335: incremental REASONING slices, for a live thinking surface. Never called\n * by a provider that does not emit reasoning. */\n onReasoningDelta?: (text: string) => void;\n /** R3-224: aborts the in-flight turn — the host stops the upstream provider\n * request and stops billing, not just the app-side stream (§3.3). */\n signal?: AbortSignal;\n }): Promise<ModelResponse>;\n}\n\n/** Executes one tool call, returning a string result (and whether it errored —\n * a `forbidden`/failed call comes back as `is_error` so the model can adapt). */\nexport type ToolExecutor = (name: string, input: Record<string, unknown>) => Promise<ToolOutcome>;\n\n/** What one tool call produced. `images` (R3-339) is how a tool hands the model\n * something to LOOK at; `content` still carries the text the model reads. */\nexport interface ToolOutcome {\n content: string;\n isError?: boolean;\n images?: ImageBlock[];\n}\n\n/** Why a no-tool-call turn looked like a stall rather than a genuine finish. */\nexport type StallReason = 'empty' | 'announced-no-call';\n\n/** Optional UI hooks so a panel can render the loop as it runs. */\nexport interface AgentEvents {\n /** A streamed token slice of the in-flight assistant turn (live preview). */\n onAssistantDelta?(text: string): void;\n /** The complete assistant text for a turn, once the turn is in. */\n onAssistantText?(text: string): void;\n onToolUse?(name: string, input: Record<string, unknown>): void;\n onToolResult?(name: string, result: ToolOutcome): void;\n /** Fired when the loop nudges a STALLED turn (the model ended without a tool\n * call despite empty or \"I'll do X\" intent text) back into action, so a panel\n * can show \"nudging the model to continue\" rather than a silent stall. */\n onNudge?(reason: StallReason): void;\n /** Fired after every turn with the running context size + window (R3-220\n * loop-observability). `contextTokens` is provider-reported when available, else\n * a char/4 estimate. */\n onUsage?(usage: {\n contextTokens: number;\n window?: number;\n spentTokens: number;\n /** R3-336 — cumulative cache reads/writes across the run, on providers that report\n * them. Surfacing this is what makes the caching claim verifiable rather than\n * believed; `undefined` means the provider reported nothing. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n }): void;\n /** Fired when the loop compacts the transcript to stay under the context window;\n * `summarizedCount` is how many older messages were folded into the summary.\n *\n * R3-336: a compaction invalidates the conversation-prefix cache it rewrote — the\n * durable system+tools prefix survives it — so the next turn pays one prefix\n * re-write. `cacheReadTokens`/`cacheWriteTokens` are the run totals AT the\n * compaction, which is what lets the cost curve across it be read off rather than\n * assumed (exit 2). */\n onCompact?(info: { summarizedCount: number; cacheReadTokens?: number; cacheWriteTokens?: number }): void;\n /** Fired when the loop stops because the token/spend budget was exhausted. */\n onBudgetStop?(info: { spentTokens: number; tokenBudget: number }): void;\n /** Fired when a turn was truncated (`max_tokens`) while emitting tool calls, so\n * the partial calls were failed-and-re-prompted rather than executed (R3-220 F3). */\n onTruncatedToolCall?(): void;\n /** R3-335: a streamed slice of the model's reasoning, for a live thinking surface. */\n onReasoningDelta?(text: string): void;\n /** R3-335: the complete reasoning block for a turn, once the turn is in. */\n onReasoning?(block: ReasoningBlock): void;\n /** R3-333: the loop applied the user's mid-run correction(s). `interrupted` is\n * true when an `interrupt`-mode steer cut an in-flight model turn short (as\n * opposed to being applied at an ordinary turn boundary). */\n onSteer?(info: { messages: SteerMessage[]; interrupted: boolean }): void;\n /** R3-562: the loop reached a turn boundary while its region was hidden and stopped\n * advancing. A surface can say \"paused — this view is hidden\" instead of looking hung. */\n onPause?(info: { turn: number }): void;\n /** R3-562: the region was revealed (or the run was stopped) and the loop resumed. */\n onResume?(info: { turn: number }): void;\n}\n\nexport interface RunAgentOptions {\n client: ModelClient;\n tools: AgentTool[];\n execute: ToolExecutor;\n system?: string;\n /** Prior turns of this conversation, replayed before the new prompt so a\n * follow-up has context (the conversation stage seeds this from the store). */\n history?: AgentMessage[];\n /** The user's instruction that kicks off the loop. */\n prompt: string;\n /** Large safety-stop on model turns (default 100). No longer the primary bound —\n * a long task is bounded by `tokenBudget` + compaction; this just backstops a\n * pathological loop the budget/compaction somehow miss. */\n maxTurns?: number;\n /** Max consecutive \"you announced work but emitted no tool call\" nudges before\n * the loop gives up (default 1). GLM-over-OpenRouter intermittently ends a turn\n * with future-tense intent (\"I'll read the files…\") or an EMPTY turn right after\n * a tool error — no tool call, a silent stall (tutorial findings §2). One nudge\n * recovers most of these; the cap keeps a genuinely-finished model (which answers\n * the nudge with another call-free turn) from looping, and the budget resets on\n * any turn that DID call a tool, so a long task's later stall is still covered.\n * Set 0 to disable the backstop. */\n maxNudges?: number;\n // ---- R3-220 accounting / compaction (all inert unless `contextWindow` is set) ----\n /** The resolved provider's context window (`describeChat().features.maxContextTokens`).\n * Compaction is disabled when this is absent/0 — the loop then behaves as before. */\n contextWindow?: number;\n /** Headroom left below the window before compacting (default: 25% of the window). */\n reserveTokens?: number;\n /** Recent messages kept verbatim across a compaction (default 8). */\n keepRecentTurns?: number;\n /** Cumulative token budget (input+output across turns). When exceeded the loop\n * stops — the runaway-cost guard that replaces the raw 12-turn cap. Off when unset. */\n tokenBudget?: number;\n /** Max consecutive truncated-tool-call re-prompts before giving up (default 2). */\n maxTruncationRetries?: number;\n /** R3-224 (§3.3): the stop button. When it fires the loop stops between turns AND\n * aborts the in-flight model turn (the host tears down the upstream provider\n * request and stops billing) — not merely the between-turn loop. The transcript so\n * far is returned; an abort is a clean stop, never a thrown error. */\n signal?: AbortSignal;\n /** R3-333: the mid-run steering queue. The loop drains it at every turn boundary\n * and folds each correction in as a `user` message, so the human can redirect a\n * run without restarting it and paying for the transcript again. Its `interrupt`\n * signal aborts the in-flight MODEL turn only — never a tool batch, which must\n * keep every `tool_use` paired with a `tool_result`. Absent ⇒ the loop behaves\n * exactly as before. */\n steering?: SteerSource;\n /**\n * R3-562 (AGENT_RUN_DURABILITY_SPEC §7 R-ARD-20a): pause the run while nobody can see\n * or stop it — the host has hidden this app's region but kept the frame mounted.\n *\n * Read at the TURN BOUNDARY only, so every `tool_use` still has its `tool_result` when\n * the loop stops advancing. Nothing is torn down and nothing is injected: the run\n * simply does not start its next turn until the region is revealed, then continues\n * with no repair pass and no resume gate. Omitted ⇒ the loop never pauses, exactly as\n * before.\n */\n pause?: PauseSource;\n events?: AgentEvents;\n}\n\nconst textOf = (blocks: { type: string; text?: string }[]): string =>\n blocks\n .filter((b): b is TextBlock => b.type === 'text')\n .map((b) => b.text)\n .join('');\n\n// Terminal stops we must NOT nudge past. Only `max_tokens` survives the SDK→loop\n// mapping distinctly (chatModelClient `mapStop`: 'length'→'max_tokens', while\n// 'end'/'filtered'→'end_turn' and 'tool'→'tool_use'); a truncated turn is a\n// token-budget problem a nudge can't fix. An empty give-up after a tool error\n// arrives as 'end_turn', so it stays nudgeable.\nconst TERMINAL_STOPS = new Set(['max_tokens', 'refusal']);\n\n// Future-tense intent to ACT (\"I'll read…\", \"let me create…\", \"next I'll edit…\").\nconst INTENT_RE =\n /\\b(i'?ll|i will|i'?m going to|going to|let me|let's|now,? i(?:'?ll| will)?|next,? i(?:'?ll| will)?)\\b[\\s\\S]{0,80}?\\b(read|write|edit|creat|add|updat|modif|regist|check|look|call|run|search|grep|list|open|fetch|inspect|review|explor|implement|fix|appl)/i;\n// A wrap-up marker → treat the turn as a genuine finish, never nudge.\nconst DONE_RE =\n /\\b(done|complete|finished|all set|no (?:further|more) (?:changes|steps)|i(?:'| ha)ve (?:creat|add|updat|made|written|regist|edit|implement|fix|appli)|here'?s (?:a |the )?summ|to summ|in summ)/i;\n\n/**\n * Classify a NO-tool-call turn as a stall (nudge-worthy) vs a genuine finish.\n * GLM-over-OpenRouter intermittently (a) writes \"I'll read the files…\" then ends\n * with no call, or (b) returns an EMPTY turn after a tool error — both silent\n * give-ups (tutorial findings §2). Conservative on purpose: a real wrap-up (a\n * summary, \"Done\", \"I've created…\") returns null so the loop never nudges a\n * finished agent. Empty text is always a stall (there is nothing a finished agent\n * would say with zero words).\n */\nexport function detectStall(text: string): StallReason | null {\n const t = text.trim();\n if (!t) return 'empty';\n if (DONE_RE.test(t)) return null;\n if (INTENT_RE.test(t)) return 'announced-no-call';\n return null;\n}\n\n// The single follow-up we inject to break a stall. Directive, short, and honest\n// about the two outcomes so a genuinely-finished model just confirms and stops\n// (→ another call-free turn, which the nudge cap then lets terminate). Exported so\n// the transcript renderer can recognise the injected turn and show it as a \"nudge\"\n// row (not a user message) when a persisted conversation is replayed.\nexport const NUDGE_TEXT =\n \"You ended your turn without calling a tool. If the task is already complete, say so plainly in one line and stop. Otherwise don't just describe the next step — emit the tool call now.\";\n\n// ---- R3-220 token accounting + compaction ----------------------------------------\n\n/** Rough token estimate (~4 chars/token) over a message array, used only when the\n * provider reports no `usage` delta. Conservative by design (over- not under-counts\n * by treating structured blocks as their JSON length). */\nexport function estimateTokens(messages: AgentMessage[]): number {\n let chars = 0;\n for (const m of messages) {\n for (const b of m.content) {\n if (b.type === 'text') chars += b.text.length;\n else if (b.type === 'tool_use') chars += JSON.stringify(b.input).length + b.name.length;\n else if (b.type === 'tool_result') chars += b.content.length;\n // R3-339: an image is large and MUST be accounted for, or it escapes exactly the\n // budget the accounting exists to enforce. base64 is ~4/3 of the bytes, and the\n // provider bills tokens per pixel area — the base64 length is the honest local\n // proxy for \"this is big\", and over-counting is the safe direction.\n else if (b.type === 'image') chars += b.data.length;\n // R3-335: reasoning occupies the window like anything else. Not counting it would\n // let a thinking model overrun the context the accounting exists to protect.\n else if (b.type === 'reasoning') chars += b.text.length + (b.redactedData?.length ?? 0);\n }\n }\n return Math.ceil(chars / 4);\n}\n\n/** Should the loop compact now? True once the running context passes\n * `window − reserveTokens`. Disabled (false) when there is no window. */\nexport function shouldCompact(contextTokens: number, window: number | undefined, reserveTokens: number): boolean {\n if (!window || window <= 0) return false;\n return contextTokens > window - reserveTokens;\n}\n\n/** Prefix marking a `user` message as a compaction summary (not a real user turn),\n * so the transcript renderer shows a \"compacted N turns\" affordance on replay. */\nexport const COMPACTION_MARKER = '␟[compacted-context]\\n';\n\nconst SUMMARY_SYSTEM =\n 'You are compacting a coding-agent transcript to fit the context window. Produce a ' +\n 'DENSE structured summary under these exact headings: Goal / Constraints / Progress / ' +\n 'Decisions / Next Steps / Critical Context. PRESERVE VERBATIM every file path, symbol/' +\n 'identifier, and error string that later steps will need — do not paraphrase them. Be ' +\n 'terse everywhere else. Output only the summary.';\nconst SUMMARY_INSTRUCTION =\n 'Summarize everything above into the structured block. Keep exact paths, symbols, and ' +\n 'error strings verbatim so work can continue from the summary alone.';\n\n/** Compact `messages` by folding the older head into a structured summary and keeping\n * a verbatim recent tail. The tail is snapped to start at an `assistant` message so a\n * `tool_use`/`tool_result` pair is never split (which would malform the next request).\n * The taint tier is NOT modelled on messages (it is run-scoped host state, R-ASG-2):\n * this is a pure content transform over the SAME session — it starts no new external\n * read — so it cannot launder taint (F6). Returns the original array unchanged when\n * there is nothing safe to summarize. */\nexport async function compactTranscript(\n messages: AgentMessage[],\n client: ModelClient,\n keepRecentTurns: number,\n): Promise<{ messages: AgentMessage[]; summarizedCount: number }> {\n if (messages.length <= keepRecentTurns + 1) return { messages, summarizedCount: 0 };\n\n // Snap the tail boundary to an assistant message so tool_use/tool_result pairs stay\n // together and the summary (a `user` turn) is followed by an `assistant` turn.\n // Prefer the first assistant at/after the keep-recent boundary; fall back to the\n // last assistant in the transcript so the tail is always well-formed.\n const boundary = Math.max(1, messages.length - keepRecentTurns);\n let tailStart = -1;\n for (let i = boundary; i < messages.length; i++) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n if (tailStart === -1) {\n for (let i = messages.length - 1; i >= 1; i--) {\n if (messages[i].role === 'assistant') {\n tailStart = i;\n break;\n }\n }\n }\n if (tailStart <= 0) return { messages, summarizedCount: 0 };\n\n const head = messages.slice(0, tailStart);\n // Compaction DROPS both image parts (R3-339) and reasoning (R3-335) from the kept\n // tail, each by an explicit rule — an implicit answer here is what corrupts a\n // transcript quietly.\n //\n // IMAGES: the largest and least summarisable thing in a transcript, and the summary\n // the head folds into is TEXT. The `tool_result` that named the image stays, so the\n // model still knows it looked at `assets/mock.png` and what it concluded; it simply\n // cannot look again without re-reading the file, which it can do.\n //\n // REASONING: only ever required by the turn that FOLLOWS it, and compaction rewrites\n // at a turn boundary — so nothing after it is mid-chain and nothing needs the block\n // replayed. Keeping them would spend the window on its most disposable content.\n const tail = messages.map(dropImages).map(dropReasoning).slice(tailStart);\n\n // Ask the model to summarize the head. Append the instruction to the final head\n // message when it is a `user` turn (avoids introducing consecutive user turns).\n const reqMessages: AgentMessage[] = head.map((m) => ({ role: m.role, content: [...m.content] }));\n const lastMsg = reqMessages[reqMessages.length - 1];\n if (lastMsg && lastMsg.role === 'user') {\n lastMsg.content = [...lastMsg.content, { type: 'text', text: SUMMARY_INSTRUCTION }];\n } else {\n reqMessages.push({ role: 'user', content: [{ type: 'text', text: SUMMARY_INSTRUCTION }] });\n }\n\n let summaryText = '(summary unavailable)';\n try {\n const res = await client.createMessage({ system: SUMMARY_SYSTEM, messages: reqMessages, tools: [] });\n summaryText = textOf(res.content).trim() || summaryText;\n } catch {\n // Summarization itself failed — keep the original transcript (caller will retry\n // or hit the safety-stop). Better a longer context than a lost transcript.\n return { messages, summarizedCount: 0 };\n }\n\n const summaryMsg: AgentMessage = {\n role: 'user',\n content: [{ type: 'text', text: COMPACTION_MARKER + summaryText }],\n };\n return { messages: [summaryMsg, ...tail], summarizedCount: head.length };\n}\n\n/** Strip blocks of one kind from a message, keeping everything else in order. A message\n * left with no content at all keeps a single empty text block so the role sequence\n * stays well-formed (a content-less message is rejected by most providers). */\nfunction dropBlocks(m: AgentMessage, kind: 'image' | 'reasoning'): AgentMessage {\n if (!m.content.some((b) => b.type === kind)) return m;\n const kept = m.content.filter((b) => b.type !== kind);\n return { role: m.role, content: kept.length ? kept : [{ type: 'text', text: '' }] };\n}\n\n/** Compaction's image-drop rule (R3-339). */\nconst dropImages = (m: AgentMessage): AgentMessage => dropBlocks(m, 'image');\n/** Compaction's reasoning-drop rule (R3-335). */\nconst dropReasoning = (m: AgentMessage): AgentMessage => dropBlocks(m, 'reasoning');\n\n/**\n * The host's typed code for \"the conversation no longer fits\" — produced by site-main's\n * `PROVIDER_ERROR_CODES` vocabulary (`src/editor/llm/providerErrors.ts`): the host maps\n * BOTH a provider's own context overflow AND the relay's bound refusal to this one code.\n * The SDK cannot import the host's vocabulary, so the literal has exactly one home HERE,\n * pointed at its producer.\n */\nexport const HOST_CONTEXT_OVERFLOW_CODE = 'context-too-large';\n\n/** Does this thrown error look like a hard context-window overflow? Used to trigger\n * recover-then-retry compaction (F3/exit-c) rather than a dead loop. */\nexport function isContextOverflow(e: unknown): boolean {\n const msg = ((e as Error)?.message ?? String(e)).toLowerCase();\n const code = String((e as { code?: unknown })?.code ?? '').toLowerCase();\n return (\n // The host's own typed code, matched EXACTLY: the host is the one place that\n // decides what counts as an overflow (R3-588) — a relay `too-large` it did NOT\n // translate must not sneak in as a substring of some message.\n code === HOST_CONTEXT_OVERFLOW_CODE ||\n code.includes('context_length') ||\n code.includes('context-length') ||\n /context (?:length|window)|maximum context|too many tokens|prompt is too long|reduce the length/.test(msg)\n );\n}\n\n// The user turn injected when a truncated (`max_tokens`) turn emitted tool calls: we\n// fail the partial calls rather than execute them (F3), and tell the model to retry.\nconst TRUNCATED_RETRY_TEXT =\n 'That turn was cut off at the token limit mid tool-call, so the call was NOT executed. ' +\n 'Emit a smaller step: fewer/shorter tool calls, or a smaller file write.';\n\n/** The cumulative cache counters, as the event fields that carry them. A counter no\n * provider has reported yet is OMITTED, never zeroed: \"the provider reports nothing\"\n * and \"the provider cached nothing\" are different facts and the consumer must be able\n * to tell them apart. Three event payloads state this rule; this is the one place it\n * is spelled out. */\nconst cacheCounterFields = (\n cacheReadTokens: number | undefined,\n cacheWriteTokens: number | undefined,\n): { cacheReadTokens?: number; cacheWriteTokens?: number } => ({\n ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),\n ...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),\n});\n\n/**\n * Drive the agent loop to completion. Returns the full message transcript\n * (including the kickoff user turn). Stops when the model returns without tool\n * calls (or a terminal stop reason), when the token budget is exhausted, or when\n * `maxTurns` (a large safety-stop) is reached. With a `contextWindow` set, the loop\n * accounts tokens and compacts automatically so it can run long.\n */\nexport async function runAgent(opts: RunAgentOptions): Promise<AgentMessage[]> {\n const { client, tools, execute, system, prompt, events, signal, steering, pause } = opts;\n const maxTurns = opts.maxTurns ?? 100;\n const maxNudges = opts.maxNudges ?? 1;\n const maxTruncationRetries = opts.maxTruncationRetries ?? 2;\n const window = opts.contextWindow;\n const reserveTokens = opts.reserveTokens ?? (window ? Math.floor(window * 0.25) : 0);\n const keepRecentTurns = opts.keepRecentTurns ?? 8;\n\n let messages: AgentMessage[] = [...(opts.history ?? []), { role: 'user', content: [{ type: 'text', text: prompt }] }];\n\n // Consecutive-stall counter: how many times in a row we've nudged a no-tool-call\n // turn. Reset to 0 by any turn that DOES call a tool, so the budget is per stall\n // *episode*, not per run.\n let nudges = 0;\n let truncationRetries = 0;\n // True when the previous iteration's model turn was cut short by an `interrupt`\n // steer, so the injected correction can be reported as an interruption.\n let interruptedLastTurn = false;\n // Running context size (provider-reported when available) + cumulative spend.\n let contextTokens = 0;\n let spentTokens = 0;\n // R3-336 — cumulative cache accounting. `undefined` until a provider reports\n // something, so \"reports nothing\" stays distinguishable from \"cached nothing\".\n let cacheReadTokens: number | undefined;\n let cacheWriteTokens: number | undefined;\n\n // Compact the transcript and, when anything was actually folded in, adopt the\n // compacted messages, re-estimate the running context, and report it. Returns how\n // many messages were summarized — 0 means \"there was nothing to compact\", which is\n // what the overflow-recovery path below treats as unrecoverable. Both compaction\n // sites (near-window before a request, and hard-overflow recovery) go through here,\n // so they cannot drift on what a compaction updates or reports.\n const compactAndReport = async (): Promise<number> => {\n const { messages: compacted, summarizedCount } = await compactTranscript(messages, client, keepRecentTurns);\n if (summarizedCount > 0) {\n messages = compacted;\n contextTokens = estimateTokens(messages);\n events?.onCompact?.({ summarizedCount, ...cacheCounterFields(cacheReadTokens, cacheWriteTokens) });\n }\n return summarizedCount;\n };\n\n for (let turn = 0; turn < maxTurns; turn++) {\n // R3-224 (§3.3): the stop button, checked between turns. Combined with the\n // per-request `signal` below (which aborts the in-flight upstream turn), this\n // halts \"the loop between tool calls AND aborts the in-flight LLM request\".\n if (signal?.aborted) break;\n\n // R3-562 (§7 R-ARD-20a): if the region is hidden, stop HERE — at the boundary, with\n // the previous turn's tool batch fully paired — and wait for the reveal. Placed after\n // the stop check and before the steer drain so a correction queued while hidden is\n // applied on the way back in, as the very next turn, rather than a turn late.\n if (pause?.isPaused()) {\n events?.onPause?.({ turn });\n await pause.whenResumed(signal);\n events?.onResume?.({ turn });\n // `whenResumed` also resolves on abort, so a run stopped while hidden lands here\n // rather than awaiting a reveal that never comes.\n if (signal?.aborted) break;\n }\n\n // R3-333: apply any queued corrections at the TURN BOUNDARY, before the next\n // request, so the model's very next turn reflects them. Draining here (rather\n // than at the point of arrival) is what makes a steer safe: whatever the loop\n // was doing — streaming a turn, running a tool batch — has finished.\n if (steering) {\n const steers = steering.drain();\n if (steers.length) {\n messages.push({\n role: 'user',\n content: steers.map((m) => ({ type: 'text' as const, text: steerWireText(m) })),\n });\n events?.onSteer?.({ messages: steers, interrupted: interruptedLastTurn });\n }\n interruptedLastTurn = false;\n steering.rearm();\n }\n\n // Compact BEFORE the next request when the running context is near the window.\n if (shouldCompact(contextTokens, window, reserveTokens)) await compactAndReport();\n\n // The in-flight turn is abortable by EITHER verb: STOP (ends the run) or an\n // `interrupt`-mode STEER (ends the turn, keeps the run). They are composed into\n // one per-turn signal, and told apart in the catch by asking which fired.\n const turnAbort = anySignal([signal, steering?.interrupt]);\n // Capture what the model had streamed when a steer cut in, so the interrupted\n // turn is recorded as what actually happened rather than dropped.\n let partialText = '';\n const onTextDelta = (text: string): void => {\n partialText += text;\n events?.onAssistantDelta?.(text);\n };\n const sendTurn = () =>\n client.createMessage({\n system,\n messages,\n tools,\n // R3-333's local `onTextDelta` (it captures the partial text a steer may cut\n // short) — NOT `events.onAssistantDelta` directly.\n onTextDelta,\n // R3-335's reasoning stream rides alongside it.\n onReasoningDelta: events?.onReasoningDelta,\n // R3-333: STOP composed with the steer INTERRUPT, so either verb ends the turn.\n signal: turnAbort.signal,\n });\n let res: ModelResponse;\n try {\n try {\n res = await sendTurn();\n } catch (e) {\n // Recover-then-retry on a hard context-overflow (exit-c): compact once and\n // re-send. If there is nothing to compact, or the retry also overflows, the\n // error propagates — a bounded recovery, never a dead loop.\n if (turnAbort.signal.aborted || !isContextOverflow(e)) throw e;\n if ((await compactAndReport()) === 0) throw e;\n res = await sendTurn();\n }\n } catch (e) {\n // R3-224: a mid-turn abort surfaces as a thrown (Abort/Stream)Error. Treat it\n // as a CLEAN stop — return the transcript so far — not a failure to bubble up.\n if (signal?.aborted) {\n turnAbort.dispose();\n break;\n }\n // R3-333: the SAME thrown abort, but from a steer — the run continues. Record\n // the turn the user cut short (an assistant message, so the transcript keeps\n // strict role alternation and replay shows the interruption where it happened),\n // then loop: the drain at the top of the next iteration injects the correction.\n if (steering?.interrupt.aborted) {\n turnAbort.dispose();\n messages.push({\n role: 'assistant',\n content: [{ type: 'text', text: partialText.trim() || INTERRUPTED_TURN_TEXT }],\n });\n events?.onAssistantText?.(partialText.trim() || INTERRUPTED_TURN_TEXT);\n interruptedLastTurn = true;\n continue;\n }\n turnAbort.dispose();\n throw e;\n }\n turnAbort.dispose();\n\n // Token accounting (R3-220): prefer the provider `usage`, else estimate. `turnCost`\n // is what this turn billed (input + output); `contextTokens` is the current window\n // occupancy (drives compaction); `spentTokens` is cumulative run spend (input is\n // re-billed every turn, so summing turnCost is the true cost signal).\n const turnCost = res.usage\n ? res.usage.inputTokens + res.usage.outputTokens\n : estimateTokens(messages) + Math.ceil(textOf(res.content).length / 4);\n contextTokens = turnCost;\n spentTokens += turnCost;\n if (res.usage?.cacheReadTokens !== undefined) {\n cacheReadTokens = (cacheReadTokens ?? 0) + res.usage.cacheReadTokens;\n }\n if (res.usage?.cacheWriteTokens !== undefined) {\n cacheWriteTokens = (cacheWriteTokens ?? 0) + res.usage.cacheWriteTokens;\n }\n events?.onUsage?.({\n contextTokens,\n window,\n spentTokens,\n ...cacheCounterFields(cacheReadTokens, cacheWriteTokens),\n });\n\n const assistantText = textOf(res.content);\n if (assistantText) events?.onAssistantText?.(assistantText);\n // R3-335: reasoning stays IN the message sequence — a provider that requires the\n // block echoed back gets it from `messages`, not from a side channel.\n for (const b of res.content) if (b.type === 'reasoning') events?.onReasoning?.(b);\n messages.push({ role: 'assistant', content: res.content });\n\n const toolUses = res.content.filter((b): b is ToolUseBlock => b.type === 'tool_use');\n\n // Truncated-tool-call guard (F3): a `max_tokens` turn that emitted tool calls\n // was cut off mid-call, so its args may be partial. Do NOT execute them — fail\n // each with an error tool_result (keeps the conversation well-formed) and\n // re-prompt for a smaller step, bounded by maxTruncationRetries.\n if (res.stopReason === 'max_tokens' && toolUses.length > 0) {\n events?.onTruncatedToolCall?.();\n const failed: ContentBlock[] = toolUses.map((c) => ({\n type: 'tool_result',\n tool_use_id: c.id,\n content: 'tool call truncated by the token limit — not executed',\n is_error: true,\n }));\n failed.push({ type: 'text', text: TRUNCATED_RETRY_TEXT });\n messages.push({ role: 'user', content: failed });\n if (++truncationRetries > maxTruncationRetries) break;\n continue;\n }\n truncationRetries = 0;\n\n if (toolUses.length === 0) {\n // No tool calls. Usually the model is genuinely done — but GLM/OpenRouter\n // intermittently ends with \"I'll read the files…\" or an empty turn after a\n // tool error and no call (findings §2). Nudge such a STALL back into action\n // once (per episode), respecting terminal stops and a real wrap-up.\n const stall = TERMINAL_STOPS.has(res.stopReason) ? null : detectStall(assistantText);\n if (stall && nudges < maxNudges) {\n nudges++;\n events?.onNudge?.(stall);\n messages.push({ role: 'user', content: [{ type: 'text', text: NUDGE_TEXT }] });\n continue;\n }\n // R3-333 follow-up: the model is done, but the user queued something while it\n // was working. Continue rather than end — the drain at the top of the next\n // iteration turns the queued message into the next turn's prompt. This is the\n // difference between a follow-up and a restart.\n if (steering?.hasPending()) continue;\n break;\n }\n\n nudges = 0; // a productive turn clears the stall budget\n\n const results: ToolResultBlock[] = [];\n // R3-339 — image parts produced by tools this turn. They ride in the SAME user\n // message as the results (after them), because a `tool_result`'s content is a string\n // on the wire; this is the shape both host adapters map to their provider.\n const images: ImageBlock[] = [];\n for (const call of toolUses) {\n events?.onToolUse?.(call.name, call.input);\n let outcome: ToolOutcome;\n try {\n outcome = await execute(call.name, call.input);\n } catch (e) {\n // A thrown executor error (e.g. host `forbidden`) becomes an error\n // tool_result so the model sees the gate's verdict and can adapt.\n const code = (e as { code?: string })?.code;\n const msg = (e as Error)?.message ?? String(e);\n outcome = { content: code ? `${code}: ${msg}` : msg, isError: true };\n }\n events?.onToolResult?.(call.name, outcome);\n results.push({\n type: 'tool_result',\n tool_use_id: call.id,\n content: outcome.content,\n is_error: outcome.isError,\n });\n if (outcome.images?.length) images.push(...outcome.images);\n }\n messages.push({ role: 'user', content: [...results, ...images] });\n\n // Runaway-cost guard: stop once cumulative spend passes the budget (the token/\n // spend bound that replaces the old raw turn cap). Compaction keeps a single\n // request small; this bounds the whole run.\n if (opts.tokenBudget && spentTokens >= opts.tokenBudget) {\n events?.onBudgetStop?.({ spentTokens, tokenBudget: opts.tokenBudget });\n break;\n }\n }\n\n return messages;\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -51,7 +51,7 @@ export { StreamError, StreamFrame, StreamTransport, consumeStream, protocolStrea
51
51
  export { ATTENDED_FIRST_FRAME_MS, ATTENDED_TIMEOUT_MS, Attendance, BoundedCallOptions, CallBounds, DeadlineBound, NETWORK_TIMEOUT_MS, PENDING_NOTICE_MS, PendingAttention, PendingState, ProtocolCancelledError, ProtocolTimeoutError, STREAM_IDLE_TIMEOUT_MS, SuspendableDeadline, UNATTENDED_TIMEOUT_MS, attendanceOf, attendanceReason, boundsFor, createSuspendableDeadline, firstFrameBoundsFor, firstFrameTimeoutFor, timeoutFor } from './protocolDeadline.cjs';
52
52
  export { EvaluationContext, FileQueryResult, FilesMetadata, Metadata, MetadataQueryEntry, MetadataQueryFunction, MetadataQueryRecord, MetadataQueryResult, ModuleExports } from './sandboxTypes.cjs';
53
53
  export * from '@immediately-run/safe-content';
54
- export { AgentEvents, AgentMessage, AgentRole, AgentTool, COMPACTION_MARKER, ContentBlock, ImageBlock, ModelClient, ModelResponse, NUDGE_TEXT, ReasoningBlock, RunAgentOptions, StallReason, TextBlock, TokenUsage, ToolExecutor, ToolOutcome, ToolResultBlock, ToolUseBlock, compactTranscript, detectStall, estimateTokens, isContextOverflow, runAgent, shouldCompact } from './agentLoop.cjs';
54
+ export { AgentEvents, AgentMessage, AgentRole, AgentTool, COMPACTION_MARKER, ContentBlock, HOST_CONTEXT_OVERFLOW_CODE, ImageBlock, ModelClient, ModelResponse, NUDGE_TEXT, ReasoningBlock, RunAgentOptions, StallReason, TextBlock, TokenUsage, ToolExecutor, ToolOutcome, ToolResultBlock, ToolUseBlock, compactTranscript, detectStall, estimateTokens, isContextOverflow, runAgent, shouldCompact } from './agentLoop.cjs';
55
55
  export { INTERRUPTED_TURN_TEXT, STEER_INTERRUPT_MARKER, STEER_MARKER, SteerController, SteerMessage, SteerMode, SteerSource, anySignal, parseSteer, steerWireText } from './agentSteering.cjs';
56
56
  export { PauseController, PauseSource } from './agentPause.cjs';
57
57
  export { createChatModelClient } from './agentChatClient.cjs';
package/dist/index.d.ts CHANGED
@@ -51,7 +51,7 @@ export { StreamError, StreamFrame, StreamTransport, consumeStream, protocolStrea
51
51
  export { ATTENDED_FIRST_FRAME_MS, ATTENDED_TIMEOUT_MS, Attendance, BoundedCallOptions, CallBounds, DeadlineBound, NETWORK_TIMEOUT_MS, PENDING_NOTICE_MS, PendingAttention, PendingState, ProtocolCancelledError, ProtocolTimeoutError, STREAM_IDLE_TIMEOUT_MS, SuspendableDeadline, UNATTENDED_TIMEOUT_MS, attendanceOf, attendanceReason, boundsFor, createSuspendableDeadline, firstFrameBoundsFor, firstFrameTimeoutFor, timeoutFor } from './protocolDeadline.js';
52
52
  export { EvaluationContext, FileQueryResult, FilesMetadata, Metadata, MetadataQueryEntry, MetadataQueryFunction, MetadataQueryRecord, MetadataQueryResult, ModuleExports } from './sandboxTypes.js';
53
53
  export * from '@immediately-run/safe-content';
54
- export { AgentEvents, AgentMessage, AgentRole, AgentTool, COMPACTION_MARKER, ContentBlock, ImageBlock, ModelClient, ModelResponse, NUDGE_TEXT, ReasoningBlock, RunAgentOptions, StallReason, TextBlock, TokenUsage, ToolExecutor, ToolOutcome, ToolResultBlock, ToolUseBlock, compactTranscript, detectStall, estimateTokens, isContextOverflow, runAgent, shouldCompact } from './agentLoop.js';
54
+ export { AgentEvents, AgentMessage, AgentRole, AgentTool, COMPACTION_MARKER, ContentBlock, HOST_CONTEXT_OVERFLOW_CODE, ImageBlock, ModelClient, ModelResponse, NUDGE_TEXT, ReasoningBlock, RunAgentOptions, StallReason, TextBlock, TokenUsage, ToolExecutor, ToolOutcome, ToolResultBlock, ToolUseBlock, compactTranscript, detectStall, estimateTokens, isContextOverflow, runAgent, shouldCompact } from './agentLoop.js';
55
55
  export { INTERRUPTED_TURN_TEXT, STEER_INTERRUPT_MARKER, STEER_MARKER, SteerController, SteerMessage, SteerMode, SteerSource, anySignal, parseSteer, steerWireText } from './agentSteering.js';
56
56
  export { PauseController, PauseSource } from './agentPause.js';
57
57
  export { createChatModelClient } from './agentChatClient.js';
package/dist/version.cjs CHANGED
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  SDK_VERSION: () => SDK_VERSION
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const SDK_VERSION = "0.69.0";
24
+ const SDK_VERSION = "0.71.0";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  SDK_VERSION
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.69.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.71.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.69.0";
2
+ declare const SDK_VERSION = "0.71.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.69.0";
2
+ declare const SDK_VERSION = "0.71.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./chunk-VHAA22YE.js";
2
- const SDK_VERSION = "0.69.0";
2
+ const SDK_VERSION = "0.71.0";
3
3
  export {
4
4
  SDK_VERSION
5
5
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.69.0';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.71.0';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/sdk",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "Runtime SDK for code executing inside an immediately.run sandbox.",
5
5
  "license": "MIT",
6
6
  "repository": "github:immediately-run/immediately-run-sdk",
@@ -40,10 +40,12 @@
40
40
  "api:update": "node scripts/check-api-stability.mjs --update",
41
41
  "check:pins": "node scripts/check-dependency-pins.mjs --self-test && node scripts/check-dependency-pins.mjs",
42
42
  "check:publish-version": "node scripts/check-publish-version.mjs --self-test && node scripts/check-publish-version.mjs",
43
+ "check:published": "node scripts/check-published-parity.mjs --self-test && node scripts/check-published-parity.mjs --offline-ok",
44
+ "check:reproducible": "node scripts/check-build-reproducible.mjs --self-test && node scripts/check-build-reproducible.mjs",
43
45
  "check:clones": "node scripts/check-clones.mjs",
44
46
  "check:unused": "node scripts/check-unused.mjs",
45
47
  "check:untested": "node scripts/check-untested.mjs",
46
- "verify": "npm run check:pins && npm run check:publish-version && npm run format:check && npm run check:circular && npm run check:clones && npm run check:unused && npm run check:untested && npm run check:bundler:selftest && npm run check:bundler && npm run build && npm test && npm run test:safe-content && npm run test:metadata-e2e && npm run test:subpath-imports && npm run api:selftest && npm run api:check && npm run compat:selftest && npm run compat:previous && npm run protocol:check && npm run protocol:selftest && npm run check:ambient:selftest && npm run check:ambient && npm run check:selfhost:selftest && npm run check:selfhost && npm run check:dist-specifiers:selftest && npm run verify:codegen-parity && npm run verify:descriptor-lockstep",
48
+ "verify": "npm run check:pins && npm run check:publish-version && npm run check:published && npm run format:check && npm run check:circular && npm run check:clones && npm run check:unused && npm run check:untested && npm run check:bundler:selftest && npm run check:bundler && npm run build && npm run check:published && npm run check:reproducible && npm test && npm run test:safe-content && npm run test:metadata-e2e && npm run test:subpath-imports && npm run api:selftest && npm run api:check && npm run compat:selftest && npm run compat:previous && npm run protocol:check && npm run protocol:selftest && npm run check:ambient:selftest && npm run check:ambient && npm run check:selfhost:selftest && npm run check:selfhost && npm run check:dist-specifiers:selftest && npm run verify:codegen-parity && npm run verify:descriptor-lockstep",
47
49
  "docs": "typedoc --json docs/api.json && node scripts/gen-descriptor-docs.mjs && node scripts/gen-llms.mjs",
48
50
  "prepublishOnly": "npm run check:circular && npm run build && npm run api:selftest && npm run api:check",
49
51
  "test:safe-content": "node scripts/build-safecontent-e2e.mjs && node --test test/safeContent.e2e.mjs",