@graphorin/agent 0.6.1 → 0.7.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 (110) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +2 -2
  3. package/dist/errors/index.d.ts +17 -4
  4. package/dist/errors/index.d.ts.map +1 -1
  5. package/dist/errors/index.js +19 -3
  6. package/dist/errors/index.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +153 -1930
  9. package/dist/factory.js.map +1 -1
  10. package/dist/fanout/index.d.ts +13 -1
  11. package/dist/fanout/index.d.ts.map +1 -1
  12. package/dist/fanout/index.js +13 -4
  13. package/dist/fanout/index.js.map +1 -1
  14. package/dist/index.d.ts +6 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -7
  17. package/dist/index.js.map +1 -1
  18. package/dist/lateral-leak/index.js +1 -1
  19. package/dist/package.js +1 -1
  20. package/dist/package.js.map +1 -1
  21. package/dist/run-state/index.d.ts +32 -6
  22. package/dist/run-state/index.d.ts.map +1 -1
  23. package/dist/run-state/index.js +46 -22
  24. package/dist/run-state/index.js.map +1 -1
  25. package/dist/runtime/agent-surface.js +122 -0
  26. package/dist/runtime/agent-surface.js.map +1 -0
  27. package/dist/runtime/agent-to-tool.d.ts +51 -0
  28. package/dist/runtime/agent-to-tool.d.ts.map +1 -0
  29. package/dist/runtime/agent-to-tool.js +145 -0
  30. package/dist/runtime/agent-to-tool.js.map +1 -0
  31. package/dist/runtime/approvals.js +0 -0
  32. package/dist/runtime/approvals.js.map +1 -0
  33. package/dist/runtime/dispatch.js +108 -0
  34. package/dist/runtime/dispatch.js.map +1 -0
  35. package/dist/runtime/executor-wiring.js +128 -0
  36. package/dist/runtime/executor-wiring.js.map +1 -0
  37. package/dist/runtime/fallback-chain.js +139 -0
  38. package/dist/runtime/fallback-chain.js.map +1 -0
  39. package/dist/runtime/handoff.js +307 -0
  40. package/dist/runtime/handoff.js.map +1 -0
  41. package/dist/runtime/messages.d.ts +22 -0
  42. package/dist/runtime/messages.d.ts.map +1 -0
  43. package/dist/runtime/messages.js +204 -0
  44. package/dist/runtime/messages.js.map +1 -0
  45. package/dist/runtime/provider-events.js +117 -0
  46. package/dist/runtime/provider-events.js.map +1 -0
  47. package/dist/runtime/run-compaction.js +210 -0
  48. package/dist/runtime/run-compaction.js.map +1 -0
  49. package/dist/runtime/run-finish.js +48 -0
  50. package/dist/runtime/run-finish.js.map +1 -0
  51. package/dist/runtime/run-gates.js +336 -0
  52. package/dist/runtime/run-gates.js.map +1 -0
  53. package/dist/runtime/run-init.js +81 -0
  54. package/dist/runtime/run-init.js.map +1 -0
  55. package/dist/runtime/run-input.js +46 -0
  56. package/dist/runtime/run-input.js.map +1 -0
  57. package/dist/runtime/step-catalogue.js +173 -0
  58. package/dist/runtime/step-catalogue.js.map +1 -0
  59. package/dist/runtime/tool-call-walk.js +189 -0
  60. package/dist/runtime/tool-call-walk.js.map +1 -0
  61. package/dist/runtime/tool-wiring.js +159 -0
  62. package/dist/runtime/tool-wiring.js.map +1 -0
  63. package/dist/tooling/adapters.js +1 -1
  64. package/dist/tooling/dataflow.js +1 -1
  65. package/dist/tooling/policy.js +2 -0
  66. package/dist/tooling/policy.js.map +1 -1
  67. package/dist/types.d.ts +63 -13
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +20 -20
  70. package/src/errors/index.ts +320 -0
  71. package/src/evaluator-optimizer/index.ts +212 -0
  72. package/src/factory.ts +957 -0
  73. package/src/fallback/index.ts +108 -0
  74. package/src/fanout/index.ts +523 -0
  75. package/src/filters/index.ts +347 -0
  76. package/src/index.ts +180 -0
  77. package/src/internal/ids.ts +46 -0
  78. package/src/internal/usage-accumulator.ts +90 -0
  79. package/src/lateral-leak/causality-monitor.ts +221 -0
  80. package/src/lateral-leak/index.ts +35 -0
  81. package/src/lateral-leak/merge-guard.ts +151 -0
  82. package/src/lateral-leak/protocol-guard.ts +222 -0
  83. package/src/preferred-model/index.ts +210 -0
  84. package/src/progress/index.ts +238 -0
  85. package/src/run-state/index.ts +607 -0
  86. package/src/runtime/agent-surface.ts +218 -0
  87. package/src/runtime/agent-to-tool.ts +323 -0
  88. package/src/runtime/approvals.ts +0 -0
  89. package/src/runtime/dispatch.ts +183 -0
  90. package/src/runtime/executor-wiring.ts +331 -0
  91. package/src/runtime/fallback-chain.ts +250 -0
  92. package/src/runtime/handoff.ts +428 -0
  93. package/src/runtime/messages.ts +309 -0
  94. package/src/runtime/provider-events.ts +175 -0
  95. package/src/runtime/run-compaction.ts +288 -0
  96. package/src/runtime/run-finish.ts +93 -0
  97. package/src/runtime/run-gates.ts +419 -0
  98. package/src/runtime/run-init.ts +169 -0
  99. package/src/runtime/run-input.ts +102 -0
  100. package/src/runtime/step-catalogue.ts +338 -0
  101. package/src/runtime/tool-call-walk.ts +301 -0
  102. package/src/runtime/tool-wiring.ts +218 -0
  103. package/src/testing/replay-provider.ts +121 -0
  104. package/src/tooling/adapters.ts +403 -0
  105. package/src/tooling/catalogue.ts +36 -0
  106. package/src/tooling/dataflow.ts +171 -0
  107. package/src/tooling/plan.ts +123 -0
  108. package/src/tooling/policy.ts +67 -0
  109. package/src/tooling/registry-build.ts +191 -0
  110. package/src/types.ts +696 -0
package/dist/factory.js CHANGED
@@ -1,619 +1,28 @@
1
- import { AgentRuntimeError, ConcurrentRunError, InvalidAgentConfigError, InvalidPreferredModelError, MultipleHandoffsInStepError, ToolNotFoundError } from "./errors/index.js";
2
- import { isAgentFallbackEligible } from "./fallback/index.js";
3
- import { runFanOut } from "./fanout/index.js";
4
- import { filters } from "./filters/index.js";
1
+ import { AgentRuntimeError, ConcurrentRunError, InvalidAgentConfigError, InvalidPreferredModelError, MultipleHandoffsInStepError } from "./errors/index.js";
5
2
  import { newId } from "./internal/ids.js";
6
3
  import { InMemoryUsageAccumulator } from "./internal/usage-accumulator.js";
7
4
  import { CausalityMonitor } from "./lateral-leak/causality-monitor.js";
8
- import { resolvePreferredModel } from "./preferred-model/index.js";
9
5
  import { createProgressIO } from "./progress/index.js";
10
- import { addModelUsage, createInitialRunState, serializeRunState } from "./run-state/index.js";
11
- import { buildMemoryGuard, buildSecretResolver, buildToolTokenCounter, createExecutorEventBridge, createMemoryRegionReader } from "./tooling/adapters.js";
12
- import { orderPromotedTools } from "./tooling/catalogue.js";
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";
16
- import { buildToolRegistry } from "./tooling/registry-build.js";
17
- import { createHash } from "node:crypto";
18
- import { NOOP_LOGGER, NOOP_TRACER, isStepCount, zeroUsage } from "@graphorin/core";
19
- import { composeGuardrails } from "@graphorin/security/guardrails";
20
- import { createReadResultTool, createToolSearchTool } from "@graphorin/tools/built-in";
21
- import { createCodeExecuteTool, createCodeSearchTool, projectToolApi } from "@graphorin/tools/code-mode";
22
- import { createToolExecutor } from "@graphorin/tools/executor";
23
- import { createDefaultSpillWriter, createFileResultReader } from "@graphorin/tools/result";
24
- import { projectSchemaToJsonSchema } from "@graphorin/tools/schema";
6
+ import { addModelUsage, serializeRunState } from "./run-state/index.js";
7
+ import { accumulateUsage, applyReasoningRetention, countLeadingSystemMessages } from "./runtime/messages.js";
8
+ import { maybeAutoCompact, noopCompactionResult, tryEmergencyCompact } from "./runtime/run-compaction.js";
9
+ import { createCompactMethod, createFanOutMethod, createProgressSurface, createRunMethods } from "./runtime/agent-surface.js";
10
+ import { createToTool } from "./runtime/agent-to-tool.js";
11
+ import { HANDOFF_TOOL_PREFIX, isDescribedFilter } from "./runtime/handoff.js";
12
+ import { dispatchResumedApprovals, processResumeDirective, processSubRunResumes } from "./runtime/approvals.js";
13
+ import { dispatchToolBatch } from "./runtime/dispatch.js";
14
+ import { wireToolExecution } from "./runtime/executor-wiring.js";
15
+ import { runProviderFallbackChain } from "./runtime/fallback-chain.js";
16
+ import { createRunFinisher } from "./runtime/run-finish.js";
17
+ import { buildStructuredInstruction, commitAssistantMessage, emitCancellation, finalizeRunOutput, runVerifierGate, screenInputGuardrails } from "./runtime/run-gates.js";
18
+ import { initializeRunState, seedInitialMessages } from "./runtime/run-init.js";
19
+ import { asMessages, isModelHintLike, isModelSpecLike, validatePreferredModel } from "./runtime/run-input.js";
20
+ import { buildBaseRequest, resolveStepToolContext, toolToDefinition } from "./runtime/step-catalogue.js";
21
+ import { processStepToolCalls } from "./runtime/tool-call-walk.js";
22
+ import { NOOP_TRACER, isStepCount } from "@graphorin/core";
25
23
 
26
24
  //#region src/factory.ts
27
25
  /**
28
- * `createAgent({...})` - the agent factory entry point.
29
- *
30
- * Wires the typed `model -> tool calls -> model` loop, the streamed
31
- * event surface, the steering / followUp queues, durable HITL via
32
- * `RunState`, the multi-agent handoff layer, the agent-level model
33
- * fallback chain, and the per-tool preferred-model resolution.
34
- *
35
- * Custom adapters override behaviour by supplying alternative
36
- * `Provider` / `Memory` / `CheckpointStore` instances; the loop
37
- * never reaches into adapter internals.
38
- *
39
- * @packageDocumentation
40
- */
41
- /**
42
- * Replacement content committed in place of assistant commentary the
43
- * causality monitor blocked (AG-10). Worded to not match any built-in
44
- * denial pattern, so the notice itself never re-triggers the monitor.
45
- */
46
- const LATERAL_LEAK_BLOCKED_NOTICE = "[graphorin] assistant commentary withheld by the lateral-leak defense.";
47
- const sha256Hex = (input) => createHash("sha256").update(input, "utf8").digest("hex");
48
- const HANDOFF_TOOL_PREFIX = "transfer_to_";
49
- /** The built-in deferred-discovery tool's stable name (WI-05 / P0-3). */
50
- const TOOL_SEARCH_NAME = "tool_search";
51
- /**
52
- * Register the built-in `tool_search` into `registry` when - and only
53
- * when - the registry holds at least one deferred tool
54
- * (`defer_loading: true`). `tool_search` is itself eager (so it is
55
- * always advertised while a deferred pool exists) and resolvable by the
56
- * executor like any other tool, so a model can both *see* it in the
57
- * per-step catalogue and *call* it.
58
- *
59
- * No-op when nothing defers (zero overhead - the tool never appears) or
60
- * when a user tool already occupies the name (the user's tool wins; we
61
- * never clobber it). Because deferral is decided at registration time
62
- * (`normaliseTool`), the deferred set is fixed for the life of the
63
- * registry - this runs once per registry, not per step.
64
- */
65
- function registerToolSearch(registry, availability) {
66
- if (registry.listDeferred().length === 0) return;
67
- if (registry.get(TOOL_SEARCH_NAME) !== void 0) return;
68
- registry.register(createToolSearchTool({
69
- registry,
70
- ...availability !== void 0 ? { availability } : {}
71
- }), {
72
- kind: "built-in",
73
- subsystem: "tool-discovery"
74
- });
75
- }
76
- /** The built-in result-handle reader tool's stable name (WI-10 / P1-4). */
77
- const READ_RESULT_NAME = "read_result";
78
- /**
79
- * Register the built-in `read_result` into `registry` when at least one
80
- * registered tool opts into the `'spill-to-file'` truncation strategy
81
- * (the sole producer of spill handles today) - or when `force` is set,
82
- * which the agent passes when an operator wires external result readers
83
- * (e.g. an MCP `resource_link` reader; WI-13). The tool is eager, so it
84
- * is advertised alongside the producing tool and the model can fetch a
85
- * handle back on demand instead of inlining the full blob. No-op when
86
- * nothing produces handles (zero overhead) or when a user tool already
87
- * occupies the name (the user's tool wins).
88
- */
89
- function registerReadResult(registry, reader, opts) {
90
- if (opts?.force !== true && !registry.list().some((entry) => entry.truncationStrategy === "spill-to-file")) return;
91
- if (registry.get(READ_RESULT_NAME) !== void 0) return;
92
- registry.register(createReadResultTool({ reader }), {
93
- kind: "built-in",
94
- subsystem: "result-handle"
95
- });
96
- }
97
- /**
98
- * Compose result readers into one that tries each in order, returning
99
- * the first that resolves the handle (WI-13). The spill-file reader is
100
- * placed first so `graphorin-spill:` handles resolve locally; operator
101
- * readers (e.g. an MCP resource reader) resolve the rest. Each reader
102
- * rejects handles it does not own, so resolution falls through cleanly.
103
- */
104
- function composeResultReaders(readers) {
105
- return { async read(uri, range) {
106
- let lastError;
107
- for (const r of readers) try {
108
- return await r.read(uri, range);
109
- } catch (err) {
110
- lastError = err;
111
- }
112
- throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`No result reader resolved handle ${JSON.stringify(uri)}.`);
113
- } };
114
- }
115
- /** The code-mode meta-tools' stable names (WI-11 / P1-2). */
116
- const CODE_EXECUTE_NAME = "code_execute";
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";
120
- /**
121
- * Wire code-mode (P1-2) into `registry`: register the `code_search` /
122
- * `code_execute` meta-tools and return them as the tools to advertise in
123
- * place of the full catalogue. The model reaches every other tool through
124
- * `code_execute`, whose in-script `tools.<name>(args)` calls route back
125
- * through `quietExecutor.executeOne(...)` under the calling step's
126
- * `runContext` - so per-tool ACL / sanitization / truncation still apply,
127
- * exactly as in direct mode. `quietExecutor` carries no `streamingSink`,
128
- * so the inner calls do not interleave `tool.execute.*` events into the
129
- * outer stream.
130
- *
131
- * Excluded from the code API (`reservedNames`): the meta-tools, the
132
- * discovery / handle built-ins, handoff tools (which stay first-class
133
- * provider tools), and - supplied by the caller - any approval-gated
134
- * tool, since code-mode has no durable-HITL path to suspend mid-script.
135
- *
136
- * Returns `[]` (registering nothing) when no real tool is exposable.
137
- */
138
- function registerCodeMode(registry, quietExecutor, reservedNames, getRunCapability) {
139
- if (registry.get(CODE_EXECUTE_NAME) !== void 0) return [];
140
- const codeTools = registry.list().filter((entry) => !reservedNames.has(entry.name) && !isApprovalGated(entry));
141
- if (codeTools.length === 0) return [];
142
- const approvalGatedTools = registry.list().filter((entry) => !reservedNames.has(entry.name) && isApprovalGated(entry)).map((entry) => entry.name);
143
- const approvalGatedSet = new Set(approvalGatedTools);
144
- const allowedTools = [...codeTools.map((entry) => entry.name), ...approvalGatedTools];
145
- const allowedSet = new Set(codeTools.map((entry) => entry.name));
146
- const projection = projectToolApi(codeTools.filter((entry) => entry.__effectiveDeferLoading !== true));
147
- const executeTool = async (call, ctx) => {
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?.();
150
- const { outcome } = await quietExecutor.executeOne({
151
- call: {
152
- toolCallId: newId("codecall"),
153
- toolName: call.name,
154
- args: call.args
155
- },
156
- runContext: ctx.runContext,
157
- stepNumber: ctx.runContext.stepNumber,
158
- ...runCapability !== void 0 ? { capability: runCapability } : {}
159
- });
160
- if ("kind" in outcome) throw new Error(`${call.name}: ${outcome.message}`);
161
- return outcome.output;
162
- };
163
- const codeSearch = createCodeSearchTool({
164
- projection,
165
- approvalGatedTools,
166
- searchDeferred: async (query, k) => (await registry.searchDeferred(query, k)).filter((match) => allowedSet.has(match.name))
167
- });
168
- const codeExecute = createCodeExecuteTool({
169
- projection,
170
- allowedTools,
171
- executeTool,
172
- approvalGatedTools
173
- });
174
- registry.register(codeSearch, {
175
- kind: "built-in",
176
- subsystem: "code-mode"
177
- });
178
- registry.register(codeExecute, {
179
- kind: "built-in",
180
- subsystem: "code-mode"
181
- });
182
- return [codeSearch, codeExecute];
183
- }
184
- /**
185
- * Fold a completed `tool_search` result into the per-run promotion set:
186
- * every matched tool name becomes advertised (and thus callable) on the
187
- * next step. Tolerant of unexpected shapes (e.g. a user-shadowed
188
- * `tool_search`) - only string `name`s inside a `matches` array promote.
189
- */
190
- function recordToolSearchPromotions(output, promoted) {
191
- if (typeof output !== "object" || output === null) return;
192
- const matches = output.matches;
193
- if (!Array.isArray(matches)) return;
194
- for (const match of matches) {
195
- const name = match?.name;
196
- if (typeof name === "string") promoted.add(name);
197
- }
198
- }
199
- function isModelHintLike(value) {
200
- return value === "fast" || value === "balanced" || value === "smart";
201
- }
202
- function isModelSpecLike(value) {
203
- if (typeof value !== "object" || value === null) return false;
204
- const v = value;
205
- if (typeof v.modelId === "string" && typeof v.name === "string") return true;
206
- if (typeof v.provider === "object" && v.provider !== null && typeof v.model === "string") return true;
207
- return false;
208
- }
209
- function validatePreferredModel(value) {
210
- if (value === void 0) return;
211
- if (isModelHintLike(value)) return;
212
- if (isModelSpecLike(value)) return;
213
- throw new InvalidPreferredModelError(value);
214
- }
215
- function isMessageObject(value) {
216
- if (typeof value !== "object" || value === null) return false;
217
- const role = value.role;
218
- return role === "system" || role === "user" || role === "assistant" || role === "tool";
219
- }
220
- function isRunStateObject(value) {
221
- if (typeof value !== "object" || value === null) return false;
222
- const v = value;
223
- return typeof v.id === "string" && typeof v.agentId === "string" && Array.isArray(v.messages) && Array.isArray(v.steps);
224
- }
225
- function asMessages(input) {
226
- if (typeof input === "string") return { seed: [{
227
- role: "user",
228
- content: input
229
- }] };
230
- if (Array.isArray(input)) return { seed: [...input] };
231
- if (isMessageObject(input)) return { seed: [input] };
232
- if (isRunStateObject(input)) return {
233
- seed: [],
234
- resumed: input
235
- };
236
- throw new InvalidAgentConfigError(`unrecognized AgentInput shape`);
237
- }
238
- /**
239
- * Strip a single Markdown code fence around a JSON payload (AG-3).
240
- * Models often wrap structured output in ```json fences even when told
241
- * not to. ReDoS-safe: the info string is matched with `[^\n]*`.
242
- */
243
- function stripJsonFence(text) {
244
- const trimmed = text.trim();
245
- return /^```[^\n]*\n([\s\S]*?)\n?```$/.exec(trimmed)?.[1] ?? trimmed;
246
- }
247
- /**
248
- * The AG-3 fallback instruction: one trailing system message that
249
- * pins JSON-only output and embeds the wire schema / description.
250
- * This is the documented structured-output contract for adapters that
251
- * do not yet consume `ProviderRequest.outputType` natively (PS-24).
252
- */
253
- function buildStructuredInstruction(spec) {
254
- const parts = ["Respond with a single valid JSON value only - no prose, no Markdown code fences."];
255
- if (spec.description !== void 0 && spec.description.length > 0) parts.push(spec.description);
256
- if (spec.jsonSchema !== void 0) parts.push(`The JSON MUST conform to this JSON Schema:\n${JSON.stringify(spec.jsonSchema)}`);
257
- return parts.join("\n");
258
- }
259
- /** Most-recent user-role text in `messages` (for context-engine auto-recall). */
260
- function lastUserText(messages) {
261
- for (let i = messages.length - 1; i >= 0; i--) {
262
- const m = messages[i];
263
- if (m?.role !== "user") continue;
264
- if (typeof m.content === "string") return m.content;
265
- const text = m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
266
- return text.length > 0 ? text : void 0;
267
- }
268
- }
269
- /**
270
- * AG-21: classify a **thrown** provider error into a {@link ProviderErrorKind}
271
- * so the fallback chain can act on it, instead of flattening every exception to
272
- * `'unknown'` (which is always fallback-ineligible). Structural - reads the
273
- * `kind` carried by `@graphorin/provider`'s `GraphorinProviderError` subclasses
274
- * without importing them, keeping the agent decoupled from the provider package.
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
- ]);
286
- function classifyThrownProviderErrorKind(cause) {
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
- }
293
- }
294
- return "unknown";
295
- }
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
- } });
314
- }
315
- function toolToDefinition(tool) {
316
- const ts = tool;
317
- const inputSchema = projectSchema(ts.inputSchema, tool.name, "input") ?? {};
318
- const outputSchema = projectSchema(ts.outputSchema, tool.name, "output");
319
- const examples = renderToolExamples(tool);
320
- return {
321
- name: tool.name,
322
- description: tool.description,
323
- inputSchema,
324
- ...outputSchema !== void 0 ? { outputSchema } : {},
325
- ...examples !== void 0 ? { examples } : {}
326
- };
327
- }
328
- /**
329
- * Project a tool's worked `examples` onto the provider wire contract
330
- * (WI-06 / P2-3). Examples are rendered only when the tool eagerly
331
- * renders them: the registry resolves the `defer_loading` auto-rule into
332
- * `examplesEagerlyRendered`, so a deferred tool resolves to `false` and is
333
- * skipped (its examples stay out of context even once `tool_search`
334
- * promotes it). `undefined` - the "runtime decides" case for a plain
335
- * eager tool - renders, since the tool is already advertised this step.
336
- *
337
- * Bounded to ≤5 to honour the `ToolExample` `[1,5]` contract even when a
338
- * tool slipped past the registry's overflow WARN (which does not truncate).
339
- */
340
- function renderToolExamples(tool) {
341
- const examples = tool.examples;
342
- if (examples === void 0 || examples.length === 0) return void 0;
343
- if (tool.examplesEagerlyRendered === false) return void 0;
344
- return examples.slice(0, 5).map((ex) => ({
345
- input: ex.input,
346
- output: ex.output,
347
- ...ex.comment !== void 0 ? { comment: ex.comment } : {}
348
- }));
349
- }
350
- const PASSTHROUGH_SCHEMA = {
351
- parse: (value) => value,
352
- safeParse: (value) => ({
353
- success: true,
354
- data: value
355
- }),
356
- toJSON: () => ({ type: "object" })
357
- };
358
- function isDescribedFilter(value) {
359
- return typeof value === "function" && "descriptor" in value && typeof value.descriptor === "object";
360
- }
361
- function buildHandoffTool(target) {
362
- const cfg = target.config;
363
- return {
364
- name: `${HANDOFF_TOOL_PREFIX}${cfg.name}`,
365
- description: `Hand off control to agent '${cfg.name}'.`,
366
- inputSchema: PASSTHROUGH_SCHEMA,
367
- sideEffectClass: "pure",
368
- async execute() {
369
- return `[handoff: ${cfg.name}]`;
370
- }
371
- };
372
- }
373
- function specToProvider(spec) {
374
- if ("provider" in spec) return spec.provider;
375
- return spec;
376
- }
377
- function handleProviderEvent(ev, state) {
378
- switch (ev.type) {
379
- case "stream-start": return {};
380
- case "reasoning-delta":
381
- state.reasoningBuffer += ev.delta;
382
- return { emit: {
383
- type: "reasoning.delta",
384
- delta: ev.delta
385
- } };
386
- case "text-delta":
387
- state.textBuffer += ev.delta;
388
- return { emit: {
389
- type: "text.delta",
390
- delta: ev.delta
391
- } };
392
- case "tool-call-start":
393
- state.calls.set(ev.toolCallId, {
394
- toolCallId: ev.toolCallId,
395
- toolName: ev.toolName,
396
- argsBuffer: ""
397
- });
398
- return { emit: {
399
- type: "tool.call.start",
400
- toolCallId: ev.toolCallId,
401
- toolName: ev.toolName,
402
- args: void 0
403
- } };
404
- case "tool-call-input-delta": {
405
- const acc = state.calls.get(ev.toolCallId);
406
- if (acc !== void 0) acc.argsBuffer += ev.argsDelta;
407
- return { emit: {
408
- type: "tool.call.delta",
409
- toolCallId: ev.toolCallId,
410
- argsDelta: ev.argsDelta
411
- } };
412
- }
413
- case "tool-call-end": {
414
- const acc = state.calls.get(ev.toolCallId);
415
- if (acc === void 0) {
416
- process.stderr.write(`[graphorin/agent] dropped tool-call-end '${ev.toolCallId}' with no matching tool-call-start.\n`);
417
- return {};
418
- }
419
- state.finalCalls.push({
420
- toolCallId: ev.toolCallId,
421
- toolName: acc.toolName,
422
- args: ev.finalArgs
423
- });
424
- return { emit: {
425
- type: "tool.call.end",
426
- toolCallId: ev.toolCallId,
427
- finalArgs: ev.finalArgs
428
- } };
429
- }
430
- case "file": return { emit: {
431
- type: "file.generated",
432
- mimeType: ev.mimeType,
433
- data: ev.data
434
- } };
435
- case "source": return { emit: {
436
- type: "source.cited",
437
- uri: ev.uri,
438
- ...ev.title !== void 0 ? { title: ev.title } : {}
439
- } };
440
- case "finish": return {
441
- usage: ev.usage,
442
- finished: true
443
- };
444
- case "error": return { providerError: ev.error };
445
- default: return {};
446
- }
447
- }
448
- /**
449
- * Resolve the effective {@link ReasoningRetention} for a step. The
450
- * agent-level setting wins over the provider-level default; when
451
- * neither is supplied, the provider's `reasoningContract`
452
- * capability drives the default per RB-42 / suggested DEC-158.
453
- */
454
- function effectiveReasoningRetention(agentOverride, provider) {
455
- if (agentOverride !== void 0) return agentOverride;
456
- switch (provider.capabilities.reasoningContract) {
457
- case "round-trip-required": return "pass-through-claude";
458
- case "optional": return "pass-through-all";
459
- case "hidden": return "strip";
460
- default: return "strip";
461
- }
462
- }
463
- /**
464
- * Build the assistant message that the runtime appends to the
465
- * message buffer after a successful provider call. When the
466
- * effective {@link ReasoningRetention} is not `'strip'`, the
467
- * assembled `reasoning` content parts ride along on `content` so
468
- * the next provider call honours the wire-correct round-trip
469
- * contract per RB-42.
470
- */
471
- function buildAssistantMessage(text, reasoningParts, toolCalls, agentId, retention) {
472
- if (retention !== "strip" && reasoningParts.length > 0) {
473
- const parts = [...reasoningParts];
474
- if (text.length > 0) parts.push({
475
- type: "text",
476
- text
477
- });
478
- return {
479
- role: "assistant",
480
- content: parts,
481
- ...toolCalls.length > 0 ? { toolCalls } : {},
482
- agentId
483
- };
484
- }
485
- return {
486
- role: "assistant",
487
- content: text,
488
- ...toolCalls.length > 0 ? { toolCalls } : {},
489
- agentId
490
- };
491
- }
492
- /**
493
- * Strip every {@link ReasoningContent} part from each message in
494
- * the supplied list. Used at the swap point when `prepareStep`
495
- * downgrades the provider's `reasoningContract` mid-run.
496
- */
497
- function stripReasoningFromMessages(messages) {
498
- let stripped = 0;
499
- for (let i = 0; i < messages.length; i++) {
500
- const msg = messages[i];
501
- if (msg === void 0) continue;
502
- if (msg.role === "system" || msg.role === "tool") continue;
503
- if (typeof msg.content === "string") continue;
504
- const filtered = msg.content.filter((p) => p.type !== "reasoning");
505
- if (filtered.length === msg.content.length) continue;
506
- stripped += msg.content.length - filtered.length;
507
- if (msg.role === "assistant") messages[i] = {
508
- ...msg,
509
- content: filtered
510
- };
511
- else messages[i] = {
512
- ...msg,
513
- content: filtered
514
- };
515
- }
516
- return { stripped };
517
- }
518
- /**
519
- * Count the leading contiguous run of `system` messages in the initial
520
- * buffer - the trusted, KV-cache-stable instruction prefix. Captured
521
- * once at run start (WI-09 / P1-1): auto-compaction summarises only the
522
- * messages after this prefix, so the prefix stays byte-identical across
523
- * every step (the provider's cache breakpoint is real) and a long run
524
- * never re-pays for the system prompt.
525
- *
526
- * The length is fixed for the run rather than re-derived per compaction
527
- * on purpose: each compaction inserts its summary as a `system` message
528
- * right after the prefix, so re-scanning the leading run would absorb
529
- * that summary into the prefix and shield it from the next compaction -
530
- * summaries would stack unbounded. Pinning the original length keeps
531
- * each prior summary inside the compactable body, where the next pass
532
- * folds it into a fresh summary-of-summary.
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";
544
- function countLeadingSystemMessages(messages) {
545
- let i = 0;
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
- }
552
- return i;
553
- }
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
- /**
617
26
  * Build a fresh {@link Agent} from the supplied configuration.
618
27
  *
619
28
  * @stable
@@ -641,10 +50,12 @@ function createAgent(config) {
641
50
  const subAgent = isWrappedHandoff ? entry.target : entry;
642
51
  const userFilter = isWrappedHandoff ? entry.inputFilter : void 0;
643
52
  const filter = isDescribedFilter(userFilter) ? userFilter : void 0;
53
+ const forwardEvents = isWrappedHandoff ? entry.forwardEvents : void 0;
644
54
  const toolName = `${HANDOFF_TOOL_PREFIX}${subAgent.config.name}`;
645
55
  handoffMap.set(toolName, {
646
56
  agent: subAgent,
647
- filter
57
+ filter,
58
+ forwardEvents
648
59
  });
649
60
  }
650
61
  let pendingSteer = [];
@@ -657,91 +68,18 @@ function createAgent(config) {
657
68
  let runInFlight = false;
658
69
  const externalEventQueue = [];
659
70
  const pendingManualCompacts = [];
660
- const noopCompactionResult = (skippedReason) => ({
661
- beforeTokens: 0,
662
- afterTokens: 0,
663
- summaryTokens: 0,
664
- durationMs: 0,
665
- hooksFiredCount: 0,
666
- summary: "",
667
- applied: false,
668
- skippedReason
669
- });
670
71
  const memory = config.memory;
671
72
  const progressIO = createProgressIO({ ...config.sensitivity !== void 0 ? { defaultSensitivity: config.sensitivity } : {} });
672
- const toolRegistry = buildToolRegistry({
673
- ...config.tools !== void 0 ? { tools: config.tools } : {},
674
- ...config.skills !== void 0 ? { skills: config.skills } : {}
675
- }).registry;
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
- });
683
- const spillWriter = createDefaultSpillWriter();
684
- const fileResultReader = createFileResultReader({ artifactRoot: spillWriter.artifactRoot });
685
- const externalResultReaders = config.resultReaders ?? [];
686
- const resultReader = externalResultReaders.length === 0 ? fileResultReader : composeResultReaders([fileResultReader, ...externalResultReaders]);
687
- registerReadResult(toolRegistry, resultReader, { force: externalResultReaders.length > 0 });
688
- let activeExecutorBridge;
689
- const toolApprovalGate = { request: async () => ({ granted: true }) };
690
- const toolSecretResolver = buildSecretResolver();
691
- const toolTokenCounter = buildToolTokenCounter();
692
- const memoryGuardWiring = buildMemoryGuard(memory, memory === void 0 ? {} : { regionReader: createMemoryRegionReader(["working"], async (region) => {
693
- if (region !== "working") return "";
694
- const scope = {
695
- userId: activeRunState?.userId ?? agentId,
696
- ...activeRunState?.sessionId !== void 0 ? { sessionId: activeRunState.sessionId } : {},
697
- agentId
698
- };
699
- const working = memory.working;
700
- if (working?.compile === void 0) return "";
701
- try {
702
- return await working.compile(scope, agentId);
703
- } catch {
704
- return "";
705
- }
706
- }) });
707
- const toolMemoryGuardFactory = memoryGuardWiring.memoryGuardFactory;
708
- if (memory === void 0) {
709
- const guarded = (config.tools ?? []).filter((t) => t.memoryGuardTier !== void 0);
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.`);
711
- }
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);
714
- const toolStreamingSink = (event) => activeExecutorBridge?.sink(event);
715
- const makeToolExecutor = (registry, opts) => createToolExecutor({
716
- registry,
717
- approvalGate: toolApprovalGate,
718
- secretResolver: toolSecretResolver,
719
- tokenCounter: toolTokenCounter,
720
- memoryGuardFactory: toolMemoryGuardFactory,
721
- ...memoryGuardWiring.memoryRegionReader !== void 0 ? { memoryRegionReader: memoryGuardWiring.memoryRegionReader } : {},
722
- spill: spillWriter,
723
- ...toolDataFlowGuard !== void 0 ? { dataFlowGuard: toolDataFlowGuard } : {},
724
- ...toolArgumentPolicyGuard !== void 0 ? { argumentPolicy: toolArgumentPolicyGuard } : {},
725
- ...opts?.quiet === true ? {} : { streamingSink: toolStreamingSink },
726
- ...config.maxParallelTools !== void 0 ? { maxParallelTools: config.maxParallelTools } : {},
727
- ...config.toolRetry !== void 0 ? { retry: config.toolRetry } : {}
73
+ const executorBridgeSlot = { current: void 0 };
74
+ const { toolRegistry, spillWriter, resultReader, makeToolExecutor, toolExecutor, toolDataFlowGuard, ruleOfTwoCapabilityFloor, isCodeMode, codeModeAdvertised } = wireToolExecution({
75
+ config,
76
+ memory,
77
+ agentId,
78
+ handoffToolNames: handoffMap.keys(),
79
+ getActiveRunState: () => activeRunState,
80
+ getActiveRunCapability: () => activeRunCapability,
81
+ executorBridgeSlot
728
82
  });
729
- const toolExecutor = makeToolExecutor(toolRegistry);
730
- const isCodeMode = config.toolInvocation === "code-mode";
731
- let codeModeAdvertised = [];
732
- if (isCodeMode) {
733
- const reserved = new Set([
734
- CODE_EXECUTE_NAME,
735
- CODE_SEARCH_NAME,
736
- TOOL_SEARCH_NAME,
737
- READ_RESULT_NAME,
738
- ...handoffMap.keys()
739
- ]);
740
- const metas = registerCodeMode(toolRegistry, makeToolExecutor(toolRegistry, { quiet: true }), reserved, () => activeRunCapability);
741
- registerReadResult(toolRegistry, resultReader);
742
- const readResult = toolRegistry.get(READ_RESULT_NAME);
743
- codeModeAdvertised = [...metas, ...readResult !== void 0 ? [readResult] : []];
744
- }
745
83
  const causalityMonitor = config.causalityMonitor ? new CausalityMonitor(config.causalityMonitor) : void 0;
746
84
  /**
747
85
  * AG-11: one in-flight run per Agent instance. `steer` / `followUp` /
@@ -783,17 +121,14 @@ function createAgent(config) {
783
121
  if (parentSignal !== void 0) if (parentSignal.aborted) localCtl.abort();
784
122
  else parentSignal.addEventListener("abort", onParentAbort);
785
123
  const usageAcc = new InMemoryUsageAccumulator();
786
- const state = resumed ? resumed : createInitialRunState({
787
- id: newId("run"),
124
+ const { state, promotedDeferred, runStartPromotions } = initializeRunState({
125
+ config,
788
126
  agentId,
789
127
  sessionId,
790
- ...userId !== void 0 ? { userId } : {}
791
- });
128
+ userId,
129
+ toolDataFlowGuard
130
+ }, resumed);
792
131
  activeRunState = state;
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
132
  async function* finishRun(s, snapshot) {
798
133
  const taintSnap = toolDataFlowGuard?.snapshotLedger(s.id);
799
134
  if (taintSnap !== void 0) s.taintSummary = taintSnap;
@@ -801,49 +136,22 @@ function createAgent(config) {
801
136
  return yield* finishRunBase(s, snapshot);
802
137
  }
803
138
  const messages = resumed ? [...state.messages] : [];
804
- if (!resumed) {
805
- const instructionsRaw = config.instructions;
806
- let instructionsText;
807
- if (typeof instructionsRaw === "string") instructionsText = instructionsRaw;
808
- else instructionsText = await instructionsRaw({
809
- runId: state.id,
810
- sessionId,
811
- ...userId !== void 0 ? { userId } : {},
812
- agentId,
813
- deps: options.deps ?? config.deps,
814
- tracer,
815
- signal,
816
- usage: usageAcc,
817
- stepNumber: 0,
818
- messages,
819
- state
820
- });
821
- let systemPrompt = instructionsText;
822
- if (config.autoAssembleContext === true && memory !== void 0) {
823
- const lastUser = lastUserText(seed);
824
- systemPrompt = (await memory.contextEngine.assemble(memory, {
825
- scope: {
826
- userId: userId ?? agentId,
827
- sessionId,
828
- agentId
829
- },
830
- agentId,
831
- sessionId,
832
- runId: state.id,
833
- ...instructionsText.length > 0 ? { agentInstructions: instructionsText } : {},
834
- ...lastUser !== void 0 ? { lastUserMessage: lastUser } : {}
835
- })).systemMessage.content;
836
- }
837
- if (systemPrompt.length > 0) messages.push({
838
- role: "system",
839
- content: systemPrompt
840
- });
841
- messages.push(...seed);
842
- for (const m of messages) state.messages.push(m);
843
- }
139
+ if (!resumed) await seedInitialMessages({
140
+ config,
141
+ options,
142
+ memory,
143
+ agentId,
144
+ sessionId,
145
+ userId,
146
+ tracer,
147
+ signal,
148
+ usageAcc,
149
+ state
150
+ }, messages, seed);
844
151
  const finalSnapshot = { output: "" };
845
152
  const runSpan = tracer.startSpan({
846
153
  type: "agent.run",
154
+ ...options.parentSpan !== void 0 ? { parent: options.parentSpan } : {},
847
155
  attrs: {
848
156
  "gen_ai.operation.name": "invoke_agent",
849
157
  "gen_ai.agent.id": agentId,
@@ -860,94 +168,22 @@ function createAgent(config) {
860
168
  agentId
861
169
  };
862
170
  const inputGuards = config.guardrails?.input;
863
- if (!resumed && inputGuards !== void 0 && inputGuards.length > 0) for (let i = 0; i < messages.length; i++) {
864
- const msg = messages[i];
865
- if (msg === void 0 || msg.role !== "user" || typeof msg.content !== "string") continue;
866
- const composed = await composeGuardrails(inputGuards, msg.content, {
867
- stage: "input",
868
- runId: state.id,
171
+ if (!resumed && inputGuards !== void 0 && inputGuards.length > 0) {
172
+ if (yield* screenInputGuardrails({
173
+ state,
174
+ messages,
869
175
  sessionId,
870
176
  agentId
871
- });
872
- if (!composed.ok) {
873
- yield {
874
- type: "guardrail.tripped",
875
- guardrailName: composed.name,
876
- phase: "input",
877
- reason: composed.message
878
- };
879
- const message = `input guardrail '${composed.name}' blocked the run: ${composed.message}`;
880
- yield {
881
- type: "agent.error",
882
- error: {
883
- message,
884
- code: "guardrail-blocked"
885
- }
886
- };
887
- state.status = "failed";
888
- state.error = {
889
- message,
890
- code: "guardrail-blocked"
891
- };
892
- return yield* finishRun(state, finalSnapshot);
893
- }
894
- if (composed.value !== msg.content) {
895
- const rewritten = {
896
- ...msg,
897
- content: composed.value
898
- };
899
- const stateIdx = state.messages.indexOf(msg);
900
- messages[i] = rewritten;
901
- if (stateIdx !== -1) state.messages[stateIdx] = rewritten;
902
- }
177
+ }, inputGuards)) return yield* finishRun(state, finalSnapshot);
903
178
  }
904
179
  const structuredInstruction = config.outputType?.kind === "structured" ? buildStructuredInstruction(config.outputType) : void 0;
905
180
  const resumedApprovedCalls = [];
906
181
  const grantedApprovals = [];
907
- const journaledCallIds = /* @__PURE__ */ new Set();
908
- for (const step of state.steps) for (const completed of step.toolCalls) journaledCallIds.add(completed.call.toolCallId);
909
- if (resumed && options.directive?.approvals !== void 0 && state.pendingApprovals.length > 0) {
910
- const decisions = new Map(options.directive.approvals.map((d) => [d.toolCallId, d]));
911
- const stillPending = [];
912
- for (const approval of state.pendingApprovals) {
913
- const decision = decisions.get(approval.toolCallId);
914
- if (decision === void 0) {
915
- stillPending.push(approval);
916
- continue;
917
- }
918
- if (decision.granted) {
919
- yield {
920
- type: "tool.approval.granted",
921
- toolCallId: approval.toolCallId
922
- };
923
- if (journaledCallIds.has(approval.toolCallId) && messages.some((m) => m.role === "tool" && m.toolCallId === approval.toolCallId)) continue;
924
- resumedApprovedCalls.push({
925
- toolCallId: approval.toolCallId,
926
- toolName: approval.toolName,
927
- args: approval.args
928
- });
929
- grantedApprovals.push(approval);
930
- } else {
931
- yield {
932
- type: "tool.approval.denied",
933
- toolCallId: approval.toolCallId,
934
- ...decision.reason !== void 0 ? { reason: decision.reason } : {}
935
- };
936
- messages.push({
937
- role: "tool",
938
- toolCallId: approval.toolCallId,
939
- content: `Error: tool approval denied${decision.reason ? `: ${decision.reason}` : ""}`
940
- });
941
- state.messages.push({
942
- role: "tool",
943
- toolCallId: approval.toolCallId,
944
- content: `Error: tool approval denied${decision.reason ? `: ${decision.reason}` : ""}`
945
- });
946
- }
947
- }
948
- state.pendingApprovals.splice(0, state.pendingApprovals.length, ...stillPending);
949
- if (stillPending.length === 0) state.status = "running";
950
- }
182
+ const subRunDecisions = /* @__PURE__ */ new Map();
183
+ if (resumed && options.directive?.approvals !== void 0 && state.pendingApprovals.length > 0) yield* processResumeDirective({
184
+ state,
185
+ messages
186
+ }, options.directive.approvals, resumedApprovedCalls, grantedApprovals, subRunDecisions);
951
187
  const systemPrefixLength = countLeadingSystemMessages(messages);
952
188
  const runContextBase = {
953
189
  runId: state.id,
@@ -962,378 +198,50 @@ function createAgent(config) {
962
198
  messages,
963
199
  state
964
200
  };
201
+ const runEnv = {
202
+ config,
203
+ options,
204
+ memory,
205
+ state,
206
+ messages,
207
+ sessionId,
208
+ agentId,
209
+ userId,
210
+ signal,
211
+ usageAcc,
212
+ stopWhen,
213
+ fallbackPolicy,
214
+ structuredInstruction,
215
+ systemPrefixLength,
216
+ runContextBase,
217
+ handoffMap,
218
+ handoffNames: Array.from(handoffMap.keys()),
219
+ isCodeMode,
220
+ toolRegistry,
221
+ toolExecutor,
222
+ makeToolExecutor,
223
+ resultReader,
224
+ codeModeAdvertised,
225
+ causalityMonitor,
226
+ toolDataFlowGuard,
227
+ promotedDeferred,
228
+ runStartPromotions,
229
+ activeRunCapability,
230
+ executorBridgeSlot,
231
+ pendingManualCompacts,
232
+ getPendingAbort: () => pendingAbort,
233
+ getCurrentStepSpan: () => currentStepSpan,
234
+ getActiveTodos: () => activeRunState?.todos,
235
+ tryEmergencyCompact: () => tryEmergencyCompact(runEnv),
236
+ dispatchBatch: (calls, executor, runContext, stepNum, dispatchOpts) => dispatchToolBatch(runEnv, calls, executor, runContext, stepNum, dispatchOpts)
237
+ };
965
238
  if (resumed && state.status === "failed") return yield* finishRun(state, finalSnapshot);
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
- }
988
- state.steps.push({
989
- stepNumber: 0,
990
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
991
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
992
- usage: zeroUsage(),
993
- toolCalls: [],
994
- agentId: state.currentAgentId
995
- });
996
- yield* dispatchBatch(resumedApprovedCalls, toolExecutor, {
997
- ...runContextBase,
998
- stepNumber: 0,
999
- messages
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
- });
1014
- }
1015
- if (resumed && state.status === "awaiting_approval") return yield* finishRun(state, finalSnapshot);
1016
- /**
1017
- * Dispatch a batch of (non-handoff) tool calls through the
1018
- * {@link ToolExecutor} and surface the results as `AgentEvent`s.
1019
- *
1020
- * The agent owns the `tool.execute.start` / `.end` / `.error`
1021
- * lifecycle (derived deterministically from the returned
1022
- * {@link CompletedToolCall} outcomes) so every outcome kind -
1023
- * success, unknown-tool, invalid-input, sanitization-blocked,
1024
- * execution error - yields a consistent event and tool message,
1025
- * preserving the pre-WI-03 stream shape (R10). The executor's
1026
- * genuinely-live streaming events (`tool.execute.progress` /
1027
- * `.partial`, emitted only by streaming-hint tools) are bridged
1028
- * through Adapter E while the batch runs and are purely additive.
1029
- *
1030
- * Parallelism (WI-04): the executor runs independent calls in this
1031
- * batch concurrently, bounded by `maxParallelTools`. `tool.execute.start`
1032
- * is emitted up-front in call order and `.end` / `.error` after the
1033
- * batch settles, also in call order - so result mapping and tool-message
1034
- * history are deterministic regardless of which call finishes first,
1035
- * while `.progress` / `.partial` events for concurrent calls interleave
1036
- * (keyed by `toolCallId`). Tools declaring `executionMode: 'sequential'`
1037
- * are serialised by the executor and never overlap.
1038
- */
1039
- async function* dispatchBatch(calls, executor, runContext, stepNum, dispatchOpts = {}) {
1040
- if (calls.length === 0) return;
1041
- for (const call of calls) yield {
1042
- type: "tool.execute.start",
1043
- toolCallId: call.toolCallId
1044
- };
1045
- const bridge = createExecutorEventBridge();
1046
- activeExecutorBridge = bridge;
1047
- const resultsPromise = executor.executeBatch({
1048
- calls,
1049
- runContext,
1050
- stepNumber: stepNum,
1051
- ...dispatchOpts.disableRepair !== void 0 ? { disableRepair: dispatchOpts.disableRepair } : {},
1052
- ...activeRunCapability !== void 0 ? { capability: activeRunCapability } : {}
1053
- });
1054
- const closeOnSettle = resultsPromise.then(() => bridge.close(), () => bridge.close());
1055
- for await (const event of bridge.drain()) if (event.type === "tool.execute.progress" || event.type === "tool.execute.partial") yield event;
1056
- await closeOnSettle;
1057
- activeExecutorBridge = void 0;
1058
- const completed = await resultsPromise;
1059
- const byCallId = new Map(completed.map((c) => [c.outcome.toolCallId, c]));
1060
- const stepEntry = state.steps[state.steps.length - 1];
1061
- for (const call of calls) {
1062
- const result = byCallId.get(call.toolCallId);
1063
- if (result === void 0) continue;
1064
- if (stepEntry !== void 0) stepEntry.toolCalls.push(result);
1065
- const outcome = result.outcome;
1066
- if ("kind" in outcome) {
1067
- yield {
1068
- type: "tool.execute.error",
1069
- toolCallId: call.toolCallId,
1070
- error: outcome
1071
- };
1072
- const text = renderToolErrorMessage(outcome);
1073
- messages.push({
1074
- role: "tool",
1075
- toolCallId: call.toolCallId,
1076
- content: text
1077
- });
1078
- state.messages.push({
1079
- role: "tool",
1080
- toolCallId: call.toolCallId,
1081
- content: text
1082
- });
1083
- causalityMonitor?.recordCall(`tool.error:${call.toolName}`);
1084
- } else {
1085
- const output = outcome.output;
1086
- yield {
1087
- type: "tool.execute.end",
1088
- toolCallId: call.toolCallId,
1089
- result: output,
1090
- durationMs: outcome.durationMs
1091
- };
1092
- const handle = outcome.resultHandle;
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;
1095
- messages.push({
1096
- role: "tool",
1097
- toolCallId: call.toolCallId,
1098
- content: text
1099
- });
1100
- state.messages.push({
1101
- role: "tool",
1102
- toolCallId: call.toolCallId,
1103
- content: text
1104
- });
1105
- causalityMonitor?.recordCall(`tool:${call.toolName}`);
1106
- if (call.toolName === TOOL_SEARCH_NAME) recordToolSearchPromotions(output, promotedDeferred);
1107
- }
1108
- }
1109
- }
1110
- /**
1111
- * Auto-compaction trigger (WI-09 / P1-1). Before assembling each
1112
- * provider request, ask the memory {@link ContextEngine} whether the
1113
- * in-flight buffer has crossed its per-provider threshold
1114
- * (`shouldCompact`); when it has, summarise the older turns
1115
- * (`compactNow`, `source: 'auto-trigger'`), splice the result back in
1116
- * - preserving the byte-stable system prefix and the most-recent
1117
- * turns verbatim - and emit `context.compacted`. The compaction is
1118
- * configured on the memory facade (`createMemory({ contextEngine })`,
1119
- * RB-46); there is no parallel agent-level knob.
1120
- *
1121
- * No-op when no memory is wired, when compaction is disabled or below
1122
- * threshold (the engine returns `false`), or for `secret`-tier runs
1123
- * (secret history is never shipped to the summarizer - a less-trusted
1124
- * external sink; per-result handle references land in WI-10). Best
1125
- * effort: a misconfigured engine (e.g. no summarizer) is swallowed and
1126
- * the run proceeds uncompacted rather than aborting mid-flight.
1127
- *
1128
- * Operator-requested compactions (`agent.compact()`, CE-3/AG-13) are
1129
- * serviced here too, FIRST - the queue carries the `compact()` promise
1130
- * resolvers, and manual requests bypass the trigger evaluation.
1131
- */
1132
- async function* maybeAutoCompact() {
1133
- while (pendingManualCompacts.length > 0) {
1134
- const pending = pendingManualCompacts.shift();
1135
- if (pending !== void 0) yield* serviceManualCompact(pending);
1136
- }
1137
- const mem = memory;
1138
- if (mem === void 0) return;
1139
- if (config.sensitivity === "secret") return;
1140
- const engine = mem.contextEngine;
1141
- if (!await engine.shouldCompact(messages, { compactableFromIndex: systemPrefixLength }).catch(() => false)) return;
1142
- const startedAt = Date.now();
1143
- const envelope = await engine.compactNow({
1144
- scope: {
1145
- userId: state.userId ?? agentId,
1146
- sessionId,
1147
- agentId
1148
- },
1149
- runId: state.id,
1150
- sessionId,
1151
- agentId,
1152
- source: "auto-trigger",
1153
- messages: messages.slice(systemPrefixLength),
1154
- prefixMessages: messages.slice(0, systemPrefixLength),
1155
- memory: mem
1156
- }).catch(() => void 0);
1157
- if (envelope === void 0) return;
1158
- if (envelope.result.droppedMessageIndices.length === 0) return;
1159
- spliceCompacted(envelope);
1160
- yield {
1161
- type: "context.compacted",
1162
- runId: state.id,
1163
- sessionId,
1164
- agentId,
1165
- beforeTokens: envelope.result.beforeTokens,
1166
- afterTokens: envelope.result.afterTokens,
1167
- summaryTokens: envelope.result.summaryTokens,
1168
- durationMs: Date.now() - startedAt,
1169
- source: "auto-trigger",
1170
- hooksFiredCount: envelope.result.hooksFiredCount
1171
- };
1172
- }
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
- /**
1219
- * Prefix-pinned splice shared by the auto + manual compaction paths
1220
- * (CE-3): stable system prefix + [summary, ...recent turns], with the
1221
- * post-compaction hooks' text Context Essentials re-anchored as a
1222
- * trailing system message so they survive the trim (RB-46). Mutates
1223
- * BOTH the live loop buffer and `state.messages`.
1224
- */
1225
- function spliceCompacted(envelope) {
1226
- const rebuilt = [...messages.slice(0, systemPrefixLength), ...envelope.result.trimmedMessages];
1227
- const essentials = envelope.extraContent.map((part) => typeof part === "object" && part !== null && "text" in part ? String(part.text) : "").filter((text) => text.length > 0).join("\n\n");
1228
- if (essentials.length > 0) rebuilt.push({
1229
- role: "system",
1230
- content: essentials
1231
- });
1232
- messages.splice(0, messages.length, ...rebuilt);
1233
- state.messages.splice(0, state.messages.length, ...rebuilt);
1234
- }
1235
- /**
1236
- * Service one `agent.compact()` request inside the loop (CE-3/AG-13):
1237
- * same prefix-pinned splice as the auto path, `source: 'manual'` (or
1238
- * the caller's `'pre-step'`), `preserveRecentTurns` forwarded as a
1239
- * per-call strategy override. An engine failure rejects the caller's
1240
- * promise but never aborts the live run; a summarize that trims
1241
- * nothing resolves `applied: false` without an event.
1242
- */
1243
- async function* serviceManualCompact(pending) {
1244
- const mem = memory;
1245
- if (mem === void 0) {
1246
- pending.resolve(noopCompactionResult("no-memory"));
1247
- return;
1248
- }
1249
- const source = pending.options?.source ?? "manual";
1250
- const startedAt = Date.now();
1251
- let envelope;
1252
- try {
1253
- envelope = await mem.contextEngine.compactNow({
1254
- scope: {
1255
- userId: state.userId ?? agentId,
1256
- sessionId,
1257
- agentId
1258
- },
1259
- runId: state.id,
1260
- sessionId,
1261
- agentId,
1262
- source,
1263
- messages: messages.slice(systemPrefixLength),
1264
- prefixMessages: messages.slice(0, systemPrefixLength),
1265
- memory: mem,
1266
- ...pending.options?.preserveRecentTurns !== void 0 ? { preserveRecentTurns: pending.options.preserveRecentTurns } : {}
1267
- });
1268
- } catch (cause) {
1269
- pending.reject(cause);
1270
- return;
1271
- }
1272
- const { result } = envelope;
1273
- const applied = result.droppedMessageIndices.length > 0;
1274
- if (applied) {
1275
- spliceCompacted(envelope);
1276
- yield {
1277
- type: "context.compacted",
1278
- runId: state.id,
1279
- sessionId,
1280
- agentId,
1281
- beforeTokens: result.beforeTokens,
1282
- afterTokens: result.afterTokens,
1283
- summaryTokens: result.summaryTokens,
1284
- durationMs: Date.now() - startedAt,
1285
- source,
1286
- hooksFiredCount: result.hooksFiredCount
1287
- };
1288
- }
1289
- pending.resolve({
1290
- beforeTokens: result.beforeTokens,
1291
- afterTokens: result.afterTokens,
1292
- summaryTokens: result.summaryTokens,
1293
- durationMs: Date.now() - startedAt,
1294
- hooksFiredCount: result.hooksFiredCount,
1295
- summary: result.summary ?? "",
1296
- applied,
1297
- ...applied ? {} : { skippedReason: "nothing-to-trim" }
1298
- });
1299
- }
1300
- const handoffNames = Array.from(handoffMap.keys());
1301
- let stepNumber = 0;
239
+ if (resumed && resumedApprovedCalls.length > 0) yield* dispatchResumedApprovals(runEnv, resumedApprovedCalls, grantedApprovals);
240
+ if (resumed && subRunDecisions.size > 0) yield* processSubRunResumes(runEnv, subRunDecisions);
241
+ if (resumed && (state.status === "awaiting_approval" || state.status === "aborted" && state.pendingApprovals.length > 0)) return yield* finishRun(state, finalSnapshot);
242
+ let stepNumber = state.steps.reduce((max, s) => Math.max(max, s.stepNumber), 0);
1302
243
  let verifierRoundsUsed = 0;
1303
244
  let lastStepCalledToolNames = [];
1304
- async function* emitCancellation() {
1305
- yield {
1306
- type: "agent.cancelling",
1307
- runId: state.id,
1308
- drain: pendingAbort?.drain ?? false,
1309
- onPendingApprovals: pendingAbort?.onPendingApprovals ?? "deny"
1310
- };
1311
- const policy = pendingAbort?.onPendingApprovals ?? "deny";
1312
- if (policy === "deny") {
1313
- const drained = state.pendingApprovals.splice(0, state.pendingApprovals.length);
1314
- for (const approval of drained) yield {
1315
- type: "tool.approval.denied",
1316
- toolCallId: approval.toolCallId,
1317
- reason: "auto-denied: agent.abort()"
1318
- };
1319
- } else if (policy === "fail") {
1320
- state.status = "failed";
1321
- state.error = {
1322
- message: "aborted with pending approvals",
1323
- code: "run-aborted"
1324
- };
1325
- yield {
1326
- type: "agent.error",
1327
- error: {
1328
- message: "aborted with pending approvals",
1329
- code: "run-aborted"
1330
- }
1331
- };
1332
- return true;
1333
- }
1334
- state.status = "aborted";
1335
- return false;
1336
- }
1337
245
  try {
1338
246
  while (!stopWhen.check(state)) {
1339
247
  while (externalEventQueue.length > 0) {
@@ -1341,7 +249,7 @@ function createAgent(config) {
1341
249
  if (ev !== void 0) yield ev;
1342
250
  }
1343
251
  if (signal.aborted) {
1344
- if (yield* emitCancellation()) return yield* finishRun(state, finalSnapshot);
252
+ if (yield* emitCancellation(runEnv)) return yield* finishRun(state, finalSnapshot);
1345
253
  break;
1346
254
  }
1347
255
  stepNumber += 1;
@@ -1369,7 +277,7 @@ function createAgent(config) {
1369
277
  "graphorin.step.number": stepNumber
1370
278
  }
1371
279
  });
1372
- yield* maybeAutoCompact();
280
+ yield* maybeAutoCompact(runEnv);
1373
281
  const stepCtx = {
1374
282
  ...runContextBase,
1375
283
  stepNumber,
@@ -1377,181 +285,14 @@ function createAgent(config) {
1377
285
  ...currentStepSpan !== void 0 ? { span: currentStepSpan } : {}
1378
286
  };
1379
287
  const overrides = config.prepareStep ? await config.prepareStep(stepCtx) : {};
1380
- const useOverrideRegistry = overrides.tools !== void 0 && !isCodeMode;
1381
- const stepRegistry = useOverrideRegistry ? buildToolRegistry({ tools: overrides.tools }).registry : toolRegistry;
1382
- if (useOverrideRegistry) {
1383
- registerToolSearch(stepRegistry, config.toolPromotion === "run-boundary" ? "next-run" : "next-step");
1384
- registerReadResult(stepRegistry, resultReader);
1385
- }
1386
- const stepExecutor = useOverrideRegistry ? makeToolExecutor(stepRegistry) : toolExecutor;
1387
- const handoffTools = handoffNames.map((n) => {
1388
- const h = handoffMap.get(n);
1389
- if (h === void 0) throw new ToolNotFoundError(n);
1390
- return buildHandoffTool(h.agent);
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
- };
1397
- let stepTools;
1398
- if (isCodeMode) stepTools = readOnlyRun ? [...codeModeAdvertised.filter(keepReadOnly)] : [...codeModeAdvertised, ...handoffTools];
1399
- else {
1400
- const eagerTools = stepRegistry.listEager();
1401
- const advertisedPromotions = runStartPromotions ?? promotedDeferred;
1402
- const promotedTools = advertisedPromotions.size === 0 ? [] : orderPromotedTools(advertisedPromotions, stepRegistry.listDeferred());
1403
- stepTools = readOnlyRun ? [...eagerTools.filter(keepReadOnly), ...promotedTools.filter(keepReadOnly)] : [
1404
- ...eagerTools,
1405
- ...handoffTools,
1406
- ...promotedTools
1407
- ];
1408
- }
1409
- const calledLastStep = new Set(lastStepCalledToolNames);
1410
- const toolPreferences = stepTools.filter((t) => calledLastStep.has(t.name)).map((t) => {
1411
- return t.preferredModel;
1412
- });
1413
- const primary = resolvePreferredModel({
1414
- ...overrides.provider !== void 0 ? { prepareStepProvider: overrides.provider } : {},
1415
- toolPreferredModels: toolPreferences,
1416
- ...config.preferredModel !== void 0 ? { agentPreferredModel: config.preferredModel } : {},
1417
- agentDefaultProvider: config.provider,
1418
- ...config.modelTierMap !== void 0 ? { modelTierMap: config.modelTierMap } : {}
1419
- });
1420
- const fallbackChain = primary.source === "prepare-step" ? [primary.resolvedProvider] : [primary.resolvedProvider, ...(config.fallbackModels ?? []).map(specToProvider)];
1421
- const reasoningPolicy = effectiveReasoningRetention(config.reasoningRetention, primary.resolvedProvider);
1422
- if (reasoningPolicy === "strip") {
1423
- const { stripped } = stripReasoningFromMessages(messages);
1424
- if (stripped > 0) stripReasoningFromMessages(state.messages);
1425
- }
1426
- const toolDefs = stepTools.map((t) => toolToDefinition(t));
1427
- const baseRequest = {
1428
- messages: buildStepMessages(messages, structuredInstruction, activeRunState?.todos),
1429
- ...config.outputType !== void 0 ? { outputType: {
1430
- kind: config.outputType.kind,
1431
- ...config.outputType.description !== void 0 ? { description: config.outputType.description } : {},
1432
- ...config.outputType.jsonSchema !== void 0 ? { jsonSchema: config.outputType.jsonSchema } : {}
1433
- } } : {},
1434
- tools: toolDefs,
1435
- ...overrides.toolChoice !== void 0 ? { toolChoice: overrides.toolChoice } : config.toolChoice !== void 0 ? { toolChoice: config.toolChoice } : {},
1436
- metadata: {
1437
- sessionId,
1438
- agentId,
1439
- ...userId !== void 0 ? { userId } : {},
1440
- runId: state.id,
1441
- stepNumber
1442
- },
1443
- signal,
1444
- ...overrides.temperature !== void 0 ? { temperature: overrides.temperature } : {},
1445
- ...overrides.maxTokens !== void 0 ? { maxTokens: overrides.maxTokens } : {},
1446
- ...config.cachePolicy !== void 0 ? { cachePolicy: config.cachePolicy } : {},
1447
- ...currentStepSpan !== void 0 ? { parentSpan: currentStepSpan } : {},
1448
- reasoningRetention: reasoningPolicy
1449
- };
1450
- const stepUsage = zeroUsage();
1451
- let attempt = 0;
1452
- let textBuffer = "";
1453
- let providerForStep = primary.resolvedProvider;
1454
- let lastModelId = primary.resolvedModelId;
1455
- let modelSucceeded = false;
1456
- let lastError;
1457
- let finalCalls = [];
1458
- let stepReasoningParts = [];
1459
- let requestForStep = baseRequest;
1460
- let emergencyCompactTried = false;
1461
- for (let chainIdx = 0; chainIdx < fallbackChain.length; chainIdx++) {
1462
- const candidate = fallbackChain[chainIdx];
1463
- if (candidate === void 0) continue;
1464
- providerForStep = candidate;
1465
- const providerModelId = providerForStep.modelId;
1466
- if (chainIdx > 0) {
1467
- attempt += 1;
1468
- const reason = lastError ? isAgentFallbackEligible(lastError, fallbackPolicy).reason ?? "transient" : "transient";
1469
- yield {
1470
- type: "agent.model.fellback",
1471
- runId: state.id,
1472
- sessionId,
1473
- agentId,
1474
- from: lastModelId,
1475
- to: providerModelId,
1476
- reason,
1477
- stepNumber,
1478
- attempt
1479
- };
1480
- lastModelId = providerModelId;
1481
- }
1482
- const evState = {
1483
- textBuffer: "",
1484
- reasoningBuffer: "",
1485
- reasoningParts: [],
1486
- calls: /* @__PURE__ */ new Map(),
1487
- finalCalls: []
1488
- };
1489
- let providerError;
1490
- let providerCallCompleted = false;
1491
- let providerStepUsage = zeroUsage();
1492
- try {
1493
- const stream$1 = providerForStep.stream(requestForStep);
1494
- for await (const ev of stream$1) {
1495
- if (signal.aborted && pendingAbort?.drain !== true) throw new AgentRuntimeError("run-aborted", "aborted");
1496
- const out = handleProviderEvent(ev, evState);
1497
- if (out.emit !== void 0) yield out.emit;
1498
- if (out.providerError !== void 0) providerError = out.providerError;
1499
- if (out.usage !== void 0) providerStepUsage = addUsage(providerStepUsage, out.usage);
1500
- if (out.finished === true) providerCallCompleted = true;
1501
- }
1502
- } catch (cause) {
1503
- if (signal.aborted || cause instanceof AgentRuntimeError && cause.code === "run-aborted") break;
1504
- const message = cause instanceof Error ? cause.message : String(cause);
1505
- providerError = {
1506
- kind: classifyThrownProviderErrorKind(cause),
1507
- message,
1508
- cause
1509
- };
1510
- }
1511
- if (providerError !== void 0) {
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
- }
1524
- if (!isAgentFallbackEligible(providerError, fallbackPolicy).eligible || chainIdx === fallbackChain.length - 1) {
1525
- yield {
1526
- type: "agent.error",
1527
- error: {
1528
- message: providerError.message,
1529
- code: providerError.kind
1530
- }
1531
- };
1532
- state.status = "failed";
1533
- state.error = {
1534
- message: providerError.message,
1535
- code: providerError.kind
1536
- };
1537
- return yield* finishRun(state, finalSnapshot);
1538
- }
1539
- continue;
1540
- }
1541
- if (providerCallCompleted) {
1542
- modelSucceeded = true;
1543
- textBuffer = evState.textBuffer;
1544
- finalCalls = evState.finalCalls;
1545
- if (evState.reasoningBuffer.length > 0) stepReasoningParts = [{
1546
- type: "reasoning",
1547
- text: evState.reasoningBuffer
1548
- }];
1549
- accumulateUsage(stepUsage, providerStepUsage);
1550
- break;
1551
- }
1552
- }
288
+ const { stepRegistry, stepExecutor, stepTools, primary, fallbackChain } = resolveStepToolContext(runEnv, overrides, lastStepCalledToolNames);
289
+ const reasoningPolicy = applyReasoningRetention(config.reasoningRetention, primary.resolvedProvider, messages, state.messages);
290
+ const chain = yield* runProviderFallbackChain(runEnv, fallbackChain, buildBaseRequest(runEnv, overrides, stepTools.map((t) => toolToDefinition(t)), reasoningPolicy, stepNumber, currentStepSpan), primary, stepNumber);
291
+ if (chain.failed) return yield* finishRun(state, finalSnapshot);
292
+ const { modelSucceeded, textBuffer, finalCalls, stepReasoningParts } = chain;
293
+ const { stepUsage, lastModelId } = chain;
1553
294
  if (signal.aborted && !modelSucceeded) {
1554
- yield* emitCancellation();
295
+ yield* emitCancellation(runEnv);
1555
296
  return yield* finishRun(state, finalSnapshot);
1556
297
  }
1557
298
  if (!modelSucceeded) {
@@ -1572,28 +313,7 @@ function createAgent(config) {
1572
313
  usageAcc.add(lastModelId, stepUsage);
1573
314
  addModelUsage(state, lastModelId, stepUsage);
1574
315
  accumulateUsage(state.usage, stepUsage);
1575
- const leakCheck = causalityMonitor !== void 0 && textBuffer.length > 0 ? causalityMonitor.checkMessage(textBuffer) : void 0;
1576
- const leakBlocked = leakCheck?.leakDetected === true && leakCheck.decision === "block";
1577
- const assistant = buildAssistantMessage(leakBlocked ? LATERAL_LEAK_BLOCKED_NOTICE : textBuffer, stepReasoningParts, finalCalls, agentId, reasoningPolicy);
1578
- messages.push(assistant);
1579
- state.messages.push(assistant);
1580
- if (toolDataFlowGuard !== void 0 && textBuffer.length > 0 && !leakBlocked) toolDataFlowGuard.recordAssistant(state.id, textBuffer);
1581
- if (leakCheck?.leakDetected === true) {
1582
- const sha = sha256Hex(textBuffer);
1583
- yield {
1584
- type: "agent.lateral-leak.detected",
1585
- runId: state.id,
1586
- sessionId,
1587
- agentId,
1588
- vector: leakCheck.vector,
1589
- severity: leakCheck.severity,
1590
- causalityChain: leakCheck.causalityChain,
1591
- messageContentSha256: sha,
1592
- ...leakCheck.matchedPattern !== void 0 ? { matchedPattern: leakCheck.matchedPattern } : {},
1593
- decision: leakCheck.decision,
1594
- detectedAtIso: (/* @__PURE__ */ new Date()).toISOString()
1595
- };
1596
- }
316
+ const leakBlocked = yield* commitAssistantMessage(runEnv, textBuffer, stepReasoningParts, finalCalls, reasoningPolicy);
1597
317
  const handoffCalls = finalCalls.filter((c) => handoffMap.has(c.toolName));
1598
318
  if (handoffCalls.length > 1) throw new MultipleHandoffsInStepError(handoffCalls.map((c) => c.toolName));
1599
319
  const stepRecord = {
@@ -1619,202 +339,30 @@ function createAgent(config) {
1619
339
  };
1620
340
  }
1621
341
  if (finalCalls.length > 0) {
1622
- const execRunContext = {
342
+ const walked = yield* processStepToolCalls(runEnv, finalCalls, stepRegistry, stepExecutor, {
1623
343
  ...runContextBase,
1624
344
  stepNumber,
1625
345
  messages,
1626
346
  ...currentStepSpan !== void 0 ? { span: currentStepSpan } : {}
1627
- };
1628
- let batch = [];
1629
- let stepApprovalsRequested = 0;
1630
- for (const call of finalCalls) {
1631
- const handoff = handoffMap.get(call.toolName);
1632
- if (handoff !== void 0) {
1633
- if (batch.length > 0) {
1634
- yield* dispatchBatch(batch, stepExecutor, execRunContext, stepNumber);
1635
- batch = [];
1636
- }
1637
- yield {
1638
- type: "tool.execute.start",
1639
- toolCallId: call.toolCallId
1640
- };
1641
- const filter = handoff.filter ?? filters.defaultHandoffFilter();
1642
- const filtered = filter(messages);
1643
- const targetId = handoff.agent.id;
1644
- const handoffRec = {
1645
- fromAgentId: agentId,
1646
- toAgentId: targetId,
347
+ }, stepNumber);
348
+ if (walked.suspended) {
349
+ if (walked.abortPending === true) {
350
+ yield* emitCancellation(runEnv);
351
+ if (config.checkpointStore !== void 0) await config.checkpointStore.put(state.id, "agent", {
352
+ id: state.id,
353
+ threadId: state.id,
354
+ namespace: "agent",
355
+ state: serializeRunState(state, { stripTracingApiKey: true }),
356
+ channelVersions: {},
1647
357
  stepNumber,
1648
- at: (/* @__PURE__ */ new Date()).toISOString(),
1649
- inputFilter: filter.descriptor,
1650
- secretsInheritance: "inherit-allowlist",
1651
- inheritedSecrets: []
1652
- };
1653
- state.handoffs.push(handoffRec);
1654
- yield {
1655
- type: "handoff",
1656
- fromAgentId: agentId,
1657
- toAgentId: targetId
1658
- };
1659
- state.currentAgentId = targetId;
1660
- const subAgent = handoff.agent;
1661
- const subStart = Date.now();
1662
- const subOutputs = [];
1663
- let subResult;
1664
- const subStream = subAgent.stream(filtered, {
1665
- signal,
1666
- ...options.deps !== void 0 || config.deps !== void 0 ? { deps: options.deps ?? config.deps } : {},
1667
- sessionId
358
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
359
+ }, {
360
+ source: "sync",
361
+ status: state.status === "failed" ? "failed" : "aborted",
362
+ nodeName: "agent.abort",
363
+ sessionId: state.sessionId
1668
364
  });
1669
- for await (const subEv of subStream) if (subEv.type === "text.complete") subOutputs.push(subEv.text);
1670
- else if (subEv.type === "agent.end") subResult = subEv.result;
1671
- const subDurationMs = Date.now() - subStart;
1672
- const stepEntry = state.steps[state.steps.length - 1];
1673
- if (subResult !== void 0 && subResult.status !== "completed") {
1674
- const toolError = {
1675
- toolCallId: call.toolCallId,
1676
- toolName: call.toolName,
1677
- kind: subResult.status === "aborted" ? "aborted" : "execution_failed",
1678
- message: `handoff to '${targetId}' ${subResult.status}${subResult.error !== void 0 ? `: ${subResult.error.message}` : ""}`
1679
- };
1680
- if (stepEntry !== void 0) stepEntry.toolCalls.push({
1681
- call,
1682
- outcome: toolError,
1683
- stepNumber
1684
- });
1685
- yield {
1686
- type: "tool.execute.error",
1687
- toolCallId: call.toolCallId,
1688
- error: toolError
1689
- };
1690
- const text = renderToolErrorMessage(toolError);
1691
- messages.push({
1692
- role: "tool",
1693
- toolCallId: call.toolCallId,
1694
- content: text
1695
- });
1696
- state.messages.push({
1697
- role: "tool",
1698
- toolCallId: call.toolCallId,
1699
- content: text
1700
- });
1701
- continue;
1702
- }
1703
- const result = subOutputs.join("");
1704
- const completed = {
1705
- call,
1706
- outcome: {
1707
- toolCallId: call.toolCallId,
1708
- toolName: call.toolName,
1709
- output: result,
1710
- durationMs: subDurationMs
1711
- },
1712
- stepNumber
1713
- };
1714
- if (stepEntry !== void 0) stepEntry.toolCalls.push(completed);
1715
- yield {
1716
- type: "tool.execute.end",
1717
- toolCallId: call.toolCallId,
1718
- result,
1719
- durationMs: subDurationMs
1720
- };
1721
- messages.push({
1722
- role: "tool",
1723
- toolCallId: call.toolCallId,
1724
- content: result
1725
- });
1726
- state.messages.push({
1727
- role: "tool",
1728
- toolCallId: call.toolCallId,
1729
- content: result
1730
- });
1731
- continue;
1732
- }
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
365
  }
1774
- if (await invokeNeedsApproval(resolvedTool, gateInput, execRunContext, signal)) {
1775
- if (batch.length > 0) {
1776
- yield* dispatchBatch(batch, stepExecutor, execRunContext, stepNumber);
1777
- batch = [];
1778
- }
1779
- yield {
1780
- type: "tool.execute.start",
1781
- toolCallId: call.toolCallId
1782
- };
1783
- const approval = {
1784
- toolCallId: call.toolCallId,
1785
- toolName: call.toolName,
1786
- args: call.args,
1787
- requestedAt: (/* @__PURE__ */ new Date()).toISOString()
1788
- };
1789
- state.pendingApprovals.push(approval);
1790
- stepApprovalsRequested += 1;
1791
- yield {
1792
- type: "tool.approval.requested",
1793
- toolCallId: call.toolCallId
1794
- };
1795
- continue;
1796
- }
1797
- batch.push(call);
1798
- }
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
366
  return yield* finishRun(state, finalSnapshot);
1819
367
  }
1820
368
  }
@@ -1831,37 +379,9 @@ function createAgent(config) {
1831
379
  };
1832
380
  if (finalCalls.length === 0) {
1833
381
  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
- }
382
+ const gate = yield* runVerifierGate(runEnv, finalSnapshot, stepNumber, verifierRoundsUsed);
383
+ verifierRoundsUsed = gate.verifierRoundsUsed;
384
+ if (gate.continueRun) continue;
1865
385
  }
1866
386
  state.status = "completed";
1867
387
  break;
@@ -1895,135 +415,17 @@ function createAgent(config) {
1895
415
  runSpan.end();
1896
416
  if (parentSignal !== void 0) parentSignal.removeEventListener("abort", onParentAbort);
1897
417
  }
1898
- if (state.status === "running") {
1899
- const message = `run stopped by stop condition: ${stopWhen.description}`;
1900
- state.status = "failed";
1901
- state.error = {
1902
- message,
1903
- code: "stop-condition"
1904
- };
1905
- yield {
1906
- type: "agent.error",
1907
- error: {
1908
- message,
1909
- code: "stop-condition"
1910
- }
1911
- };
1912
- }
1913
- if (state.status === "completed" && config.outputType?.kind === "structured") {
1914
- const raw = String(finalSnapshot.output ?? "");
1915
- try {
1916
- const parsed = JSON.parse(stripJsonFence(raw));
1917
- finalSnapshot.output = config.outputType.schema !== void 0 ? config.outputType.schema.parse(parsed) : parsed;
1918
- } catch (cause) {
1919
- const message = `structured output validation failed: ${cause instanceof Error ? cause.message : String(cause)}`;
1920
- yield {
1921
- type: "agent.error",
1922
- error: {
1923
- message,
1924
- code: "output-validation-failed"
1925
- }
1926
- };
1927
- state.status = "failed";
1928
- state.error = {
1929
- message,
1930
- code: "output-validation-failed"
1931
- };
1932
- }
1933
- }
1934
- const outputGuards = config.guardrails?.output;
1935
- if (state.status === "completed" && outputGuards !== void 0 && outputGuards.length > 0) {
1936
- const composed = await composeGuardrails(outputGuards, finalSnapshot.output, {
1937
- stage: "output",
1938
- runId: state.id,
1939
- sessionId,
1940
- agentId
1941
- });
1942
- if (!composed.ok) {
1943
- yield {
1944
- type: "guardrail.tripped",
1945
- guardrailName: composed.name,
1946
- phase: "output",
1947
- reason: composed.message
1948
- };
1949
- const message = `output guardrail '${composed.name}' blocked the run: ${composed.message}`;
1950
- yield {
1951
- type: "agent.error",
1952
- error: {
1953
- message,
1954
- code: "guardrail-blocked"
1955
- }
1956
- };
1957
- state.status = "failed";
1958
- state.error = {
1959
- message,
1960
- code: "guardrail-blocked"
1961
- };
1962
- } else if (composed.value !== finalSnapshot.output) finalSnapshot.output = composed.value;
1963
- }
418
+ yield* finalizeRunOutput(runEnv, finalSnapshot);
1964
419
  activeRunState = void 0;
1965
420
  return yield* finishRun(state, finalSnapshot);
1966
421
  }
1967
- /**
1968
- * Terminal wrapper around {@link finalize}: every exit path of the run
1969
- * loop - completed, failed, aborted, suspended - ends the stream with
1970
- * an `agent.end` event carrying the final {@link AgentResult} (AG-20).
1971
- */
1972
- async function* finishRunBase(state, snapshot) {
1973
- while (pendingManualCompacts.length > 0) pendingManualCompacts.shift()?.resolve(noopCompactionResult("no-active-run"));
1974
- const result = finalize(state, snapshot);
1975
- if (result.status === "completed" || result.status === "failed") await spillWriter.clear?.(result.state.id).catch(() => {});
1976
- yield {
1977
- type: "agent.end",
1978
- runId: result.state.id,
1979
- result
1980
- };
1981
- return result;
1982
- }
1983
- function finalize(state, snapshot) {
1984
- state.finishedAt = state.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1985
- return {
1986
- output: snapshot.output,
1987
- usage: state.usage,
1988
- status: state.status,
1989
- ...state.error !== void 0 ? { error: state.error } : {},
1990
- state
1991
- };
1992
- }
1993
- const stream = (input, options) => {
1994
- const opts = options ?? {};
1995
- return { [Symbol.asyncIterator]: () => {
1996
- const gen = runLoop(input, opts);
1997
- return {
1998
- async next() {
1999
- const r = await gen.next();
2000
- if (r.done === true) return {
2001
- done: true,
2002
- value: void 0
2003
- };
2004
- return {
2005
- done: false,
2006
- value: r.value
2007
- };
2008
- },
2009
- async return() {
2010
- await gen.return(void 0);
2011
- return {
2012
- done: true,
2013
- value: void 0
2014
- };
2015
- }
2016
- };
2017
- } };
2018
- };
2019
- const run = async (input, options) => {
2020
- const gen = runLoop(input, options ?? {});
2021
- let next = await gen.next();
2022
- while (next.done !== true) next = await gen.next();
2023
- const result = next.value;
2024
- if (result === void 0) throw new Error("unreachable: agent run loop ended without a result");
2025
- return result;
2026
- };
422
+ const finishRunBase = createRunFinisher({
423
+ pendingManualCompacts,
424
+ spillWriter,
425
+ checkpointStore: config.checkpointStore,
426
+ checkpointPolicy: config.checkpointPolicy
427
+ });
428
+ const { stream, run } = createRunMethods(runLoop);
2027
429
  const steer = (message) => {
2028
430
  const { seed } = asMessages(message);
2029
431
  pendingSteer.push(...seed);
@@ -2044,136 +446,23 @@ function createAgent(config) {
2044
446
  pendingAbort = options ?? {};
2045
447
  abortController?.abort();
2046
448
  };
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
- };
2063
- const toTool = (options = {}) => {
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;
2067
- return {
2068
- name: options.name ?? `subagent_${config.name}`,
2069
- description: options.description ?? `Invoke sub-agent '${config.name}'.`,
2070
- inputSchema: {
2071
- parse: (v) => v,
2072
- safeParse: (v) => ({
2073
- success: true,
2074
- data: v
2075
- }),
2076
- toJSON: () => ({
2077
- type: "object",
2078
- properties: { input: { type: "string" } },
2079
- required: ["input"]
2080
- })
2081
- },
2082
- sideEffectClass: "side-effecting",
2083
- async execute(input, ctx) {
2084
- const callOpts = {
2085
- ...ctx?.signal !== void 0 ? { signal: ctx.signal } : {},
2086
- ...ctx?.runContext.deps !== void 0 ? { deps: ctx.runContext.deps } : {},
2087
- ...ctx?.runContext.sessionId !== void 0 ? { sessionId: ctx.runContext.sessionId } : {},
2088
- ...options.capability !== void 0 ? { capability: options.capability } : {}
2089
- };
2090
- const seed = options.inputFilter !== void 0 && ctx !== void 0 ? [...options.inputFilter(ctx.runContext.messages), {
2091
- role: "user",
2092
- content: input.input
2093
- }] : input.input;
2094
- if (exposeTurns === "all") {
2095
- const turns = [];
2096
- let endResult;
2097
- for await (const ev of stream(seed, callOpts)) if (ev.type === "text.complete") turns.push(ev.text);
2098
- else if (ev.type === "agent.end") endResult = ev.result;
2099
- if (endResult !== void 0 && endResult.status !== "completed") throw new Error(`sub-agent '${config.name}' ${endResult.status}${endResult.error !== void 0 ? `: ${endResult.error.message}` : ""}`);
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;
2106
- }
2107
- const result = await run(seed, callOpts);
2108
- if (result.status !== "completed") throw new Error(`sub-agent '${config.name}' ${result.status}${result.error !== void 0 ? `: ${result.error.message}` : ""}`);
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;
2115
- }
2116
- };
2117
- };
2118
- const compact = async (options) => {
2119
- if (memory === void 0) return noopCompactionResult("no-memory");
2120
- if (config.sensitivity === "secret") return noopCompactionResult("sensitivity-gated");
2121
- if (activeRunState === void 0) return noopCompactionResult("no-active-run");
2122
- return await new Promise((resolve, reject) => {
2123
- pendingManualCompacts.push({
2124
- options,
2125
- resolve,
2126
- reject
2127
- });
2128
- });
2129
- };
2130
- const fanOut = async (options) => {
2131
- const runId = activeRunState?.id ?? `run_${newId()}`;
2132
- const sessionId = activeRunState?.sessionId ?? `session_${newId()}`;
2133
- return runFanOut({
2134
- children: options.children,
2135
- ...options.maxConcurrentChildren !== void 0 ? { maxConcurrentChildren: options.maxConcurrentChildren } : {},
2136
- ...options.perBudget !== void 0 ? { perBudget: options.perBudget } : {},
2137
- ...options.mergeStrategy !== void 0 ? { mergeStrategy: options.mergeStrategy } : {},
2138
- ...options.signal !== void 0 ? { signal: options.signal } : {},
2139
- emit: (event) => {
2140
- externalEventQueue.push(event);
2141
- },
2142
- ...config.mergeGuard !== void 0 ? { mergeGuard: config.mergeGuard } : {},
2143
- runId,
2144
- sessionId,
2145
- agentId
2146
- });
2147
- };
2148
- const progressFallbackRunId = `run_${newId()}`;
2149
- const progress = {
2150
- write: async (content, opts) => {
2151
- const runId = activeRunState?.id ?? progressFallbackRunId;
2152
- const ref = await progressIO.write(runId, content, opts);
2153
- externalEventQueue.push({
2154
- type: "agent.progress.written",
2155
- runId,
2156
- sessionId: activeRunState?.sessionId ?? "",
2157
- agentId,
2158
- ref
2159
- });
2160
- return ref;
2161
- },
2162
- read: async (opts) => {
2163
- const queriedRunId = opts?.runId ?? activeRunState?.id ?? progressFallbackRunId;
2164
- const refs = await progressIO.read(queriedRunId, opts);
2165
- externalEventQueue.push({
2166
- type: "agent.progress.read",
2167
- runId: activeRunState?.id ?? queriedRunId,
2168
- sessionId: activeRunState?.sessionId ?? "",
2169
- agentId,
2170
- refs,
2171
- queriedRunId,
2172
- queriedRole: opts?.role
2173
- });
2174
- return refs;
2175
- }
449
+ const toTool = createToTool({
450
+ config,
451
+ run,
452
+ stream
453
+ });
454
+ const surfaceDeps = {
455
+ config,
456
+ memory,
457
+ agentId,
458
+ getActiveRunState: () => activeRunState,
459
+ externalEventQueue,
460
+ pendingManualCompacts,
461
+ progressIO
2176
462
  };
463
+ const compact = createCompactMethod(surfaceDeps);
464
+ const fanOut = createFanOutMethod(surfaceDeps);
465
+ const progress = createProgressSurface(surfaceDeps);
2177
466
  config.sensitivity;
2178
467
  return {
2179
468
  id: agentId,
@@ -2190,72 +479,6 @@ function createAgent(config) {
2190
479
  registry: toolRegistry
2191
480
  };
2192
481
  }
2193
- /**
2194
- * Pre-execution approval screen (Adapter G / durable HITL). Evaluates a
2195
- * (registry-resolved) tool's `needsApproval` against the realized args.
2196
- * Returns `true` when the run must suspend before the tool executes.
2197
- *
2198
- * Actual execution flows through the `@graphorin/tools` executor, whose
2199
- * `ApprovalGate` auto-grants because only no-approval / pre-approved
2200
- * calls ever reach it; this probe is what keeps the suspend in the
2201
- * agent so the durable-HITL contract (persist `RunState`, resume via
2202
- * directive) is preserved.
2203
- */
2204
- async function invokeNeedsApproval(tool, args, baseCtx, signal) {
2205
- const predicate = tool?.needsApproval;
2206
- if (predicate === void 0 || predicate === false) return false;
2207
- if (predicate === true) return true;
2208
- const probeCtx = {
2209
- toolCallId: "probe",
2210
- runContext: baseCtx,
2211
- signal,
2212
- tracer: baseCtx.tracer,
2213
- logger: NOOP_LOGGER,
2214
- secrets: probeSecretsAccessor(),
2215
- reportProgress: () => {},
2216
- streamContent: () => {}
2217
- };
2218
- return Boolean(await predicate(args, probeCtx));
2219
- }
2220
- /**
2221
- * Rejecting secrets accessor used only by the {@link invokeNeedsApproval}
2222
- * probe. Real tool execution resolves secrets through the executor's
2223
- * ACL-scoped accessor; an approval predicate has no legitimate need to
2224
- * read secret material, so every `require(...)` rejects.
2225
- */
2226
- function probeSecretsAccessor() {
2227
- const rejector = (_key, _options) => Promise.reject(/* @__PURE__ */ new Error("secrets.require is unavailable inside a needsApproval predicate"));
2228
- return { require: rejector };
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
- }
2259
482
 
2260
483
  //#endregion
2261
484
  export { createAgent };