agent-accelerator 0.4.5 → 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.
Files changed (45) hide show
  1. package/README.md +149 -57
  2. package/bunfig.toml +1 -0
  3. package/package.json +2 -3
  4. package/src/agent/agent.ts +401 -120
  5. package/src/agent/context.ts +11 -16
  6. package/src/agent/delegation.ts +111 -274
  7. package/src/agent/loop.ts +253 -98
  8. package/src/agent/subagent.ts +4 -12
  9. package/src/agent/worker.ts +435 -0
  10. package/src/index.ts +39 -5
  11. package/src/models/catalog-cache.ts +65 -25
  12. package/src/models/catalog.ts +21 -2
  13. package/src/providers/google.ts +282 -74
  14. package/src/providers/openai-compat.ts +61 -41
  15. package/src/providers/openai.ts +46 -31
  16. package/src/providers/openrouter.ts +42 -31
  17. package/src/providers/shared.ts +110 -16
  18. package/src/providers.ts +119 -6
  19. package/src/scheduler/scheduled-actions.md +41 -0
  20. package/src/scheduler/scheduler.ts +814 -0
  21. package/src/scheduler/store.ts +263 -0
  22. package/src/scheduler/time.ts +148 -0
  23. package/src/scheduler/tool.ts +151 -0
  24. package/src/session/store.ts +211 -104
  25. package/src/streaming/event-stream.ts +6 -4
  26. package/src/telemetry-file.ts +31 -0
  27. package/src/telemetry.ts +65 -0
  28. package/src/tools/executor.ts +163 -38
  29. package/src/tools/subagent-result.ts +60 -0
  30. package/src/types/agent.ts +57 -20
  31. package/src/types/model.ts +18 -0
  32. package/src/types/response.ts +2 -0
  33. package/src/types/scheduler.ts +81 -0
  34. package/src/types/tool.ts +15 -0
  35. package/src/utils/cache.ts +14 -5
  36. package/src/utils/documents.ts +112 -52
  37. package/src/utils/errors.ts +11 -0
  38. package/src/utils/headers.ts +15 -15
  39. package/src/utils/home.ts +30 -0
  40. package/src/utils/media.ts +57 -12
  41. package/src/utils/raw-capture.ts +76 -0
  42. package/src/utils/retry.ts +11 -3
  43. package/src/utils/safe-io.ts +252 -0
  44. package/src/utils/session.ts +7 -8
  45. package/src/data/README.md +0 -84
@@ -163,32 +163,27 @@ export class AgentContext {
163
163
  }
164
164
 
165
165
  /**
166
- * Captures a JSON snapshot of conversation state for run-level rollback
167
- * (Q-19). Returns stringified payloads so the checkpoint is detached from
168
- * live mutation.
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: string; signatures: string } {
171
+ checkpoint(): { messages: Message[]; signatures: string[] } {
171
172
  return {
172
- messages: JSON.stringify(this.messages),
173
- signatures: JSON.stringify(this.thoughtSignatures),
173
+ messages: deepCloneValue(this.messages),
174
+ signatures: [...this.thoughtSignatures],
174
175
  };
175
176
  }
176
177
 
177
178
  /**
178
- * Restores a checkpoint captured by `checkpoint()`. Corrupt checkpoints
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: string; signatures: string }, sessionId?: string): void {
184
- try {
185
- const msgs = JSON.parse(cp.messages);
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 {}
@@ -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 { resolveModel } from "../providers/registry.ts";
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 type { SubAgentStep } from "../types/response.ts";
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 ~42 chars (`accel-<uuid>`), so naive
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
- /** Lists tracking ids of known worker traces (mainly for `track` misses). */
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
- // Normalize chosenModel to string|ModelSpec handling
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 (modelToUse: any, apiKey: string | undefined, baseUrl: string | undefined) => {
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
- const onWorkerTurn = (turn: { turns: number; text?: string; thinking?: string; toolCalls?: Array<{ name: string }>; toolResults?: Array<{ name: string; isError?: boolean }> }) => {
699
- try {
700
- liveTurn = turn.turns;
701
- // Finalize any partial streaming entry for this turn, then
702
- // log structural steps (tool calls/results) as before.
703
- const timestamp = Date.now();
704
- const partialEntry = findPartial(liveTurn);
705
- if (partialEntry) {
706
- if (turn.text) partialEntry.text = turn.text;
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 textListener = (e: any) => onWorkerDelta(e.delta, undefined);
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 { workerStream.off("text_delta", textListener); } catch {}
868
- try { workerStream.off("thinking_delta", thinkingListener); } catch {}
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(chosenModel, apiKeyToUse, baseUrlToUse);
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
- // 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
- }
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 = attemptedProvider || providerOf(chosenModelStrForProvider) || providerOf(chosenModel as any) || "unknown";
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 xmlBlocks = executedResults.map((r) => {
901
+ const xmlOutput = buildSubagentsXml(executedResults.map((r) => {
992
902
  subagentMetadataList.push(r.metadata);
993
- const escaped = escapeXml(r.text);
994
- const indentedText = escaped.split("\n").map((line) => ` ${line}`).join("\n");
995
- return ` <${r.name}>\n${indentedText}\n </${r.name}>`;
996
- });
903
+ return r;
904
+ }));
997
905
 
998
- const xmlOutput = `<SUB-AGENTS-RESPONSE>\n${xmlBlocks.join("\n")}\n</SUB-AGENTS-RESPONSE>`;
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
- const onWorkerTurn = (turn: { turns: number; text?: string; thinking?: string; toolCalls?: Array<{ name: string }>; toolResults?: Array<{ name: string; isError?: boolean }> }) => {
1064
- try {
1065
- liveTurn = turn.turns;
1066
- const timestamp = Date.now();
1067
- const partialEntry = findPartial(liveTurn);
1068
- if (partialEntry) {
1069
- if (turn.text) partialEntry.text = turn.text;
1070
- if (turn.thinking) partialEntry.thinking = turn.thinking;
1071
- delete (partialEntry as any).partial;
1072
- } else if (turn.text) {
1073
- trace.steps.push({ turn: turn.turns, type: "assistant", text: turn.text.slice(0, 2000), timestamp });
1074
- }
1075
- for (const tc of turn.toolCalls ?? []) trace.steps.push({ turn: turn.turns, type: "tool_call", name: tc.name, timestamp });
1076
- for (const tr of turn.toolResults ?? []) trace.steps.push({ turn: turn.turns, type: "tool_result", name: tr.name, isError: tr.isError, timestamp });
1077
- trace.turns = Math.max(trace.turns, turn.turns);
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 { workerStream.off("text_delta", textListener); } catch {}
1138
- try { workerStream.off("thinking_delta", thinkingListener); } catch {}
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
- const wrapper: any = {
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
- const wrapper: any = {
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
  });