@illuma-ai/agents 1.1.25 → 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 +20 -3
- 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 +87 -31
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
- package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
- 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 +115 -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/stream.cjs +4 -4
- package/dist/cjs/stream.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 -4
- 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 +20 -3
- 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 +87 -31
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
- package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
- 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 +21 -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/stream.mjs +4 -4
- package/dist/esm/stream.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 -5
- 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 +97 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
- package/dist/types/graphs/index.d.ts +1 -0
- 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 +16 -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 +26 -3
- 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 +94 -43
- package/src/graphs/HandoffRegistry.ts +199 -0
- package/src/graphs/MultiAgentGraph.ts +694 -226
- package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
- 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/index.ts +1 -0
- 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/stream.ts +4 -6
- 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 +4 -4
- 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 +16 -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 -4
- 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,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-agent context preparation utilities.
|
|
3
|
+
*
|
|
4
|
+
* When a parent agent invokes a child agent — via handoff, sequence edge,
|
|
5
|
+
* or scoped subgraph — the child cannot just receive `state.messages`
|
|
6
|
+
* verbatim. The parent's conversation contains tool_use/tool_result blocks
|
|
7
|
+
* that are (a) often incompatible with the child's tool registry, and
|
|
8
|
+
* (b) actively harmful to the child's ability to reason cleanly about its
|
|
9
|
+
* own task (noise → schema confusion → malformed tool_use).
|
|
10
|
+
*
|
|
11
|
+
* This module provides the two canonical strategies, extracted from
|
|
12
|
+
* `MultiAgentGraph` so they can be unit-tested in isolation and reused by
|
|
13
|
+
* future sub-agent orchestrators:
|
|
14
|
+
*
|
|
15
|
+
* 1. `prepareHandoffMessages` — "cleaned parent history"
|
|
16
|
+
* Used when the child still needs the orchestrator's context (it's
|
|
17
|
+
* the handoff target). Drops orphaned tool_use, compacts paired
|
|
18
|
+
* tool_use/tool_result into text summaries, and guarantees the tail
|
|
19
|
+
* is a HumanMessage so Bedrock/VertexAI won't reject the conversation
|
|
20
|
+
* with "assistant message prefill" errors.
|
|
21
|
+
*
|
|
22
|
+
* 2. `prepareIsolatedChildMessages` — "fresh session"
|
|
23
|
+
* Used for downstream sequence-node children inside a scoped subgraph.
|
|
24
|
+
* The child sees only the original user request plus a synthetic
|
|
25
|
+
* HumanMessage summarizing the upstream agent's final text output and
|
|
26
|
+
* directing the child to act. Raw upstream tool_use/tool_result blocks
|
|
27
|
+
* are discarded.
|
|
28
|
+
*
|
|
29
|
+
* Both helpers are pure functions over message arrays — no I/O, no
|
|
30
|
+
* LangGraph coupling — so they can be exercised by unit tests with
|
|
31
|
+
* synthetic message fixtures.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
35
|
+
import type { AIMessageChunk, BaseMessage } from '@langchain/core/messages';
|
|
36
|
+
|
|
37
|
+
/* -------------------------------------------------------------------------- */
|
|
38
|
+
/* Prompt template constants (kept outside the functions for reuse/tuning) */
|
|
39
|
+
/* -------------------------------------------------------------------------- */
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Prefix injected in front of a trailing AIMessage when we flip it to a
|
|
43
|
+
* HumanMessage to satisfy provider "last message must be user" rules.
|
|
44
|
+
*/
|
|
45
|
+
export const HANDOFF_TAIL_CONTEXT_PREFIX = '[Context from orchestrator]: ';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Directive task-framing wrapper for downstream scoped-subgraph children.
|
|
49
|
+
*
|
|
50
|
+
* Design notes — each line is load-bearing:
|
|
51
|
+
* - "Prior step output" names the upstream role without leaking the
|
|
52
|
+
* agent's internal id.
|
|
53
|
+
* - "You MUST now perform..." replaces ambiguity with obligation.
|
|
54
|
+
* - "system instructions" references the agent's stored system prompt
|
|
55
|
+
* as the source of task definition — so operators can tune behavior
|
|
56
|
+
* via data, not code.
|
|
57
|
+
* - The tool-first clause prevents small/fast models from stalling on a
|
|
58
|
+
* text-only acknowledgement when a tool action is expected.
|
|
59
|
+
*/
|
|
60
|
+
export function buildIsolatedChildPrompt(upstreamText: string): string {
|
|
61
|
+
return (
|
|
62
|
+
'## Prior step output\n\n' +
|
|
63
|
+
upstreamText +
|
|
64
|
+
'\n\n---\n\n' +
|
|
65
|
+
'## Your task\n\n' +
|
|
66
|
+
'The previous step in this workflow has completed. You MUST now ' +
|
|
67
|
+
'perform your own task as defined in your system instructions, ' +
|
|
68
|
+
"using the prior step's output as input where relevant.\n\n" +
|
|
69
|
+
'If your task requires calling a tool, call it directly — do not ' +
|
|
70
|
+
'ask for clarification and do not produce a text-only response when ' +
|
|
71
|
+
'a tool action is expected.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* -------------------------------------------------------------------------- */
|
|
76
|
+
/* Internal helpers */
|
|
77
|
+
/* -------------------------------------------------------------------------- */
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract concatenated text content from an AI message's content field.
|
|
81
|
+
* Handles both the string shape (OpenAI/plain) and the array-of-blocks
|
|
82
|
+
* shape (Anthropic/Bedrock).
|
|
83
|
+
*/
|
|
84
|
+
function extractAIText(msg: AIMessage | AIMessageChunk): string {
|
|
85
|
+
const content = msg.content;
|
|
86
|
+
if (typeof content === 'string') return content;
|
|
87
|
+
if (!Array.isArray(content)) return '';
|
|
88
|
+
return (content as Array<{ type?: string; text?: string }>)
|
|
89
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
90
|
+
.map((b) => b.text ?? '')
|
|
91
|
+
.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* -------------------------------------------------------------------------- */
|
|
95
|
+
/* Strategy 1: cleaned parent history (handoff target / root subgraph) */
|
|
96
|
+
/* -------------------------------------------------------------------------- */
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Prepare messages for a handoff child agent.
|
|
100
|
+
*
|
|
101
|
+
* Handles two problems that break Bedrock/Anthropic conversations:
|
|
102
|
+
*
|
|
103
|
+
* 1. **Orphaned tool_use**: The parent's AI message contains a `tool_use`
|
|
104
|
+
* block for the handoff tool itself, with no matching `tool_result`.
|
|
105
|
+
* Providers (Bedrock/Anthropic) reject this.
|
|
106
|
+
*
|
|
107
|
+
* 2. **Paired tool_use/tool_result in history**: The child may not have
|
|
108
|
+
* the same tools as the parent. Bedrock requires `toolConfig` when any
|
|
109
|
+
* tool_use/tool_result blocks exist in the history. Compacting these
|
|
110
|
+
* into text summaries avoids the requirement and reduces context bloat.
|
|
111
|
+
*
|
|
112
|
+
* Also ensures the tail is a HumanMessage — some providers reject a
|
|
113
|
+
* conversation that ends with an assistant message.
|
|
114
|
+
*
|
|
115
|
+
* @param messages - Current state messages from the parent
|
|
116
|
+
* @returns A sanitized copy, safe to pass to any provider as the child's
|
|
117
|
+
* input regardless of which tools the child has registered.
|
|
118
|
+
*/
|
|
119
|
+
export function prepareHandoffMessages(messages: BaseMessage[]): BaseMessage[] {
|
|
120
|
+
if (messages.length === 0) return messages;
|
|
121
|
+
|
|
122
|
+
/** Collect tool_result IDs so we know which tool_use blocks are paired */
|
|
123
|
+
const pairedToolCallIds = new Set<string>();
|
|
124
|
+
for (const msg of messages) {
|
|
125
|
+
if (msg.getType() === 'tool') {
|
|
126
|
+
const tm = msg as ToolMessage;
|
|
127
|
+
if (tm.tool_call_id) pairedToolCallIds.add(tm.tool_call_id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Pass 1: Drop all ToolMessages (paired ones are compacted in pass 2),
|
|
133
|
+
* rewrite AI messages with tool_calls into plain-text summaries, leave
|
|
134
|
+
* other messages untouched.
|
|
135
|
+
*/
|
|
136
|
+
const cleaned: BaseMessage[] = [];
|
|
137
|
+
for (const msg of messages) {
|
|
138
|
+
if (msg.getType() === 'tool') continue;
|
|
139
|
+
|
|
140
|
+
if (msg.getType() !== 'ai') {
|
|
141
|
+
cleaned.push(msg);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const aiMsg = msg as AIMessage | AIMessageChunk;
|
|
146
|
+
const toolCalls = aiMsg.tool_calls ?? [];
|
|
147
|
+
if (toolCalls.length === 0) {
|
|
148
|
+
cleaned.push(msg);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const textContent = extractAIText(aiMsg);
|
|
153
|
+
|
|
154
|
+
const toolSummaries: string[] = [];
|
|
155
|
+
for (const tc of toolCalls) {
|
|
156
|
+
if (tc.id != null && pairedToolCallIds.has(tc.id)) {
|
|
157
|
+
const toolResult = messages.find(
|
|
158
|
+
(m) =>
|
|
159
|
+
m.getType() === 'tool' && (m as ToolMessage).tool_call_id === tc.id
|
|
160
|
+
) as ToolMessage | undefined;
|
|
161
|
+
const resultContent = toolResult
|
|
162
|
+
? typeof toolResult.content === 'string'
|
|
163
|
+
? toolResult.content.slice(0, 500)
|
|
164
|
+
: '[complex result]'
|
|
165
|
+
: '[no result]';
|
|
166
|
+
toolSummaries.push(`[Tool "${tc.name}": ${resultContent}]`);
|
|
167
|
+
}
|
|
168
|
+
// Orphaned tool_use blocks (no matching result) are silently dropped.
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const parts = [textContent, ...toolSummaries].filter(Boolean);
|
|
172
|
+
if (parts.length > 0) {
|
|
173
|
+
cleaned.push(
|
|
174
|
+
new AIMessage({ content: parts.join('\n\n'), id: aiMsg.id })
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Ensure messages end with a HumanMessage. After stripping tool artifacts
|
|
181
|
+
* the tail may be an AIMessage, which Bedrock/VertexAI reject. Convert it
|
|
182
|
+
* to a HumanMessage preserving whatever text content was present, or drop
|
|
183
|
+
* it entirely if empty.
|
|
184
|
+
*/
|
|
185
|
+
if (cleaned.length > 0 && cleaned[cleaned.length - 1].getType() === 'ai') {
|
|
186
|
+
const lastAI = cleaned[cleaned.length - 1];
|
|
187
|
+
const content = typeof lastAI.content === 'string' ? lastAI.content : '';
|
|
188
|
+
if (content.trim()) {
|
|
189
|
+
cleaned[cleaned.length - 1] = new HumanMessage(
|
|
190
|
+
`${HANDOFF_TAIL_CONTEXT_PREFIX}${content}`
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
cleaned.pop();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return cleaned;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* -------------------------------------------------------------------------- */
|
|
201
|
+
/* Strategy 2: isolated fresh session (downstream scoped-subgraph child) */
|
|
202
|
+
/* -------------------------------------------------------------------------- */
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Build an ISOLATED message context for a downstream scoped-subgraph node.
|
|
206
|
+
*
|
|
207
|
+
* Unlike `prepareHandoffMessages` (which cleans up tool_use artifacts but
|
|
208
|
+
* preserves most of the parent history), this helper produces a fresh
|
|
209
|
+
* minimal context containing only:
|
|
210
|
+
*
|
|
211
|
+
* 1. The original user request (first HumanMessage in the history)
|
|
212
|
+
* 2. A synthetic HumanMessage summarizing the upstream agent's final
|
|
213
|
+
* text output and directing the downstream agent to act on it
|
|
214
|
+
*
|
|
215
|
+
* Tool_use / tool_result blocks from the upstream agent are discarded —
|
|
216
|
+
* the downstream agent shouldn't reason about how the upstream agent did
|
|
217
|
+
* its work, only about the result.
|
|
218
|
+
*
|
|
219
|
+
* This "fresh subagent session" pattern is the primary defense against
|
|
220
|
+
* schema confusion / malformed tool_use JSON that occurs when downstream
|
|
221
|
+
* models see a noisy upstream conversation.
|
|
222
|
+
*
|
|
223
|
+
* Defensive fallback: if the messages array contains neither a user
|
|
224
|
+
* message nor a non-empty upstream AI message, return the input unchanged
|
|
225
|
+
* so the caller still has something to invoke on. This only matters for
|
|
226
|
+
* malformed state fixtures in tests.
|
|
227
|
+
*/
|
|
228
|
+
export function prepareIsolatedChildMessages(
|
|
229
|
+
messages: BaseMessage[]
|
|
230
|
+
): BaseMessage[] {
|
|
231
|
+
if (messages.length === 0) return messages;
|
|
232
|
+
|
|
233
|
+
/** First HumanMessage is the original user request */
|
|
234
|
+
const originalUser = messages.find((m) => m.getType() === 'human');
|
|
235
|
+
|
|
236
|
+
/** Most recent AIMessage with non-empty text content */
|
|
237
|
+
let upstreamText = '';
|
|
238
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
239
|
+
const msg = messages[i];
|
|
240
|
+
if (msg.getType() !== 'ai') continue;
|
|
241
|
+
const text = extractAIText(msg as AIMessage | AIMessageChunk);
|
|
242
|
+
if (text.trim()) {
|
|
243
|
+
upstreamText = text;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const result: BaseMessage[] = [];
|
|
249
|
+
if (originalUser) result.push(originalUser);
|
|
250
|
+
|
|
251
|
+
if (upstreamText.trim()) {
|
|
252
|
+
result.push(new HumanMessage(buildIsolatedChildPrompt(upstreamText)));
|
|
253
|
+
} else if (result.length === 0) {
|
|
254
|
+
/** Defensive: nothing to isolate — fall back to raw messages */
|
|
255
|
+
return messages;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
package/src/utils/events.ts
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
// src/utils/events.ts
|
|
3
3
|
import { dispatchCustomEvent } from '@langchain/core/callbacks/dispatch';
|
|
4
|
+
import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
|
|
4
5
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Returns the RunnableConfig currently active in LangChain's AsyncLocalStorage,
|
|
9
|
+
* or undefined if none is installed. This is the per-async-branch config that
|
|
10
|
+
* LangGraph installs when entering a node — it carries the correct
|
|
11
|
+
* `metadata.spawnKey` for child subgraph invocations inside `Promise.all`
|
|
12
|
+
* parallel handoffs.
|
|
13
|
+
*
|
|
14
|
+
* Prefer this over any Graph-instance-cached config (e.g. `this.config`)
|
|
15
|
+
* when dispatching events from code that may run concurrently across multiple
|
|
16
|
+
* child subgraphs. An instance-level cache is shared state and races between
|
|
17
|
+
* siblings — the last child to enter wins, so events fire with the wrong
|
|
18
|
+
* child's metadata and the backend routes them to the wrong spawnKey.
|
|
19
|
+
*/
|
|
20
|
+
export function getCurrentRunnableConfig(): RunnableConfig | undefined {
|
|
21
|
+
try {
|
|
22
|
+
return AsyncLocalStorageProviderSingleton.getInstance().getStore() as
|
|
23
|
+
| RunnableConfig
|
|
24
|
+
| undefined;
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
/**
|
|
7
31
|
* Safely dispatches a custom event and properly awaits it to avoid
|
|
8
32
|
* race conditions where events are dispatched after run cleanup.
|
|
33
|
+
*
|
|
34
|
+
* **Parallel-handoff correctness:** callers should prefer passing
|
|
35
|
+
* `undefined` (or the per-node runtime config). When `config` is omitted,
|
|
36
|
+
* LangChain's `ensureConfig` reads the current RunnableConfig from
|
|
37
|
+
* AsyncLocalStorage, which is correctly isolated per async branch under
|
|
38
|
+
* `Promise.all`. Passing a stale instance-cached config overrides that
|
|
39
|
+
* implicit config's metadata and cross-contaminates parallel children.
|
|
9
40
|
*/
|
|
10
41
|
export async function safeDispatchCustomEvent(
|
|
11
42
|
event: string,
|
|
@@ -13,7 +44,11 @@ export async function safeDispatchCustomEvent(
|
|
|
13
44
|
config?: RunnableConfig
|
|
14
45
|
): Promise<void> {
|
|
15
46
|
try {
|
|
16
|
-
|
|
47
|
+
// If the caller did not pass a config, fall back to the current
|
|
48
|
+
// AsyncLocalStorage-resident runnable config so nested Promise.all
|
|
49
|
+
// branches each use their own metadata.
|
|
50
|
+
const effectiveConfig = config ?? getCurrentRunnableConfig();
|
|
51
|
+
await dispatchCustomEvent(event, payload, effectiveConfig);
|
|
17
52
|
} catch (e) {
|
|
18
53
|
// Check if this is the known EventStreamCallbackHandler error
|
|
19
54
|
if (
|
|
@@ -21,9 +56,7 @@ export async function safeDispatchCustomEvent(
|
|
|
21
56
|
e.message.includes('handleCustomEvent: Run ID') &&
|
|
22
57
|
e.message.includes('not found in run map')
|
|
23
58
|
) {
|
|
24
|
-
// Suppress
|
|
25
|
-
// when EventStreamCallbackHandler loses track of run IDs
|
|
26
|
-
// console.debug('Suppressed error dispatching custom event:', e);
|
|
59
|
+
// Suppress — expected during parallel/async execution
|
|
27
60
|
return;
|
|
28
61
|
}
|
|
29
62
|
// Log other errors
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finish-reason constants and helpers.
|
|
3
|
+
*
|
|
4
|
+
* LLM providers emit different keys (`finish_reason`, `stop_reason`,
|
|
5
|
+
* `stopReason`, `finishReason`) and different values for the same concept.
|
|
6
|
+
* This module is the single source of truth for detecting *truncation* —
|
|
7
|
+
* i.e., the model hit its output budget and the response is incomplete.
|
|
8
|
+
*
|
|
9
|
+
* Used by:
|
|
10
|
+
* - `Graph.ts` — sticky `lastFinishReason` across inner subgraph invokes,
|
|
11
|
+
* so host's continuation retry can detect truncation that happens
|
|
12
|
+
* inside a scoped-subgraph child node.
|
|
13
|
+
* - Any future continuation / auto-retry logic added to agents.
|
|
14
|
+
*
|
|
15
|
+
* Kept as a small utils module (instead of inline in Graph.ts) so that
|
|
16
|
+
* additional callers — e.g., structured output recovery, sub-agent
|
|
17
|
+
* orchestrators — can reuse the exact same detection logic without drift.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Canonical set of finish-reason strings that mean "output was truncated".
|
|
22
|
+
*
|
|
23
|
+
* Covers:
|
|
24
|
+
* - `max_tokens` — Anthropic direct API, Bedrock
|
|
25
|
+
* - `length` — OpenAI/Azure
|
|
26
|
+
* - `MAX_TOKENS` — VertexAI/Google (uppercased enum)
|
|
27
|
+
*/
|
|
28
|
+
export const TRUNCATION_FINISH_REASONS: ReadonlySet<string> = new Set([
|
|
29
|
+
'max_tokens',
|
|
30
|
+
'length',
|
|
31
|
+
'MAX_TOKENS',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @returns true when the given finish/stop reason indicates the response
|
|
36
|
+
* was cut short by the output token budget.
|
|
37
|
+
*/
|
|
38
|
+
export function isTruncationReason(reason: string | undefined | null): boolean {
|
|
39
|
+
return reason != null && TRUNCATION_FINISH_REASONS.has(reason);
|
|
40
|
+
}
|
package/src/utils/llm.ts
CHANGED
package/src/utils/logging.ts
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import util from 'util';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Multi-agent debug gate. Controls chatty graph/handoff/transfer/sequence
|
|
7
|
+
* trace logs emitted from `MultiAgentGraph` and `Graph`. Off by default so the
|
|
8
|
+
* package stays quiet when embedded in host apps. Flip to
|
|
9
|
+
* `DEBUG_MULTIAGENT=true` in the host env to re-enable for testing.
|
|
10
|
+
*
|
|
11
|
+
* Exported as functions so runtime toggling via env reload works; the check
|
|
12
|
+
* is cheap (string equality) and only runs when a log site fires.
|
|
13
|
+
*/
|
|
14
|
+
function isMultiAgentDebugEnabled(): boolean {
|
|
15
|
+
return process.env.DEBUG_MULTIAGENT === 'true';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
export const mlog = (...args: any[]): void => {
|
|
20
|
+
if (isMultiAgentDebugEnabled()) {
|
|
21
|
+
console.debug(...args);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
export const mwarn = (...args: any[]): void => {
|
|
27
|
+
if (isMultiAgentDebugEnabled()) {
|
|
28
|
+
console.warn(...args);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
5
32
|
export function setupLogging(logFileName: string): void {
|
|
6
33
|
const logFile = fs.createWriteStream(logFileName, { flags: 'a' });
|
|
7
34
|
|
|
@@ -10,39 +37,49 @@ export function setupLogging(logFileName: string): void {
|
|
|
10
37
|
const originalStdoutWrite = process.stdout.write;
|
|
11
38
|
const originalStderrWrite = process.stderr.write;
|
|
12
39
|
|
|
13
|
-
console.log = function(...args): void {
|
|
40
|
+
console.log = function (...args): void {
|
|
14
41
|
logFile.write(util.format.apply(null, args) + ' ');
|
|
15
42
|
originalConsoleLog.apply(console, args);
|
|
16
43
|
};
|
|
17
44
|
|
|
18
|
-
console.error = function(...args): void {
|
|
45
|
+
console.error = function (...args): void {
|
|
19
46
|
logFile.write(util.format.apply(null, args) + ' ');
|
|
20
47
|
originalConsoleError.apply(console, args);
|
|
21
48
|
};
|
|
22
49
|
|
|
23
|
-
process.stdout.write = function(
|
|
50
|
+
process.stdout.write = function (
|
|
24
51
|
buffer: Uint8Array | string,
|
|
25
52
|
cb?: ((err?: Error) => void) | string,
|
|
26
|
-
fd?: (
|
|
53
|
+
fd?: (err?: Error) => void
|
|
27
54
|
): boolean {
|
|
28
55
|
if (typeof buffer === 'string') {
|
|
29
56
|
logFile.write(buffer);
|
|
30
57
|
} else {
|
|
31
58
|
logFile.write(buffer.toString());
|
|
32
59
|
}
|
|
33
|
-
return originalStdoutWrite.call(
|
|
60
|
+
return originalStdoutWrite.call(
|
|
61
|
+
process.stdout,
|
|
62
|
+
buffer,
|
|
63
|
+
cb as BufferEncoding | undefined,
|
|
64
|
+
fd
|
|
65
|
+
);
|
|
34
66
|
};
|
|
35
67
|
|
|
36
|
-
process.stderr.write = function(
|
|
68
|
+
process.stderr.write = function (
|
|
37
69
|
buffer: Uint8Array | string,
|
|
38
70
|
cb?: ((err?: Error) => void) | string,
|
|
39
|
-
fd?: (
|
|
71
|
+
fd?: (err?: Error) => void
|
|
40
72
|
): boolean {
|
|
41
73
|
if (typeof buffer === 'string') {
|
|
42
74
|
logFile.write(buffer);
|
|
43
75
|
} else {
|
|
44
76
|
logFile.write(buffer.toString());
|
|
45
77
|
}
|
|
46
|
-
return originalStderrWrite.call(
|
|
78
|
+
return originalStderrWrite.call(
|
|
79
|
+
process.stderr,
|
|
80
|
+
buffer,
|
|
81
|
+
cb as BufferEncoding | undefined,
|
|
82
|
+
fd
|
|
83
|
+
);
|
|
47
84
|
};
|
|
48
85
|
}
|