@graphorin/agent 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +2 -2
  3. package/dist/errors/index.d.ts +17 -4
  4. package/dist/errors/index.d.ts.map +1 -1
  5. package/dist/errors/index.js +19 -3
  6. package/dist/errors/index.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +153 -1930
  9. package/dist/factory.js.map +1 -1
  10. package/dist/fanout/index.d.ts +13 -1
  11. package/dist/fanout/index.d.ts.map +1 -1
  12. package/dist/fanout/index.js +13 -4
  13. package/dist/fanout/index.js.map +1 -1
  14. package/dist/index.d.ts +6 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -7
  17. package/dist/index.js.map +1 -1
  18. package/dist/lateral-leak/index.js +1 -1
  19. package/dist/package.js +1 -1
  20. package/dist/package.js.map +1 -1
  21. package/dist/run-state/index.d.ts +32 -6
  22. package/dist/run-state/index.d.ts.map +1 -1
  23. package/dist/run-state/index.js +46 -22
  24. package/dist/run-state/index.js.map +1 -1
  25. package/dist/runtime/agent-surface.js +122 -0
  26. package/dist/runtime/agent-surface.js.map +1 -0
  27. package/dist/runtime/agent-to-tool.d.ts +51 -0
  28. package/dist/runtime/agent-to-tool.d.ts.map +1 -0
  29. package/dist/runtime/agent-to-tool.js +145 -0
  30. package/dist/runtime/agent-to-tool.js.map +1 -0
  31. package/dist/runtime/approvals.js +0 -0
  32. package/dist/runtime/approvals.js.map +1 -0
  33. package/dist/runtime/dispatch.js +108 -0
  34. package/dist/runtime/dispatch.js.map +1 -0
  35. package/dist/runtime/executor-wiring.js +128 -0
  36. package/dist/runtime/executor-wiring.js.map +1 -0
  37. package/dist/runtime/fallback-chain.js +139 -0
  38. package/dist/runtime/fallback-chain.js.map +1 -0
  39. package/dist/runtime/handoff.js +307 -0
  40. package/dist/runtime/handoff.js.map +1 -0
  41. package/dist/runtime/messages.d.ts +22 -0
  42. package/dist/runtime/messages.d.ts.map +1 -0
  43. package/dist/runtime/messages.js +204 -0
  44. package/dist/runtime/messages.js.map +1 -0
  45. package/dist/runtime/provider-events.js +117 -0
  46. package/dist/runtime/provider-events.js.map +1 -0
  47. package/dist/runtime/run-compaction.js +210 -0
  48. package/dist/runtime/run-compaction.js.map +1 -0
  49. package/dist/runtime/run-finish.js +48 -0
  50. package/dist/runtime/run-finish.js.map +1 -0
  51. package/dist/runtime/run-gates.js +336 -0
  52. package/dist/runtime/run-gates.js.map +1 -0
  53. package/dist/runtime/run-init.js +81 -0
  54. package/dist/runtime/run-init.js.map +1 -0
  55. package/dist/runtime/run-input.js +46 -0
  56. package/dist/runtime/run-input.js.map +1 -0
  57. package/dist/runtime/step-catalogue.js +173 -0
  58. package/dist/runtime/step-catalogue.js.map +1 -0
  59. package/dist/runtime/tool-call-walk.js +189 -0
  60. package/dist/runtime/tool-call-walk.js.map +1 -0
  61. package/dist/runtime/tool-wiring.js +159 -0
  62. package/dist/runtime/tool-wiring.js.map +1 -0
  63. package/dist/tooling/adapters.js +1 -1
  64. package/dist/tooling/dataflow.js +1 -1
  65. package/dist/tooling/policy.js +2 -0
  66. package/dist/tooling/policy.js.map +1 -1
  67. package/dist/types.d.ts +63 -13
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +20 -20
  70. package/src/errors/index.ts +320 -0
  71. package/src/evaluator-optimizer/index.ts +212 -0
  72. package/src/factory.ts +957 -0
  73. package/src/fallback/index.ts +108 -0
  74. package/src/fanout/index.ts +523 -0
  75. package/src/filters/index.ts +347 -0
  76. package/src/index.ts +180 -0
  77. package/src/internal/ids.ts +46 -0
  78. package/src/internal/usage-accumulator.ts +90 -0
  79. package/src/lateral-leak/causality-monitor.ts +221 -0
  80. package/src/lateral-leak/index.ts +35 -0
  81. package/src/lateral-leak/merge-guard.ts +151 -0
  82. package/src/lateral-leak/protocol-guard.ts +222 -0
  83. package/src/preferred-model/index.ts +210 -0
  84. package/src/progress/index.ts +238 -0
  85. package/src/run-state/index.ts +607 -0
  86. package/src/runtime/agent-surface.ts +218 -0
  87. package/src/runtime/agent-to-tool.ts +323 -0
  88. package/src/runtime/approvals.ts +0 -0
  89. package/src/runtime/dispatch.ts +183 -0
  90. package/src/runtime/executor-wiring.ts +331 -0
  91. package/src/runtime/fallback-chain.ts +250 -0
  92. package/src/runtime/handoff.ts +428 -0
  93. package/src/runtime/messages.ts +309 -0
  94. package/src/runtime/provider-events.ts +175 -0
  95. package/src/runtime/run-compaction.ts +288 -0
  96. package/src/runtime/run-finish.ts +93 -0
  97. package/src/runtime/run-gates.ts +419 -0
  98. package/src/runtime/run-init.ts +169 -0
  99. package/src/runtime/run-input.ts +102 -0
  100. package/src/runtime/step-catalogue.ts +338 -0
  101. package/src/runtime/tool-call-walk.ts +301 -0
  102. package/src/runtime/tool-wiring.ts +218 -0
  103. package/src/testing/replay-provider.ts +121 -0
  104. package/src/tooling/adapters.ts +403 -0
  105. package/src/tooling/catalogue.ts +36 -0
  106. package/src/tooling/dataflow.ts +171 -0
  107. package/src/tooling/plan.ts +123 -0
  108. package/src/tooling/policy.ts +67 -0
  109. package/src/tooling/registry-build.ts +191 -0
  110. package/src/types.ts +696 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Warm-up tool wiring for the agent runtime: registration of the
3
+ * built-in `tool_search` / `read_result` tools, code-mode meta-tool
4
+ * wiring, result-reader composition, and the `tool_search` promotion
5
+ * fold. Extracted verbatim from `factory.ts` (issue #23).
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { Tool } from '@graphorin/core';
11
+ import { createReadResultTool, createToolSearchTool } from '@graphorin/tools/built-in';
12
+ import {
13
+ type CodeExecuteBridge,
14
+ createCodeExecuteTool,
15
+ createCodeSearchTool,
16
+ type ProjectableTool,
17
+ projectToolApi,
18
+ } from '@graphorin/tools/code-mode';
19
+ import type { ToolExecutor } from '@graphorin/tools/executor';
20
+ import type { ToolRegistry } from '@graphorin/tools/registry';
21
+ import type { ResultReader } from '@graphorin/tools/result';
22
+ import { newId } from '../internal/ids.js';
23
+
24
+ /** The built-in deferred-discovery tool's stable name (WI-05 / P0-3). */
25
+ export const TOOL_SEARCH_NAME = 'tool_search';
26
+
27
+ /**
28
+ * Register the built-in `tool_search` into `registry` when - and only
29
+ * when - the registry holds at least one deferred tool
30
+ * (`defer_loading: true`). `tool_search` is itself eager (so it is
31
+ * always advertised while a deferred pool exists) and resolvable by the
32
+ * executor like any other tool, so a model can both *see* it in the
33
+ * per-step catalogue and *call* it.
34
+ *
35
+ * No-op when nothing defers (zero overhead - the tool never appears) or
36
+ * when a user tool already occupies the name (the user's tool wins; we
37
+ * never clobber it). Because deferral is decided at registration time
38
+ * (`normaliseTool`), the deferred set is fixed for the life of the
39
+ * registry - this runs once per registry, not per step.
40
+ */
41
+ export function registerToolSearch(
42
+ registry: ToolRegistry,
43
+ availability?: 'next-step' | 'next-run',
44
+ ): void {
45
+ if (registry.listDeferred().length === 0) return;
46
+ if (registry.get(TOOL_SEARCH_NAME) !== undefined) return;
47
+ registry.register(
48
+ createToolSearchTool({ registry, ...(availability !== undefined ? { availability } : {}) }),
49
+ {
50
+ kind: 'built-in',
51
+ subsystem: 'tool-discovery',
52
+ },
53
+ );
54
+ }
55
+
56
+ /** The built-in result-handle reader tool's stable name (WI-10 / P1-4). */
57
+ export const READ_RESULT_NAME = 'read_result';
58
+
59
+ /**
60
+ * Register the built-in `read_result` into `registry` when at least one
61
+ * registered tool opts into the `'spill-to-file'` truncation strategy
62
+ * (the sole producer of spill handles today) - or when `force` is set,
63
+ * which the agent passes when an operator wires external result readers
64
+ * (e.g. an MCP `resource_link` reader; WI-13). The tool is eager, so it
65
+ * is advertised alongside the producing tool and the model can fetch a
66
+ * handle back on demand instead of inlining the full blob. No-op when
67
+ * nothing produces handles (zero overhead) or when a user tool already
68
+ * occupies the name (the user's tool wins).
69
+ */
70
+ export function registerReadResult(
71
+ registry: ToolRegistry,
72
+ reader: ResultReader,
73
+ opts?: { readonly force?: boolean },
74
+ ): void {
75
+ if (
76
+ opts?.force !== true &&
77
+ !registry.list().some((entry) => entry.truncationStrategy === 'spill-to-file')
78
+ ) {
79
+ return;
80
+ }
81
+ if (registry.get(READ_RESULT_NAME) !== undefined) return;
82
+ registry.register(createReadResultTool({ reader }), {
83
+ kind: 'built-in',
84
+ subsystem: 'result-handle',
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Compose result readers into one that tries each in order, returning
90
+ * the first that resolves the handle (WI-13). The spill-file reader is
91
+ * placed first so `graphorin-spill:` handles resolve locally; operator
92
+ * readers (e.g. an MCP resource reader) resolve the rest. Each reader
93
+ * rejects handles it does not own, so resolution falls through cleanly.
94
+ */
95
+ export function composeResultReaders(readers: ReadonlyArray<ResultReader>): ResultReader {
96
+ return {
97
+ async read(uri, range) {
98
+ let lastError: unknown;
99
+ for (const r of readers) {
100
+ try {
101
+ return await r.read(uri, range);
102
+ } catch (err) {
103
+ lastError = err;
104
+ }
105
+ }
106
+ throw lastError instanceof Error
107
+ ? lastError
108
+ : new Error(`No result reader resolved handle ${JSON.stringify(uri)}.`);
109
+ },
110
+ };
111
+ }
112
+
113
+ /** The code-mode meta-tools' stable names (WI-11 / P1-2). */
114
+ export const CODE_EXECUTE_NAME = 'code_execute';
115
+ export const CODE_SEARCH_NAME = 'code_search';
116
+
117
+ /** Structural check: is this tool approval-gated (static or predicate form)? */
118
+ export const isApprovalGated = (t: { readonly needsApproval?: unknown }): boolean =>
119
+ t.needsApproval === true || typeof t.needsApproval === 'function';
120
+
121
+ /**
122
+ * Wire code-mode (P1-2) into `registry`: register the `code_search` /
123
+ * `code_execute` meta-tools and return them as the tools to advertise in
124
+ * place of the full catalogue. The model reaches every other tool through
125
+ * `code_execute`, whose in-script `tools.<name>(args)` calls route back
126
+ * through `quietExecutor.executeOne(...)` under the calling step's
127
+ * `runContext` - so per-tool ACL / sanitization / truncation still apply,
128
+ * exactly as in direct mode. `quietExecutor` carries no `streamingSink`,
129
+ * so the inner calls do not interleave `tool.execute.*` events into the
130
+ * outer stream.
131
+ *
132
+ * Excluded from the code API (`reservedNames`): the meta-tools, the
133
+ * discovery / handle built-ins, handoff tools (which stay first-class
134
+ * provider tools), and - supplied by the caller - any approval-gated
135
+ * tool, since code-mode has no durable-HITL path to suspend mid-script.
136
+ *
137
+ * Returns `[]` (registering nothing) when no real tool is exposable.
138
+ */
139
+ export function registerCodeMode(
140
+ registry: ToolRegistry,
141
+ quietExecutor: ToolExecutor,
142
+ reservedNames: ReadonlySet<string>,
143
+ getRunCapability?: () => 'read-only' | undefined,
144
+ ): ReadonlyArray<Tool<unknown, unknown, unknown>> {
145
+ if (registry.get(CODE_EXECUTE_NAME) !== undefined) return []; // already wired
146
+ const codeTools = registry
147
+ .list()
148
+ .filter((entry) => !reservedNames.has(entry.name) && !isApprovalGated(entry));
149
+ if (codeTools.length === 0) return [];
150
+
151
+ // TL-8: gated tools cannot suspend for HITL mid-script, so they are
152
+ // excluded from the code API - but VISIBLY: they appear in the
153
+ // catalogue/search with a call-directly marker, and a bridged call
154
+ // fails with the same hint instead of an opaque unknown-tool error.
155
+ const approvalGatedTools = registry
156
+ .list()
157
+ .filter((entry) => !reservedNames.has(entry.name) && isApprovalGated(entry))
158
+ .map((entry) => entry.name);
159
+ const approvalGatedSet = new Set(approvalGatedTools);
160
+
161
+ const allowedTools = [...codeTools.map((entry) => entry.name), ...approvalGatedTools];
162
+ const allowedSet = new Set(codeTools.map((entry) => entry.name));
163
+ const eagerProjectable = codeTools.filter(
164
+ (entry) => entry.__effectiveDeferLoading !== true,
165
+ ) as unknown as ReadonlyArray<ProjectableTool>;
166
+ const projection = projectToolApi(eagerProjectable);
167
+
168
+ const executeTool: CodeExecuteBridge = async (call, ctx) => {
169
+ if (approvalGatedSet.has(call.name)) {
170
+ throw new Error(
171
+ `${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.`,
172
+ );
173
+ }
174
+ const runCapability = getRunCapability?.();
175
+ const completed = await quietExecutor.executeOne({
176
+ call: { toolCallId: newId('codecall'), toolName: call.name, args: call.args },
177
+ runContext: ctx.runContext,
178
+ stepNumber: ctx.runContext.stepNumber,
179
+ // D2: in-script calls inherit the active run's capability.
180
+ ...(runCapability !== undefined ? { capability: runCapability } : {}),
181
+ });
182
+ const { outcome } = completed;
183
+ if ('kind' in outcome) throw new Error(`${call.name}: ${outcome.message}`);
184
+ return outcome.output;
185
+ };
186
+
187
+ const codeSearch = createCodeSearchTool({
188
+ projection,
189
+ approvalGatedTools,
190
+ searchDeferred: async (query, k) =>
191
+ (await registry.searchDeferred(query, k)).filter((match) => allowedSet.has(match.name)),
192
+ });
193
+ const codeExecute = createCodeExecuteTool({
194
+ projection,
195
+ allowedTools,
196
+ executeTool,
197
+ approvalGatedTools,
198
+ });
199
+ registry.register(codeSearch, { kind: 'built-in', subsystem: 'code-mode' });
200
+ registry.register(codeExecute, { kind: 'built-in', subsystem: 'code-mode' });
201
+ return [codeSearch, codeExecute] as ReadonlyArray<Tool<unknown, unknown, unknown>>;
202
+ }
203
+
204
+ /**
205
+ * Fold a completed `tool_search` result into the per-run promotion set:
206
+ * every matched tool name becomes advertised (and thus callable) on the
207
+ * next step. Tolerant of unexpected shapes (e.g. a user-shadowed
208
+ * `tool_search`) - only string `name`s inside a `matches` array promote.
209
+ */
210
+ export function recordToolSearchPromotions(output: unknown, promoted: Set<string>): void {
211
+ if (typeof output !== 'object' || output === null) return;
212
+ const matches = (output as { readonly matches?: unknown }).matches;
213
+ if (!Array.isArray(matches)) return;
214
+ for (const match of matches) {
215
+ const name = (match as { readonly name?: unknown } | null)?.name;
216
+ if (typeof name === 'string') promoted.add(name);
217
+ }
218
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * `createReplayProvider(state)` - a Provider that replays the model
3
+ * responses journaled on `RunState.steps[].providerResponse` (C3, opt-in
4
+ * via `AgentConfig.recordProviderResponses`). Re-running the same input
5
+ * against a replay provider reproduces the original run deterministically
6
+ * with zero live model calls - the mocked-completion harness that gives
7
+ * agent loops reproducible integration tests.
8
+ *
9
+ * The provider is strict by design: it throws when the state carries no
10
+ * journaled responses, and emits an error event when the replayed run
11
+ * asks for more steps than were recorded (a divergence, not a fixture
12
+ * gap - fail loudly instead of hallucinating a response).
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+
17
+ import type {
18
+ Provider,
19
+ ProviderEvent,
20
+ ProviderRequest,
21
+ ProviderResponse,
22
+ RunState,
23
+ RunStepProviderResponse,
24
+ } from '@graphorin/core';
25
+
26
+ /** Options for {@link createReplayProvider}. */
27
+ export interface ReplayProviderOptions {
28
+ /** Provider name for spans/logs. Default `'replay'`. */
29
+ readonly name?: string;
30
+ }
31
+
32
+ /**
33
+ * Build a Provider that serves the journaled step responses in order.
34
+ *
35
+ * @stable
36
+ */
37
+ export function createReplayProvider(
38
+ state: RunState,
39
+ options: ReplayProviderOptions = {},
40
+ ): Provider {
41
+ const recorded: RunStepProviderResponse[] = [];
42
+ for (const step of state.steps) {
43
+ if (step.providerResponse !== undefined) recorded.push(step.providerResponse);
44
+ }
45
+ if (recorded.length === 0) {
46
+ throw new TypeError(
47
+ 'createReplayProvider: RunState carries no journaled provider responses - ' +
48
+ 'run the original agent with recordProviderResponses: true.',
49
+ );
50
+ }
51
+ let cursor = 0;
52
+ const modelId = recorded[0]?.modelId ?? 'replay';
53
+
54
+ function nextRecorded(): RunStepProviderResponse | undefined {
55
+ const entry = recorded[cursor];
56
+ cursor += 1;
57
+ return entry;
58
+ }
59
+
60
+ return {
61
+ name: options.name ?? 'replay',
62
+ modelId,
63
+ capabilities: {
64
+ streaming: true,
65
+ toolCalling: true,
66
+ parallelToolCalls: true,
67
+ multimodal: false,
68
+ structuredOutput: true,
69
+ reasoning: false,
70
+ contextWindow: 1_000_000,
71
+ maxOutput: 1_000_000,
72
+ },
73
+ async *stream(_req: ProviderRequest): AsyncIterable<ProviderEvent> {
74
+ const entry = nextRecorded();
75
+ if (entry === undefined) {
76
+ yield {
77
+ type: 'error',
78
+ error: {
79
+ kind: 'unknown',
80
+ message: `replay exhausted after ${recorded.length} recorded step(s) - the replayed run diverged from the original`,
81
+ },
82
+ };
83
+ return;
84
+ }
85
+ yield { type: 'stream-start', metadata: { providerName: 'replay', modelId: entry.modelId } };
86
+ if (entry.text !== undefined && entry.text.length > 0) {
87
+ yield { type: 'text-delta', delta: entry.text };
88
+ }
89
+ for (const call of entry.toolCalls ?? []) {
90
+ yield { type: 'tool-call-start', toolCallId: call.toolCallId, toolName: call.toolName };
91
+ yield {
92
+ type: 'tool-call-input-delta',
93
+ toolCallId: call.toolCallId,
94
+ argsDelta: JSON.stringify(call.args),
95
+ };
96
+ yield { type: 'tool-call-end', toolCallId: call.toolCallId, finalArgs: call.args };
97
+ }
98
+ yield {
99
+ type: 'finish',
100
+ finishReason: (entry.toolCalls?.length ?? 0) > 0 ? 'tool-calls' : 'stop',
101
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
102
+ };
103
+ },
104
+ async generate(_req: ProviderRequest): Promise<ProviderResponse> {
105
+ const entry = nextRecorded();
106
+ if (entry === undefined) {
107
+ throw new Error(
108
+ `replay exhausted after ${recorded.length} recorded step(s) - the replayed run diverged from the original`,
109
+ );
110
+ }
111
+ return {
112
+ ...(entry.text !== undefined ? { text: entry.text } : {}),
113
+ ...(entry.toolCalls !== undefined && entry.toolCalls.length > 0
114
+ ? { toolCalls: entry.toolCalls.map((c) => ({ ...c })) }
115
+ : {}),
116
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
117
+ finishReason: (entry.toolCalls?.length ?? 0) > 0 ? 'tool-calls' : 'stop',
118
+ };
119
+ },
120
+ };
121
+ }