agent-accelerator 0.2.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 +94 -14
- package/package.json +1 -1
- package/src/agent/agent.ts +194 -2
- package/src/agent/delegation.ts +388 -24
- package/src/agent/loop.ts +112 -5
- package/src/index.ts +34 -2
- package/src/models/catalog.ts +4 -3
- package/src/providers.ts +11 -9
- package/src/session/store.ts +806 -0
- package/src/tools/executor.ts +4 -1
- package/src/types/agent.ts +25 -1
- package/src/types/core.ts +2 -1
- package/src/types/response.ts +24 -0
- package/src/types/tool.ts +2 -0
- package/src/utils/session.ts +36 -0
package/src/agent/delegation.ts
CHANGED
|
@@ -5,6 +5,9 @@ import type { ToolDefinition } from "../types/tool.ts";
|
|
|
5
5
|
import type { SubAgentExecutionMetadata } from "../types/response.ts";
|
|
6
6
|
import type { Agent } from "./agent.ts";
|
|
7
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";
|
|
8
11
|
|
|
9
12
|
/** Task descriptor accepted by the automatic `spawn_subagents` tool.
|
|
10
13
|
*
|
|
@@ -26,14 +29,14 @@ export interface DynamicSubagentTask {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function sanitizeXmlTag(raw: string): string {
|
|
29
|
-
let s = raw.toUpperCase().replace(/[^A-Z0-
|
|
30
|
-
if (!/^[A-
|
|
31
|
-
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, "");
|
|
32
35
|
return s.slice(0, 64) || "SUBAGENT";
|
|
33
36
|
}
|
|
34
37
|
function sanitizeToolName(raw: string): string {
|
|
35
38
|
const tag = sanitizeXmlTag(raw);
|
|
36
|
-
return tag.toLowerCase().replace(/[^a-z0-
|
|
39
|
+
return tag.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").slice(0, 64) || "sub-agent";
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
function escapeXml(text: string): string {
|
|
@@ -45,7 +48,7 @@ function escapeXml(text: string): string {
|
|
|
45
48
|
.replace(/'/g, "'");
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
function createChildSessionId(parentId: string, tag: string): string {
|
|
51
|
+
export function createChildSessionId(parentId: string, tag: string): string {
|
|
49
52
|
let rand: string;
|
|
50
53
|
try {
|
|
51
54
|
if (typeof globalThis !== "undefined" && (globalThis as any).crypto?.randomUUID) {
|
|
@@ -64,24 +67,128 @@ function createChildSessionId(parentId: string, tag: string): string {
|
|
|
64
67
|
// `${parent}-sub-${tag}-${rand}` overflows (observed 65-72 chars → every
|
|
65
68
|
// sub-agent 400s with 0 usage). Truncate the parent portion to fit, keeping
|
|
66
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).
|
|
67
73
|
const cleanTag = tag.toLowerCase().slice(0, 16);
|
|
68
74
|
const suffix = `-sub-${cleanTag}-${rand}`;
|
|
69
75
|
const maxParent = Math.max(0, 64 - suffix.length);
|
|
70
|
-
|
|
71
|
-
|
|
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}`;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/** Builds a deterministic fixed-subagent session id that fits 64 chars. */
|
|
75
|
-
function createFixedChildSessionId(parentId: string, name: string): string {
|
|
83
|
+
export function createFixedChildSessionId(parentId: string, name: string): string {
|
|
76
84
|
const suffix = `-sub-${name}`;
|
|
77
85
|
if ((parentId + suffix).length <= 64) return parentId + suffix;
|
|
78
|
-
// Truncate parent first (preserves full tool name for debugging)
|
|
79
|
-
//
|
|
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.
|
|
80
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
|
+
}
|
|
81
94
|
if (maxParent > 0) return parentId.slice(0, maxParent) + suffix;
|
|
82
95
|
return (`${parentId}-sub-${name}`).slice(0, 64);
|
|
83
96
|
}
|
|
84
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);
|
|
190
|
+
}
|
|
191
|
+
|
|
85
192
|
function providerOf(modelStr: string | any | undefined): string | undefined {
|
|
86
193
|
if (!modelStr) return undefined;
|
|
87
194
|
try {
|
|
@@ -127,8 +234,8 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
127
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. `
|
|
128
235
|
: "No worker tools are available; omit the per-task tools list. ") +
|
|
129
236
|
timeoutNote + " " +
|
|
130
|
-
"Every entry in tasks MUST include all of: name (
|
|
131
|
-
"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\"]}]}",
|
|
132
239
|
input: z.object({
|
|
133
240
|
tasks: z
|
|
134
241
|
.array(
|
|
@@ -136,7 +243,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
136
243
|
name: z
|
|
137
244
|
.string()
|
|
138
245
|
.optional()
|
|
139
|
-
.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."),
|
|
140
247
|
role: z
|
|
141
248
|
.string()
|
|
142
249
|
.optional()
|
|
@@ -173,7 +280,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
173
280
|
}),
|
|
174
281
|
execute: async ({ tasks }, context) => {
|
|
175
282
|
if (!tasks || tasks.length === 0) {
|
|
176
|
-
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.";
|
|
177
284
|
}
|
|
178
285
|
const repaired = tasks.map((entry: Record<string, unknown>, index: number) => {
|
|
179
286
|
const rec = (entry ?? {}) as Record<string, unknown>;
|
|
@@ -188,7 +295,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
188
295
|
"";
|
|
189
296
|
const nameText =
|
|
190
297
|
(typeof rec["name"] === "string" && (rec["name"] as string).trim()) ||
|
|
191
|
-
`
|
|
298
|
+
`SUBAGENT-${index + 1}`;
|
|
192
299
|
const roleText =
|
|
193
300
|
typeof rec["role"] === "string" ? ((rec["role"] as string).trim() || undefined) : undefined;
|
|
194
301
|
const toolsList = Array.isArray(rec["tools"])
|
|
@@ -204,8 +311,8 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
204
311
|
return (
|
|
205
312
|
`Error: tasks[${invalid}].task is missing and could not be inferred. ` +
|
|
206
313
|
`Received keys: [${keys}]. ` +
|
|
207
|
-
`Each tasks[] entry MUST include task (concrete assignment) plus instructions (system prompt) and name (
|
|
208
|
-
`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.\"}. ` +
|
|
209
316
|
`Fix the entry and call spawn_subagents again.`
|
|
210
317
|
);
|
|
211
318
|
}
|
|
@@ -241,10 +348,15 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
241
348
|
let deduped = sanitized;
|
|
242
349
|
let suffix = 1;
|
|
243
350
|
while (seenNames.has(deduped)) {
|
|
244
|
-
deduped = `${sanitized}
|
|
351
|
+
deduped = `${sanitized}-${suffix++}`;
|
|
245
352
|
}
|
|
246
353
|
seenNames.add(deduped);
|
|
247
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;
|
|
248
360
|
|
|
249
361
|
// Fixed developer-configured worker model — never LLM-choosable.
|
|
250
362
|
const chosenModel: any = parentAgent.subagentModel;
|
|
@@ -267,6 +379,106 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
267
379
|
const runWithModel = async (modelToUse: any, apiKey: string | undefined, baseUrl: string | undefined) => {
|
|
268
380
|
if (context?.signal?.aborted) throw new Error("Aborted before spawn");
|
|
269
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
|
+
};
|
|
270
482
|
// Grant ONLY the Main Agent-selected subset from the developer pool. Unknown names are dropped.
|
|
271
483
|
// Matching is case/format-insensitive (same rules as tool execution): the model may emit
|
|
272
484
|
// "RECENT_NEWS" or "recent news" for a registered "recent_news" tool.
|
|
@@ -299,6 +511,12 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
299
511
|
headers: parentAgent.customHeaders,
|
|
300
512
|
maxTurns: parentAgent.maxTurns,
|
|
301
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 {}
|
|
302
520
|
let abortListener: (() => void) | null = null;
|
|
303
521
|
// Per-worker controller: the parent signal alone cannot stop a
|
|
304
522
|
// worker that hits its own timeout, so the timeout path aborts
|
|
@@ -333,7 +551,14 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
333
551
|
}, workerTimeout);
|
|
334
552
|
})
|
|
335
553
|
: null;
|
|
336
|
-
|
|
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();
|
|
337
562
|
try {
|
|
338
563
|
const racers: Promise<unknown>[] = [taskPromise as unknown as Promise<unknown>];
|
|
339
564
|
if (abortPromise) racers.push(abortPromise);
|
|
@@ -342,6 +567,9 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
342
567
|
return res;
|
|
343
568
|
} finally {
|
|
344
569
|
if (timeoutId) clearTimeout(timeoutId);
|
|
570
|
+
try { workerStream.cancel(); } catch {}
|
|
571
|
+
try { workerStream.off("text_delta", textListener); } catch {}
|
|
572
|
+
try { workerStream.off("thinking_delta", thinkingListener); } catch {}
|
|
345
573
|
if (abortListener && parentSignal) {
|
|
346
574
|
try { parentSignal.removeEventListener("abort", abortListener as any); } catch {}
|
|
347
575
|
}
|
|
@@ -358,8 +586,24 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
358
586
|
? rawText
|
|
359
587
|
: `[Sub-agent ${subagentName} produced no output (empty response).]`;
|
|
360
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 {}
|
|
361
602
|
const metadata: SubAgentExecutionMetadata = {
|
|
362
603
|
name: subagentName,
|
|
604
|
+
trackingId: displayId,
|
|
605
|
+
sessionId: workerSessionId,
|
|
606
|
+
parentSessionId: parentAgent.sessionId,
|
|
363
607
|
role: t.role,
|
|
364
608
|
task: t.task,
|
|
365
609
|
model: (res as any).model,
|
|
@@ -373,15 +617,26 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
373
617
|
thinking: (res as any).thinking,
|
|
374
618
|
toolCalls: (res as any).toolCalls,
|
|
375
619
|
raw: (res as any).raw,
|
|
620
|
+
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
376
621
|
isError: false,
|
|
377
622
|
};
|
|
378
|
-
return { name: subagentName, text: subagentText, metadata };
|
|
623
|
+
return { name: subagentName, trackingId: displayId, text: subagentText, metadata };
|
|
379
624
|
} catch (err: any) {
|
|
380
625
|
const durationMs = Date.now() - startTime;
|
|
381
626
|
const errorMessage = err?.message || String(err);
|
|
382
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 {}
|
|
383
635
|
const metadata: SubAgentExecutionMetadata = {
|
|
384
636
|
name: subagentName,
|
|
637
|
+
trackingId: displayId,
|
|
638
|
+
sessionId: workerSessionId,
|
|
639
|
+
parentSessionId: parentAgent.sessionId,
|
|
385
640
|
role: t.role,
|
|
386
641
|
task: t.task,
|
|
387
642
|
model: typeof chosenModel === "string" ? chosenModel : ((chosenModel as any)?.model ?? (chosenModel as any)?.id ?? "unknown"),
|
|
@@ -390,10 +645,11 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
390
645
|
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
391
646
|
turns: 0,
|
|
392
647
|
text: `Error executing sub-agent ${t.name}: ${errorMessage}`,
|
|
648
|
+
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
393
649
|
isError: true,
|
|
394
650
|
error: errorMessage,
|
|
395
651
|
};
|
|
396
|
-
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 };
|
|
397
653
|
}
|
|
398
654
|
})
|
|
399
655
|
);
|
|
@@ -436,7 +692,7 @@ export function agentToTool(
|
|
|
436
692
|
const rawName =
|
|
437
693
|
("name" in input && (input as any).name) ||
|
|
438
694
|
agentInstance.name ||
|
|
439
|
-
`
|
|
695
|
+
`sub-agent-${Math.random().toString(36).slice(2, 7)}`;
|
|
440
696
|
const name = sanitizeToolName(rawName);
|
|
441
697
|
const description =
|
|
442
698
|
("description" in input && (input as any).description) ||
|
|
@@ -451,13 +707,112 @@ export function agentToTool(
|
|
|
451
707
|
}),
|
|
452
708
|
execute: async ({ task }, ctx) => {
|
|
453
709
|
const startTime = Date.now();
|
|
710
|
+
const trackingId = createTrackingId();
|
|
711
|
+
const displayId = `${name}-${trackingId}`;
|
|
454
712
|
try {
|
|
455
713
|
const parentSessionId = ctx?.sessionId || agentInstance.sessionId || "session";
|
|
456
714
|
const subSessionId = createFixedChildSessionId(parentSessionId, sanitizeToolName(name));
|
|
457
|
-
const
|
|
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
|
+
}
|
|
458
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
|
+
}
|
|
459
811
|
const metadata: SubAgentExecutionMetadata = {
|
|
460
812
|
name,
|
|
813
|
+
trackingId: displayId,
|
|
814
|
+
sessionId: subSessionId,
|
|
815
|
+
parentSessionId,
|
|
461
816
|
task,
|
|
462
817
|
model: response.model,
|
|
463
818
|
provider: response.provider,
|
|
@@ -470,6 +825,7 @@ export function agentToTool(
|
|
|
470
825
|
thinking: response.thinking,
|
|
471
826
|
toolCalls: response.toolCalls,
|
|
472
827
|
raw: response.raw,
|
|
828
|
+
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
473
829
|
isError: false,
|
|
474
830
|
};
|
|
475
831
|
const wrapper: any = {
|
|
@@ -481,8 +837,15 @@ export function agentToTool(
|
|
|
481
837
|
} catch (err: any) {
|
|
482
838
|
const durationMs = Date.now() - startTime;
|
|
483
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
|
+
}
|
|
484
846
|
const metadata: SubAgentExecutionMetadata = {
|
|
485
847
|
name,
|
|
848
|
+
trackingId: displayId,
|
|
486
849
|
task,
|
|
487
850
|
model: "unknown",
|
|
488
851
|
provider: "unknown",
|
|
@@ -490,6 +853,7 @@ export function agentToTool(
|
|
|
490
853
|
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
491
854
|
turns: 0,
|
|
492
855
|
text: `Error in ${name}: ${msg}`,
|
|
856
|
+
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
493
857
|
isError: true,
|
|
494
858
|
error: msg,
|
|
495
859
|
};
|
|
@@ -521,7 +885,7 @@ export function buildAgentTools(
|
|
|
521
885
|
let finalName = t.name;
|
|
522
886
|
let n = 1;
|
|
523
887
|
while (tools[finalName]) {
|
|
524
|
-
finalName = `${t.name}
|
|
888
|
+
finalName = `${t.name}-${n++}`;
|
|
525
889
|
}
|
|
526
890
|
tools[finalName] = { ...t, name: finalName };
|
|
527
891
|
}
|