@librechat/agents 3.8.6 → 3.8.8

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/main.cjs +1 -0
  2. package/dist/cjs/tools/BashExecutor.cjs +83 -28
  3. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  4. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +50 -7
  5. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  6. package/dist/cjs/tools/CodeExecutor.cjs +5 -2
  7. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  8. package/dist/cjs/tools/CodeSessionFileSummary.cjs +11 -1
  9. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -1
  10. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +54 -8
  11. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  12. package/dist/cjs/tools/ToolNode.cjs +52 -20
  13. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  14. package/dist/cjs/tools/toolBatchReplay.cjs +3 -1
  15. package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -1
  16. package/dist/cjs/tools/workspaceIdentity.cjs +12 -0
  17. package/dist/cjs/tools/workspaceIdentity.cjs.map +1 -0
  18. package/dist/esm/main.mjs +2 -2
  19. package/dist/esm/tools/BashExecutor.mjs +85 -30
  20. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  21. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +51 -9
  22. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  23. package/dist/esm/tools/CodeExecutor.mjs +5 -2
  24. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  25. package/dist/esm/tools/CodeSessionFileSummary.mjs +11 -2
  26. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -1
  27. package/dist/esm/tools/ProgrammaticToolCalling.mjs +55 -9
  28. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  29. package/dist/esm/tools/ToolNode.mjs +52 -20
  30. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  31. package/dist/esm/tools/toolBatchReplay.mjs +3 -1
  32. package/dist/esm/tools/toolBatchReplay.mjs.map +1 -1
  33. package/dist/esm/tools/workspaceIdentity.mjs +12 -0
  34. package/dist/esm/tools/workspaceIdentity.mjs.map +1 -0
  35. package/dist/types/tools/BashExecutor.d.ts +2 -0
  36. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +1 -0
  37. package/dist/types/tools/CodeExecutor.d.ts +1 -1
  38. package/dist/types/tools/CodeSessionFileSummary.d.ts +3 -0
  39. package/dist/types/tools/ProgrammaticToolCalling.d.ts +2 -2
  40. package/dist/types/tools/ToolNode.d.ts +2 -0
  41. package/dist/types/tools/toolBatchReplay.d.ts +2 -0
  42. package/dist/types/tools/workspaceIdentity.d.ts +1 -0
  43. package/dist/types/types/tools.d.ts +20 -2
  44. package/package.json +2 -2
  45. package/src/tools/BashExecutor.ts +156 -40
  46. package/src/tools/BashProgrammaticToolCalling.ts +90 -10
  47. package/src/tools/CodeExecutor.ts +12 -2
  48. package/src/tools/CodeSessionFileSummary.ts +25 -1
  49. package/src/tools/ProgrammaticToolCalling.ts +77 -6
  50. package/src/tools/ToolNode.ts +152 -26
  51. package/src/tools/toolBatchReplay.ts +23 -1
  52. package/src/tools/workspaceIdentity.ts +15 -0
  53. package/src/types/tools.ts +23 -2
@@ -1,5 +1,6 @@
1
1
  // src/tools/ProgrammaticToolCalling.ts
2
2
  import { config } from 'dotenv';
3
+ import { randomUUID } from 'node:crypto';
3
4
  import fetch, { RequestInit } from 'node-fetch';
4
5
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
5
6
  import type { ToolCall } from '@langchain/core/messages/tool';
@@ -21,6 +22,7 @@ import {
21
22
  resolveCodeApiAuthHeaders,
22
23
  selectRuntimeSessionHint,
23
24
  } from './CodeExecutor';
25
+ import { appendExecutionArtifactFileSummary } from './CodeSessionFileSummary';
24
26
  import {
25
27
  assertUnambiguousIdentifiers,
26
28
  projectProgrammaticToolMap,
@@ -51,6 +53,9 @@ config();
51
53
  const DEFAULT_MAX_ROUND_TRIPS = 20;
52
54
 
53
55
  const DEFAULT_RUN_TIMEOUT_MS = resolveCodeApiRunTimeoutMs();
56
+ const CODE_API_PROGRAMMATIC_REQUEST_HEADER =
57
+ 'X-LibreChat-Code-Request-ID';
58
+ const CANCELLATION_REQUEST_TIMEOUT_MS = 2_000;
54
59
 
55
60
  // ============================================================================
56
61
  // Description Components (Single Source of Truth)
@@ -513,10 +518,12 @@ export async function makeRequest(
513
518
  body: Record<string, unknown>,
514
519
  proxy?: string,
515
520
  authHeaders?: t.CodeApiAuthHeaders,
516
- executionProfile?: t.CodeApiExecutionProfile
521
+ executionProfile?: t.CodeApiExecutionProfile,
522
+ signal?: AbortSignal
517
523
  ): Promise<t.ProgrammaticExecutionResponse> {
518
524
  try {
519
525
  const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders);
526
+ const requestId = signal != null ? randomUUID() : undefined;
520
527
  const fetchOptions: RequestInit = {
521
528
  method: 'POST',
522
529
  headers: {
@@ -526,8 +533,12 @@ export async function makeRequest(
526
533
  resolvedAuthHeaders,
527
534
  executionProfile
528
535
  ),
536
+ ...(requestId != null
537
+ ? { [CODE_API_PROGRAMMATIC_REQUEST_HEADER]: requestId }
538
+ : {}),
529
539
  },
530
540
  body: JSON.stringify(body),
541
+ signal: signal as RequestInit['signal'],
531
542
  };
532
543
 
533
544
  const proxyAgent = resolveFetchProxyAgent(endpoint, proxy);
@@ -535,7 +546,59 @@ export async function makeRequest(
535
546
  fetchOptions.agent = proxyAgent;
536
547
  }
537
548
 
538
- const response = await fetch(endpoint, fetchOptions);
549
+ let cancellationRequest: Promise<void> | undefined;
550
+ const requestCancellation = (): void => {
551
+ if (requestId == null || cancellationRequest != null) {
552
+ return;
553
+ }
554
+ cancellationRequest = (async (): Promise<void> => {
555
+ const cancellationController = new AbortController();
556
+ const timeout = setTimeout(
557
+ () => cancellationController.abort(),
558
+ CANCELLATION_REQUEST_TIMEOUT_MS
559
+ );
560
+ try {
561
+ const cancelOptions: RequestInit = {
562
+ method: 'POST',
563
+ headers: {
564
+ 'Content-Type': 'application/json',
565
+ 'User-Agent': 'LibreChat/1.0',
566
+ ...addCodeApiExecutionProfileHeader(
567
+ resolvedAuthHeaders,
568
+ executionProfile
569
+ ),
570
+ },
571
+ body: JSON.stringify({ request_id: requestId }),
572
+ signal: cancellationController.signal as RequestInit['signal'],
573
+ };
574
+ if (proxyAgent != null) {
575
+ cancelOptions.agent = proxyAgent;
576
+ }
577
+ await fetch(`${endpoint}/cancel`, cancelOptions);
578
+ } catch {
579
+ // Cancellation is best effort for older Code API versions and
580
+ // transient network failures. The original abort remains the public
581
+ // result while the server-side execution deadline is the fallback.
582
+ } finally {
583
+ clearTimeout(timeout);
584
+ }
585
+ })();
586
+ };
587
+ signal?.addEventListener('abort', requestCancellation, { once: true });
588
+ if (signal?.aborted === true) requestCancellation();
589
+
590
+ let response;
591
+ try {
592
+ response = await fetch(endpoint, fetchOptions);
593
+ } catch (error) {
594
+ if (signal?.aborted === true) {
595
+ requestCancellation();
596
+ await cancellationRequest;
597
+ }
598
+ throw error;
599
+ } finally {
600
+ signal?.removeEventListener('abort', requestCancellation);
601
+ }
539
602
 
540
603
  if (!response.ok) {
541
604
  throw new CodeApiRequestError(
@@ -860,7 +923,8 @@ export async function executeTools(
860
923
  */
861
924
  export function formatCompletedResponse(
862
925
  response: t.ProgrammaticExecutionResponse,
863
- sourceCode = ''
926
+ sourceCode = '',
927
+ filePersistence: 'session' | 'execution' = 'session'
864
928
  ): [string, t.ProgrammaticExecutionArtifact] {
865
929
  let formatted = '';
866
930
 
@@ -884,10 +948,15 @@ export function formatCompletedResponse(
884
948
  );
885
949
 
886
950
  return [
887
- appendCodeSessionFileSummary(outputWithDeliveryWarning, response.files),
951
+ filePersistence === 'execution'
952
+ ? appendExecutionArtifactFileSummary(outputWithDeliveryWarning, response.files)
953
+ : appendCodeSessionFileSummary(outputWithDeliveryWarning, response.files),
888
954
  {
889
955
  session_id: response.session_id,
890
956
  files: response.files,
957
+ ...(response.deleted_files != null
958
+ ? { deleted_files: response.deleted_files }
959
+ : {}),
891
960
  ...(artifactDelivery != null
892
961
  ? { artifact_delivery: artifactDelivery }
893
962
  : {}),
@@ -1161,7 +1230,8 @@ export function createProgrammaticToolCallingTool(
1161
1230
  },
1162
1231
  proxy,
1163
1232
  initParams.authHeaders,
1164
- initParams.executionProfile
1233
+ initParams.executionProfile,
1234
+ config.signal
1165
1235
  );
1166
1236
 
1167
1237
  // ====================================================================
@@ -1199,7 +1269,8 @@ export function createProgrammaticToolCallingTool(
1199
1269
  },
1200
1270
  proxy,
1201
1271
  initParams.authHeaders,
1202
- initParams.executionProfile
1272
+ initParams.executionProfile,
1273
+ config.signal
1203
1274
  );
1204
1275
  }
1205
1276
 
@@ -221,6 +221,8 @@ type RunToolBatchContext<T = unknown> = {
221
221
  batchScopeId?: string;
222
222
  /** Batch-local sink for post-substitution args. */
223
223
  resolvedArgsByCallId?: ResolvedArgsByCallId;
224
+ /** File identities actually injected into each direct code call. */
225
+ codeSessionBaselineByCallId?: Map<string, ReadonlyMap<string, string>>;
224
226
  /**
225
227
  * Frozen pre-batch view of the tool-output registry. When supplied,
226
228
  * `runTool` resolves `{{tool…turn…}}` placeholders against this
@@ -651,29 +653,78 @@ function toInjectedFileRef(
651
653
 
652
654
  /* Stable file identity = `(storage_session_id, id)`. Same name in
653
655
  * different storage sessions are distinct files. */
654
- function fileIdentityKey(file: {
655
- storage_session_id?: string;
656
- id: string;
657
- }): string {
658
- return `${file.storage_session_id ?? ''}\0${file.id}`;
656
+ function fileIdentityKey(
657
+ file: {
658
+ storage_session_id?: string;
659
+ id: string;
660
+ },
661
+ fallbackSessionId = ''
662
+ ): string {
663
+ return `${file.storage_session_id ?? fallbackSessionId}\0${file.id}`;
664
+ }
665
+
666
+ function codeSessionIdentityByName(
667
+ context:
668
+ | t.ToolCallRequest['codeSessionContext']
669
+ | t.CodeSessionContext
670
+ | undefined
671
+ ): ReadonlyMap<string, string> {
672
+ return new Map(
673
+ (context?.files ?? []).map((file) => [
674
+ file.name,
675
+ fileIdentityKey(file, context?.session_id),
676
+ ])
677
+ );
678
+ }
679
+
680
+ function storageSessionIdFromIdentity(
681
+ identity: string | undefined,
682
+ fileId: string
683
+ ): string | undefined {
684
+ const suffix = `\0${fileId}`;
685
+ return identity != null && identity.endsWith(suffix)
686
+ ? identity.slice(0, identity.length - suffix.length)
687
+ : undefined;
659
688
  }
660
689
 
661
690
  function updateCodeSession(
662
691
  sessions: t.ToolSessionMap,
663
692
  sessionKey: string,
664
693
  execSessionId: string,
665
- files: t.FileRefs | undefined
694
+ files: t.FileRefs | undefined,
695
+ deletedFiles: readonly string[] | undefined,
696
+ baselineIdentityByName: ReadonlyMap<string, string> | undefined
666
697
  ): void {
667
698
  const newFiles = files ?? [];
668
699
  const existingSession = sessions.get(sessionKey) as
669
700
  | t.CodeSessionContext
670
701
  | undefined;
671
- const existingFiles = existingSession?.files ?? [];
702
+ const existingSessionId = existingSession?.session_id ?? '';
703
+ const existingFiles = (existingSession?.files ?? []).map((file) =>
704
+ file.storage_session_id == null
705
+ ? { ...file, storage_session_id: existingSessionId }
706
+ : file
707
+ );
708
+ const deletedIdentityByName = new Map<string, string>();
709
+ for (const name of deletedFiles ?? []) {
710
+ const identity = baselineIdentityByName?.get(name);
711
+ if (identity != null) {
712
+ deletedIdentityByName.set(name, identity);
713
+ }
714
+ }
672
715
 
716
+ /* Absence from `files` is not a deletion signal: older Code API versions
717
+ * and truncated responses can omit otherwise-live refs. Remove only paths
718
+ * explicitly attested by `deleted_files`, and only while the current ref
719
+ * still has the identity injected into that request. */
673
720
  if (newFiles.length === 0) {
674
721
  sessions.set(sessionKey, {
675
722
  session_id: execSessionId,
676
- files: existingFiles,
723
+ files: existingFiles.filter(
724
+ (file) =>
725
+ deletedIdentityByName.get(file.name) !==
726
+ fileIdentityKey(file, existingSessionId)
727
+ ),
677
728
  lastUpdated: Date.now(),
678
729
  });
679
730
  return;
@@ -685,26 +736,46 @@ function updateCodeSession(
685
736
  const filesWithSession: t.FileRefs = [];
686
737
  const newFileNames = new Set<string>();
687
738
  const incomingByIdentity = new Map<string, number>();
739
+ const existingIdentities = new Set(
740
+ existingFiles.map((file) => fileIdentityKey(file, existingSessionId))
741
+ );
688
742
  for (const file of newFiles) {
743
+ const baselineIdentity = baselineIdentityByName?.get(file.name);
689
744
  const withSession = {
690
745
  ...file,
691
- storage_session_id: file.storage_session_id ?? execSessionId,
746
+ storage_session_id:
747
+ file.storage_session_id ??
748
+ storageSessionIdFromIdentity(baselineIdentity, file.id) ??
749
+ execSessionId,
692
750
  };
693
- incomingByIdentity.set(
694
- fileIdentityKey(withSession),
695
- filesWithSession.length
696
- );
751
+ const identity = fileIdentityKey(withSession);
752
+ const wasTrackedByRequest = baselineIdentity === identity;
753
+ /* An inherited echo is evidence that a request observed an existing ref,
754
+ * not authority to resurrect one removed by a concurrent sibling. Fresh
755
+ * and modified outputs remain eligible to replace by path below. */
756
+ if (
757
+ file.inherited === true &&
758
+ wasTrackedByRequest &&
759
+ (!existingIdentities.has(identity) ||
760
+ deletedIdentityByName.get(file.name) === identity)
761
+ ) {
762
+ continue;
763
+ }
764
+ incomingByIdentity.set(identity, filesWithSession.length);
697
765
  newFileNames.add(withSession.name);
698
766
  filesWithSession.push(withSession);
699
767
  }
700
768
 
701
769
  const filteredExisting: t.FileRefs = [];
702
770
  for (const e of existingFiles) {
703
- const idx = incomingByIdentity.get(fileIdentityKey(e));
771
+ const idx = incomingByIdentity.get(fileIdentityKey(e, existingSessionId));
704
772
  if (idx !== undefined) {
705
773
  filesWithSession[idx] = { ...e, ...filesWithSession[idx] };
706
774
  }
707
- if (!newFileNames.has(e.name)) {
775
+ const explicitlyDeleted =
776
+ deletedIdentityByName.get(e.name) ===
777
+ fileIdentityKey(e, existingSessionId);
778
+ if (!newFileNames.has(e.name) && !explicitlyDeleted) {
708
779
  filteredExisting.push(e);
709
780
  }
710
781
  }
@@ -1898,6 +1969,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1898
1969
  | t.CodeSessionContext
1899
1970
  | undefined;
1900
1971
  const execSessionId = codeSession?.session_id;
1972
+ if (batchContext.codeSessionBaselineByCallId != null) {
1973
+ batchContext.codeSessionBaselineByCallId.set(
1974
+ call.id ?? '',
1975
+ codeSessionIdentityByName(codeSession)
1976
+ );
1977
+ }
1901
1978
  if (execSessionId != null && execSessionId !== '') {
1902
1979
  invokeParams = {
1903
1980
  ...invokeParams,
@@ -3095,7 +3172,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3095
3172
  baselineByRequestId: ReadonlyMap<
3096
3173
  string,
3097
3174
  ReadonlyMap<string, string>
3098
- >
3175
+ > = new Map(),
3176
+ executionBaselineByRequestId: ReadonlyMap<
3177
+ string,
3178
+ ReadonlyMap<string, string>
3179
+ > = baselineByRequestId
3099
3180
  ): void {
3100
3181
  if (!this.sessions) {
3101
3182
  return;
@@ -3131,7 +3212,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3131
3212
  this.sessions,
3132
3213
  this.codeSessionKey,
3133
3214
  execSessionId,
3134
- artifact?.files
3215
+ artifact?.files,
3216
+ artifact?.deleted_files,
3217
+ executionBaselineByRequestId.get(request.id)
3135
3218
  );
3136
3219
  }
3137
3220
  }
@@ -3200,6 +3283,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3200
3283
  outputs: (BaseMessage | Command)[],
3201
3284
  config: RunnableConfig,
3202
3285
  resolvedArgsByCallId?: ResolvedArgsByCallId,
3286
+ codeSessionBaselineByCallId?: ReadonlyMap<
3287
+ string,
3288
+ ReadonlyMap<string, string>
3289
+ >,
3203
3290
  errorOwnership?: ToolErrorOwnership
3204
3291
  ): Promise<void> {
3205
3292
  const ownership = errorOwnership ?? this.looseErrorOwnership;
@@ -3235,7 +3322,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3235
3322
  ownership.undispatched.delete(toolCallId);
3236
3323
  }
3237
3324
 
3238
- if (this.sessions && this.participatesInCodeSession(call.name)) {
3325
+ if (
3326
+ toolMessage.status !== 'error' &&
3327
+ this.sessions &&
3328
+ this.participatesInCodeSession(call.name)
3329
+ ) {
3239
3330
  const artifact = toolMessage.artifact as
3240
3331
  | t.CodeExecutionArtifact
3241
3332
  | undefined;
@@ -3245,7 +3336,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3245
3336
  this.sessions,
3246
3337
  this.codeSessionKey,
3247
3338
  execSessionId,
3248
- artifact?.files
3339
+ artifact?.files,
3340
+ artifact?.deleted_files,
3341
+ codeSessionBaselineByCallId?.get(toolCallId)
3249
3342
  );
3250
3343
  }
3251
3344
  }
@@ -4112,12 +4205,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4112
4205
  >(
4113
4206
  plan.allRequests.map((request) => [
4114
4207
  request.id,
4115
- new Map(
4116
- (request.codeSessionContext?.files ?? []).map((file) => [
4117
- file.name,
4118
- fileIdentityKey(file),
4119
- ])
4120
- ),
4208
+ codeSessionIdentityByName(request.codeSessionContext),
4121
4209
  ])
4122
4210
  );
4123
4211
  const eagerExecutions: Array<{
@@ -4319,11 +4407,25 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4319
4407
  ...flattenedEagerResults,
4320
4408
  ...dispatchedResults,
4321
4409
  ];
4410
+ /* Hosts may lazily refresh codeSessionContext while dispatching. The
4411
+ * pre-dispatch snapshot above remains the authority for detecting those
4412
+ * additions, while deletion reconciliation must use the identities the
4413
+ * execution actually received. */
4414
+ const executionBaselineByRequestId = new Map<
4415
+ string,
4416
+ ReadonlyMap<string, string>
4417
+ >(
4418
+ plan.allRequests.map((request) => [
4419
+ request.id,
4420
+ codeSessionIdentityByName(request.codeSessionContext),
4421
+ ])
4422
+ );
4322
4423
 
4323
4424
  this.storeCodeSessionFromResults(
4324
4425
  results,
4325
4426
  requestMap,
4326
- codeSessionBaselineByRequestId
4427
+ codeSessionBaselineByRequestId,
4428
+ executionBaselineByRequestId
4327
4429
  );
4328
4430
 
4329
4431
  for (const result of results) {
@@ -4993,6 +5095,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
4993
5095
  ) {
4994
5096
  baseContext.resolvedArgsByCallId.set(call.id, result.resolvedArgs);
4995
5097
  }
5098
+ if (
5099
+ result.codeSessionBaseline != null &&
5100
+ baseContext.codeSessionBaselineByCallId != null
5101
+ ) {
5102
+ baseContext.codeSessionBaselineByCallId.set(
5103
+ call.id ?? '',
5104
+ new Map(result.codeSessionBaseline)
5105
+ );
5106
+ }
4996
5107
  return result;
4997
5108
  };
4998
5109
  const runOne = async (
@@ -5029,6 +5140,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5029
5140
  call.id == null
5030
5141
  ? undefined
5031
5142
  : baseContext.resolvedArgsByCallId?.get(call.id);
5143
+ const codeSessionBaseline =
5144
+ baseContext.codeSessionBaselineByCallId?.get(call.id ?? '');
5032
5145
  const result: SettledDirectToolResult = {
5033
5146
  proposal: structuredClone({
5034
5147
  name: call.name,
@@ -5042,6 +5155,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5042
5155
  output,
5043
5156
  additionalContexts,
5044
5157
  ...(resolvedArgs == null ? {} : { resolvedArgs }),
5158
+ ...(codeSessionBaseline == null
5159
+ ? {}
5160
+ : { codeSessionBaseline: [...codeSessionBaseline.entries()] }),
5045
5161
  };
5046
5162
  const refMeta = isBaseMessage(output)
5047
5163
  ? (output.additional_kwargs as t.ToolMessageRefMetadata)
@@ -5260,6 +5376,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5260
5376
  * ToolNode cannot read or wipe each other's entries.
5261
5377
  */
5262
5378
  const resolvedArgsByCallId = new Map<string, Record<string, unknown>>();
5379
+ const codeSessionBaselineByCallId = new Map<
5380
+ string,
5381
+ ReadonlyMap<string, string>
5382
+ >();
5263
5383
  /** Per-invocation error-completion ownership — see `ToolErrorOwnership`. */
5264
5384
  const errorOwnership = createToolErrorOwnership();
5265
5385
  /**
@@ -5340,6 +5460,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5340
5460
  batchScopeId,
5341
5461
  preBatchSnapshot: replayInputSnapshot,
5342
5462
  resolvedArgsByCallId,
5463
+ codeSessionBaselineByCallId,
5343
5464
  errorOwnership,
5344
5465
  additionalContextsSink: directAdditionalContexts,
5345
5466
  replayBatchKey: sendReplayBatchKey,
@@ -5368,6 +5489,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5368
5489
  [sendOutput],
5369
5490
  config,
5370
5491
  resolvedArgsByCallId,
5492
+ codeSessionBaselineByCallId,
5371
5493
  errorOwnership
5372
5494
  );
5373
5495
  } else {
@@ -5625,6 +5747,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5625
5747
  turn,
5626
5748
  batchScopeId,
5627
5749
  resolvedArgsByCallId,
5750
+ codeSessionBaselineByCallId,
5628
5751
  errorOwnership,
5629
5752
  preBatchSnapshot,
5630
5753
  additionalContextsSink: directAdditionalContexts,
@@ -5662,6 +5785,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5662
5785
  unhandledDirectOutputs,
5663
5786
  config,
5664
5787
  resolvedArgsByCallId,
5788
+ codeSessionBaselineByCallId,
5665
5789
  errorOwnership
5666
5790
  );
5667
5791
  for (let i = 0; i < directCalls.length; i++) {
@@ -5737,6 +5861,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5737
5861
  turn,
5738
5862
  batchScopeId,
5739
5863
  resolvedArgsByCallId,
5864
+ codeSessionBaselineByCallId,
5740
5865
  errorOwnership,
5741
5866
  preBatchSnapshot,
5742
5867
  additionalContextsSink: directAdditionalContexts,
@@ -5751,6 +5876,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
5751
5876
  toolOutputs,
5752
5877
  config,
5753
5878
  resolvedArgsByCallId,
5879
+ codeSessionBaselineByCallId,
5754
5880
  errorOwnership
5755
5881
  );
5756
5882
  // Append accumulated additionalContexts as a single
@@ -32,6 +32,8 @@ export interface SettledToolBatchResult {
32
32
  output: BaseMessage | Command;
33
33
  additionalContexts: string[];
34
34
  resolvedArgs?: Record<string, unknown>;
35
+ /** Code-session identities injected into this completed direct call. */
36
+ codeSessionBaseline?: Array<[string, string]>;
35
37
  completionHandled?: boolean;
36
38
  referenceContent?: string;
37
39
  turn?: number;
@@ -474,6 +476,14 @@ export async function restoreToolBatchReplayState(
474
476
  throw new Error('Invalid settled tool batch results');
475
477
  }
476
478
  const result = entry[1] as Partial<SettledToolBatchResult> | null;
479
+ const codeSessionBaseline: unknown = (
480
+ result as { codeSessionBaseline?: unknown } | null
481
+ )?.codeSessionBaseline;
482
+ const baselineNames = Array.isArray(codeSessionBaseline)
483
+ ? codeSessionBaseline.map((pair) =>
484
+ Array.isArray(pair) ? pair[0] : undefined
485
+ )
486
+ : [];
477
487
  if (
478
488
  result == null ||
479
489
  (!isBaseMessage(result.output) && !isCommand(result.output)) ||
@@ -491,7 +501,19 @@ export async function restoreToolBatchReplayState(
491
501
  typeof result.proposal.name !== 'string' ||
492
502
  result.proposal.name.length === 0 ||
493
503
  !isRecord(result.proposal.args) ||
494
- (result.resolvedArgs != null && !isRecord(result.resolvedArgs))
504
+ (result.resolvedArgs != null && !isRecord(result.resolvedArgs)) ||
505
+ (codeSessionBaseline != null &&
506
+ (!Array.isArray(codeSessionBaseline) ||
507
+ codeSessionBaseline.some(
508
+ (pair) =>
509
+ !Array.isArray(pair) ||
510
+ pair.length !== 2 ||
511
+ typeof pair[0] !== 'string' ||
512
+ pair[0].length === 0 ||
513
+ typeof pair[1] !== 'string' ||
514
+ pair[1].length === 0
515
+ ) ||
516
+ new Set(baselineNames).size !== baselineNames.length))
495
517
  ) {
496
518
  throw new Error('Invalid settled tool batch results');
497
519
  }
@@ -0,0 +1,15 @@
1
+ const WORKSPACE_INSTANCE_ID_PATTERN = /^[a-f0-9]{64}$/;
2
+
3
+ export function resolveAttachedWorkspaceInstanceId(
4
+ value: string | undefined,
5
+ hasWorkspace: boolean
6
+ ): string | undefined {
7
+ const workspaceInstanceId = value?.trim();
8
+ if (workspaceInstanceId == null || workspaceInstanceId === '') {
9
+ return undefined;
10
+ }
11
+ if (!hasWorkspace || !WORKSPACE_INSTANCE_ID_PATTERN.test(workspaceInstanceId)) {
12
+ throw new Error('Invalid attached workspace instance identifier');
13
+ }
14
+ return workspaceInstanceId;
15
+ }
@@ -502,6 +502,8 @@ export type ExecuteResult = {
502
502
  stdout: string;
503
503
  stderr: string;
504
504
  files?: FileRefs;
505
+ /** Persisted input paths explicitly removed during this execution. */
506
+ deleted_files?: string[];
505
507
  artifact_delivery?: ArtifactDeliveryFailure;
506
508
  /**
507
509
  * Durable runtime session id echoed by a stateful Code API backend
@@ -1346,6 +1348,8 @@ export type ProgrammaticExecutionResponse = {
1346
1348
  stdout?: string;
1347
1349
  stderr?: string;
1348
1350
  files?: FileRefs;
1351
+ /** Persisted input paths explicitly removed during this execution. */
1352
+ deleted_files?: string[];
1349
1353
  artifact_delivery?: ArtifactDeliveryFailure;
1350
1354
 
1351
1355
  /** Durable runtime session echo from a stateful backend (additive). */
@@ -1363,6 +1367,8 @@ export type ProgrammaticExecutionArtifact = {
1363
1367
  /** Execution session — see `CodeSessionContext.session_id`. */
1364
1368
  session_id?: string;
1365
1369
  files?: FileRefs;
1370
+ /** Persisted input paths explicitly removed during this execution. */
1371
+ deleted_files?: string[];
1366
1372
  artifact_delivery?: ArtifactDeliveryFailure;
1367
1373
  /** Durable runtime session echo from a stateful backend (additive). */
1368
1374
  runtime_session_id?: string;
@@ -1370,10 +1376,23 @@ export type ProgrammaticExecutionArtifact = {
1370
1376
  };
1371
1377
 
1372
1378
  /** Parameters for creating a bash execution tool (same API as CodeExecutor, bash-only) */
1373
- export type BashExecutionToolParams = CodeExecutionToolParams;
1379
+ export type BashExecutionToolParams =
1380
+ | undefined
1381
+ | (Exclude<CodeExecutionToolParams, undefined> & {
1382
+ /** Trusted attached-workspace identifier selected by the host. */
1383
+ workspaceId?: string;
1384
+ /** Trusted conversation-owned workspace instance selected by the host. */
1385
+ workspaceInstanceId?: string;
1386
+ });
1374
1387
 
1375
1388
  /** Parameters for creating a bash programmatic tool calling tool (same API as PTC, bash-only) */
1376
- export type BashProgrammaticToolCallingParams = ProgrammaticToolCallingParams;
1389
+ export type BashProgrammaticToolCallingParams =
1390
+ ProgrammaticToolCallingParams & {
1391
+ /** Trusted attached-workspace identifier selected by the host. */
1392
+ workspaceId?: string;
1393
+ /** Trusted conversation-owned workspace instance selected by the host. */
1394
+ workspaceInstanceId?: string;
1395
+ };
1377
1396
 
1378
1397
  /**
1379
1398
  * Initialization parameters for the PTC tool
@@ -1430,6 +1449,8 @@ export type CodeExecutionArtifact = {
1430
1449
  /** Execution session — see `CodeSessionContext.session_id`. */
1431
1450
  session_id?: string;
1432
1451
  files?: FileRefs;
1452
+ /** Persisted input paths explicitly removed during this execution. */
1453
+ deleted_files?: string[];
1433
1454
  artifact_delivery?: ArtifactDeliveryFailure;
1434
1455
  /** Durable runtime session echo from a stateful backend (additive). */
1435
1456
  runtime_session_id?: string;