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