@tangle-network/agent-runtime 0.316.0 → 0.318.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.
Files changed (62) hide show
  1. package/dist/{activation-CvdE2HIU.js → activation-CRG-tjnR.js} +2 -2
  2. package/dist/{activation-CvdE2HIU.js.map → activation-CRG-tjnR.js.map} +1 -1
  3. package/dist/{activation-BFDmjaKP.d.ts → activation-RExX9RDm.d.ts} +2 -2
  4. package/dist/agent.d.ts +1 -1
  5. package/dist/agent.js +2 -2
  6. package/dist/{coordination-driver-DGhN8GO9.js → coordination-driver-DK3pI_HB.js} +2 -2
  7. package/dist/{coordination-driver-DGhN8GO9.js.map → coordination-driver-DK3pI_HB.js.map} +1 -1
  8. package/dist/{delegate-B5pmSbQa.js → delegate-Co9vJIgE.js} +2 -2
  9. package/dist/{delegate-B5pmSbQa.js.map → delegate-Co9vJIgE.js.map} +1 -1
  10. package/dist/durable.d.ts +1 -1
  11. package/dist/durable.js +2 -2
  12. package/dist/{graph-G0j1ysqe.js → graph-DYBCYZjg.js} +5 -5
  13. package/dist/{graph-G0j1ysqe.js.map → graph-DYBCYZjg.js.map} +1 -1
  14. package/dist/{improvement-cycle-D7yR6GC2.js → improvement-cycle-jp80l_ZQ.js} +4 -4
  15. package/dist/{improvement-cycle-D7yR6GC2.js.map → improvement-cycle-jp80l_ZQ.js.map} +1 -1
  16. package/dist/{index-CUh40yUJ.d.ts → index-CHlWvoo5.d.ts} +85 -52
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.js +8 -8
  19. package/dist/intelligence.d.ts +2 -2
  20. package/dist/intelligence.js +3 -3
  21. package/dist/kernel.d.ts +2 -2
  22. package/dist/kernel.js +10 -10
  23. package/dist/{loop-runner-bin-DQKLUt0l.d.ts → loop-runner-bin-C-QaaVE2.d.ts} +2 -2
  24. package/dist/{loop-runner-bin-CFVX4oi0.js → loop-runner-bin-C8jLr2gR.js} +3 -3
  25. package/dist/{loop-runner-bin-CFVX4oi0.js.map → loop-runner-bin-C8jLr2gR.js.map} +1 -1
  26. package/dist/loop-runner-bin.d.ts +1 -1
  27. package/dist/loop-runner-bin.js +1 -1
  28. package/dist/mcp/bin.js +3 -3
  29. package/dist/mcp/index.d.ts +2 -2
  30. package/dist/mcp/index.js +5 -5
  31. package/dist/{model-policy-DhvyZ8-O.js → model-policy-B-rOb8Nj.js} +37 -2
  32. package/dist/model-policy-B-rOb8Nj.js.map +1 -0
  33. package/dist/{openai-tools-u3gWBi3x.d.ts → openai-tools-BuJB3JQH.d.ts} +2 -2
  34. package/dist/profiles.d.ts +1 -1
  35. package/dist/profiles.js +1 -1
  36. package/dist/{provision-supervisor-BQLLrUtn.js → provision-supervisor-CunKnayY.js} +4 -4
  37. package/dist/{provision-supervisor-BQLLrUtn.js.map → provision-supervisor-CunKnayY.js.map} +1 -1
  38. package/dist/{runtime-B9BOFyEp.js → runtime-UG5tV_yf.js} +231 -288
  39. package/dist/runtime-UG5tV_yf.js.map +1 -0
  40. package/dist/{search-executor-B5ZVFsDV.js → search-executor-DMX6tCY4.js} +3 -3
  41. package/dist/{search-executor-B5ZVFsDV.js.map → search-executor-DMX6tCY4.js.map} +1 -1
  42. package/dist/{server-TwpH9aik.js → server-fSBvPPLH.js} +3 -3
  43. package/dist/{server-TwpH9aik.js.map → server-fSBvPPLH.js.map} +1 -1
  44. package/dist/{supervise-CU6AuxWY.js → supervise-C41ywfFd.js} +4 -4
  45. package/dist/{supervise-CU6AuxWY.js.map → supervise-C41ywfFd.js.map} +1 -1
  46. package/dist/{supervisor-CbFyf48U.js → supervisor-BXEL_Fxe.js} +480 -50
  47. package/dist/supervisor-BXEL_Fxe.js.map +1 -0
  48. package/dist/testing.d.ts +1 -1
  49. package/dist/testing.js +12 -12
  50. package/dist/tool-loop.d.ts +15 -2
  51. package/dist/tool-loop.js +5 -3
  52. package/dist/tool-loop.js.map +1 -1
  53. package/dist/tui/index.d.ts +1 -1
  54. package/dist/tui/index.js +1 -1
  55. package/package.json +3 -2
  56. package/skills/agent-graphs/references/authoring.md +7 -0
  57. package/skills/codemode/SKILL.md +6 -4
  58. package/skills/codemode/references/runtime-execution.md +56 -11
  59. package/skills/profile-authoring/SKILL.md +17 -15
  60. package/dist/model-policy-DhvyZ8-O.js.map +0 -1
  61. package/dist/runtime-B9BOFyEp.js.map +0 -1
  62. package/dist/supervisor-CbFyf48U.js.map +0 -1
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Av as ToolLoopCallContext, Bt as RunGraphTestOptions, Dc as superviseWithTestBrain, Tc as SuperviseTestOptions, Wt as runGraphWithTestBrain, jv as ToolLoopChat, mn as driverAgent, pn as DriverAgentOptions, ul as SupervisorAgentTestDeps, yl as supervisorAgentWithTestBrain } from "./index-CUh40yUJ.js";
1
+ import { Bt as RunGraphTestOptions, Cc as superviseWithTestBrain, Dv as ToolLoopChat, Ev as ToolLoopCallContext, Wt as runGraphWithTestBrain, hl as supervisorAgentWithTestBrain, mn as driverAgent, ol as SupervisorAgentTestDeps, pn as DriverAgentOptions, xc as SuperviseTestOptions } from "./index-CHlWvoo5.js";
2
2
  import { AgentImprovementProposal, AgentProfile, AgentProfileImprovementMeasuredComparison, SandboxSizePreset } from "@tangle-network/agent-interface";
3
3
  //#region src/testing/index.d.ts
4
4
  /** A proposal produced by Runtime's opaque profile-improvement path. */
package/dist/testing.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { I as immutableCandidateValue, M as canonicalCandidateDigest$1 } from "./workspace-streams-DkijayqX.js";
2
- import { f as verifyAgentImprovementProposal } from "./improvement-cycle-D7yR6GC2.js";
2
+ import { f as verifyAgentImprovementProposal } from "./improvement-cycle-jp80l_ZQ.js";
3
3
  import { i as applyExactAgentProfileDiff, p as parseExactAgentProfile } from "./profile-CHXYmpGd.js";
4
- import { r as driverAgent } from "./coordination-driver-DGhN8GO9.js";
5
- import { a as superviseWithTestBrain, m as supervisorAgentWithTestBrain } from "./supervise-CU6AuxWY.js";
6
- import { i as runGraphWithTestBrain } from "./graph-G0j1ysqe.js";
4
+ import { r as driverAgent } from "./coordination-driver-DK3pI_HB.js";
5
+ import { a as superviseWithTestBrain, m as supervisorAgentWithTestBrain } from "./supervise-C41ywfFd.js";
6
+ import { i as runGraphWithTestBrain } from "./graph-DYBCYZjg.js";
7
7
  import { SANDBOX_SIZE_PRESET_NAMES, canonicalAgentProfileDigest } from "@tangle-network/agent-interface";
8
8
  //#region src/testing/fixtures/agent-improvement-proposal.json
9
9
  var agent_improvement_proposal_default = {
10
10
  changedSurfaces: ["prompt"],
11
- digest: "sha256:a9a15f7a7a0d46045f8ec065659d19154e192e4e158a56869dd4f6d68f7b0cd5",
11
+ digest: "sha256:6fa8d47783fd932aec5ede5b31e1b1eda4cdf2b40e9180c5da9c0c5d5298d174",
12
12
  evaluation: {
13
13
  "decision": {
14
14
  "contributingChecks": [
@@ -4579,7 +4579,7 @@ var agent_improvement_proposal_default = {
4579
4579
  ],
4580
4580
  "metadata": {
4581
4581
  "fixture": "agent-improvement-proposal",
4582
- "runtimeVersion": "0.316.0"
4582
+ "runtimeVersion": "0.318.0"
4583
4583
  },
4584
4584
  "objectives": [
4585
4585
  {
@@ -4690,8 +4690,8 @@ var agent_improvement_proposal_default = {
4690
4690
  "baselineContentHash": "sha256:5c21ee53e513fc604cb09754e21c392b24a424da0ef37dbf8f1ee4a8a0b08f09",
4691
4691
  "candidateContentHash": "sha256:60fcbb1c728194bd51d7d19cb732d1c3f1881dce7e0a6266b41c8b98cfd65693",
4692
4692
  "kind": "agent-eval-loop",
4693
- "recordDigest": "sha256:2220856da7165d815cd6182b0d333b3db190ac767616f2493011b3656223d3da",
4694
- "runId": "agent-runtime-0.316.0-proposal-fixture",
4693
+ "recordDigest": "sha256:ac37057ac69db1739b5e62c052813c08efc956a8eaee649ce0146044ff40b7e5",
4694
+ "runId": "agent-runtime-0.318.0-proposal-fixture",
4695
4695
  "schema": "agent-candidate-experiment"
4696
4696
  }
4697
4697
  },
@@ -4714,13 +4714,13 @@ var agent_improvement_proposal_default = {
4714
4714
  }],
4715
4715
  kind: "agent-improvement-proposal",
4716
4716
  proposedAt: "2026-07-10T01:00:00.000Z",
4717
- runId: "agent-runtime-0.316.0-proposal-fixture"
4717
+ runId: "agent-runtime-0.318.0-proposal-fixture"
4718
4718
  };
4719
4719
  //#endregion
4720
4720
  //#region src/testing/fixtures/agent-profile-improvement-proposal.json
4721
4721
  var agent_profile_improvement_proposal_default = {
4722
4722
  changedSurfaces: ["prompt", "skills"],
4723
- digest: "sha256:9ac9d2ca85137901e2368e4b8829392b658a026c0ec9b6ca4a74805acd7a9da2",
4723
+ digest: "sha256:3884e3f5c91e1007b37615c5ee1261c385c3548e0acd0d58453a8db73c3b513f",
4724
4724
  evaluation: {
4725
4725
  "decision": {
4726
4726
  "contributingChecks": [
@@ -6354,7 +6354,7 @@ var agent_profile_improvement_proposal_default = {
6354
6354
  ],
6355
6355
  "metadata": {
6356
6356
  "fixture": "agent-profile-improvement-proposal",
6357
- "runtimeVersion": "0.316.0"
6357
+ "runtimeVersion": "0.318.0"
6358
6358
  },
6359
6359
  "objectives": [
6360
6360
  {
@@ -6465,7 +6465,7 @@ var agent_profile_improvement_proposal_default = {
6465
6465
  "baselineContentHash": "sha256:21c495a37c418c10bde64fbaa188beddeed31f1f051ea60a6a6582a9ee0db704",
6466
6466
  "candidateContentHash": "sha256:103f77bc8481601eef1ad5fe6ba84a40dffabc3a44f421f8c8559121edab84e9",
6467
6467
  "kind": "agent-eval-loop",
6468
- "recordDigest": "sha256:3b5bd31ff4221182c11d47ab2ddcfc34ed49c05b3790eead4226cc89a5a0972a",
6468
+ "recordDigest": "sha256:f5da112352ed105eaf6a64b9979fc67f608bd359d677fade6ac080c31563d886",
6469
6469
  "runId": "profile-improvement-1",
6470
6470
  "schema": "agent-profile-improvement-experiment"
6471
6471
  }
@@ -4,6 +4,13 @@ interface ToolLoopCall {
4
4
  toolCallId?: string;
5
5
  toolName: string;
6
6
  args: Record<string, unknown>;
7
+ /**
8
+ * Provider-specific fields the model attached to this call, echoed back on
9
+ * its assistant `tool_calls` entry in the next request. Gemini 3 returns
10
+ * `extra_content: { google: { thought_signature } }` on each call and answers
11
+ * HTTP 400 when the next request omits it.
12
+ */
13
+ providerFields?: Record<string, unknown>;
7
14
  }
8
15
  /** Outcome of one tool dispatch — structurally compatible with a hub/integration
9
16
  * tool-outcome union, so callers can fold either through the loop. */
@@ -16,8 +23,10 @@ type ToolCallOutcome = {
16
23
  message: string;
17
24
  status?: number;
18
25
  };
19
- /** One OpenAI-shaped tool-call entry carried on an assistant message. */
26
+ /** One OpenAI-shaped tool-call entry carried on an assistant message, plus any
27
+ * provider-specific fields the model attached to the call (`ToolLoopCall.providerFields`). */
20
28
  interface ToolLoopAssistantToolCall {
29
+ [providerField: string]: unknown;
21
30
  id: string;
22
31
  type: 'function';
23
32
  function: {
@@ -145,10 +154,14 @@ type StreamToolLoopYield<Raw> = {
145
154
  kind: 'capped';
146
155
  pending: number;
147
156
  stopReason: Exclude<ToolLoopStopReason, 'completed'>;
157
+ /** On `deadline` only: the conversation after the system prompt, every tool call answered.
158
+ * Passing it back as `priorMessages` with no `userMessage` continues the same turn. */
159
+ messages?: ToolLoopMessage[];
148
160
  };
149
161
  interface StreamToolLoopOptions<Raw> {
150
162
  systemPrompt: string;
151
- userMessage: string;
163
+ /** Omit to continue a checkpointed turn whose `priorMessages` already end with tool results. */
164
+ userMessage?: string;
152
165
  priorMessages?: ToolLoopMessage[];
153
166
  streamTurn: (messages: ToolLoopMessage[]) => AsyncIterable<Raw>;
154
167
  extractText: (event: Raw) => string;
package/dist/tool-loop.js CHANGED
@@ -27,6 +27,7 @@ function assistantToolCallMessage(turnText, pending) {
27
27
  role: "assistant",
28
28
  content: turnText.trim() || null,
29
29
  tool_calls: pending.map((call) => ({
30
+ ...call.providerFields,
30
31
  id: toolCallId(call),
31
32
  type: "function",
32
33
  function: {
@@ -245,10 +246,10 @@ async function* streamToolLoop(opts) {
245
246
  content: opts.systemPrompt
246
247
  },
247
248
  ...opts.priorMessages ?? [],
248
- {
249
+ ...opts.userMessage !== void 0 ? [{
249
250
  role: "user",
250
251
  content: opts.userMessage
251
- }
252
+ }] : []
252
253
  ];
253
254
  const observer = createToolLoopObserver(opts.hooks, runId, opts.scenarioId);
254
255
  let accumulatedCostUsd = 0;
@@ -264,7 +265,8 @@ async function* streamToolLoop(opts) {
264
265
  yield {
265
266
  kind: "capped",
266
267
  pending: 0,
267
- stopReason: "deadline"
268
+ stopReason: "deadline",
269
+ messages: messages.slice(1)
268
270
  };
269
271
  return;
270
272
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tool-loop.js","names":[],"sources":["../src/tool-loop.ts"],"sourcesContent":["/**\n * Bounded turn-level tool-dispatch loop.\n *\n * `runAgentTaskStream` runs ONE model turn; `runAgentRounds` orchestrates DELEGATED\n * multi-agent topologies (refine / fanout-vote). Neither is the everyday\n * interactive shape: a chat turn where the model may emit tool calls, each is\n * executed, the results are folded back, and the turn re-runs until the model\n * stops (or a turn cap). Every agent app hand-rolls that loop — this is it,\n * as a reusable primitive.\n *\n * Substrate-neutral by design: the caller supplies `streamTurn` (wrapping\n * whatever backend / `runAgentTaskStream` it uses) and `executeToolCall`\n * (routing to its executors). This module owns the LOOP; the caller owns the\n * model and the executors. `Raw` (streaming variant) is the caller's own\n * event type. The only imported contract is the runtime hook type: hooks are\n * execution-scoped observers, not part of the agent profile.\n */\n\nimport type { RuntimeDecisionEvidenceRef, RuntimeHooks } from './runtime-hooks'\nimport { notifyRuntimeDecisionPoint, notifyRuntimeHookEvent } from './runtime-hooks'\n\nexport interface ToolLoopCall {\n toolCallId?: string\n toolName: string\n args: Record<string, unknown>\n}\n\n/** Outcome of one tool dispatch — structurally compatible with a hub/integration\n * tool-outcome union, so callers can fold either through the loop. */\nexport type ToolCallOutcome =\n | { ok: true; result: unknown }\n | { ok: false; code: string; message: string; status?: number }\n\n/** Runaway-backstop: stops an infinite tool loop where cost is unmetered. Set\n * far above any legitimate workflow — this is a watchdog, not a policy cap.\n * Legitimate per-call budgets come from `maxCostUsd` + `costOf`. */\nconst RUNAWAY_BACKSTOP_TURNS = 200\nconst DEFAULT_DECISION_CONTEXT_CHARS = 12_000\nconst FAILURE_RECOVERY_ACTIONS = ['retry', 'verify', 'continue', 'stop']\n/** Consecutive identical calls (same tool + canonical-JSON args) that trigger\n * stuck-loop detection. The window resets on any different call. */\nconst STUCK_LOOP_THRESHOLD = 3\n\n/** One OpenAI-shaped tool-call entry carried on an assistant message. */\nexport interface ToolLoopAssistantToolCall {\n id: string\n type: 'function'\n function: { name: string; arguments: string }\n}\n\n/**\n * A message in the running conversation the loop sends to `streamTurn`.\n *\n * The base `{ role, content }` covers `system` / `user` / plain `assistant`\n * turns. Two optional fields carry the OpenAI function-calling contract so a\n * strict model (Claude, and any OpenAI-compatible provider that validates tool\n * history) reads its own tool use back instead of re-issuing the same call:\n *\n * - an assistant turn that emitted tool calls carries `tool_calls`, and its\n * `content` is `null` when the turn was tool-only;\n * - each tool result is its own `{ role: 'tool', tool_call_id, content }`\n * message keyed to the call that produced it.\n *\n * Widening is additive: a `streamTurn` that reads only `role` + `content` still\n * works; one that forwards the whole message to an OpenAI-compatible endpoint\n * now sends correct tool history.\n */\nexport type ToolLoopMessage = {\n role: string\n content: string | null\n tool_calls?: ToolLoopAssistantToolCall[]\n tool_call_id?: string\n}\n\n/** A tool-call id is required to key a `role: 'tool'` result back to its call.\n * When the model omitted one, derive a stable id from the tool name so the\n * assistant `tool_calls` entry and its `tool` result still match. */\nfunction toolCallId(call: ToolLoopCall): string {\n return call.toolCallId ?? `call_${call.toolName}`\n}\n\n/** The assistant turn that emitted `pending`, in OpenAI shape: text content\n * (null when the turn was tool-only) plus its `tool_calls` array. */\nfunction assistantToolCallMessage(turnText: string, pending: ToolLoopCall[]): ToolLoopMessage {\n return {\n role: 'assistant',\n content: turnText.trim() || null,\n tool_calls: pending.map((call) => ({\n id: toolCallId(call),\n type: 'function',\n function: { name: call.toolName, arguments: JSON.stringify(call.args) },\n })),\n }\n}\n\n/** One `role: 'tool'` result message keyed to its call by `tool_call_id`. */\nfunction toolResultMessage(call: ToolLoopCall, content: string): ToolLoopMessage {\n return { role: 'tool', tool_call_id: toolCallId(call), content }\n}\n\n/**\n * One progress report from a running tool, shaped like an MCP `notifications/progress`\n * payload: a free-text `message`, a monotonic `progress` count with an optional\n * `total`, and any structured `data` the caller's UI understands.\n */\nexport interface ToolProgressUpdate {\n message?: string\n progress?: number\n total?: number\n data?: unknown\n}\n\n/** What the loop hands a running tool besides its call. */\nexport interface ToolCallContext {\n /**\n * Report progress while the call runs. The streaming loop yields each update as a\n * `tool_progress` step, in order, before the call's `tool_result`; updates sent after\n * the call settles are dropped.\n */\n progress: (update: ToolProgressUpdate) => void\n}\n\n/** Executes one tool call. Implementations that take only the call stay valid. */\nexport type ToolCallExecutor = (\n call: ToolLoopCall,\n context: ToolCallContext,\n) => Promise<ToolCallOutcome>\n\nfunction defaultRender(label: string, outcome: ToolCallOutcome): string {\n if (outcome.ok) return `- ${label} → ok: ${JSON.stringify(outcome.result)}`\n return `- ${label} → failed (${outcome.code}): ${outcome.message}`\n}\n\n// ── Awaitable variant (drain-only callers, tests) ──────────────────────────\n\nexport type ToolLoopEvent =\n | { type: 'text'; text: string }\n | { type: 'tool_call'; call: ToolLoopCall }\n | { type: 'other'; event: unknown }\n\n/** Why the loop stopped. `completed` = model finished naturally; `stuck-loop` =\n * ≥3 consecutive identical tool calls (same tool + args); `backstop` = hit the\n * runaway-backstop cap (200 by default); `deadline` = wall-clock deadlineMs\n * exceeded; `budget` = maxCostUsd exhausted. Non-`completed` stops are infra /\n * resource outcomes — eval scoring must distinguish them from capability failure. */\nexport type ToolLoopStopReason = 'completed' | 'stuck-loop' | 'backstop' | 'deadline' | 'budget'\n\nexport interface ToolLoopResult {\n finalText: string\n toolResults: Array<{ call: ToolLoopCall; label: string; outcome: ToolCallOutcome }>\n turns: number\n stopReason: ToolLoopStopReason\n /** @deprecated Use `stopReason !== 'completed'` instead. */\n cappedOut: boolean\n}\n\nexport interface RunToolLoopOptions {\n systemPrompt: string\n userMessage: string\n priorMessages?: ToolLoopMessage[]\n streamTurn: (messages: ToolLoopMessage[]) => AsyncIterable<ToolLoopEvent>\n executeToolCall: ToolCallExecutor\n /** Receives each progress update a running tool reports, in order. */\n onToolProgress?: (call: ToolLoopCall, update: ToolProgressUpdate) => void\n isExecutableTool: (toolName: string) => boolean\n /** Runaway-backstop cap. Default 200 — set far above any legitimate workflow.\n * For per-workflow limits, use `maxCostUsd` or `deadlineMs` instead. */\n maxToolTurns?: number\n /** Wall-clock deadline in ms since epoch (Date.now()-based). When exceeded the\n * loop stops with stopReason `deadline`. */\n deadlineMs?: number\n /** Maximum total cost in USD. Requires `costOf` to meter each tool call. */\n maxCostUsd?: number\n /** Return the USD cost of one outcome. Required for `maxCostUsd` to work. */\n costOf?: (call: ToolLoopCall, outcome: ToolCallOutcome) => number\n renderResult?: (label: string, outcome: ToolCallOutcome) => string\n labelFor?: (call: ToolLoopCall) => string\n runId?: string\n scenarioId?: string\n hooks?: RuntimeHooks\n}\n\n/** Run the bounded tool loop and return the final text + every executed tool\n * outcome. Awaitable — callers needing to stream events to a UI use\n * {@link streamToolLoop}. */\nexport async function runToolLoop(opts: RunToolLoopOptions): Promise<ToolLoopResult> {\n const backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: ToolLoopCall) => c.toolName)\n const runId = opts.runId ?? `agent-run-${randomSuffix()}`\n const messages: ToolLoopMessage[] = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n const observer = createToolLoopObserver(opts.hooks, runId, opts.scenarioId)\n const toolResults: ToolLoopResult['toolResults'] = []\n let finalText = ''\n let turns = 0\n let accumulatedCostUsd = 0\n // Stuck-loop detection: track the last canonical-JSON call signature and how\n // many consecutive times we've seen it.\n let lastCallHash: string | null = null\n let consecutiveCount = 0\n\n observer.loopBefore(backstop, messages.length)\n\n for (let toolTurn = 0; ; toolTurn++) {\n turns++\n\n // Wall-clock deadline check — before every new turn.\n if (opts.deadlineMs !== undefined && Date.now() >= opts.deadlineMs) {\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'deadline' })\n return { finalText, toolResults, turns, stopReason: 'deadline', cappedOut: true }\n }\n\n let turnText = ''\n const pending: ToolLoopCall[] = []\n const turnEventId = observer.turnBefore(toolTurn, messages.length)\n for await (const ev of opts.streamTurn([...messages])) {\n if (ev.type === 'text') {\n turnText += ev.text\n finalText += ev.text\n } else if (ev.type === 'tool_call' && opts.isExecutableTool(ev.call.toolName)) {\n pending.push(ev.call)\n }\n }\n if (pending.length === 0) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: 0,\n finalTextChars: finalText.length,\n })\n break\n }\n\n // Runaway backstop — the model keeps emitting calls past the safety ceiling.\n if (toolTurn >= backstop) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'backstop',\n })\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'backstop' })\n return { finalText, toolResults, turns, stopReason: 'backstop', cappedOut: true }\n }\n\n // The assistant turn that emitted the calls, carrying its tool_calls array,\n // so a strict model reads its own tool use back in OpenAI shape.\n messages.push(assistantToolCallMessage(turnText, pending))\n const outcomes: ExecutedToolCall[] = []\n for (const [callIndex, call] of pending.entries()) {\n // Stuck-loop detection: hash the first pending call each turn (a model\n // stuck in a loop re-issues the same call repeatedly, not alternating calls).\n const callHash = canonicalCallHash(call)\n if (callHash === lastCallHash) {\n consecutiveCount++\n } else {\n lastCallHash = callHash\n consecutiveCount = 1\n }\n if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'stuck-loop',\n })\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'stuck-loop' })\n return { finalText, toolResults, turns, stopReason: 'stuck-loop', cappedOut: true }\n }\n\n const callEventId = observer.toolCallBefore(toolTurn, turnEventId, callIndex, call)\n let settled = false\n const outcome = await executeGuarded(opts.executeToolCall, call, (update) => {\n if (!settled) opts.onToolProgress?.(call, update)\n })\n settled = true\n\n // Budget check after each tool call.\n if (opts.maxCostUsd !== undefined && opts.costOf !== undefined) {\n accumulatedCostUsd += opts.costOf(call, outcome)\n if (accumulatedCostUsd >= opts.maxCostUsd) {\n const label = labelFor(call)\n toolResults.push({ call, label, outcome })\n messages.push(toolResultMessage(call, render(label, outcome)))\n observer.toolCallAfter(toolTurn, callEventId, call, outcome)\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'budget',\n })\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'budget' })\n return { finalText, toolResults, turns, stopReason: 'budget', cappedOut: true }\n }\n }\n\n const label = labelFor(call)\n const rendered = render(label, outcome)\n toolResults.push({ call, label, outcome })\n outcomes.push({ call, label, outcome, rendered })\n // One role:'tool' message per result, keyed to its call by tool_call_id.\n messages.push(toolResultMessage(call, rendered))\n observer.toolCallAfter(toolTurn, callEventId, call, outcome)\n }\n observer.failureRecovery({\n toolTurn,\n messages,\n turnText,\n outcomes,\n })\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n toolResults: outcomes.map((item) => ({\n toolName: item.call.toolName,\n toolCallId: item.call.toolCallId,\n ok: item.outcome.ok,\n })),\n failedToolCalls: outcomes.filter((item) => !item.outcome.ok).length,\n })\n }\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'completed' })\n return { finalText, toolResults, turns, stopReason: 'completed', cappedOut: false }\n}\n\n// ── Streaming variant (SSE chat runtimes + per-event telemetry) ────────────\n\nexport type StreamToolLoopYield<Raw> =\n | { kind: 'event'; event: Raw }\n | {\n kind: 'tool_progress'\n toolName: string\n toolCallId?: string\n update: ToolProgressUpdate\n }\n | {\n kind: 'tool_result'\n toolName: string\n toolCallId?: string\n label: string\n outcome: ToolCallOutcome\n }\n | { kind: 'capped'; pending: number; stopReason: Exclude<ToolLoopStopReason, 'completed'> }\n\nexport interface StreamToolLoopOptions<Raw> {\n systemPrompt: string\n userMessage: string\n priorMessages?: ToolLoopMessage[]\n streamTurn: (messages: ToolLoopMessage[]) => AsyncIterable<Raw>\n extractText: (event: Raw) => string\n extractToolCall: (event: Raw) => ToolLoopCall | null\n isExecutableTool: (toolName: string) => boolean\n executeToolCall: ToolCallExecutor\n /** Runaway-backstop cap. Default 200 — set far above any legitimate workflow. */\n maxToolTurns?: number\n /** Wall-clock deadline in ms since epoch (Date.now()-based). */\n deadlineMs?: number\n /** Maximum total cost in USD. Requires `costOf` to meter each tool call. */\n maxCostUsd?: number\n /** Return the USD cost of one outcome. Required for `maxCostUsd` to work. */\n costOf?: (call: ToolLoopCall, outcome: ToolCallOutcome) => number\n renderResult?: (label: string, outcome: ToolCallOutcome) => string\n labelFor?: (call: ToolLoopCall) => string\n runId?: string\n scenarioId?: string\n hooks?: RuntimeHooks\n}\n\n/** Streaming bounded tool loop: yields each raw turn event (the caller maps +\n * telemetries + re-emits it), each `tool_progress` a running tool reports, and\n * each executed `tool_result`; emits one `capped` if it stops for any\n * non-completed reason with calls still pending. */\nexport async function* streamToolLoop<Raw>(\n opts: StreamToolLoopOptions<Raw>,\n): AsyncGenerator<StreamToolLoopYield<Raw>, void, unknown> {\n const backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: ToolLoopCall) => c.toolName)\n const runId = opts.runId ?? `agent-run-${randomSuffix()}`\n const messages: ToolLoopMessage[] = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n const observer = createToolLoopObserver(opts.hooks, runId, opts.scenarioId)\n let accumulatedCostUsd = 0\n let lastCallHash: string | null = null\n let consecutiveCount = 0\n\n observer.loopBefore(backstop, messages.length)\n\n for (let toolTurn = 0; ; toolTurn++) {\n // Wall-clock deadline check before every new turn.\n if (opts.deadlineMs !== undefined && Date.now() >= opts.deadlineMs) {\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'deadline' })\n yield { kind: 'capped', pending: 0, stopReason: 'deadline' }\n return\n }\n\n let turnText = ''\n const pending: ToolLoopCall[] = []\n const turnEventId = observer.turnBefore(toolTurn, messages.length)\n for await (const event of opts.streamTurn([...messages])) {\n yield { kind: 'event', event }\n turnText += opts.extractText(event)\n const call = opts.extractToolCall(event)\n if (call && opts.isExecutableTool(call.toolName)) pending.push(call)\n }\n if (pending.length === 0) {\n observer.turnAfter(toolTurn, turnEventId, { pendingToolCalls: 0 })\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'completed' })\n return\n }\n\n // Runaway backstop.\n if (toolTurn >= backstop) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'backstop',\n })\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'backstop' })\n yield { kind: 'capped', pending: pending.length, stopReason: 'backstop' }\n return\n }\n\n // The assistant turn that emitted the calls, carrying its tool_calls array,\n // so a strict model reads its own tool use back in OpenAI shape.\n messages.push(assistantToolCallMessage(turnText, pending))\n const outcomes: ExecutedToolCall[] = []\n for (const [callIndex, call] of pending.entries()) {\n // Stuck-loop detection.\n const callHash = canonicalCallHash(call)\n if (callHash === lastCallHash) {\n consecutiveCount++\n } else {\n lastCallHash = callHash\n consecutiveCount = 1\n }\n if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'stuck-loop',\n })\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'stuck-loop' })\n yield { kind: 'capped', pending: pending.length, stopReason: 'stuck-loop' }\n return\n }\n\n const callEventId = observer.toolCallBefore(toolTurn, turnEventId, callIndex, call)\n // Progress arrives while the call is in flight; yield it as it lands, in order,\n // and stop accepting it once the call settles so it never trails the result.\n const progress: ToolProgressUpdate[] = []\n let wake: (() => void) | undefined\n let settled = false\n const running = executeGuarded(opts.executeToolCall, call, (update) => {\n if (settled) return\n progress.push(update)\n wake?.()\n }).then((result) => {\n settled = true\n wake?.()\n return result\n })\n while (!settled || progress.length > 0) {\n const update = progress.shift()\n if (update !== undefined) {\n yield {\n kind: 'tool_progress',\n toolName: call.toolName,\n toolCallId: call.toolCallId,\n update,\n }\n continue\n }\n await new Promise<void>((resolve) => {\n wake = resolve\n })\n wake = undefined\n }\n const outcome = await running\n\n // Budget check after each tool call.\n if (opts.maxCostUsd !== undefined && opts.costOf !== undefined) {\n accumulatedCostUsd += opts.costOf(call, outcome)\n if (accumulatedCostUsd >= opts.maxCostUsd) {\n const label = labelFor(call)\n yield {\n kind: 'tool_result',\n toolName: call.toolName,\n toolCallId: call.toolCallId,\n label,\n outcome,\n }\n messages.push(toolResultMessage(call, render(label, outcome)))\n observer.toolCallAfter(toolTurn, callEventId, call, outcome)\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'budget',\n })\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'budget' })\n yield { kind: 'capped', pending: pending.length, stopReason: 'budget' }\n return\n }\n }\n\n const label = labelFor(call)\n yield {\n kind: 'tool_result',\n toolName: call.toolName,\n toolCallId: call.toolCallId,\n label,\n outcome,\n }\n const rendered = render(label, outcome)\n outcomes.push({ call, label, outcome, rendered })\n // One role:'tool' message per result, keyed to its call by tool_call_id.\n messages.push(toolResultMessage(call, rendered))\n observer.toolCallAfter(toolTurn, callEventId, call, outcome)\n }\n observer.failureRecovery({\n toolTurn,\n messages,\n turnText,\n outcomes,\n })\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n toolResults: outcomes.map((item) => ({\n toolName: item.call.toolName,\n toolCallId: item.call.toolCallId,\n ok: item.outcome.ok,\n })),\n failedToolCalls: outcomes.filter((item) => !item.outcome.ok).length,\n })\n }\n}\n\n/** Run one call; a thrown executor becomes an `executor_error` outcome. */\nasync function executeGuarded(\n execute: ToolCallExecutor,\n call: ToolLoopCall,\n progress: (update: ToolProgressUpdate) => void,\n): Promise<ToolCallOutcome> {\n try {\n return await execute(call, { progress })\n } catch (err) {\n return {\n ok: false,\n code: 'executor_error',\n message: err instanceof Error ? err.message : String(err),\n }\n }\n}\n\ninterface ExecutedToolCall {\n call: ToolLoopCall\n label: string\n outcome: ToolCallOutcome\n rendered: string\n}\n\ninterface NotifyToolFailureRecoveryOptions {\n hooks?: RuntimeHooks\n runId: string\n scenarioId?: string\n stepIndex: number\n messages: ToolLoopMessage[]\n turnText: string\n outcomes: ExecutedToolCall[]\n}\n\ninterface NotifyToolLoopEventOptions {\n hooks?: RuntimeHooks\n runId: string\n scenarioId?: string\n target: 'agent.run' | 'agent.turn' | 'agent.tool_call'\n phase: 'before' | 'after' | 'error' | 'event'\n id?: string\n stepIndex?: number\n parentId?: string\n payload?: Record<string, unknown>\n metadata?: Record<string, unknown>\n}\n\ninterface ToolLoopObserver {\n loopBefore(maxToolTurns: number, messageCount: number): void\n loopAfter(payload: Record<string, unknown>): void\n turnBefore(toolTurn: number, messageCount: number): string\n turnAfter(toolTurn: number, turnEventId: string, payload: Record<string, unknown>): void\n toolCallBefore(\n toolTurn: number,\n turnEventId: string,\n callIndex: number,\n call: ToolLoopCall,\n ): string\n toolCallAfter(\n toolTurn: number,\n callEventId: string,\n call: ToolLoopCall,\n outcome: ToolCallOutcome,\n ): void\n failureRecovery(options: {\n toolTurn: number\n messages: ToolLoopMessage[]\n turnText: string\n outcomes: ExecutedToolCall[]\n }): void\n}\n\nfunction createToolLoopObserver(\n hooks: RuntimeHooks | undefined,\n runId: string,\n scenarioId: string | undefined,\n): ToolLoopObserver {\n const loopEventId = `${runId}:agent.run`\n return {\n loopBefore: (maxToolTurns, messageCount) => {\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.run',\n phase: 'before',\n id: `${loopEventId}:before`,\n payload: { maxToolTurns, messageCount },\n })\n },\n loopAfter: (payload) => {\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.run',\n phase: 'after',\n id: `${loopEventId}:after`,\n payload,\n })\n },\n turnBefore: (toolTurn, messageCount) => {\n const turnEventId = `${loopEventId}:${toolTurn}`\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.turn',\n phase: 'before',\n id: turnEventId,\n stepIndex: toolTurn,\n parentId: loopEventId,\n payload: { messageCount },\n })\n return turnEventId\n },\n turnAfter: (toolTurn, turnEventId, payload) => {\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.turn',\n phase: 'after',\n id: `${turnEventId}:after`,\n stepIndex: toolTurn,\n parentId: turnEventId,\n payload,\n })\n },\n toolCallBefore: (toolTurn, turnEventId, callIndex, call) => {\n const callEventId = `${turnEventId}:tool-call:${callIndex}`\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.tool_call',\n phase: 'before',\n id: callEventId,\n stepIndex: toolTurn,\n parentId: turnEventId,\n payload: toolCallPayload(call),\n })\n return callEventId\n },\n toolCallAfter: (toolTurn, callEventId, call, outcome) => {\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.tool_call',\n phase: 'after',\n id: `${callEventId}:after`,\n stepIndex: toolTurn,\n parentId: callEventId,\n payload: { ...toolCallPayload(call), outcome: outcomePayload(outcome) },\n })\n },\n failureRecovery: (options) => {\n notifyToolFailureRecovery({\n hooks,\n runId,\n scenarioId,\n stepIndex: options.toolTurn,\n messages: options.messages,\n turnText: options.turnText,\n outcomes: options.outcomes,\n })\n },\n }\n}\n\nfunction notifyToolLoopEvent(options: NotifyToolLoopEventOptions): void {\n notifyRuntimeHookEvent(options.hooks, {\n id: options.id ?? `${options.runId}:${options.target}:${options.phase}`,\n runId: options.runId,\n scenarioId: options.scenarioId,\n target: options.target,\n phase: options.phase,\n timestamp: Date.now(),\n stepIndex: options.stepIndex,\n parentId: options.parentId,\n payload: options.payload,\n metadata: { producer: 'tool-loop', ...options.metadata },\n })\n}\n\nfunction notifyToolFailureRecovery(options: NotifyToolFailureRecoveryOptions): void {\n const failed = options.outcomes.filter((item) => !item.outcome.ok)\n if (failed.length === 0) return\n\n const evidence: RuntimeDecisionEvidenceRef[] = []\n for (const item of failed) {\n const id = item.call.toolCallId ?? `${options.stepIndex}:${item.label}`\n evidence.push({\n source: 'tool_call',\n id,\n detail: `${item.call.toolName} ${stringifySafe(item.call.args, 2_000)}`,\n metadata: { toolName: item.call.toolName, label: item.label },\n })\n evidence.push({\n source: 'tool_result',\n id: `${id}:result`,\n detail: item.rendered,\n metadata: failureMetadata(item.outcome),\n })\n }\n\n notifyRuntimeDecisionPoint(options.hooks, {\n id: `${options.runId}:agent.turn:${options.stepIndex}:failure-recovery`,\n runId: options.runId,\n scenarioId: options.scenarioId,\n stepIndex: options.stepIndex,\n kind: 'retry',\n candidateActions: [...FAILURE_RECOVERY_ACTIONS],\n context: renderDecisionContext(options.messages, options.turnText, options.outcomes),\n evidence,\n metadata: {\n target: 'failure-recovery',\n source: 'agent.turn',\n failedToolCount: failed.length,\n toolNames: failed.map((item) => item.call.toolName),\n },\n })\n}\n\nfunction toolCallPayload(call: ToolLoopCall): Record<string, unknown> {\n return {\n toolName: call.toolName,\n toolCallId: call.toolCallId,\n argsPreview: stringifySafe(call.args, 2_000),\n }\n}\n\nfunction outcomePayload(outcome: ToolCallOutcome): Record<string, unknown> {\n if (!outcome.ok) {\n return {\n ok: false,\n code: outcome.code,\n message: trimText(outcome.message, 2_000),\n status: outcome.status,\n }\n }\n return {\n ok: true,\n resultPreview: stringifySafe(outcome.result, 2_000),\n }\n}\n\nfunction failureMetadata(outcome: ToolCallOutcome): Record<string, unknown> | undefined {\n if (outcome.ok) return undefined\n return {\n code: outcome.code,\n message: outcome.message,\n status: outcome.status,\n }\n}\n\nfunction renderDecisionContext(\n messages: ToolLoopMessage[],\n turnText: string,\n outcomes: ExecutedToolCall[],\n): string {\n const recent = messages.slice(-6).map((message) => `[${message.role}]\\n${message.content ?? ''}`)\n const assistant = turnText.trim() ? [`[assistant]\\n${turnText}`] : []\n const toolResults = [`[tool results]\\n${outcomes.map((item) => item.rendered).join('\\n')}`]\n return trimText(\n [...recent, ...assistant, ...toolResults].join('\\n\\n'),\n DEFAULT_DECISION_CONTEXT_CHARS,\n )\n}\n\n/** Canonical identifier for a tool call used by stuck-loop detection.\n * Keys are sorted so `{b:1,a:2}` and `{a:2,b:1}` produce the same hash. */\nfunction canonicalCallHash(call: ToolLoopCall): string {\n const sortedArgs = Object.fromEntries(\n Object.entries(call.args).sort(([a], [b]) => a.localeCompare(b)),\n )\n return `${call.toolName}:${JSON.stringify(sortedArgs)}`\n}\n\nfunction stringifySafe(value: unknown, max: number): string {\n let text: string\n try {\n text = JSON.stringify(value) ?? String(value)\n } catch {\n text = String(value)\n }\n return trimText(text, max)\n}\n\nfunction trimText(text: string, max: number): string {\n if (text.length <= max) return text\n return `${text.slice(0, max)}…`\n}\n\nfunction randomSuffix(len = 8): string {\n return Math.random()\n .toString(36)\n .slice(2, 2 + len)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tool-loop.js","names":[],"sources":["../src/tool-loop.ts"],"sourcesContent":["/**\n * Bounded turn-level tool-dispatch loop.\n *\n * `runAgentTaskStream` runs ONE model turn; `runAgentRounds` orchestrates DELEGATED\n * multi-agent topologies (refine / fanout-vote). Neither is the everyday\n * interactive shape: a chat turn where the model may emit tool calls, each is\n * executed, the results are folded back, and the turn re-runs until the model\n * stops (or a turn cap). Every agent app hand-rolls that loop — this is it,\n * as a reusable primitive.\n *\n * Substrate-neutral by design: the caller supplies `streamTurn` (wrapping\n * whatever backend / `runAgentTaskStream` it uses) and `executeToolCall`\n * (routing to its executors). This module owns the LOOP; the caller owns the\n * model and the executors. `Raw` (streaming variant) is the caller's own\n * event type. The only imported contract is the runtime hook type: hooks are\n * execution-scoped observers, not part of the agent profile.\n */\n\nimport type { RuntimeDecisionEvidenceRef, RuntimeHooks } from './runtime-hooks'\nimport { notifyRuntimeDecisionPoint, notifyRuntimeHookEvent } from './runtime-hooks'\n\nexport interface ToolLoopCall {\n toolCallId?: string\n toolName: string\n args: Record<string, unknown>\n /**\n * Provider-specific fields the model attached to this call, echoed back on\n * its assistant `tool_calls` entry in the next request. Gemini 3 returns\n * `extra_content: { google: { thought_signature } }` on each call and answers\n * HTTP 400 when the next request omits it.\n */\n providerFields?: Record<string, unknown>\n}\n\n/** Outcome of one tool dispatch — structurally compatible with a hub/integration\n * tool-outcome union, so callers can fold either through the loop. */\nexport type ToolCallOutcome =\n | { ok: true; result: unknown }\n | { ok: false; code: string; message: string; status?: number }\n\n/** Runaway-backstop: stops an infinite tool loop where cost is unmetered. Set\n * far above any legitimate workflow — this is a watchdog, not a policy cap.\n * Legitimate per-call budgets come from `maxCostUsd` + `costOf`. */\nconst RUNAWAY_BACKSTOP_TURNS = 200\nconst DEFAULT_DECISION_CONTEXT_CHARS = 12_000\nconst FAILURE_RECOVERY_ACTIONS = ['retry', 'verify', 'continue', 'stop']\n/** Consecutive identical calls (same tool + canonical-JSON args) that trigger\n * stuck-loop detection. The window resets on any different call. */\nconst STUCK_LOOP_THRESHOLD = 3\n\n/** One OpenAI-shaped tool-call entry carried on an assistant message, plus any\n * provider-specific fields the model attached to the call (`ToolLoopCall.providerFields`). */\nexport interface ToolLoopAssistantToolCall {\n [providerField: string]: unknown\n id: string\n type: 'function'\n function: { name: string; arguments: string }\n}\n\n/**\n * A message in the running conversation the loop sends to `streamTurn`.\n *\n * The base `{ role, content }` covers `system` / `user` / plain `assistant`\n * turns. Two optional fields carry the OpenAI function-calling contract so a\n * strict model (Claude, and any OpenAI-compatible provider that validates tool\n * history) reads its own tool use back instead of re-issuing the same call:\n *\n * - an assistant turn that emitted tool calls carries `tool_calls`, and its\n * `content` is `null` when the turn was tool-only;\n * - each tool result is its own `{ role: 'tool', tool_call_id, content }`\n * message keyed to the call that produced it.\n *\n * Widening is additive: a `streamTurn` that reads only `role` + `content` still\n * works; one that forwards the whole message to an OpenAI-compatible endpoint\n * now sends correct tool history.\n */\nexport type ToolLoopMessage = {\n role: string\n content: string | null\n tool_calls?: ToolLoopAssistantToolCall[]\n tool_call_id?: string\n}\n\n/** A tool-call id is required to key a `role: 'tool'` result back to its call.\n * When the model omitted one, derive a stable id from the tool name so the\n * assistant `tool_calls` entry and its `tool` result still match. */\nfunction toolCallId(call: ToolLoopCall): string {\n return call.toolCallId ?? `call_${call.toolName}`\n}\n\n/** The assistant turn that emitted `pending`, in OpenAI shape: text content\n * (null when the turn was tool-only) plus its `tool_calls` array. */\nfunction assistantToolCallMessage(turnText: string, pending: ToolLoopCall[]): ToolLoopMessage {\n return {\n role: 'assistant',\n content: turnText.trim() || null,\n tool_calls: pending.map((call) => ({\n ...call.providerFields,\n id: toolCallId(call),\n type: 'function',\n function: { name: call.toolName, arguments: JSON.stringify(call.args) },\n })),\n }\n}\n\n/** One `role: 'tool'` result message keyed to its call by `tool_call_id`. */\nfunction toolResultMessage(call: ToolLoopCall, content: string): ToolLoopMessage {\n return { role: 'tool', tool_call_id: toolCallId(call), content }\n}\n\n/**\n * One progress report from a running tool, shaped like an MCP `notifications/progress`\n * payload: a free-text `message`, a monotonic `progress` count with an optional\n * `total`, and any structured `data` the caller's UI understands.\n */\nexport interface ToolProgressUpdate {\n message?: string\n progress?: number\n total?: number\n data?: unknown\n}\n\n/** What the loop hands a running tool besides its call. */\nexport interface ToolCallContext {\n /**\n * Report progress while the call runs. The streaming loop yields each update as a\n * `tool_progress` step, in order, before the call's `tool_result`; updates sent after\n * the call settles are dropped.\n */\n progress: (update: ToolProgressUpdate) => void\n}\n\n/** Executes one tool call. Implementations that take only the call stay valid. */\nexport type ToolCallExecutor = (\n call: ToolLoopCall,\n context: ToolCallContext,\n) => Promise<ToolCallOutcome>\n\nfunction defaultRender(label: string, outcome: ToolCallOutcome): string {\n if (outcome.ok) return `- ${label} → ok: ${JSON.stringify(outcome.result)}`\n return `- ${label} → failed (${outcome.code}): ${outcome.message}`\n}\n\n// ── Awaitable variant (drain-only callers, tests) ──────────────────────────\n\nexport type ToolLoopEvent =\n | { type: 'text'; text: string }\n | { type: 'tool_call'; call: ToolLoopCall }\n | { type: 'other'; event: unknown }\n\n/** Why the loop stopped. `completed` = model finished naturally; `stuck-loop` =\n * ≥3 consecutive identical tool calls (same tool + args); `backstop` = hit the\n * runaway-backstop cap (200 by default); `deadline` = wall-clock deadlineMs\n * exceeded; `budget` = maxCostUsd exhausted. Non-`completed` stops are infra /\n * resource outcomes — eval scoring must distinguish them from capability failure. */\nexport type ToolLoopStopReason = 'completed' | 'stuck-loop' | 'backstop' | 'deadline' | 'budget'\n\nexport interface ToolLoopResult {\n finalText: string\n toolResults: Array<{ call: ToolLoopCall; label: string; outcome: ToolCallOutcome }>\n turns: number\n stopReason: ToolLoopStopReason\n /** @deprecated Use `stopReason !== 'completed'` instead. */\n cappedOut: boolean\n}\n\nexport interface RunToolLoopOptions {\n systemPrompt: string\n userMessage: string\n priorMessages?: ToolLoopMessage[]\n streamTurn: (messages: ToolLoopMessage[]) => AsyncIterable<ToolLoopEvent>\n executeToolCall: ToolCallExecutor\n /** Receives each progress update a running tool reports, in order. */\n onToolProgress?: (call: ToolLoopCall, update: ToolProgressUpdate) => void\n isExecutableTool: (toolName: string) => boolean\n /** Runaway-backstop cap. Default 200 — set far above any legitimate workflow.\n * For per-workflow limits, use `maxCostUsd` or `deadlineMs` instead. */\n maxToolTurns?: number\n /** Wall-clock deadline in ms since epoch (Date.now()-based). When exceeded the\n * loop stops with stopReason `deadline`. */\n deadlineMs?: number\n /** Maximum total cost in USD. Requires `costOf` to meter each tool call. */\n maxCostUsd?: number\n /** Return the USD cost of one outcome. Required for `maxCostUsd` to work. */\n costOf?: (call: ToolLoopCall, outcome: ToolCallOutcome) => number\n renderResult?: (label: string, outcome: ToolCallOutcome) => string\n labelFor?: (call: ToolLoopCall) => string\n runId?: string\n scenarioId?: string\n hooks?: RuntimeHooks\n}\n\n/** Run the bounded tool loop and return the final text + every executed tool\n * outcome. Awaitable — callers needing to stream events to a UI use\n * {@link streamToolLoop}. */\nexport async function runToolLoop(opts: RunToolLoopOptions): Promise<ToolLoopResult> {\n const backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: ToolLoopCall) => c.toolName)\n const runId = opts.runId ?? `agent-run-${randomSuffix()}`\n const messages: ToolLoopMessage[] = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n const observer = createToolLoopObserver(opts.hooks, runId, opts.scenarioId)\n const toolResults: ToolLoopResult['toolResults'] = []\n let finalText = ''\n let turns = 0\n let accumulatedCostUsd = 0\n // Stuck-loop detection: track the last canonical-JSON call signature and how\n // many consecutive times we've seen it.\n let lastCallHash: string | null = null\n let consecutiveCount = 0\n\n observer.loopBefore(backstop, messages.length)\n\n for (let toolTurn = 0; ; toolTurn++) {\n turns++\n\n // Wall-clock deadline check — before every new turn.\n if (opts.deadlineMs !== undefined && Date.now() >= opts.deadlineMs) {\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'deadline' })\n return { finalText, toolResults, turns, stopReason: 'deadline', cappedOut: true }\n }\n\n let turnText = ''\n const pending: ToolLoopCall[] = []\n const turnEventId = observer.turnBefore(toolTurn, messages.length)\n for await (const ev of opts.streamTurn([...messages])) {\n if (ev.type === 'text') {\n turnText += ev.text\n finalText += ev.text\n } else if (ev.type === 'tool_call' && opts.isExecutableTool(ev.call.toolName)) {\n pending.push(ev.call)\n }\n }\n if (pending.length === 0) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: 0,\n finalTextChars: finalText.length,\n })\n break\n }\n\n // Runaway backstop — the model keeps emitting calls past the safety ceiling.\n if (toolTurn >= backstop) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'backstop',\n })\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'backstop' })\n return { finalText, toolResults, turns, stopReason: 'backstop', cappedOut: true }\n }\n\n // The assistant turn that emitted the calls, carrying its tool_calls array,\n // so a strict model reads its own tool use back in OpenAI shape.\n messages.push(assistantToolCallMessage(turnText, pending))\n const outcomes: ExecutedToolCall[] = []\n for (const [callIndex, call] of pending.entries()) {\n // Stuck-loop detection: hash the first pending call each turn (a model\n // stuck in a loop re-issues the same call repeatedly, not alternating calls).\n const callHash = canonicalCallHash(call)\n if (callHash === lastCallHash) {\n consecutiveCount++\n } else {\n lastCallHash = callHash\n consecutiveCount = 1\n }\n if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'stuck-loop',\n })\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'stuck-loop' })\n return { finalText, toolResults, turns, stopReason: 'stuck-loop', cappedOut: true }\n }\n\n const callEventId = observer.toolCallBefore(toolTurn, turnEventId, callIndex, call)\n let settled = false\n const outcome = await executeGuarded(opts.executeToolCall, call, (update) => {\n if (!settled) opts.onToolProgress?.(call, update)\n })\n settled = true\n\n // Budget check after each tool call.\n if (opts.maxCostUsd !== undefined && opts.costOf !== undefined) {\n accumulatedCostUsd += opts.costOf(call, outcome)\n if (accumulatedCostUsd >= opts.maxCostUsd) {\n const label = labelFor(call)\n toolResults.push({ call, label, outcome })\n messages.push(toolResultMessage(call, render(label, outcome)))\n observer.toolCallAfter(toolTurn, callEventId, call, outcome)\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'budget',\n })\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'budget' })\n return { finalText, toolResults, turns, stopReason: 'budget', cappedOut: true }\n }\n }\n\n const label = labelFor(call)\n const rendered = render(label, outcome)\n toolResults.push({ call, label, outcome })\n outcomes.push({ call, label, outcome, rendered })\n // One role:'tool' message per result, keyed to its call by tool_call_id.\n messages.push(toolResultMessage(call, rendered))\n observer.toolCallAfter(toolTurn, callEventId, call, outcome)\n }\n observer.failureRecovery({\n toolTurn,\n messages,\n turnText,\n outcomes,\n })\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n toolResults: outcomes.map((item) => ({\n toolName: item.call.toolName,\n toolCallId: item.call.toolCallId,\n ok: item.outcome.ok,\n })),\n failedToolCalls: outcomes.filter((item) => !item.outcome.ok).length,\n })\n }\n observer.loopAfter({ turns, toolResults: toolResults.length, stopReason: 'completed' })\n return { finalText, toolResults, turns, stopReason: 'completed', cappedOut: false }\n}\n\n// ── Streaming variant (SSE chat runtimes + per-event telemetry) ────────────\n\nexport type StreamToolLoopYield<Raw> =\n | { kind: 'event'; event: Raw }\n | {\n kind: 'tool_progress'\n toolName: string\n toolCallId?: string\n update: ToolProgressUpdate\n }\n | {\n kind: 'tool_result'\n toolName: string\n toolCallId?: string\n label: string\n outcome: ToolCallOutcome\n }\n | {\n kind: 'capped'\n pending: number\n stopReason: Exclude<ToolLoopStopReason, 'completed'>\n /** On `deadline` only: the conversation after the system prompt, every tool call answered.\n * Passing it back as `priorMessages` with no `userMessage` continues the same turn. */\n messages?: ToolLoopMessage[]\n }\n\nexport interface StreamToolLoopOptions<Raw> {\n systemPrompt: string\n /** Omit to continue a checkpointed turn whose `priorMessages` already end with tool results. */\n userMessage?: string\n priorMessages?: ToolLoopMessage[]\n streamTurn: (messages: ToolLoopMessage[]) => AsyncIterable<Raw>\n extractText: (event: Raw) => string\n extractToolCall: (event: Raw) => ToolLoopCall | null\n isExecutableTool: (toolName: string) => boolean\n executeToolCall: ToolCallExecutor\n /** Runaway-backstop cap. Default 200 — set far above any legitimate workflow. */\n maxToolTurns?: number\n /** Wall-clock deadline in ms since epoch (Date.now()-based). */\n deadlineMs?: number\n /** Maximum total cost in USD. Requires `costOf` to meter each tool call. */\n maxCostUsd?: number\n /** Return the USD cost of one outcome. Required for `maxCostUsd` to work. */\n costOf?: (call: ToolLoopCall, outcome: ToolCallOutcome) => number\n renderResult?: (label: string, outcome: ToolCallOutcome) => string\n labelFor?: (call: ToolLoopCall) => string\n runId?: string\n scenarioId?: string\n hooks?: RuntimeHooks\n}\n\n/** Streaming bounded tool loop: yields each raw turn event (the caller maps +\n * telemetries + re-emits it), each `tool_progress` a running tool reports, and\n * each executed `tool_result`; emits one `capped` if it stops for any\n * non-completed reason with calls still pending. */\nexport async function* streamToolLoop<Raw>(\n opts: StreamToolLoopOptions<Raw>,\n): AsyncGenerator<StreamToolLoopYield<Raw>, void, unknown> {\n const backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: ToolLoopCall) => c.toolName)\n const runId = opts.runId ?? `agent-run-${randomSuffix()}`\n const messages: ToolLoopMessage[] = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n ...(opts.userMessage !== undefined\n ? [{ role: 'user' as const, content: opts.userMessage }]\n : []),\n ]\n const observer = createToolLoopObserver(opts.hooks, runId, opts.scenarioId)\n let accumulatedCostUsd = 0\n let lastCallHash: string | null = null\n let consecutiveCount = 0\n\n observer.loopBefore(backstop, messages.length)\n\n for (let toolTurn = 0; ; toolTurn++) {\n // Wall-clock deadline check before every new turn.\n if (opts.deadlineMs !== undefined && Date.now() >= opts.deadlineMs) {\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'deadline' })\n // A turn boundary: every call is answered, so the history is a resumable checkpoint.\n yield { kind: 'capped', pending: 0, stopReason: 'deadline', messages: messages.slice(1) }\n return\n }\n\n let turnText = ''\n const pending: ToolLoopCall[] = []\n const turnEventId = observer.turnBefore(toolTurn, messages.length)\n for await (const event of opts.streamTurn([...messages])) {\n yield { kind: 'event', event }\n turnText += opts.extractText(event)\n const call = opts.extractToolCall(event)\n if (call && opts.isExecutableTool(call.toolName)) pending.push(call)\n }\n if (pending.length === 0) {\n observer.turnAfter(toolTurn, turnEventId, { pendingToolCalls: 0 })\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'completed' })\n return\n }\n\n // Runaway backstop.\n if (toolTurn >= backstop) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'backstop',\n })\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'backstop' })\n yield { kind: 'capped', pending: pending.length, stopReason: 'backstop' }\n return\n }\n\n // The assistant turn that emitted the calls, carrying its tool_calls array,\n // so a strict model reads its own tool use back in OpenAI shape.\n messages.push(assistantToolCallMessage(turnText, pending))\n const outcomes: ExecutedToolCall[] = []\n for (const [callIndex, call] of pending.entries()) {\n // Stuck-loop detection.\n const callHash = canonicalCallHash(call)\n if (callHash === lastCallHash) {\n consecutiveCount++\n } else {\n lastCallHash = callHash\n consecutiveCount = 1\n }\n if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'stuck-loop',\n })\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'stuck-loop' })\n yield { kind: 'capped', pending: pending.length, stopReason: 'stuck-loop' }\n return\n }\n\n const callEventId = observer.toolCallBefore(toolTurn, turnEventId, callIndex, call)\n // Progress arrives while the call is in flight; yield it as it lands, in order,\n // and stop accepting it once the call settles so it never trails the result.\n const progress: ToolProgressUpdate[] = []\n let wake: (() => void) | undefined\n let settled = false\n const running = executeGuarded(opts.executeToolCall, call, (update) => {\n if (settled) return\n progress.push(update)\n wake?.()\n }).then((result) => {\n settled = true\n wake?.()\n return result\n })\n while (!settled || progress.length > 0) {\n const update = progress.shift()\n if (update !== undefined) {\n yield {\n kind: 'tool_progress',\n toolName: call.toolName,\n toolCallId: call.toolCallId,\n update,\n }\n continue\n }\n await new Promise<void>((resolve) => {\n wake = resolve\n })\n wake = undefined\n }\n const outcome = await running\n\n // Budget check after each tool call.\n if (opts.maxCostUsd !== undefined && opts.costOf !== undefined) {\n accumulatedCostUsd += opts.costOf(call, outcome)\n if (accumulatedCostUsd >= opts.maxCostUsd) {\n const label = labelFor(call)\n yield {\n kind: 'tool_result',\n toolName: call.toolName,\n toolCallId: call.toolCallId,\n label,\n outcome,\n }\n messages.push(toolResultMessage(call, render(label, outcome)))\n observer.toolCallAfter(toolTurn, callEventId, call, outcome)\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n stopReason: 'budget',\n })\n observer.loopAfter({ turns: toolTurn + 1, stopReason: 'budget' })\n yield { kind: 'capped', pending: pending.length, stopReason: 'budget' }\n return\n }\n }\n\n const label = labelFor(call)\n yield {\n kind: 'tool_result',\n toolName: call.toolName,\n toolCallId: call.toolCallId,\n label,\n outcome,\n }\n const rendered = render(label, outcome)\n outcomes.push({ call, label, outcome, rendered })\n // One role:'tool' message per result, keyed to its call by tool_call_id.\n messages.push(toolResultMessage(call, rendered))\n observer.toolCallAfter(toolTurn, callEventId, call, outcome)\n }\n observer.failureRecovery({\n toolTurn,\n messages,\n turnText,\n outcomes,\n })\n observer.turnAfter(toolTurn, turnEventId, {\n pendingToolCalls: pending.length,\n toolResults: outcomes.map((item) => ({\n toolName: item.call.toolName,\n toolCallId: item.call.toolCallId,\n ok: item.outcome.ok,\n })),\n failedToolCalls: outcomes.filter((item) => !item.outcome.ok).length,\n })\n }\n}\n\n/** Run one call; a thrown executor becomes an `executor_error` outcome. */\nasync function executeGuarded(\n execute: ToolCallExecutor,\n call: ToolLoopCall,\n progress: (update: ToolProgressUpdate) => void,\n): Promise<ToolCallOutcome> {\n try {\n return await execute(call, { progress })\n } catch (err) {\n return {\n ok: false,\n code: 'executor_error',\n message: err instanceof Error ? err.message : String(err),\n }\n }\n}\n\ninterface ExecutedToolCall {\n call: ToolLoopCall\n label: string\n outcome: ToolCallOutcome\n rendered: string\n}\n\ninterface NotifyToolFailureRecoveryOptions {\n hooks?: RuntimeHooks\n runId: string\n scenarioId?: string\n stepIndex: number\n messages: ToolLoopMessage[]\n turnText: string\n outcomes: ExecutedToolCall[]\n}\n\ninterface NotifyToolLoopEventOptions {\n hooks?: RuntimeHooks\n runId: string\n scenarioId?: string\n target: 'agent.run' | 'agent.turn' | 'agent.tool_call'\n phase: 'before' | 'after' | 'error' | 'event'\n id?: string\n stepIndex?: number\n parentId?: string\n payload?: Record<string, unknown>\n metadata?: Record<string, unknown>\n}\n\ninterface ToolLoopObserver {\n loopBefore(maxToolTurns: number, messageCount: number): void\n loopAfter(payload: Record<string, unknown>): void\n turnBefore(toolTurn: number, messageCount: number): string\n turnAfter(toolTurn: number, turnEventId: string, payload: Record<string, unknown>): void\n toolCallBefore(\n toolTurn: number,\n turnEventId: string,\n callIndex: number,\n call: ToolLoopCall,\n ): string\n toolCallAfter(\n toolTurn: number,\n callEventId: string,\n call: ToolLoopCall,\n outcome: ToolCallOutcome,\n ): void\n failureRecovery(options: {\n toolTurn: number\n messages: ToolLoopMessage[]\n turnText: string\n outcomes: ExecutedToolCall[]\n }): void\n}\n\nfunction createToolLoopObserver(\n hooks: RuntimeHooks | undefined,\n runId: string,\n scenarioId: string | undefined,\n): ToolLoopObserver {\n const loopEventId = `${runId}:agent.run`\n return {\n loopBefore: (maxToolTurns, messageCount) => {\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.run',\n phase: 'before',\n id: `${loopEventId}:before`,\n payload: { maxToolTurns, messageCount },\n })\n },\n loopAfter: (payload) => {\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.run',\n phase: 'after',\n id: `${loopEventId}:after`,\n payload,\n })\n },\n turnBefore: (toolTurn, messageCount) => {\n const turnEventId = `${loopEventId}:${toolTurn}`\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.turn',\n phase: 'before',\n id: turnEventId,\n stepIndex: toolTurn,\n parentId: loopEventId,\n payload: { messageCount },\n })\n return turnEventId\n },\n turnAfter: (toolTurn, turnEventId, payload) => {\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.turn',\n phase: 'after',\n id: `${turnEventId}:after`,\n stepIndex: toolTurn,\n parentId: turnEventId,\n payload,\n })\n },\n toolCallBefore: (toolTurn, turnEventId, callIndex, call) => {\n const callEventId = `${turnEventId}:tool-call:${callIndex}`\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.tool_call',\n phase: 'before',\n id: callEventId,\n stepIndex: toolTurn,\n parentId: turnEventId,\n payload: toolCallPayload(call),\n })\n return callEventId\n },\n toolCallAfter: (toolTurn, callEventId, call, outcome) => {\n notifyToolLoopEvent({\n hooks,\n runId,\n scenarioId,\n target: 'agent.tool_call',\n phase: 'after',\n id: `${callEventId}:after`,\n stepIndex: toolTurn,\n parentId: callEventId,\n payload: { ...toolCallPayload(call), outcome: outcomePayload(outcome) },\n })\n },\n failureRecovery: (options) => {\n notifyToolFailureRecovery({\n hooks,\n runId,\n scenarioId,\n stepIndex: options.toolTurn,\n messages: options.messages,\n turnText: options.turnText,\n outcomes: options.outcomes,\n })\n },\n }\n}\n\nfunction notifyToolLoopEvent(options: NotifyToolLoopEventOptions): void {\n notifyRuntimeHookEvent(options.hooks, {\n id: options.id ?? `${options.runId}:${options.target}:${options.phase}`,\n runId: options.runId,\n scenarioId: options.scenarioId,\n target: options.target,\n phase: options.phase,\n timestamp: Date.now(),\n stepIndex: options.stepIndex,\n parentId: options.parentId,\n payload: options.payload,\n metadata: { producer: 'tool-loop', ...options.metadata },\n })\n}\n\nfunction notifyToolFailureRecovery(options: NotifyToolFailureRecoveryOptions): void {\n const failed = options.outcomes.filter((item) => !item.outcome.ok)\n if (failed.length === 0) return\n\n const evidence: RuntimeDecisionEvidenceRef[] = []\n for (const item of failed) {\n const id = item.call.toolCallId ?? `${options.stepIndex}:${item.label}`\n evidence.push({\n source: 'tool_call',\n id,\n detail: `${item.call.toolName} ${stringifySafe(item.call.args, 2_000)}`,\n metadata: { toolName: item.call.toolName, label: item.label },\n })\n evidence.push({\n source: 'tool_result',\n id: `${id}:result`,\n detail: item.rendered,\n metadata: failureMetadata(item.outcome),\n })\n }\n\n notifyRuntimeDecisionPoint(options.hooks, {\n id: `${options.runId}:agent.turn:${options.stepIndex}:failure-recovery`,\n runId: options.runId,\n scenarioId: options.scenarioId,\n stepIndex: options.stepIndex,\n kind: 'retry',\n candidateActions: [...FAILURE_RECOVERY_ACTIONS],\n context: renderDecisionContext(options.messages, options.turnText, options.outcomes),\n evidence,\n metadata: {\n target: 'failure-recovery',\n source: 'agent.turn',\n failedToolCount: failed.length,\n toolNames: failed.map((item) => item.call.toolName),\n },\n })\n}\n\nfunction toolCallPayload(call: ToolLoopCall): Record<string, unknown> {\n return {\n toolName: call.toolName,\n toolCallId: call.toolCallId,\n argsPreview: stringifySafe(call.args, 2_000),\n }\n}\n\nfunction outcomePayload(outcome: ToolCallOutcome): Record<string, unknown> {\n if (!outcome.ok) {\n return {\n ok: false,\n code: outcome.code,\n message: trimText(outcome.message, 2_000),\n status: outcome.status,\n }\n }\n return {\n ok: true,\n resultPreview: stringifySafe(outcome.result, 2_000),\n }\n}\n\nfunction failureMetadata(outcome: ToolCallOutcome): Record<string, unknown> | undefined {\n if (outcome.ok) return undefined\n return {\n code: outcome.code,\n message: outcome.message,\n status: outcome.status,\n }\n}\n\nfunction renderDecisionContext(\n messages: ToolLoopMessage[],\n turnText: string,\n outcomes: ExecutedToolCall[],\n): string {\n const recent = messages.slice(-6).map((message) => `[${message.role}]\\n${message.content ?? ''}`)\n const assistant = turnText.trim() ? [`[assistant]\\n${turnText}`] : []\n const toolResults = [`[tool results]\\n${outcomes.map((item) => item.rendered).join('\\n')}`]\n return trimText(\n [...recent, ...assistant, ...toolResults].join('\\n\\n'),\n DEFAULT_DECISION_CONTEXT_CHARS,\n )\n}\n\n/** Canonical identifier for a tool call used by stuck-loop detection.\n * Keys are sorted so `{b:1,a:2}` and `{a:2,b:1}` produce the same hash. */\nfunction canonicalCallHash(call: ToolLoopCall): string {\n const sortedArgs = Object.fromEntries(\n Object.entries(call.args).sort(([a], [b]) => a.localeCompare(b)),\n )\n return `${call.toolName}:${JSON.stringify(sortedArgs)}`\n}\n\nfunction stringifySafe(value: unknown, max: number): string {\n let text: string\n try {\n text = JSON.stringify(value) ?? String(value)\n } catch {\n text = String(value)\n }\n return trimText(text, max)\n}\n\nfunction trimText(text: string, max: number): string {\n if (text.length <= max) return text\n return `${text.slice(0, max)}…`\n}\n\nfunction randomSuffix(len = 8): string {\n return Math.random()\n .toString(36)\n .slice(2, 2 + len)\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { _t as SupervisorCleanupReceipt, gt as ProvisionedSupervisor, ht as ProvisionSupervisorRequest, mt as ProvisionSupervisorConnection, vt as provisionSupervisor } from "../index-CUh40yUJ.js";
1
+ import { _t as SupervisorCleanupReceipt, gt as ProvisionedSupervisor, ht as ProvisionSupervisorRequest, mt as ProvisionSupervisorConnection, vt as provisionSupervisor } from "../index-CHlWvoo5.js";
2
2
  //#region src/tui/top-app.d.ts
3
3
  /**
4
4
  * The interactive side of the supervisor-run TUI: a keypress/mouse loop over the frames
package/dist/tui/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as provisionSupervisor } from "../provision-supervisor-BQLLrUtn.js";
1
+ import { t as provisionSupervisor } from "../provision-supervisor-CunKnayY.js";
2
2
  import { a as renderTopFrameWithLayout, i as renderTopFrame, n as runTopApp, r as loadTopSnapshot, t as renderTopOnce } from "../top-app-DaiR4LXM.js";
3
3
  export { loadTopSnapshot, provisionSupervisor, renderTopFrame, renderTopFrameWithLayout, renderTopOnce, runTopApp };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-runtime",
3
- "version": "0.316.0",
3
+ "version": "0.318.0",
4
4
  "description": "Shared task-lifecycle skeleton for agents: a recursive loop kernel for chat turns, one-shot tasks, and multi-attempt loops, with trace capture and eval-gated self-improvement. Domain behavior lives in adapters; scoring and ship-gates in @tangle-network/agent-eval.",
5
5
  "homepage": "https://github.com/tangle-network/agent-runtime#readme",
6
6
  "repository": {
@@ -133,7 +133,7 @@
133
133
  "yaml": "2.9.0"
134
134
  },
135
135
  "engines": {
136
- "node": ">=22.13.0"
136
+ "node": ">=22.19.0"
137
137
  },
138
138
  "license": "MIT",
139
139
  "peerDependencies": {
@@ -142,6 +142,7 @@
142
142
  "@tangle-network/sandbox": ">=0.58.4 <0.61.0"
143
143
  },
144
144
  "dependencies": {
145
+ "@earendil-works/pi-codemode": "1.0.4",
145
146
  "@tangle-network/agent-core": ">=0.10.3 <0.11.0",
146
147
  "@tangle-network/agent-knowledge": "^20.0.1",
147
148
  "@tangle-network/agent-profile-materialize": ">=0.21.0 <0.23.0",
@@ -10,6 +10,13 @@ Each node holds an id and a canonical AgentProfile; its profile name matches the
10
10
  Standing roles belong in the profile prompt, with explicit tools and resources.
11
11
  Put the concrete mission in the deliverable description and supply an independent completion check that rejects incomplete output.
12
12
 
13
+ A node whose work is mechanical composition (fan-out, filtering, pagination, cross-tool joins)
14
+ can be a CODE node: `model.metadata.codeMode` on its profile presents `tool_search` + `codemode`
15
+ instead of the raw tools, so the model authors one program per assignment instead of one tool
16
+ call per turn. The grant (`profile.tools`) is unchanged — every nested call still crosses the
17
+ authorized handler, and spend still settles on the node. Reserve code nodes for judgment-free
18
+ stretches; anything that must re-plan on an observed result belongs in an ordinary agent node.
19
+
13
20
  ## Edges
14
21
 
15
22
  Delegation edges carry work from the root to a worker through registered prompt references.
@@ -21,11 +21,13 @@ A batch does not expand the permission of its individual operations.
21
21
  Meter paid operations on the owning execution path and preserve their usage records.
22
22
  Keep dependent actions sequential unless their contract supports safe composition.
23
23
 
24
- ## Runtime-supervised code
24
+ ## Runtime scripts and agents
25
25
 
26
- When configuring code execution for a Runtime supervisor, read [the execution boundary](references/runtime-execution.md).
27
- That branch supplies a generated API over Runtime's existing coordination tools and requires an explicit runner.
28
- For ordinary shell or session-tool batching, no additional runtime is needed.
26
+ Use Runtime's maintained code-mode execution rather than creating a script runner or an agent loop.
27
+ Read [the Runtime execution surface](references/runtime-execution.md) for `runCodeMode`, the
28
+ existing router-tools executor's `codeMode` option, structured results and retained state.
29
+ The implementation is independent of Pi's coding harness and uses the standalone QuickJS engine.
30
+ For a harness already offering native code mode, use that surface instead of nesting code mode.
29
31
 
30
32
  Complete the requested work and inspect the resulting artifact.
31
33
  Code reduces mechanical round trips; it does not replace judgment or prove the quality of the result.
@@ -1,14 +1,59 @@
1
- # Runtime code execution boundary
1
+ # Runtime code mode
2
2
 
3
- Read [code-mode implementation and types](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/code-mode.ts) and its [contract tests](https://github.com/tangle-network/agent-runtime/blob/main/tests/kernel/code-mode.test.ts).
4
- Use `codeModeSupervisorTools(runner)` with an explicit `CodeModeRunner`; there is no default runner.
5
- For untrusted model output, supply a real isolated execution environment.
6
- The in-process runner and source lint are not security boundaries.
3
+ Use `runCodeMode` from `@tangle-network/agent-runtime/kernel` for an authored Runtime script.
4
+ Pass the already-authorized tool descriptors and their original execution handlers. Scripts
5
+ call `tools.name(args)`, compose calls with normal JavaScript, and return a small result.
6
+ `tool_search` generates input and output declarations when using `createCodeModeTools`.
7
7
 
8
- The generated API follows the live coordination grant.
9
- Code can spawn or steer through Runtime-provided bindings such as `api.spawn_worker`; these retain authorization, shared budgets, and journal records.
10
- Direct coordination requests over HTTP or a second scheduler bypass that contract.
8
+ For a model-driven Runtime agent, select `codeMode: true` (or explicit execution limits)
9
+ on the existing `createExecutor({ backend: 'router-tools', ... })` configuration. Keep
10
+ `AgentProfile.tools` as the actual capability grant. Runtime presents `tool_search` and
11
+ `codemode`; nested calls still go through the original `executeToolCall`, with a cancellation
12
+ signal, parent call ID and nested call ID. The original tool-step observer records them.
13
+ The existing internal brain loop and local MCP client accept the same presentation option.
14
+ This does not require the Pi coding harness.
11
15
 
12
- Lifecycle decisions remain model tools: `submit_result`, `stop`, and `ask_parent` are outside the generated code API.
13
- Keep judgment in the model and mechanics in the program.
14
- Before broad use, exercise one allowed call, one denied call, a failed operation, and cancellation through the selected runner.
16
+ For ONE node of a graph — a code node beside ordinary agent nodes — declare it on the
17
+ node's pinned profile instead of the environment: `model.metadata.codeMode = true` (or
18
+ `{ concurrency, maxCalls, timeoutMs, memoryLimitBytes, directTools }`). The profile
19
+ declaration overrides the backend default for that node alone, and every graph node is
20
+ its pinned profile, so this is the whole authoring surface: no seam change, no new node
21
+ kind. Validation is fail-loud (unknown or host-owned fields are refused before any request).
22
+
23
+ The implementation uses the standalone `@earendil-works/pi-codemode` QuickJS/WASM worker.
24
+ There is no unsafe Node VM fallback. Code cannot read host files, processes or network
25
+ except through explicitly supplied tools. The default VM memory cap is 64 MiB, wall deadline
26
+ five minutes, tool concurrency four (excess calls queue), and total call cap 1024. Exceeding the call cap aborts the program, even if it catches
27
+ tool errors; failed programs do not commit store writes. Limits
28
+ are caller-authored; finite wall deadlines must fit the worker timer (at most 2,147,483,647 ms).
29
+ `timeoutMs: Infinity` explicitly removes the wall deadline.
30
+
31
+ Return structured objects from tool handlers when programs need structured data. Existing
32
+ text handlers remain text: Runtime does not guess that arbitrary strings should be parsed as
33
+ JSON. Tool input validation, approvals, external-effect idempotency and metering remain the
34
+ original handler's responsibility. Code mode does not grant access or automatically retry effects.
35
+
36
+ Use `text()` for text output and `image()` with actual inline image data for image output.
37
+ Only selected output and the return value enter the next model turn; intermediate results stay
38
+ in the script, while the existing execution trace retains calls. `store`/`load` hold small JSON
39
+ state. Retain `codeModeStore` through the existing run/session owner and provide it as
40
+ `codeMode.store` on continuation. The router executor's `onMessages` callback receives the
41
+ store snapshot as its optional second argument. Only successful programs commit store writes.
42
+
43
+ A fresh VM runs each program. A stored JSON snapshot is not a checkpoint of the instruction
44
+ pointer or guest heap. A lost result does not prove an external operation did not run; inspect
45
+ its existing effect receipt rather than blindly replaying the script. Cancellation stops new
46
+ calls and closes program authority; it cannot undo a previously accepted external effect.
47
+
48
+ A harness that already supplies code mode should use its native surface, not wrap it in another
49
+ code-mode tool. `createCodeModeTools` refuses reserved-tool collisions instead of nesting engines.
50
+ Use `directTools` for capabilities the host keeps as direct-only model tools (for example, a
51
+ lifecycle decision). They are not available inside programs.
52
+
53
+ Tool names must have distinct normalized JavaScript identifiers. Runtime refuses collisions
54
+ (such as `send-email` and `send_email`) before running a program, because the engine otherwise
55
+ selects the first handler even for an exact-name access. Rename the host descriptors explicitly.
56
+
57
+ Run Node host applications from a file. The current engine inherits Node worker flags;
58
+ `node --input-type=module --eval ...` is unsupported because Node rejects that flag on
59
+ file-backed workers. This returns a sandbox error and never falls back to host execution.
@@ -13,7 +13,7 @@ Do not constrain agents with prohibitions: change what is rewarded and who is in
13
13
  ## A profile that starts
14
14
 
15
15
  A refused profile fails before its first model turn, and it fails the same way on every retry.
16
- On 2026-10-10 Discovery plays lost 20 driver attempts to child profiles naming a tool the harness does not publish, and 3 spawns to a missing subscription source.
16
+ On 2026-10-10 Discovery lost 20 driver attempts to unpublished tool names and 3 spawns to a missing subscription source.
17
17
  Start from a profile that already ran (in a Discovery play, `inputs/profile-examples/<family>.json`) and change only `name`, `description` and `prompt.instructions`.
18
18
 
19
19
  | Field | Rule |
@@ -25,7 +25,7 @@ Start from a profile that already ran (in a Discovery play, `inputs/profile-exam
25
25
  | `prompt` | The role in `instructions[0]`, the procedure after it. No `systemPrompt`: it replaces the harness's own guidance (17,730 characters on Codex), and Discovery refuses one under 4,000 characters. No `appendSystemPrompt` on Codex or Gemini. |
26
26
  | `resources` | `failOnError: true`. Hand each file by path, `{"path": "<mount>", "resource": {"kind": "inline", "name": "<name>", "path": "<file in your workspace>"}}`: Runtime reads the bytes (UTF-8, at most 4 MiB). Never retype a file into `content`: a manager that did delivered 5 wrong characters in 24,008. |
27
27
 
28
- Native tool ids (agent-dev-container `packages/agent-profile-materialize/src/control-maps.ts` and `execution-controls.ts`; matching ignores case and every character but letters, digits and `_`):
28
+ Native tool ids (agent-dev-container `packages/agent-profile-materialize/src/control-maps.ts` and `execution-controls.ts`; matching ignores case and all but letters, digits and `_`):
29
29
 
30
30
  | Harness | Accepted | Refused |
31
31
  |---|---|---|
@@ -46,7 +46,7 @@ Runtime keys (agent-runtime `src/runtime/supervise/provider-visible-profile.ts`;
46
46
 
47
47
  Leaf or manager is decided by the keys you write (`withInheritedSpawnRights`, `src/runtime/supervise/supervise.ts`).
48
48
  A child whose profile names any `agent_runtime_coordination_` key gets exactly those keys.
49
- A child that names none, under a parent that can spawn, inherits the parent's coordination verbs, `spawn_worker` included, plus `submit_result`; it does not inherit product verbs, so it cannot write Knowledge pages.
49
+ A child that names none, under a parent that can spawn, inherits the parent's coordination verbs (`spawn_worker` included) and `submit_result`, but no product verbs, so it cannot write Knowledge pages.
50
50
  So always write them. A leaf gets the product verbs it uses, `submit_result` and `report_blocked`.
51
51
  A manager adds `agent_runtime_coordination_spawn_worker: true` and only the observation and steering verbs it will use, and carries this skill byte for byte: `"skills": [{"kind": "inline", "name": "profile-authoring", "path": "<this file in your workspace>"}]` beside `resources.failOnError: true`.
52
52
 
@@ -73,7 +73,7 @@ The spawn's assignment carries the work; the profile carries the role.
73
73
  Every assignment names four things: the exact page path and format to record, with one literal example line; the command that checks the part; what to do when blocked; and when to check in.
74
74
  Hand every file the child judges by path, never a summary.
75
75
  A spawn's `deadlineMs` is a check-in, not a kill: the child is told to record its state and submit what it has, and is stopped only if it then makes no progress. Set it from the work.
76
- A manager waits by ending its turn: Runtime wakes it with each settlement, finding or message, and with a notice before its deadline. A shell timer or polling loop is not re-entry, and it spends turns. Ending a turn with work open is not completion; a manager with a check ends on an accepted `submit_result` or a bound.
76
+ A manager waits by ending its turn: Runtime wakes it with each settlement, finding or message, and with a notice before its deadline. Polling or a shell timer is not re-entry and spends turns. Ending a turn with work open is not completion; a manager with a check ends on an accepted `submit_result` or a bound.
77
77
  A nested manager that returns early can end its scope and cancel live descendants.
78
78
  Require each child to record partial work where its parent can recover it after an interruption.
79
79
  Before retrying a failed child, read what it recorded; finished work may be usable. A retry names the specific failure in a new profile.
@@ -89,43 +89,45 @@ Make a literature search part of acceptance, and record an answer already publis
89
89
  ## 2. Probe the proxy's gap before launch
90
90
 
91
91
  The proxy is the check you can run during the work: an exact checker, a test suite, a rater or a demo. The gap is everything it accepts and the audience rejects; hacks live there, and a stronger model does not close it.
92
- Try to make the proxy accept what the audience rejects: plant the source's trivial and excluded cases, a published answer, a degenerate artifact, an impossible variant, a stale comparator. Each must be refused or classed as known; if one passes, fix the proxy or narrow the claim.
93
- Probe the other side too: write one correct result in a format the brief allows but the proxy's author did not expect, and run the proxy on it. A proxy that refuses it trains agents on format, not substance.
92
+ Try to make the proxy accept what the audience rejects: plant trivial and excluded cases, a published answer, a degenerate artifact, an impossible variant, a stale comparator. Each must be refused or classed as known; if one passes, fix the proxy or narrow the claim.
93
+ Probe the other side too: run the proxy on one correct result in a format the brief allows but its author did not expect. A proxy that refuses it trains agents on format, not substance.
94
94
  Use exact or interval arithmetic, and count only what is proven.
95
95
  An LLM judge becomes a gate only after it beats an always-reject baseline on cases a human graded.
96
96
 
97
97
  ## 3. Keep acceptance out of the reward loop
98
98
 
99
99
  Give agents a development check they can run; hiding every check also blocks honest work.
100
- Hold back a stricter acceptance check, run from the delivered packet in a fresh environment by a party outside the authoring lineage, and grade only what its rerun reproduces.
100
+ Hold back a stricter acceptance check, run from the delivered packet in a fresh environment by a party outside the authoring lineage; grade only what its rerun reproduces.
101
+ Record any root-authored boundary or comparison claim in Knowledge before a child sees it, so the blind check compares the same statement.
101
102
  A parent that promotes children on proxy passes alone selects for the gap; have the referee attack a result before anyone builds on it.
102
103
  Keep the headline gate fixed for the whole task; report a stricter bar as a second line.
103
- For a customer commission the bar is the first deliverable: the root's first milestone writes 50 to 100 checkable expectations, the deliverable spec and a workplan, and a partner outside the authoring lineage raises the bar against named exemplars and holds part of it back.
104
+ For a customer commission the bar is the first deliverable: the root's first milestone writes 50 to 100 checkable expectations, the deliverable spec and a workplan; a partner outside the authoring lineage raises the bar against named exemplars and holds part back.
104
105
 
105
106
  ## 4. Choose roles and a model for each
106
107
 
107
108
  Proposers: cheap, high-throughput models where an exact checker exists, with an occasional strong model.
108
109
  Referee: a skeptic from another model family, holding the artifact and its own instrument (exact recomputation, execution, a literature search, an attempted counter-case). It can reject a claim; only the held-back check accepts one. A change of family alone is weak.
109
110
  Monitor: reads the journal and live notes, flags drift or proxy gaming, and steers or reports; it never scores.
110
- Choose each model from measured cost and quality on this kind of work, state its dollars, and admit it in `allowedModels`; Runtime refuses a model outside that list at spawn.
111
+ Choose each model from measured cost and quality on this kind of work, state its dollars, and admit it in `allowedModels`; Runtime refuses any other at spawn.
111
112
  Spawn a helper only for work that is independent, long enough to repay a cold start, and checkable on its own.
112
113
 
113
114
  ## 5. Define success, the honest null, and the stop
114
115
 
115
116
  Success is an accepted result or a reproduced null. Separate execution settlement, development progress and final acceptance; an unknown outside verdict is not absent progress.
116
- A null from a search counts only with a coverage certificate: the searched space as data (ranges, method, seeds, bound reached), one row per cell, a calibration cell where the search must find objects that exist, and the sha256 of the reproducer.
117
+ A null from a search counts only with a coverage certificate: the searched space as data (ranges, method, seeds, bound reached), one row per cell, a calibration cell with objects the search must find, and the reproducer's sha256.
117
118
  The reproducer belongs to the check: registered before launch with its hash, the statements it covers and its own calibration; agents may run it and never write it.
118
- The outside check reruns its own copy, reruns the calibration first and credits nothing on a miss, then reruns cells until its budget ends; one mismatched row refuses the certificate, and only rerun, matched cells earn credit.
119
+ The outside check reruns its own copy: the calibration first, crediting nothing on a miss, then cells until its budget ends; one mismatched row refuses the certificate, and only rerun, matched cells earn credit.
119
120
  A null that is no search (a spec conflict, a gap list) counts when the held-back check reproduces each named item. A null without a certificate is "stopped, unverified"; one inside a range already covered adds nothing.
120
121
  Tell agents the checker may be wrong; credit a reproduced checker-defect report like a result.
121
- Grant `submit_result` only where Runtime gives the node a check; otherwise its settlement stays unassessed. Match completion instructions to the tools actually served to each node.
122
+ Grant `submit_result` only where Runtime gives the node a check; otherwise its settlement stays unassessed, and a child returning no structured validity verdict stays unresolved in the parent's comparison.
123
+ Match completion instructions to the tools served to each node. List every harness tool a child needs in `tools`, defaults included: a harness honoring a map of coordination tools alone serves nothing else. A revision states why it drops any tool its source profile listed.
122
124
  State the stop: what the agent does the moment its check passes, and that work after a pass spends seats and adds nothing.
123
125
  For continued research, read the installed continuation contract first and choose `maxBarren` from the measured feedback cadence.
124
126
 
125
127
  ## 6. Carry known traps
126
128
 
127
129
  Write each trap as what the held-back acceptance refuses, never as what the proxy passes: "the outside check does not count a c = 0 instance" is a trap; "the checker passed c = 0 claims" is a recipe. Give the rule, not the incident: the operator owns the stack.
128
- Typical traps: excluded or trivial cases, stale or wrong-field comparators, published answers, tests versus users, a demo versus a customer, a rater versus an expert.
130
+ Typical traps beyond the gap probe's canaries: wrong-field comparators, tests versus users, a demo versus a customer, a rater versus an expert.
129
131
  Record each new trap where the next author reads it, in the same change as its fix.
130
132
 
131
133
  ## 7. Set budget and caps
@@ -170,5 +172,5 @@ Records that expose a hack, and unavailable observations:
170
172
  ## Then consider
171
173
 
172
174
  - `supervise` to drive the tree once its profiles exist.
173
- - `eval-engineering` (its pre-spend calibration) when a judge or checker has no calibration yet.
174
- - `pursue` (its controlled architecture comparison) when the claim compares arms.
175
+ - `eval-engineering` when a judge or checker has no calibration yet.
176
+ - `pursue` when the claim compares arms.