@illuma-ai/agents 1.1.28 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/spawnPath.cjs +104 -0
- package/dist/cjs/common/spawnPath.cjs.map +1 -0
- package/dist/cjs/graphs/Graph.cjs +84 -33
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +47 -8
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +493 -267
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
- package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/index.cjs +4 -3
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +113 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/memory/citations.cjs +69 -0
- package/dist/cjs/memory/citations.cjs.map +1 -0
- package/dist/cjs/memory/compositeBackend.cjs +60 -0
- package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
- package/dist/cjs/memory/constants.cjs +232 -0
- package/dist/cjs/memory/constants.cjs.map +1 -0
- package/dist/cjs/memory/embeddings.cjs +151 -0
- package/dist/cjs/memory/embeddings.cjs.map +1 -0
- package/dist/cjs/memory/factory.cjs +95 -0
- package/dist/cjs/memory/factory.cjs.map +1 -0
- package/dist/cjs/memory/migrate.cjs +81 -0
- package/dist/cjs/memory/migrate.cjs.map +1 -0
- package/dist/cjs/memory/mmr.cjs +138 -0
- package/dist/cjs/memory/mmr.cjs.map +1 -0
- package/dist/cjs/memory/paths.cjs +217 -0
- package/dist/cjs/memory/paths.cjs.map +1 -0
- package/dist/cjs/memory/pgvectorStore.cjs +225 -0
- package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
- package/dist/cjs/memory/recallTracking.cjs +98 -0
- package/dist/cjs/memory/recallTracking.cjs.map +1 -0
- package/dist/cjs/memory/schema.sql +51 -0
- package/dist/cjs/memory/temporalDecay.cjs +118 -0
- package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
- package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
- package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
- package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
- package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
- package/dist/cjs/run.cjs +16 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/AskUser.cjs +6 -1
- package/dist/cjs/tools/AskUser.cjs.map +1 -1
- package/dist/cjs/tools/BrowserTools.cjs +1 -1
- package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +127 -10
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/approval/constants.cjs +2 -2
- package/dist/cjs/tools/approval/constants.cjs.map +1 -1
- package/dist/cjs/tools/memory/index.cjs +58 -0
- package/dist/cjs/tools/memory/index.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/shared.cjs +106 -0
- package/dist/cjs/tools/memory/shared.cjs.map +1 -0
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/childAgentContext.cjs +242 -0
- package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +36 -7
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/finishReasons.cjs +44 -0
- package/dist/cjs/utils/finishReasons.cjs.map +1 -0
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/logging.cjs +34 -0
- package/dist/cjs/utils/logging.cjs.map +1 -0
- package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
- package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/spawnPath.mjs +95 -0
- package/dist/esm/common/spawnPath.mjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +84 -33
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
- package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
- package/dist/esm/llm/bedrock/index.mjs +4 -3
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +20 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/memory/citations.mjs +64 -0
- package/dist/esm/memory/citations.mjs.map +1 -0
- package/dist/esm/memory/compositeBackend.mjs +58 -0
- package/dist/esm/memory/compositeBackend.mjs.map +1 -0
- package/dist/esm/memory/constants.mjs +198 -0
- package/dist/esm/memory/constants.mjs.map +1 -0
- package/dist/esm/memory/embeddings.mjs +148 -0
- package/dist/esm/memory/embeddings.mjs.map +1 -0
- package/dist/esm/memory/factory.mjs +93 -0
- package/dist/esm/memory/factory.mjs.map +1 -0
- package/dist/esm/memory/migrate.mjs +78 -0
- package/dist/esm/memory/migrate.mjs.map +1 -0
- package/dist/esm/memory/mmr.mjs +130 -0
- package/dist/esm/memory/mmr.mjs.map +1 -0
- package/dist/esm/memory/paths.mjs +207 -0
- package/dist/esm/memory/paths.mjs.map +1 -0
- package/dist/esm/memory/pgvectorStore.mjs +223 -0
- package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
- package/dist/esm/memory/recallTracking.mjs +94 -0
- package/dist/esm/memory/recallTracking.mjs.map +1 -0
- package/dist/esm/memory/schema.sql +51 -0
- package/dist/esm/memory/temporalDecay.mjs +110 -0
- package/dist/esm/memory/temporalDecay.mjs.map +1 -0
- package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
- package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
- package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
- package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
- package/dist/esm/run.mjs +16 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/AskUser.mjs +6 -1
- package/dist/esm/tools/AskUser.mjs.map +1 -1
- package/dist/esm/tools/BrowserTools.mjs +1 -1
- package/dist/esm/tools/BrowserTools.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +128 -11
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/approval/constants.mjs +2 -2
- package/dist/esm/tools/approval/constants.mjs.map +1 -1
- package/dist/esm/tools/memory/index.mjs +46 -0
- package/dist/esm/tools/memory/index.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
- package/dist/esm/tools/memory/shared.mjs +98 -0
- package/dist/esm/tools/memory/shared.mjs.map +1 -0
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/childAgentContext.mjs +237 -0
- package/dist/esm/utils/childAgentContext.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +36 -8
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/finishReasons.mjs +41 -0
- package/dist/esm/utils/finishReasons.mjs.map +1 -0
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/logging.mjs +31 -0
- package/dist/esm/utils/logging.mjs.map +1 -0
- package/dist/esm/utils/toolCallNormalization.mjs +247 -0
- package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
- package/dist/types/common/index.d.ts +1 -0
- package/dist/types/common/spawnPath.d.ts +59 -0
- package/dist/types/graphs/HandoffRegistry.d.ts +24 -7
- package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
- package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
- package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
- package/dist/types/memory/citations.d.ts +39 -0
- package/dist/types/memory/compositeBackend.d.ts +30 -0
- package/dist/types/memory/constants.d.ts +121 -0
- package/dist/types/memory/embeddings.d.ts +15 -0
- package/dist/types/memory/factory.d.ts +23 -0
- package/dist/types/memory/index.d.ts +21 -0
- package/dist/types/memory/migrate.d.ts +14 -0
- package/dist/types/memory/mmr.d.ts +50 -0
- package/dist/types/memory/paths.d.ts +107 -0
- package/dist/types/memory/pgvectorStore.d.ts +56 -0
- package/dist/types/memory/recallTracking.d.ts +30 -0
- package/dist/types/memory/temporalDecay.d.ts +53 -0
- package/dist/types/memory/types.d.ts +182 -0
- package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/AskUser.d.ts +1 -1
- package/dist/types/tools/BrowserTools.d.ts +2 -2
- package/dist/types/tools/approval/constants.d.ts +2 -2
- package/dist/types/tools/memory/index.d.ts +39 -0
- package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
- package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
- package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
- package/dist/types/tools/memory/shared.d.ts +106 -0
- package/dist/types/types/graph.d.ts +10 -3
- package/dist/types/utils/childAgentContext.d.ts +99 -0
- package/dist/types/utils/events.d.ts +21 -0
- package/dist/types/utils/finishReasons.d.ts +32 -0
- package/dist/types/utils/logging.d.ts +2 -0
- package/dist/types/utils/toolCallNormalization.d.ts +44 -0
- package/package.json +6 -4
- package/src/agents/AgentContext.ts +12 -4
- package/src/common/__tests__/enum.test.ts +4 -2
- package/src/common/__tests__/spawnPath.test.ts +110 -0
- package/src/common/index.ts +1 -0
- package/src/common/spawnPath.ts +101 -0
- package/src/graphs/Graph.ts +90 -47
- package/src/graphs/HandoffRegistry.ts +48 -17
- package/src/graphs/MultiAgentGraph.ts +588 -327
- package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
- package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
- package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
- package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
- package/src/graphs/contextManagement.e2e.test.ts +1 -1
- package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
- package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
- package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
- package/src/graphs/phases/flushLoop.ts +303 -0
- package/src/graphs/phases/memoryFlushPhase.ts +209 -0
- package/src/index.ts +30 -1
- package/src/llm/bedrock/index.ts +4 -5
- package/src/memory/__tests__/citations.test.ts +61 -0
- package/src/memory/__tests__/compositeBackend.test.ts +79 -0
- package/src/memory/__tests__/isolation.test.ts +206 -0
- package/src/memory/__tests__/mmr.test.ts +148 -0
- package/src/memory/__tests__/mockBackend.ts +161 -0
- package/src/memory/__tests__/paths.test.ts +168 -0
- package/src/memory/__tests__/recallTracking.test.ts +96 -0
- package/src/memory/__tests__/temporalDecay.test.ts +151 -0
- package/src/memory/citations.ts +80 -0
- package/src/memory/compositeBackend.ts +99 -0
- package/src/memory/constants.ts +229 -0
- package/src/memory/embeddings.ts +188 -0
- package/src/memory/factory.ts +111 -0
- package/src/memory/index.ts +46 -0
- package/src/memory/migrate.ts +116 -0
- package/src/memory/mmr.ts +161 -0
- package/src/memory/paths.ts +258 -0
- package/src/memory/pgvectorStore.ts +324 -0
- package/src/memory/recallTracking.ts +127 -0
- package/src/memory/schema.sql +51 -0
- package/src/memory/temporalDecay.ts +134 -0
- package/src/memory/types.ts +185 -0
- package/src/nodes/ApprovalGateNode.ts +4 -10
- package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
- package/src/prompts/memoryFlushPrompt.ts +78 -0
- package/src/run.ts +17 -6
- package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
- package/src/specs/agent-handoffs.test.ts +8 -2
- package/src/tools/AskUser.ts +7 -2
- package/src/tools/BrowserTools.ts +3 -5
- package/src/tools/ToolNode.ts +150 -13
- package/src/tools/__tests__/ToolApproval.test.ts +22 -9
- package/src/tools/approval/__tests__/constants.test.ts +1 -1
- package/src/tools/approval/constants.ts +2 -2
- package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
- package/src/tools/memory/index.ts +96 -0
- package/src/tools/memory/memoryAppendTool.ts +101 -0
- package/src/tools/memory/memoryGetTool.ts +53 -0
- package/src/tools/memory/memorySearchTool.ts +80 -0
- package/src/tools/memory/shared.ts +169 -0
- package/src/tools/search/search.test.ts +6 -1
- package/src/types/graph.ts +10 -3
- package/src/utils/__tests__/childAgentContext.test.ts +217 -0
- package/src/utils/__tests__/finishReasons.test.ts +55 -0
- package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
- package/src/utils/childAgentContext.ts +259 -0
- package/src/utils/events.ts +37 -7
- package/src/utils/finishReasons.ts +40 -0
- package/src/utils/llm.ts +0 -1
- package/src/utils/logging.ts +45 -8
- package/src/utils/toolCallNormalization.ts +271 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawnPath — hierarchical invocation identity for nested multi-agent orchestration.
|
|
3
|
+
*
|
|
4
|
+
* A spawnPath is a slash-separated chain of spawnKeys from the root of the current
|
|
5
|
+
* agent invocation to the current spawn. The root agent has an empty spawnPath;
|
|
6
|
+
* each handoff/transfer/sequence that spawns a new subgraph appends a new spawnKey.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* "" → primary agent (no spawn)
|
|
10
|
+
* "call_abc" → first-level handoff child
|
|
11
|
+
* "call_abc/call_def" → grandchild (depth 2)
|
|
12
|
+
* "call_abc/call_def/call_ghi" → depth 3
|
|
13
|
+
*
|
|
14
|
+
* These utilities are the single source of truth for path manipulation across:
|
|
15
|
+
* - @illuma-ai/agents (MultiAgentGraph, HandoffRegistry, callbacks)
|
|
16
|
+
* - host api (initialize.js, callbacks.js, ExecutionTrace writes)
|
|
17
|
+
* - host client (subagent store, sidebar rendering)
|
|
18
|
+
*
|
|
19
|
+
* See docs/multi-agent-nesting-architecture.md for the full design.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Separator between spawnKeys in a spawnPath. Chosen so that the path looks
|
|
23
|
+
* like a filesystem/URL path, which makes it easy to read in logs and traces. */
|
|
24
|
+
export const SPAWN_PATH_SEP = '/';
|
|
25
|
+
|
|
26
|
+
/** Hard cap on nested multi-agent invocations. Prevents runaway recursion.
|
|
27
|
+
* Can be overridden at the host api layer via MULTI_AGENT_MAX_NESTING_DEPTH. */
|
|
28
|
+
export const MAX_NESTING_DEPTH = 5;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Append a spawnKey to a parent spawnPath.
|
|
32
|
+
*
|
|
33
|
+
* @param parent - Parent spawnPath (may be undefined/null/empty for root)
|
|
34
|
+
* @param key - spawnKey to append
|
|
35
|
+
* @returns New spawnPath string
|
|
36
|
+
*/
|
|
37
|
+
export function buildSpawnPath(
|
|
38
|
+
parent: string | undefined | null,
|
|
39
|
+
key: string
|
|
40
|
+
): string {
|
|
41
|
+
if (!key) {
|
|
42
|
+
throw new Error('[spawnPath] buildSpawnPath called with empty key');
|
|
43
|
+
}
|
|
44
|
+
if (parent == null || parent === '') return key;
|
|
45
|
+
return `${parent}${SPAWN_PATH_SEP}${key}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute the depth of a spawnPath.
|
|
50
|
+
* Root (empty) → 0; single-segment → 1; etc.
|
|
51
|
+
*/
|
|
52
|
+
export function spawnPathDepth(path: string | undefined | null): number {
|
|
53
|
+
if (path == null || path === '') return 0;
|
|
54
|
+
return path.split(SPAWN_PATH_SEP).filter(Boolean).length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return the parent spawnPath, or null if the input is already root.
|
|
59
|
+
*
|
|
60
|
+
* - parentSpawnPath("a/b/c") === "a/b"
|
|
61
|
+
* - parentSpawnPath("a") === ""
|
|
62
|
+
* - parentSpawnPath("") === null
|
|
63
|
+
*/
|
|
64
|
+
export function parentSpawnPath(
|
|
65
|
+
path: string | undefined | null
|
|
66
|
+
): string | null {
|
|
67
|
+
if (path == null || path === '') return null;
|
|
68
|
+
const parts = path.split(SPAWN_PATH_SEP).filter(Boolean);
|
|
69
|
+
if (parts.length <= 1) return '';
|
|
70
|
+
return parts.slice(0, -1).join(SPAWN_PATH_SEP);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Split a spawnPath into its constituent spawnKey segments. */
|
|
74
|
+
export function spawnPathParts(path: string | undefined | null): string[] {
|
|
75
|
+
if (path == null || path === '') return [];
|
|
76
|
+
return path.split(SPAWN_PATH_SEP).filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Return the last spawnKey in a spawnPath (the "current" spawn).
|
|
81
|
+
* Returns null for root.
|
|
82
|
+
*/
|
|
83
|
+
export function leafSpawnKey(path: string | undefined | null): string | null {
|
|
84
|
+
const parts = spawnPathParts(path);
|
|
85
|
+
return parts.length === 0 ? null : parts[parts.length - 1];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* True if `ancestor` is a strict ancestor of `descendant`. Root ("") is
|
|
90
|
+
* ancestor of everything except itself.
|
|
91
|
+
*/
|
|
92
|
+
export function isAncestorSpawnPath(
|
|
93
|
+
ancestor: string | undefined | null,
|
|
94
|
+
descendant: string | undefined | null
|
|
95
|
+
): boolean {
|
|
96
|
+
const a = ancestor ?? '';
|
|
97
|
+
const d = descendant ?? '';
|
|
98
|
+
if (a === d) return false;
|
|
99
|
+
if (a === '') return d !== '';
|
|
100
|
+
return d.startsWith(a + SPAWN_PATH_SEP);
|
|
101
|
+
}
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -79,6 +79,9 @@ import { getChatModelClass, manualToolStreamProviders } from '@/llm/providers';
|
|
|
79
79
|
import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
|
|
80
80
|
import { ChatOpenAI, AzureChatOpenAI } from '@/llm/openai';
|
|
81
81
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
82
|
+
import { mlog, mwarn } from '@/utils/logging';
|
|
83
|
+
import { normalizeMessageToolCalls } from '@/utils/toolCallNormalization';
|
|
84
|
+
import { isTruncationReason } from '@/utils/finishReasons';
|
|
82
85
|
import {
|
|
83
86
|
detectDocuments,
|
|
84
87
|
shouldInjectMultiDocHint,
|
|
@@ -1144,10 +1147,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1144
1147
|
const resolved = agentContext.resolveStructuredOutputMode();
|
|
1145
1148
|
method = resolved.method;
|
|
1146
1149
|
if (resolved.warnings.length > 0) {
|
|
1147
|
-
|
|
1148
|
-
'[Graph] Structured output mode warnings:',
|
|
1149
|
-
resolved.warnings
|
|
1150
|
-
);
|
|
1150
|
+
mwarn('[Graph] Structured output mode warnings:', resolved.warnings);
|
|
1151
1151
|
}
|
|
1152
1152
|
} else {
|
|
1153
1153
|
// Legacy fallback: use the old mode-based resolution
|
|
@@ -1172,7 +1172,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1172
1172
|
);
|
|
1173
1173
|
preparedSchema = prepared;
|
|
1174
1174
|
if (warnings.length > 0) {
|
|
1175
|
-
|
|
1175
|
+
mwarn('[Graph] Schema preparation warnings:', warnings);
|
|
1176
1176
|
}
|
|
1177
1177
|
}
|
|
1178
1178
|
|
|
@@ -1264,7 +1264,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1264
1264
|
? handleErrors
|
|
1265
1265
|
: `The response did not match the expected schema. Error: ${lastError.message}. Please try again with a valid response.`;
|
|
1266
1266
|
|
|
1267
|
-
|
|
1267
|
+
mwarn(
|
|
1268
1268
|
`[Graph] Structured output attempt ${attempts} failed: ${lastError.message}. Retrying...`
|
|
1269
1269
|
);
|
|
1270
1270
|
|
|
@@ -1467,7 +1467,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1467
1467
|
this._toolDiscoveryCache.getNewDiscoveries(messages);
|
|
1468
1468
|
if (cachedDiscoveries.length > 0) {
|
|
1469
1469
|
agentContext.markToolsAsDiscovered(cachedDiscoveries);
|
|
1470
|
-
|
|
1470
|
+
mlog(
|
|
1471
1471
|
`[Graph:ToolDiscovery] Cached ${cachedDiscoveries.length} new tools (total: ${this._toolDiscoveryCache.size})`
|
|
1472
1472
|
);
|
|
1473
1473
|
}
|
|
@@ -1498,11 +1498,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1498
1498
|
});
|
|
1499
1499
|
|
|
1500
1500
|
// DEBUG: Log which model and tools each agent uses during handoff
|
|
1501
|
-
|
|
1501
|
+
mlog(
|
|
1502
1502
|
`[createCallModel] Agent "${agentId}" invoking LLM | provider=${agentContext.provider} | ` +
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1503
|
+
`model=${(effectiveClientOptions as Record<string, unknown>).model ?? 'default'} | ` +
|
|
1504
|
+
`toolsForBinding=${toolsForBinding?.length ?? 0} | ` +
|
|
1505
|
+
`toolNames=[${(toolsForBinding ?? []).map((t) => (t as { name?: string }).name ?? 'unknown').join(', ')}]`
|
|
1506
1506
|
);
|
|
1507
1507
|
|
|
1508
1508
|
if (agentContext.systemRunnable) {
|
|
@@ -1515,7 +1515,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1515
1515
|
if (!config.signal) {
|
|
1516
1516
|
config.signal = this.signal;
|
|
1517
1517
|
}
|
|
1518
|
-
this.config
|
|
1518
|
+
// First-writer-wins: `this.config` is used ONLY as a "has a run started"
|
|
1519
|
+
// existence flag by the dispatch* methods (they never read its value —
|
|
1520
|
+
// they read the current RunnableConfig from LangChain AsyncLocalStorage).
|
|
1521
|
+
// Unconditionally reassigning here races across concurrent child
|
|
1522
|
+
// subgraph.invoke() calls under parallel multi-agent handoffs; the last
|
|
1523
|
+
// writer wins, and any dispatch firing between writes would historically
|
|
1524
|
+
// have been tagged with the wrong child's metadata. Keeping the first
|
|
1525
|
+
// write pinned makes this a true flag, eliminating the race.
|
|
1526
|
+
this.config ??= config;
|
|
1519
1527
|
|
|
1520
1528
|
let messagesToUse = messages;
|
|
1521
1529
|
|
|
@@ -1619,7 +1627,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1619
1627
|
|
|
1620
1628
|
if (oldMessages.length > 0) {
|
|
1621
1629
|
this._summaryInFlight = true;
|
|
1622
|
-
|
|
1630
|
+
mlog(
|
|
1623
1631
|
`[Graph:ProactiveSummary] Context at ${utilization.toFixed(1)}% (threshold ${threshold}%) — summarizing ${oldMessages.length} older msgs in background`
|
|
1624
1632
|
);
|
|
1625
1633
|
|
|
@@ -1628,7 +1636,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1628
1636
|
.then((updated) => {
|
|
1629
1637
|
if (updated != null && updated !== '') {
|
|
1630
1638
|
this._cachedRunSummary = updated;
|
|
1631
|
-
|
|
1639
|
+
mlog(
|
|
1632
1640
|
`[Graph:ProactiveSummary] Background summary ready (len=${updated.length})`
|
|
1633
1641
|
);
|
|
1634
1642
|
}
|
|
@@ -1838,7 +1846,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1838
1846
|
}
|
|
1839
1847
|
agentContext.indexTokenCountMap = viewTokenMap;
|
|
1840
1848
|
|
|
1841
|
-
|
|
1849
|
+
mlog(
|
|
1842
1850
|
`[Graph:Compaction] ${messages.length}→${viewParts.length} msgs | ` +
|
|
1843
1851
|
`compacted=${compactedMessages.length} window=${recentMessages.length} | ` +
|
|
1844
1852
|
`summary=${summarySource} | budget=${usedTokens}/${recentBudget}` +
|
|
@@ -1862,7 +1870,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1862
1870
|
if (shouldSummarize) {
|
|
1863
1871
|
if (this._summaryInFlight) {
|
|
1864
1872
|
this._pendingMessagesToRefine.push(...compactedMessages);
|
|
1865
|
-
|
|
1873
|
+
mlog(
|
|
1866
1874
|
`[Graph:Compaction] Summary in-flight, queued ${compactedMessages.length} msgs (pending=${this._pendingMessagesToRefine.length})`
|
|
1867
1875
|
);
|
|
1868
1876
|
} else {
|
|
@@ -1915,7 +1923,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1915
1923
|
deduplicateSystemMessages(messagesToUse);
|
|
1916
1924
|
if (removedCount > 0) {
|
|
1917
1925
|
messagesToUse = dedupedMessages;
|
|
1918
|
-
|
|
1926
|
+
mlog(
|
|
1919
1927
|
`[Graph:Dedup] Removed ${removedCount} duplicate system message(s)`
|
|
1920
1928
|
);
|
|
1921
1929
|
}
|
|
@@ -2039,7 +2047,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2039
2047
|
);
|
|
2040
2048
|
}
|
|
2041
2049
|
|
|
2042
|
-
|
|
2043
2050
|
// Get model info for analytics
|
|
2044
2051
|
const bedrockOpts = agentContext.clientOptions as
|
|
2045
2052
|
| t.BedrockAnthropicClientOptions
|
|
@@ -2153,11 +2160,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2153
2160
|
|
|
2154
2161
|
// Log when we detect the error
|
|
2155
2162
|
if (isInputTooLongError) {
|
|
2156
|
-
|
|
2163
|
+
mwarn(
|
|
2157
2164
|
'[Graph] Detected input too long error:',
|
|
2158
2165
|
errorMessage.substring(0, 200)
|
|
2159
2166
|
);
|
|
2160
|
-
|
|
2167
|
+
mwarn('[Graph] Checking emergency pruning conditions:', {
|
|
2161
2168
|
hasPruneMessages: !!agentContext.pruneMessages,
|
|
2162
2169
|
hasTokenCounter: !!agentContext.tokenCounter,
|
|
2163
2170
|
maxContextTokens: agentContext.maxContextTokens,
|
|
@@ -2182,7 +2189,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2182
2189
|
const reducedMaxTokens = Math.floor(
|
|
2183
2190
|
agentContext.maxContextTokens! * reductionFactor
|
|
2184
2191
|
);
|
|
2185
|
-
|
|
2192
|
+
mwarn(
|
|
2186
2193
|
`[Graph] Input too long. Retrying with ${reductionFactor * 100}% context (${reducedMaxTokens} tokens)...`
|
|
2187
2194
|
);
|
|
2188
2195
|
|
|
@@ -2190,7 +2197,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2190
2197
|
// This is needed when messages were dynamically added without updating the token map
|
|
2191
2198
|
let tokenMapForPruning = agentContext.indexTokenCountMap;
|
|
2192
2199
|
if (Object.keys(tokenMapForPruning).length < messages.length) {
|
|
2193
|
-
|
|
2200
|
+
mwarn(
|
|
2194
2201
|
'[Graph] Building fresh token count map for emergency pruning...'
|
|
2195
2202
|
);
|
|
2196
2203
|
tokenMapForPruning = {};
|
|
@@ -2215,7 +2222,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2215
2222
|
|
|
2216
2223
|
// Skip if we can't fit any messages
|
|
2217
2224
|
if (reducedMessages.length === 0) {
|
|
2218
|
-
|
|
2225
|
+
mwarn(
|
|
2219
2226
|
`[Graph] Cannot fit any messages at ${reductionFactor * 100}% reduction, trying next level...`
|
|
2220
2227
|
);
|
|
2221
2228
|
continue;
|
|
@@ -2298,7 +2305,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2298
2305
|
retryErrorMsg.includes('validationexception');
|
|
2299
2306
|
|
|
2300
2307
|
if (stillTooLong && reductionFactor > 0.1) {
|
|
2301
|
-
|
|
2308
|
+
mwarn(
|
|
2302
2309
|
`[Graph] Still too long at ${reductionFactor * 100}%, trying more aggressive pruning...`
|
|
2303
2310
|
);
|
|
2304
2311
|
} else {
|
|
@@ -2370,6 +2377,27 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2370
2377
|
* handled everything — both paths become no-ops.
|
|
2371
2378
|
*/
|
|
2372
2379
|
const responseMessage = result.messages?.[0];
|
|
2380
|
+
|
|
2381
|
+
// Tool-call name normalization — catches LLM output that names tools
|
|
2382
|
+
// with wrong delimiters (outlook/operations), prefixes
|
|
2383
|
+
// (functions.outlook_operations), case drift, counter suffixes, or
|
|
2384
|
+
// empty names recoverable from the tool_call id. Mutates in place so
|
|
2385
|
+
// the downstream ToolNode dispatch sees the corrected names.
|
|
2386
|
+
if (responseMessage && agentContext.toolMap) {
|
|
2387
|
+
const allowedNames = new Set(Object.keys(agentContext.toolMap));
|
|
2388
|
+
if (allowedNames.size > 0) {
|
|
2389
|
+
const rewrote = normalizeMessageToolCalls(
|
|
2390
|
+
responseMessage,
|
|
2391
|
+
allowedNames
|
|
2392
|
+
);
|
|
2393
|
+
if (rewrote) {
|
|
2394
|
+
mlog(
|
|
2395
|
+
`[Graph] normalized tool_call names on agent "${agentId}" response`
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2373
2401
|
const toolCalls = (responseMessage as AIMessageChunk | undefined)
|
|
2374
2402
|
?.tool_calls;
|
|
2375
2403
|
const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
|
|
@@ -2485,12 +2513,22 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2485
2513
|
const messageStop = meta.messageStop as
|
|
2486
2514
|
| Record<string, unknown>
|
|
2487
2515
|
| undefined;
|
|
2488
|
-
|
|
2516
|
+
const nextReason =
|
|
2489
2517
|
(meta.finish_reason as string | undefined) ?? // OpenAI/Azure
|
|
2490
2518
|
(meta.stop_reason as string | undefined) ?? // Anthropic direct API
|
|
2491
2519
|
(meta.stopReason as string | undefined) ?? // Bedrock invoke (non-streaming)
|
|
2492
2520
|
(messageStop?.stopReason as string | undefined) ?? // Bedrock streaming
|
|
2493
2521
|
(meta.finishReason as string | undefined); // VertexAI/Google
|
|
2522
|
+
|
|
2523
|
+
// Sticky on truncation: a single Graph instance is reused across
|
|
2524
|
+
// every scoped-subgraph inner node invocation (see MultiAgentGraph
|
|
2525
|
+
// buildScopedSubgraph). If an earlier inner node hit max_tokens
|
|
2526
|
+
// but a later inner node finished cleanly, the host's continuation layer
|
|
2527
|
+
// would miss the truncation signal unless we preserve it. Keep the
|
|
2528
|
+
// truncation reason pinned so the outer caller can retry.
|
|
2529
|
+
if (!isTruncationReason(this.lastFinishReason)) {
|
|
2530
|
+
this.lastFinishReason = nextReason;
|
|
2531
|
+
}
|
|
2494
2532
|
}
|
|
2495
2533
|
|
|
2496
2534
|
this.cleanupSignalListener();
|
|
@@ -2553,7 +2591,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2553
2591
|
'[Graph] Deferred structured output failed after successful tool use:',
|
|
2554
2592
|
structuredError
|
|
2555
2593
|
);
|
|
2556
|
-
|
|
2594
|
+
mwarn(
|
|
2557
2595
|
'[Graph] Falling back to unstructured response from tool-use phase'
|
|
2558
2596
|
);
|
|
2559
2597
|
return result;
|
|
@@ -2578,7 +2616,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2578
2616
|
state: t.BaseGraphState,
|
|
2579
2617
|
config?: RunnableConfig
|
|
2580
2618
|
): string => {
|
|
2581
|
-
this.config
|
|
2619
|
+
// First-writer-wins — see note in createCallModel. `this.config` is an
|
|
2620
|
+
// existence flag only; assigning unconditionally would race under
|
|
2621
|
+
// parallel child subgraph.invoke().
|
|
2622
|
+
this.config ??= config;
|
|
2582
2623
|
return toolsCondition(state, toolNode, this.invokedToolIds);
|
|
2583
2624
|
};
|
|
2584
2625
|
|
|
@@ -2623,10 +2664,16 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2623
2664
|
default: () => [],
|
|
2624
2665
|
}),
|
|
2625
2666
|
});
|
|
2667
|
+
// Pass compileOptions (including the HITL checkpointer) to the OUTER
|
|
2668
|
+
// workflow — not just the inner agent subgraph. hasInterrupts() calls
|
|
2669
|
+
// getState() on the outer compiled graph; without a checkpointer here,
|
|
2670
|
+
// getState reports zero tasks and the HITL interrupt/resume loop breaks
|
|
2671
|
+
// out immediately even though interrupt() fired correctly inside the
|
|
2672
|
+
// agent subgraph.
|
|
2626
2673
|
const workflow = new StateGraph(StateAnnotation)
|
|
2627
2674
|
.addNode(this.defaultAgentId, agentNode, { ends: [END] })
|
|
2628
2675
|
.addEdge(START, this.defaultAgentId)
|
|
2629
|
-
.compile();
|
|
2676
|
+
.compile(this.compileOptions as unknown as never);
|
|
2630
2677
|
|
|
2631
2678
|
return workflow;
|
|
2632
2679
|
}
|
|
@@ -2709,7 +2756,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2709
2756
|
}
|
|
2710
2757
|
} catch (_e) {
|
|
2711
2758
|
/** If we can't get agent context, that's okay - agentId remains undefined */
|
|
2712
|
-
|
|
2759
|
+
mlog(
|
|
2713
2760
|
`[dispatchRunStep] Could not resolve agentId from metadata.langgraph_node="${(metadata as Record<string, unknown>).langgraph_node}": ${(_e as Error).message}`
|
|
2714
2761
|
);
|
|
2715
2762
|
}
|
|
@@ -2717,11 +2764,11 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2717
2764
|
|
|
2718
2765
|
this.contentData.push(runStep);
|
|
2719
2766
|
this.contentIndexMap.set(stepId, runStep.index);
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
);
|
|
2767
|
+
// Pass undefined so safeDispatchCustomEvent resolves the runnable config
|
|
2768
|
+
// from LangChain's AsyncLocalStorage. Using the shared `this.config` would
|
|
2769
|
+
// race across concurrent child subgraph.invoke calls under parallel
|
|
2770
|
+
// multi-agent handoffs and tag events with the wrong child's spawnKey.
|
|
2771
|
+
await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP, runStep);
|
|
2725
2772
|
return stepId;
|
|
2726
2773
|
}
|
|
2727
2774
|
|
|
@@ -2862,7 +2909,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2862
2909
|
}
|
|
2863
2910
|
|
|
2864
2911
|
if (!data.id) {
|
|
2865
|
-
|
|
2912
|
+
mwarn('No Tool ID provided for Tool Error');
|
|
2866
2913
|
return;
|
|
2867
2914
|
}
|
|
2868
2915
|
|
|
@@ -2927,11 +2974,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2927
2974
|
id,
|
|
2928
2975
|
delta,
|
|
2929
2976
|
};
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
);
|
|
2977
|
+
// See dispatchRunStep note: do not pass `this.config`. The implicit
|
|
2978
|
+
// AsyncLocalStorage config is the correct per-async-branch source under
|
|
2979
|
+
// parallel handoffs.
|
|
2980
|
+
await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_DELTA, runStepDelta);
|
|
2935
2981
|
}
|
|
2936
2982
|
|
|
2937
2983
|
async dispatchMessageDelta(id: string, delta: t.MessageDelta): Promise<void> {
|
|
@@ -2942,11 +2988,8 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2942
2988
|
id,
|
|
2943
2989
|
delta,
|
|
2944
2990
|
};
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
messageDelta,
|
|
2948
|
-
this.config
|
|
2949
|
-
);
|
|
2991
|
+
// See dispatchRunStep note.
|
|
2992
|
+
await safeDispatchCustomEvent(GraphEvents.ON_MESSAGE_DELTA, messageDelta);
|
|
2950
2993
|
}
|
|
2951
2994
|
|
|
2952
2995
|
dispatchReasoningDelta = async (
|
|
@@ -2960,10 +3003,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2960
3003
|
id: stepId,
|
|
2961
3004
|
delta,
|
|
2962
3005
|
};
|
|
3006
|
+
// See dispatchRunStep note.
|
|
2963
3007
|
await safeDispatchCustomEvent(
|
|
2964
3008
|
GraphEvents.ON_REASONING_DELTA,
|
|
2965
|
-
reasoningDelta
|
|
2966
|
-
this.config
|
|
3009
|
+
reasoningDelta
|
|
2967
3010
|
);
|
|
2968
3011
|
};
|
|
2969
3012
|
}
|
|
@@ -3,11 +3,12 @@ import type * as t from '@/types';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Tracks the lifecycle of a spawned handoff child agent.
|
|
6
|
-
* Mirrors OpenClaw's SubagentRunRecord pattern.
|
|
7
6
|
*/
|
|
8
7
|
export type HandoffRecord = {
|
|
9
|
-
/**
|
|
8
|
+
/** Agent identity (destination agentId) — stable across multiple spawns of the same agent */
|
|
10
9
|
id: string;
|
|
10
|
+
/** Unique internal key for this specific spawn (agentId + monotonic counter) */
|
|
11
|
+
spawnKey: string;
|
|
11
12
|
/** Display name of the child agent */
|
|
12
13
|
name: string;
|
|
13
14
|
/** Task description / instructions passed to child */
|
|
@@ -31,7 +32,7 @@ export type HandoffRecord = {
|
|
|
31
32
|
/**
|
|
32
33
|
* Registry for async handoff execution.
|
|
33
34
|
*
|
|
34
|
-
* Enables the
|
|
35
|
+
* Enables the autonomous orchestration pattern:
|
|
35
36
|
* 1. Orchestrator spawns children (non-blocking)
|
|
36
37
|
* 2. Orchestrator stays alive to reason, spawn more, or check status
|
|
37
38
|
* 3. Orchestrator collects results when ready
|
|
@@ -40,10 +41,14 @@ export type HandoffRecord = {
|
|
|
40
41
|
*/
|
|
41
42
|
export class HandoffRegistry {
|
|
42
43
|
private records: Map<string, HandoffRecord> = new Map();
|
|
44
|
+
/** Monotonically increasing counter for unique spawn IDs */
|
|
45
|
+
private spawnCounter = 0;
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Register a spawned handoff child.
|
|
46
49
|
* The promise runs in the background — not awaited here.
|
|
50
|
+
* Uses a unique key per spawn so the same agent can be spawned multiple times
|
|
51
|
+
* across rounds without overwriting prior records.
|
|
47
52
|
*/
|
|
48
53
|
spawn(params: {
|
|
49
54
|
id: string;
|
|
@@ -56,8 +61,13 @@ export class HandoffRegistry {
|
|
|
56
61
|
/** Callback when child completes (for SSE events) */
|
|
57
62
|
onComplete?: (record: HandoffRecord) => void;
|
|
58
63
|
}): void {
|
|
64
|
+
// Unique internal key: agentId + counter to support multiple spawns of same agent.
|
|
65
|
+
// record.id stays as the agent identity so callers/observability can attribute
|
|
66
|
+
// results to the agent regardless of which spawn round they came from.
|
|
67
|
+
const spawnKey = `${params.id}__${this.spawnCounter++}`;
|
|
59
68
|
const record: HandoffRecord = {
|
|
60
69
|
id: params.id,
|
|
70
|
+
spawnKey,
|
|
61
71
|
name: params.name,
|
|
62
72
|
task: params.task,
|
|
63
73
|
spawnedAt: Date.now(),
|
|
@@ -68,10 +78,7 @@ export class HandoffRegistry {
|
|
|
68
78
|
// Wire up the promise to update the record on completion
|
|
69
79
|
params.promise
|
|
70
80
|
.then((result) => {
|
|
71
|
-
const resultText = params.extractResult(
|
|
72
|
-
result.messages,
|
|
73
|
-
params.id
|
|
74
|
-
);
|
|
81
|
+
const resultText = params.extractResult(result.messages, params.id);
|
|
75
82
|
const truncated = params.truncateResult(
|
|
76
83
|
resultText,
|
|
77
84
|
params.maxResultChars
|
|
@@ -89,7 +96,7 @@ export class HandoffRegistry {
|
|
|
89
96
|
params.onComplete?.(record);
|
|
90
97
|
});
|
|
91
98
|
|
|
92
|
-
this.records.set(
|
|
99
|
+
this.records.set(spawnKey, record);
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
/** List all pending (running) handoffs */
|
|
@@ -111,9 +118,21 @@ export class HandoffRegistry {
|
|
|
111
118
|
return Array.from(this.records.values());
|
|
112
119
|
}
|
|
113
120
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Get a handoff record by either its unique spawnKey or by agentId.
|
|
123
|
+
* - Exact spawnKey match wins (O(1)).
|
|
124
|
+
* - Falls back to the most recently spawned record for that agentId — matching
|
|
125
|
+
* lookups by callers that only know the agent identity, not the spawn round.
|
|
126
|
+
*/
|
|
127
|
+
get(idOrSpawnKey: string): HandoffRecord | undefined {
|
|
128
|
+
const direct = this.records.get(idOrSpawnKey);
|
|
129
|
+
if (direct) return direct;
|
|
130
|
+
let latest: HandoffRecord | undefined;
|
|
131
|
+
for (const record of this.records.values()) {
|
|
132
|
+
if (record.id !== idOrSpawnKey) continue;
|
|
133
|
+
if (!latest || record.spawnedAt >= latest.spawnedAt) latest = record;
|
|
134
|
+
}
|
|
135
|
+
return latest;
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
/** Check if any handoffs are still running */
|
|
@@ -122,15 +141,16 @@ export class HandoffRegistry {
|
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
/**
|
|
125
|
-
* Wait for ALL pending handoffs to complete.
|
|
126
|
-
*
|
|
144
|
+
* Wait for ALL pending handoffs to complete and return all records.
|
|
145
|
+
* Records are NOT auto-cleared — caller removes collected records via remove().
|
|
127
146
|
*/
|
|
128
147
|
async waitForAll(): Promise<HandoffRecord[]> {
|
|
129
148
|
const pending = this.listPending();
|
|
130
149
|
if (pending.length > 0) {
|
|
131
150
|
await Promise.allSettled(pending.map((r) => r.promise));
|
|
132
151
|
}
|
|
133
|
-
|
|
152
|
+
const results = this.listAll();
|
|
153
|
+
return results;
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
/**
|
|
@@ -145,9 +165,7 @@ export class HandoffRegistry {
|
|
|
145
165
|
|
|
146
166
|
// Race all pending promises — at least one will resolve
|
|
147
167
|
await Promise.race(
|
|
148
|
-
pending.map((r) =>
|
|
149
|
-
r.promise.then(() => r).catch(() => r)
|
|
150
|
-
)
|
|
168
|
+
pending.map((r) => r.promise.then(() => r).catch(() => r))
|
|
151
169
|
);
|
|
152
170
|
|
|
153
171
|
// Small yield to let promise handlers update records
|
|
@@ -156,6 +174,19 @@ export class HandoffRegistry {
|
|
|
156
174
|
return this.listCompleted();
|
|
157
175
|
}
|
|
158
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Remove record(s) by spawnKey or agentId.
|
|
179
|
+
* - Exact spawnKey match removes only that record.
|
|
180
|
+
* - agentId match removes ALL records for that agent (covers callers that
|
|
181
|
+
* want to forget everything tied to a given agent).
|
|
182
|
+
*/
|
|
183
|
+
remove(idOrSpawnKey: string): void {
|
|
184
|
+
if (this.records.delete(idOrSpawnKey)) return;
|
|
185
|
+
for (const [key, record] of this.records) {
|
|
186
|
+
if (record.id === idOrSpawnKey) this.records.delete(key);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
159
190
|
/** Clear all records (for cleanup between graph invocations) */
|
|
160
191
|
clear(): void {
|
|
161
192
|
this.records.clear();
|