@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
@@ -11,6 +11,7 @@ import {
11
11
  addCodeApiExecutionProfileHeader,
12
12
  emptyOutputMessage,
13
13
  buildCodeApiHttpErrorMessage,
14
+ buildCodeApiExecutionErrorMessage,
14
15
  CodeApiRequestError,
15
16
  buildCodeApiEndpoint,
16
17
  getCodeBaseURL,
@@ -23,9 +24,13 @@ import {
23
24
  normalizeArtifactDeliveryFailure,
24
25
  } from '@/tools/ArtifactDelivery';
25
26
  import { logCodeApiDiagnostic } from '@/tools/diagnostics';
27
+ import { appendExecutionArtifactFileSummary } from '@/tools/CodeSessionFileSummary';
28
+ import { resolveAttachedWorkspaceInstanceId } from '@/tools/workspaceIdentity';
26
29
  import { resolveFetchProxyAgent } from '@/utils/proxy';
27
30
  import { INTENT_PROPERTY } from '@/tools/intentArg';
28
31
  import { Constants } from '@/common';
32
+ import { prepareBashProgrammaticCode } from './BashProgrammaticToolCalling';
33
+ import { makeRequest } from './ProgrammaticToolCalling';
29
34
 
30
35
  config();
31
36
 
@@ -90,6 +95,17 @@ Usage:
90
95
  - NEVER use this tool to execute malicious commands.
91
96
  `.trim();
92
97
 
98
+ const AttachedWorkspaceBashExecutionToolDescription = `
99
+ Runs bash commands in the selected persistent project through an isolated sandbox process.
100
+
101
+ Usage:
102
+ - Project file changes persist between calls; shell variables, background processes, and execution-private temporary files do not.
103
+ - Injected files and generated artifacts use \${LIBRECHAT_CODE_DATA_DIR:-/mnt/data}; write durable files to the project root.
104
+ - Generated artifacts are automatically delivered; **DO NOT** provide download links.
105
+ - ${BASH_SHELL_GUIDANCE}
106
+ - NEVER use this tool to execute malicious commands.
107
+ `.trim();
108
+
93
109
  /**
94
110
  * Supplemental prompt documenting the tool-output reference feature.
95
111
  *
@@ -121,11 +137,14 @@ Referencing previous tool outputs:
121
137
  export function buildBashExecutionToolDescription(options?: {
122
138
  enableToolOutputReferences?: boolean;
123
139
  statefulSessions?: boolean;
140
+ attachedWorkspace?: boolean;
124
141
  }): string {
125
- const base =
126
- options?.statefulSessions === true
127
- ? StatefulBashExecutionToolDescription
128
- : BashExecutionToolDescription;
142
+ let base = BashExecutionToolDescription;
143
+ if (options?.attachedWorkspace === true) {
144
+ base = AttachedWorkspaceBashExecutionToolDescription;
145
+ } else if (options?.statefulSessions === true) {
146
+ base = StatefulBashExecutionToolDescription;
147
+ }
129
148
  if (options?.enableToolOutputReferences === true) {
130
149
  return `${base}\n\n${BashToolOutputReferencesGuide}`;
131
150
  }
@@ -136,19 +155,33 @@ const STATELESS_BASH_PARAM_NOTE =
136
155
  'The environment is stateless; variables and state don\'t persist between executions.';
137
156
  const STATEFUL_BASH_PARAM_NOTE =
138
157
  'Files written to /mnt/data persist between calls on the same warm machine. Each call runs in a fresh sandbox: shell variables, cwd, /tmp, and background processes do NOT survive the call. Only /mnt/data is durable.';
158
+ const ATTACHED_BASH_PARAM_NOTE =
159
+ 'Commands start in the selected persistent project. Project file changes persist, but shell variables, background processes, and execution-private temporary files do not.';
160
+ const ATTACHED_BASH_ARTIFACT_PATH_GUIDANCE =
161
+ 'Injected files and generated artifacts use `${LIBRECHAT_CODE_DATA_DIR:-/mnt/data}` for this execution only. Write anything needed later into the selected project.';
162
+ const ATTACHED_BASH_TMP_REMINDER =
163
+ 'Note: /tmp files are same-call scratch only and were not persisted; write files needed later into the selected project.';
139
164
 
140
165
  export function buildBashExecutionToolSchema(opts?: {
141
166
  statefulSessions?: boolean;
167
+ attachedWorkspace?: boolean;
142
168
  }): typeof BashExecutionToolSchema {
143
- const note =
144
- opts?.statefulSessions === true
145
- ? STATEFUL_BASH_PARAM_NOTE
146
- : STATELESS_BASH_PARAM_NOTE;
147
- const commandDescription =
169
+ let note = STATELESS_BASH_PARAM_NOTE;
170
+ if (opts?.attachedWorkspace === true) {
171
+ note = ATTACHED_BASH_PARAM_NOTE;
172
+ } else if (opts?.statefulSessions === true) {
173
+ note = STATEFUL_BASH_PARAM_NOTE;
174
+ }
175
+ let commandDescription =
148
176
  BashExecutionToolSchema.properties.command.description.replace(
149
177
  STATELESS_BASH_PARAM_NOTE,
150
178
  note
151
179
  );
180
+ if (opts?.attachedWorkspace === true) {
181
+ commandDescription = commandDescription
182
+ .replace('- Prior /mnt/data files are available and can be modified in place.\n', '')
183
+ .replace(CODE_ARTIFACT_PATH_GUIDANCE, ATTACHED_BASH_ARTIFACT_PATH_GUIDANCE);
184
+ }
152
185
  return {
153
186
  ...BashExecutionToolSchema,
154
187
  properties: {
@@ -163,6 +196,15 @@ export function buildBashExecutionToolSchema(opts?: {
163
196
 
164
197
  export const BashExecutionToolName = Constants.BASH_TOOL;
165
198
 
199
+ function quoteBashArgument(value: string): string {
200
+ return `'${value.replace(/'/g, '\'"\'"\'')}'`;
201
+ }
202
+
203
+ function commandWithArguments(command: string, args: string[] | undefined): string {
204
+ if (args == null || args.length === 0) return command;
205
+ return `bash -c ${quoteBashArgument(command)} -- ${args.map(quoteBashArgument).join(' ')}`;
206
+ }
207
+
166
208
  /**
167
209
  * Default bash tool definition using the base description.
168
210
  *
@@ -182,9 +224,21 @@ export const BashExecutionToolDefinition = {
182
224
  function createBashExecutionTool(
183
225
  params: t.BashExecutionToolParams | null = {}
184
226
  ): DynamicStructuredTool {
227
+ const workspaceId = params?.workspaceId?.trim();
228
+ const hasWorkspace = workspaceId != null && workspaceId !== '';
229
+ const workspaceInstanceId = resolveAttachedWorkspaceInstanceId(
230
+ params?.workspaceInstanceId,
231
+ hasWorkspace
232
+ );
233
+ if (
234
+ hasWorkspace &&
235
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(workspaceId)
236
+ ) {
237
+ throw new Error('Invalid attached workspace identifier');
238
+ }
185
239
  const execEndpoint = buildCodeApiEndpoint(
186
240
  params?.baseUrl ?? getCodeBaseURL(),
187
- 'exec'
241
+ hasWorkspace ? 'exec/programmatic' : 'exec'
188
242
  );
189
243
 
190
244
  return tool(
@@ -197,26 +251,37 @@ function createBashExecutionTool(
197
251
  executionProfile,
198
252
  runtimeSessionHint,
199
253
  statefulSessions,
254
+ workspaceId: _workspaceId,
255
+ workspaceInstanceId: _workspaceInstanceId,
200
256
  ...executionParams
201
257
  } = params ?? {};
202
258
  void _baseUrl;
259
+ void _workspaceId;
260
+ void _workspaceInstanceId;
203
261
  /* Drop any model-supplied `runtime_session_hint` from the raw args: the
204
262
  * hint must only come from ToolNode's injected `_runtime_session_hint`
205
263
  * (below), never from the tool call itself. */
206
264
  /* `intent` is a UI display label — never part of the wire body. */
207
265
  const {
208
- command,
266
+ command: rawCommand,
209
267
  intent: _ignoredIntent,
210
268
  runtime_session_hint: _ignoredModelHint,
269
+ workspace_instance_id: _ignoredModelWorkspaceInstanceId,
270
+ args,
211
271
  ...rest
212
272
  } = rawInput as {
213
273
  command: string;
214
274
  intent?: unknown;
215
275
  runtime_session_hint?: unknown;
276
+ workspace_instance_id?: unknown;
216
277
  args?: string[];
217
278
  };
218
279
  void _ignoredModelHint;
280
+ void _ignoredModelWorkspaceInstanceId;
219
281
  void _ignoredIntent;
282
+ const command = hasWorkspace
283
+ ? commandWithArguments(rawCommand, args)
284
+ : rawCommand;
220
285
  const { session_id, _injected_files, _runtime_session_hint } =
221
286
  (config.toolCall ?? {}) as {
222
287
  session_id?: string;
@@ -226,9 +291,14 @@ function createBashExecutionTool(
226
291
 
227
292
  const postData: Record<string, unknown> = {
228
293
  lang: 'bash',
229
- code: command,
294
+ code: hasWorkspace ? prepareBashProgrammaticCode(command) : command,
295
+ ...(hasWorkspace ? { tools: [] } : {}),
296
+ ...(!hasWorkspace && args != null ? { args } : {}),
230
297
  ...rest,
231
298
  ...executionParams,
299
+ ...(workspaceInstanceId != null && workspaceInstanceId !== ''
300
+ ? { workspace_instance_id: workspaceInstanceId }
301
+ : {}),
232
302
  };
233
303
 
234
304
  const effectiveRuntimeSessionHint = selectRuntimeSessionHint(
@@ -265,44 +335,70 @@ function createBashExecutionTool(
265
335
  try {
266
336
  const resolvedAuthHeaders =
267
337
  await resolveCodeApiAuthHeaders(authHeaders);
268
- const fetchOptions: RequestInit = {
269
- method: 'POST',
270
- headers: {
271
- 'Content-Type': 'application/json',
272
- 'User-Agent': 'LibreChat/1.0',
273
- ...addCodeApiExecutionProfileHeader(
274
- resolvedAuthHeaders,
275
- executionProfile
276
- ),
277
- },
278
- body: JSON.stringify(postData),
279
- };
338
+ let result: Partial<t.ExecuteResult> & t.ProgrammaticExecutionResponse;
339
+ if (hasWorkspace) {
340
+ result = await makeRequest(
341
+ execEndpoint,
342
+ postData,
343
+ undefined,
344
+ {
345
+ ...resolvedAuthHeaders,
346
+ 'X-LibreChat-Code-Workspace-ID': workspaceId,
347
+ },
348
+ executionProfile,
349
+ config.signal
350
+ );
351
+ } else {
352
+ const fetchOptions: RequestInit = {
353
+ method: 'POST',
354
+ headers: {
355
+ 'Content-Type': 'application/json',
356
+ 'User-Agent': 'LibreChat/1.0',
357
+ ...addCodeApiExecutionProfileHeader(
358
+ resolvedAuthHeaders,
359
+ executionProfile
360
+ ),
361
+ },
362
+ body: JSON.stringify(postData),
363
+ signal: config.signal,
364
+ };
280
365
 
281
- const proxyAgent = resolveFetchProxyAgent(execEndpoint);
282
- if (proxyAgent != null) {
283
- fetchOptions.agent = proxyAgent;
366
+ const proxyAgent = resolveFetchProxyAgent(execEndpoint);
367
+ if (proxyAgent != null) {
368
+ fetchOptions.agent = proxyAgent;
369
+ }
370
+ const response = await fetch(execEndpoint, fetchOptions);
371
+ if (!response.ok) {
372
+ throw new CodeApiRequestError(
373
+ await buildCodeApiHttpErrorMessage('POST', execEndpoint, response, {
374
+ profile: executionProfile,
375
+ })
376
+ );
377
+ }
378
+ result = (await response.json()) as Partial<t.ExecuteResult> &
379
+ t.ProgrammaticExecutionResponse;
284
380
  }
285
- const response = await fetch(execEndpoint, fetchOptions);
286
- if (!response.ok) {
381
+ if (hasWorkspace && result.status !== 'completed') {
287
382
  throw new CodeApiRequestError(
288
- await buildCodeApiHttpErrorMessage('POST', execEndpoint, response, {
289
- profile: executionProfile,
290
- })
383
+ buildCodeApiExecutionErrorMessage(
384
+ result as t.ProgrammaticExecutionResponse
385
+ )
291
386
  );
292
387
  }
293
-
294
- const result: t.ExecuteResult = await response.json();
295
388
  let formattedOutput = '';
296
- if (result.stdout) {
389
+ if (typeof result.stdout === 'string' && result.stdout.length > 0) {
297
390
  formattedOutput += `stdout:\n${result.stdout}\n`;
298
391
  } else {
299
392
  formattedOutput += emptyOutputMessage;
300
393
  }
301
- if (result.stderr) formattedOutput += `stderr:\n${result.stderr}\n`;
394
+ if (typeof result.stderr === 'string' && result.stderr.length > 0) {
395
+ formattedOutput += `stderr:\n${result.stderr}\n`;
396
+ }
302
397
 
303
398
  const outputWithReminder = appendTmpScratchReminder(
304
399
  formattedOutput,
305
- command
400
+ command,
401
+ hasWorkspace ? ATTACHED_BASH_TMP_REMINDER : undefined
306
402
  );
307
403
  const artifactDelivery = normalizeArtifactDeliveryFailure(
308
404
  result.artifact_delivery
@@ -312,6 +408,10 @@ function createBashExecutionTool(
312
408
  artifactDelivery
313
409
  );
314
410
  const hasFiles = result.files != null && result.files.length > 0;
411
+ const deletionEcho =
412
+ result.deleted_files != null
413
+ ? { deleted_files: result.deleted_files }
414
+ : {};
315
415
  const runtimeEcho =
316
416
  result.runtime_session_id != null
317
417
  ? {
@@ -320,7 +420,15 @@ function createBashExecutionTool(
320
420
  }
321
421
  : {};
322
422
  return [
323
- appendCodeSessionFileSummary(outputWithDeliveryWarning, result.files),
423
+ hasWorkspace
424
+ ? appendExecutionArtifactFileSummary(
425
+ outputWithDeliveryWarning,
426
+ result.files
427
+ )
428
+ : appendCodeSessionFileSummary(
429
+ outputWithDeliveryWarning,
430
+ result.files
431
+ ),
324
432
  (hasFiles
325
433
  ? {
326
434
  session_id: result.session_id,
@@ -328,6 +436,7 @@ function createBashExecutionTool(
328
436
  ...(artifactDelivery != null
329
437
  ? { artifact_delivery: artifactDelivery }
330
438
  : {}),
439
+ ...deletionEcho,
331
440
  ...runtimeEcho,
332
441
  }
333
442
  : {
@@ -335,6 +444,7 @@ function createBashExecutionTool(
335
444
  ...(artifactDelivery != null
336
445
  ? { artifact_delivery: artifactDelivery }
337
446
  : {}),
447
+ ...deletionEcho,
338
448
  ...runtimeEcho,
339
449
  }) satisfies t.CodeExecutionArtifact,
340
450
  ];
@@ -343,15 +453,21 @@ function createBashExecutionTool(
343
453
  normalizeCodeApiRequestError(error).message,
344
454
  command
345
455
  );
346
- throw new CodeApiRequestError(`Execution error:\n\n${messageWithReminder}`);
456
+ throw new CodeApiRequestError(
457
+ `Execution error:\n\n${messageWithReminder}`
458
+ );
347
459
  }
348
460
  },
349
461
  {
350
462
  name: BashExecutionToolName,
351
463
  description: buildBashExecutionToolDescription({
352
464
  statefulSessions: params?.statefulSessions,
465
+ attachedWorkspace: hasWorkspace,
466
+ }),
467
+ schema: buildBashExecutionToolSchema({
468
+ statefulSessions: params?.statefulSessions,
469
+ attachedWorkspace: hasWorkspace,
353
470
  }),
354
- schema: buildBashExecutionToolSchema(params ?? undefined),
355
471
  responseFormat: Constants.CONTENT_AND_ARTIFACT,
356
472
  }
357
473
  );
@@ -11,6 +11,7 @@ import {
11
11
  buildCodeApiEndpoint,
12
12
  CodeApiRequestError,
13
13
  getCodeBaseURL,
14
+ resolveCodeApiAuthHeaders,
14
15
  selectRuntimeSessionHint,
15
16
  } from './CodeExecutor';
16
17
  import {
@@ -32,6 +33,7 @@ import {
32
33
  formatCompletedResponse,
33
34
  } from './ProgrammaticToolCalling';
34
35
  import { logCodeApiDiagnostic } from '@/tools/diagnostics';
36
+ import { resolveAttachedWorkspaceInstanceId } from '@/tools/workspaceIdentity';
35
37
  import { INTENT_PROPERTY } from '@/tools/intentArg';
36
38
  import { Constants } from '@/common';
37
39
 
@@ -44,6 +46,11 @@ config();
44
46
  const DEFAULT_MAX_ROUND_TRIPS = 20;
45
47
  const DEFAULT_RUN_TIMEOUT_MS = resolveCodeApiRunTimeoutMs();
46
48
  const BASH_LAST_BACKGROUND_PID_GUARD = ': &\nwait "$!"';
49
+ const CODE_API_WORKSPACE_HEADER = 'X-LibreChat-Code-Workspace-ID';
50
+ const ATTACHED_BASH_DATA_DIRECTORY = '"${LIBRECHAT_CODE_DATA_DIR:-/mnt/data}"';
51
+ const ATTACHED_BASH_ARTIFACT_PATH_GUIDANCE =
52
+ `Use ${ATTACHED_BASH_DATA_DIRECTORY} for injected files and generated artifacts. ` +
53
+ 'The directory is execution-scoped; the selected workspace is the persistent project root.';
47
54
 
48
55
  /** Bash reserved words that get `_tool` suffix when used as function names */
49
56
  const BASH_RESERVED = new Set([
@@ -81,6 +88,11 @@ Each call is a fresh bash shell. Variables and state do NOT persist between call
81
88
  You MUST complete your entire workflow in ONE code block.
82
89
  DO NOT split work across multiple calls expecting to reuse variables.`;
83
90
 
91
+ const ATTACHED_WORKSPACE_WARNING = `ATTACHED WORKSPACE EXECUTION:
92
+ - Commands start in the selected persistent workspace; project file changes persist between calls.
93
+ - Each sandbox run is a fresh process, so shell variables, background processes, and temporary execution data do not persist.
94
+ - Injected files and generated artifacts use \${LIBRECHAT_CODE_DATA_DIR:-/mnt/data}; do not copy them into the project unless the task requires it.`;
95
+
84
96
  const CORE_RULES = `Rules:
85
97
  - One call: state does not persist
86
98
  - Tools are pre-defined as bash functions—DO NOT redefine them
@@ -107,6 +119,30 @@ Example (Parallel calls):
107
119
  echo "SF: $(jq -r . /mnt/data/sf.json)"
108
120
  echo "NY: $(jq -r . /mnt/data/ny.json)"`;
109
121
 
122
+ const ATTACHED_CORE_RULES = `Rules:
123
+ - One call: process state does not persist; project files do
124
+ - Tools are pre-defined as bash functions—DO NOT redefine them
125
+ - Each tool function accepts a JSON string argument
126
+ - Resolve tool calls into variables before changing project files; do not redirect a tool call directly into the project
127
+ - Set data_dir=${ATTACHED_BASH_DATA_DIRECTORY}; save generated artifacts there, and write durable project files relative to the working directory
128
+ - Tool stdout is normalized to one compact JSON value when possible; parse saved stdout once, then use fromjson? // . only for JSON-string fields
129
+ - Only echo/printf output returns to the model
130
+ - ${ATTACHED_BASH_ARTIFACT_PATH_GUIDANCE}
131
+ - ${BASH_SHELL_GUIDANCE}
132
+ - timeout caps one sandbox run/replay iteration, not the total multi-round-trip workflow`;
133
+
134
+ const ATTACHED_EXAMPLES = `Example (Complete workflow in one call):
135
+ data=$(query_database '{"sql": "SELECT * FROM users"}')
136
+ echo "$data" | jq '.[] | .name'
137
+
138
+ Example (Parallel calls):
139
+ data_dir=${ATTACHED_BASH_DATA_DIRECTORY}
140
+ { sf=$(web_search '{"query": "SF weather"}'); printf '%s\n' "$sf" > "$data_dir/sf.json"; } &
141
+ { ny=$(web_search '{"query": "NY weather"}'); printf '%s\n' "$ny" > "$data_dir/ny.json"; } &
142
+ wait
143
+ echo "SF: $(jq -r . "$data_dir/sf.json")"
144
+ echo "NY: $(jq -r . "$data_dir/ny.json")"`;
145
+
110
146
  const CODE_PARAM_DESCRIPTION = `Bash code that calls tools programmatically. Tools are available as bash functions.
111
147
 
112
148
  ${STATELESS_WARNING}
@@ -175,7 +211,7 @@ export const BashProgrammaticToolCallingDefinition = {
175
211
  schema: BashProgrammaticToolCallingSchema,
176
212
  } as const;
177
213
 
178
- function prepareBashProgrammaticCode(code: string): string {
214
+ export function prepareBashProgrammaticCode(code: string): string {
179
215
  /* The Code API's generated Bash wrapper reads `$!` after user code. A user
180
216
  * `set -u` makes that expansion fail when no background process has run.
181
217
  * Seed and reap a no-op job before user code so strict mode remains active
@@ -326,7 +362,42 @@ export function createBashProgrammaticToolCallingTool(
326
362
  const maxRunTimeoutMs = resolveCodeApiRunTimeoutMs(initParams.runTimeoutMs);
327
363
  const proxy = initParams.proxy ?? process.env.PROXY;
328
364
  const debug = initParams.debug ?? process.env.BASH_PTC_DEBUG === 'true';
365
+ const workspaceId = initParams.workspaceId?.trim();
366
+ const hasWorkspace = workspaceId != null && workspaceId !== '';
367
+ if (
368
+ hasWorkspace &&
369
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(workspaceId)
370
+ ) {
371
+ throw new Error('Invalid attached workspace identifier');
372
+ }
373
+ const workspaceInstanceId = resolveAttachedWorkspaceInstanceId(
374
+ initParams.workspaceInstanceId,
375
+ hasWorkspace
376
+ );
377
+ const requestAuthHeaders: t.CodeApiAuthHeaders = hasWorkspace
378
+ ? async (): Promise<t.CodeApiAuthHeaderMap> => ({
379
+ ...(await resolveCodeApiAuthHeaders(initParams.authHeaders)),
380
+ [CODE_API_WORKSPACE_HEADER]: workspaceId,
381
+ })
382
+ : (initParams.authHeaders ?? {});
329
383
  const EXEC_ENDPOINT = buildCodeApiEndpoint(baseUrl, 'exec/programmatic');
384
+ const description = hasWorkspace
385
+ ? BashProgrammaticToolCallingDescription.replace(
386
+ STATELESS_WARNING,
387
+ ATTACHED_WORKSPACE_WARNING
388
+ )
389
+ .replace(CORE_RULES, ATTACHED_CORE_RULES)
390
+ .replace(EXAMPLES, ATTACHED_EXAMPLES)
391
+ : BashProgrammaticToolCallingDescription;
392
+ const schema = createBashProgrammaticToolCallingSchema(maxRunTimeoutMs);
393
+ if (hasWorkspace) {
394
+ schema.properties.code.description = CODE_PARAM_DESCRIPTION.replace(
395
+ STATELESS_WARNING,
396
+ ATTACHED_WORKSPACE_WARNING
397
+ )
398
+ .replace(CORE_RULES, ATTACHED_CORE_RULES)
399
+ .replace(EXAMPLES, ATTACHED_EXAMPLES);
400
+ }
330
401
 
331
402
  return tool(
332
403
  async (rawParams, config) => {
@@ -449,7 +520,7 @@ export function createBashProgrammaticToolCallingTool(
449
520
 
450
521
  /* Raw `code`, not `preparedCode`: the `$!` guard exists for the
451
522
  * programmatic replay wrapper, which plain `/exec` never applies. */
452
- if (needsNoTools) {
523
+ if (needsNoTools && !hasWorkspace) {
453
524
  return await runPlainExecution({
454
525
  baseUrl,
455
526
  lang: 'bash',
@@ -459,7 +530,7 @@ export function createBashProgrammaticToolCallingTool(
459
530
  files,
460
531
  runtimeSessionHint,
461
532
  proxy,
462
- authHeaders: initParams.authHeaders,
533
+ authHeaders: requestAuthHeaders,
463
534
  executionProfile: initParams.executionProfile,
464
535
  });
465
536
  }
@@ -472,14 +543,18 @@ export function createBashProgrammaticToolCallingTool(
472
543
  tools: effectiveTools,
473
544
  session_id,
474
545
  timeout,
546
+ ...(workspaceInstanceId != null && workspaceInstanceId !== ''
547
+ ? { workspace_instance_id: workspaceInstanceId }
548
+ : {}),
475
549
  ...(files && files.length > 0 ? { files } : {}),
476
550
  ...(runtimeSessionHint != null
477
551
  ? { runtime_session_hint: runtimeSessionHint }
478
552
  : {}),
479
553
  },
480
554
  proxy,
481
- initParams.authHeaders,
482
- initParams.executionProfile
555
+ requestAuthHeaders,
556
+ initParams.executionProfile,
557
+ config.signal
483
558
  );
484
559
 
485
560
  // ====================================================================
@@ -519,8 +594,9 @@ export function createBashProgrammaticToolCallingTool(
519
594
  tool_results: toolResults,
520
595
  },
521
596
  proxy,
522
- initParams.authHeaders,
523
- initParams.executionProfile
597
+ requestAuthHeaders,
598
+ initParams.executionProfile,
599
+ config.signal
524
600
  );
525
601
  }
526
602
 
@@ -529,7 +605,11 @@ export function createBashProgrammaticToolCallingTool(
529
605
  // ====================================================================
530
606
 
531
607
  if (response.status === 'completed') {
532
- return formatCompletedResponse(response, code);
608
+ return formatCompletedResponse(
609
+ response,
610
+ code,
611
+ hasWorkspace ? 'execution' : 'session'
612
+ );
533
613
  }
534
614
 
535
615
  if (response.status === 'error') {
@@ -551,8 +631,8 @@ export function createBashProgrammaticToolCallingTool(
551
631
  },
552
632
  {
553
633
  name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
554
- description: BashProgrammaticToolCallingDescription,
555
- schema: createBashProgrammaticToolCallingSchema(maxRunTimeoutMs),
634
+ description,
635
+ schema,
556
636
  responseFormat: Constants.CONTENT_AND_ARTIFACT,
557
637
  }
558
638
  );
@@ -58,11 +58,15 @@ export const TMP_SCRATCH_OUTPUT_REMINDER =
58
58
  export const FAILED_EXECUTION_FILE_REMINDER =
59
59
  'Note: any files written during this failed call were not registered for later calls; fix the error and rerun before relying on them.';
60
60
 
61
- export function appendTmpScratchReminder(output: string, code: string): string {
61
+ export function appendTmpScratchReminder(
62
+ output: string,
63
+ code: string,
64
+ reminder = TMP_SCRATCH_OUTPUT_REMINDER
65
+ ): string {
62
66
  if (!TMP_PATH_PATTERN.test(code)) {
63
67
  return output;
64
68
  }
65
- return `${output.trimEnd()}\n${TMP_SCRATCH_OUTPUT_REMINDER}\n`;
69
+ return `${output.trimEnd()}\n${reminder}\n`;
66
70
  }
67
71
 
68
72
  export function appendFailedExecutionFileReminder(
@@ -668,6 +672,10 @@ function createCodeExecutionTool(
668
672
  artifactDelivery
669
673
  );
670
674
  const hasFiles = result.files != null && result.files.length > 0;
675
+ const deletionEcho =
676
+ result.deleted_files != null
677
+ ? { deleted_files: result.deleted_files }
678
+ : {};
671
679
  /* Echo the durable runtime session (stateful backends only) so hosts
672
680
  * can surface a "session active / was reset" signal later. Additive:
673
681
  * absent on stateless servers. */
@@ -687,6 +695,7 @@ function createCodeExecutionTool(
687
695
  ...(artifactDelivery != null
688
696
  ? { artifact_delivery: artifactDelivery }
689
697
  : {}),
698
+ ...deletionEcho,
690
699
  ...runtimeEcho,
691
700
  }
692
701
  : {
@@ -694,6 +703,7 @@ function createCodeExecutionTool(
694
703
  ...(artifactDelivery != null
695
704
  ? { artifact_delivery: artifactDelivery }
696
705
  : {}),
706
+ ...deletionEcho,
697
707
  ...runtimeEcho,
698
708
  }) satisfies t.CodeExecutionArtifact,
699
709
  ];
@@ -15,6 +15,8 @@ const IMAGE_FILE_EXTENSIONS = new Set([
15
15
 
16
16
  const CODE_SESSION_FILE_SUMMARY_PATTERN =
17
17
  /^Generated files:\nSession files: \d+ persisted file\(s\) are available in \/mnt\/data, including \d+ image\(s\)\. Use known \/mnt\/data paths directly in later code-tool calls\. The app displays files\/images automatically; do not invent download links or wrap generated images in Markdown\.$/;
18
+ const EXECUTION_ARTIFACT_FILE_SUMMARY_PATTERN =
19
+ /^Generated files:\nExecution artifacts: \d+ file\(s\), including \d+ image\(s\), were delivered to the app\. They are not retained in the attached project; write durable files to the project root when later commands must reuse them\. The app displays files\/images automatically; do not invent download links or wrap generated images in Markdown\.$/;
18
20
 
19
21
  function getFileExtension(name: string): string {
20
22
  const lastSlash = name.lastIndexOf('/');
@@ -53,7 +55,12 @@ export function stripCodeSessionFileSummary(output: string): string {
53
55
  const beforeSummary = output.slice(0, summaryStart);
54
56
  if (beforeSummary !== '' && !beforeSummary.endsWith('\n\n')) return output;
55
57
  const maybeSummary = output.slice(summaryStart);
56
- if (!CODE_SESSION_FILE_SUMMARY_PATTERN.test(maybeSummary)) return output;
58
+ if (
59
+ !CODE_SESSION_FILE_SUMMARY_PATTERN.test(maybeSummary) &&
60
+ !EXECUTION_ARTIFACT_FILE_SUMMARY_PATTERN.test(maybeSummary)
61
+ ) {
62
+ return output;
63
+ }
57
64
  return beforeSummary.trimEnd();
58
65
  }
59
66
 
@@ -78,3 +85,20 @@ export function appendCodeSessionFileSummary(
78
85
 
79
86
  return `${output.trimEnd()}\n\n${summary}`.trim();
80
87
  }
88
+
89
+ /** Attached-worker artifacts are delivered out of execution-private scratch,
90
+ * not retained in the selected project or a reusable `/mnt/data` session. */
91
+ export function appendExecutionArtifactFileSummary(
92
+ output: string,
93
+ files: t.FileRefs | undefined
94
+ ): string {
95
+ const generatedFiles = files?.filter(isGeneratedFile) ?? [];
96
+ if (generatedFiles.length === 0) return output.trim();
97
+ const imageCount = generatedFiles.filter(isImageFile).length;
98
+ const summary =
99
+ 'Generated files:\n' +
100
+ `Execution artifacts: ${generatedFiles.length} file(s), including ${imageCount} image(s), were delivered to the app. ` +
101
+ 'They are not retained in the attached project; write durable files to the project root when later commands must reuse them. ' +
102
+ 'The app displays files/images automatically; do not invent download links or wrap generated images in Markdown.';
103
+ return `${output.trimEnd()}\n\n${summary}`.trim();
104
+ }