@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +31 -11
  3. package/dist/errors/index.d.ts +17 -4
  4. package/dist/errors/index.d.ts.map +1 -1
  5. package/dist/errors/index.js +19 -3
  6. package/dist/errors/index.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +153 -1930
  9. package/dist/factory.js.map +1 -1
  10. package/dist/fanout/index.d.ts +13 -1
  11. package/dist/fanout/index.d.ts.map +1 -1
  12. package/dist/fanout/index.js +13 -4
  13. package/dist/fanout/index.js.map +1 -1
  14. package/dist/index.d.ts +7 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +11 -9
  17. package/dist/index.js.map +1 -1
  18. package/dist/lateral-leak/index.js +1 -1
  19. package/dist/package.js +6 -0
  20. package/dist/package.js.map +1 -0
  21. package/dist/run-state/index.d.ts +32 -6
  22. package/dist/run-state/index.d.ts.map +1 -1
  23. package/dist/run-state/index.js +46 -22
  24. package/dist/run-state/index.js.map +1 -1
  25. package/dist/runtime/agent-surface.js +122 -0
  26. package/dist/runtime/agent-surface.js.map +1 -0
  27. package/dist/runtime/agent-to-tool.d.ts +51 -0
  28. package/dist/runtime/agent-to-tool.d.ts.map +1 -0
  29. package/dist/runtime/agent-to-tool.js +145 -0
  30. package/dist/runtime/agent-to-tool.js.map +1 -0
  31. package/dist/runtime/approvals.js +0 -0
  32. package/dist/runtime/approvals.js.map +1 -0
  33. package/dist/runtime/dispatch.js +108 -0
  34. package/dist/runtime/dispatch.js.map +1 -0
  35. package/dist/runtime/executor-wiring.js +128 -0
  36. package/dist/runtime/executor-wiring.js.map +1 -0
  37. package/dist/runtime/fallback-chain.js +139 -0
  38. package/dist/runtime/fallback-chain.js.map +1 -0
  39. package/dist/runtime/handoff.js +307 -0
  40. package/dist/runtime/handoff.js.map +1 -0
  41. package/dist/runtime/messages.d.ts +22 -0
  42. package/dist/runtime/messages.d.ts.map +1 -0
  43. package/dist/runtime/messages.js +204 -0
  44. package/dist/runtime/messages.js.map +1 -0
  45. package/dist/runtime/provider-events.js +117 -0
  46. package/dist/runtime/provider-events.js.map +1 -0
  47. package/dist/runtime/run-compaction.js +210 -0
  48. package/dist/runtime/run-compaction.js.map +1 -0
  49. package/dist/runtime/run-finish.js +48 -0
  50. package/dist/runtime/run-finish.js.map +1 -0
  51. package/dist/runtime/run-gates.js +336 -0
  52. package/dist/runtime/run-gates.js.map +1 -0
  53. package/dist/runtime/run-init.js +81 -0
  54. package/dist/runtime/run-init.js.map +1 -0
  55. package/dist/runtime/run-input.js +46 -0
  56. package/dist/runtime/run-input.js.map +1 -0
  57. package/dist/runtime/step-catalogue.js +173 -0
  58. package/dist/runtime/step-catalogue.js.map +1 -0
  59. package/dist/runtime/tool-call-walk.js +189 -0
  60. package/dist/runtime/tool-call-walk.js.map +1 -0
  61. package/dist/runtime/tool-wiring.js +159 -0
  62. package/dist/runtime/tool-wiring.js.map +1 -0
  63. package/dist/tooling/adapters.js +1 -1
  64. package/dist/tooling/dataflow.js +1 -1
  65. package/dist/tooling/policy.js +2 -0
  66. package/dist/tooling/policy.js.map +1 -1
  67. package/dist/types.d.ts +63 -13
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +20 -20
  70. package/src/errors/index.ts +320 -0
  71. package/src/evaluator-optimizer/index.ts +212 -0
  72. package/src/factory.ts +957 -0
  73. package/src/fallback/index.ts +108 -0
  74. package/src/fanout/index.ts +523 -0
  75. package/src/filters/index.ts +347 -0
  76. package/src/index.ts +180 -0
  77. package/src/internal/ids.ts +46 -0
  78. package/src/internal/usage-accumulator.ts +90 -0
  79. package/src/lateral-leak/causality-monitor.ts +221 -0
  80. package/src/lateral-leak/index.ts +35 -0
  81. package/src/lateral-leak/merge-guard.ts +151 -0
  82. package/src/lateral-leak/protocol-guard.ts +222 -0
  83. package/src/preferred-model/index.ts +210 -0
  84. package/src/progress/index.ts +238 -0
  85. package/src/run-state/index.ts +607 -0
  86. package/src/runtime/agent-surface.ts +218 -0
  87. package/src/runtime/agent-to-tool.ts +323 -0
  88. package/src/runtime/approvals.ts +0 -0
  89. package/src/runtime/dispatch.ts +183 -0
  90. package/src/runtime/executor-wiring.ts +331 -0
  91. package/src/runtime/fallback-chain.ts +250 -0
  92. package/src/runtime/handoff.ts +428 -0
  93. package/src/runtime/messages.ts +309 -0
  94. package/src/runtime/provider-events.ts +175 -0
  95. package/src/runtime/run-compaction.ts +288 -0
  96. package/src/runtime/run-finish.ts +93 -0
  97. package/src/runtime/run-gates.ts +419 -0
  98. package/src/runtime/run-init.ts +169 -0
  99. package/src/runtime/run-input.ts +102 -0
  100. package/src/runtime/step-catalogue.ts +338 -0
  101. package/src/runtime/tool-call-walk.ts +301 -0
  102. package/src/runtime/tool-wiring.ts +218 -0
  103. package/src/testing/replay-provider.ts +121 -0
  104. package/src/tooling/adapters.ts +403 -0
  105. package/src/tooling/catalogue.ts +36 -0
  106. package/src/tooling/dataflow.ts +171 -0
  107. package/src/tooling/plan.ts +123 -0
  108. package/src/tooling/policy.ts +67 -0
  109. package/src/tooling/registry-build.ts +191 -0
  110. package/src/types.ts +696 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Tool-batch dispatch bridging for the agent runtime: routes a batch of
3
+ * (non-handoff) tool calls through the shared `@graphorin/tools`
4
+ * executor and surfaces the outcomes as the long-standing
5
+ * `tool.execute.*` agent events + tool messages (R10), including the
6
+ * WI-10 result-handle rendering and the WI-05 `tool_search` promotion
7
+ * fold. Extracted verbatim from `factory.ts` (issue #23); the former
8
+ * run-loop closure now takes an explicit {@link DispatchRunEnv}.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ import type {
14
+ AgentEvent,
15
+ CompletedToolCall,
16
+ Message,
17
+ RunContext,
18
+ RunState,
19
+ ToolCall,
20
+ } from '@graphorin/core';
21
+ import type { ToolExecutor } from '@graphorin/tools/executor';
22
+ import type { CausalityMonitor } from '../lateral-leak/causality-monitor.js';
23
+ import { createExecutorEventBridge, type ExecutorEventBridge } from '../tooling/adapters.js';
24
+ import { renderToolErrorMessage } from './messages.js';
25
+ import type { MutableRunState } from './run-input.js';
26
+ import { recordToolSearchPromotions, TOOL_SEARCH_NAME } from './tool-wiring.js';
27
+
28
+ /**
29
+ * Shape of the run-bound `dispatchBatch` wrapper the factory threads
30
+ * into the resume-dispatch and tool-call-walk modules, so their call
31
+ * sites read exactly as the former in-loop closure calls did.
32
+ */
33
+ export type DispatchBatchFn<TDeps, TOutput> = (
34
+ calls: ReadonlyArray<ToolCall>,
35
+ executor: ToolExecutor,
36
+ runContext: RunContext<TDeps>,
37
+ stepNum: number,
38
+ dispatchOpts?: { readonly disableRepair?: boolean },
39
+ ) => AsyncGenerator<AgentEvent<TOutput>, void, void>;
40
+
41
+ /**
42
+ * The run-scoped context a batch dispatch operates on. Field names
43
+ * mirror the run-loop locals the former closure captured; the
44
+ * executor-bridge slot is the shared mutable cell the warm-up
45
+ * streaming sink reads while a batch is in flight.
46
+ */
47
+ export interface DispatchRunEnv {
48
+ readonly state: MutableRunState & RunState;
49
+ readonly messages: Message[];
50
+ readonly causalityMonitor: CausalityMonitor | undefined;
51
+ readonly promotedDeferred: Set<string>;
52
+ readonly activeRunCapability: 'read-only' | undefined;
53
+ readonly executorBridgeSlot: { current: ExecutorEventBridge | undefined };
54
+ }
55
+
56
+ /**
57
+ * Dispatch a batch of (non-handoff) tool calls through the
58
+ * {@link ToolExecutor} and surface the results as `AgentEvent`s.
59
+ *
60
+ * The agent owns the `tool.execute.start` / `.end` / `.error`
61
+ * lifecycle (derived deterministically from the returned
62
+ * {@link CompletedToolCall} outcomes) so every outcome kind -
63
+ * success, unknown-tool, invalid-input, sanitization-blocked,
64
+ * execution error - yields a consistent event and tool message,
65
+ * preserving the pre-WI-03 stream shape (R10). The executor's
66
+ * genuinely-live streaming events (`tool.execute.progress` /
67
+ * `.partial`, emitted only by streaming-hint tools) are bridged
68
+ * through Adapter E while the batch runs and are purely additive.
69
+ *
70
+ * Parallelism (WI-04): the executor runs independent calls in this
71
+ * batch concurrently, bounded by `maxParallelTools`. `tool.execute.start`
72
+ * is emitted up-front in call order and `.end` / `.error` after the
73
+ * batch settles, also in call order - so result mapping and tool-message
74
+ * history are deterministic regardless of which call finishes first,
75
+ * while `.progress` / `.partial` events for concurrent calls interleave
76
+ * (keyed by `toolCallId`). Tools declaring `executionMode: 'sequential'`
77
+ * are serialised by the executor and never overlap.
78
+ */
79
+ export async function* dispatchToolBatch<TDeps, TOutput>(
80
+ env: DispatchRunEnv,
81
+ calls: ReadonlyArray<ToolCall>,
82
+ executor: ToolExecutor,
83
+ runContext: RunContext<TDeps>,
84
+ stepNum: number,
85
+ dispatchOpts: { readonly disableRepair?: boolean } = {},
86
+ ): AsyncGenerator<AgentEvent<TOutput>, void, void> {
87
+ const { state, messages, causalityMonitor, promotedDeferred, activeRunCapability } = env;
88
+ const { executorBridgeSlot } = env;
89
+ if (calls.length === 0) return;
90
+ for (const call of calls) {
91
+ // W-049: toolName duplicated for subscriber convenience.
92
+ yield { type: 'tool.execute.start', toolCallId: call.toolCallId, toolName: call.toolName };
93
+ }
94
+
95
+ const bridge = createExecutorEventBridge();
96
+ executorBridgeSlot.current = bridge;
97
+ const resultsPromise = executor.executeBatch({
98
+ calls,
99
+ runContext,
100
+ stepNumber: stepNum,
101
+ ...(dispatchOpts.disableRepair !== undefined
102
+ ? { disableRepair: dispatchOpts.disableRepair }
103
+ : {}),
104
+ // D2: the run's capability restriction rides every batch.
105
+ ...(activeRunCapability !== undefined ? { capability: activeRunCapability } : {}),
106
+ });
107
+ // Close the bridge once the batch settles so `drain()` ends; the
108
+ // executor catches per-call errors, so the batch never rejects.
109
+ const closeOnSettle = resultsPromise.then(
110
+ () => bridge.close(),
111
+ () => bridge.close(),
112
+ );
113
+ for await (const event of bridge.drain()) {
114
+ if (event.type === 'tool.execute.progress' || event.type === 'tool.execute.partial') {
115
+ yield event as AgentEvent<TOutput>;
116
+ }
117
+ }
118
+ await closeOnSettle;
119
+ executorBridgeSlot.current = undefined;
120
+
121
+ const completed = await resultsPromise;
122
+ const byCallId = new Map(completed.map((c) => [c.outcome.toolCallId, c]));
123
+ const stepEntry = state.steps[state.steps.length - 1];
124
+ for (const call of calls) {
125
+ const result = byCallId.get(call.toolCallId);
126
+ if (result === undefined) continue;
127
+ if (stepEntry !== undefined) {
128
+ (stepEntry.toolCalls as CompletedToolCall[]).push(result);
129
+ }
130
+ const outcome = result.outcome;
131
+ if ('kind' in outcome) {
132
+ yield {
133
+ type: 'tool.execute.error',
134
+ toolCallId: call.toolCallId,
135
+ toolName: call.toolName,
136
+ error: outcome,
137
+ };
138
+ const text = renderToolErrorMessage(outcome);
139
+ messages.push({ role: 'tool', toolCallId: call.toolCallId, content: text });
140
+ state.messages.push({ role: 'tool', toolCallId: call.toolCallId, content: text });
141
+ causalityMonitor?.recordCall(`tool.error:${call.toolName}`);
142
+ } else {
143
+ const output = outcome.output;
144
+ yield {
145
+ type: 'tool.execute.end',
146
+ toolCallId: call.toolCallId,
147
+ toolName: call.toolName,
148
+ result: output,
149
+ durationMs: outcome.durationMs,
150
+ };
151
+ // WI-10 (P1-4): when the result spilled to a handle, inline only
152
+ // the bounded preview plus a retrieval hint so the full blob never
153
+ // enters the context window - the model fetches the rest on demand
154
+ // via `read_result`. Inlined results serialise exactly as before
155
+ // (preserves the happy-path message contract, R10).
156
+ const handle = outcome.resultHandle;
157
+ const rendered =
158
+ handle !== undefined
159
+ ? `${handle.preview}\n\n[Full result${
160
+ handle.bytes !== undefined ? ` (${handle.bytes} bytes)` : ''
161
+ } stored behind a handle. Call read_result with handle ${JSON.stringify(
162
+ handle.uri,
163
+ )} to retrieve it - optionally narrow with offset/length (bytes) or startLine/endLine.]`
164
+ : typeof output === 'string'
165
+ ? output
166
+ : JSON.stringify(output);
167
+ // C3 (ACI): an empty body reads as a glitch to models; say
168
+ // explicitly that the tool ran and simply had nothing to print.
169
+ const text =
170
+ rendered === undefined || rendered.trim().length === 0
171
+ ? '(tool ran successfully with no output)'
172
+ : rendered;
173
+ messages.push({ role: 'tool', toolCallId: call.toolCallId, content: text });
174
+ state.messages.push({ role: 'tool', toolCallId: call.toolCallId, content: text });
175
+ causalityMonitor?.recordCall(`tool:${call.toolName}`);
176
+ // WI-05: a successful `tool_search` promotes its matches so the
177
+ // catalogue advertises them on the next step.
178
+ if (call.toolName === TOOL_SEARCH_NAME) {
179
+ recordToolSearchPromotions(output, promotedDeferred);
180
+ }
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Warm-up wiring of the agent's tool-execution stack: the spill
3
+ * writer / result-reader pair (WI-10 / WI-13), the memory guard,
4
+ * data-flow guard and Progent argument-policy hooks, the shared
5
+ * `ToolExecutor` factory (WI-03), and the opt-in code-mode meta-tool
6
+ * surface (WI-11). Extracted verbatim from `factory.ts` (issue #23).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import type { RunState, Tool } from '@graphorin/core';
12
+ import type { Memory } from '@graphorin/memory';
13
+ import {
14
+ createToolExecutor,
15
+ type ExecutorOptions,
16
+ type ToolExecutor,
17
+ } from '@graphorin/tools/executor';
18
+ import type { ToolRegistry } from '@graphorin/tools/registry';
19
+ import {
20
+ createDefaultSpillWriter,
21
+ createFileResultReader,
22
+ type ResultReader,
23
+ } from '@graphorin/tools/result';
24
+ import {
25
+ buildMemoryGuard,
26
+ buildSecretResolver,
27
+ buildToolTokenCounter,
28
+ createMemoryRegionReader,
29
+ type ExecutorEventBridge,
30
+ } from '../tooling/adapters.js';
31
+ import { buildDataFlowGuard } from '../tooling/dataflow.js';
32
+ import { createPlanTool, PLAN_TOOL_NAME } from '../tooling/plan.js';
33
+ import { buildToolArgumentPolicy } from '../tooling/policy.js';
34
+ import { buildToolRegistry } from '../tooling/registry-build.js';
35
+ import type { AgentConfig } from '../types.js';
36
+ import {
37
+ CODE_EXECUTE_NAME,
38
+ CODE_SEARCH_NAME,
39
+ composeResultReaders,
40
+ READ_RESULT_NAME,
41
+ registerCodeMode,
42
+ registerReadResult,
43
+ registerToolSearch,
44
+ TOOL_SEARCH_NAME,
45
+ } from './tool-wiring.js';
46
+
47
+ /** What {@link wireToolExecution} needs from the agent factory scope. */
48
+ export interface ExecutorWiringDeps<TDeps, TOutput> {
49
+ readonly config: AgentConfig<TDeps, TOutput>;
50
+ readonly memory: Memory | undefined;
51
+ readonly agentId: string;
52
+ /** The synthetic handoff tool names (reserved from the code API). */
53
+ readonly handoffToolNames: Iterable<string>;
54
+ /** Lazy view of the in-flight run (read mid-run by the memory guard). */
55
+ readonly getActiveRunState: () => RunState | undefined;
56
+ /** Lazy view of the active run's capability (read by the code bridge). */
57
+ readonly getActiveRunCapability: () => 'read-only' | undefined;
58
+ /** Shared cell the streaming sink reads while a batch is in flight. */
59
+ readonly executorBridgeSlot: { current: ExecutorEventBridge | undefined };
60
+ }
61
+
62
+ /** The executor stack {@link wireToolExecution} hands back to the factory. */
63
+ export interface ExecutorWiring<TDeps> {
64
+ readonly toolRegistry: ToolRegistry;
65
+ readonly spillWriter: ReturnType<typeof createDefaultSpillWriter>;
66
+ readonly resultReader: ResultReader;
67
+ readonly makeToolExecutor: (
68
+ registry: ToolRegistry,
69
+ opts?: { readonly quiet?: boolean },
70
+ ) => ToolExecutor;
71
+ readonly toolExecutor: ToolExecutor;
72
+ readonly toolDataFlowGuard: ReturnType<typeof buildDataFlowGuard> | undefined;
73
+ readonly ruleOfTwoCapabilityFloor: 'read-only' | undefined;
74
+ readonly isCodeMode: boolean;
75
+ readonly codeModeAdvertised: ReadonlyArray<Tool<unknown, unknown, TDeps>>;
76
+ }
77
+
78
+ /**
79
+ * Construct the unified ToolExecutor once at warm-up (WI-03 / P0-1),
80
+ * bound to the registry above. Routing tool execution through the
81
+ * executor activates the documented tool fields the inline loop
82
+ * bypassed: per-tool `secretsAllowed` ACL, result truncation
83
+ * (`maxResultTokens` / `truncationStrategy`), inbound sanitization,
84
+ * memory-guard, idempotency keys and single-round repair.
85
+ *
86
+ * Durable HITL stays in the agent: approval is pre-screened below and
87
+ * suspends the run, so the executor's `ApprovalGate` only ever sees
88
+ * no-approval / pre-approved calls - it auto-grants and never blocks
89
+ * the generator (Adapter G).
90
+ *
91
+ * Sandbox note: `config.tools` are inline `tool({...})` closures that
92
+ * cannot be serialised to an out-of-process sandbox, and
93
+ * `resolveSandbox` defaults user-defined tools to `worker-threads`.
94
+ * Wiring a resolver that returned a real sandbox for that kind would
95
+ * break every inline tool, so `sandboxResolver` is intentionally left
96
+ * unset (the executor then runs inline - its documented fallback).
97
+ * The resolved policy is still surfaced on the tool-execute span /
98
+ * audit; real isolation applies to module-loadable (skill / MCP)
99
+ * tools and is wired when those land.
100
+ */
101
+ export function wireToolExecution<TDeps, TOutput>(
102
+ deps: ExecutorWiringDeps<TDeps, TOutput>,
103
+ ): ExecutorWiring<TDeps> {
104
+ const { config, memory, agentId, executorBridgeSlot } = deps;
105
+ const { getActiveRunState, getActiveRunCapability, handoffToolNames } = deps;
106
+
107
+ // Assemble the unified tool registry at warm-up (Principle #12): one
108
+ // registry across first-party + skill sources, with deterministic
109
+ // cross-source collision resolution. Exposed read-only as
110
+ // `agent.registry`; the run loop and `tool_search` consume it. The
111
+ // registry is the tool-validation authority, so a malformed tool
112
+ // fails fast here at construction.
113
+ const toolRegistry = buildToolRegistry({
114
+ ...(config.tools !== undefined
115
+ ? { tools: config.tools as ReadonlyArray<Tool<unknown, unknown, unknown>> }
116
+ : {}),
117
+ ...(config.skills !== undefined ? { skills: config.skills } : {}),
118
+ }).registry;
119
+
120
+ // WI-05 (deferred loading + tool_search / P0-3): if any registered
121
+ // tool sets `defer_loading: true`, register the built-in `tool_search`
122
+ // so the model can discover those tools on demand. Tools that defer are
123
+ // withheld from the per-step catalogue (see the loop below) until a
124
+ // `tool_search` match promotes them, keeping large tool sets out of the
125
+ // context window. When nothing defers this is a no-op.
126
+ registerToolSearch(
127
+ toolRegistry,
128
+ config.toolPromotion === 'run-boundary' ? 'next-run' : 'next-step',
129
+ );
130
+
131
+ // D6: opt-in structured plan tool (TodoWrite-style, journaled). It
132
+ // mutates the ACTIVE run's todos through a closure; attention
133
+ // recitation renders them back into each step's request copy.
134
+ if (config.plan === true && toolRegistry.get(PLAN_TOOL_NAME) === undefined) {
135
+ toolRegistry.register(
136
+ createPlanTool((todos) => {
137
+ const activeRunState = getActiveRunState();
138
+ if (activeRunState !== undefined) {
139
+ (activeRunState as { todos?: ReadonlyArray<import('@graphorin/core').TodoItem> }).todos =
140
+ todos;
141
+ }
142
+ }) as unknown as Parameters<typeof toolRegistry.register>[0],
143
+ { kind: 'built-in', subsystem: 'planning' },
144
+ );
145
+ }
146
+
147
+ // WI-10 (result references / handles / P1-4): construct one spill
148
+ // writer + reader pair at warm-up. The writer is handed to the executor
149
+ // so a tool's `'spill-to-file'` truncation strategy externalises large
150
+ // bodies to disk (0600, run-scoped); the reader - over the *same*
151
+ // artifact root - backs the built-in `read_result` tool so the model can
152
+ // page through a spilled artifact on demand instead of inlining the
153
+ // whole blob. `read_result` is registered only when some tool spills.
154
+ const spillWriter = createDefaultSpillWriter();
155
+ const fileResultReader = createFileResultReader({ artifactRoot: spillWriter.artifactRoot });
156
+ // WI-13 (P2-2): compose any operator-supplied result readers (e.g. an
157
+ // MCP resource reader from `createMcpResourceReader`, for resolving
158
+ // `resource_link` handles) after the spill-file reader. `read_result`
159
+ // then pages both `graphorin-spill:` artifacts and external handles,
160
+ // and is force-registered when external readers exist (even if no tool
161
+ // spills) so those handles are resolvable.
162
+ const externalResultReaders = config.resultReaders ?? [];
163
+ const resultReader: ResultReader =
164
+ externalResultReaders.length === 0
165
+ ? fileResultReader
166
+ : composeResultReaders([fileResultReader, ...externalResultReaders]);
167
+ registerReadResult(toolRegistry, resultReader, { force: externalResultReaders.length > 0 });
168
+
169
+ const toolApprovalGate: NonNullable<ExecutorOptions['approvalGate']> = {
170
+ request: async () => ({ granted: true }),
171
+ };
172
+ const toolSecretResolver = buildSecretResolver();
173
+ const toolTokenCounter = buildToolTokenCounter();
174
+ // SDF-1: when memory is wired, bind a scope-aware region reader so the
175
+ // executor's DEC-153 snapshot/verify cycle actually runs (the scope
176
+ // resolves lazily from the in-flight run - the executor only invokes
177
+ // the reader mid-run). Without memory the guard tiers stay skipped,
178
+ // and a one-time WARN below makes that silent no-op visible.
179
+ const memoryGuardWiring = buildMemoryGuard(
180
+ memory,
181
+ memory === undefined
182
+ ? {}
183
+ : {
184
+ regionReader: createMemoryRegionReader(['working'], async (region) => {
185
+ if (region !== 'working') return '';
186
+ const activeRunState = getActiveRunState();
187
+ const scope = {
188
+ userId: activeRunState?.userId ?? agentId,
189
+ ...(activeRunState?.sessionId !== undefined
190
+ ? { sessionId: activeRunState.sessionId }
191
+ : {}),
192
+ agentId,
193
+ };
194
+ // W-054: call the statically-typed `Memory.working.compile`
195
+ // directly - a signature change must break THIS build, not
196
+ // silently turn the DEC-153 guard into a no-op through an
197
+ // unchecked double cast returning ''.
198
+ try {
199
+ return await memory.working.compile(scope, agentId);
200
+ } catch {
201
+ // The reader is best-effort: a failed region read must not
202
+ // turn the guard step into a run failure.
203
+ return '';
204
+ }
205
+ }),
206
+ },
207
+ );
208
+ const toolMemoryGuardFactory = memoryGuardWiring.memoryGuardFactory;
209
+ if (memory === undefined) {
210
+ const guarded = (config.tools ?? []).filter((t) => t.memoryGuardTier !== undefined);
211
+ if (guarded.length > 0) {
212
+ console.warn(
213
+ `[graphorin/agent] ${guarded.length} tool(s) declare memoryGuardTier but no memory is wired - ` +
214
+ 'the DEC-153 snapshot/verify guard is skipped (SDF-1). Wire `memory` to activate it.',
215
+ );
216
+ }
217
+ }
218
+ // Provenance / data-flow guard (WI-12 / P1-3, opt-in). Built once and
219
+ // shared by every executor (direct + code-mode quiet), so the sink gate
220
+ // and taint recording apply uniformly. Off unless configured with a
221
+ // non-`'off'` mode - zero overhead on the default path.
222
+ const toolDataFlowGuard =
223
+ config.dataFlowPolicy !== undefined && config.dataFlowPolicy.mode !== 'off'
224
+ ? buildDataFlowGuard(config.dataFlowPolicy)
225
+ : undefined;
226
+ // W-103: the lethal-trifecta leg arms only when some tool can classify
227
+ // as `sensitive` - by default that means `sensitivity: 'secret'`, and
228
+ // no built-in tool ships with that tag. An operator who enables the
229
+ // policy without tagging private-data tools silently gets only the
230
+ // best-effort verbatim probe. One deterministic warn at construction
231
+ // (mirrors the memoryGuardTier warn above); tools registered later are
232
+ // not scanned - this is a wiring aid, not a gate.
233
+ if (
234
+ toolDataFlowGuard !== undefined &&
235
+ (config.dataFlowPolicy?.guardTrifecta ?? true) &&
236
+ config.dataFlowPolicy?.treatPiiAsSensitive !== true
237
+ ) {
238
+ const effectiveTiers = config.dataFlowPolicy?.sensitiveTiers ?? (['secret'] as const);
239
+ const anySensitiveTool = (config.tools ?? []).some(
240
+ (t) => t.sensitivity !== undefined && effectiveTiers.includes(t.sensitivity),
241
+ );
242
+ if (!anySensitiveTool) {
243
+ console.warn(
244
+ '[graphorin/agent] dataFlowPolicy is enabled but no registered tool declares a ' +
245
+ "sensitivity within sensitiveTiers (default ['secret']) and treatPiiAsSensitive is " +
246
+ 'off - the lethal-trifecta leg cannot arm; only the best-effort verbatim ' +
247
+ "untrusted-to-sink probe is active. Tag private-data tools (sensitivity: 'secret') " +
248
+ 'or widen sensitiveTiers / treatPiiAsSensitive.',
249
+ );
250
+ }
251
+ }
252
+ // D4 Progent tool-argument policy + Rule-of-Two floor. `ruleOfTwo`
253
+ // compiles to a policy (+ a read-only capability floor when it denies
254
+ // external side effects); an explicit `toolPolicy` composes on top
255
+ // (its rules are appended, so an explicit forbid still wins). Built
256
+ // once, shared by every executor.
257
+ const { guard: toolArgumentPolicyGuard, capabilityFloor: ruleOfTwoCapabilityFloor } =
258
+ buildToolArgumentPolicy(config.toolPolicy, config.ruleOfTwo);
259
+ const toolStreamingSink: NonNullable<ExecutorOptions['streamingSink']> = (event) =>
260
+ executorBridgeSlot.current?.sink(event);
261
+ // `quiet` builds an executor without the streaming sink - used for
262
+ // code-mode's in-script tool calls (WI-11), whose `tool.execute.*`
263
+ // events must not interleave into the outer agent stream.
264
+ const makeToolExecutor = (
265
+ registry: ToolRegistry,
266
+ opts?: { readonly quiet?: boolean },
267
+ ): ToolExecutor =>
268
+ createToolExecutor({
269
+ registry,
270
+ approvalGate: toolApprovalGate,
271
+ secretResolver: toolSecretResolver,
272
+ tokenCounter: toolTokenCounter,
273
+ memoryGuardFactory: toolMemoryGuardFactory,
274
+ ...(memoryGuardWiring.memoryRegionReader !== undefined
275
+ ? { memoryRegionReader: memoryGuardWiring.memoryRegionReader }
276
+ : {}),
277
+ spill: spillWriter,
278
+ ...(toolDataFlowGuard !== undefined ? { dataFlowGuard: toolDataFlowGuard } : {}),
279
+ ...(toolArgumentPolicyGuard !== undefined ? { argumentPolicy: toolArgumentPolicyGuard } : {}),
280
+ ...(opts?.quiet === true ? {} : { streamingSink: toolStreamingSink }),
281
+ ...(config.maxParallelTools !== undefined
282
+ ? { maxParallelTools: config.maxParallelTools }
283
+ : {}),
284
+ ...(config.toolRetry !== undefined ? { retry: config.toolRetry } : {}),
285
+ });
286
+ const toolExecutor = makeToolExecutor(toolRegistry);
287
+
288
+ // Code-mode (WI-11 / P1-2, opt-in): advertise only the `code_search` /
289
+ // `code_execute` meta-tools and let the model orchestrate tools inside a
290
+ // sandbox, so intermediate results stay out of context. A quiet executor
291
+ // backs the in-script tool bridge (same per-tool governance as direct
292
+ // mode). `read_result` is registered after the meta-tools because
293
+ // `code_execute` opts into `'spill-to-file'`, so a large final result can
294
+ // be fetched back on demand. Default `'direct'` mode leaves all of this
295
+ // untouched - `codeModeAdvertised` stays empty and the loop is unchanged.
296
+ const isCodeMode = config.toolInvocation === 'code-mode';
297
+ let codeModeAdvertised: ReadonlyArray<Tool<unknown, unknown, TDeps>> = [];
298
+ if (isCodeMode) {
299
+ const reserved = new Set<string>([
300
+ CODE_EXECUTE_NAME,
301
+ CODE_SEARCH_NAME,
302
+ TOOL_SEARCH_NAME,
303
+ READ_RESULT_NAME,
304
+ ...handoffToolNames,
305
+ ]);
306
+ const metas = registerCodeMode(
307
+ toolRegistry,
308
+ makeToolExecutor(toolRegistry, { quiet: true }),
309
+ reserved,
310
+ getActiveRunCapability,
311
+ );
312
+ registerReadResult(toolRegistry, resultReader);
313
+ const readResult = toolRegistry.get(READ_RESULT_NAME);
314
+ codeModeAdvertised = [
315
+ ...metas,
316
+ ...(readResult !== undefined ? [readResult] : []),
317
+ ] as ReadonlyArray<Tool<unknown, unknown, TDeps>>;
318
+ }
319
+
320
+ return {
321
+ toolRegistry,
322
+ spillWriter,
323
+ resultReader,
324
+ makeToolExecutor,
325
+ toolExecutor,
326
+ toolDataFlowGuard,
327
+ ruleOfTwoCapabilityFloor,
328
+ isCodeMode,
329
+ codeModeAdvertised,
330
+ };
331
+ }