agent-accelerator 0.1.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/LICENSE +21 -0
- package/README.md +1220 -0
- package/SYSTEM_PROMPT.md +14 -0
- package/SYSTEM_PROMPT_AGENT.md +34 -0
- package/SYSTEM_PROMPT_TOOLS.md +9 -0
- package/bunfig.toml +2 -0
- package/package.json +59 -0
- package/src/agent/agent.ts +615 -0
- package/src/agent/context.ts +161 -0
- package/src/agent/delegation.ts +481 -0
- package/src/agent/loop.ts +569 -0
- package/src/agent/subagent.ts +83 -0
- package/src/ai-sdk/converters.ts +342 -0
- package/src/ai-sdk/errors.ts +122 -0
- package/src/ai-sdk/executor.ts +454 -0
- package/src/ai-sdk/index.ts +55 -0
- package/src/ai-sdk/model-provider.ts +303 -0
- package/src/ai-sdk/options.ts +306 -0
- package/src/ai-sdk/provider.ts +415 -0
- package/src/ai-sdk/registry.ts +416 -0
- package/src/data/README.md +84 -0
- package/src/index.ts +190 -0
- package/src/models/catalog-cache.ts +273 -0
- package/src/models/catalog.ts +503 -0
- package/src/streaming/event-stream.ts +211 -0
- package/src/streaming/sse-parser.ts +97 -0
- package/src/tokens/counter.ts +136 -0
- package/src/tools/executor.ts +365 -0
- package/src/tools/schema.ts +221 -0
- package/src/tools/tool.ts +101 -0
- package/src/types/agent.ts +87 -0
- package/src/types/core.ts +86 -0
- package/src/types/message.ts +115 -0
- package/src/types/model.ts +212 -0
- package/src/types/provider-payloads.ts +434 -0
- package/src/types/response.ts +158 -0
- package/src/types/tool.ts +61 -0
- package/src/utils/base64.ts +27 -0
- package/src/utils/cache.ts +146 -0
- package/src/utils/env.ts +78 -0
- package/src/utils/headers.ts +110 -0
- package/src/utils/media.ts +137 -0
- package/src/utils/serialization.ts +91 -0
- package/src/utils/session.ts +26 -0
- package/src/utils/thought-signature.ts +27 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import type { Provider, ProviderRequestOptions, ModelSpec } from "../types/model.ts";
|
|
2
|
+
import type { ToolDefinition, ToolCallRecord, ToolResultRecord } from "../types/tool.ts";
|
|
3
|
+
import type { TokenUsage } from "../types/core.ts";
|
|
4
|
+
import type { AgentRunOptions } from "../types/agent.ts";
|
|
5
|
+
import { AgentResponse, type SubAgentExecutionMetadata, type StreamEvent } from "../types/response.ts";
|
|
6
|
+
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
7
|
+
import { AgentContext } from "./context.ts";
|
|
8
|
+
import { toStandardToolDeclarations } from "../tools/tool.ts";
|
|
9
|
+
import { executeToolCalls } from "../tools/executor.ts";
|
|
10
|
+
import { getModelFromCatalog, ensureModelCatalogFresh } from "../models/catalog.ts";
|
|
11
|
+
import { countTokens } from "../tokens/counter.ts";
|
|
12
|
+
|
|
13
|
+
export interface AgentLoopConfig {
|
|
14
|
+
agentName?: string;
|
|
15
|
+
provider: Provider;
|
|
16
|
+
modelId: string;
|
|
17
|
+
context: AgentContext;
|
|
18
|
+
tools: Record<string, ToolDefinition>;
|
|
19
|
+
options?: ProviderRequestOptions;
|
|
20
|
+
runOptions?: AgentRunOptions;
|
|
21
|
+
maxTurns?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec): TokenUsage["cost"] {
|
|
25
|
+
if (usage.cost && usage.cost.totalCost !== undefined && usage.cost.totalCost > 0) {
|
|
26
|
+
return usage.cost;
|
|
27
|
+
}
|
|
28
|
+
if (!spec) return usage.cost;
|
|
29
|
+
|
|
30
|
+
const costData = spec.cost || {};
|
|
31
|
+
const pricingData = spec.pricing || {};
|
|
32
|
+
const inputPrice = pricingData.inputPerMillion ?? costData.input ?? 0;
|
|
33
|
+
const outputPrice = pricingData.outputPerMillion ?? costData.output ?? 0;
|
|
34
|
+
const cacheReadPrice = pricingData.cacheReadPerMillion ?? costData.cache_read ?? 0;
|
|
35
|
+
const cacheWritePrice = pricingData.cacheWritePerMillion ?? costData.cache_write ?? 0;
|
|
36
|
+
|
|
37
|
+
if (inputPrice === 0 && outputPrice === 0 && cacheReadPrice === 0 && cacheWritePrice === 0) {
|
|
38
|
+
return usage.cost;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cachedTokens = usage.cachedTokens ?? usage.cacheReadTokens ?? 0;
|
|
42
|
+
const nonCachedInputTokens = Math.max(0, (usage.inputTokens || 0) - cachedTokens);
|
|
43
|
+
const cacheWriteTokens = usage.cacheWriteTokens ?? 0;
|
|
44
|
+
const outputTokens = usage.outputTokens ?? 0;
|
|
45
|
+
|
|
46
|
+
const inputCost = (nonCachedInputTokens / 1_000_000) * inputPrice;
|
|
47
|
+
const cacheReadCost = (cachedTokens / 1_000_000) * cacheReadPrice;
|
|
48
|
+
const cacheWriteCost = (cacheWriteTokens / 1_000_000) * cacheWritePrice;
|
|
49
|
+
const outputCost = (outputTokens / 1_000_000) * outputPrice;
|
|
50
|
+
const totalCost = inputCost + cacheReadCost + cacheWriteCost + outputCost;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
inputCost,
|
|
54
|
+
outputCost,
|
|
55
|
+
cacheReadCost,
|
|
56
|
+
cacheWriteCost,
|
|
57
|
+
totalCost,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec): void {
|
|
62
|
+
target.inputTokens += source.inputTokens || 0;
|
|
63
|
+
target.outputTokens += source.outputTokens || 0;
|
|
64
|
+
target.totalTokens += source.totalTokens || 0;
|
|
65
|
+
target.cachedTokens = (target.cachedTokens ?? 0) + (source.cachedTokens ?? 0);
|
|
66
|
+
target.cacheReadTokens = (target.cacheReadTokens ?? 0) + (source.cacheReadTokens ?? 0);
|
|
67
|
+
target.cacheWriteTokens = (target.cacheWriteTokens ?? 0) + (source.cacheWriteTokens ?? 0);
|
|
68
|
+
target.thinkingTokens = (target.thinkingTokens ?? 0) + (source.thinkingTokens ?? 0);
|
|
69
|
+
|
|
70
|
+
const sourceCost = computeCostFromPricing(source, spec);
|
|
71
|
+
if (sourceCost) {
|
|
72
|
+
source.cost = sourceCost;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (sourceCost || target.cost) {
|
|
76
|
+
target.cost = {
|
|
77
|
+
inputCost: (target.cost?.inputCost ?? 0) + (sourceCost?.inputCost ?? 0),
|
|
78
|
+
outputCost: (target.cost?.outputCost ?? 0) + (sourceCost?.outputCost ?? 0),
|
|
79
|
+
cacheReadCost: (target.cost?.cacheReadCost ?? 0) + (sourceCost?.cacheReadCost ?? 0),
|
|
80
|
+
cacheWriteCost: (target.cost?.cacheWriteCost ?? 0) + (sourceCost?.cacheWriteCost ?? 0),
|
|
81
|
+
totalCost: (target.cost?.totalCost ?? 0) + (sourceCost?.totalCost ?? 0),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sanitizeToolResult(r: ToolResultRecord): { sanitized: ToolResultRecord; metas: SubAgentExecutionMetadata[] } {
|
|
87
|
+
if (r.result && typeof r.result === "object" && "_subagentMetadata" in (r.result as any)) {
|
|
88
|
+
const metaList = (r.result as any)._subagentMetadata as SubAgentExecutionMetadata[];
|
|
89
|
+
const xml = (r.result as any).xml || (r.result as any).toString?.() || String(r.result);
|
|
90
|
+
return {
|
|
91
|
+
sanitized: { ...r, result: xml },
|
|
92
|
+
metas: Array.isArray(metaList) ? metaList : [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { sanitized: r, metas: [] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toolCallFingerprint(call: ToolCallRecord): string {
|
|
99
|
+
const stableSerialize = (value: any, seen = new Set<any>()): string => {
|
|
100
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
101
|
+
if (seen.has(value)) return "[Circular]";
|
|
102
|
+
seen.add(value);
|
|
103
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableSerialize(item, seen)).join(",")}]`;
|
|
104
|
+
const output = `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key], seen)}`).join(",")}}`;
|
|
105
|
+
seen.delete(value);
|
|
106
|
+
return output;
|
|
107
|
+
};
|
|
108
|
+
const args = stableSerialize(call.arguments ?? {});
|
|
109
|
+
return `${call.name}\u0000${args}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Prevents an identical tool call from being executed in consecutive model
|
|
114
|
+
* turns. The synthetic error is sent back to the model so it can reuse the
|
|
115
|
+
* previous result or choose a different action.
|
|
116
|
+
*/
|
|
117
|
+
async function executeToolCallsWithRepeatGuard(options: {
|
|
118
|
+
tools: Record<string, ToolDefinition>;
|
|
119
|
+
toolCalls: ToolCallRecord[];
|
|
120
|
+
previousFingerprints: Set<string>;
|
|
121
|
+
agentName?: string;
|
|
122
|
+
signal?: AbortSignal;
|
|
123
|
+
sessionId?: string;
|
|
124
|
+
}): Promise<{ results: ToolResultRecord[]; fingerprints: Set<string> }> {
|
|
125
|
+
const { toolCalls, previousFingerprints } = options;
|
|
126
|
+
const currentFingerprints = new Set<string>();
|
|
127
|
+
const blocked = new Map<string, ToolResultRecord>();
|
|
128
|
+
const executable: ToolCallRecord[] = [];
|
|
129
|
+
|
|
130
|
+
for (const call of toolCalls) {
|
|
131
|
+
const fingerprint = toolCallFingerprint(call);
|
|
132
|
+
currentFingerprints.add(fingerprint);
|
|
133
|
+
if (previousFingerprints.has(fingerprint)) {
|
|
134
|
+
blocked.set(call.id, {
|
|
135
|
+
id: call.id,
|
|
136
|
+
name: call.name,
|
|
137
|
+
result:
|
|
138
|
+
`Error: Tool '${call.name}' was called again immediately with identical arguments. ` +
|
|
139
|
+
"The previous result is already available; reuse it or call the tool with different arguments.",
|
|
140
|
+
isError: true,
|
|
141
|
+
durationMs: 0,
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
executable.push(call);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const executed = executable.length > 0
|
|
149
|
+
? await executeToolCalls({
|
|
150
|
+
tools: options.tools,
|
|
151
|
+
toolCalls: executable,
|
|
152
|
+
agentName: options.agentName,
|
|
153
|
+
parallel: true,
|
|
154
|
+
signal: options.signal,
|
|
155
|
+
sessionId: options.sessionId,
|
|
156
|
+
})
|
|
157
|
+
: [];
|
|
158
|
+
const byId = new Map<string, ToolResultRecord>();
|
|
159
|
+
for (const result of executed) byId.set(result.id, result);
|
|
160
|
+
for (const [id, result] of blocked) byId.set(id, result);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
results: toolCalls.map((call) => byId.get(call.id)!).filter(Boolean),
|
|
164
|
+
fingerprints: currentFingerprints,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Runs a single non-streaming agent turn or multi-turn loop
|
|
170
|
+
*/
|
|
171
|
+
export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentResponse> {
|
|
172
|
+
await ensureModelCatalogFresh();
|
|
173
|
+
const {
|
|
174
|
+
agentName,
|
|
175
|
+
provider,
|
|
176
|
+
modelId,
|
|
177
|
+
context,
|
|
178
|
+
tools,
|
|
179
|
+
options,
|
|
180
|
+
runOptions,
|
|
181
|
+
maxTurns = 10,
|
|
182
|
+
} = config;
|
|
183
|
+
|
|
184
|
+
const standardTools = toStandardToolDeclarations(tools);
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
|
|
187
|
+
let accumulatedUsage: TokenUsage = {
|
|
188
|
+
inputTokens: 0,
|
|
189
|
+
outputTokens: 0,
|
|
190
|
+
totalTokens: 0,
|
|
191
|
+
cachedTokens: 0,
|
|
192
|
+
cacheReadTokens: 0,
|
|
193
|
+
cacheWriteTokens: 0,
|
|
194
|
+
thinkingTokens: 0,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const allToolCalls: ToolCallRecord[] = [];
|
|
198
|
+
const allToolResults: ToolResultRecord[] = [];
|
|
199
|
+
const allSubagents: SubAgentExecutionMetadata[] = [];
|
|
200
|
+
let finalResult: any = null;
|
|
201
|
+
let turns = 0;
|
|
202
|
+
let previousToolCallFingerprints = new Set<string>();
|
|
203
|
+
|
|
204
|
+
while (turns < maxTurns) {
|
|
205
|
+
turns++;
|
|
206
|
+
|
|
207
|
+
// Battle-tested context window check — trim oldest history if needed, keep cached prefix (system+tools)
|
|
208
|
+
const spec = getModelFromCatalog(provider.id, modelId);
|
|
209
|
+
const contextWindow = spec?.limit?.context ?? spec?.contextWindow ?? 128000;
|
|
210
|
+
const maxOutput = spec?.limit?.output ?? spec?.maxOutputTokens ?? 8192;
|
|
211
|
+
// Reserve for output + 10% headroom, ensure first turn already cache-friendly
|
|
212
|
+
const budgetForInput = Math.floor(contextWindow * 0.9) - maxOutput;
|
|
213
|
+
let estimated = 0;
|
|
214
|
+
try {
|
|
215
|
+
estimated = countTokens({ systemPrompt: context.systemPrompt, messages: context.messages, tools: standardTools as any });
|
|
216
|
+
} catch {}
|
|
217
|
+
if (estimated > budgetForInput && context.messages.length > 2) {
|
|
218
|
+
// Keep system + last 70% of history, drop oldest middle (preserve cached prefix stability)
|
|
219
|
+
const keepCount = Math.max(2, Math.floor(context.messages.length * 0.7));
|
|
220
|
+
const toKeep = context.messages.slice(-keepCount);
|
|
221
|
+
// Preserve at least system and first user if possible
|
|
222
|
+
const firstUserIdx = context.messages.findIndex((m) => m.role === "user");
|
|
223
|
+
if (firstUserIdx >= 0 && firstUserIdx < context.messages.length - keepCount) {
|
|
224
|
+
// Drop middle, keep head + tail for cache stability
|
|
225
|
+
const head = context.messages.slice(0, 1);
|
|
226
|
+
context.messages = [...head, ...toKeep];
|
|
227
|
+
} else {
|
|
228
|
+
context.messages = toKeep;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const providerOptions: ProviderRequestOptions = {
|
|
233
|
+
...options,
|
|
234
|
+
// Merge sessionId from cache or top-level (C4 fix: read both)
|
|
235
|
+
sessionId: runOptions?.sessionId || options?.sessionId || options?.cache?.sessionId,
|
|
236
|
+
cache: options?.cache,
|
|
237
|
+
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
238
|
+
signal: runOptions?.signal,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const genResult = await provider.generate(
|
|
242
|
+
modelId,
|
|
243
|
+
{
|
|
244
|
+
systemPrompt: context.systemPrompt,
|
|
245
|
+
messages: context.messages,
|
|
246
|
+
cachedContentId: context.cachedContentId,
|
|
247
|
+
},
|
|
248
|
+
providerOptions
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
finalResult = genResult;
|
|
252
|
+
|
|
253
|
+
// Safety fallback: if no tool calls and text is empty, rescue answer from thinking
|
|
254
|
+
if ((!genResult.text || genResult.text.trim() === "") && (!genResult.toolCalls || genResult.toolCalls.length === 0) && genResult.thinking) {
|
|
255
|
+
if (genResult.thinking.includes("</think>")) {
|
|
256
|
+
const parts = genResult.thinking.split(/<\/(?:think|thought)>/i);
|
|
257
|
+
genResult.thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
|
|
258
|
+
genResult.text = parts.slice(1).join("").trim();
|
|
259
|
+
} else {
|
|
260
|
+
genResult.text = genResult.thinking;
|
|
261
|
+
genResult.thinking = undefined;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
accumulateUsage(accumulatedUsage, genResult.usage, spec);
|
|
266
|
+
|
|
267
|
+
// Record assistant turn in context
|
|
268
|
+
context.addAssistantMessage(
|
|
269
|
+
genResult.text,
|
|
270
|
+
genResult.toolCalls,
|
|
271
|
+
genResult.thinking,
|
|
272
|
+
genResult.thoughtSignature
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// If no tool calls, generation is complete!
|
|
276
|
+
if (!genResult.toolCalls || genResult.toolCalls.length === 0) {
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Execute tool calls — always parallel (model-driven, bloatfree DX7)
|
|
281
|
+
allToolCalls.push(...genResult.toolCalls);
|
|
282
|
+
const guarded = await executeToolCallsWithRepeatGuard({
|
|
283
|
+
tools,
|
|
284
|
+
toolCalls: genResult.toolCalls,
|
|
285
|
+
previousFingerprints: previousToolCallFingerprints,
|
|
286
|
+
agentName,
|
|
287
|
+
signal: runOptions?.signal,
|
|
288
|
+
sessionId: runOptions?.sessionId || options?.sessionId || options?.cache?.sessionId,
|
|
289
|
+
});
|
|
290
|
+
const results = guarded.results;
|
|
291
|
+
previousToolCallFingerprints = guarded.fingerprints;
|
|
292
|
+
|
|
293
|
+
// Process results and extract any sub-agent execution metadata
|
|
294
|
+
const sanitizedResults: ToolResultRecord[] = [];
|
|
295
|
+
|
|
296
|
+
for (const r of results) {
|
|
297
|
+
const { sanitized, metas } = sanitizeToolResult(r);
|
|
298
|
+
if (metas.length > 0) {
|
|
299
|
+
allSubagents.push(...metas);
|
|
300
|
+
for (const s of metas) {
|
|
301
|
+
const subagentSpec = getModelFromCatalog(s.provider, s.model);
|
|
302
|
+
if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
|
|
303
|
+
s.usage.cost = computeCostFromPricing(s.usage, subagentSpec);
|
|
304
|
+
}
|
|
305
|
+
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
sanitizedResults.push(sanitized);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
allToolResults.push(...sanitizedResults);
|
|
312
|
+
context.addToolResults(sanitizedResults);
|
|
313
|
+
|
|
314
|
+
if (runOptions?.signal?.aborted) break;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return new AgentResponse({
|
|
318
|
+
text: finalResult?.text || "",
|
|
319
|
+
thinking: finalResult?.thinking,
|
|
320
|
+
thoughtSignature: finalResult?.thoughtSignature,
|
|
321
|
+
toolCalls: allToolCalls,
|
|
322
|
+
toolResults: allToolResults,
|
|
323
|
+
subagents: allSubagents,
|
|
324
|
+
usage: accumulatedUsage,
|
|
325
|
+
responseId: finalResult?.responseId,
|
|
326
|
+
model: modelId,
|
|
327
|
+
provider: provider.id,
|
|
328
|
+
finishReason: finalResult?.finishReason,
|
|
329
|
+
durationMs: Date.now() - startTime,
|
|
330
|
+
raw: finalResult?.raw,
|
|
331
|
+
turns,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Runs a streaming agent loop, pushing deltas to an AssistantMessageEventStream
|
|
337
|
+
*/
|
|
338
|
+
export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventStream {
|
|
339
|
+
const outerStream = new AssistantMessageEventStream();
|
|
340
|
+
const startTime = Date.now();
|
|
341
|
+
|
|
342
|
+
// Cancellation: merge user signal + outer cancel() into one linked controller.
|
|
343
|
+
// Vercel doStream honors abortSignal for all providers, so aborting this
|
|
344
|
+
// stops the HTTP request; we also cancel the active inner provider stream.
|
|
345
|
+
const linked = new AbortController();
|
|
346
|
+
const userSignal = config.runOptions?.signal;
|
|
347
|
+
const forwardUserAbort = () => {
|
|
348
|
+
try { linked.abort((userSignal as any)?.reason); } catch { try { linked.abort(); } catch {} }
|
|
349
|
+
};
|
|
350
|
+
if (userSignal?.aborted) forwardUserAbort();
|
|
351
|
+
else userSignal?.addEventListener("abort", forwardUserAbort, { once: true });
|
|
352
|
+
let currentInner: AssistantMessageEventStream | null = null;
|
|
353
|
+
const removeOuterCancel = outerStream.onCancel(() => {
|
|
354
|
+
try { linked.abort(); } catch {}
|
|
355
|
+
try { currentInner?.cancel(); } catch {}
|
|
356
|
+
});
|
|
357
|
+
linked.signal.addEventListener("abort", () => {
|
|
358
|
+
try { currentInner?.cancel(); } catch {}
|
|
359
|
+
});
|
|
360
|
+
const throwIfCancelled = () => {
|
|
361
|
+
if (linked.signal.aborted || outerStream.isCancelled())
|
|
362
|
+
throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
(async () => {
|
|
366
|
+
try {
|
|
367
|
+
await ensureModelCatalogFresh();
|
|
368
|
+
const {
|
|
369
|
+
agentName,
|
|
370
|
+
provider,
|
|
371
|
+
modelId,
|
|
372
|
+
context,
|
|
373
|
+
tools,
|
|
374
|
+
options,
|
|
375
|
+
runOptions,
|
|
376
|
+
maxTurns = 10,
|
|
377
|
+
} = config;
|
|
378
|
+
|
|
379
|
+
const standardTools = toStandardToolDeclarations(tools);
|
|
380
|
+
|
|
381
|
+
let accumulatedUsage: TokenUsage = {
|
|
382
|
+
inputTokens: 0,
|
|
383
|
+
outputTokens: 0,
|
|
384
|
+
totalTokens: 0,
|
|
385
|
+
cachedTokens: 0,
|
|
386
|
+
cacheReadTokens: 0,
|
|
387
|
+
cacheWriteTokens: 0,
|
|
388
|
+
thinkingTokens: 0,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const allToolCalls: ToolCallRecord[] = [];
|
|
392
|
+
const allToolResults: ToolResultRecord[] = [];
|
|
393
|
+
const allSubagents: SubAgentExecutionMetadata[] = [];
|
|
394
|
+
let lastResponse: AgentResponse | null = null;
|
|
395
|
+
let turns = 0;
|
|
396
|
+
let previousToolCallFingerprints = new Set<string>();
|
|
397
|
+
|
|
398
|
+
while (turns < maxTurns) {
|
|
399
|
+
throwIfCancelled();
|
|
400
|
+
turns++;
|
|
401
|
+
|
|
402
|
+
// Same context-window trim as non-stream (ensure cache prefix stable)
|
|
403
|
+
const spec2 = getModelFromCatalog(provider.id, modelId);
|
|
404
|
+
const cw2 = spec2?.limit?.context ?? spec2?.contextWindow ?? 128000;
|
|
405
|
+
const mo2 = spec2?.limit?.output ?? spec2?.maxOutputTokens ?? 8192;
|
|
406
|
+
const budget2 = Math.floor(cw2 * 0.9) - mo2;
|
|
407
|
+
try {
|
|
408
|
+
const est2 = countTokens({ systemPrompt: context.systemPrompt, messages: context.messages, tools: standardTools as any });
|
|
409
|
+
if (est2 > budget2 && context.messages.length > 2) {
|
|
410
|
+
const keep2 = Math.max(2, Math.floor(context.messages.length * 0.7));
|
|
411
|
+
const toKeep = context.messages.slice(-keep2);
|
|
412
|
+
const firstUserIdx = context.messages.findIndex((m) => m.role === "user");
|
|
413
|
+
if (firstUserIdx >= 0 && firstUserIdx < context.messages.length - keep2) {
|
|
414
|
+
const head = context.messages.slice(0, 1);
|
|
415
|
+
context.messages = [...head, ...toKeep];
|
|
416
|
+
} else {
|
|
417
|
+
context.messages = toKeep;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch {}
|
|
421
|
+
|
|
422
|
+
const providerOptions: ProviderRequestOptions = {
|
|
423
|
+
...options,
|
|
424
|
+
sessionId: runOptions?.sessionId || options?.sessionId || options?.cache?.sessionId,
|
|
425
|
+
cache: options?.cache,
|
|
426
|
+
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
427
|
+
signal: linked.signal,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const innerStream = provider.stream(
|
|
431
|
+
modelId,
|
|
432
|
+
{
|
|
433
|
+
systemPrompt: context.systemPrompt,
|
|
434
|
+
messages: context.messages,
|
|
435
|
+
cachedContentId: context.cachedContentId,
|
|
436
|
+
},
|
|
437
|
+
providerOptions
|
|
438
|
+
);
|
|
439
|
+
currentInner = innerStream as AssistantMessageEventStream;
|
|
440
|
+
|
|
441
|
+
for await (const event of innerStream) {
|
|
442
|
+
throwIfCancelled();
|
|
443
|
+
if (event.type !== "done") {
|
|
444
|
+
outerStream.push(event);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const turnResponse = await innerStream.result();
|
|
449
|
+
currentInner = null;
|
|
450
|
+
throwIfCancelled();
|
|
451
|
+
lastResponse = turnResponse;
|
|
452
|
+
|
|
453
|
+
// Safety fallback: if no tool calls and text is empty, rescue answer from thinking
|
|
454
|
+
if ((!turnResponse.text || turnResponse.text.trim() === "") && (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) && turnResponse.thinking) {
|
|
455
|
+
if (turnResponse.thinking.includes("</think>")) {
|
|
456
|
+
const parts = turnResponse.thinking.split(/<\/(?:think|thought)>/i);
|
|
457
|
+
(turnResponse as any).thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
|
|
458
|
+
(turnResponse as any).text = parts.slice(1).join("").trim();
|
|
459
|
+
} else {
|
|
460
|
+
(turnResponse as any).text = turnResponse.thinking;
|
|
461
|
+
(turnResponse as any).thinking = undefined;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
accumulateUsage(accumulatedUsage, turnResponse.usage, spec2);
|
|
466
|
+
|
|
467
|
+
context.addAssistantMessage(
|
|
468
|
+
turnResponse.text,
|
|
469
|
+
turnResponse.toolCalls,
|
|
470
|
+
turnResponse.thinking,
|
|
471
|
+
turnResponse.thoughtSignature
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) {
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
allToolCalls.push(...turnResponse.toolCalls);
|
|
479
|
+
const guarded = await executeToolCallsWithRepeatGuard({
|
|
480
|
+
tools,
|
|
481
|
+
toolCalls: turnResponse.toolCalls,
|
|
482
|
+
previousFingerprints: previousToolCallFingerprints,
|
|
483
|
+
agentName,
|
|
484
|
+
signal: linked.signal,
|
|
485
|
+
sessionId: runOptions?.sessionId || options?.sessionId || options?.cache?.sessionId,
|
|
486
|
+
});
|
|
487
|
+
throwIfCancelled();
|
|
488
|
+
const results = guarded.results;
|
|
489
|
+
previousToolCallFingerprints = guarded.fingerprints;
|
|
490
|
+
|
|
491
|
+
const sanitizedResults: ToolResultRecord[] = [];
|
|
492
|
+
|
|
493
|
+
for (const r of results) {
|
|
494
|
+
const { sanitized, metas } = sanitizeToolResult(r);
|
|
495
|
+
if (metas.length > 0) {
|
|
496
|
+
allSubagents.push(...metas);
|
|
497
|
+
for (const s of metas) {
|
|
498
|
+
const subagentSpec = getModelFromCatalog(s.provider, s.model);
|
|
499
|
+
if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
|
|
500
|
+
s.usage.cost = computeCostFromPricing(s.usage, subagentSpec);
|
|
501
|
+
}
|
|
502
|
+
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
503
|
+
outerStream.push({
|
|
504
|
+
type: "subagent_complete",
|
|
505
|
+
subagent: s,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
sanitizedResults.push(sanitized);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const res of sanitizedResults) {
|
|
513
|
+
outerStream.push({
|
|
514
|
+
type: "tool_result",
|
|
515
|
+
toolResult: res,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
allToolResults.push(...sanitizedResults);
|
|
520
|
+
context.addToolResults(sanitizedResults);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const finalAgentResponse = new AgentResponse({
|
|
524
|
+
text: lastResponse?.text || "",
|
|
525
|
+
thinking: lastResponse?.thinking,
|
|
526
|
+
thoughtSignature: lastResponse?.thoughtSignature,
|
|
527
|
+
toolCalls: allToolCalls,
|
|
528
|
+
toolResults: allToolResults,
|
|
529
|
+
subagents: allSubagents,
|
|
530
|
+
usage: accumulatedUsage,
|
|
531
|
+
responseId: lastResponse?.responseId,
|
|
532
|
+
model: modelId,
|
|
533
|
+
provider: provider.id,
|
|
534
|
+
finishReason: lastResponse?.finishReason,
|
|
535
|
+
durationMs: Date.now() - startTime,
|
|
536
|
+
raw: lastResponse?.raw as any,
|
|
537
|
+
turns,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
outerStream.push({
|
|
541
|
+
type: "done",
|
|
542
|
+
delta: "",
|
|
543
|
+
usage: accumulatedUsage,
|
|
544
|
+
finishReason: lastResponse?.finishReason,
|
|
545
|
+
responseId: lastResponse?.responseId,
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
outerStream.end(finalAgentResponse);
|
|
549
|
+
} catch (err: any) {
|
|
550
|
+
const raw = err instanceof Error ? err : new Error(String(err));
|
|
551
|
+
const isAbort =
|
|
552
|
+
linked.signal.aborted ||
|
|
553
|
+
outerStream.isCancelled() ||
|
|
554
|
+
(raw as any)?.name === "AbortError" ||
|
|
555
|
+
/abort|cancell?ed/i.test(String((raw as any)?.message ?? raw));
|
|
556
|
+
outerStream.fail(
|
|
557
|
+
isAbort
|
|
558
|
+
? Object.assign(raw.name === "AbortError" ? raw : new Error("Stream aborted"), { name: "AbortError" })
|
|
559
|
+
: raw
|
|
560
|
+
);
|
|
561
|
+
} finally {
|
|
562
|
+
try { currentInner?.cancel(); } catch {}
|
|
563
|
+
try { userSignal?.removeEventListener("abort", forwardUserAbort); } catch {}
|
|
564
|
+
try { removeOuterCancel(); } catch {}
|
|
565
|
+
}
|
|
566
|
+
})();
|
|
567
|
+
|
|
568
|
+
return outerStream;
|
|
569
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Agent } from "./agent.ts";
|
|
2
|
+
import type { AgentConfig } from "../types/agent.ts";
|
|
3
|
+
import type { ToolDefinition } from "../types/tool.ts";
|
|
4
|
+
import { agentToTool } from "./delegation.ts";
|
|
5
|
+
import { getSubModel, getModel } from "../utils/env.ts";
|
|
6
|
+
|
|
7
|
+
/** AgentConfig alias for a delegated worker agent. */
|
|
8
|
+
export interface SubAgentConfig extends AgentConfig {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SubAgent is a specialized Agent designed for modular delegation,
|
|
12
|
+
* evaluation, reviewing, or parallel task execution.
|
|
13
|
+
*
|
|
14
|
+
* It inherits 100% of the capabilities of Agent, defaults to SUB_AGENT_MODEL,
|
|
15
|
+
* and provides .asTool() and .toTool() for seamless registration into parent agents.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const researcher = new SubAgent({
|
|
20
|
+
* name: "researcher",
|
|
21
|
+
* model: "google/gemini-3.5-flash-lite",
|
|
22
|
+
* instructions: "Return only sourced findings.",
|
|
23
|
+
* });
|
|
24
|
+
* const lead = new Agent({ model, subagents: [researcher] });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class SubAgent extends Agent {
|
|
28
|
+
/**
|
|
29
|
+
* Creates a delegated worker, defaulting to `SUB_AGENT_MODEL` or `MODEL`
|
|
30
|
+
* when `model` is omitted, and retaining short-lived cache state by default.
|
|
31
|
+
*
|
|
32
|
+
* @param config Worker configuration. It accepts the same options as `Agent`
|
|
33
|
+
* and can be registered with a parent via `subagents`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const researcher = new SubAgent({
|
|
38
|
+
* name: "researcher",
|
|
39
|
+
* model: "google/gemini-3.5-flash-lite",
|
|
40
|
+
* instructions: "Return only sourced findings.",
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* const lead = new Agent({ model, subagents: [researcher] });
|
|
44
|
+
* const result = await researcher.run("Find the relevant facts");
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
constructor(config: SubAgentConfig) {
|
|
48
|
+
const resolvedModel =
|
|
49
|
+
config.model ??
|
|
50
|
+
getSubModel() ??
|
|
51
|
+
getModel();
|
|
52
|
+
|
|
53
|
+
if (!resolvedModel) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`[Agent Accelerator] SubAgent '${config.name || "subagent"}' requires a model. ` +
|
|
56
|
+
`Specify 'model' in SubAgent config, or set SUB_AGENT_MODEL or MODEL in environment variables.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
super({
|
|
61
|
+
...config,
|
|
62
|
+
model: resolvedModel,
|
|
63
|
+
cache: config.cache ?? { retention: "short" },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Converts this SubAgent instance into a standard ToolDefinition for parent agent tool execution.
|
|
69
|
+
* @example `const research = researcher.asTool("research");`
|
|
70
|
+
*/
|
|
71
|
+
asTool(nameOverride?: string, descriptionOverride?: string): ToolDefinition {
|
|
72
|
+
return agentToTool({
|
|
73
|
+
name: nameOverride || this.name,
|
|
74
|
+
description: descriptionOverride || this.description,
|
|
75
|
+
agent: this,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Fluent alias for `.asTool()`. @example `const research = researcher.toTool();` */
|
|
80
|
+
toTool(nameOverride?: string, descriptionOverride?: string): ToolDefinition {
|
|
81
|
+
return this.asTool(nameOverride, descriptionOverride);
|
|
82
|
+
}
|
|
83
|
+
}
|