@librechat/agents 3.1.78-dev.0 → 3.1.79

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 (53) hide show
  1. package/dist/cjs/graphs/Graph.cjs +7 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/index.cjs +44 -55
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +33 -21
  6. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -4
  8. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  9. package/dist/cjs/messages/anthropicToolCache.cjs +48 -15
  10. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  11. package/dist/cjs/messages/format.cjs +97 -14
  12. package/dist/cjs/messages/format.cjs.map +1 -1
  13. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +14 -16
  14. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  15. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +30 -0
  16. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  17. package/dist/esm/graphs/Graph.mjs +7 -0
  18. package/dist/esm/graphs/Graph.mjs.map +1 -1
  19. package/dist/esm/llm/anthropic/index.mjs +43 -54
  20. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  21. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -21
  22. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  23. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -4
  24. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  25. package/dist/esm/messages/anthropicToolCache.mjs +48 -15
  26. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  27. package/dist/esm/messages/format.mjs +97 -14
  28. package/dist/esm/messages/format.mjs.map +1 -1
  29. package/dist/esm/tools/local/LocalExecutionEngine.mjs +14 -16
  30. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  31. package/dist/esm/tools/subagent/SubagentExecutor.mjs +30 -0
  32. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  33. package/dist/types/llm/anthropic/index.d.ts +1 -9
  34. package/dist/types/messages/anthropicToolCache.d.ts +5 -5
  35. package/dist/types/tools/subagent/SubagentExecutor.d.ts +29 -0
  36. package/package.json +1 -1
  37. package/src/graphs/Graph.ts +9 -0
  38. package/src/llm/anthropic/index.ts +55 -64
  39. package/src/llm/anthropic/llm.spec.ts +585 -0
  40. package/src/llm/anthropic/utils/message_inputs.ts +36 -21
  41. package/src/llm/anthropic/utils/message_outputs.ts +0 -4
  42. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +95 -13
  43. package/src/messages/__tests__/anthropicToolCache.test.ts +46 -0
  44. package/src/messages/anthropicToolCache.ts +70 -25
  45. package/src/messages/format.ts +117 -18
  46. package/src/messages/formatAgentMessages.test.ts +202 -1
  47. package/src/scripts/subagent-configurable-inheritance.ts +252 -0
  48. package/src/specs/summarization.test.ts +3 -3
  49. package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
  50. package/src/tools/__tests__/SubagentExecutor.test.ts +148 -0
  51. package/src/tools/local/LocalExecutionEngine.ts +55 -54
  52. package/src/tools/subagent/SubagentExecutor.ts +60 -0
  53. package/src/types/diff.d.ts +15 -0
@@ -428,7 +428,7 @@ const hasAnthropic = process.env.ANTHROPIC_API_KEY != null;
428
428
 
429
429
  // Turn 7: absolute minimum context if still nothing
430
430
  if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
431
- ({ run, contentParts } = await createRun(3100));
431
+ ({ run, contentParts } = await createRun(2200));
432
432
  await runTurn({ run, conversationHistory }, 'What is 1+1?', streamConfig);
433
433
  logTurn('T7', conversationHistory);
434
434
  }
@@ -722,7 +722,7 @@ const hasAnthropic = process.env.ANTHROPIC_API_KEY != null;
722
722
  }
723
723
 
724
724
  if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
725
- run = await createRun(2800);
725
+ run = await createRun(1000);
726
726
  await runTurn(
727
727
  { run, conversationHistory },
728
728
  'What is 9 * 9? Calculator.',
@@ -876,7 +876,7 @@ const hasAnthropic = process.env.ANTHROPIC_API_KEY != null;
876
876
 
877
877
  // Turn 6: squeeze harder if needed
878
878
  if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
879
- ({ run, contentParts } = await createRun(2000));
879
+ ({ run, contentParts } = await createRun(1000));
880
880
  await runTurn(
881
881
  { run, conversationHistory },
882
882
  'What is 42 * 42? Use the calculator.',
@@ -0,0 +1,8 @@
1
+ import { getReadRoots, getWriteRoots } from '../local/LocalExecutionEngine';
2
+
3
+ describe('local execution workspace roots', () => {
4
+ it('uses the current working directory boundary when config is omitted', () => {
5
+ expect(getWriteRoots()).toEqual([process.cwd()]);
6
+ expect(getReadRoots()).toEqual([process.cwd()]);
7
+ });
8
+ });
@@ -546,6 +546,154 @@ describe('SubagentExecutor', () => {
546
546
  expect(observedChildInputs!.maxSubagentDepth).toBeUndefined();
547
547
  });
548
548
 
549
+ describe('parentConfigurable inheritance', () => {
550
+ /**
551
+ * Build a stub factory that captures the second argument to
552
+ * `workflow.invoke()` (the runnable config) so tests can assert on
553
+ * the `configurable` we forwarded to the child graph.
554
+ */
555
+ function makeCapturingGraphFactory(): {
556
+ factory: () => StandardGraph;
557
+ getInvokeConfig: () => Record<string, unknown> | undefined;
558
+ } {
559
+ let capturedConfig: Record<string, unknown> | undefined;
560
+ const factory = (): StandardGraph =>
561
+ ({
562
+ createWorkflow: (): { invoke: jest.Mock } => ({
563
+ invoke: jest
564
+ .fn()
565
+ .mockImplementation(
566
+ async (
567
+ _input: unknown,
568
+ config: Record<string, unknown>
569
+ ): Promise<{ messages: BaseMessage[] }> => {
570
+ capturedConfig = config;
571
+ return { messages: [new AIMessage('done')] };
572
+ }
573
+ ),
574
+ }),
575
+ clearHeavyState: jest.fn(),
576
+ }) as unknown as StandardGraph;
577
+ return { factory, getInvokeConfig: () => capturedConfig };
578
+ }
579
+
580
+ it('forwards parentConfigurable into the child workflow.invoke configurable', async () => {
581
+ const { factory, getInvokeConfig } = makeCapturingGraphFactory();
582
+ const executor = createExecutor({ createChildGraph: factory });
583
+
584
+ await executor.execute({
585
+ description: 'task',
586
+ subagentType: 'researcher',
587
+ parentConfigurable: {
588
+ requestBody: { messageId: 'msg-123', conversationId: 'conv-456' },
589
+ user: { id: 'user_abc' },
590
+ user_id: 'user_abc',
591
+ userMCPAuthMap: { 'mcp-github': { token: 'abc' } },
592
+ },
593
+ });
594
+
595
+ const invokeConfig = getInvokeConfig();
596
+ expect(invokeConfig).toBeDefined();
597
+ const configurable = invokeConfig!.configurable as Record<string, unknown>;
598
+ expect(configurable.requestBody).toEqual({
599
+ messageId: 'msg-123',
600
+ conversationId: 'conv-456',
601
+ });
602
+ expect(configurable.user).toEqual({ id: 'user_abc' });
603
+ expect(configurable.user_id).toBe('user_abc');
604
+ expect(configurable.userMCPAuthMap).toEqual({
605
+ 'mcp-github': { token: 'abc' },
606
+ });
607
+ });
608
+
609
+ it('inherits parent thread_id when supplied (subagent is part of same conversation)', async () => {
610
+ const { factory, getInvokeConfig } = makeCapturingGraphFactory();
611
+ const executor = createExecutor({
612
+ createChildGraph: factory,
613
+ parentRunId: 'parent-run-xyz',
614
+ });
615
+
616
+ await executor.execute({
617
+ description: 'task',
618
+ subagentType: 'researcher',
619
+ parentConfigurable: { thread_id: 'parent-thread-conv-abc' },
620
+ });
621
+
622
+ const configurable = getInvokeConfig()!.configurable as Record<
623
+ string,
624
+ unknown
625
+ >;
626
+ expect(configurable.thread_id).toBe('parent-thread-conv-abc');
627
+ });
628
+
629
+ it('falls back to childRunId for thread_id when parent did not supply one', async () => {
630
+ const { factory, getInvokeConfig } = makeCapturingGraphFactory();
631
+ const executor = createExecutor({
632
+ createChildGraph: factory,
633
+ parentRunId: 'parent-run-xyz',
634
+ });
635
+
636
+ await executor.execute({
637
+ description: 'task',
638
+ subagentType: 'researcher',
639
+ parentConfigurable: { user_id: 'user_abc' },
640
+ });
641
+
642
+ const configurable = getInvokeConfig()!.configurable as Record<
643
+ string,
644
+ unknown
645
+ >;
646
+ expect(configurable.thread_id as string).toMatch(/^parent-run-xyz_sub_/);
647
+ expect(configurable.user_id).toBe('user_abc');
648
+ });
649
+
650
+ it('forwards run-identity fields verbatim into the child invoke configurable', async () => {
651
+ const { factory, getInvokeConfig } = makeCapturingGraphFactory();
652
+ const executor = createExecutor({ createChildGraph: factory });
653
+
654
+ await executor.execute({
655
+ description: 'task',
656
+ subagentType: 'researcher',
657
+ parentConfigurable: {
658
+ run_id: 'parent-run-id',
659
+ parent_run_id: 'grandparent-run-id',
660
+ requestBody: { messageId: 'msg-1' },
661
+ },
662
+ });
663
+
664
+ const configurable = getInvokeConfig()!.configurable as Record<
665
+ string,
666
+ unknown
667
+ >;
668
+ // The SDK forwards these fields as part of its inheritance contract.
669
+ // NOTE: the LangGraph runtime overwrites `configurable.run_id` at
670
+ // actual child-invoke time (verified empirically); this unit test
671
+ // only asserts what the SDK forwards into `workflow.invoke` — not
672
+ // what tools downstream observe. `parent_run_id` and other
673
+ // host-set keys do survive the runtime pass-through.
674
+ expect(configurable.run_id).toBe('parent-run-id');
675
+ expect(configurable.parent_run_id).toBe('grandparent-run-id');
676
+ expect(configurable.requestBody).toEqual({ messageId: 'msg-1' });
677
+ });
678
+
679
+ it('does not require parentConfigurable (back-compat with hosts that omit it)', async () => {
680
+ const { factory, getInvokeConfig } = makeCapturingGraphFactory();
681
+ const executor = createExecutor({ createChildGraph: factory });
682
+
683
+ await executor.execute({
684
+ description: 'task',
685
+ subagentType: 'researcher',
686
+ });
687
+
688
+ const configurable = getInvokeConfig()!.configurable as Record<
689
+ string,
690
+ unknown
691
+ >;
692
+ // Only thread_id (childRunId fallback) is set when no parent context is supplied.
693
+ expect(Object.keys(configurable)).toEqual(['thread_id']);
694
+ });
695
+ });
696
+
549
697
  describe('hooks', () => {
550
698
  let capturedStart: unknown;
551
699
  let capturedStop: unknown;
@@ -5,7 +5,6 @@ import { mkdir, realpath, rm, writeFile } from 'fs/promises';
5
5
  import { createWriteStream } from 'fs';
6
6
  import { spawn } from 'child_process';
7
7
  import type { ChildProcess } from 'child_process';
8
- import type { SandboxRuntimeConfig } from '@anthropic-ai/sandbox-runtime';
9
8
  import { runBashAstChecks, bashAstFindingsToErrors } from './bashAst';
10
9
  import { nodeWorkspaceFS } from './workspaceFS';
11
10
  import type { WorkspaceFS } from './workspaceFS';
@@ -188,8 +187,17 @@ type RuntimeCommand = {
188
187
  source?: string;
189
188
  };
190
189
 
191
- type SandboxRuntimeModule = typeof import('@anthropic-ai/sandbox-runtime');
192
- type SandboxManagerType = SandboxRuntimeModule['SandboxManager'];
190
+ type SandboxManagerType = {
191
+ checkDependencies(): { errors: string[] };
192
+ initialize(config: BuiltSandboxRuntimeConfig): Promise<void>;
193
+ reset(): Promise<void>;
194
+ wrapWithSandbox(command: string): Promise<string>;
195
+ };
196
+
197
+ type SandboxRuntimeModule = {
198
+ getDefaultWritePaths(): string[];
199
+ SandboxManager: SandboxManagerType;
200
+ };
193
201
 
194
202
  let sandboxConfigKey: string | undefined;
195
203
  let sandboxInitialized = false;
@@ -229,9 +237,7 @@ export function getLocalCwd(config?: t.LocalExecutionConfig): string {
229
237
  * Returns plain absolute paths — callers symlink-resolve when they
230
238
  * need realpath equality (see `resolveWorkspacePathSafe`).
231
239
  */
232
- export function getWorkspaceRoots(
233
- config?: t.LocalExecutionConfig
234
- ): string[] {
240
+ export function getWorkspaceRoots(config?: t.LocalExecutionConfig): string[] {
235
241
  const root = getLocalCwd(config);
236
242
  const extras = config?.workspace?.additionalRoots ?? [];
237
243
  if (extras.length === 0) return [root];
@@ -258,9 +264,7 @@ export function getWorkspaceRoots(
258
264
  * back to the legacy top-level `local.spawn`, then to Node's
259
265
  * `child_process.spawn`. Centralised so engine swapping is one knob.
260
266
  */
261
- export function getSpawn(
262
- config?: t.LocalExecutionConfig
263
- ): t.LocalSpawn {
267
+ export function getSpawn(config?: t.LocalExecutionConfig): t.LocalSpawn {
264
268
  return (config?.exec?.spawn ?? config?.spawn ?? spawn) as t.LocalSpawn;
265
269
  }
266
270
 
@@ -269,9 +273,7 @@ export function getSpawn(
269
273
  * to the Node-host implementation. A future remote engine supplies
270
274
  * its own implementation here and inherits every file-touching tool.
271
275
  */
272
- export function getWorkspaceFS(
273
- config?: t.LocalExecutionConfig
274
- ): WorkspaceFS {
276
+ export function getWorkspaceFS(config?: t.LocalExecutionConfig): WorkspaceFS {
275
277
  return config?.exec?.fs ?? nodeWorkspaceFS;
276
278
  }
277
279
 
@@ -282,17 +284,17 @@ export function getWorkspaceFS(
282
284
  * helpers interpret as "skip the write clamp".
283
285
  */
284
286
  export function getWriteRoots(
285
- config?: t.LocalExecutionConfig
287
+ config: t.LocalExecutionConfig = {}
286
288
  ): string[] | null {
287
289
  // Granular flag wins over the legacy one when explicitly set
288
290
  // (true OR false) — otherwise a host tightening access during
289
291
  // migration (`allowOutsideWorkspace: true, workspace.
290
292
  // allowWriteOutside: false`) would still get the loose behavior
291
293
  // because the legacy flag short-circuited the OR. Codex P1 #36.
292
- const granular = config?.workspace?.allowWriteOutside;
294
+ const granular = config.workspace?.allowWriteOutside;
293
295
  if (granular === true) return null;
294
296
  if (granular === false) return getWorkspaceRoots(config);
295
- if (config?.allowOutsideWorkspace === true) return null;
297
+ if (config.allowOutsideWorkspace === true) return null;
296
298
  return getWorkspaceRoots(config);
297
299
  }
298
300
 
@@ -302,14 +304,14 @@ export function getWriteRoots(
302
304
  * `allowOutsideWorkspace`) by returning `null`.
303
305
  */
304
306
  export function getReadRoots(
305
- config?: t.LocalExecutionConfig
307
+ config: t.LocalExecutionConfig = {}
306
308
  ): string[] | null {
307
309
  // Same precedence as getWriteRoots: granular flag is authoritative
308
310
  // when set, legacy flag is the fallback. Codex P1 #36.
309
- const granular = config?.workspace?.allowReadOutside;
311
+ const granular = config.workspace?.allowReadOutside;
310
312
  if (granular === true) return null;
311
313
  if (granular === false) return getWorkspaceRoots(config);
312
- if (config?.allowOutsideWorkspace === true) return null;
314
+ if (config.allowOutsideWorkspace === true) return null;
313
315
  return getWorkspaceRoots(config);
314
316
  }
315
317
 
@@ -323,10 +325,13 @@ const missingSandboxRuntimeMessage = [
323
325
  'Local sandbox is enabled, but @anthropic-ai/sandbox-runtime is not installed.',
324
326
  'Install it with `npm install @anthropic-ai/sandbox-runtime`, or disable local sandboxing with `local.sandbox.enabled: false`.',
325
327
  ].join(' ');
328
+ const sandboxRuntimePackage = '@anthropic-ai/sandbox-runtime';
326
329
 
327
330
  /** Lazy-loads the ESM-only sandbox runtime only when sandboxing is enabled. */
328
331
  function loadSandboxRuntime(): Promise<SandboxRuntimeModule> {
329
- sandboxRuntimePromise ??= import('@anthropic-ai/sandbox-runtime');
332
+ sandboxRuntimePromise ??= import(
333
+ sandboxRuntimePackage
334
+ ) as Promise<SandboxRuntimeModule>;
330
335
  return sandboxRuntimePromise;
331
336
  }
332
337
 
@@ -487,7 +492,9 @@ export async function validateBashCommand(
487
492
  }
488
493
 
489
494
  if (config.readOnly === true && mutatingCommandPattern.test(normalized)) {
490
- errors.push('Command appears to mutate files or repository state in read-only local mode.');
495
+ errors.push(
496
+ 'Command appears to mutate files or repository state in read-only local mode.'
497
+ );
491
498
  }
492
499
 
493
500
  // Use the same shell the actual execution path will use. Hard-coding
@@ -504,12 +511,14 @@ export async function validateBashCommand(
504
511
  sandbox: { enabled: false },
505
512
  },
506
513
  { internal: true }
507
- ).catch((error: Error): SpawnResult => ({
508
- stdout: '',
509
- stderr: error.message,
510
- exitCode: 1,
511
- timedOut: false,
512
- }));
514
+ ).catch(
515
+ (error: Error): SpawnResult => ({
516
+ stdout: '',
517
+ stderr: error.message,
518
+ exitCode: 1,
519
+ timedOut: false,
520
+ })
521
+ );
513
522
 
514
523
  if (syntax.exitCode !== 0) {
515
524
  errors.push(
@@ -567,14 +576,7 @@ async function ensureSandbox(
567
576
  await runtime.SandboxManager.reset();
568
577
  }
569
578
 
570
- // Cast at the runtime boundary — our public `BuiltSandboxRuntimeConfig`
571
- // is intentionally structural to keep the optional peer dep out of
572
- // generated `.d.ts` (Codex P1 #22). It's a structural subset of the
573
- // peer's `SandboxRuntimeConfig`, so the assignment is sound at the
574
- // one site where the peer is actually loaded.
575
- await runtime.SandboxManager.initialize(
576
- runtimeConfig as unknown as SandboxRuntimeConfig
577
- );
579
+ await runtime.SandboxManager.initialize(runtimeConfig);
578
580
  sandboxInitialized = true;
579
581
  sandboxConfigKey = nextKey;
580
582
  return runtime.SandboxManager;
@@ -715,8 +717,7 @@ export async function spawnLocalProcess(
715
717
  // so a process producing unbounded output gets stopped instead of
716
718
  // letting the host OOM.
717
719
  const inMemoryCapBytes = maxOutputChars * 2;
718
- const hardKillBytes =
719
- config.maxSpawnedBytes ?? DEFAULT_MAX_SPAWNED_BYTES;
720
+ const hardKillBytes = config.maxSpawnedBytes ?? DEFAULT_MAX_SPAWNED_BYTES;
720
721
  const sandboxManager = await ensureSandbox(config, cwd);
721
722
  // Internal probes (validateBashCommand syntax preflight,
722
723
  // isRipgrepAvailable, syntax-check probe cache priming) pass
@@ -775,10 +776,7 @@ export async function spawnLocalProcess(
775
776
  spillStream.write('\n===== overflow stream begins here =====\n');
776
777
  };
777
778
 
778
- const handleChunk = (
779
- buf: Buffer,
780
- kind: 'stdout' | 'stderr'
781
- ): void => {
779
+ const handleChunk = (buf: Buffer, kind: 'stdout' | 'stderr'): void => {
782
780
  totalSpawnedBytes += buf.length;
783
781
  // hardKillBytes <= 0 means "no cap" per the public config contract
784
782
  // (see LocalExecutionConfig.maxSpawnedBytes). Skip the kill check
@@ -857,11 +855,11 @@ export async function spawnLocalProcess(
857
855
  }, timeoutMs);
858
856
  }
859
857
 
860
- child.stdout?.on('data', (chunk: Buffer) => {
858
+ child.stdout.on('data', (chunk: Buffer) => {
861
859
  handleChunk(chunk, 'stdout');
862
860
  });
863
861
 
864
- child.stderr?.on('data', (chunk: Buffer) => {
862
+ child.stderr.on('data', (chunk: Buffer) => {
865
863
  handleChunk(chunk, 'stderr');
866
864
  });
867
865
 
@@ -928,8 +926,7 @@ export async function executeLocalBash(
928
926
  * Codex P1 [45], extended for dot-glob in Codex P1 [47] (mirrors the
929
927
  * `DESTRUCTIVE_TARGET` suffix matrix exactly).
930
928
  */
931
- const PROTECTED_TARGET_ARG_RE =
932
- /^(?:\/|~|\$\{?HOME\}?|\.)(?:\/?\.?\*|\/)?$/;
929
+ const PROTECTED_TARGET_ARG_RE = /^(?:\/|~|\$\{?HOME\}?|\.)(?:\/?\.?\*|\/)?$/;
933
930
 
934
931
  /**
935
932
  * Mutating-op recognizer for the args check. Conservative: only the
@@ -971,11 +968,7 @@ export async function executeLocalBashWithArgs(
971
968
  }
972
969
  }
973
970
  const shell = config.shell ?? DEFAULT_SHELL;
974
- return spawnLocalProcess(
975
- shell,
976
- ['-lc', command, '--', ...args],
977
- config
978
- );
971
+ return spawnLocalProcess(shell, ['-lc', command, '--', ...args], config);
979
972
  }
980
973
 
981
974
  export async function executeLocalCode(
@@ -1009,7 +1002,11 @@ export async function executeLocalCode(
1009
1002
  config.shell
1010
1003
  );
1011
1004
  if (runtime.source != null) {
1012
- await writeFile(resolve(tempDir, runtime.fileName), runtime.source, 'utf8');
1005
+ await writeFile(
1006
+ resolve(tempDir, runtime.fileName),
1007
+ runtime.source,
1008
+ 'utf8'
1009
+ );
1013
1010
  }
1014
1011
  return await spawnLocalProcess(runtime.command, runtime.args, config);
1015
1012
  } finally {
@@ -1205,7 +1202,7 @@ function killProcessTree(child: ChildProcess): void {
1205
1202
  // window. Use unref() so the timer doesn't keep the Node process
1206
1203
  // alive past the parent's natural exit.
1207
1204
  const escalation = setTimeout(() => sigkill(child), SIGKILL_ESCALATION_MS);
1208
- escalation.unref?.();
1205
+ escalation.unref();
1209
1206
  child.once('close', () => clearTimeout(escalation));
1210
1207
  }
1211
1208
 
@@ -1225,9 +1222,12 @@ export function resolveWorkspacePath(
1225
1222
  intent: 'read' | 'write' = 'write'
1226
1223
  ): string {
1227
1224
  const cwd = getLocalCwd(config);
1228
- const absolutePath = isAbsolute(filePath) ? resolve(filePath) : resolve(cwd, filePath);
1225
+ const absolutePath = isAbsolute(filePath)
1226
+ ? resolve(filePath)
1227
+ : resolve(cwd, filePath);
1229
1228
 
1230
- const roots = intent === 'write' ? getWriteRoots(config) : getReadRoots(config);
1229
+ const roots =
1230
+ intent === 'write' ? getWriteRoots(config) : getReadRoots(config);
1231
1231
  if (roots == null) return absolutePath; // explicit allow-outside
1232
1232
 
1233
1233
  if (absolutePath === cwd || isInsideAnyRoot(absolutePath, roots)) {
@@ -1306,7 +1306,8 @@ export async function resolveWorkspacePathSafe(
1306
1306
  intent: 'read' | 'write' = 'write'
1307
1307
  ): Promise<string> {
1308
1308
  const lexical = resolveWorkspacePath(filePath, config, intent);
1309
- const roots = intent === 'write' ? getWriteRoots(config) : getReadRoots(config);
1309
+ const roots =
1310
+ intent === 'write' ? getWriteRoots(config) : getReadRoots(config);
1310
1311
  if (roots == null) {
1311
1312
  return lexical;
1312
1313
  }
@@ -40,6 +40,35 @@ export type SubagentExecuteParams = {
40
40
  * without relying on event ordering heuristics.
41
41
  */
42
42
  parentToolCallId?: string;
43
+ /**
44
+ * Snapshot of the parent invocation's `config.configurable` at the
45
+ * spawn-tool call site. Inherited verbatim into the child workflow's
46
+ * `configurable` so host-set fields (`requestBody`, `user`,
47
+ * `userMCPAuthMap`, etc.) propagate — fixing MCP body-placeholder
48
+ * substitution and per-user lookups for subagent tool calls.
49
+ *
50
+ * Inheritance details (verified empirically against LangGraph):
51
+ * - host-set keys propagate as-is into the child's tool dispatches;
52
+ * - `thread_id` propagates (with `childRunId` as a fallback when
53
+ * parent did not supply one) — matches the "subagent is part of
54
+ * the same conversation" mental model and aligns with the
55
+ * `sessionId: this.parentRunId` convention this executor already
56
+ * uses for `SubagentStart` / `SubagentStop` hooks;
57
+ * - `parent_run_id` propagates when the host put it on parent's
58
+ * configurable;
59
+ * - `run_id` is *overwritten by the LangGraph runtime* at child
60
+ * invoke time regardless of what we forward — child's tool
61
+ * dispatches see the child graph's runtime runId in
62
+ * `configurable.run_id`, not the parent's. Hosts that need
63
+ * parent-scoped run identity for downstream consumers should
64
+ * plumb it via a host-defined key (e.g. `requestBody.messageId`),
65
+ * not `run_id`.
66
+ *
67
+ * A future revision will likely make this inheritance configurable
68
+ * per spawn type — background / async subagents may want isolation
69
+ * rather than sharing parent's host context.
70
+ */
71
+ parentConfigurable?: Record<string, unknown>;
43
72
  };
44
73
 
45
74
  export type SubagentExecuteResult = {
@@ -246,6 +275,36 @@ export class SubagentExecutor {
246
275
  * nested trace pollution).
247
276
  */
248
277
  const callbacks: Callbacks = forwarder ? [forwarder] : [];
278
+ /**
279
+ * Inherit the parent's `configurable` verbatim — host-set fields
280
+ * (`requestBody`, `user`, `userMCPAuthMap`, etc.) AND the run-
281
+ * identity fields (`run_id`, `parent_run_id`, `thread_id`) all
282
+ * propagate.
283
+ *
284
+ * Run-identity propagation is intentional and matches the
285
+ * convention this executor itself already uses for `SubagentStart`
286
+ * / `SubagentStop` hooks (`sessionId: this.parentRunId`): the
287
+ * subagent runs under the parent's session scope, not its own.
288
+ * Forwarding `run_id` / `parent_run_id` / `thread_id` makes
289
+ * `ToolNode`'s hook lookups (`hasHookFor(eventName, runId)`),
290
+ * `ToolOutputReferenceRegistry` keying, and trace lineage all
291
+ * resolve to the parent's session for tools dispatched from the
292
+ * subagent — so `PreToolUse` / `PostToolUse` hooks the host
293
+ * registered against the parent's run fire for subagent tool
294
+ * calls too. "Same run" matches the user-perceptual mental model.
295
+ *
296
+ * `thread_id` falls back to `childRunId` only when the parent
297
+ * didn't supply one (legacy behavior preserved for hosts that
298
+ * never set thread_id).
299
+ *
300
+ * NOTE: a future revision will likely make this configurable per
301
+ * spawn type — e.g. a background / async subagent that runs after
302
+ * the parent's run completes wants isolation, not inheritance.
303
+ * For now the inheritance path matches LibreChat's primary use
304
+ * case (synchronous subagents within a single user turn).
305
+ */
306
+ const inheritedConfigurable: Record<string, unknown> =
307
+ params.parentConfigurable ?? {};
249
308
  result = await workflow.invoke(
250
309
  { messages: [new HumanMessage(description)] },
251
310
  {
@@ -255,6 +314,7 @@ export class SubagentExecutor {
255
314
  runName: `subagent:${subagentType}`,
256
315
  configurable: {
257
316
  thread_id: childRunId,
317
+ ...inheritedConfigurable,
258
318
  },
259
319
  }
260
320
  );
@@ -0,0 +1,15 @@
1
+ declare module 'diff' {
2
+ export type PatchOptions = {
3
+ context?: number;
4
+ };
5
+
6
+ export function createTwoFilesPatch(
7
+ oldFileName: string,
8
+ newFileName: string,
9
+ oldStr: string,
10
+ newStr: string,
11
+ oldHeader?: string,
12
+ newHeader?: string,
13
+ options?: PatchOptions
14
+ ): string;
15
+ }