agent-accelerator 0.4.0 → 0.5.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 +149 -57
- package/bunfig.toml +1 -0
- package/package.json +2 -3
- package/src/agent/agent.ts +414 -118
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +327 -99
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +58 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +47 -2
- package/src/providers/google.ts +338 -81
- package/src/providers/openai-compat.ts +171 -61
- package/src/providers/openai.ts +174 -44
- package/src/providers/openrouter.ts +129 -44
- package/src/providers/shared.ts +169 -17
- package/src/providers.ts +375 -11
- package/src/scheduler/scheduled-actions.md +41 -0
- package/src/scheduler/scheduler.ts +814 -0
- package/src/scheduler/store.ts +263 -0
- package/src/scheduler/time.ts +148 -0
- package/src/scheduler/tool.ts +151 -0
- package/src/session/store.ts +229 -113
- package/src/streaming/event-stream.ts +6 -4
- package/src/telemetry-file.ts +31 -0
- package/src/telemetry.ts +65 -0
- package/src/tools/executor.ts +163 -38
- package/src/tools/subagent-result.ts +60 -0
- package/src/types/agent.ts +74 -22
- package/src/types/model.ts +39 -0
- package/src/types/response.ts +9 -0
- package/src/types/scheduler.ts +81 -0
- package/src/types/tool.ts +15 -0
- package/src/utils/cache.ts +14 -5
- package/src/utils/documents.ts +112 -52
- package/src/utils/env.ts +21 -7
- package/src/utils/errors.ts +26 -1
- package/src/utils/headers.ts +15 -15
- package/src/utils/home.ts +30 -0
- package/src/utils/media.ts +57 -12
- package/src/utils/raw-capture.ts +76 -0
- package/src/utils/retry.ts +11 -3
- package/src/utils/safe-io.ts +252 -0
- package/src/utils/session.ts +7 -8
- package/src/data/README.md +0 -84
package/src/agent/context.ts
CHANGED
|
@@ -163,32 +163,27 @@ export class AgentContext {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
* Captures a
|
|
167
|
-
* (Q-19).
|
|
168
|
-
*
|
|
166
|
+
* Captures a detached snapshot of conversation state for run-level rollback
|
|
167
|
+
* (Q-19). The snapshot is a structural deep copy (NOT JSON): `Uint8Array` /
|
|
168
|
+
* `ArrayBuffer` media must survive a rollback byte-for-byte, and large
|
|
169
|
+
* base64 strings are shared by reference rather than re-serialized.
|
|
169
170
|
*/
|
|
170
|
-
checkpoint(): { messages:
|
|
171
|
+
checkpoint(): { messages: Message[]; signatures: string[] } {
|
|
171
172
|
return {
|
|
172
|
-
messages:
|
|
173
|
-
signatures:
|
|
173
|
+
messages: deepCloneValue(this.messages),
|
|
174
|
+
signatures: [...this.thoughtSignatures],
|
|
174
175
|
};
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
/**
|
|
178
|
-
* Restores a checkpoint captured by `checkpoint()`.
|
|
179
|
+
* Restores a checkpoint captured by `checkpoint()`. Malformed checkpoints
|
|
179
180
|
* are ignored (current state kept). When `sessionId` is provided, also
|
|
180
181
|
* clears session-scoped provider state (Q-25) so a rollback never resumes
|
|
181
182
|
* a stale interaction chain.
|
|
182
183
|
*/
|
|
183
|
-
rollback(cp: { messages:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (Array.isArray(msgs)) this.messages = msgs;
|
|
187
|
-
} catch {}
|
|
188
|
-
try {
|
|
189
|
-
const sigs = JSON.parse(cp.signatures);
|
|
190
|
-
if (Array.isArray(sigs)) this.thoughtSignatures = sigs;
|
|
191
|
-
} catch {}
|
|
184
|
+
rollback(cp: { messages: Message[]; signatures: string[] }, sessionId?: string): void {
|
|
185
|
+
if (Array.isArray(cp?.messages)) this.messages = deepCloneValue(cp.messages);
|
|
186
|
+
if (Array.isArray(cp?.signatures)) this.thoughtSignatures = [...cp.signatures];
|
|
192
187
|
if (sessionId) {
|
|
193
188
|
try { clearInteractionChains(sessionId); } catch {}
|
|
194
189
|
try { clearSessionRouting(sessionId); } catch {}
|
package/src/agent/delegation.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { tool } from "../tools/tool.ts";
|
|
3
|
-
import { normalizeToolName } from "../tools/executor.ts";
|
|
4
3
|
import { escapeXml } from "../utils/serialization.ts";
|
|
5
4
|
import type { ToolDefinition } from "../types/tool.ts";
|
|
6
|
-
import type { SubAgentExecutionMetadata } from "../types/response.ts";
|
|
5
|
+
import type { SubAgentExecutionMetadata, SubAgentStep } from "../types/response.ts";
|
|
7
6
|
import type { Agent } from "./agent.ts";
|
|
8
7
|
import type { Budget, BudgetState } from "../types/agent.ts";
|
|
9
8
|
import { BudgetExceededError } from "../types/errors.ts";
|
|
10
|
-
import {
|
|
9
|
+
import { makeSubagentResult } from "../tools/subagent-result.ts";
|
|
11
10
|
import { hashSessionPart } from "../utils/session.ts";
|
|
12
11
|
import { createTrackingId } from "../utils/session.ts";
|
|
13
12
|
import { nowMs } from "../utils/session.ts";
|
|
14
13
|
import { newRunId } from "../utils/session.ts";
|
|
15
14
|
import { newTurnId } from "../utils/session.ts";
|
|
16
|
-
import
|
|
15
|
+
import {
|
|
16
|
+
sanitizeToolName,
|
|
17
|
+
sanitizeXmlTag,
|
|
18
|
+
buildDynamicWorkerConfig,
|
|
19
|
+
buildSubagentsXml,
|
|
20
|
+
createLiveWorker,
|
|
21
|
+
grantWorkerTools,
|
|
22
|
+
normalizeWorkerTimeoutValue,
|
|
23
|
+
providerOfWorkerModel,
|
|
24
|
+
} from "./worker.ts";
|
|
17
25
|
|
|
18
26
|
export { escapeXml };
|
|
19
27
|
|
|
@@ -36,17 +44,6 @@ export interface DynamicSubagentTask {
|
|
|
36
44
|
timeoutMs?: number;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
function sanitizeXmlTag(raw: string): string {
|
|
40
|
-
let s = raw.toUpperCase().replace(/[^A-Z0-9.-]/g, "-");
|
|
41
|
-
if (!/^[A-Z]/.test(s)) s = `AGENT-${s}`;
|
|
42
|
-
s = s.replace(/[.]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
43
|
-
return s.slice(0, 64) || "SUBAGENT";
|
|
44
|
-
}
|
|
45
|
-
function sanitizeToolName(raw: string): string {
|
|
46
|
-
const tag = sanitizeXmlTag(raw);
|
|
47
|
-
return tag.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").slice(0, 64) || "sub-agent";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
47
|
export function createChildSessionId(parentId: string, tag: string): string {
|
|
51
48
|
let rand: string;
|
|
52
49
|
try {
|
|
@@ -62,7 +59,7 @@ export function createChildSessionId(parentId: string, tag: string): string {
|
|
|
62
59
|
rand = Math.random().toString(36).slice(2, 10);
|
|
63
60
|
}
|
|
64
61
|
// Provider-safe: OpenAI `prompt_cache_key` enforces max 64 chars (400
|
|
65
|
-
// otherwise). Parent ids are already ~
|
|
62
|
+
// otherwise). Parent ids are already ~36+ chars (a UUID), so naive
|
|
66
63
|
// `${parent}-sub-${tag}-${rand}` overflows (observed 65-72 chars → every
|
|
67
64
|
// sub-agent 400s with 0 usage). Truncate the parent portion to fit, keeping
|
|
68
65
|
// the tag + rand suffix intact for uniqueness/debuggability.
|
|
@@ -284,12 +281,54 @@ export function getSubAgentTraceScoped(id: string, parentSessionId: string): Sub
|
|
|
284
281
|
return snapshotTrace(t);
|
|
285
282
|
}
|
|
286
283
|
|
|
287
|
-
/**
|
|
284
|
+
/**
|
|
285
|
+
* Lists tracking ids of EVERY worker trace in the process (all agents, all
|
|
286
|
+
* sessions). Diagnostics only: `Agent` never uses it, because in a
|
|
287
|
+
* multi-tenant host it would expose other tenants' workers. Use
|
|
288
|
+
* {@link listSubAgentTraceIdsForSessions} for agent-bound callers.
|
|
289
|
+
*/
|
|
288
290
|
export function listSubAgentTraceIds(): string[] {
|
|
289
291
|
pruneTraces();
|
|
290
292
|
return [...subagentTraces.keys()];
|
|
291
293
|
}
|
|
292
294
|
|
|
295
|
+
/** Scoped lookup across a set of parent session ids (an agent's default + named sessions). */
|
|
296
|
+
export function getSubAgentTraceForSessions(
|
|
297
|
+
id: string,
|
|
298
|
+
parentSessionIds: ReadonlySet<string>
|
|
299
|
+
): SubAgentTrace | undefined {
|
|
300
|
+
if (!id || parentSessionIds.size === 0) return undefined;
|
|
301
|
+
const t = lookupTrace(id);
|
|
302
|
+
if (!t || !parentSessionIds.has(t.parentSessionId)) return undefined;
|
|
303
|
+
return snapshotTrace(t);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Raw tracking ids of traces spawned from any of `parentSessionIds` (deduped). */
|
|
307
|
+
export function listSubAgentTraceIdsForSessions(parentSessionIds: ReadonlySet<string>): string[] {
|
|
308
|
+
pruneTraces();
|
|
309
|
+
const out = new Set<string>();
|
|
310
|
+
for (const t of subagentTraces.values()) {
|
|
311
|
+
if (parentSessionIds.has(t.parentSessionId)) out.add(t.trackingId);
|
|
312
|
+
}
|
|
313
|
+
return [...out];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Like {@link subscribeToSubAgent} but refuses (returns a no-op unsubscribe)
|
|
318
|
+
* when the trace exists and belongs to a different parent session. An id that
|
|
319
|
+
* does not exist yet is accepted: ids are 128-bit random, so subscribing early
|
|
320
|
+
* cannot reach another tenant's trace.
|
|
321
|
+
*/
|
|
322
|
+
export function subscribeToSubAgentForSessions(
|
|
323
|
+
id: string,
|
|
324
|
+
parentSessionIds: ReadonlySet<string>,
|
|
325
|
+
fn: (trace: SubAgentTrace) => void
|
|
326
|
+
): () => void {
|
|
327
|
+
const t = lookupTrace(id);
|
|
328
|
+
if (t && !parentSessionIds.has(t.parentSessionId)) return () => {};
|
|
329
|
+
return subscribeToSubAgent(id, fn);
|
|
330
|
+
}
|
|
331
|
+
|
|
293
332
|
/**
|
|
294
333
|
* Subscribes to live updates of one worker trace. Returns an unsubscribe fn.
|
|
295
334
|
* Subscriptions auto-expire after 5min to avoid listener leaks (Q-30).
|
|
@@ -474,16 +513,6 @@ export function isStreamingLoop(value: unknown): value is StreamingLoopHandle {
|
|
|
474
513
|
return typeof r["on"] === "function" && typeof r["result"] === "function";
|
|
475
514
|
}
|
|
476
515
|
|
|
477
|
-
function providerOf(modelStr: string | any | undefined): string | undefined {
|
|
478
|
-
if (!modelStr) return undefined;
|
|
479
|
-
try {
|
|
480
|
-
const m = typeof modelStr === "string" ? modelStr : (modelStr as any)?.model ?? (modelStr as any)?.id ?? String(modelStr);
|
|
481
|
-
return resolveModel(m as any).provider.id;
|
|
482
|
-
} catch {
|
|
483
|
-
return undefined;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
516
|
/**
|
|
488
517
|
* Creates the built-in tool that spawns stateless dynamic sub-agents concurrently.
|
|
489
518
|
*
|
|
@@ -654,23 +683,9 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
654
683
|
|
|
655
684
|
// Fixed developer-configured worker model — never LLM-choosable.
|
|
656
685
|
const chosenModel: any = parentAgent.subagentModel;
|
|
657
|
-
|
|
658
|
-
let chosenModelStrForProvider = typeof chosenModel === "string" ? chosenModel : (chosenModel as any)?.model ?? (chosenModel as any)?.id ?? String(chosenModel);
|
|
659
|
-
let attemptedProvider = providerOf(chosenModelStrForProvider);
|
|
660
|
-
// Determine creds: only reuse parent creds if provider matches parent provider, else let env resolve for cross-provider
|
|
661
|
-
const parentProvider = providerOf(parentAgent.modelStringOrSpec as any);
|
|
662
|
-
const targetProviderFinal = providerOf(chosenModelStrForProvider);
|
|
663
|
-
let apiKeyToUse: string | undefined;
|
|
664
|
-
let baseUrlToUse: string | undefined;
|
|
665
|
-
if (targetProviderFinal === parentProvider) {
|
|
666
|
-
apiKeyToUse = parentAgent.apiKey;
|
|
667
|
-
baseUrlToUse = parentAgent.baseUrl;
|
|
668
|
-
} else {
|
|
669
|
-
apiKeyToUse = undefined;
|
|
670
|
-
baseUrlToUse = undefined;
|
|
671
|
-
}
|
|
686
|
+
const chosenModelStrForProvider = typeof chosenModel === "string" ? chosenModel : (chosenModel as any)?.model ?? (chosenModel as any)?.id ?? String(chosenModel);
|
|
672
687
|
|
|
673
|
-
const runWithModel = async (
|
|
688
|
+
const runWithModel = async () => {
|
|
674
689
|
if (context?.signal?.aborted) throw new Error("Aborted before spawn");
|
|
675
690
|
const childSessionId = createChildSessionId(parentAgent.sessionId, subagentName);
|
|
676
691
|
workerSessionId = childSessionId;
|
|
@@ -695,118 +710,29 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
695
710
|
const persistParent = () => {
|
|
696
711
|
try { (parentAgent as any).persistNow?.(); } catch {}
|
|
697
712
|
};
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (turn.thinking) partialEntry.thinking = turn.thinking;
|
|
708
|
-
delete (partialEntry as any).partial;
|
|
709
|
-
} else if (turn.text) {
|
|
710
|
-
trace.steps.push({ turn: turn.turns, type: "assistant", text: turn.text.slice(0, 2000), timestamp });
|
|
711
|
-
}
|
|
712
|
-
for (const tc of turn.toolCalls ?? []) {
|
|
713
|
-
trace.steps.push({ turn: turn.turns, type: "tool_call", name: tc.name, timestamp });
|
|
714
|
-
}
|
|
715
|
-
for (const tr of turn.toolResults ?? []) {
|
|
716
|
-
trace.steps.push({ turn: turn.turns, type: "tool_result", name: tr.name, isError: tr.isError, timestamp });
|
|
717
|
-
}
|
|
718
|
-
trace.turns = Math.max(trace.turns, turn.turns);
|
|
719
|
-
trimTraceSteps(trace);
|
|
720
|
-
touchTrace(trace.trackingId);
|
|
721
|
-
notifyTrace(trackingId);
|
|
722
|
-
persistParent();
|
|
723
|
-
} catch {}
|
|
724
|
-
};
|
|
725
|
-
// Realtime thinking/text: the worker is streamed internally and
|
|
726
|
-
// every delta lands in the trace (partial entry, updated in
|
|
727
|
-
// place) so `agent.track(id)` shows generation as it happens —
|
|
728
|
-
// not just at turn end. Deltas also ride to the parent stream via
|
|
729
|
-
// `context.onSubagentEvent` (streaming parent runs only).
|
|
730
|
-
let liveTurn = 1;
|
|
731
|
-
let partialText = "";
|
|
732
|
-
let partialThinking = "";
|
|
733
|
-
const findPartial = (turn: number): SubAgentStep | undefined => {
|
|
734
|
-
for (let i = trace.steps.length - 1; i >= 0; i--) {
|
|
735
|
-
const s = trace.steps[i]!;
|
|
736
|
-
if (s.turn === turn && s.partial) return s;
|
|
737
|
-
}
|
|
738
|
-
return undefined;
|
|
739
|
-
};
|
|
740
|
-
const forwardDelta = (delta?: string, thinkingDelta?: string) => {
|
|
741
|
-
try {
|
|
742
|
-
context?.onSubagentEvent?.({
|
|
743
|
-
trackingId: displayId,
|
|
744
|
-
delta,
|
|
745
|
-
thinkingDelta,
|
|
746
|
-
partialText: partialText || undefined,
|
|
747
|
-
partialThinking: partialThinking || undefined,
|
|
748
|
-
});
|
|
749
|
-
} catch {}
|
|
750
|
-
};
|
|
751
|
-
const onWorkerDelta = (delta?: string, thinkingDelta?: string) => {
|
|
752
|
-
try {
|
|
753
|
-
if (delta) {
|
|
754
|
-
partialText = (partialText + delta).slice(-10000);
|
|
755
|
-
let entry = findPartial(liveTurn);
|
|
756
|
-
if (!entry) {
|
|
757
|
-
entry = { turn: liveTurn, type: "assistant", text: "", partial: true, timestamp: Date.now() };
|
|
758
|
-
trace.steps.push(entry);
|
|
759
|
-
}
|
|
760
|
-
entry.text = partialText;
|
|
761
|
-
entry.timestamp = Date.now();
|
|
762
|
-
}
|
|
763
|
-
if (thinkingDelta) {
|
|
764
|
-
partialThinking = (partialThinking + thinkingDelta).slice(-10000);
|
|
765
|
-
let entry = findPartial(liveTurn);
|
|
766
|
-
if (!entry) {
|
|
767
|
-
entry = { turn: liveTurn, type: "assistant", text: "", partial: true, timestamp: Date.now() };
|
|
768
|
-
trace.steps.push(entry);
|
|
769
|
-
}
|
|
770
|
-
entry.thinking = partialThinking;
|
|
771
|
-
entry.timestamp = Date.now();
|
|
772
|
-
}
|
|
773
|
-
trimTraceSteps(trace);
|
|
774
|
-
notifyTrace(trackingId);
|
|
775
|
-
forwardDelta(delta, thinkingDelta);
|
|
776
|
-
} catch {}
|
|
777
|
-
};
|
|
778
|
-
// Grant ONLY the Main Agent-selected subset from the developer pool. Unknown names are dropped.
|
|
779
|
-
// Matching is case/format-insensitive (same rules as tool execution): the model may emit
|
|
780
|
-
// "RECENT_NEWS" or "recent news" for a registered "recent_news" tool.
|
|
781
|
-
const poolByNormalized = new Map<string, ToolDefinition>();
|
|
782
|
-
for (const [key, def] of Object.entries(toolPool)) {
|
|
783
|
-
poolByNormalized.set(normalizeToolName(key), def);
|
|
784
|
-
const declared = (def as ToolDefinition)?.name;
|
|
785
|
-
if (declared) poolByNormalized.set(normalizeToolName(declared), def);
|
|
786
|
-
}
|
|
787
|
-
const grantedTools: Record<string, ToolDefinition> = {};
|
|
788
|
-
for (const toolName of (t as any).tools ?? []) {
|
|
789
|
-
const pooled = toolPool[toolName] ?? poolByNormalized.get(normalizeToolName(String(toolName)));
|
|
790
|
-
if (pooled) grantedTools[(pooled as ToolDefinition).name || toolName] = pooled;
|
|
791
|
-
}
|
|
792
|
-
// Timeout: >0 fixed for every worker; 0 = no limit; -1 = per-task timeoutMs from the Main Agent.
|
|
793
|
-
const workerTimeout = cfgTimeout > 0 ? cfgTimeout : cfgTimeout === -1 && (t as any).timeoutMs > 0 ? (t as any).timeoutMs : 0;
|
|
794
|
-
const subAgent = new AgentClass({
|
|
795
|
-
name: t.name,
|
|
796
|
-
description: t.role || `Sub-agent ${t.name}`,
|
|
797
|
-
instructions: t.instructions,
|
|
798
|
-
model: modelToUse,
|
|
799
|
-
apiKey,
|
|
800
|
-
baseUrl,
|
|
801
|
-
thinkingLevel: (dynCfg?.thinkingLevel as any) ?? parentAgent.thinkingConfig?.level,
|
|
802
|
-
sessionId: childSessionId,
|
|
803
|
-
tools: grantedTools,
|
|
804
|
-
// Stateless by design: one task in, one result out, then shut down. No history, no recursion.
|
|
805
|
-
stateless: true,
|
|
806
|
-
serviceTier: parentAgent.serviceTier,
|
|
807
|
-
headers: parentAgent.customHeaders,
|
|
808
|
-
maxTurns: parentAgent.maxTurns,
|
|
713
|
+
// Unified live worker state machine (shared with fixed workers).
|
|
714
|
+
const { onWorkerTurn, onWorkerDelta, attachToStream } = createLiveWorker(trace, trackingId, displayId, {
|
|
715
|
+
trim: (t) => trimTraceSteps(t),
|
|
716
|
+
touch: (id) => touchTrace(id),
|
|
717
|
+
notify: (id) => notifyTrace(id),
|
|
718
|
+
persist: persistParent,
|
|
719
|
+
emit: (ev) => {
|
|
720
|
+
try { context?.onSubagentEvent?.(ev); } catch {}
|
|
721
|
+
},
|
|
809
722
|
});
|
|
723
|
+
// Grant ONLY the Main Agent-selected subset from the developer pool.
|
|
724
|
+
const grantedTools = grantWorkerTools(toolPool, (t as { tools?: unknown }).tools);
|
|
725
|
+
// Timeout: >0 fixed for every worker; 0 = no limit; -1 = per-task timeoutMs.
|
|
726
|
+
const workerTimeout = normalizeWorkerTimeoutValue(cfgTimeout, (t as { timeoutMs?: unknown }).timeoutMs);
|
|
727
|
+
const subAgent = new AgentClass(
|
|
728
|
+
buildDynamicWorkerConfig(parentAgent, {
|
|
729
|
+
name: t.name,
|
|
730
|
+
role: t.role,
|
|
731
|
+
instructions: t.instructions,
|
|
732
|
+
sessionId: childSessionId,
|
|
733
|
+
tools: grantedTools,
|
|
734
|
+
})
|
|
735
|
+
);
|
|
810
736
|
// Lineage binding: child records its parent session so persisted
|
|
811
737
|
// snapshots and routing can tell siblings apart even when ids
|
|
812
738
|
// are truncated to 64 chars.
|
|
@@ -850,10 +776,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
850
776
|
// Streamed internally (consumed to completion): same final result
|
|
851
777
|
// as run(), but text/thinking deltas flow into the trace live.
|
|
852
778
|
const workerStream = subAgent.stream(t.task, { signal: workerController.signal, onTurn: onWorkerTurn } as any);
|
|
853
|
-
const
|
|
854
|
-
const thinkingListener = (e: any) => onWorkerDelta(undefined, e.thinkingDelta);
|
|
855
|
-
workerStream.on("text_delta", textListener);
|
|
856
|
-
workerStream.on("thinking_delta", thinkingListener);
|
|
779
|
+
const detachWorkerListeners = attachToStream(workerStream as unknown as Parameters<typeof attachToStream>[0]);
|
|
857
780
|
const taskPromise = workerStream.result();
|
|
858
781
|
try {
|
|
859
782
|
const racers: Promise<unknown>[] = [taskPromise as unknown as Promise<unknown>];
|
|
@@ -864,8 +787,8 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
864
787
|
} finally {
|
|
865
788
|
if (timeoutId) clearTimeout(timeoutId);
|
|
866
789
|
try { workerStream.cancel(); } catch {}
|
|
867
|
-
try {
|
|
868
|
-
try {
|
|
790
|
+
try { subAgent.reset(); } catch {} // one-shot worker: free its chain/routing state
|
|
791
|
+
try { detachWorkerListeners(); } catch {}
|
|
869
792
|
if (abortListener && parentSignal) {
|
|
870
793
|
try { parentSignal.removeEventListener("abort", abortListener as any); } catch {}
|
|
871
794
|
}
|
|
@@ -874,7 +797,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
874
797
|
};
|
|
875
798
|
|
|
876
799
|
try {
|
|
877
|
-
const res: any = await runWithModel(
|
|
800
|
+
const res: any = await runWithModel();
|
|
878
801
|
|
|
879
802
|
const durationMs = Math.max(0, nowMs() - startTime);
|
|
880
803
|
const rawText = (res as any).text;
|
|
@@ -897,21 +820,8 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
897
820
|
notifyTrace(trackingId);
|
|
898
821
|
}
|
|
899
822
|
try { (parentAgent as any).persistNow?.(); } catch {}
|
|
900
|
-
//
|
|
901
|
-
|
|
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
|
-
}
|
|
823
|
+
// Worker usage is charged against the parent's budget centrally by the
|
|
824
|
+
// loop (it sees every finished worker, fixed and dynamic, exactly once).
|
|
915
825
|
const metadata: SubAgentExecutionMetadata = {
|
|
916
826
|
name: subagentName,
|
|
917
827
|
trackingId: displayId,
|
|
@@ -949,7 +859,7 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
949
859
|
if (err && (err as { code?: unknown }).code === "budget_exceeded") throw err;
|
|
950
860
|
const durationMs = Math.max(0, nowMs() - startTime);
|
|
951
861
|
const errorMessage = err?.message || String(err);
|
|
952
|
-
const prov =
|
|
862
|
+
const prov = providerOfWorkerModel(chosenModelStrForProvider) || providerOfWorkerModel(chosenModel as unknown) || "unknown";
|
|
953
863
|
const live = subagentTraces.get(trackingId);
|
|
954
864
|
if (live) {
|
|
955
865
|
live.status = "error";
|
|
@@ -988,21 +898,12 @@ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
|
|
|
988
898
|
})
|
|
989
899
|
);
|
|
990
900
|
|
|
991
|
-
const
|
|
901
|
+
const xmlOutput = buildSubagentsXml(executedResults.map((r) => {
|
|
992
902
|
subagentMetadataList.push(r.metadata);
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
return ` <${r.name}>\n${indentedText}\n </${r.name}>`;
|
|
996
|
-
});
|
|
903
|
+
return r;
|
|
904
|
+
}));
|
|
997
905
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
const resultPayload = {
|
|
1001
|
-
xml: xmlOutput,
|
|
1002
|
-
_subagentMetadata: subagentMetadataList,
|
|
1003
|
-
toString() { return xmlOutput; },
|
|
1004
|
-
};
|
|
1005
|
-
return resultPayload;
|
|
906
|
+
return makeSubagentResult(xmlOutput, subagentMetadataList);
|
|
1006
907
|
},
|
|
1007
908
|
});
|
|
1008
909
|
}
|
|
@@ -1060,82 +961,28 @@ export function agentToTool(
|
|
|
1060
961
|
steps: [],
|
|
1061
962
|
};
|
|
1062
963
|
insertTrace(trace, displayId);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
trimTraceSteps(trace);
|
|
1079
|
-
touchTrace(trace.trackingId);
|
|
1080
|
-
notifyTrace(trackingId);
|
|
1081
|
-
} catch {}
|
|
1082
|
-
};
|
|
1083
|
-
let liveTurn = 1;
|
|
1084
|
-
let partialText = "";
|
|
1085
|
-
let partialThinking = "";
|
|
1086
|
-
const findPartial = (turn: number): SubAgentStep | undefined => {
|
|
1087
|
-
for (let i = trace.steps.length - 1; i >= 0; i--) {
|
|
1088
|
-
const s = trace.steps[i]!;
|
|
1089
|
-
if (s.turn === turn && s.partial) return s;
|
|
1090
|
-
}
|
|
1091
|
-
return undefined;
|
|
1092
|
-
};
|
|
1093
|
-
const onWorkerDelta = (delta?: string, thinkingDelta?: string) => {
|
|
1094
|
-
try {
|
|
1095
|
-
if (delta) {
|
|
1096
|
-
partialText = (partialText + delta).slice(-10000);
|
|
1097
|
-
let entry = findPartial(liveTurn);
|
|
1098
|
-
if (!entry) {
|
|
1099
|
-
entry = { turn: liveTurn, type: "assistant", text: "", partial: true, timestamp: Date.now() };
|
|
1100
|
-
trace.steps.push(entry);
|
|
1101
|
-
}
|
|
1102
|
-
entry.text = partialText;
|
|
1103
|
-
entry.timestamp = Date.now();
|
|
1104
|
-
}
|
|
1105
|
-
if (thinkingDelta) {
|
|
1106
|
-
partialThinking = (partialThinking + thinkingDelta).slice(-10000);
|
|
1107
|
-
let entry = findPartial(liveTurn);
|
|
1108
|
-
if (!entry) {
|
|
1109
|
-
entry = { turn: liveTurn, type: "assistant", text: "", partial: true, timestamp: Date.now() };
|
|
1110
|
-
trace.steps.push(entry);
|
|
1111
|
-
}
|
|
1112
|
-
entry.thinking = partialThinking;
|
|
1113
|
-
entry.timestamp = Date.now();
|
|
1114
|
-
}
|
|
1115
|
-
trimTraceSteps(trace);
|
|
1116
|
-
notifyTrace(trackingId);
|
|
1117
|
-
try {
|
|
1118
|
-
ctx?.onSubagentEvent?.({
|
|
1119
|
-
trackingId: displayId,
|
|
1120
|
-
delta,
|
|
1121
|
-
thinkingDelta,
|
|
1122
|
-
partialText: partialText || undefined,
|
|
1123
|
-
partialThinking: partialThinking || undefined,
|
|
1124
|
-
});
|
|
1125
|
-
} catch {}
|
|
1126
|
-
} catch {}
|
|
1127
|
-
};
|
|
1128
|
-
const workerStream = agentInstance.stream(task, { signal: ctx?.signal, sessionId: subSessionId, onTurn: onWorkerTurn } as any);
|
|
1129
|
-
const textListener = (e: any) => onWorkerDelta(e.delta, undefined);
|
|
1130
|
-
const thinkingListener = (e: any) => onWorkerDelta(undefined, e.thinkingDelta);
|
|
1131
|
-
workerStream.on("text_delta", textListener);
|
|
1132
|
-
workerStream.on("thinking_delta", thinkingListener);
|
|
964
|
+
// Unified live worker state machine (shared with dynamic workers).
|
|
965
|
+
const { onWorkerTurn, attachToStream } = createLiveWorker(trace, trackingId, displayId, {
|
|
966
|
+
trim: (t) => trimTraceSteps(t),
|
|
967
|
+
touch: (id) => touchTrace(id),
|
|
968
|
+
notify: (id) => notifyTrace(id),
|
|
969
|
+
emit: (ev) => {
|
|
970
|
+
try { ctx?.onSubagentEvent?.(ev); } catch {}
|
|
971
|
+
},
|
|
972
|
+
});
|
|
973
|
+
// One isolated copy per call: the shared instance is single-flight, so
|
|
974
|
+
// calling it directly would serialize (or steer) parallel tool calls.
|
|
975
|
+
const worker: Agent =
|
|
976
|
+
typeof (agentInstance as Agent).fork === "function" ? agentInstance.fork(subSessionId, parentSessionId) : agentInstance;
|
|
977
|
+
const workerStream = worker.stream(task, { signal: ctx?.signal, sessionId: subSessionId, onTurn: onWorkerTurn } as any);
|
|
978
|
+
const detachWorkerListeners = attachToStream(workerStream as unknown as Parameters<typeof attachToStream>[0]);
|
|
1133
979
|
let response: any;
|
|
1134
980
|
try {
|
|
1135
981
|
response = await workerStream.result();
|
|
1136
982
|
} finally {
|
|
1137
|
-
try {
|
|
1138
|
-
|
|
983
|
+
try { detachWorkerListeners(); } catch {}
|
|
984
|
+
// The per-call session id is never reused: drop its provider chain/routing state.
|
|
985
|
+
if (worker !== agentInstance) { try { worker.reset(); } catch {} }
|
|
1139
986
|
}
|
|
1140
987
|
const durationMs = Math.max(0, nowMs() - startTime);
|
|
1141
988
|
const live = subagentTraces.get(trackingId);
|
|
@@ -1175,12 +1022,7 @@ export function agentToTool(
|
|
|
1175
1022
|
m["turnId"] = callTurnId;
|
|
1176
1023
|
if (response.meta !== undefined) m["meta"] = response.meta;
|
|
1177
1024
|
} catch {}
|
|
1178
|
-
|
|
1179
|
-
xml: response.text,
|
|
1180
|
-
_subagentMetadata: [metadata],
|
|
1181
|
-
toString() { return response.text; },
|
|
1182
|
-
};
|
|
1183
|
-
return wrapper;
|
|
1025
|
+
return makeSubagentResult(response.text, [metadata]);
|
|
1184
1026
|
} catch (err: any) {
|
|
1185
1027
|
const durationMs = Math.max(0, nowMs() - startTime);
|
|
1186
1028
|
const msg = err?.message || String(err);
|
|
@@ -1211,12 +1053,7 @@ export function agentToTool(
|
|
|
1211
1053
|
m["runId"] = callRunId;
|
|
1212
1054
|
m["turnId"] = callTurnId;
|
|
1213
1055
|
} catch {}
|
|
1214
|
-
|
|
1215
|
-
xml: `Error in ${name}: ${msg}`,
|
|
1216
|
-
_subagentMetadata: [metadata],
|
|
1217
|
-
toString() { return `Error in ${name}: ${msg}`; },
|
|
1218
|
-
};
|
|
1219
|
-
return wrapper;
|
|
1056
|
+
return makeSubagentResult(`Error in ${name}: ${msg}`, [metadata]);
|
|
1220
1057
|
}
|
|
1221
1058
|
},
|
|
1222
1059
|
});
|