@librechat/agents 3.1.77 → 3.1.78

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 (185) hide show
  1. package/dist/cjs/common/enum.cjs +54 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +155 -4
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +291 -0
  6. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -0
  7. package/dist/cjs/main.cjs +90 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/anthropicToolCache.cjs +102 -0
  10. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -0
  11. package/dist/cjs/messages/prune.cjs +27 -0
  12. package/dist/cjs/messages/prune.cjs.map +1 -1
  13. package/dist/cjs/messages/recency.cjs +99 -0
  14. package/dist/cjs/messages/recency.cjs.map +1 -0
  15. package/dist/cjs/run.cjs +30 -0
  16. package/dist/cjs/run.cjs.map +1 -1
  17. package/dist/cjs/summarization/node.cjs +100 -6
  18. package/dist/cjs/summarization/node.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +635 -23
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/cjs/tools/local/CompileCheckTool.cjs +227 -0
  22. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -0
  23. package/dist/cjs/tools/local/FileCheckpointer.cjs +90 -0
  24. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -0
  25. package/dist/cjs/tools/local/LocalCodingTools.cjs +1098 -0
  26. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -0
  27. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1042 -0
  28. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -0
  29. package/dist/cjs/tools/local/LocalExecutionTools.cjs +122 -0
  30. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -0
  31. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +453 -0
  32. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -0
  33. package/dist/cjs/tools/local/attachments.cjs +183 -0
  34. package/dist/cjs/tools/local/attachments.cjs.map +1 -0
  35. package/dist/cjs/tools/local/bashAst.cjs +129 -0
  36. package/dist/cjs/tools/local/bashAst.cjs.map +1 -0
  37. package/dist/cjs/tools/local/editStrategies.cjs +188 -0
  38. package/dist/cjs/tools/local/editStrategies.cjs.map +1 -0
  39. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +141 -0
  40. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -0
  41. package/dist/cjs/tools/local/syntaxCheck.cjs +182 -0
  42. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -0
  43. package/dist/cjs/tools/local/textEncoding.cjs +30 -0
  44. package/dist/cjs/tools/local/textEncoding.cjs.map +1 -0
  45. package/dist/cjs/tools/local/workspaceFS.cjs +51 -0
  46. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -0
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +31 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  49. package/dist/esm/common/enum.mjs +53 -1
  50. package/dist/esm/common/enum.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +156 -5
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +289 -0
  54. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -0
  55. package/dist/esm/main.mjs +17 -2
  56. package/dist/esm/main.mjs.map +1 -1
  57. package/dist/esm/messages/anthropicToolCache.mjs +99 -0
  58. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -0
  59. package/dist/esm/messages/prune.mjs +26 -1
  60. package/dist/esm/messages/prune.mjs.map +1 -1
  61. package/dist/esm/messages/recency.mjs +97 -0
  62. package/dist/esm/messages/recency.mjs.map +1 -0
  63. package/dist/esm/run.mjs +30 -0
  64. package/dist/esm/run.mjs.map +1 -1
  65. package/dist/esm/summarization/node.mjs +100 -6
  66. package/dist/esm/summarization/node.mjs.map +1 -1
  67. package/dist/esm/tools/ToolNode.mjs +635 -23
  68. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  69. package/dist/esm/tools/local/CompileCheckTool.mjs +223 -0
  70. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -0
  71. package/dist/esm/tools/local/FileCheckpointer.mjs +87 -0
  72. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -0
  73. package/dist/esm/tools/local/LocalCodingTools.mjs +1075 -0
  74. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -0
  75. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1022 -0
  76. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -0
  77. package/dist/esm/tools/local/LocalExecutionTools.mjs +117 -0
  78. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -0
  79. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +448 -0
  80. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -0
  81. package/dist/esm/tools/local/attachments.mjs +180 -0
  82. package/dist/esm/tools/local/attachments.mjs.map +1 -0
  83. package/dist/esm/tools/local/bashAst.mjs +126 -0
  84. package/dist/esm/tools/local/bashAst.mjs.map +1 -0
  85. package/dist/esm/tools/local/editStrategies.mjs +185 -0
  86. package/dist/esm/tools/local/editStrategies.mjs.map +1 -0
  87. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +137 -0
  88. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -0
  89. package/dist/esm/tools/local/syntaxCheck.mjs +179 -0
  90. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -0
  91. package/dist/esm/tools/local/textEncoding.mjs +27 -0
  92. package/dist/esm/tools/local/textEncoding.mjs.map +1 -0
  93. package/dist/esm/tools/local/workspaceFS.mjs +49 -0
  94. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -0
  95. package/dist/esm/tools/subagent/SubagentExecutor.mjs +31 -0
  96. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  97. package/dist/types/common/enum.d.ts +39 -1
  98. package/dist/types/graphs/Graph.d.ts +34 -0
  99. package/dist/types/hooks/createWorkspacePolicyHook.d.ts +95 -0
  100. package/dist/types/hooks/index.d.ts +2 -0
  101. package/dist/types/index.d.ts +1 -0
  102. package/dist/types/messages/anthropicToolCache.d.ts +51 -0
  103. package/dist/types/messages/index.d.ts +2 -0
  104. package/dist/types/messages/prune.d.ts +11 -0
  105. package/dist/types/messages/recency.d.ts +64 -0
  106. package/dist/types/run.d.ts +21 -0
  107. package/dist/types/tools/ToolNode.d.ts +145 -2
  108. package/dist/types/tools/local/CompileCheckTool.d.ts +31 -0
  109. package/dist/types/tools/local/FileCheckpointer.d.ts +39 -0
  110. package/dist/types/tools/local/LocalCodingTools.d.ts +57 -0
  111. package/dist/types/tools/local/LocalExecutionEngine.d.ts +149 -0
  112. package/dist/types/tools/local/LocalExecutionTools.d.ts +9 -0
  113. package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +21 -0
  114. package/dist/types/tools/local/attachments.d.ts +84 -0
  115. package/dist/types/tools/local/bashAst.d.ts +11 -0
  116. package/dist/types/tools/local/editStrategies.d.ts +28 -0
  117. package/dist/types/tools/local/index.d.ts +12 -0
  118. package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +38 -0
  119. package/dist/types/tools/local/syntaxCheck.d.ts +42 -0
  120. package/dist/types/tools/local/textEncoding.d.ts +21 -0
  121. package/dist/types/tools/local/workspaceFS.d.ts +49 -0
  122. package/dist/types/tools/subagent/SubagentExecutor.d.ts +29 -0
  123. package/dist/types/types/hitl.d.ts +56 -27
  124. package/dist/types/types/run.d.ts +8 -1
  125. package/dist/types/types/summarize.d.ts +30 -0
  126. package/dist/types/types/tools.d.ts +341 -6
  127. package/package.json +21 -2
  128. package/src/common/enum.ts +54 -0
  129. package/src/graphs/Graph.ts +173 -6
  130. package/src/hooks/__tests__/compactHooks.test.ts +38 -2
  131. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
  132. package/src/hooks/createWorkspacePolicyHook.ts +355 -0
  133. package/src/hooks/index.ts +6 -0
  134. package/src/index.ts +1 -0
  135. package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
  136. package/src/messages/__tests__/recency.test.ts +267 -0
  137. package/src/messages/anthropicToolCache.ts +116 -0
  138. package/src/messages/index.ts +2 -0
  139. package/src/messages/prune.ts +27 -1
  140. package/src/messages/recency.ts +155 -0
  141. package/src/run.ts +31 -0
  142. package/src/scripts/compare_pi_vs_ours.ts +840 -0
  143. package/src/scripts/local_engine.ts +166 -0
  144. package/src/scripts/local_engine_checkpointer.ts +205 -0
  145. package/src/scripts/local_engine_compile.ts +263 -0
  146. package/src/scripts/local_engine_hooks.ts +226 -0
  147. package/src/scripts/local_engine_image.ts +201 -0
  148. package/src/scripts/local_engine_ptc.ts +151 -0
  149. package/src/scripts/local_engine_workspace.ts +258 -0
  150. package/src/scripts/subagent-configurable-inheritance.ts +252 -0
  151. package/src/scripts/summarization-recency.ts +462 -0
  152. package/src/specs/prune.test.ts +39 -0
  153. package/src/summarization/__tests__/node.test.ts +499 -3
  154. package/src/summarization/node.ts +124 -7
  155. package/src/tools/ToolNode.ts +769 -20
  156. package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
  157. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
  158. package/src/tools/__tests__/SubagentExecutor.test.ts +148 -0
  159. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
  160. package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
  161. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
  162. package/src/tools/__tests__/directToolHooks.test.ts +411 -0
  163. package/src/tools/__tests__/localToolNames.test.ts +73 -0
  164. package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
  165. package/src/tools/local/CompileCheckTool.ts +278 -0
  166. package/src/tools/local/FileCheckpointer.ts +93 -0
  167. package/src/tools/local/LocalCodingTools.ts +1342 -0
  168. package/src/tools/local/LocalExecutionEngine.ts +1329 -0
  169. package/src/tools/local/LocalExecutionTools.ts +167 -0
  170. package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
  171. package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
  172. package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
  173. package/src/tools/local/attachments.ts +251 -0
  174. package/src/tools/local/bashAst.ts +151 -0
  175. package/src/tools/local/editStrategies.ts +188 -0
  176. package/src/tools/local/index.ts +12 -0
  177. package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
  178. package/src/tools/local/syntaxCheck.ts +243 -0
  179. package/src/tools/local/textEncoding.ts +37 -0
  180. package/src/tools/local/workspaceFS.ts +89 -0
  181. package/src/tools/subagent/SubagentExecutor.ts +60 -0
  182. package/src/types/hitl.ts +56 -27
  183. package/src/types/run.ts +12 -1
  184. package/src/types/summarize.ts +31 -0
  185. package/src/types/tools.ts +359 -7
@@ -0,0 +1,258 @@
1
+ /**
2
+ * src/scripts/local_engine_workspace.ts
3
+ *
4
+ * Live demo of the workspace boundary + HITL "ask the user when the
5
+ * agent wants to leave the workspace" flow.
6
+ *
7
+ * Setup:
8
+ * - workspace.root = a temp dir
9
+ * - additionalRoots = a sibling temp dir (sim. monorepo)
10
+ * - allowReadOutside = true (so the file tool's hard clamp lets the
11
+ * hook be the gate)
12
+ * - createWorkspacePolicyHook with outsideRead='ask'
13
+ * - humanInTheLoop.enabled = true
14
+ *
15
+ * The agent is then asked to read three files:
16
+ * 1. one inside the workspace → expect: PreToolUse 'allow', tool runs
17
+ * 2. one inside the additional root → expect: PreToolUse 'allow', tool runs
18
+ * 3. one truly outside → expect: PreToolUse 'ask' interrupt
19
+ * → script auto-approves on resume → tool runs
20
+ *
21
+ * Asserts that the host received the interrupt for case (3) and that
22
+ * the resume completed cleanly. Verifies the existing HITL machinery
23
+ * (#134) does the lifting end-to-end with the new policy hook.
24
+ *
25
+ * Run with: `npm run local:workspace`
26
+ */
27
+ import { config } from 'dotenv';
28
+ config();
29
+ import { tmpdir } from 'os';
30
+ import { join } from 'path';
31
+ import { mkdtemp, rm, writeFile } from 'fs/promises';
32
+ import { MemorySaver } from '@langchain/langgraph';
33
+ import { HumanMessage, ToolMessage } from '@langchain/core/messages';
34
+ import type { BaseMessage } from '@langchain/core/messages';
35
+ import type * as t from '@/types';
36
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
37
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
38
+ import { HookRegistry, createWorkspacePolicyHook } from '@/hooks';
39
+ import { getLLMConfig } from '@/utils/llmConfig';
40
+ import { getArgs } from '@/scripts/args';
41
+ import { GraphEvents, Providers } from '@/common';
42
+ import { Run } from '@/run';
43
+
44
+ async function main(): Promise<void> {
45
+ const { userName, provider } = await getArgs();
46
+ const workspace = await mkdtemp(join(tmpdir(), 'lc-ws-demo-'));
47
+ const sibling = await mkdtemp(join(tmpdir(), 'lc-ws-extra-'));
48
+ const outside = await mkdtemp(join(tmpdir(), 'lc-ws-outside-'));
49
+ console.log(`[ws] workspace root: ${workspace}`);
50
+ console.log(`[ws] additional root: ${sibling}`);
51
+ console.log(`[ws] outside root : ${outside}`);
52
+
53
+ await writeFile(join(workspace, 'inside.txt'), 'INSIDE\n', 'utf8');
54
+ await writeFile(join(sibling, 'sibling.txt'), 'SIBLING\n', 'utf8');
55
+ await writeFile(join(outside, 'secret.txt'), 'SECRET\n', 'utf8');
56
+
57
+ const hookRegistry = new HookRegistry();
58
+ hookRegistry.register('PreToolUse', {
59
+ hooks: [
60
+ createWorkspacePolicyHook({
61
+ root: workspace,
62
+ additionalRoots: [sibling],
63
+ outsideRead: 'ask',
64
+ outsideWrite: 'ask',
65
+ reason: 'workspace-policy: {tool} wants outside paths {paths}',
66
+ }),
67
+ ],
68
+ });
69
+
70
+ const { aggregateContent } = createContentAggregator();
71
+ const customHandlers = {
72
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
73
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
74
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
75
+ // Forward ON_RUN_STEP so the aggregator's stepMap is seeded
76
+ // before ON_RUN_STEP_COMPLETED arrives (issue #142).
77
+ [GraphEvents.ON_RUN_STEP]: {
78
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData): void => {
79
+ aggregateContent({ event, data: data as t.RunStep });
80
+ },
81
+ },
82
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
83
+ handle: (
84
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
85
+ data: t.StreamEventData
86
+ ): void => {
87
+ aggregateContent({
88
+ event,
89
+ data: data as unknown as { result: t.ToolEndEvent },
90
+ });
91
+ },
92
+ },
93
+ [GraphEvents.TOOL_START]: {
94
+ handle: (_event: string, data: t.StreamEventData): void => {
95
+ const obj = data as unknown as { name?: string; input?: unknown };
96
+ console.log(
97
+ `====== TOOL_START tool=${obj.name ?? '?'} input=${JSON.stringify(obj.input)} ======`
98
+ );
99
+ },
100
+ },
101
+ };
102
+
103
+ const llmConfig = getLLMConfig(Providers.ANTHROPIC);
104
+ const checkpointer = new MemorySaver();
105
+ const conversation: BaseMessage[] = [];
106
+
107
+ const buildRun = async (): Promise<Run<t.IState>> => {
108
+ const runConfig: t.RunConfig = {
109
+ runId: `local-engine-ws-${Date.now()}`,
110
+ graphConfig: {
111
+ type: 'standard',
112
+ llmConfig: { ...llmConfig, promptCache: true },
113
+ compileOptions: { checkpointer },
114
+ instructions:
115
+ `You are ${userName}'s assistant. The host has a workspace policy ` +
116
+ 'on `read_file`. If a read is outside the workspace, the host will ' +
117
+ 'ask the user to approve. After approval, retry the read.',
118
+ },
119
+ toolExecution: {
120
+ engine: 'local',
121
+ local: {
122
+ workspace: {
123
+ root: workspace,
124
+ additionalRoots: [sibling],
125
+ allowReadOutside: true,
126
+ },
127
+ timeoutMs: 30_000,
128
+ },
129
+ },
130
+ hooks: hookRegistry,
131
+ humanInTheLoop: { enabled: true },
132
+ returnContent: true,
133
+ skipCleanup: true,
134
+ customHandlers,
135
+ };
136
+ return Run.create<t.IState>(runConfig);
137
+ };
138
+
139
+ const threadId = `ws-thread-${Date.now()}`;
140
+ const streamConfig = {
141
+ configurable: { provider, thread_id: threadId },
142
+ streamMode: 'values',
143
+ version: 'v2' as const,
144
+ };
145
+
146
+ const userMessage = new HumanMessage(
147
+ `Hi ${userName}. Please call \`read_file\` on each of these in order:\n` +
148
+ ` 1. \`inside.txt\` (relative)\n` +
149
+ ` 2. \`${join(sibling, 'sibling.txt')}\` (sibling root, should be allowed)\n` +
150
+ ` 3. \`${join(outside, 'secret.txt')}\` (outside the workspace)\n\n` +
151
+ `For each, tell me what's in it. If a call is blocked or asks for approval, just retry it once.`
152
+ );
153
+ conversation.push(userMessage);
154
+ console.log('====== USER ======\n' + userMessage.content + '\n');
155
+
156
+ // First run — likely to interrupt on the third read.
157
+ let run = await buildRun();
158
+ await run.processStream(
159
+ { messages: conversation },
160
+ streamConfig as Parameters<typeof run.processStream>[1]
161
+ );
162
+ let finalMessages = run.getRunMessages();
163
+ if (finalMessages) conversation.push(...finalMessages);
164
+
165
+ let interrupt = run.getInterrupt();
166
+ let resumes = 0;
167
+ while (interrupt != null && resumes < 4) {
168
+ resumes++;
169
+ const payload = interrupt.payload as t.ToolApprovalInterruptPayload;
170
+ console.log(
171
+ `\n====== INTERRUPT raised (resume #${resumes}) ======\n` +
172
+ `payload.action_requests = ${JSON.stringify(
173
+ payload.action_requests,
174
+ null,
175
+ 2
176
+ )}`
177
+ );
178
+
179
+ // Auto-approve every action request.
180
+ const decisions: t.ToolApprovalDecision[] = payload.action_requests.map(
181
+ (req) => ({ tool_call_id: req.tool_call_id, type: 'approve' })
182
+ );
183
+ console.log(
184
+ `[ws] auto-approving ${decisions.length} tool call(s) and resuming`
185
+ );
186
+
187
+ run = await buildRun();
188
+ await run.resume(
189
+ decisions,
190
+ streamConfig as Parameters<typeof run.processStream>[1]
191
+ );
192
+ finalMessages = run.getRunMessages();
193
+ if (finalMessages) conversation.push(...finalMessages);
194
+ interrupt = run.getInterrupt();
195
+ }
196
+
197
+ console.log('\n====== TOOL MESSAGES IN HISTORY ======');
198
+ let askesObserved = 0;
199
+ let successesObserved = 0;
200
+ for (const msg of conversation) {
201
+ if (msg instanceof ToolMessage) {
202
+ const head =
203
+ typeof msg.content === 'string'
204
+ ? msg.content.slice(0, 200)
205
+ : JSON.stringify(msg.content).slice(0, 200);
206
+ console.log(`- ${msg.name} (${msg.status}): ${head.replace(/\n/g, ' ⏎ ')}`);
207
+ if (msg.status === 'error') askesObserved++;
208
+ if (msg.status === 'success') successesObserved++;
209
+ }
210
+ }
211
+
212
+ console.log('\n====== ASSISTANT FINAL TEXT ======');
213
+ const lastAssistant = [...conversation]
214
+ .reverse()
215
+ .find((m) => m._getType() === 'ai');
216
+ if (lastAssistant) {
217
+ const c = lastAssistant.content;
218
+ console.log(
219
+ typeof c === 'string'
220
+ ? c
221
+ : Array.isArray(c)
222
+ ? c
223
+ .map((b) => ('text' in b ? b.text : `<${b.type}>`))
224
+ .join(' ')
225
+ : JSON.stringify(c)
226
+ );
227
+ }
228
+
229
+ console.log('\n====== ASSERTIONS ======');
230
+ console.log(`HITL interrupts handled: ${resumes}`);
231
+ console.log(`successful tool messages: ${successesObserved}`);
232
+ if (resumes === 0) {
233
+ console.error('[ws] expected at least one HITL interrupt; got 0');
234
+ process.exitCode = 1;
235
+ }
236
+ if (successesObserved < 2) {
237
+ console.error('[ws] expected at least 2 successful reads; got', successesObserved);
238
+ process.exitCode = 1;
239
+ } else {
240
+ console.log(`[ws] workspace + HITL flow ✔`);
241
+ }
242
+
243
+ await Promise.all([
244
+ rm(workspace, { recursive: true, force: true }),
245
+ rm(sibling, { recursive: true, force: true }),
246
+ rm(outside, { recursive: true, force: true }),
247
+ ]);
248
+ }
249
+
250
+ process.on('unhandledRejection', (reason) => {
251
+ console.error('Unhandled Rejection:', reason);
252
+ process.exit(1);
253
+ });
254
+
255
+ main().catch((err) => {
256
+ console.error(err);
257
+ process.exit(1);
258
+ });
@@ -0,0 +1,252 @@
1
+ import { config } from 'dotenv';
2
+ config();
3
+
4
+ import { HumanMessage } from '@langchain/core/messages';
5
+ import type * as t from '@/types';
6
+ import { ChatModelStreamHandler } from '@/stream';
7
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
8
+ import { Providers, GraphEvents } from '@/common';
9
+ import { Run } from '@/run';
10
+
11
+ /**
12
+ * Live verification that host-set fields on the parent's outer
13
+ * `configurable` (e.g. `requestBody`, `user`, `userMCPAuthMap`)
14
+ * propagate into the subagent's `ON_TOOL_EXECUTE` dispatches.
15
+ *
16
+ * Pass criteria: when the SUBAGENT calls the calculator tool, the
17
+ * `data.configurable` arriving at the parent's ON_TOOL_EXECUTE
18
+ * handler contains every key the parent put on its outer
19
+ * configurable (with `thread_id` overridden to a child run id).
20
+ */
21
+ const apiKey = process.env.OPENAI_API_KEY!;
22
+ if (!apiKey) {
23
+ console.error('Missing OPENAI_API_KEY');
24
+ process.exit(1);
25
+ }
26
+
27
+ const calculatorDef: t.LCTool = {
28
+ name: 'calculator',
29
+ description: 'Evaluate a math expression. Use for any arithmetic.',
30
+ parameters: {
31
+ type: 'object',
32
+ properties: {
33
+ expression: {
34
+ type: 'string',
35
+ description: "A JS math expression, e.g. '42 * 58'",
36
+ },
37
+ },
38
+ required: ['expression'],
39
+ },
40
+ };
41
+
42
+ type ConfigurableSnapshot = {
43
+ agentId: string | undefined;
44
+ configurable: Record<string, unknown> | undefined;
45
+ metadata: Record<string, unknown> | undefined;
46
+ };
47
+
48
+ async function main() {
49
+ console.log('=== Subagent parentConfigurable inheritance — live ===\n');
50
+
51
+ // Parent has NO tools — it can only delegate via the math subagent.
52
+ // The math subagent has the calculator. This forces the spawn-subagent
53
+ // path so we can observe the subagent's `ON_TOOL_EXECUTE` dispatch.
54
+ const mathSubagentInputs: t.AgentInputs = {
55
+ agentId: 'math-worker',
56
+ provider: Providers.OPENAI,
57
+ clientOptions: { modelName: 'gpt-4o', apiKey },
58
+ instructions:
59
+ 'You compute arithmetic. Always use the calculator tool — never estimate. Return the final numeric result as plain text.',
60
+ maxContextTokens: 8000,
61
+ toolDefinitions: [calculatorDef],
62
+ };
63
+
64
+ const parentAgent: t.AgentInputs = {
65
+ agentId: 'supervisor',
66
+ provider: Providers.OPENAI,
67
+ clientOptions: { modelName: 'gpt-4o', apiKey },
68
+ instructions: `You delegate arithmetic to the "math" subagent. You have NO calculator yourself. For any math task, spawn the "math" subagent with the full task as its description, then echo the subagent's text result back to the user.`,
69
+ maxContextTokens: 8000,
70
+ // No toolDefinitions on the parent — only the subagent gets the calculator.
71
+ subagentConfigs: [
72
+ {
73
+ type: 'math',
74
+ name: 'math',
75
+ description:
76
+ 'A focused arithmetic worker that uses the calculator tool to compute numerical results.',
77
+ agentInputs: mathSubagentInputs,
78
+ },
79
+ ],
80
+ };
81
+
82
+ const parentSnapshots: ConfigurableSnapshot[] = [];
83
+ const subagentSnapshots: ConfigurableSnapshot[] = [];
84
+
85
+ const customHandlers: Record<string, t.EventHandler> = {
86
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
87
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
88
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
89
+ [GraphEvents.ON_TOOL_EXECUTE]: {
90
+ handle: (_event, rawData): void => {
91
+ const data = rawData as t.ToolExecuteBatchRequest;
92
+ const snapshot: ConfigurableSnapshot = {
93
+ agentId: data.agentId,
94
+ configurable: data.configurable as Record<string, unknown> | undefined,
95
+ metadata: data.metadata as Record<string, unknown> | undefined,
96
+ };
97
+ const callsLabel = data.toolCalls.map((c) => c.name).join(',');
98
+ // Parent and subagent have different agent IDs in this script
99
+ // (parent: 'supervisor', subagent: 'math-worker'). With a self-spawn
100
+ // subagent both would be the same; this script uses a non-self
101
+ // subagent precisely so we can distinguish reliably.
102
+ const isSubagent = data.agentId !== 'supervisor';
103
+ const metadataRunId = (data.metadata as { run_id?: string } | undefined)
104
+ ?.run_id;
105
+ if (isSubagent) {
106
+ subagentSnapshots.push(snapshot);
107
+ } else {
108
+ parentSnapshots.push(snapshot);
109
+ }
110
+ console.log(
111
+ `[ON_TOOL_EXECUTE] origin=${isSubagent ? 'SUBAGENT' : 'PARENT'} agentId=${data.agentId} calls=${callsLabel}`
112
+ );
113
+ console.log(
114
+ ` metadata keys: ${Object.keys(data.metadata ?? {}).join(',') || '<none>'}`
115
+ );
116
+ console.log(
117
+ ` metadata.run_id="${metadataRunId ?? '<none>'}" configurable.run_id="${(data.configurable as { run_id?: string } | undefined)?.run_id ?? '<none>'}" configurable.thread_id="${(data.configurable as { thread_id?: string } | undefined)?.thread_id ?? '<none>'}"`
118
+ );
119
+ const results: t.ToolExecuteResult[] = data.toolCalls.map((call) => {
120
+ const args = call.args as { expression?: string };
121
+ const expression = args.expression ?? '';
122
+ let content: string;
123
+ try {
124
+ // eslint-disable-next-line no-eval
125
+ const result = eval(expression);
126
+ content = `${expression} = ${result}`;
127
+ } catch (err) {
128
+ content = `Error: ${String(err)}`;
129
+ }
130
+ return {
131
+ toolCallId: call.id!,
132
+ status: 'success',
133
+ content,
134
+ };
135
+ });
136
+ data.resolve(results);
137
+ },
138
+ },
139
+ };
140
+
141
+ const run = await Run.create<t.IState>({
142
+ runId: `sub-cfg-inherit-${Date.now()}`,
143
+ graphConfig: { type: 'standard', agents: [parentAgent] },
144
+ customHandlers,
145
+ });
146
+
147
+ const question = new HumanMessage(
148
+ 'Compute (42 * 58) + (13 ** 3). Use the self subagent, and have it use the calculator.'
149
+ );
150
+
151
+ // Parent's outer configurable carries host-set fields AND explicit
152
+ // run-identity fields so we can verify whether LangGraph respects or
153
+ // overwrites parent's `run_id` / `parent_run_id` when we forward them
154
+ // into the child's `workflow.invoke`.
155
+ const outerConfigurable = {
156
+ thread_id: 'parent-thread-conv-xyz',
157
+ run_id: 'parent-run-id-001',
158
+ parent_run_id: 'grandparent-run-id-000',
159
+ user_id: 'user_abc',
160
+ user: { id: 'user_abc', email: 'a@b.c', role: 'USER' },
161
+ requestBody: {
162
+ messageId: 'msg-response-id-001',
163
+ conversationId: 'parent-thread-conv-xyz',
164
+ parentMessageId: 'user-message-id-000',
165
+ },
166
+ userMCPAuthMap: { 'mcp-github': { token: 'abc' } },
167
+ };
168
+
169
+ console.log('User:', question.content);
170
+ console.log('Parent outer configurable keys:', Object.keys(outerConfigurable));
171
+ console.log();
172
+
173
+ await run.processStream(
174
+ { messages: [question] },
175
+ {
176
+ configurable: outerConfigurable,
177
+ version: 'v2' as const,
178
+ }
179
+ );
180
+
181
+ console.log('\n=== Verification ===');
182
+ console.log(
183
+ `Parent ON_TOOL_EXECUTE dispatches captured: ${parentSnapshots.length}`
184
+ );
185
+ console.log(
186
+ `Subagent ON_TOOL_EXECUTE dispatches captured: ${subagentSnapshots.length}`
187
+ );
188
+
189
+ if (subagentSnapshots.length === 0) {
190
+ console.error(
191
+ '\n❌ FAIL: subagent never invoked a tool — model may not have spawned the subagent.'
192
+ );
193
+ process.exit(2);
194
+ }
195
+
196
+ const expectedHostKeys = ['user_id', 'user', 'requestBody', 'userMCPAuthMap'];
197
+ let allPassed = true;
198
+ subagentSnapshots.forEach((snap, idx) => {
199
+ const cfg = snap.configurable ?? {};
200
+ const meta = snap.metadata ?? {};
201
+ console.log(
202
+ `\nSubagent dispatch #${idx + 1} (agentId=${snap.agentId}, metadata.run_id=${(meta as { run_id?: string }).run_id ?? '-'}):`
203
+ );
204
+
205
+ // Host-set fields must propagate.
206
+ for (const key of expectedHostKeys) {
207
+ const present = key in cfg;
208
+ const value = cfg[key];
209
+ console.log(
210
+ ` ${present ? '✅' : '❌'} ${key} = ${JSON.stringify(value)}`
211
+ );
212
+ if (!present) allPassed = false;
213
+ }
214
+
215
+ // Run-identity fields: with full inheritance we expect parent's
216
+ // values to flow through. LangGraph runtime MAY overwrite them at
217
+ // child-invoke time — the script logs what actually arrived so we
218
+ // can see empirically what propagates.
219
+ console.log(` ⓘ thread_id observed: "${cfg.thread_id as string}" (parent's: "${outerConfigurable.thread_id}")`);
220
+ console.log(` ⓘ run_id observed: "${cfg.run_id as string}" (parent's: "${outerConfigurable.run_id}")`);
221
+ console.log(` ⓘ parent_run_id observed: "${cfg.parent_run_id as string}" (parent's: "${outerConfigurable.parent_run_id}")`);
222
+
223
+ const threadInherited = cfg.thread_id === outerConfigurable.thread_id;
224
+ const runInherited = cfg.run_id === outerConfigurable.run_id;
225
+ const parentRunInherited =
226
+ cfg.parent_run_id === outerConfigurable.parent_run_id;
227
+ console.log(
228
+ ` ${threadInherited ? '✅' : '⚠️ '} thread_id inherited from parent: ${threadInherited}`
229
+ );
230
+ console.log(
231
+ ` ${runInherited ? '✅' : '⚠️ '} run_id inherited from parent: ${runInherited}`
232
+ );
233
+ console.log(
234
+ ` ${parentRunInherited ? '✅' : '⚠️ '} parent_run_id inherited from parent: ${parentRunInherited}`
235
+ );
236
+ });
237
+
238
+ if (allPassed) {
239
+ console.log(
240
+ '\n✅ Host-set fields propagate. (Run-identity inheritance is informational — see ⚠️ markers above for any LangGraph-runtime overwrites.)'
241
+ );
242
+ process.exit(0);
243
+ } else {
244
+ console.log('\n❌ FAIL: at least one expected host-set key was missing.');
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ main().catch((err) => {
250
+ console.error('Script error:', err);
251
+ process.exit(1);
252
+ });