@graphorin/agent 0.5.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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/errors/index.d.ts +170 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +204 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/evaluator-optimizer/index.d.ts +91 -0
- package/dist/evaluator-optimizer/index.d.ts.map +1 -0
- package/dist/evaluator-optimizer/index.js +85 -0
- package/dist/evaluator-optimizer/index.js.map +1 -0
- package/dist/factory.d.ts +13 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +1853 -0
- package/dist/factory.js.map +1 -0
- package/dist/fallback/index.d.ts +52 -0
- package/dist/fallback/index.d.ts.map +1 -0
- package/dist/fallback/index.js +53 -0
- package/dist/fallback/index.js.map +1 -0
- package/dist/fanout/index.d.ts +142 -0
- package/dist/fanout/index.d.ts.map +1 -0
- package/dist/fanout/index.js +252 -0
- package/dist/fanout/index.js.map +1 -0
- package/dist/filters/index.d.ts +137 -0
- package/dist/filters/index.d.ts.map +1 -0
- package/dist/filters/index.js +273 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/ids.js +46 -0
- package/dist/internal/ids.js.map +1 -0
- package/dist/internal/usage-accumulator.js +62 -0
- package/dist/internal/usage-accumulator.js.map +1 -0
- package/dist/lateral-leak/causality-monitor.d.ts +97 -0
- package/dist/lateral-leak/causality-monitor.d.ts.map +1 -0
- package/dist/lateral-leak/causality-monitor.js +139 -0
- package/dist/lateral-leak/causality-monitor.js.map +1 -0
- package/dist/lateral-leak/index.d.ts +4 -0
- package/dist/lateral-leak/index.js +5 -0
- package/dist/lateral-leak/merge-guard.d.ts +89 -0
- package/dist/lateral-leak/merge-guard.d.ts.map +1 -0
- package/dist/lateral-leak/merge-guard.js +65 -0
- package/dist/lateral-leak/merge-guard.js.map +1 -0
- package/dist/lateral-leak/protocol-guard.d.ts +76 -0
- package/dist/lateral-leak/protocol-guard.d.ts.map +1 -0
- package/dist/lateral-leak/protocol-guard.js +147 -0
- package/dist/lateral-leak/protocol-guard.js.map +1 -0
- package/dist/preferred-model/index.d.ts +53 -0
- package/dist/preferred-model/index.d.ts.map +1 -0
- package/dist/preferred-model/index.js +141 -0
- package/dist/preferred-model/index.js.map +1 -0
- package/dist/progress/index.d.ts +62 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/index.js +150 -0
- package/dist/progress/index.js.map +1 -0
- package/dist/run-state/index.d.ts +152 -0
- package/dist/run-state/index.d.ts.map +1 -0
- package/dist/run-state/index.js +311 -0
- package/dist/run-state/index.js.map +1 -0
- package/dist/tooling/adapters.js +154 -0
- package/dist/tooling/adapters.js.map +1 -0
- package/dist/tooling/catalogue.js +37 -0
- package/dist/tooling/catalogue.js.map +1 -0
- package/dist/tooling/dataflow.js +99 -0
- package/dist/tooling/dataflow.js.map +1 -0
- package/dist/tooling/registry-build.js +85 -0
- package/dist/tooling/registry-build.js.map +1 -0
- package/dist/types.d.ts +413 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +115 -0
package/dist/factory.js
ADDED
|
@@ -0,0 +1,1853 @@
|
|
|
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";
|
|
5
|
+
import { newId } from "./internal/ids.js";
|
|
6
|
+
import { InMemoryUsageAccumulator } from "./internal/usage-accumulator.js";
|
|
7
|
+
import { CausalityMonitor } from "./lateral-leak/causality-monitor.js";
|
|
8
|
+
import { resolvePreferredModel } from "./preferred-model/index.js";
|
|
9
|
+
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 { buildToolRegistry } from "./tooling/registry-build.js";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { NOOP_LOGGER, NOOP_TRACER, isStepCount, zeroUsage } from "@graphorin/core";
|
|
17
|
+
import { composeGuardrails } from "@graphorin/security/guardrails";
|
|
18
|
+
import { createReadResultTool, createToolSearchTool } from "@graphorin/tools/built-in";
|
|
19
|
+
import { createCodeExecuteTool, createCodeSearchTool, projectToolApi } from "@graphorin/tools/code-mode";
|
|
20
|
+
import { createToolExecutor } from "@graphorin/tools/executor";
|
|
21
|
+
import { createDefaultSpillWriter, createFileResultReader } from "@graphorin/tools/result";
|
|
22
|
+
|
|
23
|
+
//#region src/factory.ts
|
|
24
|
+
/**
|
|
25
|
+
* `createAgent({...})` — the agent factory entry point.
|
|
26
|
+
*
|
|
27
|
+
* Wires the typed `model -> tool calls -> model` loop, the streamed
|
|
28
|
+
* event surface, the steering / followUp queues, durable HITL via
|
|
29
|
+
* `RunState`, the multi-agent handoff layer, the agent-level model
|
|
30
|
+
* fallback chain, and the per-tool preferred-model resolution.
|
|
31
|
+
*
|
|
32
|
+
* Custom adapters override behaviour by supplying alternative
|
|
33
|
+
* `Provider` / `Memory` / `CheckpointStore` instances; the loop
|
|
34
|
+
* never reaches into adapter internals.
|
|
35
|
+
*
|
|
36
|
+
* @packageDocumentation
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Replacement content committed in place of assistant commentary the
|
|
40
|
+
* causality monitor blocked (AG-10). Worded to not match any built-in
|
|
41
|
+
* denial pattern, so the notice itself never re-triggers the monitor.
|
|
42
|
+
*/
|
|
43
|
+
const LATERAL_LEAK_BLOCKED_NOTICE = "[graphorin] assistant commentary withheld by the lateral-leak defense.";
|
|
44
|
+
const sha256Hex = (input) => createHash("sha256").update(input, "utf8").digest("hex");
|
|
45
|
+
const HANDOFF_TOOL_PREFIX = "transfer_to_";
|
|
46
|
+
/** The built-in deferred-discovery tool's stable name (WI-05 / P0-3). */
|
|
47
|
+
const TOOL_SEARCH_NAME = "tool_search";
|
|
48
|
+
/**
|
|
49
|
+
* Register the built-in `tool_search` into `registry` when — and only
|
|
50
|
+
* when — the registry holds at least one deferred tool
|
|
51
|
+
* (`defer_loading: true`). `tool_search` is itself eager (so it is
|
|
52
|
+
* always advertised while a deferred pool exists) and resolvable by the
|
|
53
|
+
* executor like any other tool, so a model can both *see* it in the
|
|
54
|
+
* per-step catalogue and *call* it.
|
|
55
|
+
*
|
|
56
|
+
* No-op when nothing defers (zero overhead — the tool never appears) or
|
|
57
|
+
* when a user tool already occupies the name (the user's tool wins; we
|
|
58
|
+
* never clobber it). Because deferral is decided at registration time
|
|
59
|
+
* (`normaliseTool`), the deferred set is fixed for the life of the
|
|
60
|
+
* registry — this runs once per registry, not per step.
|
|
61
|
+
*/
|
|
62
|
+
function registerToolSearch(registry) {
|
|
63
|
+
if (registry.listDeferred().length === 0) return;
|
|
64
|
+
if (registry.get(TOOL_SEARCH_NAME) !== void 0) return;
|
|
65
|
+
registry.register(createToolSearchTool({ registry }), {
|
|
66
|
+
kind: "built-in",
|
|
67
|
+
subsystem: "tool-discovery"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** The built-in result-handle reader tool's stable name (WI-10 / P1-4). */
|
|
71
|
+
const READ_RESULT_NAME = "read_result";
|
|
72
|
+
/**
|
|
73
|
+
* Register the built-in `read_result` into `registry` when at least one
|
|
74
|
+
* registered tool opts into the `'spill-to-file'` truncation strategy
|
|
75
|
+
* (the sole producer of spill handles today) — or when `force` is set,
|
|
76
|
+
* which the agent passes when an operator wires external result readers
|
|
77
|
+
* (e.g. an MCP `resource_link` reader; WI-13). The tool is eager, so it
|
|
78
|
+
* is advertised alongside the producing tool and the model can fetch a
|
|
79
|
+
* handle back on demand instead of inlining the full blob. No-op when
|
|
80
|
+
* nothing produces handles (zero overhead) or when a user tool already
|
|
81
|
+
* occupies the name (the user's tool wins).
|
|
82
|
+
*/
|
|
83
|
+
function registerReadResult(registry, reader, opts) {
|
|
84
|
+
if (opts?.force !== true && !registry.list().some((entry) => entry.truncationStrategy === "spill-to-file")) return;
|
|
85
|
+
if (registry.get(READ_RESULT_NAME) !== void 0) return;
|
|
86
|
+
registry.register(createReadResultTool({ reader }), {
|
|
87
|
+
kind: "built-in",
|
|
88
|
+
subsystem: "result-handle"
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Compose result readers into one that tries each in order, returning
|
|
93
|
+
* the first that resolves the handle (WI-13). The spill-file reader is
|
|
94
|
+
* placed first so `graphorin-spill:` handles resolve locally; operator
|
|
95
|
+
* readers (e.g. an MCP resource reader) resolve the rest. Each reader
|
|
96
|
+
* rejects handles it does not own, so resolution falls through cleanly.
|
|
97
|
+
*/
|
|
98
|
+
function composeResultReaders(readers) {
|
|
99
|
+
return { async read(uri, range) {
|
|
100
|
+
let lastError;
|
|
101
|
+
for (const r of readers) try {
|
|
102
|
+
return await r.read(uri, range);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
lastError = err;
|
|
105
|
+
}
|
|
106
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`No result reader resolved handle ${JSON.stringify(uri)}.`);
|
|
107
|
+
} };
|
|
108
|
+
}
|
|
109
|
+
/** The code-mode meta-tools' stable names (WI-11 / P1-2). */
|
|
110
|
+
const CODE_EXECUTE_NAME = "code_execute";
|
|
111
|
+
const CODE_SEARCH_NAME = "code_search";
|
|
112
|
+
/**
|
|
113
|
+
* Wire code-mode (P1-2) into `registry`: register the `code_search` /
|
|
114
|
+
* `code_execute` meta-tools and return them as the tools to advertise in
|
|
115
|
+
* place of the full catalogue. The model reaches every other tool through
|
|
116
|
+
* `code_execute`, whose in-script `tools.<name>(args)` calls route back
|
|
117
|
+
* through `quietExecutor.executeOne(...)` under the calling step's
|
|
118
|
+
* `runContext` — so per-tool ACL / sanitization / truncation still apply,
|
|
119
|
+
* exactly as in direct mode. `quietExecutor` carries no `streamingSink`,
|
|
120
|
+
* so the inner calls do not interleave `tool.execute.*` events into the
|
|
121
|
+
* outer stream.
|
|
122
|
+
*
|
|
123
|
+
* Excluded from the code API (`reservedNames`): the meta-tools, the
|
|
124
|
+
* discovery / handle built-ins, handoff tools (which stay first-class
|
|
125
|
+
* provider tools), and — supplied by the caller — any approval-gated
|
|
126
|
+
* tool, since code-mode has no durable-HITL path to suspend mid-script.
|
|
127
|
+
*
|
|
128
|
+
* Returns `[]` (registering nothing) when no real tool is exposable.
|
|
129
|
+
*/
|
|
130
|
+
function registerCodeMode(registry, quietExecutor, reservedNames) {
|
|
131
|
+
if (registry.get(CODE_EXECUTE_NAME) !== void 0) return [];
|
|
132
|
+
const isApprovalGated = (t) => t.needsApproval === true || typeof t.needsApproval === "function";
|
|
133
|
+
const codeTools = registry.list().filter((entry) => !reservedNames.has(entry.name) && !isApprovalGated(entry));
|
|
134
|
+
if (codeTools.length === 0) return [];
|
|
135
|
+
const approvalGatedTools = registry.list().filter((entry) => !reservedNames.has(entry.name) && isApprovalGated(entry)).map((entry) => entry.name);
|
|
136
|
+
const approvalGatedSet = new Set(approvalGatedTools);
|
|
137
|
+
const allowedTools = [...codeTools.map((entry) => entry.name), ...approvalGatedTools];
|
|
138
|
+
const allowedSet = new Set(codeTools.map((entry) => entry.name));
|
|
139
|
+
const projection = projectToolApi(codeTools.filter((entry) => entry.__effectiveDeferLoading !== true));
|
|
140
|
+
const executeTool = async (call, ctx) => {
|
|
141
|
+
if (approvalGatedSet.has(call.name)) throw new Error(`${call.name} requires human approval and cannot run inside code_execute — call it directly as a standalone tool call so the run can suspend for the approval.`);
|
|
142
|
+
const { outcome } = await quietExecutor.executeOne({
|
|
143
|
+
call: {
|
|
144
|
+
toolCallId: newId("codecall"),
|
|
145
|
+
toolName: call.name,
|
|
146
|
+
args: call.args
|
|
147
|
+
},
|
|
148
|
+
runContext: ctx.runContext,
|
|
149
|
+
stepNumber: ctx.runContext.stepNumber
|
|
150
|
+
});
|
|
151
|
+
if ("kind" in outcome) throw new Error(`${call.name}: ${outcome.message}`);
|
|
152
|
+
return outcome.output;
|
|
153
|
+
};
|
|
154
|
+
const codeSearch = createCodeSearchTool({
|
|
155
|
+
projection,
|
|
156
|
+
approvalGatedTools,
|
|
157
|
+
searchDeferred: async (query, k) => (await registry.searchDeferred(query, k)).filter((match) => allowedSet.has(match.name))
|
|
158
|
+
});
|
|
159
|
+
const codeExecute = createCodeExecuteTool({
|
|
160
|
+
projection,
|
|
161
|
+
allowedTools,
|
|
162
|
+
executeTool,
|
|
163
|
+
approvalGatedTools
|
|
164
|
+
});
|
|
165
|
+
registry.register(codeSearch, {
|
|
166
|
+
kind: "built-in",
|
|
167
|
+
subsystem: "code-mode"
|
|
168
|
+
});
|
|
169
|
+
registry.register(codeExecute, {
|
|
170
|
+
kind: "built-in",
|
|
171
|
+
subsystem: "code-mode"
|
|
172
|
+
});
|
|
173
|
+
return [codeSearch, codeExecute];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Fold a completed `tool_search` result into the per-run promotion set:
|
|
177
|
+
* every matched tool name becomes advertised (and thus callable) on the
|
|
178
|
+
* next step. Tolerant of unexpected shapes (e.g. a user-shadowed
|
|
179
|
+
* `tool_search`) — only string `name`s inside a `matches` array promote.
|
|
180
|
+
*/
|
|
181
|
+
function recordToolSearchPromotions(output, promoted) {
|
|
182
|
+
if (typeof output !== "object" || output === null) return;
|
|
183
|
+
const matches = output.matches;
|
|
184
|
+
if (!Array.isArray(matches)) return;
|
|
185
|
+
for (const match of matches) {
|
|
186
|
+
const name = match?.name;
|
|
187
|
+
if (typeof name === "string") promoted.add(name);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function isModelHintLike(value) {
|
|
191
|
+
return value === "fast" || value === "balanced" || value === "smart";
|
|
192
|
+
}
|
|
193
|
+
function isModelSpecLike(value) {
|
|
194
|
+
if (typeof value !== "object" || value === null) return false;
|
|
195
|
+
const v = value;
|
|
196
|
+
if (typeof v.modelId === "string" && typeof v.name === "string") return true;
|
|
197
|
+
if (typeof v.provider === "object" && v.provider !== null && typeof v.model === "string") return true;
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
function validatePreferredModel(value) {
|
|
201
|
+
if (value === void 0) return;
|
|
202
|
+
if (isModelHintLike(value)) return;
|
|
203
|
+
if (isModelSpecLike(value)) return;
|
|
204
|
+
throw new InvalidPreferredModelError(value);
|
|
205
|
+
}
|
|
206
|
+
function isMessageObject(value) {
|
|
207
|
+
if (typeof value !== "object" || value === null) return false;
|
|
208
|
+
const role = value.role;
|
|
209
|
+
return role === "system" || role === "user" || role === "assistant" || role === "tool";
|
|
210
|
+
}
|
|
211
|
+
function isRunStateObject(value) {
|
|
212
|
+
if (typeof value !== "object" || value === null) return false;
|
|
213
|
+
const v = value;
|
|
214
|
+
return typeof v.id === "string" && typeof v.agentId === "string" && Array.isArray(v.messages) && Array.isArray(v.steps);
|
|
215
|
+
}
|
|
216
|
+
function asMessages(input) {
|
|
217
|
+
if (typeof input === "string") return { seed: [{
|
|
218
|
+
role: "user",
|
|
219
|
+
content: input
|
|
220
|
+
}] };
|
|
221
|
+
if (Array.isArray(input)) return { seed: [...input] };
|
|
222
|
+
if (isMessageObject(input)) return { seed: [input] };
|
|
223
|
+
if (isRunStateObject(input)) return {
|
|
224
|
+
seed: [],
|
|
225
|
+
resumed: input
|
|
226
|
+
};
|
|
227
|
+
throw new InvalidAgentConfigError(`unrecognized AgentInput shape`);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Strip a single Markdown code fence around a JSON payload (AG-3).
|
|
231
|
+
* Models often wrap structured output in ```json fences even when told
|
|
232
|
+
* not to. ReDoS-safe: the info string is matched with `[^\n]*`.
|
|
233
|
+
*/
|
|
234
|
+
function stripJsonFence(text) {
|
|
235
|
+
const trimmed = text.trim();
|
|
236
|
+
return /^```[^\n]*\n([\s\S]*?)\n?```$/.exec(trimmed)?.[1] ?? trimmed;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The AG-3 fallback instruction: one trailing system message that
|
|
240
|
+
* pins JSON-only output and embeds the wire schema / description.
|
|
241
|
+
* This is the documented structured-output contract for adapters that
|
|
242
|
+
* do not yet consume `ProviderRequest.outputType` natively (PS-24).
|
|
243
|
+
*/
|
|
244
|
+
function buildStructuredInstruction(spec) {
|
|
245
|
+
const parts = ["Respond with a single valid JSON value only — no prose, no Markdown code fences."];
|
|
246
|
+
if (spec.description !== void 0 && spec.description.length > 0) parts.push(spec.description);
|
|
247
|
+
if (spec.jsonSchema !== void 0) parts.push(`The JSON MUST conform to this JSON Schema:\n${JSON.stringify(spec.jsonSchema)}`);
|
|
248
|
+
return parts.join("\n");
|
|
249
|
+
}
|
|
250
|
+
/** Most-recent user-role text in `messages` (for context-engine auto-recall). */
|
|
251
|
+
function lastUserText(messages) {
|
|
252
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
253
|
+
const m = messages[i];
|
|
254
|
+
if (m?.role !== "user") continue;
|
|
255
|
+
if (typeof m.content === "string") return m.content;
|
|
256
|
+
const text = m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
257
|
+
return text.length > 0 ? text : void 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* AG-21: classify a **thrown** provider error into a {@link ProviderErrorKind}
|
|
262
|
+
* so the fallback chain can act on it, instead of flattening every exception to
|
|
263
|
+
* `'unknown'` (which is always fallback-ineligible). Structural — reads the
|
|
264
|
+
* `kind` carried by `@graphorin/provider`'s `GraphorinProviderError` subclasses
|
|
265
|
+
* without importing them, keeping the agent decoupled from the provider package.
|
|
266
|
+
*/
|
|
267
|
+
function classifyThrownProviderErrorKind(cause) {
|
|
268
|
+
if (typeof cause === "object" && cause !== null && "kind" in cause) switch (cause.kind) {
|
|
269
|
+
case "rate-limit-exceeded": return "rate-limit";
|
|
270
|
+
}
|
|
271
|
+
return "unknown";
|
|
272
|
+
}
|
|
273
|
+
/** Resolve a Zod-like schema to a JSON-Schema record (via `toJSON`), else pass an object through. */
|
|
274
|
+
function projectSchema(raw) {
|
|
275
|
+
if (raw === null || raw === void 0) return void 0;
|
|
276
|
+
const toJson = raw.toJSON;
|
|
277
|
+
if (typeof toJson === "function") return toJson.call(raw);
|
|
278
|
+
if (typeof raw === "object") return raw;
|
|
279
|
+
}
|
|
280
|
+
function toolToDefinition(tool) {
|
|
281
|
+
const ts = tool;
|
|
282
|
+
const inputSchema = projectSchema(ts.inputSchema) ?? {};
|
|
283
|
+
const outputSchema = projectSchema(ts.outputSchema);
|
|
284
|
+
const examples = renderToolExamples(tool);
|
|
285
|
+
return {
|
|
286
|
+
name: tool.name,
|
|
287
|
+
description: tool.description,
|
|
288
|
+
inputSchema,
|
|
289
|
+
...outputSchema !== void 0 ? { outputSchema } : {},
|
|
290
|
+
...examples !== void 0 ? { examples } : {}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Project a tool's worked `examples` onto the provider wire contract
|
|
295
|
+
* (WI-06 / P2-3). Examples are rendered only when the tool eagerly
|
|
296
|
+
* renders them: the registry resolves the `defer_loading` auto-rule into
|
|
297
|
+
* `examplesEagerlyRendered`, so a deferred tool resolves to `false` and is
|
|
298
|
+
* skipped (its examples stay out of context even once `tool_search`
|
|
299
|
+
* promotes it). `undefined` — the "runtime decides" case for a plain
|
|
300
|
+
* eager tool — renders, since the tool is already advertised this step.
|
|
301
|
+
*
|
|
302
|
+
* Bounded to ≤5 to honour the `ToolExample` `[1,5]` contract even when a
|
|
303
|
+
* tool slipped past the registry's overflow WARN (which does not truncate).
|
|
304
|
+
*/
|
|
305
|
+
function renderToolExamples(tool) {
|
|
306
|
+
const examples = tool.examples;
|
|
307
|
+
if (examples === void 0 || examples.length === 0) return void 0;
|
|
308
|
+
if (tool.examplesEagerlyRendered === false) return void 0;
|
|
309
|
+
return examples.slice(0, 5).map((ex) => ({
|
|
310
|
+
input: ex.input,
|
|
311
|
+
output: ex.output,
|
|
312
|
+
...ex.comment !== void 0 ? { comment: ex.comment } : {}
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
const PASSTHROUGH_SCHEMA = {
|
|
316
|
+
parse: (value) => value,
|
|
317
|
+
safeParse: (value) => ({
|
|
318
|
+
success: true,
|
|
319
|
+
data: value
|
|
320
|
+
}),
|
|
321
|
+
toJSON: () => ({ type: "object" })
|
|
322
|
+
};
|
|
323
|
+
function isDescribedFilter(value) {
|
|
324
|
+
return typeof value === "function" && "descriptor" in value && typeof value.descriptor === "object";
|
|
325
|
+
}
|
|
326
|
+
function buildHandoffTool(target) {
|
|
327
|
+
const cfg = target.config;
|
|
328
|
+
return {
|
|
329
|
+
name: `${HANDOFF_TOOL_PREFIX}${cfg.name}`,
|
|
330
|
+
description: `Hand off control to agent '${cfg.name}'.`,
|
|
331
|
+
inputSchema: PASSTHROUGH_SCHEMA,
|
|
332
|
+
sideEffectClass: "pure",
|
|
333
|
+
async execute() {
|
|
334
|
+
return `[handoff: ${cfg.name}]`;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function specToProvider(spec) {
|
|
339
|
+
if ("provider" in spec) return spec.provider;
|
|
340
|
+
return spec;
|
|
341
|
+
}
|
|
342
|
+
function handleProviderEvent(ev, state) {
|
|
343
|
+
switch (ev.type) {
|
|
344
|
+
case "stream-start": return {};
|
|
345
|
+
case "reasoning-delta":
|
|
346
|
+
state.reasoningBuffer += ev.delta;
|
|
347
|
+
return { emit: {
|
|
348
|
+
type: "reasoning.delta",
|
|
349
|
+
delta: ev.delta
|
|
350
|
+
} };
|
|
351
|
+
case "text-delta":
|
|
352
|
+
state.textBuffer += ev.delta;
|
|
353
|
+
return { emit: {
|
|
354
|
+
type: "text.delta",
|
|
355
|
+
delta: ev.delta
|
|
356
|
+
} };
|
|
357
|
+
case "tool-call-start":
|
|
358
|
+
state.calls.set(ev.toolCallId, {
|
|
359
|
+
toolCallId: ev.toolCallId,
|
|
360
|
+
toolName: ev.toolName,
|
|
361
|
+
argsBuffer: ""
|
|
362
|
+
});
|
|
363
|
+
return { emit: {
|
|
364
|
+
type: "tool.call.start",
|
|
365
|
+
toolCallId: ev.toolCallId,
|
|
366
|
+
toolName: ev.toolName,
|
|
367
|
+
args: void 0
|
|
368
|
+
} };
|
|
369
|
+
case "tool-call-input-delta": {
|
|
370
|
+
const acc = state.calls.get(ev.toolCallId);
|
|
371
|
+
if (acc !== void 0) acc.argsBuffer += ev.argsDelta;
|
|
372
|
+
return { emit: {
|
|
373
|
+
type: "tool.call.delta",
|
|
374
|
+
toolCallId: ev.toolCallId,
|
|
375
|
+
argsDelta: ev.argsDelta
|
|
376
|
+
} };
|
|
377
|
+
}
|
|
378
|
+
case "tool-call-end": {
|
|
379
|
+
const acc = state.calls.get(ev.toolCallId);
|
|
380
|
+
if (acc === void 0) {
|
|
381
|
+
process.stderr.write(`[graphorin/agent] dropped tool-call-end '${ev.toolCallId}' with no matching tool-call-start.\n`);
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
state.finalCalls.push({
|
|
385
|
+
toolCallId: ev.toolCallId,
|
|
386
|
+
toolName: acc.toolName,
|
|
387
|
+
args: ev.finalArgs
|
|
388
|
+
});
|
|
389
|
+
return { emit: {
|
|
390
|
+
type: "tool.call.end",
|
|
391
|
+
toolCallId: ev.toolCallId,
|
|
392
|
+
finalArgs: ev.finalArgs
|
|
393
|
+
} };
|
|
394
|
+
}
|
|
395
|
+
case "file": return { emit: {
|
|
396
|
+
type: "file.generated",
|
|
397
|
+
mimeType: ev.mimeType,
|
|
398
|
+
data: ev.data
|
|
399
|
+
} };
|
|
400
|
+
case "source": return { emit: {
|
|
401
|
+
type: "source.cited",
|
|
402
|
+
uri: ev.uri,
|
|
403
|
+
...ev.title !== void 0 ? { title: ev.title } : {}
|
|
404
|
+
} };
|
|
405
|
+
case "finish": return {
|
|
406
|
+
usage: ev.usage,
|
|
407
|
+
finished: true
|
|
408
|
+
};
|
|
409
|
+
case "error": return { providerError: ev.error };
|
|
410
|
+
default: return {};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Resolve the effective {@link ReasoningRetention} for a step. The
|
|
415
|
+
* agent-level setting wins over the provider-level default; when
|
|
416
|
+
* neither is supplied, the provider's `reasoningContract`
|
|
417
|
+
* capability drives the default per RB-42 / suggested DEC-158.
|
|
418
|
+
*/
|
|
419
|
+
function effectiveReasoningRetention(agentOverride, provider) {
|
|
420
|
+
if (agentOverride !== void 0) return agentOverride;
|
|
421
|
+
switch (provider.capabilities.reasoningContract) {
|
|
422
|
+
case "round-trip-required": return "pass-through-claude";
|
|
423
|
+
case "optional": return "pass-through-all";
|
|
424
|
+
case "hidden": return "strip";
|
|
425
|
+
default: return "strip";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Build the assistant message that the runtime appends to the
|
|
430
|
+
* message buffer after a successful provider call. When the
|
|
431
|
+
* effective {@link ReasoningRetention} is not `'strip'`, the
|
|
432
|
+
* assembled `reasoning` content parts ride along on `content` so
|
|
433
|
+
* the next provider call honours the wire-correct round-trip
|
|
434
|
+
* contract per RB-42.
|
|
435
|
+
*/
|
|
436
|
+
function buildAssistantMessage(text, reasoningParts, toolCalls, agentId, retention) {
|
|
437
|
+
if (retention !== "strip" && reasoningParts.length > 0) {
|
|
438
|
+
const parts = [...reasoningParts];
|
|
439
|
+
if (text.length > 0) parts.push({
|
|
440
|
+
type: "text",
|
|
441
|
+
text
|
|
442
|
+
});
|
|
443
|
+
return {
|
|
444
|
+
role: "assistant",
|
|
445
|
+
content: parts,
|
|
446
|
+
...toolCalls.length > 0 ? { toolCalls } : {},
|
|
447
|
+
agentId
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
role: "assistant",
|
|
452
|
+
content: text,
|
|
453
|
+
...toolCalls.length > 0 ? { toolCalls } : {},
|
|
454
|
+
agentId
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Strip every {@link ReasoningContent} part from each message in
|
|
459
|
+
* the supplied list. Used at the swap point when `prepareStep`
|
|
460
|
+
* downgrades the provider's `reasoningContract` mid-run.
|
|
461
|
+
*/
|
|
462
|
+
function stripReasoningFromMessages(messages) {
|
|
463
|
+
let stripped = 0;
|
|
464
|
+
for (let i = 0; i < messages.length; i++) {
|
|
465
|
+
const msg = messages[i];
|
|
466
|
+
if (msg === void 0) continue;
|
|
467
|
+
if (msg.role === "system" || msg.role === "tool") continue;
|
|
468
|
+
if (typeof msg.content === "string") continue;
|
|
469
|
+
const filtered = msg.content.filter((p) => p.type !== "reasoning");
|
|
470
|
+
if (filtered.length === msg.content.length) continue;
|
|
471
|
+
stripped += msg.content.length - filtered.length;
|
|
472
|
+
if (msg.role === "assistant") messages[i] = {
|
|
473
|
+
...msg,
|
|
474
|
+
content: filtered
|
|
475
|
+
};
|
|
476
|
+
else messages[i] = {
|
|
477
|
+
...msg,
|
|
478
|
+
content: filtered
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return { stripped };
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Count the leading contiguous run of `system` messages in the initial
|
|
485
|
+
* buffer — the trusted, KV-cache-stable instruction prefix. Captured
|
|
486
|
+
* once at run start (WI-09 / P1-1): auto-compaction summarises only the
|
|
487
|
+
* messages after this prefix, so the prefix stays byte-identical across
|
|
488
|
+
* every step (the provider's cache breakpoint is real) and a long run
|
|
489
|
+
* never re-pays for the system prompt.
|
|
490
|
+
*
|
|
491
|
+
* The length is fixed for the run rather than re-derived per compaction
|
|
492
|
+
* on purpose: each compaction inserts its summary as a `system` message
|
|
493
|
+
* right after the prefix, so re-scanning the leading run would absorb
|
|
494
|
+
* that summary into the prefix and shield it from the next compaction —
|
|
495
|
+
* summaries would stack unbounded. Pinning the original length keeps
|
|
496
|
+
* each prior summary inside the compactable body, where the next pass
|
|
497
|
+
* folds it into a fresh summary-of-summary.
|
|
498
|
+
*/
|
|
499
|
+
function countLeadingSystemMessages(messages) {
|
|
500
|
+
let i = 0;
|
|
501
|
+
while (i < messages.length && messages[i]?.role === "system") i += 1;
|
|
502
|
+
return i;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Build a fresh {@link Agent} from the supplied configuration.
|
|
506
|
+
*
|
|
507
|
+
* @stable
|
|
508
|
+
*/
|
|
509
|
+
function createAgent(config) {
|
|
510
|
+
if (typeof config.name !== "string" || config.name.length === 0) throw new InvalidAgentConfigError("missing 'name'");
|
|
511
|
+
if (config.provider === void 0 || config.provider === null) throw new InvalidAgentConfigError("missing 'provider'");
|
|
512
|
+
if (config.outputType?.kind === "text" && config.outputType.schema !== void 0) throw new InvalidAgentConfigError("outputType.kind 'text' with a schema — did you mean kind: 'structured'?");
|
|
513
|
+
validatePreferredModel(config.preferredModel);
|
|
514
|
+
if (config.modelTierMap !== void 0) for (const [tier, spec] of Object.entries(config.modelTierMap)) {
|
|
515
|
+
if (!isModelHintLike(tier)) throw new InvalidPreferredModelError({ tier });
|
|
516
|
+
if (spec === void 0) continue;
|
|
517
|
+
if (!isModelSpecLike(spec)) throw new InvalidPreferredModelError(spec);
|
|
518
|
+
}
|
|
519
|
+
if (config.fallbackModels !== void 0) {
|
|
520
|
+
for (const spec of config.fallbackModels) if (!isModelSpecLike(spec)) throw new InvalidPreferredModelError(spec);
|
|
521
|
+
}
|
|
522
|
+
const agentId = newId("agent");
|
|
523
|
+
const tracer = config.tracer ?? NOOP_TRACER;
|
|
524
|
+
const stopWhen = config.stopWhen ?? isStepCount(50);
|
|
525
|
+
const fallbackPolicy = config.fallbackPolicy ?? {};
|
|
526
|
+
const handoffMap = /* @__PURE__ */ new Map();
|
|
527
|
+
for (const entry of config.handoffs ?? []) {
|
|
528
|
+
const isWrappedHandoff = typeof entry === "object" && entry !== null && "target" in entry;
|
|
529
|
+
const subAgent = isWrappedHandoff ? entry.target : entry;
|
|
530
|
+
const userFilter = isWrappedHandoff ? entry.inputFilter : void 0;
|
|
531
|
+
const filter = isDescribedFilter(userFilter) ? userFilter : void 0;
|
|
532
|
+
const toolName = `${HANDOFF_TOOL_PREFIX}${subAgent.config.name}`;
|
|
533
|
+
handoffMap.set(toolName, {
|
|
534
|
+
agent: subAgent,
|
|
535
|
+
filter
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
let pendingSteer = [];
|
|
539
|
+
const pendingFollowUp = [];
|
|
540
|
+
let abortController;
|
|
541
|
+
let pendingAbort;
|
|
542
|
+
let activeRunState;
|
|
543
|
+
/** AG-11: guards the one-in-flight-run-per-instance invariant. */
|
|
544
|
+
let runInFlight = false;
|
|
545
|
+
const externalEventQueue = [];
|
|
546
|
+
const pendingManualCompacts = [];
|
|
547
|
+
const noopCompactionResult = (skippedReason) => ({
|
|
548
|
+
beforeTokens: 0,
|
|
549
|
+
afterTokens: 0,
|
|
550
|
+
summaryTokens: 0,
|
|
551
|
+
durationMs: 0,
|
|
552
|
+
hooksFiredCount: 0,
|
|
553
|
+
summary: "",
|
|
554
|
+
applied: false,
|
|
555
|
+
skippedReason
|
|
556
|
+
});
|
|
557
|
+
const memory = config.memory;
|
|
558
|
+
const progressIO = createProgressIO({ ...config.sensitivity !== void 0 ? { defaultSensitivity: config.sensitivity } : {} });
|
|
559
|
+
const toolRegistry = buildToolRegistry({
|
|
560
|
+
...config.tools !== void 0 ? { tools: config.tools } : {},
|
|
561
|
+
...config.skills !== void 0 ? { skills: config.skills } : {}
|
|
562
|
+
}).registry;
|
|
563
|
+
registerToolSearch(toolRegistry);
|
|
564
|
+
const spillWriter = createDefaultSpillWriter();
|
|
565
|
+
const fileResultReader = createFileResultReader({ artifactRoot: spillWriter.artifactRoot });
|
|
566
|
+
const externalResultReaders = config.resultReaders ?? [];
|
|
567
|
+
const resultReader = externalResultReaders.length === 0 ? fileResultReader : composeResultReaders([fileResultReader, ...externalResultReaders]);
|
|
568
|
+
registerReadResult(toolRegistry, resultReader, { force: externalResultReaders.length > 0 });
|
|
569
|
+
let activeExecutorBridge;
|
|
570
|
+
const toolApprovalGate = { request: async () => ({ granted: true }) };
|
|
571
|
+
const toolSecretResolver = buildSecretResolver();
|
|
572
|
+
const toolTokenCounter = buildToolTokenCounter();
|
|
573
|
+
const memoryGuardWiring = buildMemoryGuard(memory, memory === void 0 ? {} : { regionReader: createMemoryRegionReader(["working"], async (region) => {
|
|
574
|
+
if (region !== "working") return "";
|
|
575
|
+
const scope = {
|
|
576
|
+
userId: activeRunState?.userId ?? agentId,
|
|
577
|
+
...activeRunState?.sessionId !== void 0 ? { sessionId: activeRunState.sessionId } : {},
|
|
578
|
+
agentId
|
|
579
|
+
};
|
|
580
|
+
const working = memory.working;
|
|
581
|
+
if (working?.compile === void 0) return "";
|
|
582
|
+
try {
|
|
583
|
+
return await working.compile(scope, agentId);
|
|
584
|
+
} catch {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
}) });
|
|
588
|
+
const toolMemoryGuardFactory = memoryGuardWiring.memoryGuardFactory;
|
|
589
|
+
if (memory === void 0) {
|
|
590
|
+
const guarded = (config.tools ?? []).filter((t) => t.memoryGuardTier !== void 0);
|
|
591
|
+
if (guarded.length > 0) console.warn(`[graphorin/agent] ${guarded.length} tool(s) declare memoryGuardTier but no memory is wired — the DEC-153 snapshot/verify guard is skipped (SDF-1). Wire \`memory\` to activate it.`);
|
|
592
|
+
}
|
|
593
|
+
const toolDataFlowGuard = config.dataFlowPolicy !== void 0 && config.dataFlowPolicy.mode !== "off" ? buildDataFlowGuard(config.dataFlowPolicy) : void 0;
|
|
594
|
+
const toolStreamingSink = (event) => activeExecutorBridge?.sink(event);
|
|
595
|
+
const makeToolExecutor = (registry, opts) => createToolExecutor({
|
|
596
|
+
registry,
|
|
597
|
+
approvalGate: toolApprovalGate,
|
|
598
|
+
secretResolver: toolSecretResolver,
|
|
599
|
+
tokenCounter: toolTokenCounter,
|
|
600
|
+
memoryGuardFactory: toolMemoryGuardFactory,
|
|
601
|
+
...memoryGuardWiring.memoryRegionReader !== void 0 ? { memoryRegionReader: memoryGuardWiring.memoryRegionReader } : {},
|
|
602
|
+
spill: spillWriter,
|
|
603
|
+
...toolDataFlowGuard !== void 0 ? { dataFlowGuard: toolDataFlowGuard } : {},
|
|
604
|
+
...opts?.quiet === true ? {} : { streamingSink: toolStreamingSink },
|
|
605
|
+
...config.maxParallelTools !== void 0 ? { maxParallelTools: config.maxParallelTools } : {}
|
|
606
|
+
});
|
|
607
|
+
const toolExecutor = makeToolExecutor(toolRegistry);
|
|
608
|
+
const isCodeMode = config.toolInvocation === "code-mode";
|
|
609
|
+
let codeModeAdvertised = [];
|
|
610
|
+
if (isCodeMode) {
|
|
611
|
+
const reserved = new Set([
|
|
612
|
+
CODE_EXECUTE_NAME,
|
|
613
|
+
CODE_SEARCH_NAME,
|
|
614
|
+
TOOL_SEARCH_NAME,
|
|
615
|
+
READ_RESULT_NAME,
|
|
616
|
+
...handoffMap.keys()
|
|
617
|
+
]);
|
|
618
|
+
const metas = registerCodeMode(toolRegistry, makeToolExecutor(toolRegistry, { quiet: true }), reserved);
|
|
619
|
+
registerReadResult(toolRegistry, resultReader);
|
|
620
|
+
const readResult = toolRegistry.get(READ_RESULT_NAME);
|
|
621
|
+
codeModeAdvertised = [...metas, ...readResult !== void 0 ? [readResult] : []];
|
|
622
|
+
}
|
|
623
|
+
const causalityMonitor = config.causalityMonitor ? new CausalityMonitor(config.causalityMonitor) : void 0;
|
|
624
|
+
/**
|
|
625
|
+
* AG-11: one in-flight run per Agent instance. `steer` / `followUp` /
|
|
626
|
+
* `abort` / `compact` address "the run" with no run handle, so
|
|
627
|
+
* overlapping runs on one instance cannot be expressed safely — they
|
|
628
|
+
* would share the abort controller, steer queue, active-run ref and
|
|
629
|
+
* executor bridge. A second concurrent `run()` / `stream()` rejects
|
|
630
|
+
* with {@link ConcurrentRunError}; run-scoped state is reset on entry
|
|
631
|
+
* (a steer/abort queued after the previous run ended belongs to NO
|
|
632
|
+
* run) and cleared in a `finally` that also covers abandoned streams
|
|
633
|
+
* (consumer `break`) and thrown runs.
|
|
634
|
+
*/
|
|
635
|
+
async function* runLoop(input, options) {
|
|
636
|
+
if (runInFlight) throw new ConcurrentRunError();
|
|
637
|
+
runInFlight = true;
|
|
638
|
+
pendingSteer = [];
|
|
639
|
+
pendingAbort = void 0;
|
|
640
|
+
causalityMonitor?.reset();
|
|
641
|
+
try {
|
|
642
|
+
return yield* runLoopInner(input, options);
|
|
643
|
+
} finally {
|
|
644
|
+
runInFlight = false;
|
|
645
|
+
activeRunState = void 0;
|
|
646
|
+
while (pendingManualCompacts.length > 0) pendingManualCompacts.shift()?.resolve(noopCompactionResult("no-active-run"));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async function* runLoopInner(input, options) {
|
|
650
|
+
const { seed: rawSeed, resumed } = asMessages(input);
|
|
651
|
+
const seed = resumed === void 0 && pendingFollowUp.length > 0 ? [...pendingFollowUp.splice(0, pendingFollowUp.length), ...rawSeed] : rawSeed;
|
|
652
|
+
const sessionId = options.sessionId ?? config.sessionId ?? `session_${newId()}`;
|
|
653
|
+
const userId = options.userId ?? config.userId;
|
|
654
|
+
const localCtl = new AbortController();
|
|
655
|
+
abortController = localCtl;
|
|
656
|
+
const signal = localCtl.signal;
|
|
657
|
+
const parentSignal = options.signal;
|
|
658
|
+
const onParentAbort = () => localCtl.abort();
|
|
659
|
+
if (parentSignal !== void 0) if (parentSignal.aborted) localCtl.abort();
|
|
660
|
+
else parentSignal.addEventListener("abort", onParentAbort);
|
|
661
|
+
const usageAcc = new InMemoryUsageAccumulator();
|
|
662
|
+
const state = resumed ? resumed : createInitialRunState({
|
|
663
|
+
id: newId("run"),
|
|
664
|
+
agentId,
|
|
665
|
+
sessionId,
|
|
666
|
+
...userId !== void 0 ? { userId } : {}
|
|
667
|
+
});
|
|
668
|
+
activeRunState = state;
|
|
669
|
+
if (resumed && state.taintSummary !== void 0) toolDataFlowGuard?.seedLedger(state.id, state.taintSummary);
|
|
670
|
+
const messages = resumed ? [...state.messages] : [];
|
|
671
|
+
if (!resumed) {
|
|
672
|
+
const instructionsRaw = config.instructions;
|
|
673
|
+
let instructionsText;
|
|
674
|
+
if (typeof instructionsRaw === "string") instructionsText = instructionsRaw;
|
|
675
|
+
else instructionsText = await instructionsRaw({
|
|
676
|
+
runId: state.id,
|
|
677
|
+
sessionId,
|
|
678
|
+
...userId !== void 0 ? { userId } : {},
|
|
679
|
+
agentId,
|
|
680
|
+
deps: options.deps ?? config.deps,
|
|
681
|
+
tracer,
|
|
682
|
+
signal,
|
|
683
|
+
usage: usageAcc,
|
|
684
|
+
stepNumber: 0,
|
|
685
|
+
messages,
|
|
686
|
+
state
|
|
687
|
+
});
|
|
688
|
+
let systemPrompt = instructionsText;
|
|
689
|
+
if (config.autoAssembleContext === true && memory !== void 0) {
|
|
690
|
+
const lastUser = lastUserText(seed);
|
|
691
|
+
systemPrompt = (await memory.contextEngine.assemble(memory, {
|
|
692
|
+
scope: {
|
|
693
|
+
userId: userId ?? agentId,
|
|
694
|
+
sessionId,
|
|
695
|
+
agentId
|
|
696
|
+
},
|
|
697
|
+
agentId,
|
|
698
|
+
sessionId,
|
|
699
|
+
runId: state.id,
|
|
700
|
+
...instructionsText.length > 0 ? { agentInstructions: instructionsText } : {},
|
|
701
|
+
...lastUser !== void 0 ? { lastUserMessage: lastUser } : {}
|
|
702
|
+
})).systemMessage.content;
|
|
703
|
+
}
|
|
704
|
+
if (systemPrompt.length > 0) messages.push({
|
|
705
|
+
role: "system",
|
|
706
|
+
content: systemPrompt
|
|
707
|
+
});
|
|
708
|
+
messages.push(...seed);
|
|
709
|
+
for (const m of messages) state.messages.push(m);
|
|
710
|
+
}
|
|
711
|
+
const finalSnapshot = { output: "" };
|
|
712
|
+
yield {
|
|
713
|
+
type: "agent.start",
|
|
714
|
+
runId: state.id,
|
|
715
|
+
agentId
|
|
716
|
+
};
|
|
717
|
+
const inputGuards = config.guardrails?.input;
|
|
718
|
+
if (!resumed && inputGuards !== void 0 && inputGuards.length > 0) for (let i = 0; i < messages.length; i++) {
|
|
719
|
+
const msg = messages[i];
|
|
720
|
+
if (msg === void 0 || msg.role !== "user" || typeof msg.content !== "string") continue;
|
|
721
|
+
const composed = await composeGuardrails(inputGuards, msg.content, {
|
|
722
|
+
stage: "input",
|
|
723
|
+
runId: state.id,
|
|
724
|
+
sessionId,
|
|
725
|
+
agentId
|
|
726
|
+
});
|
|
727
|
+
if (!composed.ok) {
|
|
728
|
+
yield {
|
|
729
|
+
type: "guardrail.tripped",
|
|
730
|
+
guardrailName: composed.name,
|
|
731
|
+
phase: "input",
|
|
732
|
+
reason: composed.message
|
|
733
|
+
};
|
|
734
|
+
const message = `input guardrail '${composed.name}' blocked the run: ${composed.message}`;
|
|
735
|
+
yield {
|
|
736
|
+
type: "agent.error",
|
|
737
|
+
error: {
|
|
738
|
+
message,
|
|
739
|
+
code: "guardrail-blocked"
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
state.status = "failed";
|
|
743
|
+
state.error = {
|
|
744
|
+
message,
|
|
745
|
+
code: "guardrail-blocked"
|
|
746
|
+
};
|
|
747
|
+
return yield* finishRun(state, finalSnapshot);
|
|
748
|
+
}
|
|
749
|
+
if (composed.value !== msg.content) {
|
|
750
|
+
const rewritten = {
|
|
751
|
+
...msg,
|
|
752
|
+
content: composed.value
|
|
753
|
+
};
|
|
754
|
+
const stateIdx = state.messages.indexOf(msg);
|
|
755
|
+
messages[i] = rewritten;
|
|
756
|
+
if (stateIdx !== -1) state.messages[stateIdx] = rewritten;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const structuredInstruction = config.outputType?.kind === "structured" ? buildStructuredInstruction(config.outputType) : void 0;
|
|
760
|
+
const resumedApprovedCalls = [];
|
|
761
|
+
const journaledCallIds = /* @__PURE__ */ new Set();
|
|
762
|
+
for (const step of state.steps) for (const completed of step.toolCalls) journaledCallIds.add(completed.call.toolCallId);
|
|
763
|
+
if (resumed && options.directive?.approvals !== void 0 && state.pendingApprovals.length > 0) {
|
|
764
|
+
const decisions = new Map(options.directive.approvals.map((d) => [d.toolCallId, d]));
|
|
765
|
+
const stillPending = [];
|
|
766
|
+
for (const approval of state.pendingApprovals) {
|
|
767
|
+
const decision = decisions.get(approval.toolCallId);
|
|
768
|
+
if (decision === void 0) {
|
|
769
|
+
stillPending.push(approval);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (decision.granted) {
|
|
773
|
+
yield {
|
|
774
|
+
type: "tool.approval.granted",
|
|
775
|
+
toolCallId: approval.toolCallId
|
|
776
|
+
};
|
|
777
|
+
if (journaledCallIds.has(approval.toolCallId) && messages.some((m) => m.role === "tool" && m.toolCallId === approval.toolCallId)) continue;
|
|
778
|
+
resumedApprovedCalls.push({
|
|
779
|
+
toolCallId: approval.toolCallId,
|
|
780
|
+
toolName: approval.toolName,
|
|
781
|
+
args: approval.args
|
|
782
|
+
});
|
|
783
|
+
} else {
|
|
784
|
+
yield {
|
|
785
|
+
type: "tool.approval.denied",
|
|
786
|
+
toolCallId: approval.toolCallId,
|
|
787
|
+
...decision.reason !== void 0 ? { reason: decision.reason } : {}
|
|
788
|
+
};
|
|
789
|
+
messages.push({
|
|
790
|
+
role: "tool",
|
|
791
|
+
toolCallId: approval.toolCallId,
|
|
792
|
+
content: `Error: tool approval denied${decision.reason ? `: ${decision.reason}` : ""}`
|
|
793
|
+
});
|
|
794
|
+
state.messages.push({
|
|
795
|
+
role: "tool",
|
|
796
|
+
toolCallId: approval.toolCallId,
|
|
797
|
+
content: `Error: tool approval denied${decision.reason ? `: ${decision.reason}` : ""}`
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
state.pendingApprovals.splice(0, state.pendingApprovals.length, ...stillPending);
|
|
802
|
+
if (stillPending.length === 0) state.status = "running";
|
|
803
|
+
}
|
|
804
|
+
const systemPrefixLength = countLeadingSystemMessages(messages);
|
|
805
|
+
const runContextBase = {
|
|
806
|
+
runId: state.id,
|
|
807
|
+
sessionId,
|
|
808
|
+
...userId !== void 0 ? { userId } : {},
|
|
809
|
+
agentId,
|
|
810
|
+
deps: options.deps ?? config.deps,
|
|
811
|
+
tracer,
|
|
812
|
+
signal,
|
|
813
|
+
usage: usageAcc,
|
|
814
|
+
stepNumber: 0,
|
|
815
|
+
messages,
|
|
816
|
+
state
|
|
817
|
+
};
|
|
818
|
+
if (resumed && (state.status === "awaiting_approval" || state.status === "failed")) return yield* finishRun(state, finalSnapshot);
|
|
819
|
+
if (resumed && resumedApprovedCalls.length > 0) {
|
|
820
|
+
state.steps.push({
|
|
821
|
+
stepNumber: 0,
|
|
822
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
823
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
824
|
+
usage: zeroUsage(),
|
|
825
|
+
toolCalls: [],
|
|
826
|
+
agentId: state.currentAgentId
|
|
827
|
+
});
|
|
828
|
+
yield* dispatchBatch(resumedApprovedCalls, toolExecutor, {
|
|
829
|
+
...runContextBase,
|
|
830
|
+
stepNumber: 0,
|
|
831
|
+
messages
|
|
832
|
+
}, 0);
|
|
833
|
+
}
|
|
834
|
+
const promotedDeferred = /* @__PURE__ */ new Set();
|
|
835
|
+
if (resumed && state.promotedTools !== void 0) for (const name of state.promotedTools) promotedDeferred.add(name);
|
|
836
|
+
/**
|
|
837
|
+
* Dispatch a batch of (non-handoff) tool calls through the
|
|
838
|
+
* {@link ToolExecutor} and surface the results as `AgentEvent`s.
|
|
839
|
+
*
|
|
840
|
+
* The agent owns the `tool.execute.start` / `.end` / `.error`
|
|
841
|
+
* lifecycle (derived deterministically from the returned
|
|
842
|
+
* {@link CompletedToolCall} outcomes) so every outcome kind —
|
|
843
|
+
* success, unknown-tool, invalid-input, sanitization-blocked,
|
|
844
|
+
* execution error — yields a consistent event and tool message,
|
|
845
|
+
* preserving the pre-WI-03 stream shape (R10). The executor's
|
|
846
|
+
* genuinely-live streaming events (`tool.execute.progress` /
|
|
847
|
+
* `.partial`, emitted only by streaming-hint tools) are bridged
|
|
848
|
+
* through Adapter E while the batch runs and are purely additive.
|
|
849
|
+
*
|
|
850
|
+
* Parallelism (WI-04): the executor runs independent calls in this
|
|
851
|
+
* batch concurrently, bounded by `maxParallelTools`. `tool.execute.start`
|
|
852
|
+
* is emitted up-front in call order and `.end` / `.error` after the
|
|
853
|
+
* batch settles, also in call order — so result mapping and tool-message
|
|
854
|
+
* history are deterministic regardless of which call finishes first,
|
|
855
|
+
* while `.progress` / `.partial` events for concurrent calls interleave
|
|
856
|
+
* (keyed by `toolCallId`). Tools declaring `executionMode: 'sequential'`
|
|
857
|
+
* are serialised by the executor and never overlap.
|
|
858
|
+
*/
|
|
859
|
+
async function* dispatchBatch(calls, executor, runContext, stepNum) {
|
|
860
|
+
if (calls.length === 0) return;
|
|
861
|
+
for (const call of calls) yield {
|
|
862
|
+
type: "tool.execute.start",
|
|
863
|
+
toolCallId: call.toolCallId
|
|
864
|
+
};
|
|
865
|
+
const bridge = createExecutorEventBridge();
|
|
866
|
+
activeExecutorBridge = bridge;
|
|
867
|
+
const resultsPromise = executor.executeBatch({
|
|
868
|
+
calls,
|
|
869
|
+
runContext,
|
|
870
|
+
stepNumber: stepNum
|
|
871
|
+
});
|
|
872
|
+
const closeOnSettle = resultsPromise.then(() => bridge.close(), () => bridge.close());
|
|
873
|
+
for await (const event of bridge.drain()) if (event.type === "tool.execute.progress" || event.type === "tool.execute.partial") yield event;
|
|
874
|
+
await closeOnSettle;
|
|
875
|
+
activeExecutorBridge = void 0;
|
|
876
|
+
const completed = await resultsPromise;
|
|
877
|
+
const byCallId = new Map(completed.map((c) => [c.outcome.toolCallId, c]));
|
|
878
|
+
const stepEntry = state.steps[state.steps.length - 1];
|
|
879
|
+
for (const call of calls) {
|
|
880
|
+
const result = byCallId.get(call.toolCallId);
|
|
881
|
+
if (result === void 0) continue;
|
|
882
|
+
if (stepEntry !== void 0) stepEntry.toolCalls.push(result);
|
|
883
|
+
const outcome = result.outcome;
|
|
884
|
+
if ("kind" in outcome) {
|
|
885
|
+
yield {
|
|
886
|
+
type: "tool.execute.error",
|
|
887
|
+
toolCallId: call.toolCallId,
|
|
888
|
+
error: outcome
|
|
889
|
+
};
|
|
890
|
+
const text = `Error: ${outcome.message}`;
|
|
891
|
+
messages.push({
|
|
892
|
+
role: "tool",
|
|
893
|
+
toolCallId: call.toolCallId,
|
|
894
|
+
content: text
|
|
895
|
+
});
|
|
896
|
+
state.messages.push({
|
|
897
|
+
role: "tool",
|
|
898
|
+
toolCallId: call.toolCallId,
|
|
899
|
+
content: text
|
|
900
|
+
});
|
|
901
|
+
causalityMonitor?.recordCall(`tool.error:${call.toolName}`);
|
|
902
|
+
} else {
|
|
903
|
+
const output = outcome.output;
|
|
904
|
+
yield {
|
|
905
|
+
type: "tool.execute.end",
|
|
906
|
+
toolCallId: call.toolCallId,
|
|
907
|
+
result: output,
|
|
908
|
+
durationMs: outcome.durationMs
|
|
909
|
+
};
|
|
910
|
+
const handle = outcome.resultHandle;
|
|
911
|
+
const text = handle !== void 0 ? `${handle.preview}\n\n[Full result${handle.bytes !== void 0 ? ` (${handle.bytes} bytes)` : ""} stored behind a handle. Call read_result with handle ${JSON.stringify(handle.uri)} to retrieve it — optionally narrow with offset/length (bytes) or startLine/endLine.]` : typeof output === "string" ? output : JSON.stringify(output);
|
|
912
|
+
messages.push({
|
|
913
|
+
role: "tool",
|
|
914
|
+
toolCallId: call.toolCallId,
|
|
915
|
+
content: text
|
|
916
|
+
});
|
|
917
|
+
state.messages.push({
|
|
918
|
+
role: "tool",
|
|
919
|
+
toolCallId: call.toolCallId,
|
|
920
|
+
content: text
|
|
921
|
+
});
|
|
922
|
+
causalityMonitor?.recordCall(`tool:${call.toolName}`);
|
|
923
|
+
if (call.toolName === TOOL_SEARCH_NAME) recordToolSearchPromotions(output, promotedDeferred);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Auto-compaction trigger (WI-09 / P1-1). Before assembling each
|
|
929
|
+
* provider request, ask the memory {@link ContextEngine} whether the
|
|
930
|
+
* in-flight buffer has crossed its per-provider threshold
|
|
931
|
+
* (`shouldCompact`); when it has, summarise the older turns
|
|
932
|
+
* (`compactNow`, `source: 'auto-trigger'`), splice the result back in
|
|
933
|
+
* — preserving the byte-stable system prefix and the most-recent
|
|
934
|
+
* turns verbatim — and emit `context.compacted`. The compaction is
|
|
935
|
+
* configured on the memory facade (`createMemory({ contextEngine })`,
|
|
936
|
+
* RB-46); there is no parallel agent-level knob.
|
|
937
|
+
*
|
|
938
|
+
* No-op when no memory is wired, when compaction is disabled or below
|
|
939
|
+
* threshold (the engine returns `false`), or for `secret`-tier runs
|
|
940
|
+
* (secret history is never shipped to the summarizer — a less-trusted
|
|
941
|
+
* external sink; per-result handle references land in WI-10). Best
|
|
942
|
+
* effort: a misconfigured engine (e.g. no summarizer) is swallowed and
|
|
943
|
+
* the run proceeds uncompacted rather than aborting mid-flight.
|
|
944
|
+
*
|
|
945
|
+
* Operator-requested compactions (`agent.compact()`, CE-3/AG-13) are
|
|
946
|
+
* serviced here too, FIRST — the queue carries the `compact()` promise
|
|
947
|
+
* resolvers, and manual requests bypass the trigger evaluation.
|
|
948
|
+
*/
|
|
949
|
+
async function* maybeAutoCompact() {
|
|
950
|
+
while (pendingManualCompacts.length > 0) {
|
|
951
|
+
const pending = pendingManualCompacts.shift();
|
|
952
|
+
if (pending !== void 0) yield* serviceManualCompact(pending);
|
|
953
|
+
}
|
|
954
|
+
const mem = memory;
|
|
955
|
+
if (mem === void 0) return;
|
|
956
|
+
if (config.sensitivity === "secret") return;
|
|
957
|
+
const engine = mem.contextEngine;
|
|
958
|
+
if (!await engine.shouldCompact(messages).catch(() => false)) return;
|
|
959
|
+
const startedAt = Date.now();
|
|
960
|
+
const envelope = await engine.compactNow({
|
|
961
|
+
scope: {
|
|
962
|
+
userId: state.userId ?? agentId,
|
|
963
|
+
sessionId,
|
|
964
|
+
agentId
|
|
965
|
+
},
|
|
966
|
+
runId: state.id,
|
|
967
|
+
sessionId,
|
|
968
|
+
agentId,
|
|
969
|
+
source: "auto-trigger",
|
|
970
|
+
messages: messages.slice(systemPrefixLength),
|
|
971
|
+
memory: mem
|
|
972
|
+
}).catch(() => void 0);
|
|
973
|
+
if (envelope === void 0) return;
|
|
974
|
+
if (envelope.result.droppedMessageIndices.length === 0) return;
|
|
975
|
+
spliceCompacted(envelope);
|
|
976
|
+
yield {
|
|
977
|
+
type: "context.compacted",
|
|
978
|
+
runId: state.id,
|
|
979
|
+
sessionId,
|
|
980
|
+
agentId,
|
|
981
|
+
beforeTokens: envelope.result.beforeTokens,
|
|
982
|
+
afterTokens: envelope.result.afterTokens,
|
|
983
|
+
summaryTokens: envelope.result.summaryTokens,
|
|
984
|
+
durationMs: Date.now() - startedAt,
|
|
985
|
+
source: "auto-trigger",
|
|
986
|
+
hooksFiredCount: envelope.result.hooksFiredCount
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Prefix-pinned splice shared by the auto + manual compaction paths
|
|
991
|
+
* (CE-3): stable system prefix + [summary, ...recent turns], with the
|
|
992
|
+
* post-compaction hooks' text Context Essentials re-anchored as a
|
|
993
|
+
* trailing system message so they survive the trim (RB-46). Mutates
|
|
994
|
+
* BOTH the live loop buffer and `state.messages`.
|
|
995
|
+
*/
|
|
996
|
+
function spliceCompacted(envelope) {
|
|
997
|
+
const rebuilt = [...messages.slice(0, systemPrefixLength), ...envelope.result.trimmedMessages];
|
|
998
|
+
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");
|
|
999
|
+
if (essentials.length > 0) rebuilt.push({
|
|
1000
|
+
role: "system",
|
|
1001
|
+
content: essentials
|
|
1002
|
+
});
|
|
1003
|
+
messages.splice(0, messages.length, ...rebuilt);
|
|
1004
|
+
state.messages.splice(0, state.messages.length, ...rebuilt);
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Service one `agent.compact()` request inside the loop (CE-3/AG-13):
|
|
1008
|
+
* same prefix-pinned splice as the auto path, `source: 'manual'` (or
|
|
1009
|
+
* the caller's `'pre-step'`), `preserveRecentTurns` forwarded as a
|
|
1010
|
+
* per-call strategy override. An engine failure rejects the caller's
|
|
1011
|
+
* promise but never aborts the live run; a summarize that trims
|
|
1012
|
+
* nothing resolves `applied: false` without an event.
|
|
1013
|
+
*/
|
|
1014
|
+
async function* serviceManualCompact(pending) {
|
|
1015
|
+
const mem = memory;
|
|
1016
|
+
if (mem === void 0) {
|
|
1017
|
+
pending.resolve(noopCompactionResult("no-memory"));
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const source = pending.options?.source ?? "manual";
|
|
1021
|
+
const startedAt = Date.now();
|
|
1022
|
+
let envelope;
|
|
1023
|
+
try {
|
|
1024
|
+
envelope = await mem.contextEngine.compactNow({
|
|
1025
|
+
scope: {
|
|
1026
|
+
userId: state.userId ?? agentId,
|
|
1027
|
+
sessionId,
|
|
1028
|
+
agentId
|
|
1029
|
+
},
|
|
1030
|
+
runId: state.id,
|
|
1031
|
+
sessionId,
|
|
1032
|
+
agentId,
|
|
1033
|
+
source,
|
|
1034
|
+
messages: messages.slice(systemPrefixLength),
|
|
1035
|
+
memory: mem,
|
|
1036
|
+
...pending.options?.preserveRecentTurns !== void 0 ? { preserveRecentTurns: pending.options.preserveRecentTurns } : {}
|
|
1037
|
+
});
|
|
1038
|
+
} catch (cause) {
|
|
1039
|
+
pending.reject(cause);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const { result } = envelope;
|
|
1043
|
+
const applied = result.droppedMessageIndices.length > 0;
|
|
1044
|
+
if (applied) {
|
|
1045
|
+
spliceCompacted(envelope);
|
|
1046
|
+
yield {
|
|
1047
|
+
type: "context.compacted",
|
|
1048
|
+
runId: state.id,
|
|
1049
|
+
sessionId,
|
|
1050
|
+
agentId,
|
|
1051
|
+
beforeTokens: result.beforeTokens,
|
|
1052
|
+
afterTokens: result.afterTokens,
|
|
1053
|
+
summaryTokens: result.summaryTokens,
|
|
1054
|
+
durationMs: Date.now() - startedAt,
|
|
1055
|
+
source,
|
|
1056
|
+
hooksFiredCount: result.hooksFiredCount
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
pending.resolve({
|
|
1060
|
+
beforeTokens: result.beforeTokens,
|
|
1061
|
+
afterTokens: result.afterTokens,
|
|
1062
|
+
summaryTokens: result.summaryTokens,
|
|
1063
|
+
durationMs: Date.now() - startedAt,
|
|
1064
|
+
hooksFiredCount: result.hooksFiredCount,
|
|
1065
|
+
summary: result.summary ?? "",
|
|
1066
|
+
applied,
|
|
1067
|
+
...applied ? {} : { skippedReason: "nothing-to-trim" }
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
const handoffNames = Array.from(handoffMap.keys());
|
|
1071
|
+
let stepNumber = 0;
|
|
1072
|
+
let lastStepCalledToolNames = [];
|
|
1073
|
+
async function* emitCancellation() {
|
|
1074
|
+
yield {
|
|
1075
|
+
type: "agent.cancelling",
|
|
1076
|
+
runId: state.id,
|
|
1077
|
+
drain: pendingAbort?.drain ?? false,
|
|
1078
|
+
onPendingApprovals: pendingAbort?.onPendingApprovals ?? "deny"
|
|
1079
|
+
};
|
|
1080
|
+
const policy = pendingAbort?.onPendingApprovals ?? "deny";
|
|
1081
|
+
if (policy === "deny") {
|
|
1082
|
+
const drained = state.pendingApprovals.splice(0, state.pendingApprovals.length);
|
|
1083
|
+
for (const approval of drained) yield {
|
|
1084
|
+
type: "tool.approval.denied",
|
|
1085
|
+
toolCallId: approval.toolCallId,
|
|
1086
|
+
reason: "auto-denied: agent.abort()"
|
|
1087
|
+
};
|
|
1088
|
+
} else if (policy === "fail") {
|
|
1089
|
+
state.status = "failed";
|
|
1090
|
+
state.error = {
|
|
1091
|
+
message: "aborted with pending approvals",
|
|
1092
|
+
code: "run-aborted"
|
|
1093
|
+
};
|
|
1094
|
+
yield {
|
|
1095
|
+
type: "agent.error",
|
|
1096
|
+
error: {
|
|
1097
|
+
message: "aborted with pending approvals",
|
|
1098
|
+
code: "run-aborted"
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
state.status = "aborted";
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
while (!stopWhen.check(state)) {
|
|
1108
|
+
while (externalEventQueue.length > 0) {
|
|
1109
|
+
const ev = externalEventQueue.shift();
|
|
1110
|
+
if (ev !== void 0) yield ev;
|
|
1111
|
+
}
|
|
1112
|
+
if (signal.aborted) {
|
|
1113
|
+
if (yield* emitCancellation()) return yield* finishRun(state, finalSnapshot);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
stepNumber += 1;
|
|
1117
|
+
const stepStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
1118
|
+
if (pendingSteer.length > 0) {
|
|
1119
|
+
for (const m of pendingSteer) {
|
|
1120
|
+
messages.push(m);
|
|
1121
|
+
state.messages.push(m);
|
|
1122
|
+
}
|
|
1123
|
+
pendingSteer = [];
|
|
1124
|
+
}
|
|
1125
|
+
yield {
|
|
1126
|
+
type: "step.start",
|
|
1127
|
+
stepNumber
|
|
1128
|
+
};
|
|
1129
|
+
yield* maybeAutoCompact();
|
|
1130
|
+
const stepCtx = {
|
|
1131
|
+
...runContextBase,
|
|
1132
|
+
stepNumber,
|
|
1133
|
+
messages
|
|
1134
|
+
};
|
|
1135
|
+
const overrides = config.prepareStep ? await config.prepareStep(stepCtx) : {};
|
|
1136
|
+
const useOverrideRegistry = overrides.tools !== void 0 && !isCodeMode;
|
|
1137
|
+
const stepRegistry = useOverrideRegistry ? buildToolRegistry({ tools: overrides.tools }).registry : toolRegistry;
|
|
1138
|
+
if (useOverrideRegistry) {
|
|
1139
|
+
registerToolSearch(stepRegistry);
|
|
1140
|
+
registerReadResult(stepRegistry, resultReader);
|
|
1141
|
+
}
|
|
1142
|
+
const stepExecutor = useOverrideRegistry ? makeToolExecutor(stepRegistry) : toolExecutor;
|
|
1143
|
+
const handoffTools = handoffNames.map((n) => {
|
|
1144
|
+
const h = handoffMap.get(n);
|
|
1145
|
+
if (h === void 0) throw new ToolNotFoundError(n);
|
|
1146
|
+
return buildHandoffTool(h.agent);
|
|
1147
|
+
});
|
|
1148
|
+
let stepTools;
|
|
1149
|
+
if (isCodeMode) stepTools = [...codeModeAdvertised, ...handoffTools];
|
|
1150
|
+
else {
|
|
1151
|
+
const eagerTools = stepRegistry.listEager();
|
|
1152
|
+
const promotedTools = promotedDeferred.size === 0 ? [] : orderPromotedTools(promotedDeferred, stepRegistry.listDeferred());
|
|
1153
|
+
stepTools = [
|
|
1154
|
+
...eagerTools,
|
|
1155
|
+
...promotedTools,
|
|
1156
|
+
...handoffTools
|
|
1157
|
+
];
|
|
1158
|
+
}
|
|
1159
|
+
const calledLastStep = new Set(lastStepCalledToolNames);
|
|
1160
|
+
const toolPreferences = stepTools.filter((t) => calledLastStep.has(t.name)).map((t) => {
|
|
1161
|
+
return t.preferredModel;
|
|
1162
|
+
});
|
|
1163
|
+
const primary = resolvePreferredModel({
|
|
1164
|
+
...overrides.provider !== void 0 ? { prepareStepProvider: overrides.provider } : {},
|
|
1165
|
+
toolPreferredModels: toolPreferences,
|
|
1166
|
+
...config.preferredModel !== void 0 ? { agentPreferredModel: config.preferredModel } : {},
|
|
1167
|
+
agentDefaultProvider: config.provider,
|
|
1168
|
+
...config.modelTierMap !== void 0 ? { modelTierMap: config.modelTierMap } : {}
|
|
1169
|
+
});
|
|
1170
|
+
const fallbackChain = primary.source === "prepare-step" ? [primary.resolvedProvider] : [primary.resolvedProvider, ...(config.fallbackModels ?? []).map(specToProvider)];
|
|
1171
|
+
const reasoningPolicy = effectiveReasoningRetention(config.reasoningRetention, primary.resolvedProvider);
|
|
1172
|
+
if (reasoningPolicy === "strip") {
|
|
1173
|
+
const { stripped } = stripReasoningFromMessages(messages);
|
|
1174
|
+
if (stripped > 0) stripReasoningFromMessages(state.messages);
|
|
1175
|
+
}
|
|
1176
|
+
const toolDefs = stepTools.map((t) => toolToDefinition(t));
|
|
1177
|
+
const baseRequest = {
|
|
1178
|
+
messages: structuredInstruction === void 0 ? messages : [...messages, {
|
|
1179
|
+
role: "system",
|
|
1180
|
+
content: structuredInstruction
|
|
1181
|
+
}],
|
|
1182
|
+
...config.outputType !== void 0 ? { outputType: {
|
|
1183
|
+
kind: config.outputType.kind,
|
|
1184
|
+
...config.outputType.description !== void 0 ? { description: config.outputType.description } : {},
|
|
1185
|
+
...config.outputType.jsonSchema !== void 0 ? { jsonSchema: config.outputType.jsonSchema } : {}
|
|
1186
|
+
} } : {},
|
|
1187
|
+
tools: toolDefs,
|
|
1188
|
+
...overrides.toolChoice !== void 0 ? { toolChoice: overrides.toolChoice } : config.toolChoice !== void 0 ? { toolChoice: config.toolChoice } : {},
|
|
1189
|
+
metadata: {
|
|
1190
|
+
sessionId,
|
|
1191
|
+
agentId,
|
|
1192
|
+
...userId !== void 0 ? { userId } : {},
|
|
1193
|
+
runId: state.id,
|
|
1194
|
+
stepNumber
|
|
1195
|
+
},
|
|
1196
|
+
signal,
|
|
1197
|
+
...overrides.temperature !== void 0 ? { temperature: overrides.temperature } : {},
|
|
1198
|
+
...overrides.maxTokens !== void 0 ? { maxTokens: overrides.maxTokens } : {},
|
|
1199
|
+
reasoningRetention: reasoningPolicy
|
|
1200
|
+
};
|
|
1201
|
+
const stepUsage = zeroUsage();
|
|
1202
|
+
let attempt = 0;
|
|
1203
|
+
let textBuffer = "";
|
|
1204
|
+
let providerForStep = primary.resolvedProvider;
|
|
1205
|
+
let lastModelId = primary.resolvedModelId;
|
|
1206
|
+
let modelSucceeded = false;
|
|
1207
|
+
let lastError;
|
|
1208
|
+
let finalCalls = [];
|
|
1209
|
+
let stepReasoningParts = [];
|
|
1210
|
+
for (let chainIdx = 0; chainIdx < fallbackChain.length; chainIdx++) {
|
|
1211
|
+
const candidate = fallbackChain[chainIdx];
|
|
1212
|
+
if (candidate === void 0) continue;
|
|
1213
|
+
providerForStep = candidate;
|
|
1214
|
+
const providerModelId = providerForStep.modelId;
|
|
1215
|
+
if (chainIdx > 0) {
|
|
1216
|
+
attempt += 1;
|
|
1217
|
+
const reason = lastError ? isAgentFallbackEligible(lastError, fallbackPolicy).reason ?? "transient" : "transient";
|
|
1218
|
+
yield {
|
|
1219
|
+
type: "agent.model.fellback",
|
|
1220
|
+
runId: state.id,
|
|
1221
|
+
sessionId,
|
|
1222
|
+
agentId,
|
|
1223
|
+
from: lastModelId,
|
|
1224
|
+
to: providerModelId,
|
|
1225
|
+
reason,
|
|
1226
|
+
stepNumber,
|
|
1227
|
+
attempt
|
|
1228
|
+
};
|
|
1229
|
+
lastModelId = providerModelId;
|
|
1230
|
+
}
|
|
1231
|
+
const evState = {
|
|
1232
|
+
textBuffer: "",
|
|
1233
|
+
reasoningBuffer: "",
|
|
1234
|
+
reasoningParts: [],
|
|
1235
|
+
calls: /* @__PURE__ */ new Map(),
|
|
1236
|
+
finalCalls: []
|
|
1237
|
+
};
|
|
1238
|
+
let providerError;
|
|
1239
|
+
let providerCallCompleted = false;
|
|
1240
|
+
let providerStepUsage = zeroUsage();
|
|
1241
|
+
try {
|
|
1242
|
+
const stream$1 = providerForStep.stream(baseRequest);
|
|
1243
|
+
for await (const ev of stream$1) {
|
|
1244
|
+
if (signal.aborted && pendingAbort?.drain !== true) throw new AgentRuntimeError("run-aborted", "aborted");
|
|
1245
|
+
const out = handleProviderEvent(ev, evState);
|
|
1246
|
+
if (out.emit !== void 0) yield out.emit;
|
|
1247
|
+
if (out.providerError !== void 0) providerError = out.providerError;
|
|
1248
|
+
if (out.usage !== void 0) providerStepUsage = {
|
|
1249
|
+
promptTokens: providerStepUsage.promptTokens + out.usage.promptTokens,
|
|
1250
|
+
completionTokens: providerStepUsage.completionTokens + out.usage.completionTokens,
|
|
1251
|
+
totalTokens: providerStepUsage.totalTokens + out.usage.totalTokens,
|
|
1252
|
+
...out.usage.reasoningTokens !== void 0 || providerStepUsage.reasoningTokens !== void 0 ? { reasoningTokens: (providerStepUsage.reasoningTokens ?? 0) + (out.usage.reasoningTokens ?? 0) } : {}
|
|
1253
|
+
};
|
|
1254
|
+
if (out.finished === true) providerCallCompleted = true;
|
|
1255
|
+
}
|
|
1256
|
+
} catch (cause) {
|
|
1257
|
+
if (signal.aborted || cause instanceof AgentRuntimeError && cause.code === "run-aborted") break;
|
|
1258
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1259
|
+
providerError = {
|
|
1260
|
+
kind: classifyThrownProviderErrorKind(cause),
|
|
1261
|
+
message,
|
|
1262
|
+
cause
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
if (providerError !== void 0) {
|
|
1266
|
+
lastError = providerError;
|
|
1267
|
+
if (!isAgentFallbackEligible(providerError, fallbackPolicy).eligible || chainIdx === fallbackChain.length - 1) {
|
|
1268
|
+
yield {
|
|
1269
|
+
type: "agent.error",
|
|
1270
|
+
error: {
|
|
1271
|
+
message: providerError.message,
|
|
1272
|
+
code: providerError.kind
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
state.status = "failed";
|
|
1276
|
+
state.error = {
|
|
1277
|
+
message: providerError.message,
|
|
1278
|
+
code: providerError.kind
|
|
1279
|
+
};
|
|
1280
|
+
return yield* finishRun(state, finalSnapshot);
|
|
1281
|
+
}
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (providerCallCompleted) {
|
|
1285
|
+
modelSucceeded = true;
|
|
1286
|
+
textBuffer = evState.textBuffer;
|
|
1287
|
+
finalCalls = evState.finalCalls;
|
|
1288
|
+
if (evState.reasoningBuffer.length > 0) stepReasoningParts = [{
|
|
1289
|
+
type: "reasoning",
|
|
1290
|
+
text: evState.reasoningBuffer
|
|
1291
|
+
}];
|
|
1292
|
+
stepUsage.promptTokens += providerStepUsage.promptTokens;
|
|
1293
|
+
stepUsage.completionTokens += providerStepUsage.completionTokens;
|
|
1294
|
+
stepUsage.totalTokens += providerStepUsage.totalTokens;
|
|
1295
|
+
if (providerStepUsage.reasoningTokens !== void 0) stepUsage.reasoningTokens = (stepUsage.reasoningTokens ?? 0) + providerStepUsage.reasoningTokens;
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (signal.aborted && !modelSucceeded) {
|
|
1300
|
+
yield* emitCancellation();
|
|
1301
|
+
return yield* finishRun(state, finalSnapshot);
|
|
1302
|
+
}
|
|
1303
|
+
if (!modelSucceeded) {
|
|
1304
|
+
yield {
|
|
1305
|
+
type: "agent.error",
|
|
1306
|
+
error: {
|
|
1307
|
+
message: "all configured providers failed without finishing",
|
|
1308
|
+
code: "no-provider-completed"
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
state.status = "failed";
|
|
1312
|
+
state.error = {
|
|
1313
|
+
message: "no provider completed",
|
|
1314
|
+
code: "no-provider-completed"
|
|
1315
|
+
};
|
|
1316
|
+
return yield* finishRun(state, finalSnapshot);
|
|
1317
|
+
}
|
|
1318
|
+
usageAcc.add(lastModelId, stepUsage);
|
|
1319
|
+
addModelUsage(state, lastModelId, stepUsage);
|
|
1320
|
+
state.usage.promptTokens += stepUsage.promptTokens;
|
|
1321
|
+
state.usage.completionTokens += stepUsage.completionTokens;
|
|
1322
|
+
state.usage.totalTokens += stepUsage.totalTokens;
|
|
1323
|
+
if (stepUsage.reasoningTokens !== void 0) state.usage.reasoningTokens = (state.usage.reasoningTokens ?? 0) + stepUsage.reasoningTokens;
|
|
1324
|
+
const leakCheck = causalityMonitor !== void 0 && textBuffer.length > 0 ? causalityMonitor.checkMessage(textBuffer) : void 0;
|
|
1325
|
+
const leakBlocked = leakCheck?.leakDetected === true && leakCheck.decision === "block";
|
|
1326
|
+
const assistant = buildAssistantMessage(leakBlocked ? LATERAL_LEAK_BLOCKED_NOTICE : textBuffer, stepReasoningParts, finalCalls, agentId, reasoningPolicy);
|
|
1327
|
+
messages.push(assistant);
|
|
1328
|
+
state.messages.push(assistant);
|
|
1329
|
+
if (leakCheck?.leakDetected === true) {
|
|
1330
|
+
const sha = sha256Hex(textBuffer);
|
|
1331
|
+
yield {
|
|
1332
|
+
type: "agent.lateral-leak.detected",
|
|
1333
|
+
runId: state.id,
|
|
1334
|
+
sessionId,
|
|
1335
|
+
agentId,
|
|
1336
|
+
vector: leakCheck.vector,
|
|
1337
|
+
severity: leakCheck.severity,
|
|
1338
|
+
causalityChain: leakCheck.causalityChain,
|
|
1339
|
+
messageContentSha256: sha,
|
|
1340
|
+
...leakCheck.matchedPattern !== void 0 ? { matchedPattern: leakCheck.matchedPattern } : {},
|
|
1341
|
+
decision: leakCheck.decision,
|
|
1342
|
+
detectedAtIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
const handoffCalls = finalCalls.filter((c) => handoffMap.has(c.toolName));
|
|
1346
|
+
if (handoffCalls.length > 1) throw new MultipleHandoffsInStepError(handoffCalls.map((c) => c.toolName));
|
|
1347
|
+
const stepRecord = {
|
|
1348
|
+
stepNumber,
|
|
1349
|
+
startedAt: stepStart,
|
|
1350
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1351
|
+
usage: stepUsage,
|
|
1352
|
+
toolCalls: [],
|
|
1353
|
+
agentId: state.currentAgentId
|
|
1354
|
+
};
|
|
1355
|
+
state.steps.push(stepRecord);
|
|
1356
|
+
lastStepCalledToolNames = finalCalls.map((c) => c.toolName);
|
|
1357
|
+
if (textBuffer.length > 0 && !leakBlocked) {
|
|
1358
|
+
finalSnapshot.output = textBuffer;
|
|
1359
|
+
yield {
|
|
1360
|
+
type: "text.complete",
|
|
1361
|
+
text: textBuffer
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
if (finalCalls.length > 0) {
|
|
1365
|
+
const execRunContext = {
|
|
1366
|
+
...runContextBase,
|
|
1367
|
+
stepNumber,
|
|
1368
|
+
messages
|
|
1369
|
+
};
|
|
1370
|
+
let batch = [];
|
|
1371
|
+
for (const call of finalCalls) {
|
|
1372
|
+
const handoff = handoffMap.get(call.toolName);
|
|
1373
|
+
if (handoff !== void 0) {
|
|
1374
|
+
if (batch.length > 0) {
|
|
1375
|
+
yield* dispatchBatch(batch, stepExecutor, execRunContext, stepNumber);
|
|
1376
|
+
batch = [];
|
|
1377
|
+
}
|
|
1378
|
+
yield {
|
|
1379
|
+
type: "tool.execute.start",
|
|
1380
|
+
toolCallId: call.toolCallId
|
|
1381
|
+
};
|
|
1382
|
+
const filter = handoff.filter ?? filters.defaultHandoffFilter();
|
|
1383
|
+
const filtered = filter(messages);
|
|
1384
|
+
const targetId = handoff.agent.id;
|
|
1385
|
+
const handoffRec = {
|
|
1386
|
+
fromAgentId: agentId,
|
|
1387
|
+
toAgentId: targetId,
|
|
1388
|
+
stepNumber,
|
|
1389
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1390
|
+
inputFilter: filter.descriptor,
|
|
1391
|
+
secretsInheritance: "inherit-allowlist",
|
|
1392
|
+
inheritedSecrets: []
|
|
1393
|
+
};
|
|
1394
|
+
state.handoffs.push(handoffRec);
|
|
1395
|
+
yield {
|
|
1396
|
+
type: "handoff",
|
|
1397
|
+
fromAgentId: agentId,
|
|
1398
|
+
toAgentId: targetId
|
|
1399
|
+
};
|
|
1400
|
+
state.currentAgentId = targetId;
|
|
1401
|
+
const subAgent = handoff.agent;
|
|
1402
|
+
const subStart = Date.now();
|
|
1403
|
+
const subOutputs = [];
|
|
1404
|
+
let subResult;
|
|
1405
|
+
const subStream = subAgent.stream(filtered, {
|
|
1406
|
+
signal,
|
|
1407
|
+
...options.deps !== void 0 || config.deps !== void 0 ? { deps: options.deps ?? config.deps } : {},
|
|
1408
|
+
sessionId
|
|
1409
|
+
});
|
|
1410
|
+
for await (const subEv of subStream) if (subEv.type === "text.complete") subOutputs.push(subEv.text);
|
|
1411
|
+
else if (subEv.type === "agent.end") subResult = subEv.result;
|
|
1412
|
+
const subDurationMs = Date.now() - subStart;
|
|
1413
|
+
const stepEntry = state.steps[state.steps.length - 1];
|
|
1414
|
+
if (subResult !== void 0 && subResult.status !== "completed") {
|
|
1415
|
+
const toolError = {
|
|
1416
|
+
toolCallId: call.toolCallId,
|
|
1417
|
+
toolName: call.toolName,
|
|
1418
|
+
kind: subResult.status === "aborted" ? "aborted" : "execution_failed",
|
|
1419
|
+
message: `handoff to '${targetId}' ${subResult.status}${subResult.error !== void 0 ? `: ${subResult.error.message}` : ""}`
|
|
1420
|
+
};
|
|
1421
|
+
if (stepEntry !== void 0) stepEntry.toolCalls.push({
|
|
1422
|
+
call,
|
|
1423
|
+
outcome: toolError,
|
|
1424
|
+
stepNumber
|
|
1425
|
+
});
|
|
1426
|
+
yield {
|
|
1427
|
+
type: "tool.execute.error",
|
|
1428
|
+
toolCallId: call.toolCallId,
|
|
1429
|
+
error: toolError
|
|
1430
|
+
};
|
|
1431
|
+
const text = `Error: ${toolError.message}`;
|
|
1432
|
+
messages.push({
|
|
1433
|
+
role: "tool",
|
|
1434
|
+
toolCallId: call.toolCallId,
|
|
1435
|
+
content: text
|
|
1436
|
+
});
|
|
1437
|
+
state.messages.push({
|
|
1438
|
+
role: "tool",
|
|
1439
|
+
toolCallId: call.toolCallId,
|
|
1440
|
+
content: text
|
|
1441
|
+
});
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
const result = subOutputs.join("");
|
|
1445
|
+
const completed = {
|
|
1446
|
+
call,
|
|
1447
|
+
outcome: {
|
|
1448
|
+
toolCallId: call.toolCallId,
|
|
1449
|
+
toolName: call.toolName,
|
|
1450
|
+
output: result,
|
|
1451
|
+
durationMs: subDurationMs
|
|
1452
|
+
},
|
|
1453
|
+
stepNumber
|
|
1454
|
+
};
|
|
1455
|
+
if (stepEntry !== void 0) stepEntry.toolCalls.push(completed);
|
|
1456
|
+
yield {
|
|
1457
|
+
type: "tool.execute.end",
|
|
1458
|
+
toolCallId: call.toolCallId,
|
|
1459
|
+
result,
|
|
1460
|
+
durationMs: subDurationMs
|
|
1461
|
+
};
|
|
1462
|
+
messages.push({
|
|
1463
|
+
role: "tool",
|
|
1464
|
+
toolCallId: call.toolCallId,
|
|
1465
|
+
content: result
|
|
1466
|
+
});
|
|
1467
|
+
state.messages.push({
|
|
1468
|
+
role: "tool",
|
|
1469
|
+
toolCallId: call.toolCallId,
|
|
1470
|
+
content: result
|
|
1471
|
+
});
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (await invokeNeedsApproval(stepRegistry.get(call.toolName), call.args, execRunContext, signal)) {
|
|
1475
|
+
if (batch.length > 0) {
|
|
1476
|
+
yield* dispatchBatch(batch, stepExecutor, execRunContext, stepNumber);
|
|
1477
|
+
batch = [];
|
|
1478
|
+
}
|
|
1479
|
+
yield {
|
|
1480
|
+
type: "tool.execute.start",
|
|
1481
|
+
toolCallId: call.toolCallId
|
|
1482
|
+
};
|
|
1483
|
+
const approval = {
|
|
1484
|
+
toolCallId: call.toolCallId,
|
|
1485
|
+
toolName: call.toolName,
|
|
1486
|
+
args: call.args,
|
|
1487
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1488
|
+
};
|
|
1489
|
+
state.pendingApprovals.push(approval);
|
|
1490
|
+
state.status = "awaiting_approval";
|
|
1491
|
+
const taintSnap = toolDataFlowGuard?.snapshotLedger(state.id);
|
|
1492
|
+
if (taintSnap !== void 0) state.taintSummary = taintSnap;
|
|
1493
|
+
if (promotedDeferred.size > 0) state.promotedTools = [...promotedDeferred];
|
|
1494
|
+
yield {
|
|
1495
|
+
type: "tool.approval.requested",
|
|
1496
|
+
toolCallId: call.toolCallId
|
|
1497
|
+
};
|
|
1498
|
+
if (config.checkpointStore !== void 0) await config.checkpointStore.put(state.id, "agent", {
|
|
1499
|
+
id: state.id,
|
|
1500
|
+
threadId: state.id,
|
|
1501
|
+
namespace: "agent",
|
|
1502
|
+
state: serializeRunState(state, { stripTracingApiKey: true }),
|
|
1503
|
+
channelVersions: {},
|
|
1504
|
+
stepNumber,
|
|
1505
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1506
|
+
}, {
|
|
1507
|
+
source: "sync",
|
|
1508
|
+
status: "suspended",
|
|
1509
|
+
nodeName: "agent.run"
|
|
1510
|
+
});
|
|
1511
|
+
return yield* finishRun(state, finalSnapshot);
|
|
1512
|
+
}
|
|
1513
|
+
batch.push(call);
|
|
1514
|
+
}
|
|
1515
|
+
if (batch.length > 0) yield* dispatchBatch(batch, stepExecutor, execRunContext, stepNumber);
|
|
1516
|
+
}
|
|
1517
|
+
yield {
|
|
1518
|
+
type: "step.end",
|
|
1519
|
+
stepNumber,
|
|
1520
|
+
usage: stepUsage
|
|
1521
|
+
};
|
|
1522
|
+
if (finalCalls.length === 0) {
|
|
1523
|
+
state.status = "completed";
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
} catch (cause) {
|
|
1528
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1529
|
+
const code = cause instanceof AgentRuntimeError ? cause.code : "unknown";
|
|
1530
|
+
yield {
|
|
1531
|
+
type: "agent.error",
|
|
1532
|
+
error: {
|
|
1533
|
+
message,
|
|
1534
|
+
code
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
state.status = "failed";
|
|
1538
|
+
state.error = {
|
|
1539
|
+
message,
|
|
1540
|
+
code
|
|
1541
|
+
};
|
|
1542
|
+
return yield* finishRun(state, finalSnapshot);
|
|
1543
|
+
} finally {
|
|
1544
|
+
if (parentSignal !== void 0) parentSignal.removeEventListener("abort", onParentAbort);
|
|
1545
|
+
}
|
|
1546
|
+
if (state.status === "running") {
|
|
1547
|
+
const message = `run stopped by stop condition: ${stopWhen.description}`;
|
|
1548
|
+
state.status = "failed";
|
|
1549
|
+
state.error = {
|
|
1550
|
+
message,
|
|
1551
|
+
code: "stop-condition"
|
|
1552
|
+
};
|
|
1553
|
+
yield {
|
|
1554
|
+
type: "agent.error",
|
|
1555
|
+
error: {
|
|
1556
|
+
message,
|
|
1557
|
+
code: "stop-condition"
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
if (state.status === "completed" && config.outputType?.kind === "structured") {
|
|
1562
|
+
const raw = String(finalSnapshot.output ?? "");
|
|
1563
|
+
try {
|
|
1564
|
+
const parsed = JSON.parse(stripJsonFence(raw));
|
|
1565
|
+
finalSnapshot.output = config.outputType.schema !== void 0 ? config.outputType.schema.parse(parsed) : parsed;
|
|
1566
|
+
} catch (cause) {
|
|
1567
|
+
const message = `structured output validation failed: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
1568
|
+
yield {
|
|
1569
|
+
type: "agent.error",
|
|
1570
|
+
error: {
|
|
1571
|
+
message,
|
|
1572
|
+
code: "output-validation-failed"
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
state.status = "failed";
|
|
1576
|
+
state.error = {
|
|
1577
|
+
message,
|
|
1578
|
+
code: "output-validation-failed"
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
const outputGuards = config.guardrails?.output;
|
|
1583
|
+
if (state.status === "completed" && outputGuards !== void 0 && outputGuards.length > 0) {
|
|
1584
|
+
const composed = await composeGuardrails(outputGuards, finalSnapshot.output, {
|
|
1585
|
+
stage: "output",
|
|
1586
|
+
runId: state.id,
|
|
1587
|
+
sessionId,
|
|
1588
|
+
agentId
|
|
1589
|
+
});
|
|
1590
|
+
if (!composed.ok) {
|
|
1591
|
+
yield {
|
|
1592
|
+
type: "guardrail.tripped",
|
|
1593
|
+
guardrailName: composed.name,
|
|
1594
|
+
phase: "output",
|
|
1595
|
+
reason: composed.message
|
|
1596
|
+
};
|
|
1597
|
+
const message = `output guardrail '${composed.name}' blocked the run: ${composed.message}`;
|
|
1598
|
+
yield {
|
|
1599
|
+
type: "agent.error",
|
|
1600
|
+
error: {
|
|
1601
|
+
message,
|
|
1602
|
+
code: "guardrail-blocked"
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
state.status = "failed";
|
|
1606
|
+
state.error = {
|
|
1607
|
+
message,
|
|
1608
|
+
code: "guardrail-blocked"
|
|
1609
|
+
};
|
|
1610
|
+
} else if (composed.value !== finalSnapshot.output) finalSnapshot.output = composed.value;
|
|
1611
|
+
}
|
|
1612
|
+
activeRunState = void 0;
|
|
1613
|
+
return yield* finishRun(state, finalSnapshot);
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Terminal wrapper around {@link finalize}: every exit path of the run
|
|
1617
|
+
* loop — completed, failed, aborted, suspended — ends the stream with
|
|
1618
|
+
* an `agent.end` event carrying the final {@link AgentResult} (AG-20).
|
|
1619
|
+
*/
|
|
1620
|
+
async function* finishRun(state, snapshot) {
|
|
1621
|
+
while (pendingManualCompacts.length > 0) pendingManualCompacts.shift()?.resolve(noopCompactionResult("no-active-run"));
|
|
1622
|
+
const result = finalize(state, snapshot);
|
|
1623
|
+
if (result.status === "completed" || result.status === "failed") await spillWriter.clear?.(result.state.id).catch(() => {});
|
|
1624
|
+
yield {
|
|
1625
|
+
type: "agent.end",
|
|
1626
|
+
runId: result.state.id,
|
|
1627
|
+
result
|
|
1628
|
+
};
|
|
1629
|
+
return result;
|
|
1630
|
+
}
|
|
1631
|
+
function finalize(state, snapshot) {
|
|
1632
|
+
state.finishedAt = state.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1633
|
+
return {
|
|
1634
|
+
output: snapshot.output,
|
|
1635
|
+
usage: state.usage,
|
|
1636
|
+
status: state.status,
|
|
1637
|
+
...state.error !== void 0 ? { error: state.error } : {},
|
|
1638
|
+
state
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
const stream = (input, options) => {
|
|
1642
|
+
const opts = options ?? {};
|
|
1643
|
+
return { [Symbol.asyncIterator]: () => {
|
|
1644
|
+
const gen = runLoop(input, opts);
|
|
1645
|
+
return {
|
|
1646
|
+
async next() {
|
|
1647
|
+
const r = await gen.next();
|
|
1648
|
+
if (r.done === true) return {
|
|
1649
|
+
done: true,
|
|
1650
|
+
value: void 0
|
|
1651
|
+
};
|
|
1652
|
+
return {
|
|
1653
|
+
done: false,
|
|
1654
|
+
value: r.value
|
|
1655
|
+
};
|
|
1656
|
+
},
|
|
1657
|
+
async return() {
|
|
1658
|
+
await gen.return(void 0);
|
|
1659
|
+
return {
|
|
1660
|
+
done: true,
|
|
1661
|
+
value: void 0
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
} };
|
|
1666
|
+
};
|
|
1667
|
+
const run = async (input, options) => {
|
|
1668
|
+
const gen = runLoop(input, options ?? {});
|
|
1669
|
+
let next = await gen.next();
|
|
1670
|
+
while (next.done !== true) next = await gen.next();
|
|
1671
|
+
const result = next.value;
|
|
1672
|
+
if (result === void 0) throw new Error("unreachable: agent run loop ended without a result");
|
|
1673
|
+
return result;
|
|
1674
|
+
};
|
|
1675
|
+
const steer = (message) => {
|
|
1676
|
+
const { seed } = asMessages(message);
|
|
1677
|
+
pendingSteer.push(...seed);
|
|
1678
|
+
if (activeRunState !== void 0) externalEventQueue.push({
|
|
1679
|
+
type: "agent.steered",
|
|
1680
|
+
runId: activeRunState.id
|
|
1681
|
+
});
|
|
1682
|
+
};
|
|
1683
|
+
const followUp = (message) => {
|
|
1684
|
+
const { seed } = asMessages(message);
|
|
1685
|
+
pendingFollowUp.push(...seed);
|
|
1686
|
+
if (activeRunState !== void 0) externalEventQueue.push({
|
|
1687
|
+
type: "agent.followup.queued",
|
|
1688
|
+
runId: activeRunState.id
|
|
1689
|
+
});
|
|
1690
|
+
};
|
|
1691
|
+
const abort = (options) => {
|
|
1692
|
+
pendingAbort = options ?? {};
|
|
1693
|
+
abortController?.abort();
|
|
1694
|
+
};
|
|
1695
|
+
const toTool = (options = {}) => {
|
|
1696
|
+
const exposeTurns = options.exposeTurns ?? "final";
|
|
1697
|
+
return {
|
|
1698
|
+
name: options.name ?? `subagent_${config.name}`,
|
|
1699
|
+
description: options.description ?? `Invoke sub-agent '${config.name}'.`,
|
|
1700
|
+
inputSchema: {
|
|
1701
|
+
parse: (v) => v,
|
|
1702
|
+
safeParse: (v) => ({
|
|
1703
|
+
success: true,
|
|
1704
|
+
data: v
|
|
1705
|
+
}),
|
|
1706
|
+
toJSON: () => ({
|
|
1707
|
+
type: "object",
|
|
1708
|
+
properties: { input: { type: "string" } },
|
|
1709
|
+
required: ["input"]
|
|
1710
|
+
})
|
|
1711
|
+
},
|
|
1712
|
+
sideEffectClass: "side-effecting",
|
|
1713
|
+
async execute(input, ctx) {
|
|
1714
|
+
const callOpts = {
|
|
1715
|
+
...ctx?.signal !== void 0 ? { signal: ctx.signal } : {},
|
|
1716
|
+
...ctx?.runContext.deps !== void 0 ? { deps: ctx.runContext.deps } : {},
|
|
1717
|
+
...ctx?.runContext.sessionId !== void 0 ? { sessionId: ctx.runContext.sessionId } : {}
|
|
1718
|
+
};
|
|
1719
|
+
const seed = options.inputFilter !== void 0 && ctx !== void 0 ? [...options.inputFilter(ctx.runContext.messages), {
|
|
1720
|
+
role: "user",
|
|
1721
|
+
content: input.input
|
|
1722
|
+
}] : input.input;
|
|
1723
|
+
if (exposeTurns === "all") {
|
|
1724
|
+
const turns = [];
|
|
1725
|
+
let endResult;
|
|
1726
|
+
for await (const ev of stream(seed, callOpts)) if (ev.type === "text.complete") turns.push(ev.text);
|
|
1727
|
+
else if (ev.type === "agent.end") endResult = ev.result;
|
|
1728
|
+
if (endResult !== void 0 && endResult.status !== "completed") throw new Error(`sub-agent '${config.name}' ${endResult.status}${endResult.error !== void 0 ? `: ${endResult.error.message}` : ""}`);
|
|
1729
|
+
return turns.join("\n\n");
|
|
1730
|
+
}
|
|
1731
|
+
const result = await run(seed, callOpts);
|
|
1732
|
+
if (result.status !== "completed") throw new Error(`sub-agent '${config.name}' ${result.status}${result.error !== void 0 ? `: ${result.error.message}` : ""}`);
|
|
1733
|
+
if (exposeTurns === "none") return "";
|
|
1734
|
+
return result.output;
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
};
|
|
1738
|
+
const compact = async (options) => {
|
|
1739
|
+
if (memory === void 0) return noopCompactionResult("no-memory");
|
|
1740
|
+
if (config.sensitivity === "secret") return noopCompactionResult("sensitivity-gated");
|
|
1741
|
+
if (activeRunState === void 0) return noopCompactionResult("no-active-run");
|
|
1742
|
+
return await new Promise((resolve, reject) => {
|
|
1743
|
+
pendingManualCompacts.push({
|
|
1744
|
+
options,
|
|
1745
|
+
resolve,
|
|
1746
|
+
reject
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
};
|
|
1750
|
+
const fanOut = async (options) => {
|
|
1751
|
+
const runId = activeRunState?.id ?? `run_${newId()}`;
|
|
1752
|
+
const sessionId = activeRunState?.sessionId ?? `session_${newId()}`;
|
|
1753
|
+
return runFanOut({
|
|
1754
|
+
children: options.children,
|
|
1755
|
+
...options.maxConcurrentChildren !== void 0 ? { maxConcurrentChildren: options.maxConcurrentChildren } : {},
|
|
1756
|
+
...options.perBudget !== void 0 ? { perBudget: options.perBudget } : {},
|
|
1757
|
+
...options.mergeStrategy !== void 0 ? { mergeStrategy: options.mergeStrategy } : {},
|
|
1758
|
+
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
1759
|
+
emit: (event) => {
|
|
1760
|
+
externalEventQueue.push(event);
|
|
1761
|
+
},
|
|
1762
|
+
...config.mergeGuard !== void 0 ? { mergeGuard: config.mergeGuard } : {},
|
|
1763
|
+
runId,
|
|
1764
|
+
sessionId,
|
|
1765
|
+
agentId
|
|
1766
|
+
});
|
|
1767
|
+
};
|
|
1768
|
+
const progressFallbackRunId = `run_${newId()}`;
|
|
1769
|
+
const progress = {
|
|
1770
|
+
write: async (content, opts) => {
|
|
1771
|
+
const runId = activeRunState?.id ?? progressFallbackRunId;
|
|
1772
|
+
const ref = await progressIO.write(runId, content, opts);
|
|
1773
|
+
externalEventQueue.push({
|
|
1774
|
+
type: "agent.progress.written",
|
|
1775
|
+
runId,
|
|
1776
|
+
sessionId: activeRunState?.sessionId ?? "",
|
|
1777
|
+
agentId,
|
|
1778
|
+
ref
|
|
1779
|
+
});
|
|
1780
|
+
return ref;
|
|
1781
|
+
},
|
|
1782
|
+
read: async (opts) => {
|
|
1783
|
+
const queriedRunId = opts?.runId ?? activeRunState?.id ?? progressFallbackRunId;
|
|
1784
|
+
const refs = await progressIO.read(queriedRunId, opts);
|
|
1785
|
+
externalEventQueue.push({
|
|
1786
|
+
type: "agent.progress.read",
|
|
1787
|
+
runId: activeRunState?.id ?? queriedRunId,
|
|
1788
|
+
sessionId: activeRunState?.sessionId ?? "",
|
|
1789
|
+
agentId,
|
|
1790
|
+
refs,
|
|
1791
|
+
queriedRunId,
|
|
1792
|
+
queriedRole: opts?.role
|
|
1793
|
+
});
|
|
1794
|
+
return refs;
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
config.sensitivity;
|
|
1798
|
+
return {
|
|
1799
|
+
id: agentId,
|
|
1800
|
+
config,
|
|
1801
|
+
stream,
|
|
1802
|
+
run,
|
|
1803
|
+
steer,
|
|
1804
|
+
followUp,
|
|
1805
|
+
abort,
|
|
1806
|
+
toTool,
|
|
1807
|
+
compact,
|
|
1808
|
+
fanOut,
|
|
1809
|
+
progress,
|
|
1810
|
+
registry: toolRegistry
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Pre-execution approval screen (Adapter G / durable HITL). Evaluates a
|
|
1815
|
+
* (registry-resolved) tool's `needsApproval` against the realized args.
|
|
1816
|
+
* Returns `true` when the run must suspend before the tool executes.
|
|
1817
|
+
*
|
|
1818
|
+
* Actual execution flows through the `@graphorin/tools` executor, whose
|
|
1819
|
+
* `ApprovalGate` auto-grants because only no-approval / pre-approved
|
|
1820
|
+
* calls ever reach it; this probe is what keeps the suspend in the
|
|
1821
|
+
* agent so the durable-HITL contract (persist `RunState`, resume via
|
|
1822
|
+
* directive) is preserved.
|
|
1823
|
+
*/
|
|
1824
|
+
async function invokeNeedsApproval(tool, args, baseCtx, signal) {
|
|
1825
|
+
const predicate = tool?.needsApproval;
|
|
1826
|
+
if (predicate === void 0 || predicate === false) return false;
|
|
1827
|
+
if (predicate === true) return true;
|
|
1828
|
+
const probeCtx = {
|
|
1829
|
+
toolCallId: "probe",
|
|
1830
|
+
runContext: baseCtx,
|
|
1831
|
+
signal,
|
|
1832
|
+
tracer: baseCtx.tracer,
|
|
1833
|
+
logger: NOOP_LOGGER,
|
|
1834
|
+
secrets: probeSecretsAccessor(),
|
|
1835
|
+
reportProgress: () => {},
|
|
1836
|
+
streamContent: () => {}
|
|
1837
|
+
};
|
|
1838
|
+
return Boolean(await predicate(args, probeCtx));
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Rejecting secrets accessor used only by the {@link invokeNeedsApproval}
|
|
1842
|
+
* probe. Real tool execution resolves secrets through the executor's
|
|
1843
|
+
* ACL-scoped accessor; an approval predicate has no legitimate need to
|
|
1844
|
+
* read secret material, so every `require(...)` rejects.
|
|
1845
|
+
*/
|
|
1846
|
+
function probeSecretsAccessor() {
|
|
1847
|
+
const rejector = (_key, _options) => Promise.reject(/* @__PURE__ */ new Error("secrets.require is unavailable inside a needsApproval predicate"));
|
|
1848
|
+
return { require: rejector };
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
//#endregion
|
|
1852
|
+
export { createAgent };
|
|
1853
|
+
//# sourceMappingURL=factory.js.map
|