agent-accelerator 0.1.0 → 0.3.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/README.md +174 -123
- package/{SYSTEM_PROMPT_AGENT.md → examples/prompts/SYSTEM_PROMPT_AGENT.md} +1 -1
- package/package.json +14 -9
- package/src/agent/agent.ts +385 -73
- package/src/agent/context.ts +1 -0
- package/src/agent/delegation.ts +443 -30
- package/src/agent/loop.ts +183 -53
- package/src/data/README.md +6 -6
- package/src/index.ts +163 -46
- package/src/models/catalog-cache.ts +60 -9
- package/src/models/catalog.ts +55 -8
- package/src/providers/google.ts +926 -0
- package/src/providers/openai-compat.ts +1147 -0
- package/src/providers/openai.ts +959 -0
- package/src/providers/openrouter-responses.ts +949 -0
- package/src/providers/openrouter.ts +1037 -0
- package/src/{ai-sdk → providers}/registry.ts +43 -55
- package/src/providers.ts +492 -0
- package/src/session/store.ts +806 -0
- package/src/streaming/sse-parser.ts +6 -4
- package/src/tools/executor.ts +23 -7
- package/src/tools/schema.ts +21 -11
- package/src/types/agent.ts +33 -2
- package/src/types/core.ts +3 -2
- package/src/types/message.ts +5 -0
- package/src/types/model.ts +3 -7
- package/src/types/provider-payloads.ts +2 -84
- package/src/types/response.ts +24 -0
- package/src/types/tool.ts +8 -0
- package/src/update-models.ts +56 -0
- package/src/utils/cache.ts +1 -1
- package/src/utils/documents.ts +517 -0
- package/src/utils/env.ts +0 -7
- package/src/{ai-sdk → utils}/errors.ts +61 -2
- package/src/utils/headers.ts +10 -20
- package/src/utils/media.ts +5 -2
- package/src/utils/retry.ts +89 -0
- package/src/utils/serialization.ts +15 -0
- package/src/utils/session.ts +36 -0
- package/src/ai-sdk/converters.ts +0 -342
- package/src/ai-sdk/executor.ts +0 -454
- package/src/ai-sdk/index.ts +0 -55
- package/src/ai-sdk/model-provider.ts +0 -303
- package/src/ai-sdk/options.ts +0 -306
- package/src/ai-sdk/provider.ts +0 -415
- package/src/tokens/counter.ts +0 -136
- /package/{SYSTEM_PROMPT.md → examples/prompts/SYSTEM_PROMPT.md} +0 -0
- /package/{SYSTEM_PROMPT_TOOLS.md → examples/prompts/SYSTEM_PROMPT_TOOLS.md} +0 -0
package/src/agent/delegation.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { tool } from "../tools/tool.ts";
|
|
3
|
+
import { normalizeToolName } from "../tools/executor.ts";
|
|
3
4
|
import type { ToolDefinition } from "../types/tool.ts";
|
|
4
5
|
import type { SubAgentExecutionMetadata } from "../types/response.ts";
|
|
5
6
|
import type { Agent } from "./agent.ts";
|
|
6
|
-
import { resolveModel } from "../
|
|
7
|
+
import { resolveModel } from "../providers/registry.ts";
|
|
8
|
+
import { hashSessionPart } from "../utils/session.ts";
|
|
9
|
+
import { createTrackingId } from "../utils/session.ts";
|
|
10
|
+
import type { SubAgentStep } from "../types/response.ts";
|
|
7
11
|
|
|
8
12
|
/** Task descriptor accepted by the automatic `spawn_subagents` tool.
|
|
9
13
|
*
|
|
@@ -25,14 +29,14 @@ export interface DynamicSubagentTask {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
function sanitizeXmlTag(raw: string): string {
|
|
28
|
-
let s = raw.toUpperCase().replace(/[^A-Z0-
|
|
29
|
-
if (!/^[A-
|
|
30
|
-
s = s.replace(/[
|
|
32
|
+
let s = raw.toUpperCase().replace(/[^A-Z0-9.-]/g, "-");
|
|
33
|
+
if (!/^[A-Z]/.test(s)) s = `AGENT-${s}`;
|
|
34
|
+
s = s.replace(/[.]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
31
35
|
return s.slice(0, 64) || "SUBAGENT";
|
|
32
36
|
}
|
|
33
37
|
function sanitizeToolName(raw: string): string {
|
|
34
38
|
const tag = sanitizeXmlTag(raw);
|
|
35
|
-
return tag.toLowerCase().replace(/[^a-z0-
|
|
39
|
+
return tag.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").slice(0, 64) || "sub-agent";
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
function escapeXml(text: string): string {
|
|
@@ -44,7 +48,7 @@ function escapeXml(text: string): string {
|
|
|
44
48
|
.replace(/'/g, "'");
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
function createChildSessionId(parentId: string, tag: string): string {
|
|
51
|
+
export function createChildSessionId(parentId: string, tag: string): string {
|
|
48
52
|
let rand: string;
|
|
49
53
|
try {
|
|
50
54
|
if (typeof globalThis !== "undefined" && (globalThis as any).crypto?.randomUUID) {
|
|
@@ -58,7 +62,131 @@ function createChildSessionId(parentId: string, tag: string): string {
|
|
|
58
62
|
} catch {
|
|
59
63
|
rand = Math.random().toString(36).slice(2, 10);
|
|
60
64
|
}
|
|
61
|
-
|
|
65
|
+
// Provider-safe: OpenAI `prompt_cache_key` enforces max 64 chars (400
|
|
66
|
+
// otherwise). Parent ids are already ~42 chars (`accel-<uuid>`), so naive
|
|
67
|
+
// `${parent}-sub-${tag}-${rand}` overflows (observed 65-72 chars → every
|
|
68
|
+
// sub-agent 400s with 0 usage). Truncate the parent portion to fit, keeping
|
|
69
|
+
// the tag + rand suffix intact for uniqueness/debuggability.
|
|
70
|
+
// Lineage binding: when truncation is needed, embed an 8-char hash of the
|
|
71
|
+
// full parent id so two distinct parents sharing a prefix still map to
|
|
72
|
+
// distinct child ids (previously they collided and shared provider routing).
|
|
73
|
+
const cleanTag = tag.toLowerCase().slice(0, 16);
|
|
74
|
+
const suffix = `-sub-${cleanTag}-${rand}`;
|
|
75
|
+
const maxParent = Math.max(0, 64 - suffix.length);
|
|
76
|
+
if (parentId.length <= maxParent) return `${parentId}${suffix}`;
|
|
77
|
+
const parentHash = hashSessionPart(parentId);
|
|
78
|
+
const baseLen = Math.max(0, maxParent - 9);
|
|
79
|
+
return `${parentId.slice(0, baseLen)}-${parentHash}${suffix}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Builds a deterministic fixed-subagent session id that fits 64 chars. */
|
|
83
|
+
export function createFixedChildSessionId(parentId: string, name: string): string {
|
|
84
|
+
const suffix = `-sub-${name}`;
|
|
85
|
+
if ((parentId + suffix).length <= 64) return parentId + suffix;
|
|
86
|
+
// Truncate parent first (preserves full tool name for debugging), but embed
|
|
87
|
+
// the parent hash so distinct parents sharing a prefix stay distinct.
|
|
88
|
+
// If still over (very long tool name), truncate the name tail as last resort.
|
|
89
|
+
const maxParent = Math.max(0, 64 - suffix.length);
|
|
90
|
+
if (maxParent > 9) {
|
|
91
|
+
const parentHash = hashSessionPart(parentId);
|
|
92
|
+
return `${parentId.slice(0, maxParent - 9)}-${parentHash}${suffix}`;
|
|
93
|
+
}
|
|
94
|
+
if (maxParent > 0) return parentId.slice(0, maxParent) + suffix;
|
|
95
|
+
return (`${parentId}-sub-${name}`).slice(0, 64);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* True when `child` was derived from `parent` via the child-session helpers:
|
|
100
|
+
* either an untruncated `parent + suffix` prefix, or the embedded parent hash
|
|
101
|
+
* used when truncation was required. Used to validate lineage before pairing
|
|
102
|
+
* provider state across concurrent agents.
|
|
103
|
+
*/
|
|
104
|
+
export function isSessionDescendant(child: string, parent: string): boolean {
|
|
105
|
+
if (!child || !parent) return false;
|
|
106
|
+
if (child.startsWith(parent)) return true;
|
|
107
|
+
return child.includes(hashSessionPart(parent));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Live trace for one spawned worker, keyed by its TrackingID. */
|
|
111
|
+
export interface SubAgentTrace {
|
|
112
|
+
/** Sub-agent TrackingID: system-generated 32-char hex. Displayed as `NAME-{TrackingID}`. */
|
|
113
|
+
trackingId: string;
|
|
114
|
+
name: string;
|
|
115
|
+
sessionId: string;
|
|
116
|
+
parentSessionId: string;
|
|
117
|
+
status: "running" | "done" | "error";
|
|
118
|
+
task: string;
|
|
119
|
+
role?: string;
|
|
120
|
+
model?: string;
|
|
121
|
+
provider?: string;
|
|
122
|
+
turns: number;
|
|
123
|
+
usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
|
|
124
|
+
steps: SubAgentStep[];
|
|
125
|
+
text?: string;
|
|
126
|
+
error?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const subagentTraces = new Map<string, SubAgentTrace>();
|
|
130
|
+
const subagentListeners = new Map<string, Set<(trace: SubAgentTrace) => void>>();
|
|
131
|
+
|
|
132
|
+
function snapshotTrace(t: SubAgentTrace): SubAgentTrace {
|
|
133
|
+
return { ...t, steps: t.steps.map((s) => ({ ...s })), usage: t.usage ? { ...t.usage } : undefined };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function notifyTrace(trackingId: string): void {
|
|
137
|
+
const t = subagentTraces.get(trackingId);
|
|
138
|
+
if (!t) return;
|
|
139
|
+
const listeners = subagentListeners.get(trackingId);
|
|
140
|
+
if (!listeners || listeners.size === 0) return;
|
|
141
|
+
const snap = snapshotTrace(t);
|
|
142
|
+
for (const fn of [...listeners]) {
|
|
143
|
+
try { fn(snap); } catch {}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns a snapshot of one worker's trace by tracking id
|
|
149
|
+
* (`SUBAGENT-NAME-{32hex}` or the raw 32-hex suffix). Used by
|
|
150
|
+
* `agent.track(id)` for realtime inspection.
|
|
151
|
+
*/
|
|
152
|
+
export function getSubAgentTrace(id: string): SubAgentTrace | undefined {
|
|
153
|
+
if (!id) return undefined;
|
|
154
|
+
const direct = subagentTraces.get(id);
|
|
155
|
+
if (direct) return snapshotTrace(direct);
|
|
156
|
+
const lower = id.toLowerCase();
|
|
157
|
+
for (const t of subagentTraces.values()) {
|
|
158
|
+
if (t.trackingId.toLowerCase() === lower) return snapshotTrace(t);
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Lists tracking ids of known worker traces (mainly for `track` misses). */
|
|
164
|
+
export function listSubAgentTraceIds(): string[] {
|
|
165
|
+
return [...subagentTraces.keys()];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Subscribes to live updates of one worker trace. Returns an unsubscribe fn.
|
|
170
|
+
*/
|
|
171
|
+
export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => void): () => void {
|
|
172
|
+
const t = subagentTraces.get(id) ?? [...subagentTraces.values()].find((x) => x.trackingId.toLowerCase() === id.toLowerCase());
|
|
173
|
+
const key = t ? t.trackingId : id;
|
|
174
|
+
let set = subagentListeners.get(key);
|
|
175
|
+
if (!set) {
|
|
176
|
+
set = new Set();
|
|
177
|
+
subagentListeners.set(key, set);
|
|
178
|
+
}
|
|
179
|
+
set.add(fn);
|
|
180
|
+
return () => { subagentListeners.get(key)?.delete(fn); };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Appends a step to a worker trace and notifies live subscribers. */
|
|
184
|
+
export function appendSubAgentStep(trackingId: string, step: SubAgentStep): void {
|
|
185
|
+
const t = subagentTraces.get(trackingId);
|
|
186
|
+
if (!t) return;
|
|
187
|
+
t.steps.push(step);
|
|
188
|
+
t.turns = Math.max(t.turns, step.turn);
|
|
189
|
+
notifyTrace(trackingId);
|
|
62
190
|
}
|
|
63
191
|
|
|
64
192
|
function providerOf(modelStr: string | any | undefined): string | undefined {
|
|
@@ -106,8 +234,8 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
106
234
|
? `Worker-available tools: ${poolNames.join(", ")}. Grant each worker ONLY the tools its task needs via the per-task tools list; omit it for no tools. `
|
|
107
235
|
: "No worker tools are available; omit the per-task tools list. ") +
|
|
108
236
|
timeoutNote + " " +
|
|
109
|
-
"Every entry in tasks MUST include all of: name (
|
|
110
|
-
"Example: {\"tasks\": [{\"name\": \"
|
|
237
|
+
"Every entry in tasks MUST include all of: name (UPPER-KEBAB tag), instructions (system prompt for the worker), task (concrete assignment for the worker). " +
|
|
238
|
+
"Example: {\"tasks\": [{\"name\": \"HBM-PRICING-ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"You are a memory market analyst. Return sourced findings only.\", \"task\": \"Research HBM3E pricing, LTA structures, and supply constraints.\", \"tools\": [\"recent_news\"]}]}",
|
|
111
239
|
input: z.object({
|
|
112
240
|
tasks: z
|
|
113
241
|
.array(
|
|
@@ -115,7 +243,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
115
243
|
name: z
|
|
116
244
|
.string()
|
|
117
245
|
.optional()
|
|
118
|
-
.describe("Unique
|
|
246
|
+
.describe("Unique UPPER-KEBAB role tag dynamically derived from task (e.g. RESEARCH-ANALYST, MARKET-ANALYST, CODE-REVIEWER). Auto-generated when omitted."),
|
|
119
247
|
role: z
|
|
120
248
|
.string()
|
|
121
249
|
.optional()
|
|
@@ -152,7 +280,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
152
280
|
}),
|
|
153
281
|
execute: async ({ tasks }, context) => {
|
|
154
282
|
if (!tasks || tasks.length === 0) {
|
|
155
|
-
return "Error: tasks array is empty. Provide at least one entry shaped like {\"name\": \"
|
|
283
|
+
return "Error: tasks array is empty. Provide at least one entry shaped like {\"name\": \"HBM-PRICING-ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"<system prompt>\", \"task\": \"<concrete assignment>\"}. Fix the arguments and call spawn_subagents again.";
|
|
156
284
|
}
|
|
157
285
|
const repaired = tasks.map((entry: Record<string, unknown>, index: number) => {
|
|
158
286
|
const rec = (entry ?? {}) as Record<string, unknown>;
|
|
@@ -167,7 +295,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
167
295
|
"";
|
|
168
296
|
const nameText =
|
|
169
297
|
(typeof rec["name"] === "string" && (rec["name"] as string).trim()) ||
|
|
170
|
-
`
|
|
298
|
+
`SUBAGENT-${index + 1}`;
|
|
171
299
|
const roleText =
|
|
172
300
|
typeof rec["role"] === "string" ? ((rec["role"] as string).trim() || undefined) : undefined;
|
|
173
301
|
const toolsList = Array.isArray(rec["tools"])
|
|
@@ -183,8 +311,8 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
183
311
|
return (
|
|
184
312
|
`Error: tasks[${invalid}].task is missing and could not be inferred. ` +
|
|
185
313
|
`Received keys: [${keys}]. ` +
|
|
186
|
-
`Each tasks[] entry MUST include task (concrete assignment) plus instructions (system prompt) and name (
|
|
187
|
-
`Example: {\"name\": \"
|
|
314
|
+
`Each tasks[] entry MUST include task (concrete assignment) plus instructions (system prompt) and name (UPPER-KEBAB tag). ` +
|
|
315
|
+
`Example: {\"name\": \"HBM-PRICING-ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"You are a memory market analyst.\", \"task\": \"Research HBM3E pricing.\"}. ` +
|
|
188
316
|
`Fix the entry and call spawn_subagents again.`
|
|
189
317
|
);
|
|
190
318
|
}
|
|
@@ -220,10 +348,15 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
220
348
|
let deduped = sanitized;
|
|
221
349
|
let suffix = 1;
|
|
222
350
|
while (seenNames.has(deduped)) {
|
|
223
|
-
deduped = `${sanitized}
|
|
351
|
+
deduped = `${sanitized}-${suffix++}`;
|
|
224
352
|
}
|
|
225
353
|
seenNames.add(deduped);
|
|
226
354
|
const subagentName = deduped;
|
|
355
|
+
// Stable tracking id: `SUBAGENT-NAME-{32hex}` addresses one worker
|
|
356
|
+
// among many concurrent ones for `agent.track(id)` + session logs.
|
|
357
|
+
const trackingId = createTrackingId();
|
|
358
|
+
const displayId = `${subagentName}-${trackingId}`;
|
|
359
|
+
let workerSessionId: string | undefined;
|
|
227
360
|
|
|
228
361
|
// Fixed developer-configured worker model — never LLM-choosable.
|
|
229
362
|
const chosenModel: any = parentAgent.subagentModel;
|
|
@@ -246,11 +379,119 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
246
379
|
const runWithModel = async (modelToUse: any, apiKey: string | undefined, baseUrl: string | undefined) => {
|
|
247
380
|
if (context?.signal?.aborted) throw new Error("Aborted before spawn");
|
|
248
381
|
const childSessionId = createChildSessionId(parentAgent.sessionId, subagentName);
|
|
382
|
+
workerSessionId = childSessionId;
|
|
383
|
+
// Register the live trace BEFORE the run so `agent.track(id)`
|
|
384
|
+
// sees every step in realtime, even while the worker runs.
|
|
385
|
+
const trace: SubAgentTrace = {
|
|
386
|
+
trackingId,
|
|
387
|
+
name: subagentName,
|
|
388
|
+
sessionId: childSessionId,
|
|
389
|
+
parentSessionId: parentAgent.sessionId,
|
|
390
|
+
status: "running",
|
|
391
|
+
task: t.task,
|
|
392
|
+
role: t.role,
|
|
393
|
+
turns: 0,
|
|
394
|
+
steps: [],
|
|
395
|
+
};
|
|
396
|
+
subagentTraces.set(trackingId, trace);
|
|
397
|
+
subagentTraces.set(displayId, trace);
|
|
398
|
+
try {
|
|
399
|
+
const parentTraces = ((parentAgent as any).subagentTraces ??= {}) as Record<string, SubAgentTrace>;
|
|
400
|
+
parentTraces[trackingId] = trace;
|
|
401
|
+
} catch {}
|
|
402
|
+
const persistParent = () => {
|
|
403
|
+
try { (parentAgent as any).persistNow?.(); } catch {}
|
|
404
|
+
};
|
|
405
|
+
const onWorkerTurn = (turn: { turns: number; text?: string; thinking?: string; toolCalls?: Array<{ name: string }>; toolResults?: Array<{ name: string; isError?: boolean }> }) => {
|
|
406
|
+
try {
|
|
407
|
+
liveTurn = turn.turns;
|
|
408
|
+
// Finalize any partial streaming entry for this turn, then
|
|
409
|
+
// log structural steps (tool calls/results) as before.
|
|
410
|
+
const timestamp = Date.now();
|
|
411
|
+
const partialEntry = findPartial(liveTurn);
|
|
412
|
+
if (partialEntry) {
|
|
413
|
+
if (turn.text) partialEntry.text = turn.text;
|
|
414
|
+
if (turn.thinking) partialEntry.thinking = turn.thinking;
|
|
415
|
+
delete (partialEntry as any).partial;
|
|
416
|
+
} else if (turn.text) {
|
|
417
|
+
trace.steps.push({ turn: turn.turns, type: "assistant", text: turn.text.slice(0, 2000), timestamp });
|
|
418
|
+
}
|
|
419
|
+
for (const tc of turn.toolCalls ?? []) {
|
|
420
|
+
trace.steps.push({ turn: turn.turns, type: "tool_call", name: tc.name, timestamp });
|
|
421
|
+
}
|
|
422
|
+
for (const tr of turn.toolResults ?? []) {
|
|
423
|
+
trace.steps.push({ turn: turn.turns, type: "tool_result", name: tr.name, isError: tr.isError, timestamp });
|
|
424
|
+
}
|
|
425
|
+
trace.turns = Math.max(trace.turns, turn.turns);
|
|
426
|
+
notifyTrace(trackingId);
|
|
427
|
+
persistParent();
|
|
428
|
+
} catch {}
|
|
429
|
+
};
|
|
430
|
+
// Realtime thinking/text: the worker is streamed internally and
|
|
431
|
+
// every delta lands in the trace (partial entry, updated in
|
|
432
|
+
// place) so `agent.track(id)` shows generation as it happens —
|
|
433
|
+
// not just at turn end. Deltas also ride to the parent stream via
|
|
434
|
+
// `context.onSubagentEvent` (streaming parent runs only).
|
|
435
|
+
let liveTurn = 1;
|
|
436
|
+
let partialText = "";
|
|
437
|
+
let partialThinking = "";
|
|
438
|
+
const findPartial = (turn: number): SubAgentStep | undefined => {
|
|
439
|
+
for (let i = trace.steps.length - 1; i >= 0; i--) {
|
|
440
|
+
const s = trace.steps[i]!;
|
|
441
|
+
if (s.turn === turn && s.partial) return s;
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
const forwardDelta = (delta?: string, thinkingDelta?: string) => {
|
|
446
|
+
try {
|
|
447
|
+
context?.onSubagentEvent?.({
|
|
448
|
+
trackingId: displayId,
|
|
449
|
+
delta,
|
|
450
|
+
thinkingDelta,
|
|
451
|
+
partialText: partialText || undefined,
|
|
452
|
+
partialThinking: partialThinking || undefined,
|
|
453
|
+
});
|
|
454
|
+
} catch {}
|
|
455
|
+
};
|
|
456
|
+
const onWorkerDelta = (delta?: string, thinkingDelta?: string) => {
|
|
457
|
+
try {
|
|
458
|
+
if (delta) {
|
|
459
|
+
partialText = (partialText + delta).slice(-10000);
|
|
460
|
+
let entry = findPartial(liveTurn);
|
|
461
|
+
if (!entry) {
|
|
462
|
+
entry = { turn: liveTurn, type: "assistant", text: "", partial: true, timestamp: Date.now() };
|
|
463
|
+
trace.steps.push(entry);
|
|
464
|
+
}
|
|
465
|
+
entry.text = partialText;
|
|
466
|
+
entry.timestamp = Date.now();
|
|
467
|
+
}
|
|
468
|
+
if (thinkingDelta) {
|
|
469
|
+
partialThinking = (partialThinking + thinkingDelta).slice(-10000);
|
|
470
|
+
let entry = findPartial(liveTurn);
|
|
471
|
+
if (!entry) {
|
|
472
|
+
entry = { turn: liveTurn, type: "assistant", text: "", partial: true, timestamp: Date.now() };
|
|
473
|
+
trace.steps.push(entry);
|
|
474
|
+
}
|
|
475
|
+
entry.thinking = partialThinking;
|
|
476
|
+
entry.timestamp = Date.now();
|
|
477
|
+
}
|
|
478
|
+
notifyTrace(trackingId);
|
|
479
|
+
forwardDelta(delta, thinkingDelta);
|
|
480
|
+
} catch {}
|
|
481
|
+
};
|
|
249
482
|
// Grant ONLY the Main Agent-selected subset from the developer pool. Unknown names are dropped.
|
|
483
|
+
// Matching is case/format-insensitive (same rules as tool execution): the model may emit
|
|
484
|
+
// "RECENT_NEWS" or "recent news" for a registered "recent_news" tool.
|
|
485
|
+
const poolByNormalized = new Map<string, ToolDefinition>();
|
|
486
|
+
for (const [key, def] of Object.entries(toolPool)) {
|
|
487
|
+
poolByNormalized.set(normalizeToolName(key), def);
|
|
488
|
+
const declared = (def as ToolDefinition)?.name;
|
|
489
|
+
if (declared) poolByNormalized.set(normalizeToolName(declared), def);
|
|
490
|
+
}
|
|
250
491
|
const grantedTools: Record<string, ToolDefinition> = {};
|
|
251
492
|
for (const toolName of (t as any).tools ?? []) {
|
|
252
|
-
const pooled = toolPool[toolName];
|
|
253
|
-
if (pooled) grantedTools[toolName] = pooled;
|
|
493
|
+
const pooled = toolPool[toolName] ?? poolByNormalized.get(normalizeToolName(String(toolName)));
|
|
494
|
+
if (pooled) grantedTools[(pooled as ToolDefinition).name || toolName] = pooled;
|
|
254
495
|
}
|
|
255
496
|
// Timeout: >0 fixed for every worker; 0 = no limit; -1 = per-task timeoutMs from the Main Agent.
|
|
256
497
|
const workerTimeout = cfgTimeout > 0 ? cfgTimeout : cfgTimeout === -1 && (t as any).timeoutMs > 0 ? (t as any).timeoutMs : 0;
|
|
@@ -270,22 +511,54 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
270
511
|
headers: parentAgent.customHeaders,
|
|
271
512
|
maxTurns: parentAgent.maxTurns,
|
|
272
513
|
});
|
|
514
|
+
// Lineage binding: child records its parent session so persisted
|
|
515
|
+
// snapshots and routing can tell siblings apart even when ids
|
|
516
|
+
// are truncated to 64 chars.
|
|
517
|
+
try {
|
|
518
|
+
(subAgent as any).parentSessionId = parentAgent.sessionId;
|
|
519
|
+
} catch {}
|
|
273
520
|
let abortListener: (() => void) | null = null;
|
|
274
|
-
|
|
521
|
+
// Per-worker controller: the parent signal alone cannot stop a
|
|
522
|
+
// worker that hits its own timeout, so the timeout path aborts
|
|
523
|
+
// the worker explicitly instead of leaving it running unseen.
|
|
524
|
+
const workerController = new AbortController();
|
|
525
|
+
const parentSignal = context?.signal;
|
|
526
|
+
const forwardParentAbort = () => {
|
|
527
|
+
try {
|
|
528
|
+
workerController.abort((parentSignal as any)?.reason);
|
|
529
|
+
} catch {
|
|
530
|
+
try { workerController.abort(); } catch {}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
if (parentSignal?.aborted) forwardParentAbort();
|
|
534
|
+
else parentSignal?.addEventListener("abort", forwardParentAbort, { once: true });
|
|
535
|
+
const abortPromise = parentSignal
|
|
275
536
|
? new Promise<never>((_, reject) => {
|
|
276
537
|
const onAbort = () => reject(new Error("Sub-agent aborted via parent signal"));
|
|
277
538
|
abortListener = onAbort;
|
|
278
|
-
if (
|
|
279
|
-
else
|
|
539
|
+
if (parentSignal.aborted) reject(new Error("Sub-agent aborted via parent signal"));
|
|
540
|
+
else parentSignal.addEventListener("abort", onAbort, { once: true });
|
|
280
541
|
})
|
|
281
542
|
: null;
|
|
282
543
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
283
544
|
const timeoutPromise = workerTimeout > 0
|
|
284
545
|
? new Promise<never>((_, reject) => {
|
|
285
|
-
timeoutId = setTimeout(() =>
|
|
546
|
+
timeoutId = setTimeout(() => {
|
|
547
|
+
try {
|
|
548
|
+
workerController.abort(new Error(`Sub-agent ${subagentName} timed out after ${workerTimeout}ms`));
|
|
549
|
+
} catch {}
|
|
550
|
+
reject(new Error(`Sub-agent ${subagentName} timed out after ${workerTimeout}ms`));
|
|
551
|
+
}, workerTimeout);
|
|
286
552
|
})
|
|
287
553
|
: null;
|
|
288
|
-
|
|
554
|
+
// Streamed internally (consumed to completion): same final result
|
|
555
|
+
// as run(), but text/thinking deltas flow into the trace live.
|
|
556
|
+
const workerStream = subAgent.stream(t.task, { signal: workerController.signal, onTurn: onWorkerTurn } as any);
|
|
557
|
+
const textListener = (e: any) => onWorkerDelta(e.delta, undefined);
|
|
558
|
+
const thinkingListener = (e: any) => onWorkerDelta(undefined, e.thinkingDelta);
|
|
559
|
+
workerStream.on("text_delta", textListener);
|
|
560
|
+
workerStream.on("thinking_delta", thinkingListener);
|
|
561
|
+
const taskPromise = workerStream.result();
|
|
289
562
|
try {
|
|
290
563
|
const racers: Promise<unknown>[] = [taskPromise as unknown as Promise<unknown>];
|
|
291
564
|
if (abortPromise) racers.push(abortPromise);
|
|
@@ -294,9 +567,13 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
294
567
|
return res;
|
|
295
568
|
} finally {
|
|
296
569
|
if (timeoutId) clearTimeout(timeoutId);
|
|
297
|
-
|
|
298
|
-
|
|
570
|
+
try { workerStream.cancel(); } catch {}
|
|
571
|
+
try { workerStream.off("text_delta", textListener); } catch {}
|
|
572
|
+
try { workerStream.off("thinking_delta", thinkingListener); } catch {}
|
|
573
|
+
if (abortListener && parentSignal) {
|
|
574
|
+
try { parentSignal.removeEventListener("abort", abortListener as any); } catch {}
|
|
299
575
|
}
|
|
576
|
+
try { parentSignal?.removeEventListener("abort", forwardParentAbort); } catch {}
|
|
300
577
|
}
|
|
301
578
|
};
|
|
302
579
|
|
|
@@ -309,8 +586,24 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
309
586
|
? rawText
|
|
310
587
|
: `[Sub-agent ${subagentName} produced no output (empty response).]`;
|
|
311
588
|
|
|
589
|
+
const live = subagentTraces.get(trackingId);
|
|
590
|
+
if (live) {
|
|
591
|
+
live.status = "done";
|
|
592
|
+
live.model = (res as any).model;
|
|
593
|
+
live.provider = (res as any).provider;
|
|
594
|
+
live.turns = (res as any).turns ?? live.turns;
|
|
595
|
+
live.usage = (res as any).usage
|
|
596
|
+
? { inputTokens: (res as any).usage.inputTokens ?? 0, outputTokens: (res as any).usage.outputTokens ?? 0, totalTokens: (res as any).usage.totalTokens ?? 0 }
|
|
597
|
+
: live.usage;
|
|
598
|
+
live.text = subagentText;
|
|
599
|
+
notifyTrace(trackingId);
|
|
600
|
+
}
|
|
601
|
+
try { (parentAgent as any).persistNow?.(); } catch {}
|
|
312
602
|
const metadata: SubAgentExecutionMetadata = {
|
|
313
603
|
name: subagentName,
|
|
604
|
+
trackingId: displayId,
|
|
605
|
+
sessionId: workerSessionId,
|
|
606
|
+
parentSessionId: parentAgent.sessionId,
|
|
314
607
|
role: t.role,
|
|
315
608
|
task: t.task,
|
|
316
609
|
model: (res as any).model,
|
|
@@ -324,15 +617,26 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
324
617
|
thinking: (res as any).thinking,
|
|
325
618
|
toolCalls: (res as any).toolCalls,
|
|
326
619
|
raw: (res as any).raw,
|
|
620
|
+
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
327
621
|
isError: false,
|
|
328
622
|
};
|
|
329
|
-
return { name: subagentName, text: subagentText, metadata };
|
|
623
|
+
return { name: subagentName, trackingId: displayId, text: subagentText, metadata };
|
|
330
624
|
} catch (err: any) {
|
|
331
625
|
const durationMs = Date.now() - startTime;
|
|
332
626
|
const errorMessage = err?.message || String(err);
|
|
333
627
|
const prov = attemptedProvider || providerOf(chosenModelStrForProvider) || providerOf(chosenModel as any) || "unknown";
|
|
628
|
+
const live = subagentTraces.get(trackingId);
|
|
629
|
+
if (live) {
|
|
630
|
+
live.status = "error";
|
|
631
|
+
live.error = errorMessage;
|
|
632
|
+
notifyTrace(trackingId);
|
|
633
|
+
}
|
|
634
|
+
try { (parentAgent as any).persistNow?.(); } catch {}
|
|
334
635
|
const metadata: SubAgentExecutionMetadata = {
|
|
335
636
|
name: subagentName,
|
|
637
|
+
trackingId: displayId,
|
|
638
|
+
sessionId: workerSessionId,
|
|
639
|
+
parentSessionId: parentAgent.sessionId,
|
|
336
640
|
role: t.role,
|
|
337
641
|
task: t.task,
|
|
338
642
|
model: typeof chosenModel === "string" ? chosenModel : ((chosenModel as any)?.model ?? (chosenModel as any)?.id ?? "unknown"),
|
|
@@ -341,10 +645,11 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
341
645
|
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
342
646
|
turns: 0,
|
|
343
647
|
text: `Error executing sub-agent ${t.name}: ${errorMessage}`,
|
|
648
|
+
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
344
649
|
isError: true,
|
|
345
650
|
error: errorMessage,
|
|
346
651
|
};
|
|
347
|
-
return { name: subagentName, text: `Error executing sub-agent ${t.name}: ${errorMessage}`, metadata };
|
|
652
|
+
return { name: subagentName, trackingId: displayId, text: `Error executing sub-agent ${t.name}: ${errorMessage}`, metadata };
|
|
348
653
|
}
|
|
349
654
|
})
|
|
350
655
|
);
|
|
@@ -387,7 +692,7 @@ export function agentToTool(
|
|
|
387
692
|
const rawName =
|
|
388
693
|
("name" in input && (input as any).name) ||
|
|
389
694
|
agentInstance.name ||
|
|
390
|
-
`
|
|
695
|
+
`sub-agent-${Math.random().toString(36).slice(2, 7)}`;
|
|
391
696
|
const name = sanitizeToolName(rawName);
|
|
392
697
|
const description =
|
|
393
698
|
("description" in input && (input as any).description) ||
|
|
@@ -402,13 +707,112 @@ export function agentToTool(
|
|
|
402
707
|
}),
|
|
403
708
|
execute: async ({ task }, ctx) => {
|
|
404
709
|
const startTime = Date.now();
|
|
710
|
+
const trackingId = createTrackingId();
|
|
711
|
+
const displayId = `${name}-${trackingId}`;
|
|
405
712
|
try {
|
|
406
713
|
const parentSessionId = ctx?.sessionId || agentInstance.sessionId || "session";
|
|
407
|
-
const subSessionId =
|
|
408
|
-
const
|
|
714
|
+
const subSessionId = createFixedChildSessionId(parentSessionId, sanitizeToolName(name));
|
|
715
|
+
const trace: SubAgentTrace = {
|
|
716
|
+
trackingId,
|
|
717
|
+
name,
|
|
718
|
+
sessionId: subSessionId,
|
|
719
|
+
parentSessionId,
|
|
720
|
+
status: "running",
|
|
721
|
+
task,
|
|
722
|
+
turns: 0,
|
|
723
|
+
steps: [],
|
|
724
|
+
};
|
|
725
|
+
subagentTraces.set(trackingId, trace);
|
|
726
|
+
subagentTraces.set(displayId, trace);
|
|
727
|
+
const onWorkerTurn = (turn: { turns: number; text?: string; thinking?: string; toolCalls?: Array<{ name: string }>; toolResults?: Array<{ name: string; isError?: boolean }> }) => {
|
|
728
|
+
try {
|
|
729
|
+
liveTurn = turn.turns;
|
|
730
|
+
const timestamp = Date.now();
|
|
731
|
+
const partialEntry = findPartial(liveTurn);
|
|
732
|
+
if (partialEntry) {
|
|
733
|
+
if (turn.text) partialEntry.text = turn.text;
|
|
734
|
+
if (turn.thinking) partialEntry.thinking = turn.thinking;
|
|
735
|
+
delete (partialEntry as any).partial;
|
|
736
|
+
} else if (turn.text) {
|
|
737
|
+
trace.steps.push({ turn: turn.turns, type: "assistant", text: turn.text.slice(0, 2000), timestamp });
|
|
738
|
+
}
|
|
739
|
+
for (const tc of turn.toolCalls ?? []) trace.steps.push({ turn: turn.turns, type: "tool_call", name: tc.name, timestamp });
|
|
740
|
+
for (const tr of turn.toolResults ?? []) trace.steps.push({ turn: turn.turns, type: "tool_result", name: tr.name, isError: tr.isError, timestamp });
|
|
741
|
+
trace.turns = Math.max(trace.turns, turn.turns);
|
|
742
|
+
notifyTrace(trackingId);
|
|
743
|
+
} catch {}
|
|
744
|
+
};
|
|
745
|
+
let liveTurn = 1;
|
|
746
|
+
let partialText = "";
|
|
747
|
+
let partialThinking = "";
|
|
748
|
+
const findPartial = (turn: number): SubAgentStep | undefined => {
|
|
749
|
+
for (let i = trace.steps.length - 1; i >= 0; i--) {
|
|
750
|
+
const s = trace.steps[i]!;
|
|
751
|
+
if (s.turn === turn && s.partial) return s;
|
|
752
|
+
}
|
|
753
|
+
return undefined;
|
|
754
|
+
};
|
|
755
|
+
const onWorkerDelta = (delta?: string, thinkingDelta?: string) => {
|
|
756
|
+
try {
|
|
757
|
+
if (delta) {
|
|
758
|
+
partialText = (partialText + delta).slice(-10000);
|
|
759
|
+
let entry = findPartial(liveTurn);
|
|
760
|
+
if (!entry) {
|
|
761
|
+
entry = { turn: liveTurn, type: "assistant", text: "", partial: true, timestamp: Date.now() };
|
|
762
|
+
trace.steps.push(entry);
|
|
763
|
+
}
|
|
764
|
+
entry.text = partialText;
|
|
765
|
+
entry.timestamp = Date.now();
|
|
766
|
+
}
|
|
767
|
+
if (thinkingDelta) {
|
|
768
|
+
partialThinking = (partialThinking + thinkingDelta).slice(-10000);
|
|
769
|
+
let entry = findPartial(liveTurn);
|
|
770
|
+
if (!entry) {
|
|
771
|
+
entry = { turn: liveTurn, type: "assistant", text: "", partial: true, timestamp: Date.now() };
|
|
772
|
+
trace.steps.push(entry);
|
|
773
|
+
}
|
|
774
|
+
entry.thinking = partialThinking;
|
|
775
|
+
entry.timestamp = Date.now();
|
|
776
|
+
}
|
|
777
|
+
notifyTrace(trackingId);
|
|
778
|
+
try {
|
|
779
|
+
ctx?.onSubagentEvent?.({
|
|
780
|
+
trackingId: displayId,
|
|
781
|
+
delta,
|
|
782
|
+
thinkingDelta,
|
|
783
|
+
partialText: partialText || undefined,
|
|
784
|
+
partialThinking: partialThinking || undefined,
|
|
785
|
+
});
|
|
786
|
+
} catch {}
|
|
787
|
+
} catch {}
|
|
788
|
+
};
|
|
789
|
+
const workerStream = agentInstance.stream(task, { signal: ctx?.signal, sessionId: subSessionId, onTurn: onWorkerTurn } as any);
|
|
790
|
+
const textListener = (e: any) => onWorkerDelta(e.delta, undefined);
|
|
791
|
+
const thinkingListener = (e: any) => onWorkerDelta(undefined, e.thinkingDelta);
|
|
792
|
+
workerStream.on("text_delta", textListener);
|
|
793
|
+
workerStream.on("thinking_delta", thinkingListener);
|
|
794
|
+
let response: any;
|
|
795
|
+
try {
|
|
796
|
+
response = await workerStream.result();
|
|
797
|
+
} finally {
|
|
798
|
+
try { workerStream.off("text_delta", textListener); } catch {}
|
|
799
|
+
try { workerStream.off("thinking_delta", thinkingListener); } catch {}
|
|
800
|
+
}
|
|
409
801
|
const durationMs = Date.now() - startTime;
|
|
802
|
+
const live = subagentTraces.get(trackingId);
|
|
803
|
+
if (live) {
|
|
804
|
+
live.status = "done";
|
|
805
|
+
live.model = response.model;
|
|
806
|
+
live.provider = response.provider;
|
|
807
|
+
live.turns = response.turns ?? live.turns;
|
|
808
|
+
live.text = response.text;
|
|
809
|
+
notifyTrace(trackingId);
|
|
810
|
+
}
|
|
410
811
|
const metadata: SubAgentExecutionMetadata = {
|
|
411
812
|
name,
|
|
813
|
+
trackingId: displayId,
|
|
814
|
+
sessionId: subSessionId,
|
|
815
|
+
parentSessionId,
|
|
412
816
|
task,
|
|
413
817
|
model: response.model,
|
|
414
818
|
provider: response.provider,
|
|
@@ -421,6 +825,7 @@ export function agentToTool(
|
|
|
421
825
|
thinking: response.thinking,
|
|
422
826
|
toolCalls: response.toolCalls,
|
|
423
827
|
raw: response.raw,
|
|
828
|
+
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
424
829
|
isError: false,
|
|
425
830
|
};
|
|
426
831
|
const wrapper: any = {
|
|
@@ -432,8 +837,15 @@ export function agentToTool(
|
|
|
432
837
|
} catch (err: any) {
|
|
433
838
|
const durationMs = Date.now() - startTime;
|
|
434
839
|
const msg = err?.message || String(err);
|
|
840
|
+
const live = subagentTraces.get(trackingId);
|
|
841
|
+
if (live) {
|
|
842
|
+
live.status = "error";
|
|
843
|
+
live.error = msg;
|
|
844
|
+
notifyTrace(trackingId);
|
|
845
|
+
}
|
|
435
846
|
const metadata: SubAgentExecutionMetadata = {
|
|
436
847
|
name,
|
|
848
|
+
trackingId: displayId,
|
|
437
849
|
task,
|
|
438
850
|
model: "unknown",
|
|
439
851
|
provider: "unknown",
|
|
@@ -441,6 +853,7 @@ export function agentToTool(
|
|
|
441
853
|
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
442
854
|
turns: 0,
|
|
443
855
|
text: `Error in ${name}: ${msg}`,
|
|
856
|
+
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
444
857
|
isError: true,
|
|
445
858
|
error: msg,
|
|
446
859
|
};
|
|
@@ -472,7 +885,7 @@ export function buildAgentTools(
|
|
|
472
885
|
let finalName = t.name;
|
|
473
886
|
let n = 1;
|
|
474
887
|
while (tools[finalName]) {
|
|
475
|
-
finalName = `${t.name}
|
|
888
|
+
finalName = `${t.name}-${n++}`;
|
|
476
889
|
}
|
|
477
890
|
tools[finalName] = { ...t, name: finalName };
|
|
478
891
|
}
|