@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,309 @@
1
+ /**
2
+ * Message-buffer shaping and step-accounting helpers for the agent
3
+ * runtime: reasoning-retention resolution, assistant-message assembly,
4
+ * the compaction-stable system-prefix scan, per-step request assembly,
5
+ * tool-error rendering for the model, and usage arithmetic. Extracted
6
+ * verbatim from `factory.ts` (issue #23).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import type {
12
+ AssistantMessage,
13
+ Message,
14
+ MessageContent,
15
+ Provider,
16
+ ReasoningContent,
17
+ ReasoningRetention,
18
+ RunState,
19
+ ToolCall,
20
+ ToolError,
21
+ Usage,
22
+ UsageAccumulator,
23
+ } from '@graphorin/core';
24
+ import { COMPACTION_SUMMARY_MARKER } from '@graphorin/memory';
25
+ import { addModelUsage } from '../run-state/index.js';
26
+ import { renderPlanRecitation } from '../tooling/plan.js';
27
+
28
+ /** Most-recent user-role text in `messages` (for context-engine auto-recall). */
29
+ export function lastUserText(messages: ReadonlyArray<Message>): string | undefined {
30
+ for (let i = messages.length - 1; i >= 0; i--) {
31
+ const m = messages[i];
32
+ if (m?.role !== 'user') continue;
33
+ if (typeof m.content === 'string') return m.content;
34
+ const text = m.content
35
+ .filter((p): p is { readonly type: 'text'; readonly text: string } => p.type === 'text')
36
+ .map((p) => p.text)
37
+ .join(' ');
38
+ return text.length > 0 ? text : undefined;
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ /**
44
+ * Resolve the effective {@link ReasoningRetention} for a step. The
45
+ * agent-level setting wins over the provider-level default; when
46
+ * neither is supplied, the provider's `reasoningContract`
47
+ * capability drives the default per RB-42 / suggested DEC-158.
48
+ */
49
+ export function effectiveReasoningRetention(
50
+ agentOverride: ReasoningRetention | undefined,
51
+ provider: Provider,
52
+ ): ReasoningRetention {
53
+ if (agentOverride !== undefined) return agentOverride;
54
+ const contract = provider.capabilities.reasoningContract;
55
+ switch (contract) {
56
+ case 'round-trip-required':
57
+ return 'pass-through-claude';
58
+ case 'optional':
59
+ return 'pass-through-all';
60
+ case 'hidden':
61
+ return 'strip';
62
+ default:
63
+ return 'strip';
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Build the assistant message that the runtime appends to the
69
+ * message buffer after a successful provider call. When the
70
+ * effective {@link ReasoningRetention} is not `'strip'`, the
71
+ * assembled `reasoning` content parts ride along on `content` so
72
+ * the next provider call honours the wire-correct round-trip
73
+ * contract per RB-42.
74
+ */
75
+ export function buildAssistantMessage(
76
+ text: string,
77
+ reasoningParts: ReadonlyArray<ReasoningContent>,
78
+ toolCalls: ReadonlyArray<ToolCall>,
79
+ agentId: string,
80
+ retention: ReasoningRetention,
81
+ ): AssistantMessage {
82
+ const preserveReasoning = retention !== 'strip' && reasoningParts.length > 0;
83
+ if (preserveReasoning) {
84
+ const parts: MessageContent[] = [...reasoningParts];
85
+ if (text.length > 0) parts.push({ type: 'text', text });
86
+ return {
87
+ role: 'assistant',
88
+ content: parts,
89
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
90
+ agentId,
91
+ };
92
+ }
93
+ return {
94
+ role: 'assistant',
95
+ content: text,
96
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
97
+ agentId,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Strip every {@link ReasoningContent} part from each message in
103
+ * the supplied list. Used at the swap point when `prepareStep`
104
+ * downgrades the provider's `reasoningContract` mid-run.
105
+ */
106
+ export function stripReasoningFromMessages(messages: Message[]): { stripped: number } {
107
+ let stripped = 0;
108
+ for (let i = 0; i < messages.length; i++) {
109
+ const msg = messages[i];
110
+ if (msg === undefined) continue;
111
+ if (msg.role === 'system' || msg.role === 'tool') continue;
112
+ if (typeof msg.content === 'string') continue;
113
+ const filtered = msg.content.filter((p) => p.type !== 'reasoning');
114
+ if (filtered.length === msg.content.length) continue;
115
+ stripped += msg.content.length - filtered.length;
116
+ if (msg.role === 'assistant') {
117
+ messages[i] = { ...msg, content: filtered };
118
+ } else {
119
+ messages[i] = { ...msg, content: filtered };
120
+ }
121
+ }
122
+ return { stripped };
123
+ }
124
+
125
+ /**
126
+ * Count the leading contiguous run of `system` messages in the initial
127
+ * buffer - the trusted, KV-cache-stable instruction prefix. Captured
128
+ * once at run start (WI-09 / P1-1): auto-compaction summarises only the
129
+ * messages after this prefix, so the prefix stays byte-identical across
130
+ * every step (the provider's cache breakpoint is real) and a long run
131
+ * never re-pays for the system prompt.
132
+ *
133
+ * The length is fixed for the run rather than re-derived per compaction
134
+ * on purpose: each compaction inserts its summary as a `system` message
135
+ * right after the prefix, so re-scanning the leading run would absorb
136
+ * that summary into the prefix and shield it from the next compaction -
137
+ * summaries would stack unbounded. Pinning the original length keeps
138
+ * each prior summary inside the compactable body, where the next pass
139
+ * folds it into a fresh summary-of-summary.
140
+ */
141
+ /**
142
+ * Marker prefix stamped on every compaction summary. context-engine-05:
143
+ * the prefix scan must stop at it - after a compact-then-suspend cycle
144
+ * the summary is a SYSTEM message sitting right after the true prefix,
145
+ * and counting it in would pin it (and every later summary) outside the
146
+ * compactable window forever, growing the uncompactable prefix by one
147
+ * summary per cycle. W-056: the constant is canonical in
148
+ * `@graphorin/memory` (next to the summary template that stamps it);
149
+ * re-exported here for the runtime's internal imports.
150
+ */
151
+ export { COMPACTION_SUMMARY_MARKER };
152
+
153
+ export function countLeadingSystemMessages(messages: ReadonlyArray<Message>): number {
154
+ let i = 0;
155
+ while (i < messages.length) {
156
+ const msg = messages[i];
157
+ if (msg?.role !== 'system') break;
158
+ if (typeof msg.content === 'string' && msg.content.startsWith(COMPACTION_SUMMARY_MARKER)) {
159
+ break;
160
+ }
161
+ i += 1;
162
+ }
163
+ return i;
164
+ }
165
+
166
+ /**
167
+ * Immutable usage sum. Optional token fields (reasoning + prompt-cache
168
+ * legs, core-provider-02) appear in the result only when at least one
169
+ * side carries them, so pre-cache serialized shapes stay byte-identical.
170
+ */
171
+ /**
172
+ * D6: assemble the per-step request messages. Trailing, request-only
173
+ * additions ride the LAST prompt-cache anchor and never touch the shared
174
+ * `messages` buffer or the persisted RunState: the structured-output
175
+ * instruction (AG-3) and the attention-recitation plan block are both
176
+ * appended here, in that order, so the stable prompt prefix is unchanged
177
+ * and only the small trailing tail is re-sent each step.
178
+ */
179
+ export function buildStepMessages(
180
+ messages: ReadonlyArray<Message>,
181
+ structuredInstruction: string | undefined,
182
+ todos: ReadonlyArray<import('@graphorin/core').TodoItem> | undefined,
183
+ ): Message[] {
184
+ const out: Message[] = [...messages];
185
+ if (structuredInstruction !== undefined) {
186
+ out.push({ role: 'system', content: structuredInstruction });
187
+ }
188
+ const recitation = renderPlanRecitation(todos);
189
+ if (recitation !== null) {
190
+ out.push({ role: 'system', content: recitation });
191
+ }
192
+ return out;
193
+ }
194
+
195
+ export function addUsage(a: Usage, b: Usage): Usage {
196
+ const optional = (x: number | undefined, y: number | undefined): number | undefined =>
197
+ x === undefined && y === undefined ? undefined : (x ?? 0) + (y ?? 0);
198
+ const reasoningTokens = optional(a.reasoningTokens, b.reasoningTokens);
199
+ const cachedReadTokens = optional(a.cachedReadTokens, b.cachedReadTokens);
200
+ const cacheWriteTokens = optional(a.cacheWriteTokens, b.cacheWriteTokens);
201
+ return {
202
+ promptTokens: a.promptTokens + b.promptTokens,
203
+ completionTokens: a.completionTokens + b.completionTokens,
204
+ totalTokens: a.totalTokens + b.totalTokens,
205
+ ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
206
+ ...(cachedReadTokens !== undefined ? { cachedReadTokens } : {}),
207
+ ...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),
208
+ };
209
+ }
210
+
211
+ /**
212
+ * C3: render a ToolError for the model. The first line keeps the
213
+ * long-standing `Error: <message>` shape; a bracketed second line carries
214
+ * the typed kind + the recovery envelope, which is what actually changes
215
+ * model behaviour after a failure (retry vs. fix args vs. give up).
216
+ */
217
+ export function renderToolErrorMessage(error: ToolError): string {
218
+ const parts: string[] = [`kind: ${error.kind}`];
219
+ if (error.recoverable !== undefined) {
220
+ parts.push(error.recoverable ? 'recoverable: yes' : 'recoverable: no');
221
+ }
222
+ if (error.recoveryHint !== undefined) parts.push(`suggested action: ${error.recoveryHint}`);
223
+ if (error.hint !== undefined) parts.push(error.hint);
224
+ return `Error: ${error.message}\n[${parts.join('; ')}]`;
225
+ }
226
+
227
+ /** In-place variant of {@link addUsage} for mutable accumulators. */
228
+ export function accumulateUsage(target: Usage, delta: Usage): void {
229
+ target.promptTokens += delta.promptTokens;
230
+ target.completionTokens += delta.completionTokens;
231
+ target.totalTokens += delta.totalTokens;
232
+ if (delta.reasoningTokens !== undefined) {
233
+ target.reasoningTokens = (target.reasoningTokens ?? 0) + delta.reasoningTokens;
234
+ }
235
+ if (delta.cachedReadTokens !== undefined) {
236
+ target.cachedReadTokens = (target.cachedReadTokens ?? 0) + delta.cachedReadTokens;
237
+ }
238
+ if (delta.cacheWriteTokens !== undefined) {
239
+ target.cacheWriteTokens = (target.cacheWriteTokens ?? 0) + delta.cacheWriteTokens;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Fold a completed (or failed - tokens were spent either way) child
245
+ * run's usage into the parent run's accounting: `state.usage`,
246
+ * `state.usageByModel` and the run's {@link UsageAccumulator} (W-033).
247
+ * Children carrying a per-model breakdown fold model-by-model (each
248
+ * child model entry counts as one attempt on the parent); a child
249
+ * without `usageByModel` folds its aggregate under the synthetic id
250
+ * `sub-agent:<name>`, skipped entirely when all-zero so phantom
251
+ * entries never appear.
252
+ *
253
+ * Must be called exactly once per child run at exactly one seam -
254
+ * a second call double-counts (pinned by test).
255
+ */
256
+ export function foldChildRunUsage(
257
+ state: RunState,
258
+ usageAcc: UsageAccumulator | undefined,
259
+ childState: RunState,
260
+ childName: string,
261
+ ): void {
262
+ const byModel = childState.usageByModel;
263
+ if (byModel !== undefined && Object.keys(byModel).length > 0) {
264
+ for (const [modelId, usage] of Object.entries(byModel)) {
265
+ addModelUsage(state, modelId, usage);
266
+ accumulateUsage(state.usage, usage);
267
+ usageAcc?.add(modelId, usage);
268
+ }
269
+ return;
270
+ }
271
+ const aggregate = childState.usage;
272
+ if (
273
+ aggregate.promptTokens === 0 &&
274
+ aggregate.completionTokens === 0 &&
275
+ aggregate.totalTokens === 0
276
+ ) {
277
+ return;
278
+ }
279
+ const syntheticId = `sub-agent:${childName}`;
280
+ addModelUsage(state, syntheticId, aggregate);
281
+ accumulateUsage(state.usage, aggregate);
282
+ usageAcc?.add(syntheticId, aggregate);
283
+ }
284
+
285
+ /**
286
+ * Resolve the effective reasoning-retention policy for
287
+ * this step (RB-42). Drop any buffered reasoning when the
288
+ * contract downgrades to `'strip'`.
289
+ */
290
+ export function applyReasoningRetention(
291
+ agentOverride: ReasoningRetention | undefined,
292
+ provider: Provider,
293
+ messages: Message[],
294
+ stateMessages: Message[],
295
+ ): ReasoningRetention {
296
+ const reasoningPolicy = effectiveReasoningRetention(agentOverride, provider);
297
+ if (reasoningPolicy === 'strip') {
298
+ const { stripped } = stripReasoningFromMessages(messages);
299
+ // Mirror the strip into RunState so the persisted state
300
+ // matches the in-flight buffer.
301
+ if (stripped > 0) {
302
+ // The structural drop is bytes-equal across `messages`
303
+ // and `state.messages` (both arrays carry the same
304
+ // references); re-strip RunState explicitly to be safe.
305
+ stripReasoningFromMessages(stateMessages);
306
+ }
307
+ }
308
+ return reasoningPolicy;
309
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Provider stream bridging for the agent runtime: the per-step
3
+ * accumulation of `ProviderEvent`s into agent-visible events / final
4
+ * tool calls, thrown-provider-error classification for the fallback
5
+ * chain, and the `ModelSpec` -> `Provider` projection. Extracted
6
+ * verbatim from `factory.ts` (issue #23).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import type {
12
+ AgentEvent,
13
+ ModelSpec,
14
+ Provider,
15
+ ProviderError,
16
+ ProviderErrorKind,
17
+ ProviderEvent,
18
+ ReasoningContent,
19
+ ToolCall,
20
+ Usage,
21
+ } from '@graphorin/core';
22
+
23
+ /**
24
+ * AG-21: classify a **thrown** provider error into a {@link ProviderErrorKind}
25
+ * so the fallback chain can act on it, instead of flattening every exception to
26
+ * `'unknown'` (which is always fallback-ineligible). Structural - reads the
27
+ * `kind` carried by `@graphorin/provider`'s `GraphorinProviderError` subclasses
28
+ * without importing them, keeping the agent decoupled from the provider package.
29
+ */
30
+ /** Canonical `ProviderErrorKind` values honoured off a thrown error's `errorKind`. */
31
+ const CANONICAL_PROVIDER_ERROR_KINDS: ReadonlySet<string> = new Set([
32
+ 'rate-limit',
33
+ 'capacity',
34
+ 'context-length',
35
+ 'transient',
36
+ 'invalid-request',
37
+ 'unauthorized',
38
+ 'content-filter',
39
+ ]);
40
+
41
+ export function classifyThrownProviderErrorKind(cause: unknown): ProviderErrorKind {
42
+ if (typeof cause === 'object' && cause !== null) {
43
+ // B1: `ProviderHttpError` carries the canonical mapped kind on
44
+ // `errorKind` (its `kind` stays the stable 'provider-http'
45
+ // discriminant) - honour it so a thrown 429 / context overflow is
46
+ // classified like the structured-event equivalent.
47
+ const errorKind = (cause as { readonly errorKind?: unknown }).errorKind;
48
+ if (typeof errorKind === 'string' && CANONICAL_PROVIDER_ERROR_KINDS.has(errorKind)) {
49
+ return errorKind as ProviderErrorKind;
50
+ }
51
+ switch ((cause as { readonly kind?: unknown }).kind) {
52
+ case 'rate-limit-exceeded':
53
+ return 'rate-limit';
54
+ }
55
+ }
56
+ return 'unknown';
57
+ }
58
+
59
+ export function specToProvider(spec: ModelSpec): Provider {
60
+ if ('provider' in spec) return spec.provider as Provider;
61
+ return spec as Provider;
62
+ }
63
+
64
+ export interface ToolCallAccumulator {
65
+ readonly toolCallId: string;
66
+ toolName: string;
67
+ argsBuffer: string;
68
+ }
69
+
70
+ export interface ProviderEventOutcome {
71
+ readonly emit?: AgentEvent;
72
+ readonly providerError?: ProviderError;
73
+ readonly usage?: Usage;
74
+ readonly finished?: boolean;
75
+ }
76
+
77
+ export interface ProviderEventCollector {
78
+ textBuffer: string;
79
+ reasoningBuffer: string;
80
+ reasoningParts: ReasoningContent[];
81
+ calls: Map<string, ToolCallAccumulator>;
82
+ finalCalls: ToolCall[];
83
+ }
84
+
85
+ export function handleProviderEvent(
86
+ ev: ProviderEvent,
87
+ state: ProviderEventCollector,
88
+ ): ProviderEventOutcome {
89
+ switch (ev.type) {
90
+ case 'stream-start':
91
+ return {};
92
+ case 'reasoning-delta':
93
+ state.reasoningBuffer += ev.delta;
94
+ return { emit: { type: 'reasoning.delta', delta: ev.delta } };
95
+ case 'reasoning-end': {
96
+ // W-024: close the current reasoning block - flush the buffered
97
+ // deltas into a per-block ReasoningContent carrying the adapter's
98
+ // opaque round-trip meta (Anthropic thinking signature / redacted
99
+ // data). A redacted block has no deltas: its part is meta-only
100
+ // with empty text, which the retention pipeline still round-trips.
101
+ const text = state.reasoningBuffer;
102
+ state.reasoningBuffer = '';
103
+ state.reasoningParts.push({
104
+ type: 'reasoning',
105
+ text,
106
+ ...(ev.meta !== undefined ? { meta: ev.meta } : {}),
107
+ });
108
+ return {};
109
+ }
110
+ case 'text-delta':
111
+ state.textBuffer += ev.delta;
112
+ return { emit: { type: 'text.delta', delta: ev.delta } };
113
+ case 'tool-call-start':
114
+ state.calls.set(ev.toolCallId, {
115
+ toolCallId: ev.toolCallId,
116
+ toolName: ev.toolName,
117
+ argsBuffer: '',
118
+ });
119
+ return {
120
+ emit: {
121
+ type: 'tool.call.start',
122
+ toolCallId: ev.toolCallId,
123
+ toolName: ev.toolName,
124
+ args: undefined,
125
+ },
126
+ };
127
+ case 'tool-call-input-delta': {
128
+ const acc = state.calls.get(ev.toolCallId);
129
+ if (acc !== undefined) acc.argsBuffer += ev.argsDelta;
130
+ return {
131
+ emit: { type: 'tool.call.delta', toolCallId: ev.toolCallId, argsDelta: ev.argsDelta },
132
+ };
133
+ }
134
+ case 'tool-call-end': {
135
+ const acc = state.calls.get(ev.toolCallId);
136
+ if (acc === undefined) {
137
+ // AG-26: an end without a matching start has no tool name - the
138
+ // old path dispatched it as the unknown tool ''. Drop it loudly.
139
+ process.stderr.write(
140
+ `[graphorin/agent] dropped tool-call-end '${ev.toolCallId}' with no matching tool-call-start.\n`,
141
+ );
142
+ return {};
143
+ }
144
+ state.finalCalls.push({
145
+ toolCallId: ev.toolCallId,
146
+ toolName: acc.toolName,
147
+ args: ev.finalArgs,
148
+ });
149
+ return {
150
+ emit: { type: 'tool.call.end', toolCallId: ev.toolCallId, finalArgs: ev.finalArgs },
151
+ };
152
+ }
153
+ // AG-26: provider-generated files / citations are consumer-observable
154
+ // events instead of silently vanishing.
155
+ case 'file':
156
+ return { emit: { type: 'file.generated', mimeType: ev.mimeType, data: ev.data } };
157
+ case 'source':
158
+ return {
159
+ emit: {
160
+ type: 'source.cited',
161
+ uri: ev.uri,
162
+ ...(ev.title !== undefined ? { title: ev.title } : {}),
163
+ },
164
+ };
165
+ case 'finish':
166
+ return { usage: ev.usage, finished: true };
167
+ case 'error':
168
+ return { providerError: ev.error };
169
+ default: {
170
+ const _exhaustive: never = ev;
171
+ void _exhaustive;
172
+ return {};
173
+ }
174
+ }
175
+ }