@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,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