agent-accelerator 0.3.0 → 0.4.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/package.json +1 -1
- package/src/agent/agent.ts +830 -116
- package/src/agent/context.ts +35 -0
- package/src/agent/delegation.ts +390 -36
- package/src/agent/loop.ts +711 -109
- package/src/agent/subagent.ts +59 -0
- package/src/index.ts +154 -59
- package/src/models/catalog-cache.ts +68 -25
- package/src/models/catalog.ts +97 -18
- package/src/providers/google.ts +99 -39
- package/src/providers/openai-compat.ts +140 -80
- package/src/providers/openai.ts +149 -69
- package/src/providers/openrouter.ts +106 -71
- package/src/providers/registry.ts +16 -12
- package/src/providers/shared.ts +132 -0
- package/src/providers.ts +3 -4
- package/src/session/store.ts +512 -33
- package/src/streaming/event-stream.ts +156 -41
- package/src/tools/executor.ts +248 -15
- package/src/tools/tool.ts +147 -3
- package/src/types/agent.ts +229 -2
- package/src/types/errors.ts +133 -0
- package/src/types/model.ts +1 -1
- package/src/types/response.ts +126 -3
- package/src/types/tool.ts +69 -0
- package/src/update-models.ts +8 -2
- package/src/utils/cache.ts +68 -1
- package/src/utils/env.ts +2 -6
- package/src/utils/headers.ts +153 -19
- package/src/utils/retry.ts +27 -13
- package/src/utils/session.ts +284 -0
- package/src/providers/openrouter-responses.ts +0 -949
- package/src/types/provider-payloads.ts +0 -352
package/src/agent/delegation.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { tool } from "../tools/tool.ts";
|
|
3
3
|
import { normalizeToolName } from "../tools/executor.ts";
|
|
4
|
+
import { escapeXml } from "../utils/serialization.ts";
|
|
4
5
|
import type { ToolDefinition } from "../types/tool.ts";
|
|
5
6
|
import type { SubAgentExecutionMetadata } from "../types/response.ts";
|
|
6
7
|
import type { Agent } from "./agent.ts";
|
|
8
|
+
import type { Budget, BudgetState } from "../types/agent.ts";
|
|
9
|
+
import { BudgetExceededError } from "../types/errors.ts";
|
|
7
10
|
import { resolveModel } from "../providers/registry.ts";
|
|
8
11
|
import { hashSessionPart } from "../utils/session.ts";
|
|
9
12
|
import { createTrackingId } from "../utils/session.ts";
|
|
13
|
+
import { nowMs } from "../utils/session.ts";
|
|
14
|
+
import { newRunId } from "../utils/session.ts";
|
|
15
|
+
import { newTurnId } from "../utils/session.ts";
|
|
10
16
|
import type { SubAgentStep } from "../types/response.ts";
|
|
11
17
|
|
|
18
|
+
export { escapeXml };
|
|
19
|
+
|
|
12
20
|
/** Task descriptor accepted by the automatic `spawn_subagents` tool.
|
|
13
21
|
*
|
|
14
22
|
* The Main Agent controls prompts (`name`/`role`/`instructions`/`task`), which
|
|
@@ -39,15 +47,6 @@ function sanitizeToolName(raw: string): string {
|
|
|
39
47
|
return tag.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").slice(0, 64) || "sub-agent";
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
function escapeXml(text: string): string {
|
|
43
|
-
return text
|
|
44
|
-
.replace(/&/g, "&")
|
|
45
|
-
.replace(/</g, "<")
|
|
46
|
-
.replace(/>/g, ">")
|
|
47
|
-
.replace(/"/g, """)
|
|
48
|
-
.replace(/'/g, "'");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
50
|
export function createChildSessionId(parentId: string, tag: string): string {
|
|
52
51
|
let rand: string;
|
|
53
52
|
try {
|
|
@@ -103,8 +102,11 @@ export function createFixedChildSessionId(parentId: string, name: string): strin
|
|
|
103
102
|
*/
|
|
104
103
|
export function isSessionDescendant(child: string, parent: string): boolean {
|
|
105
104
|
if (!child || !parent) return false;
|
|
106
|
-
if (child
|
|
107
|
-
|
|
105
|
+
if (child === parent) return true;
|
|
106
|
+
if (child.startsWith(parent + "-")) return true;
|
|
107
|
+
// Q-34: hash includes() is collision-prone; require delimited match to reduce spoofing. Explicit {rootSessionId,parentSessionId} lineage is Q-61 future.
|
|
108
|
+
const h = hashSessionPart(parent);
|
|
109
|
+
return child.includes(`-${h}-`) || child.endsWith(`-${h}`);
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/** Live trace for one spawned worker, keyed by its TrackingID. */
|
|
@@ -129,6 +131,116 @@ export interface SubAgentTrace {
|
|
|
129
131
|
const subagentTraces = new Map<string, SubAgentTrace>();
|
|
130
132
|
const subagentListeners = new Map<string, Set<(trace: SubAgentTrace) => void>>();
|
|
131
133
|
|
|
134
|
+
/** Max trace keys held (LRU, evicts oldest whole trace on insert) (Q-30). */
|
|
135
|
+
export const MAX_SUBAGENT_TRACES = 500;
|
|
136
|
+
/** Trace TTL: 1h wall-clock, lazily expired on read (Q-30). */
|
|
137
|
+
export const SUBAGENT_TRACE_TTL_MS = 60 * 60 * 1000;
|
|
138
|
+
/** Max steps retained per trace; oldest steps are dropped beyond this (Q-30). */
|
|
139
|
+
export const MAX_SUBAGENT_STEPS_PER_TRACE = 100;
|
|
140
|
+
/** Live subscriptions auto-expire after 5min (Q-30). */
|
|
141
|
+
export const SUBAGENT_SUBSCRIBE_TTL_MS = 5 * 60 * 1000;
|
|
142
|
+
|
|
143
|
+
/** Last-write wall-clock per canonical trackingId (drives TTL; reads do not extend it). */
|
|
144
|
+
const traceTouchedAt = new Map<string, number>();
|
|
145
|
+
|
|
146
|
+
function canonicalOf(trace: SubAgentTrace): string {
|
|
147
|
+
return trace.trackingId;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function deleteTraceByCanonical(canonical: string): void {
|
|
151
|
+
for (const [key, t] of [...subagentTraces]) {
|
|
152
|
+
if (t.trackingId === canonical) subagentTraces.delete(key);
|
|
153
|
+
}
|
|
154
|
+
traceTouchedAt.delete(canonical);
|
|
155
|
+
subagentListeners.delete(canonical);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isTraceExpired(canonical: string, now = Date.now()): boolean {
|
|
159
|
+
const touched = traceTouchedAt.get(canonical);
|
|
160
|
+
if (touched === undefined) return false;
|
|
161
|
+
return now - touched > SUBAGENT_TRACE_TTL_MS;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function touchTrace(canonical: string, now = Date.now()): void {
|
|
165
|
+
traceTouchedAt.set(canonical, now);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Evicts expired traces, then oldest whole traces while over budget (Q-30).
|
|
170
|
+
* Called on every insert. Whole-trace eviction removes both the canonical
|
|
171
|
+
* key and its display-id alias.
|
|
172
|
+
*/
|
|
173
|
+
export function pruneTraces(now = Date.now()): void {
|
|
174
|
+
for (const canonical of [...new Set([...subagentTraces.values()].map((t) => t.trackingId))]) {
|
|
175
|
+
if (isTraceExpired(canonical, now)) deleteTraceByCanonical(canonical);
|
|
176
|
+
}
|
|
177
|
+
while (subagentTraces.size > MAX_SUBAGENT_TRACES) {
|
|
178
|
+
const oldestKey = subagentTraces.keys().next().value as string | undefined;
|
|
179
|
+
if (oldestKey === undefined) break;
|
|
180
|
+
const oldest = subagentTraces.get(oldestKey);
|
|
181
|
+
if (!oldest) {
|
|
182
|
+
subagentTraces.delete(oldestKey);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
deleteTraceByCanonical(oldest.trackingId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Drops oldest steps beyond {@link MAX_SUBAGENT_STEPS_PER_TRACE} (Q-30). */
|
|
190
|
+
export function trimTraceSteps(trace: SubAgentTrace): void {
|
|
191
|
+
if (trace.steps.length > MAX_SUBAGENT_STEPS_PER_TRACE) {
|
|
192
|
+
trace.steps.splice(0, trace.steps.length - MAX_SUBAGENT_STEPS_PER_TRACE);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function insertTrace(trace: SubAgentTrace, displayId: string, now = Date.now()): void {
|
|
197
|
+
pruneTraces(now);
|
|
198
|
+
trimTraceSteps(trace);
|
|
199
|
+
subagentTraces.set(trace.trackingId, trace);
|
|
200
|
+
subagentTraces.set(displayId, trace);
|
|
201
|
+
touchTrace(trace.trackingId, now);
|
|
202
|
+
// Keep LRU order: newest at the end (re-set moves existing keys).
|
|
203
|
+
const canonical = subagentTraces.get(trace.trackingId);
|
|
204
|
+
if (canonical) {
|
|
205
|
+
subagentTraces.delete(trace.trackingId);
|
|
206
|
+
subagentTraces.set(trace.trackingId, canonical);
|
|
207
|
+
}
|
|
208
|
+
const alias = subagentTraces.get(displayId);
|
|
209
|
+
if (alias && displayId !== trace.trackingId) {
|
|
210
|
+
subagentTraces.delete(displayId);
|
|
211
|
+
subagentTraces.set(displayId, alias);
|
|
212
|
+
}
|
|
213
|
+
pruneTraces(now);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function lookupTrace(id: string): SubAgentTrace | undefined {
|
|
217
|
+
if (!id) return undefined;
|
|
218
|
+
const direct = subagentTraces.get(id);
|
|
219
|
+
if (direct) {
|
|
220
|
+
if (isTraceExpired(direct.trackingId)) {
|
|
221
|
+
deleteTraceByCanonical(direct.trackingId);
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
// LRU touch (order only; TTL is write-based).
|
|
225
|
+
subagentTraces.delete(id);
|
|
226
|
+
subagentTraces.set(id, direct);
|
|
227
|
+
return direct;
|
|
228
|
+
}
|
|
229
|
+
const lower = id.toLowerCase();
|
|
230
|
+
for (const [key, t] of subagentTraces) {
|
|
231
|
+
if (t.trackingId.toLowerCase() === lower) {
|
|
232
|
+
if (isTraceExpired(t.trackingId)) {
|
|
233
|
+
deleteTraceByCanonical(t.trackingId);
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
subagentTraces.delete(key);
|
|
237
|
+
subagentTraces.set(key, t);
|
|
238
|
+
return t;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
132
244
|
function snapshotTrace(t: SubAgentTrace): SubAgentTrace {
|
|
133
245
|
return { ...t, steps: t.steps.map((s) => ({ ...s })), usage: t.usage ? { ...t.usage } : undefined };
|
|
134
246
|
}
|
|
@@ -148,28 +260,42 @@ function notifyTrace(trackingId: string): void {
|
|
|
148
260
|
* Returns a snapshot of one worker's trace by tracking id
|
|
149
261
|
* (`SUBAGENT-NAME-{32hex}` or the raw 32-hex suffix). Used by
|
|
150
262
|
* `agent.track(id)` for realtime inspection.
|
|
263
|
+
*
|
|
264
|
+
* Prefer {@link getSubAgentTraceScoped} when the parent session is known:
|
|
265
|
+
* this compat lookup matches any parent and can confuse sibling workers
|
|
266
|
+
* whose display names collide across sessions.
|
|
151
267
|
*/
|
|
152
268
|
export function getSubAgentTrace(id: string): SubAgentTrace | undefined {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
269
|
+
const t = lookupTrace(id);
|
|
270
|
+
return t ? snapshotTrace(t) : undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Scoped trace lookup (Q-30, preferred): like {@link getSubAgentTrace} but
|
|
275
|
+
* only matches when `trace.parentSessionId === parentSessionId`. Use this
|
|
276
|
+
* from `Agent.track` (or any parent-bound caller) so truncated/aliased ids
|
|
277
|
+
* cannot leak across sessions.
|
|
278
|
+
*/
|
|
279
|
+
export function getSubAgentTraceScoped(id: string, parentSessionId: string): SubAgentTrace | undefined {
|
|
280
|
+
if (!id || !parentSessionId) return undefined;
|
|
281
|
+
const t = lookupTrace(id);
|
|
282
|
+
if (!t) return undefined;
|
|
283
|
+
if (t.parentSessionId !== parentSessionId) return undefined;
|
|
284
|
+
return snapshotTrace(t);
|
|
161
285
|
}
|
|
162
286
|
|
|
163
287
|
/** Lists tracking ids of known worker traces (mainly for `track` misses). */
|
|
164
288
|
export function listSubAgentTraceIds(): string[] {
|
|
289
|
+
pruneTraces();
|
|
165
290
|
return [...subagentTraces.keys()];
|
|
166
291
|
}
|
|
167
292
|
|
|
168
293
|
/**
|
|
169
294
|
* Subscribes to live updates of one worker trace. Returns an unsubscribe fn.
|
|
295
|
+
* Subscriptions auto-expire after 5min to avoid listener leaks (Q-30).
|
|
170
296
|
*/
|
|
171
297
|
export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => void): () => void {
|
|
172
|
-
const t =
|
|
298
|
+
const t = lookupTrace(id);
|
|
173
299
|
const key = t ? t.trackingId : id;
|
|
174
300
|
let set = subagentListeners.get(key);
|
|
175
301
|
if (!set) {
|
|
@@ -177,18 +303,177 @@ export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => vo
|
|
|
177
303
|
subagentListeners.set(key, set);
|
|
178
304
|
}
|
|
179
305
|
set.add(fn);
|
|
180
|
-
|
|
306
|
+
const timer = setTimeout(() => {
|
|
307
|
+
try { subagentListeners.get(key)?.delete(fn); } catch {}
|
|
308
|
+
}, SUBAGENT_SUBSCRIBE_TTL_MS);
|
|
309
|
+
try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
|
|
310
|
+
let done = false;
|
|
311
|
+
return () => {
|
|
312
|
+
if (done) return;
|
|
313
|
+
done = true;
|
|
314
|
+
try { clearTimeout(timer); } catch {}
|
|
315
|
+
try { subagentListeners.get(key)?.delete(fn); } catch {}
|
|
316
|
+
};
|
|
181
317
|
}
|
|
182
318
|
|
|
183
319
|
/** Appends a step to a worker trace and notifies live subscribers. */
|
|
184
320
|
export function appendSubAgentStep(trackingId: string, step: SubAgentStep): void {
|
|
185
|
-
const t = subagentTraces.get(trackingId);
|
|
321
|
+
const t = subagentTraces.get(trackingId) ?? lookupTrace(trackingId);
|
|
186
322
|
if (!t) return;
|
|
323
|
+
if (isTraceExpired(t.trackingId)) {
|
|
324
|
+
deleteTraceByCanonical(t.trackingId);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
187
327
|
t.steps.push(step);
|
|
328
|
+
trimTraceSteps(t);
|
|
188
329
|
t.turns = Math.max(t.turns, step.turn);
|
|
330
|
+
touchTrace(t.trackingId);
|
|
189
331
|
notifyTrace(trackingId);
|
|
190
332
|
}
|
|
191
333
|
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Budgets (Q-39): unenforced counters + guards. The loop owner wires
|
|
336
|
+
// per-turn charging; the spawn tool below enforces `maxTotalSubagents` and
|
|
337
|
+
// charges completed worker usage. Durations use the monotonic `nowMs()`
|
|
338
|
+
// clock; TTL timestamps above use wall-clock `Date.now()`.
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
function readBudget(parentAgent: Agent): Budget | undefined {
|
|
342
|
+
return (parentAgent as unknown as { budget?: Budget }).budget;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function readBudgetState(parentAgent: Agent): BudgetState | undefined {
|
|
346
|
+
return (parentAgent as unknown as { budgetState?: BudgetState }).budgetState;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function ensureBudgetState(parentAgent: Agent): BudgetState {
|
|
350
|
+
const host = parentAgent as unknown as { budgetState?: BudgetState };
|
|
351
|
+
if (!host.budgetState) {
|
|
352
|
+
host.budgetState = { spawned: 0, totalTokens: 0, totalCostUsd: 0, startedAtMs: nowMs() };
|
|
353
|
+
}
|
|
354
|
+
return host.budgetState;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function usageTokens(usage: unknown): number {
|
|
358
|
+
const u = usage as { totalTokens?: unknown; inputTokens?: unknown; outputTokens?: unknown } | undefined;
|
|
359
|
+
if (!u || typeof u !== "object") return 0;
|
|
360
|
+
if (typeof u.totalTokens === "number" && Number.isFinite(u.totalTokens)) return Math.max(0, Math.floor(u.totalTokens));
|
|
361
|
+
const inp = typeof u.inputTokens === "number" && Number.isFinite(u.inputTokens) ? u.inputTokens : 0;
|
|
362
|
+
const out = typeof u.outputTokens === "number" && Number.isFinite(u.outputTokens) ? u.outputTokens : 0;
|
|
363
|
+
return Math.max(0, Math.floor(inp + out));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function usageCostUsd(usage: unknown): number {
|
|
367
|
+
const cost = (usage as { cost?: { totalCost?: unknown } } | undefined)?.cost;
|
|
368
|
+
const v = cost?.totalCost;
|
|
369
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.max(0, v) : 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Throws {@link BudgetExceededError} (with `partial` usage) when any budget
|
|
374
|
+
* limit is exceeded. Pure check — does not mutate state.
|
|
375
|
+
*/
|
|
376
|
+
export function checkBudget(
|
|
377
|
+
budget: Budget | undefined,
|
|
378
|
+
state: BudgetState | undefined,
|
|
379
|
+
opts?: { usage?: unknown; now?: number }
|
|
380
|
+
): void {
|
|
381
|
+
if (!budget) return;
|
|
382
|
+
const now = opts?.now ?? nowMs();
|
|
383
|
+
const spawned = state?.spawned ?? 0;
|
|
384
|
+
const totalTokens = (state?.totalTokens ?? 0) + (opts?.usage !== undefined ? usageTokens(opts.usage) : 0);
|
|
385
|
+
const totalCostUsd = (state?.totalCostUsd ?? 0) + (opts?.usage !== undefined ? usageCostUsd(opts.usage) : 0);
|
|
386
|
+
const elapsedMs = state ? Math.max(0, now - state.startedAtMs) : 0;
|
|
387
|
+
const partial = {
|
|
388
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens },
|
|
389
|
+
turns: 0 as number,
|
|
390
|
+
text: undefined as string | undefined,
|
|
391
|
+
};
|
|
392
|
+
if (budget.maxTotalSubagents !== undefined && spawned > budget.maxTotalSubagents) {
|
|
393
|
+
throw new BudgetExceededError(
|
|
394
|
+
`Sub-agent budget exceeded: spawned ${spawned} > maxTotalSubagents ${budget.maxTotalSubagents}.`,
|
|
395
|
+
{ context: { partial } }
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (budget.maxTotalTokens !== undefined && totalTokens > budget.maxTotalTokens) {
|
|
399
|
+
throw new BudgetExceededError(
|
|
400
|
+
`Token budget exceeded: ${totalTokens} > maxTotalTokens ${budget.maxTotalTokens}.`,
|
|
401
|
+
{ context: { partial } }
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (budget.maxCostUsd !== undefined && totalCostUsd > budget.maxCostUsd) {
|
|
405
|
+
throw new BudgetExceededError(
|
|
406
|
+
`Cost budget exceeded: $${totalCostUsd.toFixed(6)} > maxCostUsd $${budget.maxCostUsd}.`,
|
|
407
|
+
{ context: { partial } }
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (budget.maxDurationMs !== undefined && state && elapsedMs > budget.maxDurationMs) {
|
|
411
|
+
throw new BudgetExceededError(
|
|
412
|
+
`Duration budget exceeded: ${Math.round(elapsedMs)}ms > maxDurationMs ${budget.maxDurationMs}ms.`,
|
|
413
|
+
{ context: { partial } }
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Adds worker usage to the parent's budget counters (Q-39). Never throws;
|
|
420
|
+
* call {@link checkBudget} afterwards to enforce.
|
|
421
|
+
*/
|
|
422
|
+
export function chargeBudgetUsage(parentAgent: Agent, usage: unknown): BudgetState {
|
|
423
|
+
const state = ensureBudgetState(parentAgent);
|
|
424
|
+
state.totalTokens += usageTokens(usage);
|
|
425
|
+
state.totalCostUsd += usageCostUsd(usage);
|
|
426
|
+
return state;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Reserves `count` spawns against `maxTotalSubagents` (Q-39). Increments the
|
|
431
|
+
* parent's `budgetState.spawned` and throws {@link BudgetExceededError} when
|
|
432
|
+
* the reservation would exceed the budget. Call before spawning.
|
|
433
|
+
*/
|
|
434
|
+
export function reserveSpawn(parentAgent: Agent, count = 1): BudgetState {
|
|
435
|
+
const state = ensureBudgetState(parentAgent);
|
|
436
|
+
const budget = readBudget(parentAgent);
|
|
437
|
+
const next = state.spawned + Math.max(0, Math.floor(count));
|
|
438
|
+
if (budget?.maxTotalSubagents !== undefined && next > budget.maxTotalSubagents) {
|
|
439
|
+
throw new BudgetExceededError(
|
|
440
|
+
`Sub-agent budget exceeded: spawning ${count} would reach ${next} > maxTotalSubagents ${budget.maxTotalSubagents}.`,
|
|
441
|
+
{
|
|
442
|
+
context: {
|
|
443
|
+
partial: {
|
|
444
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: state.totalTokens },
|
|
445
|
+
turns: 0,
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
state.spawned = next;
|
|
452
|
+
checkBudget(budget, state);
|
|
453
|
+
return state;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// Single core loop note + streaming-loop guard (Q-44, partial).
|
|
458
|
+
// The canonical run/stream loops live in `loop.ts` + `agent.ts` (other
|
|
459
|
+
// owners) and are intentionally NOT duplicated here. This guard exists so
|
|
460
|
+
// future consolidation can branch on streaming handles without importing
|
|
461
|
+
// the loop.
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
/** Minimal shape of a streaming run handle (has subscribe + terminal promise). */
|
|
465
|
+
export interface StreamingLoopHandle {
|
|
466
|
+
on: (...args: unknown[]) => unknown;
|
|
467
|
+
result: (...args: unknown[]) => Promise<unknown>;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Type guard for streaming-loop handles (Q-44). */
|
|
471
|
+
export function isStreamingLoop(value: unknown): value is StreamingLoopHandle {
|
|
472
|
+
if (!value || typeof value !== "object") return false;
|
|
473
|
+
const r = value as Record<string, unknown>;
|
|
474
|
+
return typeof r["on"] === "function" && typeof r["result"] === "function";
|
|
475
|
+
}
|
|
476
|
+
|
|
192
477
|
function providerOf(modelStr: string | any | undefined): string | undefined {
|
|
193
478
|
if (!modelStr) return undefined;
|
|
194
479
|
try {
|
|
@@ -235,7 +520,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
235
520
|
: "No worker tools are available; omit the per-task tools list. ") +
|
|
236
521
|
timeoutNote + " " +
|
|
237
522
|
"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\": \"
|
|
523
|
+
"Example: {\"tasks\": [{\"name\": \"RESEARCH-ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"You are a memory market analyst. Return sourced findings only.\", \"task\": \"Research market pricing and supply constraints.\", \"tools\": [\"recent_news\"]}]}",
|
|
239
524
|
input: z.object({
|
|
240
525
|
tasks: z
|
|
241
526
|
.array(
|
|
@@ -275,12 +560,11 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
275
560
|
})
|
|
276
561
|
)
|
|
277
562
|
.min(1)
|
|
278
|
-
.max(maxSpawn)
|
|
279
563
|
.describe(`Array of sub-agents to spawn (max ${maxSpawn} per call) — each gets a personalized prompt and runs statelessly on the developer-configured model`),
|
|
280
564
|
}),
|
|
281
565
|
execute: async ({ tasks }, context) => {
|
|
282
566
|
if (!tasks || tasks.length === 0) {
|
|
283
|
-
return "Error: tasks array is empty. Provide at least one entry shaped like {\"name\": \"
|
|
567
|
+
return "Error: tasks array is empty. Provide at least one entry shaped like {\"name\": \"RESEARCH-ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"<system prompt>\", \"task\": \"<concrete assignment>\"}. Fix the arguments and call spawn_subagents again.";
|
|
284
568
|
}
|
|
285
569
|
const repaired = tasks.map((entry: Record<string, unknown>, index: number) => {
|
|
286
570
|
const rec = (entry ?? {}) as Record<string, unknown>;
|
|
@@ -312,7 +596,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
312
596
|
`Error: tasks[${invalid}].task is missing and could not be inferred. ` +
|
|
313
597
|
`Received keys: [${keys}]. ` +
|
|
314
598
|
`Each tasks[] entry MUST include task (concrete assignment) plus instructions (system prompt) and name (UPPER-KEBAB tag). ` +
|
|
315
|
-
`Example: {\"name\": \"
|
|
599
|
+
`Example: {\"name\": \"RESEARCH-ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"You are a memory market analyst.\", \"task\": \"Research market pricing.\"}. ` +
|
|
316
600
|
`Fix the entry and call spawn_subagents again.`
|
|
317
601
|
);
|
|
318
602
|
}
|
|
@@ -341,9 +625,19 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
341
625
|
|
|
342
626
|
// maxSpawn: trim extras safely — only the first N tasks run.
|
|
343
627
|
const limitedTasks = repaired.slice(0, effectiveMax);
|
|
628
|
+
// Q-39: reserve spawns against maxTotalSubagents before running.
|
|
629
|
+
reserveSpawn(parentAgent, limitedTasks.length);
|
|
630
|
+
// Q-42: batch run id for lineage; per-worker ids attach to metadata.
|
|
631
|
+
const batchRunId = newRunId();
|
|
632
|
+
const parentRunId =
|
|
633
|
+
(parentAgent as unknown as { currentRunId?: unknown; runId?: unknown }).currentRunId ??
|
|
634
|
+
(parentAgent as unknown as { runId?: unknown }).runId;
|
|
635
|
+
const rootRunId =
|
|
636
|
+
(parentAgent as unknown as { rootRunId?: unknown }).rootRunId ?? parentRunId;
|
|
344
637
|
const executedResults = await Promise.all(
|
|
345
638
|
limitedTasks.map(async (t) => {
|
|
346
|
-
const startTime =
|
|
639
|
+
const startTime = nowMs();
|
|
640
|
+
const workerTurnId = newTurnId();
|
|
347
641
|
let sanitized = sanitizeXmlTag(t.name);
|
|
348
642
|
let deduped = sanitized;
|
|
349
643
|
let suffix = 1;
|
|
@@ -393,8 +687,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
393
687
|
turns: 0,
|
|
394
688
|
steps: [],
|
|
395
689
|
};
|
|
396
|
-
|
|
397
|
-
subagentTraces.set(displayId, trace);
|
|
690
|
+
insertTrace(trace, displayId);
|
|
398
691
|
try {
|
|
399
692
|
const parentTraces = ((parentAgent as any).subagentTraces ??= {}) as Record<string, SubAgentTrace>;
|
|
400
693
|
parentTraces[trackingId] = trace;
|
|
@@ -423,6 +716,8 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
423
716
|
trace.steps.push({ turn: turn.turns, type: "tool_result", name: tr.name, isError: tr.isError, timestamp });
|
|
424
717
|
}
|
|
425
718
|
trace.turns = Math.max(trace.turns, turn.turns);
|
|
719
|
+
trimTraceSteps(trace);
|
|
720
|
+
touchTrace(trace.trackingId);
|
|
426
721
|
notifyTrace(trackingId);
|
|
427
722
|
persistParent();
|
|
428
723
|
} catch {}
|
|
@@ -475,6 +770,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
475
770
|
entry.thinking = partialThinking;
|
|
476
771
|
entry.timestamp = Date.now();
|
|
477
772
|
}
|
|
773
|
+
trimTraceSteps(trace);
|
|
478
774
|
notifyTrace(trackingId);
|
|
479
775
|
forwardDelta(delta, thinkingDelta);
|
|
480
776
|
} catch {}
|
|
@@ -580,7 +876,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
580
876
|
try {
|
|
581
877
|
const res: any = await runWithModel(chosenModel, apiKeyToUse, baseUrlToUse);
|
|
582
878
|
|
|
583
|
-
const durationMs =
|
|
879
|
+
const durationMs = Math.max(0, nowMs() - startTime);
|
|
584
880
|
const rawText = (res as any).text;
|
|
585
881
|
const subagentText = typeof rawText === "string" && rawText.trim().length > 0
|
|
586
882
|
? rawText
|
|
@@ -596,9 +892,26 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
596
892
|
? { inputTokens: (res as any).usage.inputTokens ?? 0, outputTokens: (res as any).usage.outputTokens ?? 0, totalTokens: (res as any).usage.totalTokens ?? 0 }
|
|
597
893
|
: live.usage;
|
|
598
894
|
live.text = subagentText;
|
|
895
|
+
trimTraceSteps(live);
|
|
896
|
+
touchTrace(live.trackingId);
|
|
599
897
|
notifyTrace(trackingId);
|
|
600
898
|
}
|
|
601
899
|
try { (parentAgent as any).persistNow?.(); } catch {}
|
|
900
|
+
// Q-39: charge completed worker usage, then enforce token/cost/duration budgets.
|
|
901
|
+
try { chargeBudgetUsage(parentAgent, (res as any).usage); } catch {}
|
|
902
|
+
try {
|
|
903
|
+
checkBudget(readBudget(parentAgent), readBudgetState(parentAgent), { usage: undefined });
|
|
904
|
+
} catch (budgetErr) {
|
|
905
|
+
const be = budgetErr as { context?: { partial?: Record<string, unknown> } };
|
|
906
|
+
try {
|
|
907
|
+
if (be && typeof be === "object" && be.context?.partial) {
|
|
908
|
+
(be.context.partial as Record<string, unknown>)["text"] = subagentText;
|
|
909
|
+
(be.context.partial as Record<string, unknown>)["turns"] = (res as any).turns ?? 0;
|
|
910
|
+
(be.context.partial as Record<string, unknown>)["usage"] = (res as any).usage;
|
|
911
|
+
}
|
|
912
|
+
} catch {}
|
|
913
|
+
throw budgetErr;
|
|
914
|
+
}
|
|
602
915
|
const metadata: SubAgentExecutionMetadata = {
|
|
603
916
|
name: subagentName,
|
|
604
917
|
trackingId: displayId,
|
|
@@ -620,9 +933,21 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
620
933
|
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
621
934
|
isError: false,
|
|
622
935
|
};
|
|
936
|
+
// Q-42 lineage (untyped: response.ts is owned elsewhere) + Q-43 provider meta passthrough.
|
|
937
|
+
try {
|
|
938
|
+
const m = metadata as unknown as Record<string, unknown>;
|
|
939
|
+
m["runId"] = newRunId();
|
|
940
|
+
m["turnId"] = workerTurnId;
|
|
941
|
+
if (parentRunId !== undefined) m["parentRunId"] = parentRunId;
|
|
942
|
+
if (rootRunId !== undefined) m["rootRunId"] = rootRunId;
|
|
943
|
+
else if (parentRunId !== undefined) m["rootRunId"] = parentRunId;
|
|
944
|
+
else m["rootRunId"] = batchRunId;
|
|
945
|
+
if ((res as any).meta !== undefined) m["meta"] = (res as any).meta;
|
|
946
|
+
} catch {}
|
|
623
947
|
return { name: subagentName, trackingId: displayId, text: subagentText, metadata };
|
|
624
948
|
} catch (err: any) {
|
|
625
|
-
|
|
949
|
+
if (err && (err as { code?: unknown }).code === "budget_exceeded") throw err;
|
|
950
|
+
const durationMs = Math.max(0, nowMs() - startTime);
|
|
626
951
|
const errorMessage = err?.message || String(err);
|
|
627
952
|
const prov = attemptedProvider || providerOf(chosenModelStrForProvider) || providerOf(chosenModel as any) || "unknown";
|
|
628
953
|
const live = subagentTraces.get(trackingId);
|
|
@@ -649,6 +974,15 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
649
974
|
isError: true,
|
|
650
975
|
error: errorMessage,
|
|
651
976
|
};
|
|
977
|
+
try {
|
|
978
|
+
const m = metadata as unknown as Record<string, unknown>;
|
|
979
|
+
m["runId"] = newRunId();
|
|
980
|
+
m["turnId"] = workerTurnId;
|
|
981
|
+
if (parentRunId !== undefined) m["parentRunId"] = parentRunId;
|
|
982
|
+
if (rootRunId !== undefined) m["rootRunId"] = rootRunId;
|
|
983
|
+
else if (parentRunId !== undefined) m["rootRunId"] = parentRunId;
|
|
984
|
+
else m["rootRunId"] = batchRunId;
|
|
985
|
+
} catch {}
|
|
652
986
|
return { name: subagentName, trackingId: displayId, text: `Error executing sub-agent ${t.name}: ${errorMessage}`, metadata };
|
|
653
987
|
}
|
|
654
988
|
})
|
|
@@ -706,9 +1040,12 @@ export function agentToTool(
|
|
|
706
1040
|
task: z.string().describe("The detailed instruction or prompt to pass to this sub-agent."),
|
|
707
1041
|
}),
|
|
708
1042
|
execute: async ({ task }, ctx) => {
|
|
709
|
-
const startTime =
|
|
1043
|
+
const startTime = nowMs();
|
|
710
1044
|
const trackingId = createTrackingId();
|
|
711
1045
|
const displayId = `${name}-${trackingId}`;
|
|
1046
|
+
// Q-42 lineage for fixed-delegation calls.
|
|
1047
|
+
const callRunId = newRunId();
|
|
1048
|
+
const callTurnId = newTurnId();
|
|
712
1049
|
try {
|
|
713
1050
|
const parentSessionId = ctx?.sessionId || agentInstance.sessionId || "session";
|
|
714
1051
|
const subSessionId = createFixedChildSessionId(parentSessionId, sanitizeToolName(name));
|
|
@@ -722,8 +1059,7 @@ export function agentToTool(
|
|
|
722
1059
|
turns: 0,
|
|
723
1060
|
steps: [],
|
|
724
1061
|
};
|
|
725
|
-
|
|
726
|
-
subagentTraces.set(displayId, trace);
|
|
1062
|
+
insertTrace(trace, displayId);
|
|
727
1063
|
const onWorkerTurn = (turn: { turns: number; text?: string; thinking?: string; toolCalls?: Array<{ name: string }>; toolResults?: Array<{ name: string; isError?: boolean }> }) => {
|
|
728
1064
|
try {
|
|
729
1065
|
liveTurn = turn.turns;
|
|
@@ -739,6 +1075,8 @@ export function agentToTool(
|
|
|
739
1075
|
for (const tc of turn.toolCalls ?? []) trace.steps.push({ turn: turn.turns, type: "tool_call", name: tc.name, timestamp });
|
|
740
1076
|
for (const tr of turn.toolResults ?? []) trace.steps.push({ turn: turn.turns, type: "tool_result", name: tr.name, isError: tr.isError, timestamp });
|
|
741
1077
|
trace.turns = Math.max(trace.turns, turn.turns);
|
|
1078
|
+
trimTraceSteps(trace);
|
|
1079
|
+
touchTrace(trace.trackingId);
|
|
742
1080
|
notifyTrace(trackingId);
|
|
743
1081
|
} catch {}
|
|
744
1082
|
};
|
|
@@ -774,6 +1112,7 @@ export function agentToTool(
|
|
|
774
1112
|
entry.thinking = partialThinking;
|
|
775
1113
|
entry.timestamp = Date.now();
|
|
776
1114
|
}
|
|
1115
|
+
trimTraceSteps(trace);
|
|
777
1116
|
notifyTrace(trackingId);
|
|
778
1117
|
try {
|
|
779
1118
|
ctx?.onSubagentEvent?.({
|
|
@@ -798,7 +1137,7 @@ export function agentToTool(
|
|
|
798
1137
|
try { workerStream.off("text_delta", textListener); } catch {}
|
|
799
1138
|
try { workerStream.off("thinking_delta", thinkingListener); } catch {}
|
|
800
1139
|
}
|
|
801
|
-
const durationMs =
|
|
1140
|
+
const durationMs = Math.max(0, nowMs() - startTime);
|
|
802
1141
|
const live = subagentTraces.get(trackingId);
|
|
803
1142
|
if (live) {
|
|
804
1143
|
live.status = "done";
|
|
@@ -806,6 +1145,8 @@ export function agentToTool(
|
|
|
806
1145
|
live.provider = response.provider;
|
|
807
1146
|
live.turns = response.turns ?? live.turns;
|
|
808
1147
|
live.text = response.text;
|
|
1148
|
+
trimTraceSteps(live);
|
|
1149
|
+
touchTrace(live.trackingId);
|
|
809
1150
|
notifyTrace(trackingId);
|
|
810
1151
|
}
|
|
811
1152
|
const metadata: SubAgentExecutionMetadata = {
|
|
@@ -828,6 +1169,12 @@ export function agentToTool(
|
|
|
828
1169
|
steps: live ? live.steps.map((s) => ({ ...s })) : undefined,
|
|
829
1170
|
isError: false,
|
|
830
1171
|
};
|
|
1172
|
+
try {
|
|
1173
|
+
const m = metadata as unknown as Record<string, unknown>;
|
|
1174
|
+
m["runId"] = callRunId;
|
|
1175
|
+
m["turnId"] = callTurnId;
|
|
1176
|
+
if (response.meta !== undefined) m["meta"] = response.meta;
|
|
1177
|
+
} catch {}
|
|
831
1178
|
const wrapper: any = {
|
|
832
1179
|
xml: response.text,
|
|
833
1180
|
_subagentMetadata: [metadata],
|
|
@@ -835,12 +1182,14 @@ export function agentToTool(
|
|
|
835
1182
|
};
|
|
836
1183
|
return wrapper;
|
|
837
1184
|
} catch (err: any) {
|
|
838
|
-
const durationMs =
|
|
1185
|
+
const durationMs = Math.max(0, nowMs() - startTime);
|
|
839
1186
|
const msg = err?.message || String(err);
|
|
840
1187
|
const live = subagentTraces.get(trackingId);
|
|
841
1188
|
if (live) {
|
|
842
1189
|
live.status = "error";
|
|
843
1190
|
live.error = msg;
|
|
1191
|
+
trimTraceSteps(live);
|
|
1192
|
+
touchTrace(live.trackingId);
|
|
844
1193
|
notifyTrace(trackingId);
|
|
845
1194
|
}
|
|
846
1195
|
const metadata: SubAgentExecutionMetadata = {
|
|
@@ -857,6 +1206,11 @@ export function agentToTool(
|
|
|
857
1206
|
isError: true,
|
|
858
1207
|
error: msg,
|
|
859
1208
|
};
|
|
1209
|
+
try {
|
|
1210
|
+
const m = metadata as unknown as Record<string, unknown>;
|
|
1211
|
+
m["runId"] = callRunId;
|
|
1212
|
+
m["turnId"] = callTurnId;
|
|
1213
|
+
} catch {}
|
|
860
1214
|
const wrapper: any = {
|
|
861
1215
|
xml: `Error in ${name}: ${msg}`,
|
|
862
1216
|
_subagentMetadata: [metadata],
|