@graphorin/agent 0.5.0 → 0.6.1

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 (65) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +41 -21
  3. package/dist/errors/index.d.ts +1 -1
  4. package/dist/errors/index.js +1 -1
  5. package/dist/errors/index.js.map +1 -1
  6. package/dist/evaluator-optimizer/index.d.ts +1 -1
  7. package/dist/evaluator-optimizer/index.js.map +1 -1
  8. package/dist/factory.d.ts.map +1 -1
  9. package/dist/factory.js +517 -108
  10. package/dist/factory.js.map +1 -1
  11. package/dist/fallback/index.d.ts +1 -1
  12. package/dist/fallback/index.js.map +1 -1
  13. package/dist/fanout/index.d.ts +8 -8
  14. package/dist/fanout/index.js +3 -3
  15. package/dist/fanout/index.js.map +1 -1
  16. package/dist/filters/index.d.ts +1 -1
  17. package/dist/filters/index.js +2 -2
  18. package/dist/filters/index.js.map +1 -1
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +6 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/internal/ids.js +1 -1
  24. package/dist/internal/ids.js.map +1 -1
  25. package/dist/internal/usage-accumulator.js +17 -2
  26. package/dist/internal/usage-accumulator.js.map +1 -1
  27. package/dist/lateral-leak/causality-monitor.d.ts +4 -4
  28. package/dist/lateral-leak/causality-monitor.js +3 -3
  29. package/dist/lateral-leak/causality-monitor.js.map +1 -1
  30. package/dist/lateral-leak/merge-guard.d.ts +2 -2
  31. package/dist/lateral-leak/merge-guard.js +1 -1
  32. package/dist/lateral-leak/merge-guard.js.map +1 -1
  33. package/dist/lateral-leak/protocol-guard.d.ts +4 -4
  34. package/dist/lateral-leak/protocol-guard.js +4 -4
  35. package/dist/lateral-leak/protocol-guard.js.map +1 -1
  36. package/dist/package.js +6 -0
  37. package/dist/package.js.map +1 -0
  38. package/dist/preferred-model/index.d.ts +2 -2
  39. package/dist/preferred-model/index.js +2 -2
  40. package/dist/preferred-model/index.js.map +1 -1
  41. package/dist/progress/index.js +2 -2
  42. package/dist/progress/index.js.map +1 -1
  43. package/dist/run-state/index.d.ts +7 -5
  44. package/dist/run-state/index.d.ts.map +1 -1
  45. package/dist/run-state/index.js +33 -4
  46. package/dist/run-state/index.js.map +1 -1
  47. package/dist/testing/replay-provider.d.ts +18 -0
  48. package/dist/testing/replay-provider.d.ts.map +1 -0
  49. package/dist/testing/replay-provider.js +100 -0
  50. package/dist/testing/replay-provider.js.map +1 -0
  51. package/dist/tooling/adapters.js +6 -6
  52. package/dist/tooling/adapters.js.map +1 -1
  53. package/dist/tooling/catalogue.js +1 -1
  54. package/dist/tooling/catalogue.js.map +1 -1
  55. package/dist/tooling/dataflow.js +19 -3
  56. package/dist/tooling/dataflow.js.map +1 -1
  57. package/dist/tooling/plan.js +92 -0
  58. package/dist/tooling/plan.js.map +1 -0
  59. package/dist/tooling/policy.js +45 -0
  60. package/dist/tooling/policy.js.map +1 -0
  61. package/dist/tooling/registry-build.js +1 -1
  62. package/dist/tooling/registry-build.js.map +1 -1
  63. package/dist/types.d.ts +189 -18
  64. package/dist/types.d.ts.map +1 -1
  65. package/package.json +11 -8
package/dist/factory.js CHANGED
@@ -11,6 +11,8 @@ import { addModelUsage, createInitialRunState, serializeRunState } from "./run-s
11
11
  import { buildMemoryGuard, buildSecretResolver, buildToolTokenCounter, createExecutorEventBridge, createMemoryRegionReader } from "./tooling/adapters.js";
12
12
  import { orderPromotedTools } from "./tooling/catalogue.js";
13
13
  import { buildDataFlowGuard } from "./tooling/dataflow.js";
14
+ import { PLAN_TOOL_NAME, createPlanTool, renderPlanRecitation } from "./tooling/plan.js";
15
+ import { buildToolArgumentPolicy } from "./tooling/policy.js";
14
16
  import { buildToolRegistry } from "./tooling/registry-build.js";
15
17
  import { createHash } from "node:crypto";
16
18
  import { NOOP_LOGGER, NOOP_TRACER, isStepCount, zeroUsage } from "@graphorin/core";
@@ -19,10 +21,11 @@ import { createReadResultTool, createToolSearchTool } from "@graphorin/tools/bui
19
21
  import { createCodeExecuteTool, createCodeSearchTool, projectToolApi } from "@graphorin/tools/code-mode";
20
22
  import { createToolExecutor } from "@graphorin/tools/executor";
21
23
  import { createDefaultSpillWriter, createFileResultReader } from "@graphorin/tools/result";
24
+ import { projectSchemaToJsonSchema } from "@graphorin/tools/schema";
22
25
 
23
26
  //#region src/factory.ts
24
27
  /**
25
- * `createAgent({...})` the agent factory entry point.
28
+ * `createAgent({...})` - the agent factory entry point.
26
29
  *
27
30
  * Wires the typed `model -> tool calls -> model` loop, the streamed
28
31
  * event surface, the steering / followUp queues, durable HITL via
@@ -46,23 +49,26 @@ const HANDOFF_TOOL_PREFIX = "transfer_to_";
46
49
  /** The built-in deferred-discovery tool's stable name (WI-05 / P0-3). */
47
50
  const TOOL_SEARCH_NAME = "tool_search";
48
51
  /**
49
- * Register the built-in `tool_search` into `registry` when and only
50
- * when the registry holds at least one deferred tool
52
+ * Register the built-in `tool_search` into `registry` when - and only
53
+ * when - the registry holds at least one deferred tool
51
54
  * (`defer_loading: true`). `tool_search` is itself eager (so it is
52
55
  * always advertised while a deferred pool exists) and resolvable by the
53
56
  * executor like any other tool, so a model can both *see* it in the
54
57
  * per-step catalogue and *call* it.
55
58
  *
56
- * No-op when nothing defers (zero overhead the tool never appears) or
59
+ * No-op when nothing defers (zero overhead - the tool never appears) or
57
60
  * when a user tool already occupies the name (the user's tool wins; we
58
61
  * never clobber it). Because deferral is decided at registration time
59
62
  * (`normaliseTool`), the deferred set is fixed for the life of the
60
- * registry this runs once per registry, not per step.
63
+ * registry - this runs once per registry, not per step.
61
64
  */
62
- function registerToolSearch(registry) {
65
+ function registerToolSearch(registry, availability) {
63
66
  if (registry.listDeferred().length === 0) return;
64
67
  if (registry.get(TOOL_SEARCH_NAME) !== void 0) return;
65
- registry.register(createToolSearchTool({ registry }), {
68
+ registry.register(createToolSearchTool({
69
+ registry,
70
+ ...availability !== void 0 ? { availability } : {}
71
+ }), {
66
72
  kind: "built-in",
67
73
  subsystem: "tool-discovery"
68
74
  });
@@ -72,7 +78,7 @@ const READ_RESULT_NAME = "read_result";
72
78
  /**
73
79
  * Register the built-in `read_result` into `registry` when at least one
74
80
  * registered tool opts into the `'spill-to-file'` truncation strategy
75
- * (the sole producer of spill handles today) or when `force` is set,
81
+ * (the sole producer of spill handles today) - or when `force` is set,
76
82
  * which the agent passes when an operator wires external result readers
77
83
  * (e.g. an MCP `resource_link` reader; WI-13). The tool is eager, so it
78
84
  * is advertised alongside the producing tool and the model can fetch a
@@ -109,27 +115,28 @@ function composeResultReaders(readers) {
109
115
  /** The code-mode meta-tools' stable names (WI-11 / P1-2). */
110
116
  const CODE_EXECUTE_NAME = "code_execute";
111
117
  const CODE_SEARCH_NAME = "code_search";
118
+ /** Structural check: is this tool approval-gated (static or predicate form)? */
119
+ const isApprovalGated = (t) => t.needsApproval === true || typeof t.needsApproval === "function";
112
120
  /**
113
121
  * Wire code-mode (P1-2) into `registry`: register the `code_search` /
114
122
  * `code_execute` meta-tools and return them as the tools to advertise in
115
123
  * place of the full catalogue. The model reaches every other tool through
116
124
  * `code_execute`, whose in-script `tools.<name>(args)` calls route back
117
125
  * through `quietExecutor.executeOne(...)` under the calling step's
118
- * `runContext` so per-tool ACL / sanitization / truncation still apply,
126
+ * `runContext` - so per-tool ACL / sanitization / truncation still apply,
119
127
  * exactly as in direct mode. `quietExecutor` carries no `streamingSink`,
120
128
  * so the inner calls do not interleave `tool.execute.*` events into the
121
129
  * outer stream.
122
130
  *
123
131
  * Excluded from the code API (`reservedNames`): the meta-tools, the
124
132
  * discovery / handle built-ins, handoff tools (which stay first-class
125
- * provider tools), and supplied by the caller any approval-gated
133
+ * provider tools), and - supplied by the caller - any approval-gated
126
134
  * tool, since code-mode has no durable-HITL path to suspend mid-script.
127
135
  *
128
136
  * Returns `[]` (registering nothing) when no real tool is exposable.
129
137
  */
130
- function registerCodeMode(registry, quietExecutor, reservedNames) {
138
+ function registerCodeMode(registry, quietExecutor, reservedNames, getRunCapability) {
131
139
  if (registry.get(CODE_EXECUTE_NAME) !== void 0) return [];
132
- const isApprovalGated = (t) => t.needsApproval === true || typeof t.needsApproval === "function";
133
140
  const codeTools = registry.list().filter((entry) => !reservedNames.has(entry.name) && !isApprovalGated(entry));
134
141
  if (codeTools.length === 0) return [];
135
142
  const approvalGatedTools = registry.list().filter((entry) => !reservedNames.has(entry.name) && isApprovalGated(entry)).map((entry) => entry.name);
@@ -138,7 +145,8 @@ function registerCodeMode(registry, quietExecutor, reservedNames) {
138
145
  const allowedSet = new Set(codeTools.map((entry) => entry.name));
139
146
  const projection = projectToolApi(codeTools.filter((entry) => entry.__effectiveDeferLoading !== true));
140
147
  const executeTool = async (call, ctx) => {
141
- if (approvalGatedSet.has(call.name)) throw new Error(`${call.name} requires human approval and cannot run inside code_execute call it directly as a standalone tool call so the run can suspend for the approval.`);
148
+ if (approvalGatedSet.has(call.name)) throw new Error(`${call.name} requires human approval and cannot run inside code_execute - call it directly as a standalone tool call so the run can suspend for the approval.`);
149
+ const runCapability = getRunCapability?.();
142
150
  const { outcome } = await quietExecutor.executeOne({
143
151
  call: {
144
152
  toolCallId: newId("codecall"),
@@ -146,7 +154,8 @@ function registerCodeMode(registry, quietExecutor, reservedNames) {
146
154
  args: call.args
147
155
  },
148
156
  runContext: ctx.runContext,
149
- stepNumber: ctx.runContext.stepNumber
157
+ stepNumber: ctx.runContext.stepNumber,
158
+ ...runCapability !== void 0 ? { capability: runCapability } : {}
150
159
  });
151
160
  if ("kind" in outcome) throw new Error(`${call.name}: ${outcome.message}`);
152
161
  return outcome.output;
@@ -176,7 +185,7 @@ function registerCodeMode(registry, quietExecutor, reservedNames) {
176
185
  * Fold a completed `tool_search` result into the per-run promotion set:
177
186
  * every matched tool name becomes advertised (and thus callable) on the
178
187
  * next step. Tolerant of unexpected shapes (e.g. a user-shadowed
179
- * `tool_search`) only string `name`s inside a `matches` array promote.
188
+ * `tool_search`) - only string `name`s inside a `matches` array promote.
180
189
  */
181
190
  function recordToolSearchPromotions(output, promoted) {
182
191
  if (typeof output !== "object" || output === null) return;
@@ -242,7 +251,7 @@ function stripJsonFence(text) {
242
251
  * do not yet consume `ProviderRequest.outputType` natively (PS-24).
243
252
  */
244
253
  function buildStructuredInstruction(spec) {
245
- const parts = ["Respond with a single valid JSON value only no prose, no Markdown code fences."];
254
+ const parts = ["Respond with a single valid JSON value only - no prose, no Markdown code fences."];
246
255
  if (spec.description !== void 0 && spec.description.length > 0) parts.push(spec.description);
247
256
  if (spec.jsonSchema !== void 0) parts.push(`The JSON MUST conform to this JSON Schema:\n${JSON.stringify(spec.jsonSchema)}`);
248
257
  return parts.join("\n");
@@ -260,27 +269,53 @@ function lastUserText(messages) {
260
269
  /**
261
270
  * AG-21: classify a **thrown** provider error into a {@link ProviderErrorKind}
262
271
  * so the fallback chain can act on it, instead of flattening every exception to
263
- * `'unknown'` (which is always fallback-ineligible). Structural reads the
272
+ * `'unknown'` (which is always fallback-ineligible). Structural - reads the
264
273
  * `kind` carried by `@graphorin/provider`'s `GraphorinProviderError` subclasses
265
274
  * without importing them, keeping the agent decoupled from the provider package.
266
275
  */
276
+ /** Canonical `ProviderErrorKind` values honoured off a thrown error's `errorKind`. */
277
+ const CANONICAL_PROVIDER_ERROR_KINDS = new Set([
278
+ "rate-limit",
279
+ "capacity",
280
+ "context-length",
281
+ "transient",
282
+ "invalid-request",
283
+ "unauthorized",
284
+ "content-filter"
285
+ ]);
267
286
  function classifyThrownProviderErrorKind(cause) {
268
- if (typeof cause === "object" && cause !== null && "kind" in cause) switch (cause.kind) {
269
- case "rate-limit-exceeded": return "rate-limit";
287
+ if (typeof cause === "object" && cause !== null) {
288
+ const errorKind = cause.errorKind;
289
+ if (typeof errorKind === "string" && CANONICAL_PROVIDER_ERROR_KINDS.has(errorKind)) return errorKind;
290
+ switch (cause.kind) {
291
+ case "rate-limit-exceeded": return "rate-limit";
292
+ }
270
293
  }
271
294
  return "unknown";
272
295
  }
273
- /** Resolve a Zod-like schema to a JSON-Schema record (via `toJSON`), else pass an object through. */
274
- function projectSchema(raw) {
275
- if (raw === null || raw === void 0) return void 0;
276
- const toJson = raw.toJSON;
277
- if (typeof toJson === "function") return toJson.call(raw);
278
- if (typeof raw === "object") return raw;
296
+ /** WARN-once keys for schemas the projection cannot read (per process). */
297
+ const unprojectableSchemaWarned = /* @__PURE__ */ new Set();
298
+ /**
299
+ * Resolve a tool's declared schema - plain Zod (v3/v4), `toJSON()`-bearing,
300
+ * or already-JSON-Schema data - to a JSON Schema record via the shared
301
+ * projection (tools-01). Pre-fix this only honoured `toJSON()` and passed
302
+ * everything else through verbatim, so every plain-Zod tool serialised as
303
+ * `{"_def":...}` internals on OpenAI-shaped/Ollama/vercel wire bodies.
304
+ * `undefined` when nothing usable can be projected (caller substitutes a
305
+ * permissive `{}`), with a WARN so the degradation is never silent.
306
+ */
307
+ function projectSchema(raw, toolName, slot) {
308
+ return projectSchemaToJsonSchema(raw, { onUnsupported: (detail) => {
309
+ const key = `${toolName}:${slot}:${detail}`;
310
+ if (unprojectableSchemaWarned.has(key)) return;
311
+ unprojectableSchemaWarned.add(key);
312
+ console.warn(`[graphorin/agent] tool '${toolName}' ${slot} schema: '${detail}' cannot be projected to JSON Schema - that fragment degrades to a permissive {} on the provider wire body.`);
313
+ } });
279
314
  }
280
315
  function toolToDefinition(tool) {
281
316
  const ts = tool;
282
- const inputSchema = projectSchema(ts.inputSchema) ?? {};
283
- const outputSchema = projectSchema(ts.outputSchema);
317
+ const inputSchema = projectSchema(ts.inputSchema, tool.name, "input") ?? {};
318
+ const outputSchema = projectSchema(ts.outputSchema, tool.name, "output");
284
319
  const examples = renderToolExamples(tool);
285
320
  return {
286
321
  name: tool.name,
@@ -296,8 +331,8 @@ function toolToDefinition(tool) {
296
331
  * renders them: the registry resolves the `defer_loading` auto-rule into
297
332
  * `examplesEagerlyRendered`, so a deferred tool resolves to `false` and is
298
333
  * skipped (its examples stay out of context even once `tool_search`
299
- * promotes it). `undefined` the "runtime decides" case for a plain
300
- * eager tool renders, since the tool is already advertised this step.
334
+ * promotes it). `undefined` - the "runtime decides" case for a plain
335
+ * eager tool - renders, since the tool is already advertised this step.
301
336
  *
302
337
  * Bounded to ≤5 to honour the `ToolExample` `[1,5]` contract even when a
303
338
  * tool slipped past the registry's overflow WARN (which does not truncate).
@@ -482,7 +517,7 @@ function stripReasoningFromMessages(messages) {
482
517
  }
483
518
  /**
484
519
  * Count the leading contiguous run of `system` messages in the initial
485
- * buffer the trusted, KV-cache-stable instruction prefix. Captured
520
+ * buffer - the trusted, KV-cache-stable instruction prefix. Captured
486
521
  * once at run start (WI-09 / P1-1): auto-compaction summarises only the
487
522
  * messages after this prefix, so the prefix stays byte-identical across
488
523
  * every step (the provider's cache breakpoint is real) and a long run
@@ -491,17 +526,94 @@ function stripReasoningFromMessages(messages) {
491
526
  * The length is fixed for the run rather than re-derived per compaction
492
527
  * on purpose: each compaction inserts its summary as a `system` message
493
528
  * right after the prefix, so re-scanning the leading run would absorb
494
- * that summary into the prefix and shield it from the next compaction
529
+ * that summary into the prefix and shield it from the next compaction -
495
530
  * summaries would stack unbounded. Pinning the original length keeps
496
531
  * each prior summary inside the compactable body, where the next pass
497
532
  * folds it into a fresh summary-of-summary.
498
533
  */
534
+ /**
535
+ * Marker prefix stamped on every compaction summary (see the memory
536
+ * package's summary template). context-engine-05: the prefix scan must
537
+ * stop at it - after a compact-then-suspend cycle the summary is a
538
+ * SYSTEM message sitting right after the true prefix, and counting it
539
+ * in would pin it (and every later summary) outside the compactable
540
+ * window forever, growing the uncompactable prefix by one summary per
541
+ * cycle.
542
+ */
543
+ const COMPACTION_SUMMARY_MARKER = "<graphorin_compaction_summary";
499
544
  function countLeadingSystemMessages(messages) {
500
545
  let i = 0;
501
- while (i < messages.length && messages[i]?.role === "system") i += 1;
546
+ while (i < messages.length) {
547
+ const msg = messages[i];
548
+ if (msg?.role !== "system") break;
549
+ if (typeof msg.content === "string" && msg.content.startsWith(COMPACTION_SUMMARY_MARKER)) break;
550
+ i += 1;
551
+ }
502
552
  return i;
503
553
  }
504
554
  /**
555
+ * Immutable usage sum. Optional token fields (reasoning + prompt-cache
556
+ * legs, core-provider-02) appear in the result only when at least one
557
+ * side carries them, so pre-cache serialized shapes stay byte-identical.
558
+ */
559
+ /**
560
+ * D6: assemble the per-step request messages. Trailing, request-only
561
+ * additions ride the LAST prompt-cache anchor and never touch the shared
562
+ * `messages` buffer or the persisted RunState: the structured-output
563
+ * instruction (AG-3) and the attention-recitation plan block are both
564
+ * appended here, in that order, so the stable prompt prefix is unchanged
565
+ * and only the small trailing tail is re-sent each step.
566
+ */
567
+ function buildStepMessages(messages, structuredInstruction, todos) {
568
+ const out = [...messages];
569
+ if (structuredInstruction !== void 0) out.push({
570
+ role: "system",
571
+ content: structuredInstruction
572
+ });
573
+ const recitation = renderPlanRecitation(todos);
574
+ if (recitation !== null) out.push({
575
+ role: "system",
576
+ content: recitation
577
+ });
578
+ return out;
579
+ }
580
+ function addUsage(a, b) {
581
+ const optional = (x, y) => x === void 0 && y === void 0 ? void 0 : (x ?? 0) + (y ?? 0);
582
+ const reasoningTokens = optional(a.reasoningTokens, b.reasoningTokens);
583
+ const cachedReadTokens = optional(a.cachedReadTokens, b.cachedReadTokens);
584
+ const cacheWriteTokens = optional(a.cacheWriteTokens, b.cacheWriteTokens);
585
+ return {
586
+ promptTokens: a.promptTokens + b.promptTokens,
587
+ completionTokens: a.completionTokens + b.completionTokens,
588
+ totalTokens: a.totalTokens + b.totalTokens,
589
+ ...reasoningTokens !== void 0 ? { reasoningTokens } : {},
590
+ ...cachedReadTokens !== void 0 ? { cachedReadTokens } : {},
591
+ ...cacheWriteTokens !== void 0 ? { cacheWriteTokens } : {}
592
+ };
593
+ }
594
+ /**
595
+ * C3: render a ToolError for the model. The first line keeps the
596
+ * long-standing `Error: <message>` shape; a bracketed second line carries
597
+ * the typed kind + the recovery envelope, which is what actually changes
598
+ * model behaviour after a failure (retry vs. fix args vs. give up).
599
+ */
600
+ function renderToolErrorMessage(error) {
601
+ const parts = [`kind: ${error.kind}`];
602
+ if (error.recoverable !== void 0) parts.push(error.recoverable ? "recoverable: yes" : "recoverable: no");
603
+ if (error.recoveryHint !== void 0) parts.push(`suggested action: ${error.recoveryHint}`);
604
+ if (error.hint !== void 0) parts.push(error.hint);
605
+ return `Error: ${error.message}\n[${parts.join("; ")}]`;
606
+ }
607
+ /** In-place variant of {@link addUsage} for mutable accumulators. */
608
+ function accumulateUsage(target, delta) {
609
+ target.promptTokens += delta.promptTokens;
610
+ target.completionTokens += delta.completionTokens;
611
+ target.totalTokens += delta.totalTokens;
612
+ if (delta.reasoningTokens !== void 0) target.reasoningTokens = (target.reasoningTokens ?? 0) + delta.reasoningTokens;
613
+ if (delta.cachedReadTokens !== void 0) target.cachedReadTokens = (target.cachedReadTokens ?? 0) + delta.cachedReadTokens;
614
+ if (delta.cacheWriteTokens !== void 0) target.cacheWriteTokens = (target.cacheWriteTokens ?? 0) + delta.cacheWriteTokens;
615
+ }
616
+ /**
505
617
  * Build a fresh {@link Agent} from the supplied configuration.
506
618
  *
507
619
  * @stable
@@ -509,7 +621,7 @@ function countLeadingSystemMessages(messages) {
509
621
  function createAgent(config) {
510
622
  if (typeof config.name !== "string" || config.name.length === 0) throw new InvalidAgentConfigError("missing 'name'");
511
623
  if (config.provider === void 0 || config.provider === null) throw new InvalidAgentConfigError("missing 'provider'");
512
- if (config.outputType?.kind === "text" && config.outputType.schema !== void 0) throw new InvalidAgentConfigError("outputType.kind 'text' with a schema did you mean kind: 'structured'?");
624
+ if (config.outputType?.kind === "text" && config.outputType.schema !== void 0) throw new InvalidAgentConfigError("outputType.kind 'text' with a schema - did you mean kind: 'structured'?");
513
625
  validatePreferredModel(config.preferredModel);
514
626
  if (config.modelTierMap !== void 0) for (const [tier, spec] of Object.entries(config.modelTierMap)) {
515
627
  if (!isModelHintLike(tier)) throw new InvalidPreferredModelError({ tier });
@@ -540,6 +652,7 @@ function createAgent(config) {
540
652
  let abortController;
541
653
  let pendingAbort;
542
654
  let activeRunState;
655
+ let activeRunCapability;
543
656
  /** AG-11: guards the one-in-flight-run-per-instance invariant. */
544
657
  let runInFlight = false;
545
658
  const externalEventQueue = [];
@@ -560,7 +673,13 @@ function createAgent(config) {
560
673
  ...config.tools !== void 0 ? { tools: config.tools } : {},
561
674
  ...config.skills !== void 0 ? { skills: config.skills } : {}
562
675
  }).registry;
563
- registerToolSearch(toolRegistry);
676
+ registerToolSearch(toolRegistry, config.toolPromotion === "run-boundary" ? "next-run" : "next-step");
677
+ if (config.plan === true && toolRegistry.get(PLAN_TOOL_NAME) === void 0) toolRegistry.register(createPlanTool((todos) => {
678
+ if (activeRunState !== void 0) activeRunState.todos = todos;
679
+ }), {
680
+ kind: "built-in",
681
+ subsystem: "planning"
682
+ });
564
683
  const spillWriter = createDefaultSpillWriter();
565
684
  const fileResultReader = createFileResultReader({ artifactRoot: spillWriter.artifactRoot });
566
685
  const externalResultReaders = config.resultReaders ?? [];
@@ -588,9 +707,10 @@ function createAgent(config) {
588
707
  const toolMemoryGuardFactory = memoryGuardWiring.memoryGuardFactory;
589
708
  if (memory === void 0) {
590
709
  const guarded = (config.tools ?? []).filter((t) => t.memoryGuardTier !== void 0);
591
- if (guarded.length > 0) console.warn(`[graphorin/agent] ${guarded.length} tool(s) declare memoryGuardTier but no memory is wired the DEC-153 snapshot/verify guard is skipped (SDF-1). Wire \`memory\` to activate it.`);
710
+ if (guarded.length > 0) console.warn(`[graphorin/agent] ${guarded.length} tool(s) declare memoryGuardTier but no memory is wired - the DEC-153 snapshot/verify guard is skipped (SDF-1). Wire \`memory\` to activate it.`);
592
711
  }
593
712
  const toolDataFlowGuard = config.dataFlowPolicy !== void 0 && config.dataFlowPolicy.mode !== "off" ? buildDataFlowGuard(config.dataFlowPolicy) : void 0;
713
+ const { guard: toolArgumentPolicyGuard, capabilityFloor: ruleOfTwoCapabilityFloor } = buildToolArgumentPolicy(config.toolPolicy, config.ruleOfTwo);
594
714
  const toolStreamingSink = (event) => activeExecutorBridge?.sink(event);
595
715
  const makeToolExecutor = (registry, opts) => createToolExecutor({
596
716
  registry,
@@ -601,8 +721,10 @@ function createAgent(config) {
601
721
  ...memoryGuardWiring.memoryRegionReader !== void 0 ? { memoryRegionReader: memoryGuardWiring.memoryRegionReader } : {},
602
722
  spill: spillWriter,
603
723
  ...toolDataFlowGuard !== void 0 ? { dataFlowGuard: toolDataFlowGuard } : {},
724
+ ...toolArgumentPolicyGuard !== void 0 ? { argumentPolicy: toolArgumentPolicyGuard } : {},
604
725
  ...opts?.quiet === true ? {} : { streamingSink: toolStreamingSink },
605
- ...config.maxParallelTools !== void 0 ? { maxParallelTools: config.maxParallelTools } : {}
726
+ ...config.maxParallelTools !== void 0 ? { maxParallelTools: config.maxParallelTools } : {},
727
+ ...config.toolRetry !== void 0 ? { retry: config.toolRetry } : {}
606
728
  });
607
729
  const toolExecutor = makeToolExecutor(toolRegistry);
608
730
  const isCodeMode = config.toolInvocation === "code-mode";
@@ -615,7 +737,7 @@ function createAgent(config) {
615
737
  READ_RESULT_NAME,
616
738
  ...handoffMap.keys()
617
739
  ]);
618
- const metas = registerCodeMode(toolRegistry, makeToolExecutor(toolRegistry, { quiet: true }), reserved);
740
+ const metas = registerCodeMode(toolRegistry, makeToolExecutor(toolRegistry, { quiet: true }), reserved, () => activeRunCapability);
619
741
  registerReadResult(toolRegistry, resultReader);
620
742
  const readResult = toolRegistry.get(READ_RESULT_NAME);
621
743
  codeModeAdvertised = [...metas, ...readResult !== void 0 ? [readResult] : []];
@@ -624,7 +746,7 @@ function createAgent(config) {
624
746
  /**
625
747
  * AG-11: one in-flight run per Agent instance. `steer` / `followUp` /
626
748
  * `abort` / `compact` address "the run" with no run handle, so
627
- * overlapping runs on one instance cannot be expressed safely they
749
+ * overlapping runs on one instance cannot be expressed safely - they
628
750
  * would share the abort controller, steer queue, active-run ref and
629
751
  * executor bridge. A second concurrent `run()` / `stream()` rejects
630
752
  * with {@link ConcurrentRunError}; run-scoped state is reset on entry
@@ -637,12 +759,14 @@ function createAgent(config) {
637
759
  runInFlight = true;
638
760
  pendingSteer = [];
639
761
  pendingAbort = void 0;
762
+ activeRunCapability = options.capability ?? config.capability ?? ruleOfTwoCapabilityFloor;
640
763
  causalityMonitor?.reset();
641
764
  try {
642
765
  return yield* runLoopInner(input, options);
643
766
  } finally {
644
767
  runInFlight = false;
645
768
  activeRunState = void 0;
769
+ activeRunCapability = void 0;
646
770
  while (pendingManualCompacts.length > 0) pendingManualCompacts.shift()?.resolve(noopCompactionResult("no-active-run"));
647
771
  }
648
772
  }
@@ -667,6 +791,15 @@ function createAgent(config) {
667
791
  });
668
792
  activeRunState = state;
669
793
  if (resumed && state.taintSummary !== void 0) toolDataFlowGuard?.seedLedger(state.id, state.taintSummary);
794
+ const promotedDeferred = /* @__PURE__ */ new Set();
795
+ if (resumed && state.promotedTools !== void 0) for (const name of state.promotedTools) promotedDeferred.add(name);
796
+ const runStartPromotions = config.toolPromotion === "run-boundary" ? new Set(promotedDeferred) : void 0;
797
+ async function* finishRun(s, snapshot) {
798
+ const taintSnap = toolDataFlowGuard?.snapshotLedger(s.id);
799
+ if (taintSnap !== void 0) s.taintSummary = taintSnap;
800
+ if (promotedDeferred.size > 0) s.promotedTools = [...promotedDeferred];
801
+ return yield* finishRunBase(s, snapshot);
802
+ }
670
803
  const messages = resumed ? [...state.messages] : [];
671
804
  if (!resumed) {
672
805
  const instructionsRaw = config.instructions;
@@ -709,6 +842,18 @@ function createAgent(config) {
709
842
  for (const m of messages) state.messages.push(m);
710
843
  }
711
844
  const finalSnapshot = { output: "" };
845
+ const runSpan = tracer.startSpan({
846
+ type: "agent.run",
847
+ attrs: {
848
+ "gen_ai.operation.name": "invoke_agent",
849
+ "gen_ai.agent.id": agentId,
850
+ "gen_ai.agent.name": config.name,
851
+ "graphorin.run.id": state.id,
852
+ "graphorin.session.id": sessionId,
853
+ "graphorin.run.resumed": resumed !== void 0
854
+ }
855
+ });
856
+ let currentStepSpan;
712
857
  yield {
713
858
  type: "agent.start",
714
859
  runId: state.id,
@@ -758,6 +903,7 @@ function createAgent(config) {
758
903
  }
759
904
  const structuredInstruction = config.outputType?.kind === "structured" ? buildStructuredInstruction(config.outputType) : void 0;
760
905
  const resumedApprovedCalls = [];
906
+ const grantedApprovals = [];
761
907
  const journaledCallIds = /* @__PURE__ */ new Set();
762
908
  for (const step of state.steps) for (const completed of step.toolCalls) journaledCallIds.add(completed.call.toolCallId);
763
909
  if (resumed && options.directive?.approvals !== void 0 && state.pendingApprovals.length > 0) {
@@ -780,6 +926,7 @@ function createAgent(config) {
780
926
  toolName: approval.toolName,
781
927
  args: approval.args
782
928
  });
929
+ grantedApprovals.push(approval);
783
930
  } else {
784
931
  yield {
785
932
  type: "tool.approval.denied",
@@ -815,8 +962,29 @@ function createAgent(config) {
815
962
  messages,
816
963
  state
817
964
  };
818
- if (resumed && (state.status === "awaiting_approval" || state.status === "failed")) return yield* finishRun(state, finalSnapshot);
965
+ if (resumed && state.status === "failed") return yield* finishRun(state, finalSnapshot);
819
966
  if (resumed && resumedApprovedCalls.length > 0) {
967
+ if (config.checkpointStore !== void 0) {
968
+ const prevStatus = state.status;
969
+ state.status = "awaiting_approval";
970
+ state.pendingApprovals.unshift(...grantedApprovals);
971
+ const intentState = serializeRunState(state, { stripTracingApiKey: true });
972
+ state.pendingApprovals.splice(0, grantedApprovals.length);
973
+ state.status = prevStatus;
974
+ await config.checkpointStore.put(state.id, "agent", {
975
+ id: state.id,
976
+ threadId: state.id,
977
+ namespace: "agent",
978
+ state: intentState,
979
+ channelVersions: {},
980
+ stepNumber: 0,
981
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
982
+ }, {
983
+ source: "sync",
984
+ status: "suspended",
985
+ nodeName: "agent.resume.intent"
986
+ });
987
+ }
820
988
  state.steps.push({
821
989
  stepNumber: 0,
822
990
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -829,19 +997,31 @@ function createAgent(config) {
829
997
  ...runContextBase,
830
998
  stepNumber: 0,
831
999
  messages
832
- }, 0);
1000
+ }, 0, { disableRepair: true });
1001
+ if (config.checkpointStore !== void 0) await config.checkpointStore.put(state.id, "agent", {
1002
+ id: state.id,
1003
+ threadId: state.id,
1004
+ namespace: "agent",
1005
+ state: serializeRunState(state, { stripTracingApiKey: true }),
1006
+ channelVersions: {},
1007
+ stepNumber: 0,
1008
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1009
+ }, {
1010
+ source: "sync",
1011
+ status: state.status === "awaiting_approval" ? "suspended" : "running",
1012
+ nodeName: "agent.resume.dispatched"
1013
+ });
833
1014
  }
834
- const promotedDeferred = /* @__PURE__ */ new Set();
835
- if (resumed && state.promotedTools !== void 0) for (const name of state.promotedTools) promotedDeferred.add(name);
1015
+ if (resumed && state.status === "awaiting_approval") return yield* finishRun(state, finalSnapshot);
836
1016
  /**
837
1017
  * Dispatch a batch of (non-handoff) tool calls through the
838
1018
  * {@link ToolExecutor} and surface the results as `AgentEvent`s.
839
1019
  *
840
1020
  * The agent owns the `tool.execute.start` / `.end` / `.error`
841
1021
  * lifecycle (derived deterministically from the returned
842
- * {@link CompletedToolCall} outcomes) so every outcome kind
1022
+ * {@link CompletedToolCall} outcomes) so every outcome kind -
843
1023
  * success, unknown-tool, invalid-input, sanitization-blocked,
844
- * execution error yields a consistent event and tool message,
1024
+ * execution error - yields a consistent event and tool message,
845
1025
  * preserving the pre-WI-03 stream shape (R10). The executor's
846
1026
  * genuinely-live streaming events (`tool.execute.progress` /
847
1027
  * `.partial`, emitted only by streaming-hint tools) are bridged
@@ -850,13 +1030,13 @@ function createAgent(config) {
850
1030
  * Parallelism (WI-04): the executor runs independent calls in this
851
1031
  * batch concurrently, bounded by `maxParallelTools`. `tool.execute.start`
852
1032
  * is emitted up-front in call order and `.end` / `.error` after the
853
- * batch settles, also in call order so result mapping and tool-message
1033
+ * batch settles, also in call order - so result mapping and tool-message
854
1034
  * history are deterministic regardless of which call finishes first,
855
1035
  * while `.progress` / `.partial` events for concurrent calls interleave
856
1036
  * (keyed by `toolCallId`). Tools declaring `executionMode: 'sequential'`
857
1037
  * are serialised by the executor and never overlap.
858
1038
  */
859
- async function* dispatchBatch(calls, executor, runContext, stepNum) {
1039
+ async function* dispatchBatch(calls, executor, runContext, stepNum, dispatchOpts = {}) {
860
1040
  if (calls.length === 0) return;
861
1041
  for (const call of calls) yield {
862
1042
  type: "tool.execute.start",
@@ -867,7 +1047,9 @@ function createAgent(config) {
867
1047
  const resultsPromise = executor.executeBatch({
868
1048
  calls,
869
1049
  runContext,
870
- stepNumber: stepNum
1050
+ stepNumber: stepNum,
1051
+ ...dispatchOpts.disableRepair !== void 0 ? { disableRepair: dispatchOpts.disableRepair } : {},
1052
+ ...activeRunCapability !== void 0 ? { capability: activeRunCapability } : {}
871
1053
  });
872
1054
  const closeOnSettle = resultsPromise.then(() => bridge.close(), () => bridge.close());
873
1055
  for await (const event of bridge.drain()) if (event.type === "tool.execute.progress" || event.type === "tool.execute.partial") yield event;
@@ -887,7 +1069,7 @@ function createAgent(config) {
887
1069
  toolCallId: call.toolCallId,
888
1070
  error: outcome
889
1071
  };
890
- const text = `Error: ${outcome.message}`;
1072
+ const text = renderToolErrorMessage(outcome);
891
1073
  messages.push({
892
1074
  role: "tool",
893
1075
  toolCallId: call.toolCallId,
@@ -908,7 +1090,8 @@ function createAgent(config) {
908
1090
  durationMs: outcome.durationMs
909
1091
  };
910
1092
  const handle = outcome.resultHandle;
911
- const text = handle !== void 0 ? `${handle.preview}\n\n[Full result${handle.bytes !== void 0 ? ` (${handle.bytes} bytes)` : ""} stored behind a handle. Call read_result with handle ${JSON.stringify(handle.uri)} to retrieve it optionally narrow with offset/length (bytes) or startLine/endLine.]` : typeof output === "string" ? output : JSON.stringify(output);
1093
+ const rendered = handle !== void 0 ? `${handle.preview}\n\n[Full result${handle.bytes !== void 0 ? ` (${handle.bytes} bytes)` : ""} stored behind a handle. Call read_result with handle ${JSON.stringify(handle.uri)} to retrieve it - optionally narrow with offset/length (bytes) or startLine/endLine.]` : typeof output === "string" ? output : JSON.stringify(output);
1094
+ const text = rendered === void 0 || rendered.trim().length === 0 ? "(tool ran successfully with no output)" : rendered;
912
1095
  messages.push({
913
1096
  role: "tool",
914
1097
  toolCallId: call.toolCallId,
@@ -930,20 +1113,20 @@ function createAgent(config) {
930
1113
  * in-flight buffer has crossed its per-provider threshold
931
1114
  * (`shouldCompact`); when it has, summarise the older turns
932
1115
  * (`compactNow`, `source: 'auto-trigger'`), splice the result back in
933
- * preserving the byte-stable system prefix and the most-recent
934
- * turns verbatim and emit `context.compacted`. The compaction is
1116
+ * - preserving the byte-stable system prefix and the most-recent
1117
+ * turns verbatim - and emit `context.compacted`. The compaction is
935
1118
  * configured on the memory facade (`createMemory({ contextEngine })`,
936
1119
  * RB-46); there is no parallel agent-level knob.
937
1120
  *
938
1121
  * No-op when no memory is wired, when compaction is disabled or below
939
1122
  * threshold (the engine returns `false`), or for `secret`-tier runs
940
- * (secret history is never shipped to the summarizer a less-trusted
1123
+ * (secret history is never shipped to the summarizer - a less-trusted
941
1124
  * external sink; per-result handle references land in WI-10). Best
942
1125
  * effort: a misconfigured engine (e.g. no summarizer) is swallowed and
943
1126
  * the run proceeds uncompacted rather than aborting mid-flight.
944
1127
  *
945
1128
  * Operator-requested compactions (`agent.compact()`, CE-3/AG-13) are
946
- * serviced here too, FIRST the queue carries the `compact()` promise
1129
+ * serviced here too, FIRST - the queue carries the `compact()` promise
947
1130
  * resolvers, and manual requests bypass the trigger evaluation.
948
1131
  */
949
1132
  async function* maybeAutoCompact() {
@@ -955,7 +1138,7 @@ function createAgent(config) {
955
1138
  if (mem === void 0) return;
956
1139
  if (config.sensitivity === "secret") return;
957
1140
  const engine = mem.contextEngine;
958
- if (!await engine.shouldCompact(messages).catch(() => false)) return;
1141
+ if (!await engine.shouldCompact(messages, { compactableFromIndex: systemPrefixLength }).catch(() => false)) return;
959
1142
  const startedAt = Date.now();
960
1143
  const envelope = await engine.compactNow({
961
1144
  scope: {
@@ -968,6 +1151,7 @@ function createAgent(config) {
968
1151
  agentId,
969
1152
  source: "auto-trigger",
970
1153
  messages: messages.slice(systemPrefixLength),
1154
+ prefixMessages: messages.slice(0, systemPrefixLength),
971
1155
  memory: mem
972
1156
  }).catch(() => void 0);
973
1157
  if (envelope === void 0) return;
@@ -987,6 +1171,51 @@ function createAgent(config) {
987
1171
  };
988
1172
  }
989
1173
  /**
1174
+ * context-engine-06: last-resort tier at hard context overflow. When
1175
+ * a provider rejects the request as over-window, force ONE aggressive
1176
+ * compaction (`preserveRecentTurns: 2`, trigger evaluation bypassed)
1177
+ * and let the caller retry the same provider - the fallback chain's
1178
+ * members usually share the same window, so without this the run just
1179
+ * dies. Returns `true` when the buffer actually shrank (retry is
1180
+ * worthwhile); `false` when memory is not wired, the run is
1181
+ * secret-tier, compaction trimmed nothing, or the engine threw.
1182
+ */
1183
+ async function* tryEmergencyCompact() {
1184
+ const mem = memory;
1185
+ if (mem === void 0 || config.sensitivity === "secret") return false;
1186
+ const startedAt = Date.now();
1187
+ const envelope = await mem.contextEngine.compactNow({
1188
+ scope: {
1189
+ userId: state.userId ?? agentId,
1190
+ sessionId,
1191
+ agentId
1192
+ },
1193
+ runId: state.id,
1194
+ sessionId,
1195
+ agentId,
1196
+ source: "auto-trigger",
1197
+ messages: messages.slice(systemPrefixLength),
1198
+ prefixMessages: messages.slice(0, systemPrefixLength),
1199
+ memory: mem,
1200
+ preserveRecentTurns: 2
1201
+ }).catch(() => void 0);
1202
+ if (envelope === void 0 || envelope.result.droppedMessageIndices.length === 0) return false;
1203
+ spliceCompacted(envelope);
1204
+ yield {
1205
+ type: "context.compacted",
1206
+ runId: state.id,
1207
+ sessionId,
1208
+ agentId,
1209
+ beforeTokens: envelope.result.beforeTokens,
1210
+ afterTokens: envelope.result.afterTokens,
1211
+ summaryTokens: envelope.result.summaryTokens,
1212
+ durationMs: Date.now() - startedAt,
1213
+ source: "auto-trigger",
1214
+ hooksFiredCount: envelope.result.hooksFiredCount
1215
+ };
1216
+ return true;
1217
+ }
1218
+ /**
990
1219
  * Prefix-pinned splice shared by the auto + manual compaction paths
991
1220
  * (CE-3): stable system prefix + [summary, ...recent turns], with the
992
1221
  * post-compaction hooks' text Context Essentials re-anchored as a
@@ -1032,6 +1261,7 @@ function createAgent(config) {
1032
1261
  agentId,
1033
1262
  source,
1034
1263
  messages: messages.slice(systemPrefixLength),
1264
+ prefixMessages: messages.slice(0, systemPrefixLength),
1035
1265
  memory: mem,
1036
1266
  ...pending.options?.preserveRecentTurns !== void 0 ? { preserveRecentTurns: pending.options.preserveRecentTurns } : {}
1037
1267
  });
@@ -1069,6 +1299,7 @@ function createAgent(config) {
1069
1299
  }
1070
1300
  const handoffNames = Array.from(handoffMap.keys());
1071
1301
  let stepNumber = 0;
1302
+ let verifierRoundsUsed = 0;
1072
1303
  let lastStepCalledToolNames = [];
1073
1304
  async function* emitCancellation() {
1074
1305
  yield {
@@ -1126,17 +1357,30 @@ function createAgent(config) {
1126
1357
  type: "step.start",
1127
1358
  stepNumber
1128
1359
  };
1360
+ currentStepSpan?.end();
1361
+ currentStepSpan = tracer.startSpan({
1362
+ type: "agent.step",
1363
+ parent: runSpan,
1364
+ attrs: {
1365
+ "gen_ai.operation.name": "invoke_agent",
1366
+ "gen_ai.agent.id": agentId,
1367
+ "gen_ai.agent.name": config.name,
1368
+ "graphorin.run.id": state.id,
1369
+ "graphorin.step.number": stepNumber
1370
+ }
1371
+ });
1129
1372
  yield* maybeAutoCompact();
1130
1373
  const stepCtx = {
1131
1374
  ...runContextBase,
1132
1375
  stepNumber,
1133
- messages
1376
+ messages,
1377
+ ...currentStepSpan !== void 0 ? { span: currentStepSpan } : {}
1134
1378
  };
1135
1379
  const overrides = config.prepareStep ? await config.prepareStep(stepCtx) : {};
1136
1380
  const useOverrideRegistry = overrides.tools !== void 0 && !isCodeMode;
1137
1381
  const stepRegistry = useOverrideRegistry ? buildToolRegistry({ tools: overrides.tools }).registry : toolRegistry;
1138
1382
  if (useOverrideRegistry) {
1139
- registerToolSearch(stepRegistry);
1383
+ registerToolSearch(stepRegistry, config.toolPromotion === "run-boundary" ? "next-run" : "next-step");
1140
1384
  registerReadResult(stepRegistry, resultReader);
1141
1385
  }
1142
1386
  const stepExecutor = useOverrideRegistry ? makeToolExecutor(stepRegistry) : toolExecutor;
@@ -1145,15 +1389,21 @@ function createAgent(config) {
1145
1389
  if (h === void 0) throw new ToolNotFoundError(n);
1146
1390
  return buildHandoffTool(h.agent);
1147
1391
  });
1392
+ const readOnlyRun = activeRunCapability === "read-only";
1393
+ const keepReadOnly = (t) => {
1394
+ const cls = t.__sideEffectClass ?? t.sideEffectClass;
1395
+ return cls === "pure" || cls === "read-only";
1396
+ };
1148
1397
  let stepTools;
1149
- if (isCodeMode) stepTools = [...codeModeAdvertised, ...handoffTools];
1398
+ if (isCodeMode) stepTools = readOnlyRun ? [...codeModeAdvertised.filter(keepReadOnly)] : [...codeModeAdvertised, ...handoffTools];
1150
1399
  else {
1151
1400
  const eagerTools = stepRegistry.listEager();
1152
- const promotedTools = promotedDeferred.size === 0 ? [] : orderPromotedTools(promotedDeferred, stepRegistry.listDeferred());
1153
- stepTools = [
1401
+ const advertisedPromotions = runStartPromotions ?? promotedDeferred;
1402
+ const promotedTools = advertisedPromotions.size === 0 ? [] : orderPromotedTools(advertisedPromotions, stepRegistry.listDeferred());
1403
+ stepTools = readOnlyRun ? [...eagerTools.filter(keepReadOnly), ...promotedTools.filter(keepReadOnly)] : [
1154
1404
  ...eagerTools,
1155
- ...promotedTools,
1156
- ...handoffTools
1405
+ ...handoffTools,
1406
+ ...promotedTools
1157
1407
  ];
1158
1408
  }
1159
1409
  const calledLastStep = new Set(lastStepCalledToolNames);
@@ -1175,10 +1425,7 @@ function createAgent(config) {
1175
1425
  }
1176
1426
  const toolDefs = stepTools.map((t) => toolToDefinition(t));
1177
1427
  const baseRequest = {
1178
- messages: structuredInstruction === void 0 ? messages : [...messages, {
1179
- role: "system",
1180
- content: structuredInstruction
1181
- }],
1428
+ messages: buildStepMessages(messages, structuredInstruction, activeRunState?.todos),
1182
1429
  ...config.outputType !== void 0 ? { outputType: {
1183
1430
  kind: config.outputType.kind,
1184
1431
  ...config.outputType.description !== void 0 ? { description: config.outputType.description } : {},
@@ -1196,6 +1443,8 @@ function createAgent(config) {
1196
1443
  signal,
1197
1444
  ...overrides.temperature !== void 0 ? { temperature: overrides.temperature } : {},
1198
1445
  ...overrides.maxTokens !== void 0 ? { maxTokens: overrides.maxTokens } : {},
1446
+ ...config.cachePolicy !== void 0 ? { cachePolicy: config.cachePolicy } : {},
1447
+ ...currentStepSpan !== void 0 ? { parentSpan: currentStepSpan } : {},
1199
1448
  reasoningRetention: reasoningPolicy
1200
1449
  };
1201
1450
  const stepUsage = zeroUsage();
@@ -1207,6 +1456,8 @@ function createAgent(config) {
1207
1456
  let lastError;
1208
1457
  let finalCalls = [];
1209
1458
  let stepReasoningParts = [];
1459
+ let requestForStep = baseRequest;
1460
+ let emergencyCompactTried = false;
1210
1461
  for (let chainIdx = 0; chainIdx < fallbackChain.length; chainIdx++) {
1211
1462
  const candidate = fallbackChain[chainIdx];
1212
1463
  if (candidate === void 0) continue;
@@ -1239,18 +1490,13 @@ function createAgent(config) {
1239
1490
  let providerCallCompleted = false;
1240
1491
  let providerStepUsage = zeroUsage();
1241
1492
  try {
1242
- const stream$1 = providerForStep.stream(baseRequest);
1493
+ const stream$1 = providerForStep.stream(requestForStep);
1243
1494
  for await (const ev of stream$1) {
1244
1495
  if (signal.aborted && pendingAbort?.drain !== true) throw new AgentRuntimeError("run-aborted", "aborted");
1245
1496
  const out = handleProviderEvent(ev, evState);
1246
1497
  if (out.emit !== void 0) yield out.emit;
1247
1498
  if (out.providerError !== void 0) providerError = out.providerError;
1248
- if (out.usage !== void 0) providerStepUsage = {
1249
- promptTokens: providerStepUsage.promptTokens + out.usage.promptTokens,
1250
- completionTokens: providerStepUsage.completionTokens + out.usage.completionTokens,
1251
- totalTokens: providerStepUsage.totalTokens + out.usage.totalTokens,
1252
- ...out.usage.reasoningTokens !== void 0 || providerStepUsage.reasoningTokens !== void 0 ? { reasoningTokens: (providerStepUsage.reasoningTokens ?? 0) + (out.usage.reasoningTokens ?? 0) } : {}
1253
- };
1499
+ if (out.usage !== void 0) providerStepUsage = addUsage(providerStepUsage, out.usage);
1254
1500
  if (out.finished === true) providerCallCompleted = true;
1255
1501
  }
1256
1502
  } catch (cause) {
@@ -1264,6 +1510,17 @@ function createAgent(config) {
1264
1510
  }
1265
1511
  if (providerError !== void 0) {
1266
1512
  lastError = providerError;
1513
+ if (providerError.kind === "context-length" && !emergencyCompactTried) {
1514
+ emergencyCompactTried = true;
1515
+ if (yield* tryEmergencyCompact()) {
1516
+ requestForStep = {
1517
+ ...baseRequest,
1518
+ messages: buildStepMessages(messages, structuredInstruction, activeRunState?.todos)
1519
+ };
1520
+ chainIdx -= 1;
1521
+ continue;
1522
+ }
1523
+ }
1267
1524
  if (!isAgentFallbackEligible(providerError, fallbackPolicy).eligible || chainIdx === fallbackChain.length - 1) {
1268
1525
  yield {
1269
1526
  type: "agent.error",
@@ -1289,10 +1546,7 @@ function createAgent(config) {
1289
1546
  type: "reasoning",
1290
1547
  text: evState.reasoningBuffer
1291
1548
  }];
1292
- stepUsage.promptTokens += providerStepUsage.promptTokens;
1293
- stepUsage.completionTokens += providerStepUsage.completionTokens;
1294
- stepUsage.totalTokens += providerStepUsage.totalTokens;
1295
- if (providerStepUsage.reasoningTokens !== void 0) stepUsage.reasoningTokens = (stepUsage.reasoningTokens ?? 0) + providerStepUsage.reasoningTokens;
1549
+ accumulateUsage(stepUsage, providerStepUsage);
1296
1550
  break;
1297
1551
  }
1298
1552
  }
@@ -1317,15 +1571,13 @@ function createAgent(config) {
1317
1571
  }
1318
1572
  usageAcc.add(lastModelId, stepUsage);
1319
1573
  addModelUsage(state, lastModelId, stepUsage);
1320
- state.usage.promptTokens += stepUsage.promptTokens;
1321
- state.usage.completionTokens += stepUsage.completionTokens;
1322
- state.usage.totalTokens += stepUsage.totalTokens;
1323
- if (stepUsage.reasoningTokens !== void 0) state.usage.reasoningTokens = (state.usage.reasoningTokens ?? 0) + stepUsage.reasoningTokens;
1574
+ accumulateUsage(state.usage, stepUsage);
1324
1575
  const leakCheck = causalityMonitor !== void 0 && textBuffer.length > 0 ? causalityMonitor.checkMessage(textBuffer) : void 0;
1325
1576
  const leakBlocked = leakCheck?.leakDetected === true && leakCheck.decision === "block";
1326
1577
  const assistant = buildAssistantMessage(leakBlocked ? LATERAL_LEAK_BLOCKED_NOTICE : textBuffer, stepReasoningParts, finalCalls, agentId, reasoningPolicy);
1327
1578
  messages.push(assistant);
1328
1579
  state.messages.push(assistant);
1580
+ if (toolDataFlowGuard !== void 0 && textBuffer.length > 0 && !leakBlocked) toolDataFlowGuard.recordAssistant(state.id, textBuffer);
1329
1581
  if (leakCheck?.leakDetected === true) {
1330
1582
  const sha = sha256Hex(textBuffer);
1331
1583
  yield {
@@ -1350,7 +1602,12 @@ function createAgent(config) {
1350
1602
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1351
1603
  usage: stepUsage,
1352
1604
  toolCalls: [],
1353
- agentId: state.currentAgentId
1605
+ agentId: state.currentAgentId,
1606
+ ...config.recordProviderResponses === true ? { providerResponse: {
1607
+ modelId: lastModelId,
1608
+ ...textBuffer.length > 0 ? { text: textBuffer } : {},
1609
+ ...finalCalls.length > 0 ? { toolCalls: [...finalCalls] } : {}
1610
+ } } : {}
1354
1611
  };
1355
1612
  state.steps.push(stepRecord);
1356
1613
  lastStepCalledToolNames = finalCalls.map((c) => c.toolName);
@@ -1365,9 +1622,11 @@ function createAgent(config) {
1365
1622
  const execRunContext = {
1366
1623
  ...runContextBase,
1367
1624
  stepNumber,
1368
- messages
1625
+ messages,
1626
+ ...currentStepSpan !== void 0 ? { span: currentStepSpan } : {}
1369
1627
  };
1370
1628
  let batch = [];
1629
+ let stepApprovalsRequested = 0;
1371
1630
  for (const call of finalCalls) {
1372
1631
  const handoff = handoffMap.get(call.toolName);
1373
1632
  if (handoff !== void 0) {
@@ -1428,7 +1687,7 @@ function createAgent(config) {
1428
1687
  toolCallId: call.toolCallId,
1429
1688
  error: toolError
1430
1689
  };
1431
- const text = `Error: ${toolError.message}`;
1690
+ const text = renderToolErrorMessage(toolError);
1432
1691
  messages.push({
1433
1692
  role: "tool",
1434
1693
  toolCallId: call.toolCallId,
@@ -1471,7 +1730,48 @@ function createAgent(config) {
1471
1730
  });
1472
1731
  continue;
1473
1732
  }
1474
- if (await invokeNeedsApproval(stepRegistry.get(call.toolName), call.args, execRunContext, signal)) {
1733
+ const resolvedTool = stepRegistry.get(call.toolName);
1734
+ let gateInput = call.args;
1735
+ if (resolvedTool !== void 0 && isApprovalGated(resolvedTool)) {
1736
+ const parsed = safeParseGatedArgs(resolvedTool, call.args);
1737
+ if (parsed !== void 0 && !parsed.success) {
1738
+ const toolError = {
1739
+ toolCallId: call.toolCallId,
1740
+ toolName: call.toolName,
1741
+ kind: "invalid_input",
1742
+ message: `Invalid arguments for approval-gated tool '${call.toolName}': ${parsed.message}`
1743
+ };
1744
+ const stepEntry = state.steps[state.steps.length - 1];
1745
+ if (stepEntry !== void 0) stepEntry.toolCalls.push({
1746
+ call,
1747
+ outcome: toolError,
1748
+ stepNumber
1749
+ });
1750
+ yield {
1751
+ type: "tool.execute.start",
1752
+ toolCallId: call.toolCallId
1753
+ };
1754
+ yield {
1755
+ type: "tool.execute.error",
1756
+ toolCallId: call.toolCallId,
1757
+ error: toolError
1758
+ };
1759
+ const text = renderToolErrorMessage(toolError);
1760
+ messages.push({
1761
+ role: "tool",
1762
+ toolCallId: call.toolCallId,
1763
+ content: text
1764
+ });
1765
+ state.messages.push({
1766
+ role: "tool",
1767
+ toolCallId: call.toolCallId,
1768
+ content: text
1769
+ });
1770
+ continue;
1771
+ }
1772
+ if (parsed !== void 0) gateInput = parsed.data;
1773
+ }
1774
+ if (await invokeNeedsApproval(resolvedTool, gateInput, execRunContext, signal)) {
1475
1775
  if (batch.length > 0) {
1476
1776
  yield* dispatchBatch(batch, stepExecutor, execRunContext, stepNumber);
1477
1777
  batch = [];
@@ -1487,39 +1787,82 @@ function createAgent(config) {
1487
1787
  requestedAt: (/* @__PURE__ */ new Date()).toISOString()
1488
1788
  };
1489
1789
  state.pendingApprovals.push(approval);
1490
- state.status = "awaiting_approval";
1491
- const taintSnap = toolDataFlowGuard?.snapshotLedger(state.id);
1492
- if (taintSnap !== void 0) state.taintSummary = taintSnap;
1493
- if (promotedDeferred.size > 0) state.promotedTools = [...promotedDeferred];
1790
+ stepApprovalsRequested += 1;
1494
1791
  yield {
1495
1792
  type: "tool.approval.requested",
1496
1793
  toolCallId: call.toolCallId
1497
1794
  };
1498
- if (config.checkpointStore !== void 0) await config.checkpointStore.put(state.id, "agent", {
1499
- id: state.id,
1500
- threadId: state.id,
1501
- namespace: "agent",
1502
- state: serializeRunState(state, { stripTracingApiKey: true }),
1503
- channelVersions: {},
1504
- stepNumber,
1505
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1506
- }, {
1507
- source: "sync",
1508
- status: "suspended",
1509
- nodeName: "agent.run"
1510
- });
1511
- return yield* finishRun(state, finalSnapshot);
1795
+ continue;
1512
1796
  }
1513
1797
  batch.push(call);
1514
1798
  }
1515
1799
  if (batch.length > 0) yield* dispatchBatch(batch, stepExecutor, execRunContext, stepNumber);
1800
+ if (stepApprovalsRequested > 0) {
1801
+ state.status = "awaiting_approval";
1802
+ const taintSnap = toolDataFlowGuard?.snapshotLedger(state.id);
1803
+ if (taintSnap !== void 0) state.taintSummary = taintSnap;
1804
+ if (promotedDeferred.size > 0) state.promotedTools = [...promotedDeferred];
1805
+ if (config.checkpointStore !== void 0) await config.checkpointStore.put(state.id, "agent", {
1806
+ id: state.id,
1807
+ threadId: state.id,
1808
+ namespace: "agent",
1809
+ state: serializeRunState(state, { stripTracingApiKey: true }),
1810
+ channelVersions: {},
1811
+ stepNumber,
1812
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1813
+ }, {
1814
+ source: "sync",
1815
+ status: "suspended",
1816
+ nodeName: "agent.run"
1817
+ });
1818
+ return yield* finishRun(state, finalSnapshot);
1819
+ }
1516
1820
  }
1821
+ currentStepSpan?.setAttributes({
1822
+ "gen_ai.usage.input_tokens": stepUsage.promptTokens,
1823
+ "gen_ai.usage.output_tokens": stepUsage.completionTokens
1824
+ });
1825
+ currentStepSpan?.end();
1826
+ currentStepSpan = void 0;
1517
1827
  yield {
1518
1828
  type: "step.end",
1519
1829
  stepNumber,
1520
1830
  usage: stepUsage
1521
1831
  };
1522
1832
  if (finalCalls.length === 0) {
1833
+ if (config.verifiers !== void 0 && config.verifiers.length > 0) {
1834
+ const feedbacks = [];
1835
+ for (const verifier of config.verifiers) {
1836
+ let result;
1837
+ try {
1838
+ result = await verifier.verify({
1839
+ output: String(finalSnapshot.output ?? ""),
1840
+ state,
1841
+ stepNumber
1842
+ });
1843
+ } catch {
1844
+ result = { ok: true };
1845
+ }
1846
+ yield {
1847
+ type: "verifier.result",
1848
+ verifierId: verifier.id,
1849
+ ok: result.ok,
1850
+ ...result.ok ? {} : { feedback: result.feedback },
1851
+ stepNumber
1852
+ };
1853
+ if (!result.ok) feedbacks.push(`[verifier:${verifier.id}] ${result.feedback}`);
1854
+ }
1855
+ if (feedbacks.length > 0 && verifierRoundsUsed < (config.maxVerifierRounds ?? 1)) {
1856
+ verifierRoundsUsed += 1;
1857
+ const feedbackMessage = {
1858
+ role: "user",
1859
+ content: `Your response failed ${feedbacks.length} verification check(s). Address the feedback and answer again:\n${feedbacks.join("\n")}`
1860
+ };
1861
+ messages.push(feedbackMessage);
1862
+ state.messages.push(feedbackMessage);
1863
+ continue;
1864
+ }
1865
+ }
1523
1866
  state.status = "completed";
1524
1867
  break;
1525
1868
  }
@@ -1541,6 +1884,15 @@ function createAgent(config) {
1541
1884
  };
1542
1885
  return yield* finishRun(state, finalSnapshot);
1543
1886
  } finally {
1887
+ currentStepSpan?.end();
1888
+ currentStepSpan = void 0;
1889
+ runSpan.setAttributes({
1890
+ "gen_ai.usage.input_tokens": state.usage.promptTokens,
1891
+ "gen_ai.usage.output_tokens": state.usage.completionTokens,
1892
+ "graphorin.run.status": state.status
1893
+ });
1894
+ runSpan.setStatus(state.status === "failed" ? "error" : "ok");
1895
+ runSpan.end();
1544
1896
  if (parentSignal !== void 0) parentSignal.removeEventListener("abort", onParentAbort);
1545
1897
  }
1546
1898
  if (state.status === "running") {
@@ -1614,10 +1966,10 @@ function createAgent(config) {
1614
1966
  }
1615
1967
  /**
1616
1968
  * Terminal wrapper around {@link finalize}: every exit path of the run
1617
- * loop completed, failed, aborted, suspended ends the stream with
1969
+ * loop - completed, failed, aborted, suspended - ends the stream with
1618
1970
  * an `agent.end` event carrying the final {@link AgentResult} (AG-20).
1619
1971
  */
1620
- async function* finishRun(state, snapshot) {
1972
+ async function* finishRunBase(state, snapshot) {
1621
1973
  while (pendingManualCompacts.length > 0) pendingManualCompacts.shift()?.resolve(noopCompactionResult("no-active-run"));
1622
1974
  const result = finalize(state, snapshot);
1623
1975
  if (result.status === "completed" || result.status === "failed") await spillWriter.clear?.(result.state.id).catch(() => {});
@@ -1692,8 +2044,26 @@ function createAgent(config) {
1692
2044
  pendingAbort = options ?? {};
1693
2045
  abortController?.abort();
1694
2046
  };
2047
+ const foldRunOutcome = (result, maxChars) => {
2048
+ const steps = result.state.steps;
2049
+ const toolNames = [...new Set(steps.flatMap((step) => step.toolCalls.map((c) => c.call.toolName)))];
2050
+ const header = `[sub-agent '${config.name}' outcome] status=${result.status}; steps=${steps.length}; toolCalls=${steps.reduce((n, st) => n + st.toolCalls.length, 0)}` + (toolNames.length > 0 ? `; tools=${toolNames.join(", ")}` : "");
2051
+ const text = typeof result.output === "string" ? result.output : JSON.stringify(result.output);
2052
+ return `${header}\n${text.length > maxChars ? `${text.slice(0, maxChars)}\n[... ${text.length - maxChars} chars truncated by contextFold]` : text}`;
2053
+ };
2054
+ const taintFromChildState = (state) => {
2055
+ const summary = state.taintSummary;
2056
+ if (summary === void 0 || !summary.untrustedSeen && !summary.sensitiveSeen) return;
2057
+ return {
2058
+ ...summary.untrustedSeen ? { untrusted: true } : {},
2059
+ ...summary.sensitiveSeen ? { sensitive: true } : {},
2060
+ sourceKind: "sub-agent"
2061
+ };
2062
+ };
1695
2063
  const toTool = (options = {}) => {
1696
2064
  const exposeTurns = options.exposeTurns ?? "final";
2065
+ const foldMaxChars = options.contextFold === void 0 || options.contextFold === false ? null : typeof options.contextFold === "object" ? options.contextFold.maxChars ?? 2e3 : 2e3;
2066
+ const propagateTaint = options.propagateTaint !== false;
1697
2067
  return {
1698
2068
  name: options.name ?? `subagent_${config.name}`,
1699
2069
  description: options.description ?? `Invoke sub-agent '${config.name}'.`,
@@ -1714,7 +2084,8 @@ function createAgent(config) {
1714
2084
  const callOpts = {
1715
2085
  ...ctx?.signal !== void 0 ? { signal: ctx.signal } : {},
1716
2086
  ...ctx?.runContext.deps !== void 0 ? { deps: ctx.runContext.deps } : {},
1717
- ...ctx?.runContext.sessionId !== void 0 ? { sessionId: ctx.runContext.sessionId } : {}
2087
+ ...ctx?.runContext.sessionId !== void 0 ? { sessionId: ctx.runContext.sessionId } : {},
2088
+ ...options.capability !== void 0 ? { capability: options.capability } : {}
1718
2089
  };
1719
2090
  const seed = options.inputFilter !== void 0 && ctx !== void 0 ? [...options.inputFilter(ctx.runContext.messages), {
1720
2091
  role: "user",
@@ -1726,12 +2097,21 @@ function createAgent(config) {
1726
2097
  for await (const ev of stream(seed, callOpts)) if (ev.type === "text.complete") turns.push(ev.text);
1727
2098
  else if (ev.type === "agent.end") endResult = ev.result;
1728
2099
  if (endResult !== void 0 && endResult.status !== "completed") throw new Error(`sub-agent '${config.name}' ${endResult.status}${endResult.error !== void 0 ? `: ${endResult.error.message}` : ""}`);
1729
- return turns.join("\n\n");
2100
+ const allOutput = foldMaxChars !== null && endResult !== void 0 ? foldRunOutcome(endResult, foldMaxChars) : turns.join("\n\n");
2101
+ const allTaint = propagateTaint && endResult !== void 0 ? taintFromChildState(endResult.state) : void 0;
2102
+ return allTaint !== void 0 ? {
2103
+ output: allOutput,
2104
+ taint: allTaint
2105
+ } : allOutput;
1730
2106
  }
1731
2107
  const result = await run(seed, callOpts);
1732
2108
  if (result.status !== "completed") throw new Error(`sub-agent '${config.name}' ${result.status}${result.error !== void 0 ? `: ${result.error.message}` : ""}`);
1733
- if (exposeTurns === "none") return "";
1734
- return result.output;
2109
+ const taint = propagateTaint ? taintFromChildState(result.state) : void 0;
2110
+ const output = exposeTurns === "none" ? "" : foldMaxChars !== null ? foldRunOutcome(result, foldMaxChars) : result.output;
2111
+ return taint !== void 0 ? {
2112
+ output,
2113
+ taint
2114
+ } : output;
1735
2115
  }
1736
2116
  };
1737
2117
  };
@@ -1847,6 +2227,35 @@ function probeSecretsAccessor() {
1847
2227
  const rejector = (_key, _options) => Promise.reject(/* @__PURE__ */ new Error("secrets.require is unavailable inside a needsApproval predicate"));
1848
2228
  return { require: rejector };
1849
2229
  }
2230
+ /**
2231
+ * tools-02: validate an approval-gated call's args at the pre-screen so
2232
+ * the gate decision - and what a human is asked to approve - is the input
2233
+ * that will actually execute. Structural + defensive: `undefined` when
2234
+ * the tool exposes no callable `safeParse` (nothing to validate here; the
2235
+ * executor still validates at dispatch); a throwing schema counts as a
2236
+ * validation failure rather than crashing the loop.
2237
+ */
2238
+ function safeParseGatedArgs(tool, args) {
2239
+ const schema = tool.inputSchema;
2240
+ const safeParse = schema?.safeParse;
2241
+ if (typeof safeParse !== "function") return void 0;
2242
+ try {
2243
+ const parsed = safeParse.call(schema, args);
2244
+ if (parsed.success === true) return {
2245
+ success: true,
2246
+ data: parsed.data
2247
+ };
2248
+ return {
2249
+ success: false,
2250
+ message: parsed.error?.message ?? "schema validation failed"
2251
+ };
2252
+ } catch (cause) {
2253
+ return {
2254
+ success: false,
2255
+ message: cause instanceof Error ? cause.message : String(cause)
2256
+ };
2257
+ }
2258
+ }
1850
2259
 
1851
2260
  //#endregion
1852
2261
  export { createAgent };