@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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent's public call surface, built over the factory's run loop:
|
|
3
|
+
* `stream` / `run` (the AsyncIterable wrapper and the drained-run
|
|
4
|
+
* variant, AG-9), `compact` (queued into the loop, CE-3 / AG-13),
|
|
5
|
+
* `fanOut` (AG-7 lifecycle events onto the external queue), and the
|
|
6
|
+
* structured progress-artifact IO (AG-20). Extracted verbatim from
|
|
7
|
+
* `factory.ts` (issue #23); the factory's mutable `activeRunState`
|
|
8
|
+
* scratch is read through a live getter so event payloads observe
|
|
9
|
+
* exactly what the former closures observed.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AgentEvent, AgentResult, RunState } from '@graphorin/core';
|
|
15
|
+
import type { Memory } from '@graphorin/memory';
|
|
16
|
+
import {
|
|
17
|
+
type FanOutResult,
|
|
18
|
+
type FanOutOptions as RunFanOutOptions,
|
|
19
|
+
runFanOut,
|
|
20
|
+
} from '../fanout/index.js';
|
|
21
|
+
import { newId } from '../internal/ids.js';
|
|
22
|
+
import type { ProgressIO, ProgressReadOptions, ProgressWriteOptions } from '../progress/index.js';
|
|
23
|
+
import type {
|
|
24
|
+
AgentCallOptions,
|
|
25
|
+
AgentConfig,
|
|
26
|
+
AgentFanOutOptions,
|
|
27
|
+
AgentInput,
|
|
28
|
+
AgentProgressIO,
|
|
29
|
+
CompactionApiResult,
|
|
30
|
+
CompactOptions,
|
|
31
|
+
} from '../types.js';
|
|
32
|
+
import { noopCompactionResult, type PendingManualCompact } from './run-compaction.js';
|
|
33
|
+
|
|
34
|
+
/** The factory's per-instance run entry point (concurrency-guarded). */
|
|
35
|
+
export type RunLoopFn<TDeps, TOutput> = (
|
|
36
|
+
input: AgentInput | RunState,
|
|
37
|
+
options: AgentCallOptions<TDeps>,
|
|
38
|
+
) => AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void>;
|
|
39
|
+
|
|
40
|
+
/** Build the `stream` / `run` methods over the factory's run loop. */
|
|
41
|
+
export function createRunMethods<TDeps, TOutput>(
|
|
42
|
+
runLoop: RunLoopFn<TDeps, TOutput>,
|
|
43
|
+
): {
|
|
44
|
+
readonly stream: (
|
|
45
|
+
input: AgentInput | RunState,
|
|
46
|
+
options?: AgentCallOptions<TDeps>,
|
|
47
|
+
) => AsyncIterable<AgentEvent<TOutput>>;
|
|
48
|
+
readonly run: (
|
|
49
|
+
input: AgentInput | RunState,
|
|
50
|
+
options?: AgentCallOptions<TDeps>,
|
|
51
|
+
) => Promise<AgentResult<TOutput>>;
|
|
52
|
+
} {
|
|
53
|
+
const stream = (
|
|
54
|
+
input: AgentInput | RunState,
|
|
55
|
+
options?: AgentCallOptions<TDeps>,
|
|
56
|
+
): AsyncIterable<AgentEvent<TOutput>> => {
|
|
57
|
+
const opts = options ?? {};
|
|
58
|
+
return {
|
|
59
|
+
[Symbol.asyncIterator]: () => {
|
|
60
|
+
const gen = runLoop(input, opts);
|
|
61
|
+
return {
|
|
62
|
+
async next(): Promise<IteratorResult<AgentEvent<TOutput>, void>> {
|
|
63
|
+
const r = await gen.next();
|
|
64
|
+
if (r.done === true) {
|
|
65
|
+
return { done: true, value: undefined };
|
|
66
|
+
}
|
|
67
|
+
return { done: false, value: r.value };
|
|
68
|
+
},
|
|
69
|
+
async return(): Promise<IteratorResult<AgentEvent<TOutput>, void>> {
|
|
70
|
+
await gen.return(undefined as unknown as AgentResult<TOutput>);
|
|
71
|
+
return { done: true, value: undefined };
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const run = async (
|
|
79
|
+
input: AgentInput | RunState,
|
|
80
|
+
options?: AgentCallOptions<TDeps>,
|
|
81
|
+
): Promise<AgentResult<TOutput>> => {
|
|
82
|
+
const opts = options ?? {};
|
|
83
|
+
const gen = runLoop(input, opts);
|
|
84
|
+
let next = await gen.next();
|
|
85
|
+
while (next.done !== true) {
|
|
86
|
+
next = await gen.next();
|
|
87
|
+
}
|
|
88
|
+
// Every terminal path of the run loop returns `finalize(...)`; an
|
|
89
|
+
// undefined return value would mean the generator was torn down
|
|
90
|
+
// externally - an invariant violation, not a run outcome (AG-9).
|
|
91
|
+
const result = next.value;
|
|
92
|
+
if (result === undefined) {
|
|
93
|
+
throw new Error('unreachable: agent run loop ended without a result');
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return { stream, run };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** What the out-of-loop surface methods need from the factory scope. */
|
|
102
|
+
export interface AgentSurfaceDeps<TDeps, TOutput> {
|
|
103
|
+
readonly config: Pick<AgentConfig<TDeps, TOutput>, 'sensitivity' | 'mergeGuard'>;
|
|
104
|
+
readonly memory: Memory | undefined;
|
|
105
|
+
readonly agentId: string;
|
|
106
|
+
readonly getActiveRunState: () => RunState | undefined;
|
|
107
|
+
readonly externalEventQueue: AgentEvent<TOutput>[];
|
|
108
|
+
readonly pendingManualCompacts: PendingManualCompact[];
|
|
109
|
+
readonly progressIO: ProgressIO;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Build the `compact` method (queued into the run loop, CE-3 / AG-13). */
|
|
113
|
+
export function createCompactMethod<TDeps, TOutput>(
|
|
114
|
+
deps: AgentSurfaceDeps<TDeps, TOutput>,
|
|
115
|
+
): (options?: CompactOptions) => Promise<CompactionApiResult> {
|
|
116
|
+
const { config, memory, getActiveRunState, pendingManualCompacts } = deps;
|
|
117
|
+
return async (options?: CompactOptions): Promise<CompactionApiResult> => {
|
|
118
|
+
// No memory wired - an explicit no-op (AG-13), intentionally
|
|
119
|
+
// forgiving so example apps that don't wire memory don't crash
|
|
120
|
+
// on `agent.compact()`.
|
|
121
|
+
if (memory === undefined) return noopCompactionResult('no-memory');
|
|
122
|
+
// Sensitivity gate (WI-09 step 2) applies to MANUAL compaction
|
|
123
|
+
// too: secret-tier history never ships to the summarizer.
|
|
124
|
+
if (config.sensitivity === 'secret') return noopCompactionResult('sensitivity-gated');
|
|
125
|
+
// Idle - there is no live buffer to splice (AG-13's explicit
|
|
126
|
+
// no-op marker, where the old surface silently reported zeros).
|
|
127
|
+
if (getActiveRunState() === undefined) return noopCompactionResult('no-active-run');
|
|
128
|
+
// CE-3/AG-13: the run loop owns the live buffer, so the splice
|
|
129
|
+
// happens there - enqueue and let `maybeAutoCompact` service the
|
|
130
|
+
// request at the next step boundary with the same prefix-pinned
|
|
131
|
+
// splice as auto-compaction. Don't await this from inside a tool
|
|
132
|
+
// handler: the loop can't reach the next step until the tool
|
|
133
|
+
// returns.
|
|
134
|
+
return await new Promise<CompactionApiResult>((resolve, reject) => {
|
|
135
|
+
pendingManualCompacts.push({ options, resolve, reject });
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Build the `fanOut` method (AG-7: lifecycle events reach the stream). */
|
|
141
|
+
export function createFanOutMethod<TDeps, TOutput>(
|
|
142
|
+
deps: AgentSurfaceDeps<TDeps, TOutput>,
|
|
143
|
+
): <TFanOutOutput = unknown>(
|
|
144
|
+
options: AgentFanOutOptions<TFanOutOutput>,
|
|
145
|
+
) => Promise<FanOutResult<TFanOutOutput>> {
|
|
146
|
+
const { config, agentId, getActiveRunState, externalEventQueue } = deps;
|
|
147
|
+
return async <TFanOutOutput = unknown>(
|
|
148
|
+
options: AgentFanOutOptions<TFanOutOutput>,
|
|
149
|
+
): Promise<FanOutResult<TFanOutOutput>> => {
|
|
150
|
+
const activeRunState = getActiveRunState();
|
|
151
|
+
const runId = activeRunState?.id ?? `run_${newId()}`;
|
|
152
|
+
const sessionId = activeRunState?.sessionId ?? `session_${newId()}`;
|
|
153
|
+
const fanOutOptions: RunFanOutOptions<TFanOutOutput> = {
|
|
154
|
+
children: options.children,
|
|
155
|
+
...(options.maxConcurrentChildren !== undefined
|
|
156
|
+
? { maxConcurrentChildren: options.maxConcurrentChildren }
|
|
157
|
+
: {}),
|
|
158
|
+
...(options.perBudget !== undefined ? { perBudget: options.perBudget } : {}),
|
|
159
|
+
...(options.mergeStrategy !== undefined ? { mergeStrategy: options.mergeStrategy } : {}),
|
|
160
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
161
|
+
// AG-7: fanout lifecycle events reach the agent stream - queued
|
|
162
|
+
// on the external-event queue and drained into the active (or
|
|
163
|
+
// next consumed) run, like steer/follow-up/progress events.
|
|
164
|
+
emit: (event) => {
|
|
165
|
+
externalEventQueue.push(event as AgentEvent<TOutput>);
|
|
166
|
+
},
|
|
167
|
+
// AG-7: the configured sideways-injection merge guard finally
|
|
168
|
+
// applies to the judge-merge path.
|
|
169
|
+
...(config.mergeGuard !== undefined ? { mergeGuard: config.mergeGuard } : {}),
|
|
170
|
+
runId,
|
|
171
|
+
sessionId,
|
|
172
|
+
agentId,
|
|
173
|
+
};
|
|
174
|
+
return runFanOut<TFanOutOutput>(fanOutOptions);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Build the structured progress-artifact IO surface (AG-20). */
|
|
179
|
+
export function createProgressSurface<TDeps, TOutput>(
|
|
180
|
+
deps: AgentSurfaceDeps<TDeps, TOutput>,
|
|
181
|
+
): AgentProgressIO {
|
|
182
|
+
const { agentId, getActiveRunState, externalEventQueue, progressIO } = deps;
|
|
183
|
+
// Stable fallback id so out-of-run `progress.write` → `progress.read`
|
|
184
|
+
// pairs resolve to the same artifact directory (a fresh id per call
|
|
185
|
+
// could never find what it just wrote).
|
|
186
|
+
const progressFallbackRunId = `run_${newId()}`;
|
|
187
|
+
const progress: AgentProgressIO = {
|
|
188
|
+
write: async (content: string, opts?: ProgressWriteOptions) => {
|
|
189
|
+
const runId = getActiveRunState()?.id ?? progressFallbackRunId;
|
|
190
|
+
const ref = await progressIO.write(runId, content, opts);
|
|
191
|
+
// AG-20: surface the documented `agent.progress.written` event -
|
|
192
|
+
// queued here and drained into the active (or next consumed) stream.
|
|
193
|
+
externalEventQueue.push({
|
|
194
|
+
type: 'agent.progress.written',
|
|
195
|
+
runId,
|
|
196
|
+
sessionId: getActiveRunState()?.sessionId ?? '',
|
|
197
|
+
agentId,
|
|
198
|
+
ref,
|
|
199
|
+
} as AgentEvent<TOutput>);
|
|
200
|
+
return ref;
|
|
201
|
+
},
|
|
202
|
+
read: async (opts?: ProgressReadOptions) => {
|
|
203
|
+
const queriedRunId = opts?.runId ?? getActiveRunState()?.id ?? progressFallbackRunId;
|
|
204
|
+
const refs = await progressIO.read(queriedRunId, opts);
|
|
205
|
+
externalEventQueue.push({
|
|
206
|
+
type: 'agent.progress.read',
|
|
207
|
+
runId: getActiveRunState()?.id ?? queriedRunId,
|
|
208
|
+
sessionId: getActiveRunState()?.sessionId ?? '',
|
|
209
|
+
agentId,
|
|
210
|
+
refs,
|
|
211
|
+
queriedRunId,
|
|
212
|
+
queriedRole: opts?.role,
|
|
213
|
+
} as AgentEvent<TOutput>);
|
|
214
|
+
return refs;
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
return progress;
|
|
218
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent.toTool()` - expose an agent as a callable sub-agent tool
|
|
3
|
+
* (AG-17 / D2): least-authority seeding from the parent history, the
|
|
4
|
+
* contextFold distillation of a completed child run, and the coarse
|
|
5
|
+
* child-taint propagation that re-arms the parent's data-flow ledger.
|
|
6
|
+
* Extracted verbatim from `factory.ts` (issue #23); the former factory
|
|
7
|
+
* closures now hang off {@link createToTool}'s deps.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
AgentEvent,
|
|
14
|
+
AgentResult,
|
|
15
|
+
Message,
|
|
16
|
+
RunState,
|
|
17
|
+
Tool,
|
|
18
|
+
ToolExecutionContext,
|
|
19
|
+
UsageAccumulator,
|
|
20
|
+
} from '@graphorin/core';
|
|
21
|
+
import { toolReturn } from '@graphorin/core';
|
|
22
|
+
import type { AgentCallOptions, AgentConfig, AgentInput, AgentToToolOptions } from '../types.js';
|
|
23
|
+
import { foldChildRunUsage } from './messages.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* W-033: fold the child's usage into the parent run through the tool
|
|
27
|
+
* context. `RunContext.state` is typed as a readonly projection for
|
|
28
|
+
* user tools (W-047); this runtime-internal seam is the sanctioned
|
|
29
|
+
* mutation point - the runtime owns the state lifecycle.
|
|
30
|
+
*/
|
|
31
|
+
function foldIntoParent(
|
|
32
|
+
ctx: ToolExecutionContext<unknown> | undefined,
|
|
33
|
+
childState: RunState,
|
|
34
|
+
childName: string,
|
|
35
|
+
): void {
|
|
36
|
+
if (ctx === undefined) return;
|
|
37
|
+
const parentState = ctx.runContext.state as unknown as RunState | undefined;
|
|
38
|
+
const usageAcc = ctx.runContext.usage as UsageAccumulator | undefined;
|
|
39
|
+
if (parentState !== undefined && typeof parentState.usage?.totalTokens === 'number') {
|
|
40
|
+
foldChildRunUsage(parentState, usageAcc, childState, childName);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// A foreign harness mounting this tool outside the graphorin loop may
|
|
44
|
+
// hand a structurally-minimal context - fold into the accumulator only.
|
|
45
|
+
const byModel = childState.usageByModel;
|
|
46
|
+
if (byModel !== undefined && Object.keys(byModel).length > 0) {
|
|
47
|
+
for (const [modelId, usage] of Object.entries(byModel)) usageAcc?.add(modelId, usage);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const aggregate = childState.usage;
|
|
51
|
+
if (aggregate.promptTokens > 0 || aggregate.completionTokens > 0 || aggregate.totalTokens > 0) {
|
|
52
|
+
usageAcc?.add(`sub-agent:${childName}`, aggregate);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What {@link createToTool} needs from the agent factory scope. */
|
|
57
|
+
export interface ToToolDeps<TDeps, TOutput> {
|
|
58
|
+
readonly config: AgentConfig<TDeps, TOutput>;
|
|
59
|
+
readonly run: (
|
|
60
|
+
input: AgentInput | RunState,
|
|
61
|
+
options?: AgentCallOptions<TDeps>,
|
|
62
|
+
) => Promise<AgentResult<TOutput>>;
|
|
63
|
+
readonly stream: (
|
|
64
|
+
input: AgentInput | RunState,
|
|
65
|
+
options?: AgentCallOptions<TDeps>,
|
|
66
|
+
) => AsyncIterable<AgentEvent<TOutput>>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* W-001: well-known marker on every `toTool()` tool object. The
|
|
71
|
+
* graphorin tool-call walk detects it and executes the sub-agent
|
|
72
|
+
* INLINE (through the same seam as a handoff) so a child suspending on
|
|
73
|
+
* `awaiting_approval` parks on the parent instead of throwing from the
|
|
74
|
+
* executor; the resume router resolves parked toTool sub-runs through
|
|
75
|
+
* the same refs. `Symbol.for` so duplicate package copies agree.
|
|
76
|
+
* Foreign harnesses mounting the tool outside the graphorin loop keep
|
|
77
|
+
* the plain `execute()` behavior (a suspended child throws there).
|
|
78
|
+
*/
|
|
79
|
+
export const SUBAGENT_TOOL: unique symbol = Symbol.for('graphorin.SubAgentTool');
|
|
80
|
+
|
|
81
|
+
/** Child taint flags carried across the toTool fold (D2). */
|
|
82
|
+
export interface SubAgentFoldTaint {
|
|
83
|
+
readonly untrusted?: boolean;
|
|
84
|
+
readonly sensitive?: boolean;
|
|
85
|
+
readonly sourceKind?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The live references {@link SUBAGENT_TOOL} carries (W-001). Typed
|
|
90
|
+
* loosely on purpose: the walk is generic over foreign TDeps/TOutput
|
|
91
|
+
* and only ever passes through values it received from the same
|
|
92
|
+
* factory instance.
|
|
93
|
+
*/
|
|
94
|
+
export interface SubAgentToolRefs {
|
|
95
|
+
readonly agentName: string;
|
|
96
|
+
readonly run: (
|
|
97
|
+
input: unknown,
|
|
98
|
+
options?: Record<string, unknown>,
|
|
99
|
+
) => Promise<AgentResult<unknown>>;
|
|
100
|
+
readonly stream: (
|
|
101
|
+
input: unknown,
|
|
102
|
+
options?: Record<string, unknown>,
|
|
103
|
+
) => AsyncIterable<AgentEvent<unknown>>;
|
|
104
|
+
/** D2 capability restriction from `AgentToToolOptions.capability`. */
|
|
105
|
+
readonly capability?: 'read-only';
|
|
106
|
+
/** W-036: forwarding policy from `AgentToToolOptions.forwardEvents`. */
|
|
107
|
+
readonly forwardEvents?: 'none' | 'lifecycle' | 'all';
|
|
108
|
+
/** Reproduce `execute()`'s seed semantics (inputFilter + input string). */
|
|
109
|
+
readonly buildSeed: (
|
|
110
|
+
input: { readonly input: string },
|
|
111
|
+
parentMessages: ReadonlyArray<Message>,
|
|
112
|
+
) => AgentInput;
|
|
113
|
+
/** Reproduce `execute()`'s output shaping (exposeTurns/contextFold/taint). */
|
|
114
|
+
readonly shapeCompleted: (
|
|
115
|
+
result: AgentResult<unknown>,
|
|
116
|
+
turns: ReadonlyArray<string>,
|
|
117
|
+
) => { readonly output: unknown; readonly taint?: SubAgentFoldTaint };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Read the {@link SUBAGENT_TOOL} refs off a (possibly wrapped) tool. */
|
|
121
|
+
export function getSubAgentToolRefs(tool: unknown): SubAgentToolRefs | undefined {
|
|
122
|
+
if (typeof tool !== 'object' || tool === null) return undefined;
|
|
123
|
+
const refs = (tool as Record<PropertyKey, unknown>)[SUBAGENT_TOOL];
|
|
124
|
+
return refs === undefined ? undefined : (refs as SubAgentToolRefs);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Build the agent's `toTool` method over the factory's run surface. */
|
|
128
|
+
export function createToTool<TDeps, TOutput>(
|
|
129
|
+
deps: ToToolDeps<TDeps, TOutput>,
|
|
130
|
+
): (options?: AgentToToolOptions) => Tool<{ readonly input: string }, TOutput, TDeps> {
|
|
131
|
+
const { config, run, stream } = deps;
|
|
132
|
+
|
|
133
|
+
// D2: distil a completed child run into a compact, bounded outcome the
|
|
134
|
+
// parent folds into its context instead of the raw transcript/output.
|
|
135
|
+
const foldRunOutcome = (result: AgentResult<TOutput>, maxChars: number): string => {
|
|
136
|
+
const steps = result.state.steps;
|
|
137
|
+
const toolNames = [
|
|
138
|
+
...new Set(steps.flatMap((step) => step.toolCalls.map((c) => c.call.toolName))),
|
|
139
|
+
];
|
|
140
|
+
const header =
|
|
141
|
+
`[sub-agent '${config.name}' outcome] status=${result.status}; ` +
|
|
142
|
+
`steps=${steps.length}; toolCalls=${steps.reduce((n, st) => n + st.toolCalls.length, 0)}` +
|
|
143
|
+
(toolNames.length > 0 ? `; tools=${toolNames.join(', ')}` : '');
|
|
144
|
+
const text = typeof result.output === 'string' ? result.output : JSON.stringify(result.output);
|
|
145
|
+
const body =
|
|
146
|
+
text.length > maxChars
|
|
147
|
+
? `${text.slice(0, maxChars)}\n[... ${text.length - maxChars} chars truncated by contextFold]`
|
|
148
|
+
: text;
|
|
149
|
+
return `${header}\n${body}`;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// D2: carry the child's coarse taint flags across the fold so the
|
|
153
|
+
// parent's data-flow ledger re-arms (widen-only; a no-op when the
|
|
154
|
+
// parent has no dataFlowPolicy).
|
|
155
|
+
const taintFromChildState = (
|
|
156
|
+
state: RunState,
|
|
157
|
+
): { untrusted?: boolean; sensitive?: boolean; sourceKind?: string } | undefined => {
|
|
158
|
+
const summary = state.taintSummary;
|
|
159
|
+
if (summary === undefined || (!summary.untrustedSeen && !summary.sensitiveSeen)) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
...(summary.untrustedSeen ? { untrusted: true } : {}),
|
|
164
|
+
...(summary.sensitiveSeen ? { sensitive: true } : {}),
|
|
165
|
+
sourceKind: 'sub-agent',
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const toTool = (
|
|
170
|
+
options: AgentToToolOptions = {},
|
|
171
|
+
): Tool<{ readonly input: string }, TOutput, TDeps> => {
|
|
172
|
+
const exposeTurns = options.exposeTurns ?? 'final';
|
|
173
|
+
const foldMaxChars =
|
|
174
|
+
options.contextFold === undefined || options.contextFold === false
|
|
175
|
+
? null
|
|
176
|
+
: typeof options.contextFold === 'object'
|
|
177
|
+
? (options.contextFold.maxChars ?? 2000)
|
|
178
|
+
: 2000;
|
|
179
|
+
const propagateTaint = options.propagateTaint !== false;
|
|
180
|
+
const toolName = options.name ?? `subagent_${config.name}`;
|
|
181
|
+
const description = options.description ?? `Invoke sub-agent '${config.name}'.`;
|
|
182
|
+
|
|
183
|
+
// Shared by `execute()` and the W-001 inline walk: seed semantics
|
|
184
|
+
// (least authority - input string only unless an inputFilter opts
|
|
185
|
+
// parent history in) and completed-result shaping.
|
|
186
|
+
const buildSeed = (
|
|
187
|
+
input: { readonly input: string },
|
|
188
|
+
parentMessages: ReadonlyArray<Message> | undefined,
|
|
189
|
+
): AgentInput =>
|
|
190
|
+
options.inputFilter !== undefined && parentMessages !== undefined
|
|
191
|
+
? ([
|
|
192
|
+
...options.inputFilter(parentMessages),
|
|
193
|
+
{ role: 'user' as const, content: input.input },
|
|
194
|
+
] as Message[])
|
|
195
|
+
: input.input;
|
|
196
|
+
const shapeCompleted = (
|
|
197
|
+
result: AgentResult<TOutput>,
|
|
198
|
+
turns: ReadonlyArray<string>,
|
|
199
|
+
): { readonly output: unknown; readonly taint?: SubAgentFoldTaint } => {
|
|
200
|
+
const taint = propagateTaint ? taintFromChildState(result.state) : undefined;
|
|
201
|
+
const output =
|
|
202
|
+
exposeTurns === 'all'
|
|
203
|
+
? foldMaxChars !== null
|
|
204
|
+
? foldRunOutcome(result, foldMaxChars)
|
|
205
|
+
: turns.join('\n\n')
|
|
206
|
+
: exposeTurns === 'none'
|
|
207
|
+
? ''
|
|
208
|
+
: foldMaxChars !== null
|
|
209
|
+
? foldRunOutcome(result, foldMaxChars)
|
|
210
|
+
: result.output;
|
|
211
|
+
return { output, ...(taint !== undefined ? { taint } : {}) };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const schema = {
|
|
215
|
+
parse: (v: unknown): { readonly input: string } => v as { readonly input: string },
|
|
216
|
+
safeParse: (v: unknown) => ({
|
|
217
|
+
success: true as const,
|
|
218
|
+
data: v as { readonly input: string },
|
|
219
|
+
}),
|
|
220
|
+
toJSON: (): Record<string, unknown> => ({
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: { input: { type: 'string' } },
|
|
223
|
+
required: ['input'],
|
|
224
|
+
}),
|
|
225
|
+
};
|
|
226
|
+
const tool: Tool<{ readonly input: string }, TOutput, TDeps> = {
|
|
227
|
+
name: toolName,
|
|
228
|
+
description,
|
|
229
|
+
inputSchema: schema as unknown as Tool<
|
|
230
|
+
{ readonly input: string },
|
|
231
|
+
TOutput,
|
|
232
|
+
TDeps
|
|
233
|
+
>['inputSchema'],
|
|
234
|
+
sideEffectClass: 'side-effecting',
|
|
235
|
+
async execute(input, ctx) {
|
|
236
|
+
// AG-17: the parent ToolExecutionContext propagates - the
|
|
237
|
+
// parent's abort stops the sub-agent, deps/sessionId flow
|
|
238
|
+
// through, and the optional `inputFilter` shapes a seed from
|
|
239
|
+
// the parent history. Least authority by default: without a
|
|
240
|
+
// filter the sub-agent sees ONLY the input string, never the
|
|
241
|
+
// parent conversation.
|
|
242
|
+
const callOpts: AgentCallOptions<TDeps> = {
|
|
243
|
+
...(ctx?.signal !== undefined ? { signal: ctx.signal } : {}),
|
|
244
|
+
...(ctx?.runContext.deps !== undefined ? { deps: ctx.runContext.deps as TDeps } : {}),
|
|
245
|
+
...(ctx?.runContext.sessionId !== undefined
|
|
246
|
+
? { sessionId: ctx.runContext.sessionId }
|
|
247
|
+
: {}),
|
|
248
|
+
// D2: run the child under a restricted capability (read-only
|
|
249
|
+
// workers in an orchestrator-worker fan-out).
|
|
250
|
+
...(options.capability !== undefined ? { capability: options.capability } : {}),
|
|
251
|
+
// W-036: the child's run span parents under the calling tool's
|
|
252
|
+
// span - one trace tree even through the executor path.
|
|
253
|
+
...(ctx?.runContext.span !== undefined ? { parentSpan: ctx.runContext.span } : {}),
|
|
254
|
+
};
|
|
255
|
+
const seed = buildSeed(input, ctx?.runContext.messages);
|
|
256
|
+
if (exposeTurns === 'all') {
|
|
257
|
+
// Replay the streamed text completions as the result so
|
|
258
|
+
// the parent agent sees every turn the sub-agent
|
|
259
|
+
// produced. `exposeTurns: 'final'` (default) and
|
|
260
|
+
// `'none'` skip the per-turn assembly.
|
|
261
|
+
const turns: string[] = [];
|
|
262
|
+
let endResult: AgentResult<TOutput> | undefined;
|
|
263
|
+
for await (const ev of stream(seed, callOpts)) {
|
|
264
|
+
if (ev.type === 'text.complete') turns.push(ev.text);
|
|
265
|
+
else if (ev.type === 'agent.end') endResult = ev.result;
|
|
266
|
+
}
|
|
267
|
+
// W-033: fold BEFORE the non-completed throw - tokens were
|
|
268
|
+
// spent whether or not the child completed.
|
|
269
|
+
if (endResult !== undefined) {
|
|
270
|
+
foldIntoParent(
|
|
271
|
+
ctx as ToolExecutionContext<unknown> | undefined,
|
|
272
|
+
endResult.state,
|
|
273
|
+
config.name,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (endResult !== undefined && endResult.status !== 'completed') {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`sub-agent '${config.name}' ${endResult.status}${
|
|
279
|
+
endResult.error !== undefined ? `: ${endResult.error.message}` : ''
|
|
280
|
+
}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (endResult === undefined) return turns.join('\n\n') as unknown as TOutput;
|
|
284
|
+
const shapedAll = shapeCompleted(endResult, turns);
|
|
285
|
+
return (shapedAll.taint !== undefined
|
|
286
|
+
? toolReturn({ output: shapedAll.output, taint: shapedAll.taint })
|
|
287
|
+
: shapedAll.output) as unknown as TOutput;
|
|
288
|
+
}
|
|
289
|
+
const result = await run(seed, callOpts);
|
|
290
|
+
// W-033: fold BEFORE the non-completed throw (see 'all' branch).
|
|
291
|
+
foldIntoParent(ctx as ToolExecutionContext<unknown> | undefined, result.state, config.name);
|
|
292
|
+
// AG-17/AG-22 class: a non-completed sub-run is a TOOL ERROR,
|
|
293
|
+
// never an empty-string success.
|
|
294
|
+
if (result.status !== 'completed') {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`sub-agent '${config.name}' ${result.status}${
|
|
297
|
+
result.error !== undefined ? `: ${result.error.message}` : ''
|
|
298
|
+
}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const shaped = shapeCompleted(result, []);
|
|
302
|
+
return (shaped.taint !== undefined
|
|
303
|
+
? toolReturn({ output: shaped.output, taint: shaped.taint })
|
|
304
|
+
: shaped.output) as unknown as TOutput;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
// W-001: attach the inline-walk refs (see SUBAGENT_TOOL). Symbol
|
|
308
|
+
// properties survive the registry's `{...tool}` normalization spread.
|
|
309
|
+
const refs: SubAgentToolRefs = {
|
|
310
|
+
agentName: config.name,
|
|
311
|
+
run: run as unknown as SubAgentToolRefs['run'],
|
|
312
|
+
stream: stream as unknown as SubAgentToolRefs['stream'],
|
|
313
|
+
...(options.capability !== undefined ? { capability: options.capability } : {}),
|
|
314
|
+
...(options.forwardEvents !== undefined ? { forwardEvents: options.forwardEvents } : {}),
|
|
315
|
+
buildSeed,
|
|
316
|
+
shapeCompleted: shapeCompleted as unknown as SubAgentToolRefs['shapeCompleted'],
|
|
317
|
+
};
|
|
318
|
+
(tool as unknown as Record<PropertyKey, unknown>)[SUBAGENT_TOOL] = refs;
|
|
319
|
+
return tool;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return toTool;
|
|
323
|
+
}
|
|
Binary file
|