@graphorin/agent 0.6.0 → 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.
- package/CHANGELOG.md +96 -0
- package/README.md +31 -11
- package/dist/errors/index.d.ts +17 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +19 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +153 -1930
- package/dist/factory.js.map +1 -1
- package/dist/fanout/index.d.ts +13 -1
- package/dist/fanout/index.d.ts.map +1 -1
- package/dist/fanout/index.js +13 -4
- package/dist/fanout/index.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/lateral-leak/index.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/run-state/index.d.ts +32 -6
- package/dist/run-state/index.d.ts.map +1 -1
- package/dist/run-state/index.js +46 -22
- package/dist/run-state/index.js.map +1 -1
- package/dist/runtime/agent-surface.js +122 -0
- package/dist/runtime/agent-surface.js.map +1 -0
- package/dist/runtime/agent-to-tool.d.ts +51 -0
- package/dist/runtime/agent-to-tool.d.ts.map +1 -0
- package/dist/runtime/agent-to-tool.js +145 -0
- package/dist/runtime/agent-to-tool.js.map +1 -0
- package/dist/runtime/approvals.js +0 -0
- package/dist/runtime/approvals.js.map +1 -0
- package/dist/runtime/dispatch.js +108 -0
- package/dist/runtime/dispatch.js.map +1 -0
- package/dist/runtime/executor-wiring.js +128 -0
- package/dist/runtime/executor-wiring.js.map +1 -0
- package/dist/runtime/fallback-chain.js +139 -0
- package/dist/runtime/fallback-chain.js.map +1 -0
- package/dist/runtime/handoff.js +307 -0
- package/dist/runtime/handoff.js.map +1 -0
- package/dist/runtime/messages.d.ts +22 -0
- package/dist/runtime/messages.d.ts.map +1 -0
- package/dist/runtime/messages.js +204 -0
- package/dist/runtime/messages.js.map +1 -0
- package/dist/runtime/provider-events.js +117 -0
- package/dist/runtime/provider-events.js.map +1 -0
- package/dist/runtime/run-compaction.js +210 -0
- package/dist/runtime/run-compaction.js.map +1 -0
- package/dist/runtime/run-finish.js +48 -0
- package/dist/runtime/run-finish.js.map +1 -0
- package/dist/runtime/run-gates.js +336 -0
- package/dist/runtime/run-gates.js.map +1 -0
- package/dist/runtime/run-init.js +81 -0
- package/dist/runtime/run-init.js.map +1 -0
- package/dist/runtime/run-input.js +46 -0
- package/dist/runtime/run-input.js.map +1 -0
- package/dist/runtime/step-catalogue.js +173 -0
- package/dist/runtime/step-catalogue.js.map +1 -0
- package/dist/runtime/tool-call-walk.js +189 -0
- package/dist/runtime/tool-call-walk.js.map +1 -0
- package/dist/runtime/tool-wiring.js +159 -0
- package/dist/runtime/tool-wiring.js.map +1 -0
- package/dist/tooling/adapters.js +1 -1
- package/dist/tooling/dataflow.js +1 -1
- package/dist/tooling/policy.js +2 -0
- package/dist/tooling/policy.js.map +1 -1
- package/dist/types.d.ts +63 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/errors/index.ts +320 -0
- package/src/evaluator-optimizer/index.ts +212 -0
- package/src/factory.ts +957 -0
- package/src/fallback/index.ts +108 -0
- package/src/fanout/index.ts +523 -0
- package/src/filters/index.ts +347 -0
- package/src/index.ts +180 -0
- package/src/internal/ids.ts +46 -0
- package/src/internal/usage-accumulator.ts +90 -0
- package/src/lateral-leak/causality-monitor.ts +221 -0
- package/src/lateral-leak/index.ts +35 -0
- package/src/lateral-leak/merge-guard.ts +151 -0
- package/src/lateral-leak/protocol-guard.ts +222 -0
- package/src/preferred-model/index.ts +210 -0
- package/src/progress/index.ts +238 -0
- package/src/run-state/index.ts +607 -0
- package/src/runtime/agent-surface.ts +218 -0
- package/src/runtime/agent-to-tool.ts +323 -0
- package/src/runtime/approvals.ts +0 -0
- package/src/runtime/dispatch.ts +183 -0
- package/src/runtime/executor-wiring.ts +331 -0
- package/src/runtime/fallback-chain.ts +250 -0
- package/src/runtime/handoff.ts +428 -0
- package/src/runtime/messages.ts +309 -0
- package/src/runtime/provider-events.ts +175 -0
- package/src/runtime/run-compaction.ts +288 -0
- package/src/runtime/run-finish.ts +93 -0
- package/src/runtime/run-gates.ts +419 -0
- package/src/runtime/run-init.ts +169 -0
- package/src/runtime/run-input.ts +102 -0
- package/src/runtime/step-catalogue.ts +338 -0
- package/src/runtime/tool-call-walk.ts +301 -0
- package/src/runtime/tool-wiring.ts +218 -0
- package/src/testing/replay-provider.ts +121 -0
- package/src/tooling/adapters.ts +403 -0
- package/src/tooling/catalogue.ts +36 -0
- package/src/tooling/dataflow.ts +171 -0
- package/src/tooling/plan.ts +123 -0
- package/src/tooling/policy.ts +67 -0
- package/src/tooling/registry-build.ts +191 -0
- package/src/types.ts +696 -0
package/src/factory.ts
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createAgent({...})` - the agent factory entry point.
|
|
3
|
+
*
|
|
4
|
+
* Wires the typed `model -> tool calls -> model` loop, the streamed
|
|
5
|
+
* event surface, the steering / followUp queues, durable HITL via
|
|
6
|
+
* `RunState`, the multi-agent handoff layer, the agent-level model
|
|
7
|
+
* fallback chain, and the per-tool preferred-model resolution.
|
|
8
|
+
*
|
|
9
|
+
* Custom adapters override behaviour by supplying alternative
|
|
10
|
+
* `Provider` / `Memory` / `CheckpointStore` instances; the loop
|
|
11
|
+
* never reaches into adapter internals.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
AgentEvent,
|
|
18
|
+
AgentResult,
|
|
19
|
+
AISpan,
|
|
20
|
+
Message,
|
|
21
|
+
ProviderRequest,
|
|
22
|
+
RunContext,
|
|
23
|
+
RunState,
|
|
24
|
+
RunStep,
|
|
25
|
+
Sensitivity,
|
|
26
|
+
Tool,
|
|
27
|
+
ToolApproval,
|
|
28
|
+
ToolCall,
|
|
29
|
+
ToolDefinition,
|
|
30
|
+
} from '@graphorin/core';
|
|
31
|
+
import { isStepCount, NOOP_TRACER } from '@graphorin/core';
|
|
32
|
+
import type { Memory } from '@graphorin/memory';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
AgentRuntimeError,
|
|
36
|
+
ConcurrentRunError,
|
|
37
|
+
InvalidAgentConfigError,
|
|
38
|
+
InvalidPreferredModelError,
|
|
39
|
+
MultipleHandoffsInStepError,
|
|
40
|
+
} from './errors/index.js';
|
|
41
|
+
import type { AgentFallbackPolicy } from './fallback/index.js';
|
|
42
|
+
import type { DescribedFilter } from './filters/index.js';
|
|
43
|
+
import { newId } from './internal/ids.js';
|
|
44
|
+
import { InMemoryUsageAccumulator } from './internal/usage-accumulator.js';
|
|
45
|
+
import { CausalityMonitor } from './lateral-leak/causality-monitor.js';
|
|
46
|
+
import { createProgressIO, type ProgressIO } from './progress/index.js';
|
|
47
|
+
import { addModelUsage, serializeRunState } from './run-state/index.js';
|
|
48
|
+
import {
|
|
49
|
+
createCompactMethod,
|
|
50
|
+
createFanOutMethod,
|
|
51
|
+
createProgressSurface,
|
|
52
|
+
createRunMethods,
|
|
53
|
+
} from './runtime/agent-surface.js';
|
|
54
|
+
import { createToTool } from './runtime/agent-to-tool.js';
|
|
55
|
+
import {
|
|
56
|
+
dispatchResumedApprovals,
|
|
57
|
+
processResumeDirective,
|
|
58
|
+
processSubRunResumes,
|
|
59
|
+
type ResumedDispatchEnv,
|
|
60
|
+
type ResumeRunEnv,
|
|
61
|
+
type RoutedSubRunDecision,
|
|
62
|
+
type SubRunResumeEnv,
|
|
63
|
+
} from './runtime/approvals.js';
|
|
64
|
+
import { type DispatchRunEnv, dispatchToolBatch } from './runtime/dispatch.js';
|
|
65
|
+
import { wireToolExecution } from './runtime/executor-wiring.js';
|
|
66
|
+
import { type FallbackChainEnv, runProviderFallbackChain } from './runtime/fallback-chain.js';
|
|
67
|
+
import { HANDOFF_TOOL_PREFIX, isDescribedFilter } from './runtime/handoff.js';
|
|
68
|
+
import {
|
|
69
|
+
accumulateUsage,
|
|
70
|
+
applyReasoningRetention,
|
|
71
|
+
countLeadingSystemMessages,
|
|
72
|
+
} from './runtime/messages.js';
|
|
73
|
+
import {
|
|
74
|
+
type CompactionRunEnv,
|
|
75
|
+
maybeAutoCompact,
|
|
76
|
+
noopCompactionResult,
|
|
77
|
+
type PendingManualCompact,
|
|
78
|
+
tryEmergencyCompact,
|
|
79
|
+
} from './runtime/run-compaction.js';
|
|
80
|
+
import { createRunFinisher } from './runtime/run-finish.js';
|
|
81
|
+
import {
|
|
82
|
+
type AssistantCommitEnv,
|
|
83
|
+
buildStructuredInstruction,
|
|
84
|
+
type CancellationEnv,
|
|
85
|
+
commitAssistantMessage,
|
|
86
|
+
emitCancellation,
|
|
87
|
+
finalizeRunOutput,
|
|
88
|
+
type GuardrailScreenEnv,
|
|
89
|
+
type RunOutputEnv,
|
|
90
|
+
runVerifierGate,
|
|
91
|
+
screenInputGuardrails,
|
|
92
|
+
type VerifierGateEnv,
|
|
93
|
+
} from './runtime/run-gates.js';
|
|
94
|
+
import { initializeRunState, seedInitialMessages } from './runtime/run-init.js';
|
|
95
|
+
import {
|
|
96
|
+
asMessages,
|
|
97
|
+
type InternalRunSnapshot,
|
|
98
|
+
isModelHintLike,
|
|
99
|
+
isModelSpecLike,
|
|
100
|
+
type MutableRunState,
|
|
101
|
+
validatePreferredModel,
|
|
102
|
+
} from './runtime/run-input.js';
|
|
103
|
+
import {
|
|
104
|
+
buildBaseRequest,
|
|
105
|
+
resolveStepToolContext,
|
|
106
|
+
type StepCatalogueEnv,
|
|
107
|
+
type StepRequestEnv,
|
|
108
|
+
toolToDefinition,
|
|
109
|
+
} from './runtime/step-catalogue.js';
|
|
110
|
+
import { processStepToolCalls, type ToolCallWalkEnv } from './runtime/tool-call-walk.js';
|
|
111
|
+
import type { ExecutorEventBridge } from './tooling/adapters.js';
|
|
112
|
+
import type {
|
|
113
|
+
AbortOptions,
|
|
114
|
+
Agent,
|
|
115
|
+
AgentCallOptions,
|
|
116
|
+
AgentConfig,
|
|
117
|
+
AgentInput,
|
|
118
|
+
AgentProgressIO,
|
|
119
|
+
SubagentForwardPolicy,
|
|
120
|
+
} from './types.js';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build a fresh {@link Agent} from the supplied configuration.
|
|
124
|
+
*
|
|
125
|
+
* @stable
|
|
126
|
+
*/
|
|
127
|
+
export function createAgent<TDeps = unknown, TOutput = string>(
|
|
128
|
+
config: AgentConfig<TDeps, TOutput>,
|
|
129
|
+
): Agent<TDeps, TOutput> {
|
|
130
|
+
if (typeof config.name !== 'string' || config.name.length === 0) {
|
|
131
|
+
throw new InvalidAgentConfigError("missing 'name'");
|
|
132
|
+
}
|
|
133
|
+
if (config.provider === undefined || config.provider === null) {
|
|
134
|
+
throw new InvalidAgentConfigError("missing 'provider'");
|
|
135
|
+
}
|
|
136
|
+
// AG-3: a schema on a text-kind output spec is a config mistake (the
|
|
137
|
+
// schema would never run) - reject instead of silently ignoring.
|
|
138
|
+
if (config.outputType?.kind === 'text' && config.outputType.schema !== undefined) {
|
|
139
|
+
throw new InvalidAgentConfigError(
|
|
140
|
+
"outputType.kind 'text' with a schema - did you mean kind: 'structured'?",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
validatePreferredModel(config.preferredModel);
|
|
144
|
+
if (config.modelTierMap !== undefined) {
|
|
145
|
+
for (const [tier, spec] of Object.entries(config.modelTierMap)) {
|
|
146
|
+
if (!isModelHintLike(tier)) throw new InvalidPreferredModelError({ tier });
|
|
147
|
+
if (spec === undefined) continue;
|
|
148
|
+
if (!isModelSpecLike(spec)) throw new InvalidPreferredModelError(spec);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (config.fallbackModels !== undefined) {
|
|
152
|
+
for (const spec of config.fallbackModels) {
|
|
153
|
+
if (!isModelSpecLike(spec)) throw new InvalidPreferredModelError(spec);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const agentId = newId('agent');
|
|
158
|
+
const tracer = config.tracer ?? NOOP_TRACER;
|
|
159
|
+
const stopWhen = config.stopWhen ?? isStepCount(50);
|
|
160
|
+
const fallbackPolicy: AgentFallbackPolicy = config.fallbackPolicy ?? {};
|
|
161
|
+
const handoffMap = new Map<
|
|
162
|
+
string,
|
|
163
|
+
{
|
|
164
|
+
readonly agent: Agent<TDeps, unknown>;
|
|
165
|
+
readonly filter: DescribedFilter | undefined;
|
|
166
|
+
readonly forwardEvents: SubagentForwardPolicy | undefined;
|
|
167
|
+
}
|
|
168
|
+
>();
|
|
169
|
+
for (const entry of config.handoffs ?? []) {
|
|
170
|
+
const isWrappedHandoff = typeof entry === 'object' && entry !== null && 'target' in entry;
|
|
171
|
+
const subAgent: Agent<TDeps, unknown> = isWrappedHandoff
|
|
172
|
+
? (entry as { readonly target: Agent<TDeps, unknown> }).target
|
|
173
|
+
: (entry as Agent<TDeps, unknown>);
|
|
174
|
+
const userFilter = isWrappedHandoff
|
|
175
|
+
? (
|
|
176
|
+
entry as {
|
|
177
|
+
readonly inputFilter?:
|
|
178
|
+
| DescribedFilter
|
|
179
|
+
| ((history: readonly Message[]) => readonly Message[]);
|
|
180
|
+
}
|
|
181
|
+
).inputFilter
|
|
182
|
+
: undefined;
|
|
183
|
+
const filter = isDescribedFilter(userFilter) ? userFilter : undefined;
|
|
184
|
+
const forwardEvents = isWrappedHandoff
|
|
185
|
+
? (entry as { readonly forwardEvents?: SubagentForwardPolicy }).forwardEvents
|
|
186
|
+
: undefined;
|
|
187
|
+
const toolName = `${HANDOFF_TOOL_PREFIX}${subAgent.config.name}`;
|
|
188
|
+
handoffMap.set(toolName, { agent: subAgent, filter, forwardEvents });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let pendingSteer: Message[] = [];
|
|
192
|
+
const pendingFollowUp: Message[] = [];
|
|
193
|
+
let abortController: AbortController | undefined;
|
|
194
|
+
let pendingAbort: AbortOptions | undefined;
|
|
195
|
+
// Per-run scratch refs surfaced through the public surface for
|
|
196
|
+
// event emission from `steer(...)` / `followUp(...)`.
|
|
197
|
+
let activeRunState: RunState | undefined;
|
|
198
|
+
// D2: capability of the ACTIVE run - read by the code-mode bridge so
|
|
199
|
+
// in-script tool calls inherit the run's single-writer restriction.
|
|
200
|
+
let activeRunCapability: 'read-only' | undefined;
|
|
201
|
+
/** AG-11: guards the one-in-flight-run-per-instance invariant. */
|
|
202
|
+
let runInFlight = false;
|
|
203
|
+
const externalEventQueue: AgentEvent<TOutput>[] = [];
|
|
204
|
+
|
|
205
|
+
const pendingManualCompacts: PendingManualCompact[] = [];
|
|
206
|
+
|
|
207
|
+
const memory: Memory | undefined = config.memory;
|
|
208
|
+
const progressIO: ProgressIO = createProgressIO({
|
|
209
|
+
...(config.sensitivity !== undefined ? { defaultSensitivity: config.sensitivity } : {}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Warm-up tool stack (Principle #12 / WI-03 / WI-05 / WI-10 / WI-11 /
|
|
213
|
+
// WI-13 / D6): registry assembly, built-in registration (tool_search,
|
|
214
|
+
// plan, read_result), guard hooks, the shared ToolExecutor factory and
|
|
215
|
+
// the code-mode surface - wired in `runtime/executor-wiring.ts`. The
|
|
216
|
+
// registry is exposed read-only as `agent.registry`.
|
|
217
|
+
const executorBridgeSlot: { current: ExecutorEventBridge | undefined } = { current: undefined };
|
|
218
|
+
const {
|
|
219
|
+
toolRegistry,
|
|
220
|
+
spillWriter,
|
|
221
|
+
resultReader,
|
|
222
|
+
makeToolExecutor,
|
|
223
|
+
toolExecutor,
|
|
224
|
+
toolDataFlowGuard,
|
|
225
|
+
ruleOfTwoCapabilityFloor,
|
|
226
|
+
isCodeMode,
|
|
227
|
+
codeModeAdvertised,
|
|
228
|
+
} = wireToolExecution<TDeps, TOutput>({
|
|
229
|
+
config,
|
|
230
|
+
memory,
|
|
231
|
+
agentId,
|
|
232
|
+
handoffToolNames: handoffMap.keys(),
|
|
233
|
+
getActiveRunState: () => activeRunState,
|
|
234
|
+
getActiveRunCapability: () => activeRunCapability,
|
|
235
|
+
executorBridgeSlot,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const causalityMonitor = config.causalityMonitor
|
|
239
|
+
? new CausalityMonitor(config.causalityMonitor)
|
|
240
|
+
: undefined;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* AG-11: one in-flight run per Agent instance. `steer` / `followUp` /
|
|
244
|
+
* `abort` / `compact` address "the run" with no run handle, so
|
|
245
|
+
* overlapping runs on one instance cannot be expressed safely - they
|
|
246
|
+
* would share the abort controller, steer queue, active-run ref and
|
|
247
|
+
* executor bridge. A second concurrent `run()` / `stream()` rejects
|
|
248
|
+
* with {@link ConcurrentRunError}; run-scoped state is reset on entry
|
|
249
|
+
* (a steer/abort queued after the previous run ended belongs to NO
|
|
250
|
+
* run) and cleared in a `finally` that also covers abandoned streams
|
|
251
|
+
* (consumer `break`) and thrown runs.
|
|
252
|
+
*/
|
|
253
|
+
async function* runLoop(
|
|
254
|
+
input: AgentInput | RunState,
|
|
255
|
+
options: AgentCallOptions<TDeps>,
|
|
256
|
+
): AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {
|
|
257
|
+
if (runInFlight) {
|
|
258
|
+
throw new ConcurrentRunError();
|
|
259
|
+
}
|
|
260
|
+
runInFlight = true;
|
|
261
|
+
pendingSteer = [];
|
|
262
|
+
pendingAbort = undefined;
|
|
263
|
+
// D2 + D4: per-run capability - the call-level override wins, then
|
|
264
|
+
// the agent default, then the Rule-of-Two floor (a profile denying
|
|
265
|
+
// external side effects forces read-only even without an explicit
|
|
266
|
+
// capability). Absent ⇒ all capabilities (legacy behaviour).
|
|
267
|
+
activeRunCapability = options.capability ?? config.capability ?? ruleOfTwoCapabilityFloor;
|
|
268
|
+
// AG-10: the causality chain is a per-run artifact - a denial
|
|
269
|
+
// recorded in one run must not poison detection in the next.
|
|
270
|
+
causalityMonitor?.reset();
|
|
271
|
+
try {
|
|
272
|
+
return yield* runLoopInner(input, options);
|
|
273
|
+
} finally {
|
|
274
|
+
runInFlight = false;
|
|
275
|
+
activeRunState = undefined;
|
|
276
|
+
activeRunCapability = undefined;
|
|
277
|
+
// Backstop for exits that bypass `finishRun` (abandoned stream,
|
|
278
|
+
// generator teardown): settle queued manual compactions so no
|
|
279
|
+
// `agent.compact()` promise is left hanging (CE-3/AG-13).
|
|
280
|
+
while (pendingManualCompacts.length > 0) {
|
|
281
|
+
pendingManualCompacts.shift()?.resolve(noopCompactionResult('no-active-run'));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function* runLoopInner(
|
|
287
|
+
input: AgentInput | RunState,
|
|
288
|
+
options: AgentCallOptions<TDeps>,
|
|
289
|
+
): AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {
|
|
290
|
+
const { seed: rawSeed, resumed } = asMessages(input);
|
|
291
|
+
// AG-12: queued follow-ups are next-turn metadata - they ride into
|
|
292
|
+
// the next FRESH run as leading user turns. The old path mutated a
|
|
293
|
+
// finished run back to 'running' and appended the message to a loop
|
|
294
|
+
// that never processed it, leaving a non-terminal persisted RunState
|
|
295
|
+
// with a dangling user turn. Resumed runs keep the queue intact.
|
|
296
|
+
const seed =
|
|
297
|
+
resumed === undefined && pendingFollowUp.length > 0
|
|
298
|
+
? [...pendingFollowUp.splice(0, pendingFollowUp.length), ...rawSeed]
|
|
299
|
+
: rawSeed;
|
|
300
|
+
const sessionId = options.sessionId ?? config.sessionId ?? `session_${newId()}`;
|
|
301
|
+
const userId = options.userId ?? config.userId;
|
|
302
|
+
const localCtl = new AbortController();
|
|
303
|
+
abortController = localCtl;
|
|
304
|
+
// AG-5: the loop + every provider request must observe the LOCAL
|
|
305
|
+
// controller, so `agent.abort()` (which aborts `localCtl`) is honoured even
|
|
306
|
+
// when the caller supplied their own `options.signal`. The caller's signal
|
|
307
|
+
// is propagated INTO `localCtl` by the listener below; the listener is torn
|
|
308
|
+
// down in the run's `finally` so it does not accumulate across runs that
|
|
309
|
+
// share one long-lived parent signal.
|
|
310
|
+
const signal = localCtl.signal;
|
|
311
|
+
const parentSignal = options.signal;
|
|
312
|
+
const onParentAbort = (): void => localCtl.abort();
|
|
313
|
+
if (parentSignal !== undefined) {
|
|
314
|
+
if (parentSignal.aborted) localCtl.abort();
|
|
315
|
+
else parentSignal.addEventListener('abort', onParentAbort);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const usageAcc = new InMemoryUsageAccumulator();
|
|
319
|
+
// Bootstrap the run state, the AG-19 security rehydration and the
|
|
320
|
+
// `tool_search` promotion set (see `runtime/run-init.ts`).
|
|
321
|
+
const { state, promotedDeferred, runStartPromotions } = initializeRunState<TDeps, TOutput>(
|
|
322
|
+
{ config, agentId, sessionId, userId, toolDataFlowGuard },
|
|
323
|
+
resumed,
|
|
324
|
+
);
|
|
325
|
+
activeRunState = state;
|
|
326
|
+
|
|
327
|
+
// agent-08 (F4): capture the run-scoped security state on EVERY exit
|
|
328
|
+
// through finishRun - not just the approval suspend. An 'aborted' run
|
|
329
|
+
// is resumable (the AG-14 guard blocks only awaiting_approval/failed)
|
|
330
|
+
// and a 'completed' run re-enters as a follow-up; both must rehydrate
|
|
331
|
+
// the enforce-mode sink gate and the discovered-tool catalogue.
|
|
332
|
+
// Shadows the factory-scope finishRunBase for every call in this run.
|
|
333
|
+
async function* finishRun(
|
|
334
|
+
s: MutableRunState & RunState,
|
|
335
|
+
snapshot: InternalRunSnapshot<TOutput>,
|
|
336
|
+
): AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {
|
|
337
|
+
const taintSnap = toolDataFlowGuard?.snapshotLedger(s.id);
|
|
338
|
+
if (taintSnap !== undefined) {
|
|
339
|
+
(s as { taintSummary?: typeof taintSnap }).taintSummary = taintSnap;
|
|
340
|
+
}
|
|
341
|
+
if (promotedDeferred.size > 0) {
|
|
342
|
+
(s as { promotedTools?: readonly string[] }).promotedTools = [...promotedDeferred];
|
|
343
|
+
}
|
|
344
|
+
return yield* finishRunBase(s, snapshot);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const messages: Message[] = resumed ? [...state.messages] : [];
|
|
348
|
+
if (!resumed) {
|
|
349
|
+
// Fresh run: resolve `instructions` (AG-8), optionally assemble
|
|
350
|
+
// the memory-aware system prompt (CE-1), and seed the buffer +
|
|
351
|
+
// RunState mirror (see `runtime/run-init.ts`).
|
|
352
|
+
await seedInitialMessages<TDeps, TOutput>(
|
|
353
|
+
{ config, options, memory, agentId, sessionId, userId, tracer, signal, usageAcc, state },
|
|
354
|
+
messages,
|
|
355
|
+
seed,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const finalSnapshot: InternalRunSnapshot<TOutput> = {
|
|
360
|
+
output: '' as unknown as TOutput,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// C7: one agent.run span per run; step/tool/provider spans parent
|
|
364
|
+
// under it so the whole run is a single trace tree. Attributes follow
|
|
365
|
+
// the OTel GenAI semantic conventions (gen_ai.*).
|
|
366
|
+
const runSpan = tracer.startSpan({
|
|
367
|
+
type: 'agent.run',
|
|
368
|
+
// W-036: a sub-agent invocation parents under the caller's live
|
|
369
|
+
// step/tool span so multi-agent load forms ONE trace tree.
|
|
370
|
+
...(options.parentSpan !== undefined ? { parent: options.parentSpan } : {}),
|
|
371
|
+
attrs: {
|
|
372
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
373
|
+
'gen_ai.agent.id': agentId,
|
|
374
|
+
'gen_ai.agent.name': config.name,
|
|
375
|
+
'graphorin.run.id': state.id,
|
|
376
|
+
'graphorin.session.id': sessionId,
|
|
377
|
+
'graphorin.run.resumed': resumed !== undefined,
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
let currentStepSpan: AISpan<'agent.step'> | undefined;
|
|
381
|
+
|
|
382
|
+
yield { type: 'agent.start', runId: state.id, agentId };
|
|
383
|
+
|
|
384
|
+
// AG-2 / SDF-4: input guardrails screen each fresh-run seed user
|
|
385
|
+
// message (string content) BEFORE the first provider call, using the
|
|
386
|
+
// canonical `@graphorin/security` composer. 'block' fails the run
|
|
387
|
+
// without reaching the model; 'rewrite' replaces the content in both
|
|
388
|
+
// the working buffer and the persisted RunState; 'warn' logs and
|
|
389
|
+
// continues. Resumed runs skip the pass - their seed was screened
|
|
390
|
+
// when first submitted.
|
|
391
|
+
const inputGuards = config.guardrails?.input;
|
|
392
|
+
if (!resumed && inputGuards !== undefined && inputGuards.length > 0) {
|
|
393
|
+
const blocked = yield* screenInputGuardrails<TOutput>(
|
|
394
|
+
{ state, messages, sessionId, agentId },
|
|
395
|
+
inputGuards,
|
|
396
|
+
);
|
|
397
|
+
if (blocked) {
|
|
398
|
+
return yield* finishRun(state, finalSnapshot);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// AG-3: one per-run JSON instruction for structured output, appended
|
|
403
|
+
// to each provider request (never the shared buffer / RunState).
|
|
404
|
+
const structuredInstruction =
|
|
405
|
+
config.outputType?.kind === 'structured'
|
|
406
|
+
? buildStructuredInstruction(config.outputType)
|
|
407
|
+
: undefined;
|
|
408
|
+
|
|
409
|
+
// AG-1: approved gated calls collected from a resume directive, executed
|
|
410
|
+
// for real once every approval is resolved (see the dispatch below).
|
|
411
|
+
const resumedApprovedCalls: ToolCall[] = [];
|
|
412
|
+
// agent-02: the ToolApproval records behind `resumedApprovedCalls`,
|
|
413
|
+
// kept so the write-ahead intent checkpoint can re-attach them to
|
|
414
|
+
// `pendingApprovals` (a crash-retry against the intent re-dispatches).
|
|
415
|
+
const grantedApprovals: ToolApproval[] = [];
|
|
416
|
+
// W-001: decisions addressed to PARKED sub-agent runs, grouped by the
|
|
417
|
+
// parent toolCallId of the park (routed into the children below).
|
|
418
|
+
const subRunDecisions = new Map<string, RoutedSubRunDecision[]>();
|
|
419
|
+
// Process resume directive - apply approval decisions to any
|
|
420
|
+
// pending approvals captured in the previous suspend.
|
|
421
|
+
if (
|
|
422
|
+
resumed &&
|
|
423
|
+
options.directive?.approvals !== undefined &&
|
|
424
|
+
state.pendingApprovals.length > 0
|
|
425
|
+
) {
|
|
426
|
+
yield* processResumeDirective<TOutput>(
|
|
427
|
+
{ state, messages },
|
|
428
|
+
options.directive.approvals,
|
|
429
|
+
resumedApprovedCalls,
|
|
430
|
+
grantedApprovals,
|
|
431
|
+
subRunDecisions,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
// AG-14: the resumed status is left untouched here. A 'failed' run is NOT
|
|
435
|
+
// silently rewritten to 'completed' (the terminal/suspended guard below
|
|
436
|
+
// returns it as-is); a 'completed' run keeps its status and re-enters the
|
|
437
|
+
// loop for a follow-up; an unresolved 'awaiting_approval' run is caught by
|
|
438
|
+
// that same guard.
|
|
439
|
+
|
|
440
|
+
// WI-09: pin the trusted system-prompt prefix length now, on the
|
|
441
|
+
// fully-assembled initial buffer, so auto-compaction never rewrites
|
|
442
|
+
// it and prior summaries stay re-compactable (see
|
|
443
|
+
// `countLeadingSystemMessages`).
|
|
444
|
+
const systemPrefixLength = countLeadingSystemMessages(messages);
|
|
445
|
+
|
|
446
|
+
const runContextBase: RunContext<TDeps> = {
|
|
447
|
+
runId: state.id,
|
|
448
|
+
sessionId,
|
|
449
|
+
...(userId !== undefined ? { userId } : {}),
|
|
450
|
+
agentId,
|
|
451
|
+
deps: (options.deps ?? config.deps) as TDeps,
|
|
452
|
+
tracer,
|
|
453
|
+
signal,
|
|
454
|
+
usage: usageAcc,
|
|
455
|
+
stepNumber: 0,
|
|
456
|
+
messages,
|
|
457
|
+
state,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const handoffNames = Array.from(handoffMap.keys());
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* One run-scoped env threaded into every extracted runtime module
|
|
464
|
+
* (issue #23): the intersection structurally satisfies each
|
|
465
|
+
* module's env interface, so a single object carries the live loop
|
|
466
|
+
* references the former closures captured (field names mirror the
|
|
467
|
+
* original locals). `getPendingAbort` / `getActiveTodos` are live
|
|
468
|
+
* reads of the factory's mutable scratch; `tryEmergencyCompact` and
|
|
469
|
+
* `dispatchBatch` are pre-bound to this same env.
|
|
470
|
+
*/
|
|
471
|
+
type RunLoopEnv = DispatchRunEnv &
|
|
472
|
+
CompactionRunEnv &
|
|
473
|
+
CancellationEnv &
|
|
474
|
+
GuardrailScreenEnv &
|
|
475
|
+
AssistantCommitEnv &
|
|
476
|
+
FallbackChainEnv<TOutput> &
|
|
477
|
+
StepCatalogueEnv<TDeps, TOutput> &
|
|
478
|
+
StepRequestEnv<TDeps, TOutput> &
|
|
479
|
+
ToolCallWalkEnv<TDeps, TOutput> &
|
|
480
|
+
ResumeRunEnv &
|
|
481
|
+
ResumedDispatchEnv<TDeps, TOutput> &
|
|
482
|
+
SubRunResumeEnv<TDeps, TOutput> &
|
|
483
|
+
VerifierGateEnv<TDeps, TOutput> &
|
|
484
|
+
RunOutputEnv<TDeps, TOutput>;
|
|
485
|
+
const runEnv: RunLoopEnv = {
|
|
486
|
+
config,
|
|
487
|
+
options,
|
|
488
|
+
memory,
|
|
489
|
+
state,
|
|
490
|
+
messages,
|
|
491
|
+
sessionId,
|
|
492
|
+
agentId,
|
|
493
|
+
userId,
|
|
494
|
+
signal,
|
|
495
|
+
usageAcc,
|
|
496
|
+
stopWhen,
|
|
497
|
+
fallbackPolicy,
|
|
498
|
+
structuredInstruction,
|
|
499
|
+
systemPrefixLength,
|
|
500
|
+
runContextBase,
|
|
501
|
+
handoffMap,
|
|
502
|
+
handoffNames,
|
|
503
|
+
isCodeMode,
|
|
504
|
+
toolRegistry,
|
|
505
|
+
toolExecutor,
|
|
506
|
+
makeToolExecutor,
|
|
507
|
+
resultReader,
|
|
508
|
+
codeModeAdvertised,
|
|
509
|
+
causalityMonitor,
|
|
510
|
+
toolDataFlowGuard,
|
|
511
|
+
promotedDeferred,
|
|
512
|
+
runStartPromotions,
|
|
513
|
+
activeRunCapability,
|
|
514
|
+
executorBridgeSlot,
|
|
515
|
+
pendingManualCompacts,
|
|
516
|
+
getPendingAbort: () => pendingAbort,
|
|
517
|
+
// W-036: live read of the current step span for sub-agent trace
|
|
518
|
+
// stitching (the child's agent.run parents under it).
|
|
519
|
+
getCurrentStepSpan: () => currentStepSpan,
|
|
520
|
+
getActiveTodos: () => activeRunState?.todos,
|
|
521
|
+
tryEmergencyCompact: () => tryEmergencyCompact<TOutput>(runEnv),
|
|
522
|
+
dispatchBatch: (calls, executor, runContext, stepNum, dispatchOpts) =>
|
|
523
|
+
dispatchToolBatch<TDeps, TOutput>(
|
|
524
|
+
runEnv,
|
|
525
|
+
calls,
|
|
526
|
+
executor,
|
|
527
|
+
runContext,
|
|
528
|
+
stepNum,
|
|
529
|
+
dispatchOpts,
|
|
530
|
+
),
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// AG-14 (failed half): a terminal-failed run must never re-enter the
|
|
534
|
+
// provider loop or dispatch anything - that would silently complete a
|
|
535
|
+
// failed run. Return it as-is.
|
|
536
|
+
if (resumed && state.status === 'failed') {
|
|
537
|
+
return yield* finishRun(state, finalSnapshot);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// AG-1 / agent-02 / agent-07: execute the approved gated calls for
|
|
541
|
+
// REAL before the provider loop, bracketed by the write-ahead intent
|
|
542
|
+
// + post-dispatch checkpoints (see `runtime/approvals.ts`).
|
|
543
|
+
if (resumed && resumedApprovedCalls.length > 0) {
|
|
544
|
+
yield* dispatchResumedApprovals<TDeps, TOutput>(
|
|
545
|
+
runEnv,
|
|
546
|
+
resumedApprovedCalls,
|
|
547
|
+
grantedApprovals,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// W-001: route sub-run decisions into their parked children (grant
|
|
552
|
+
// and deny alike - the child settles them in its own transcript). A
|
|
553
|
+
// child that completes folds back as the parent's tool message; one
|
|
554
|
+
// that suspends again re-parks and the parent re-suspends below.
|
|
555
|
+
if (resumed && subRunDecisions.size > 0) {
|
|
556
|
+
yield* processSubRunResumes<TDeps, TOutput>(runEnv, subRunDecisions);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// AG-14 (suspended half): a resumed run still awaiting approvals the
|
|
560
|
+
// directive did not resolve must not re-enter the provider loop - that
|
|
561
|
+
// would re-issue a dangling tool_use real providers reject. The granted
|
|
562
|
+
// subset (above) HAS executed and is journaled; return the re-suspended
|
|
563
|
+
// state carrying its results. The same guard covers a held 'aborted'
|
|
564
|
+
// state (W-038, `onPendingApprovals: 'hold'`): its pending approvals
|
|
565
|
+
// still map to dangling tool_use, so it resumes only through an
|
|
566
|
+
// explicit directive.
|
|
567
|
+
if (
|
|
568
|
+
resumed &&
|
|
569
|
+
(state.status === 'awaiting_approval' ||
|
|
570
|
+
(state.status === 'aborted' && state.pendingApprovals.length > 0))
|
|
571
|
+
) {
|
|
572
|
+
return yield* finishRun(state, finalSnapshot);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// W-035: seed from the journal so post-resume steps continue the
|
|
576
|
+
// numbering instead of colliding with pre-suspend steps (a fresh
|
|
577
|
+
// run's empty journal keeps the historical 0 start).
|
|
578
|
+
let stepNumber = state.steps.reduce((max, s) => Math.max(max, s.stepNumber), 0);
|
|
579
|
+
// C3: verifier-triggered continuation rounds consumed this run.
|
|
580
|
+
let verifierRoundsUsed = 0;
|
|
581
|
+
// AG-15: tools the model actually called on the PREVIOUS step - the
|
|
582
|
+
// per-tool preferred-model ladder consults these, never the full
|
|
583
|
+
// advertised catalogue.
|
|
584
|
+
let lastStepCalledToolNames: ReadonlyArray<string> = [];
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
while (!stopWhen.check(state)) {
|
|
588
|
+
// Drain any externally-queued lifecycle events
|
|
589
|
+
// (`agent.steered`, `agent.followup.queued`).
|
|
590
|
+
while (externalEventQueue.length > 0) {
|
|
591
|
+
const ev = externalEventQueue.shift();
|
|
592
|
+
if (ev !== undefined) yield ev;
|
|
593
|
+
}
|
|
594
|
+
if (signal.aborted) {
|
|
595
|
+
if (yield* emitCancellation<TOutput>(runEnv)) {
|
|
596
|
+
return yield* finishRun(state, finalSnapshot);
|
|
597
|
+
}
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
stepNumber += 1;
|
|
601
|
+
const stepStart = new Date().toISOString();
|
|
602
|
+
|
|
603
|
+
// Drain steering queue.
|
|
604
|
+
if (pendingSteer.length > 0) {
|
|
605
|
+
for (const m of pendingSteer) {
|
|
606
|
+
messages.push(m);
|
|
607
|
+
state.messages.push(m);
|
|
608
|
+
}
|
|
609
|
+
pendingSteer = [];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
yield { type: 'step.start', stepNumber };
|
|
613
|
+
// C7: defensive end for a span left open by a mid-step exit path.
|
|
614
|
+
currentStepSpan?.end();
|
|
615
|
+
currentStepSpan = tracer.startSpan({
|
|
616
|
+
type: 'agent.step',
|
|
617
|
+
parent: runSpan,
|
|
618
|
+
attrs: {
|
|
619
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
620
|
+
'gen_ai.agent.id': agentId,
|
|
621
|
+
'gen_ai.agent.name': config.name,
|
|
622
|
+
'graphorin.run.id': state.id,
|
|
623
|
+
'graphorin.step.number': stepNumber,
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// WI-09 (P1-1): bound context growth before the provider call.
|
|
628
|
+
// Fires `context.compacted` and rewrites the buffer in place only
|
|
629
|
+
// when the memory ContextEngine's trigger crosses threshold; a
|
|
630
|
+
// no-memory / below-threshold / secret-tier step is a no-op, so
|
|
631
|
+
// the happy-path event stream is unchanged (R10).
|
|
632
|
+
yield* maybeAutoCompact<TOutput>(runEnv);
|
|
633
|
+
|
|
634
|
+
const stepCtx: RunContext<TDeps> = {
|
|
635
|
+
...runContextBase,
|
|
636
|
+
stepNumber,
|
|
637
|
+
messages,
|
|
638
|
+
...(currentStepSpan !== undefined ? { span: currentStepSpan } : {}),
|
|
639
|
+
};
|
|
640
|
+
const overrides = config.prepareStep ? await config.prepareStep(stepCtx) : {};
|
|
641
|
+
|
|
642
|
+
// Resolve the step's registry / executor / catalogue / preferred
|
|
643
|
+
// model / fallback chain (see `runtime/step-catalogue.ts`).
|
|
644
|
+
const { stepRegistry, stepExecutor, stepTools, primary, fallbackChain } =
|
|
645
|
+
resolveStepToolContext<TDeps, TOutput>(runEnv, overrides, lastStepCalledToolNames);
|
|
646
|
+
|
|
647
|
+
// Resolve the effective reasoning-retention policy for this step
|
|
648
|
+
// (RB-42), dropping buffered reasoning when the contract
|
|
649
|
+
// downgrades to `'strip'` (see `runtime/messages.ts`).
|
|
650
|
+
const reasoningPolicy = applyReasoningRetention(
|
|
651
|
+
config.reasoningRetention,
|
|
652
|
+
primary.resolvedProvider,
|
|
653
|
+
messages,
|
|
654
|
+
state.messages,
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
const toolDefs: ReadonlyArray<ToolDefinition> = stepTools.map((t) =>
|
|
658
|
+
toolToDefinition(t as Tool<unknown, unknown, unknown>),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
// Assemble the step's base provider request (AG-3 / D6 / RB-42;
|
|
662
|
+
// see `runtime/step-catalogue.ts`).
|
|
663
|
+
const baseRequest: ProviderRequest = buildBaseRequest<TDeps, TOutput>(
|
|
664
|
+
runEnv,
|
|
665
|
+
overrides,
|
|
666
|
+
toolDefs,
|
|
667
|
+
reasoningPolicy,
|
|
668
|
+
stepNumber,
|
|
669
|
+
currentStepSpan,
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
// Stream the step's provider call across the fallback chain
|
|
673
|
+
// (see `runtime/fallback-chain.ts`); a terminal provider
|
|
674
|
+
// failure is already recorded on `state` when `failed` is set.
|
|
675
|
+
const chain = yield* runProviderFallbackChain<TOutput>(
|
|
676
|
+
runEnv,
|
|
677
|
+
fallbackChain,
|
|
678
|
+
baseRequest,
|
|
679
|
+
primary,
|
|
680
|
+
stepNumber,
|
|
681
|
+
);
|
|
682
|
+
if (chain.failed) {
|
|
683
|
+
return yield* finishRun(state, finalSnapshot);
|
|
684
|
+
}
|
|
685
|
+
const { modelSucceeded, textBuffer, finalCalls, stepReasoningParts } = chain;
|
|
686
|
+
const { stepUsage, lastModelId } = chain;
|
|
687
|
+
|
|
688
|
+
// AG-6: a mid-stream abort that interrupted the stream (no completed
|
|
689
|
+
// model) ends the run as a cancellation - 'aborted', or 'failed'
|
|
690
|
+
// under `onPendingApprovals: 'fail'` ONLY when approvals are
|
|
691
|
+
// actually pending (W-038; an empty queue aborts plainly) - rather
|
|
692
|
+
// than falling through to a 'no-provider-completed' failure. When
|
|
693
|
+
// the model DID complete (e.g. `drain: true` let the step finish),
|
|
694
|
+
// fall through so the step's tool calls run and the graceful stop
|
|
695
|
+
// happens at the loop top.
|
|
696
|
+
if (signal.aborted && !modelSucceeded) {
|
|
697
|
+
yield* emitCancellation<TOutput>(runEnv);
|
|
698
|
+
return yield* finishRun(state, finalSnapshot);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!modelSucceeded) {
|
|
702
|
+
yield {
|
|
703
|
+
type: 'agent.error',
|
|
704
|
+
error: {
|
|
705
|
+
message: 'all configured providers failed without finishing',
|
|
706
|
+
code: 'no-provider-completed',
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
state.status = 'failed';
|
|
710
|
+
state.error = { message: 'no provider completed', code: 'no-provider-completed' };
|
|
711
|
+
return yield* finishRun(state, finalSnapshot);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
usageAcc.add(lastModelId, stepUsage);
|
|
715
|
+
addModelUsage(state, lastModelId, stepUsage);
|
|
716
|
+
accumulateUsage(state.usage, stepUsage);
|
|
717
|
+
|
|
718
|
+
// Lateral-leak commit gate (RB-55 / AG-10 / C6): scan, commit
|
|
719
|
+
// the assistant message, record the taint span, and emit the
|
|
720
|
+
// detection event (see `runtime/run-gates.ts`).
|
|
721
|
+
const leakBlocked = yield* commitAssistantMessage<TOutput>(
|
|
722
|
+
runEnv,
|
|
723
|
+
textBuffer,
|
|
724
|
+
stepReasoningParts,
|
|
725
|
+
finalCalls,
|
|
726
|
+
reasoningPolicy,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
const handoffCalls = finalCalls.filter((c) => handoffMap.has(c.toolName));
|
|
730
|
+
if (handoffCalls.length > 1) {
|
|
731
|
+
throw new MultipleHandoffsInStepError(handoffCalls.map((c) => c.toolName));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const stepRecord: RunStep = {
|
|
735
|
+
stepNumber,
|
|
736
|
+
startedAt: stepStart,
|
|
737
|
+
endedAt: new Date().toISOString(),
|
|
738
|
+
usage: stepUsage,
|
|
739
|
+
toolCalls: [],
|
|
740
|
+
agentId: state.currentAgentId,
|
|
741
|
+
// C3: journal the RAW model response (pre-leak-block text) so
|
|
742
|
+
// createReplayProvider(state) can re-drive the run offline.
|
|
743
|
+
...(config.recordProviderResponses === true
|
|
744
|
+
? {
|
|
745
|
+
providerResponse: {
|
|
746
|
+
modelId: lastModelId,
|
|
747
|
+
...(textBuffer.length > 0 ? { text: textBuffer } : {}),
|
|
748
|
+
...(finalCalls.length > 0 ? { toolCalls: [...finalCalls] } : {}),
|
|
749
|
+
},
|
|
750
|
+
}
|
|
751
|
+
: {}),
|
|
752
|
+
};
|
|
753
|
+
state.steps.push(stepRecord);
|
|
754
|
+
lastStepCalledToolNames = finalCalls.map((c) => c.toolName);
|
|
755
|
+
|
|
756
|
+
if (textBuffer.length > 0 && !leakBlocked) {
|
|
757
|
+
finalSnapshot.output = textBuffer as unknown as TOutput;
|
|
758
|
+
yield { type: 'text.complete', text: textBuffer };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (finalCalls.length > 0) {
|
|
762
|
+
// `stepRegistry` / `stepExecutor` were resolved with the
|
|
763
|
+
// catalogue above (so the advertised tools and the executor's
|
|
764
|
+
// resolvable tools agree, including any `prepareStep` override).
|
|
765
|
+
const execRunContext: RunContext<TDeps> = {
|
|
766
|
+
...runContextBase,
|
|
767
|
+
stepNumber,
|
|
768
|
+
messages,
|
|
769
|
+
...(currentStepSpan !== undefined ? { span: currentStepSpan } : {}),
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// Walk the calls (batch dispatch, inline handoff, approval
|
|
773
|
+
// pre-screen, once-per-step durable-HITL suspend; see
|
|
774
|
+
// `runtime/tool-call-walk.ts`).
|
|
775
|
+
const walked = yield* processStepToolCalls<TDeps, TOutput>(
|
|
776
|
+
runEnv,
|
|
777
|
+
finalCalls,
|
|
778
|
+
stepRegistry,
|
|
779
|
+
stepExecutor,
|
|
780
|
+
execRunContext,
|
|
781
|
+
stepNumber,
|
|
782
|
+
);
|
|
783
|
+
if (walked.suspended) {
|
|
784
|
+
if (walked.abortPending === true) {
|
|
785
|
+
// W-038: the abort raced the suspend - apply the
|
|
786
|
+
// `onPendingApprovals` policy to the just-collected
|
|
787
|
+
// approvals, then persist the FINAL policy-consistent
|
|
788
|
+
// state as the last checkpoint (the walk skipped its
|
|
789
|
+
// 'suspended' put so no stale awaiting_approval trail
|
|
790
|
+
// outlives the aborted outcome).
|
|
791
|
+
yield* emitCancellation<TOutput>(runEnv);
|
|
792
|
+
if (config.checkpointStore !== undefined) {
|
|
793
|
+
await config.checkpointStore.put(
|
|
794
|
+
state.id,
|
|
795
|
+
'agent',
|
|
796
|
+
{
|
|
797
|
+
id: state.id,
|
|
798
|
+
threadId: state.id,
|
|
799
|
+
namespace: 'agent',
|
|
800
|
+
state: serializeRunState(state, { stripTracingApiKey: true }),
|
|
801
|
+
channelVersions: {},
|
|
802
|
+
stepNumber,
|
|
803
|
+
createdAt: new Date().toISOString(),
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
source: 'sync',
|
|
807
|
+
status: state.status === 'failed' ? 'failed' : 'aborted',
|
|
808
|
+
nodeName: 'agent.abort',
|
|
809
|
+
sessionId: state.sessionId,
|
|
810
|
+
},
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return yield* finishRun(state, finalSnapshot);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
currentStepSpan?.setAttributes({
|
|
819
|
+
'gen_ai.usage.input_tokens': stepUsage.promptTokens,
|
|
820
|
+
'gen_ai.usage.output_tokens': stepUsage.completionTokens,
|
|
821
|
+
});
|
|
822
|
+
currentStepSpan?.end();
|
|
823
|
+
currentStepSpan = undefined;
|
|
824
|
+
yield { type: 'step.end', stepNumber, usage: stepUsage };
|
|
825
|
+
|
|
826
|
+
if (finalCalls.length === 0) {
|
|
827
|
+
// C3: verifier gate on the terminal response (see
|
|
828
|
+
// `runtime/run-gates.ts`); a failed round feeds back and the
|
|
829
|
+
// loop takes another step while rounds remain.
|
|
830
|
+
if (config.verifiers !== undefined && config.verifiers.length > 0) {
|
|
831
|
+
const gate = yield* runVerifierGate<TDeps, TOutput>(
|
|
832
|
+
runEnv,
|
|
833
|
+
finalSnapshot,
|
|
834
|
+
stepNumber,
|
|
835
|
+
verifierRoundsUsed,
|
|
836
|
+
);
|
|
837
|
+
verifierRoundsUsed = gate.verifierRoundsUsed;
|
|
838
|
+
if (gate.continueRun) continue;
|
|
839
|
+
}
|
|
840
|
+
state.status = 'completed';
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
} catch (cause) {
|
|
845
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
846
|
+
const code = cause instanceof AgentRuntimeError ? (cause.code as string) : 'unknown';
|
|
847
|
+
yield { type: 'agent.error', error: { message, code } };
|
|
848
|
+
state.status = 'failed';
|
|
849
|
+
state.error = { message, code };
|
|
850
|
+
return yield* finishRun(state, finalSnapshot);
|
|
851
|
+
} finally {
|
|
852
|
+
// C7: close the trace tree on every exit path.
|
|
853
|
+
currentStepSpan?.end();
|
|
854
|
+
currentStepSpan = undefined;
|
|
855
|
+
runSpan.setAttributes({
|
|
856
|
+
'gen_ai.usage.input_tokens': state.usage.promptTokens,
|
|
857
|
+
'gen_ai.usage.output_tokens': state.usage.completionTokens,
|
|
858
|
+
'graphorin.run.status': state.status,
|
|
859
|
+
});
|
|
860
|
+
runSpan.setStatus(state.status === 'failed' ? 'error' : 'ok');
|
|
861
|
+
runSpan.end();
|
|
862
|
+
// AG-5: drop the parent-signal listener so it does not accumulate across
|
|
863
|
+
// runs that share one long-lived `options.signal`. Runs after this point
|
|
864
|
+
// (the follow-up loop) keep working via `agent.abort()` on `localCtl`.
|
|
865
|
+
if (parentSignal !== undefined) {
|
|
866
|
+
parentSignal.removeEventListener('abort', onParentAbort);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Terminal output phases in their frozen order: stop-condition cut
|
|
871
|
+
// (AG-24), structured-output parse (AG-3), output guardrails
|
|
872
|
+
// (AG-2 / SDF-4) - see `runtime/run-gates.ts`.
|
|
873
|
+
yield* finalizeRunOutput<TDeps, TOutput>(runEnv, finalSnapshot);
|
|
874
|
+
activeRunState = undefined;
|
|
875
|
+
return yield* finishRun(state, finalSnapshot);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Terminal path: settle the manual-compact queue, finalize the
|
|
879
|
+
// result, clear terminal-run spill artifacts and emit `agent.end`
|
|
880
|
+
// (see `runtime/run-finish.ts`).
|
|
881
|
+
const finishRunBase = createRunFinisher<TOutput>({
|
|
882
|
+
pendingManualCompacts,
|
|
883
|
+
spillWriter,
|
|
884
|
+
checkpointStore: config.checkpointStore,
|
|
885
|
+
checkpointPolicy: config.checkpointPolicy,
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// The public call surface over the run loop (AG-9; see
|
|
889
|
+
// `runtime/agent-surface.ts`).
|
|
890
|
+
const { stream, run } = createRunMethods<TDeps, TOutput>(runLoop);
|
|
891
|
+
|
|
892
|
+
const steer = (message: AgentInput): void => {
|
|
893
|
+
const { seed } = asMessages(message);
|
|
894
|
+
pendingSteer.push(...seed);
|
|
895
|
+
if (activeRunState !== undefined) {
|
|
896
|
+
externalEventQueue.push({
|
|
897
|
+
type: 'agent.steered',
|
|
898
|
+
runId: activeRunState.id,
|
|
899
|
+
} as AgentEvent<TOutput>);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const followUp = (message: AgentInput): void => {
|
|
904
|
+
const { seed } = asMessages(message);
|
|
905
|
+
pendingFollowUp.push(...seed);
|
|
906
|
+
if (activeRunState !== undefined) {
|
|
907
|
+
externalEventQueue.push({
|
|
908
|
+
type: 'agent.followup.queued',
|
|
909
|
+
runId: activeRunState.id,
|
|
910
|
+
} as AgentEvent<TOutput>);
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
const abort = (options?: AbortOptions): void => {
|
|
915
|
+
pendingAbort = options ?? {};
|
|
916
|
+
abortController?.abort();
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// `agent.toTool()` - the sub-agent tool surface, incl. contextFold
|
|
920
|
+
// and child-taint propagation (see `runtime/agent-to-tool.ts`).
|
|
921
|
+
const toTool = createToTool<TDeps, TOutput>({ config, run, stream });
|
|
922
|
+
|
|
923
|
+
// `compact` / `fanOut` / the progress IO surface (AG-13 / AG-7 /
|
|
924
|
+
// AG-20; see `runtime/agent-surface.ts`). The deps object reads the
|
|
925
|
+
// factory's mutable `activeRunState` scratch through a live getter.
|
|
926
|
+
const surfaceDeps = {
|
|
927
|
+
config,
|
|
928
|
+
memory,
|
|
929
|
+
agentId,
|
|
930
|
+
getActiveRunState: () => activeRunState,
|
|
931
|
+
externalEventQueue,
|
|
932
|
+
pendingManualCompacts,
|
|
933
|
+
progressIO,
|
|
934
|
+
};
|
|
935
|
+
const compact = createCompactMethod<TDeps, TOutput>(surfaceDeps);
|
|
936
|
+
const fanOut = createFanOutMethod<TDeps, TOutput>(surfaceDeps);
|
|
937
|
+
const progress: AgentProgressIO = createProgressSurface<TDeps, TOutput>(surfaceDeps);
|
|
938
|
+
|
|
939
|
+
void config.sensitivity as Sensitivity | undefined;
|
|
940
|
+
|
|
941
|
+
const agent: Agent<TDeps, TOutput> = {
|
|
942
|
+
id: agentId,
|
|
943
|
+
config,
|
|
944
|
+
stream,
|
|
945
|
+
run,
|
|
946
|
+
steer,
|
|
947
|
+
followUp,
|
|
948
|
+
abort,
|
|
949
|
+
toTool,
|
|
950
|
+
compact,
|
|
951
|
+
fanOut,
|
|
952
|
+
progress,
|
|
953
|
+
registry: toolRegistry,
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
return agent;
|
|
957
|
+
}
|