@sema-agent/core 3.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +2382 -0
  2. package/dist/agents/cascade.d.ts +3 -1
  3. package/dist/agents/cascade.js +31 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +48 -24
  10. package/dist/agents/retain-ledger.d.ts +2 -0
  11. package/dist/agents/retain-ledger.js +6 -5
  12. package/dist/agents/roster-store.d.ts +1 -0
  13. package/dist/agents/roster-store.js +3 -5
  14. package/dist/agents/send-message-tool.d.ts +1 -0
  15. package/dist/agents/send-message-tool.js +2 -0
  16. package/dist/agents/subagent.d.ts +3 -1
  17. package/dist/agents/subagent.js +79 -94
  18. package/dist/agents/teacher.d.ts +1 -9
  19. package/dist/agents/teacher.js +21 -24
  20. package/dist/agents/tool-filter.js +6 -7
  21. package/dist/agents/verify.d.ts +1 -0
  22. package/dist/agents/verify.js +15 -14
  23. package/dist/brain/anthropic.js +24 -21
  24. package/dist/brain/openai.js +24 -21
  25. package/dist/brain/stream-engine.js +5 -2
  26. package/dist/brain/stream-shared.d.ts +1 -0
  27. package/dist/brain/stream-shared.js +14 -0
  28. package/dist/brain/terminal-cause.d.ts +4 -0
  29. package/dist/brain/terminal-cause.js +12 -0
  30. package/dist/brain/tool-call-id.d.ts +1 -0
  31. package/dist/brain/tool-call-id.js +3 -0
  32. package/dist/brain/tool-call-repair.js +2 -1
  33. package/dist/core/arg-summary.d.ts +21 -1
  34. package/dist/core/arg-summary.js +61 -14
  35. package/dist/core/auto-compaction.d.ts +1 -0
  36. package/dist/core/auto-compaction.js +16 -25
  37. package/dist/core/background-agent-store.d.ts +22 -22
  38. package/dist/core/background-agent-store.js +19 -23
  39. package/dist/core/checkpoint-store.d.ts +6 -13
  40. package/dist/core/checkpoint-store.js +11 -5
  41. package/dist/core/file-snapshot-store.d.ts +8 -0
  42. package/dist/core/file-snapshot-store.js +4 -3
  43. package/dist/core/fs-write-gate-policy.js +2 -3
  44. package/dist/core/hooks.d.ts +4 -1
  45. package/dist/core/hooks.js +8 -18
  46. package/dist/core/lsp.js +4 -3
  47. package/dist/core/mailbox-store.d.ts +7 -10
  48. package/dist/core/mcp.d.ts +7 -10
  49. package/dist/core/mcp.js +7 -9
  50. package/dist/core/memory-engine/engine.d.ts +0 -1
  51. package/dist/core/memory-engine/engine.js +24 -34
  52. package/dist/core/memory-engine/file-backend.d.ts +1 -2
  53. package/dist/core/memory-engine/file-backend.js +25 -36
  54. package/dist/core/memory-engine/layout.d.ts +8 -0
  55. package/dist/core/memory-engine/layout.js +60 -4
  56. package/dist/core/memory-engine/scope-contract.js +9 -3
  57. package/dist/core/memory-engine/types.d.ts +1 -1
  58. package/dist/core/permission-rules.js +2 -3
  59. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  60. package/dist/core/runner/active-skill-scope.js +2 -17
  61. package/dist/core/runner/assemble-result.d.ts +2 -7
  62. package/dist/core/runner/assemble-result.js +3 -4
  63. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  64. package/dist/core/runner/compaction-call-options.js +30 -0
  65. package/dist/core/runner/prepare-task.d.ts +6 -23
  66. package/dist/core/runner/prepare-task.js +36 -61
  67. package/dist/core/runner/runtask.d.ts +1 -0
  68. package/dist/core/runner/runtask.js +84 -172
  69. package/dist/core/runner/session-rule-policy.js +7 -5
  70. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  71. package/dist/core/runner/teardown-bounded.js +36 -0
  72. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  73. package/dist/core/runner/turn-attachments.d.ts +5 -15
  74. package/dist/core/runner/turn-attachments.js +1 -1
  75. package/dist/core/safe-notify.d.ts +17 -0
  76. package/dist/core/safe-notify.js +59 -0
  77. package/dist/core/sensitive-path-policy.js +4 -4
  78. package/dist/core/session-reconcile.js +1 -2
  79. package/dist/core/skill-tool-specifier.js +2 -3
  80. package/dist/core/skills-directory.js +2 -3
  81. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  82. package/dist/core/stub-env.d.ts +2 -6
  83. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  84. package/dist/core/surrogate-safe-slice.js +18 -0
  85. package/dist/core/task-registry-monitor.js +8 -13
  86. package/dist/core/task-registry-shared.d.ts +23 -1
  87. package/dist/core/task-registry-shared.js +16 -0
  88. package/dist/core/task-registry.d.ts +6 -1
  89. package/dist/core/task-registry.js +52 -87
  90. package/dist/core/task-tool-shape.d.ts +0 -2
  91. package/dist/core/task-tool-shape.js +2 -5
  92. package/dist/core/tool-errors.js +5 -4
  93. package/dist/core/tool-name-aliases.d.ts +1 -2
  94. package/dist/core/tool-name-aliases.js +42 -59
  95. package/dist/core/tool-policy.js +21 -17
  96. package/dist/core/tool-result-store.d.ts +1 -0
  97. package/dist/core/tool-result-store.js +17 -1
  98. package/dist/core/trace.d.ts +9 -9
  99. package/dist/core/types.d.ts +15 -12
  100. package/dist/core/untrusted-egress.d.ts +4 -2
  101. package/dist/core/untrusted-egress.js +28 -14
  102. package/dist/core/workflow-journal-store.d.ts +9 -20
  103. package/dist/core/workflow-run-store.d.ts +11 -10
  104. package/dist/core/workflow-run-store.js +22 -16
  105. package/dist/engine/compaction/utils.js +4 -2
  106. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  107. package/dist/engine/execution-env/node-execution-env.js +1 -1
  108. package/dist/engine/harness/agent-harness.d.ts +1 -0
  109. package/dist/engine/harness/agent-harness.js +13 -33
  110. package/dist/engine/harness/types.d.ts +29 -67
  111. package/dist/engine/llm/types.d.ts +1 -1
  112. package/dist/engine/loop/agent-loop.js +12 -14
  113. package/dist/engine/loop/types.d.ts +11 -9
  114. package/dist/engine/session/memory-repo.d.ts +2 -6
  115. package/dist/engine/session/session.d.ts +5 -30
  116. package/dist/index.d.ts +6 -3
  117. package/dist/index.js +4 -3
  118. package/dist/internal/harness-types.d.ts +3 -1
  119. package/dist/orchestration/run-spec.js +6 -4
  120. package/dist/orchestration/run-workflow-tool.js +3 -36
  121. package/dist/orchestration/workflow-governance.js +2 -1
  122. package/dist/orchestration/workflow.d.ts +4 -11
  123. package/dist/orchestration/workflow.js +164 -204
  124. package/dist/prompt-assembly/packs/sema-default.js +1 -3
  125. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  126. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  127. package/dist/prompts/default.js +1 -3
  128. package/dist/prompts/simple-sections.d.ts +0 -1
  129. package/dist/prompts/simple-sections.js +0 -1
  130. package/dist/stores/file/background-agent-store.d.ts +3 -12
  131. package/dist/stores/file/background-agent-store.js +3 -41
  132. package/dist/stores/file/checkpoint-store.js +6 -6
  133. package/dist/stores/file/mailbox-store.d.ts +2 -6
  134. package/dist/stores/file/session-policy-store.js +15 -1
  135. package/dist/stores/file/session-store.d.ts +2 -6
  136. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  137. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  138. package/dist/stores/file/workflow-run-store.js +4 -17
  139. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  140. package/dist/tools/fs/bash-readonly-classifier.js +60 -25
  141. package/dist/tools/fs/fs-bash.js +330 -104
  142. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  143. package/dist/tools/fs/fs-shared.js +2 -2
  144. package/dist/tools/fs/fs-write.js +0 -1
  145. package/dist/tools/fs/notebook.d.ts +2 -5
  146. package/dist/tools/fs/safety.d.ts +3 -0
  147. package/dist/tools/fs/safety.js +43 -14
  148. package/dist/tools/fs/search.js +5 -5
  149. package/dist/tools/monitor.d.ts +1 -0
  150. package/dist/tools/monitor.js +7 -5
  151. package/dist/tools/worktree.d.ts +2 -7
  152. package/dist/tools/worktree.js +3 -1
  153. package/package.json +6 -2
@@ -1,7 +1,8 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../../core/tools.js";
3
3
  import { defaultTaskRegistry } from "../../core/task-registry.js";
4
- import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_ALIASES, TASK_STOP_ALIASES, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
4
+ import { accountDroppedBytes, spoolDropNote } from "../../core/task-registry-shared.js";
5
+ import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
5
6
  import { hasBackgroundShell } from "../../core/background-shell.js";
6
7
  import { delimitUntrusted } from "../../core/untrusted-text.js";
7
8
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
@@ -157,7 +158,52 @@ function unquotePayload(payload) {
157
158
  return quoted ? (quoted[2] ?? "") : s;
158
159
  }
159
160
  const AUTO_BACKGROUND_MAX_WRAPPER_DEPTH = 3;
161
+ function splitTopLevelStatements(command) {
162
+ const out = [];
163
+ let cur = "";
164
+ let quote;
165
+ for (let i = 0; i < command.length; i++) {
166
+ const c = command[i];
167
+ if (quote !== undefined) {
168
+ cur += c;
169
+ if (c === quote)
170
+ quote = undefined;
171
+ continue;
172
+ }
173
+ if (c === '"' || c === "'") {
174
+ quote = c;
175
+ cur += c;
176
+ continue;
177
+ }
178
+ if (c === ";" || c === "\n" || c === "|" || (c === "&" && command[i + 1] === "&")) {
179
+ if ((c === "&" || c === "|") && command[i + 1] === c)
180
+ i++;
181
+ out.push(cur);
182
+ cur = "";
183
+ continue;
184
+ }
185
+ if (c === "\r" && command[i + 1] === "\n") {
186
+ out.push(cur);
187
+ cur = "";
188
+ i++;
189
+ continue;
190
+ }
191
+ cur += c;
192
+ }
193
+ out.push(cur);
194
+ return out.map((s) => s.trim()).filter((s) => s.length > 0);
195
+ }
160
196
  export function canAutoBackground(command, depth = 0) {
197
+ if (depth < AUTO_BACKGROUND_MAX_WRAPPER_DEPTH) {
198
+ for (const stmt of splitTopLevelStatements(command)) {
199
+ const tokens = stripAutoBackgroundWrapperPrefixes(stmt).split(/\s+/);
200
+ for (let i = 0; i < tokens.length; i++) {
201
+ const stmtPayload = commandStringPayload(tokens.slice(i));
202
+ if (stmtPayload !== undefined && stmtPayload.trim() !== "" && !canAutoBackground(unquotePayload(stmtPayload), depth + 1))
203
+ return false;
204
+ }
205
+ }
206
+ }
161
207
  const segments = command
162
208
  .split(/&&|\|\||;|\||\r?\n/)
163
209
  .map((s) => s.trim())
@@ -412,6 +458,53 @@ ${coAuthorLines}
412
458
  - The shell is NOT sandboxed by this tool: a command can read or write any reachable path and use the network. Every call is subject to the deployment's approval policy (it may be denied or require confirmation); a denied call should not be retried verbatim.
413
459
  - Treat evidence of a deployment restriction — "Operation not permitted", "Read-only file system", a denied path, or a network failure — as a boundary, not a bug: adjust within the allowed scope or report the limit; don't reach for a destructive or privilege-escalating workaround.`;
414
460
  }
461
+ async function adoptBackgroundShell(req) {
462
+ const { env, shellId, registry, description, toolUseId, ctxTaskId, ctxPrincipal, wiring } = req;
463
+ const sessionScoped = wiring.sessionId !== undefined;
464
+ const owner = sessionScoped ? wiring.sessionId : (ctxTaskId ?? wiring.taskOwner);
465
+ const scope = ctxPrincipal ?? wiring.taskScope;
466
+ const onNotify = wiring.onNotify;
467
+ let outputFile;
468
+ if (onNotify !== undefined) {
469
+ const tf = await env.createTempFile({ prefix: "bash-task-", suffix: ".output" });
470
+ if (tf.ok) {
471
+ const tfCanon = await env.canonicalPath(tf.value);
472
+ outputFile = tfCanon.ok ? tfCanon.value : tf.value;
473
+ }
474
+ }
475
+ try {
476
+ const taskId = registry.registerBackgroundBash({
477
+ env,
478
+ shellId,
479
+ description,
480
+ toolUseId,
481
+ ...(owner !== undefined ? { owner } : {}),
482
+ ...(scope !== undefined ? { scope } : {}),
483
+ ...(sessionScoped ? { sessionScoped: true } : {}),
484
+ ...(outputFile !== undefined ? { outputFile } : {}),
485
+ ...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
486
+ });
487
+ return { ok: true, taskId, outputFile };
488
+ }
489
+ catch (e) {
490
+ const reason = e instanceof Error ? e.message : String(e);
491
+ let kill;
492
+ try {
493
+ kill = await env.killBackground(shellId);
494
+ }
495
+ catch {
496
+ kill = undefined;
497
+ }
498
+ if (kill !== undefined && (kill.ok || kill.error.code === "not_found"))
499
+ return { ok: false, reason, killed: true };
500
+ return { ok: false, reason, killed: false, killMsg: kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw" };
501
+ }
502
+ }
503
+ function backgroundLifetimeNote(caps) {
504
+ return caps.retainBackgroundProcesses === true
505
+ ? ""
506
+ : ` NOTE: background processes do NOT survive the session (reaped at task end). If this is a deliverable service that must stay alive afterwards, host it with a FOREGROUND command instead: \`nohup cmd >log 2>&1 &\` (self-detaching — survives the session).`;
507
+ }
415
508
  export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = { current: rootCanonical }, taskOpts = {}) {
416
509
  const timeoutCaps = resolveBashTimeoutCaps(taskOpts);
417
510
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
@@ -479,57 +572,33 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
479
572
  ? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) — auto-terminates if still running after ${appliedTimeoutSec}s.`
480
573
  : ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
481
574
  : "";
482
- const lifetimeNote = bgCaps.retainBackgroundProcesses === true
483
- ? ""
484
- : ` NOTE: background processes do NOT survive the session (reaped at task end). If this is a deliverable service that must stay alive afterwards, host it with a FOREGROUND command instead: \`nohup cmd >log 2>&1 &\` (self-detaching — survives the session).`;
485
- if (taskOpts.taskRegistry) {
486
- const sessionScoped = taskOpts.sessionId !== undefined;
487
- const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
488
- const scope = ctx.principal ?? taskOpts.taskScope;
575
+ const lifetimeNote = backgroundLifetimeNote(bgCaps);
576
+ const registry = taskOpts.taskRegistry;
577
+ if (registry !== undefined) {
489
578
  const onNotify = taskOpts.onTaskNotification;
490
- let outputFile;
491
- if (onNotify !== undefined) {
492
- const tf = await env.createTempFile({ prefix: "bash-task-", suffix: ".output" });
493
- if (tf.ok) {
494
- const tfCanon = await env.canonicalPath(tf.value);
495
- outputFile = tfCanon.ok ? tfCanon.value : tf.value;
496
- }
497
- }
498
- let taskId;
499
- try {
500
- taskId = taskOpts.taskRegistry.registerBackgroundBash({
501
- env,
502
- shellId: r.value.shellId,
503
- description: description ?? command,
504
- toolUseId: ctx.toolCallId,
505
- ...(owner !== undefined ? { owner } : {}),
506
- ...(scope !== undefined ? { scope } : {}),
507
- ...(sessionScoped ? { sessionScoped: true } : {}),
508
- ...(outputFile !== undefined ? { outputFile } : {}),
509
- ...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
510
- });
511
- }
512
- catch (e) {
513
- const reason = e instanceof Error ? e.message : String(e);
514
- let kill;
515
- try {
516
- kill = await env.killBackground(r.value.shellId);
517
- }
518
- catch {
519
- kill = undefined;
579
+ const adopted = await adoptBackgroundShell({
580
+ env,
581
+ shellId: r.value.shellId,
582
+ registry,
583
+ description: description ?? command,
584
+ toolUseId: ctx.toolCallId,
585
+ ctxTaskId: ctx.taskId,
586
+ ctxPrincipal: ctx.principal,
587
+ wiring: { sessionId: taskOpts.sessionId, taskOwner: taskOpts.taskOwner, taskScope: taskOpts.taskScope, onNotify },
588
+ });
589
+ if (!adopted.ok) {
590
+ if (adopted.killed) {
591
+ return errorResult(`Error (Bash): background process was started but could not be registered (${adopted.reason}); it has been terminated.`);
520
592
  }
521
- if (kill !== undefined && (kill.ok || kill.error.code === "not_found")) {
522
- return errorResult(`Error (Bash): background process was started but could not be registered (${reason}); it has been terminated.`);
523
- }
524
- const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
525
593
  return {
526
- content: `Error (Bash): background process was started but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
594
+ content: `Error (Bash): background process was started but could not be registered (${adopted.reason}), and the cleanup kill failed (${adopted.killMsg}). ` +
527
595
  `The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
528
596
  `If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).`,
529
597
  details: { type: "bash", registerFailed: true, killFailed: true },
530
598
  isError: true,
531
599
  };
532
600
  }
601
+ const { taskId, outputFile } = adopted;
533
602
  const interimNote = outputFile !== undefined
534
603
  ? ` Output file: ${outputFile} (full output is appended there — Read it any time).`
535
604
  : ` Use TaskOutput("${taskId}") to check interim output.`;
@@ -549,68 +618,46 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
549
618
  budgetNote +
550
619
  lifetimeNote;
551
620
  }
552
- return (`Started in background; task_id=${r.value.shellId}. Legacy bash_id=${r.value.shellId}. ` +
621
+ return (`Started in background; task_id=${r.value.shellId}. ` +
553
622
  `Use TaskOutput("${r.value.shellId}") to read output (poll until status is no longer "running"); ` +
554
623
  `TaskStop("${r.value.shellId}") to stop. No '&' needed.` +
555
624
  budgetNote +
556
625
  lifetimeNote);
557
626
  }
558
- const detachChain = taskOpts.detachHub && taskOpts.taskRegistry && hasBackgroundShell(env) && env.backgroundCapabilities.supportsDetach === true
627
+ const detachHub = taskOpts.detachHub;
628
+ const detachRegistry = taskOpts.taskRegistry;
629
+ const detachEnv = hasBackgroundShell(env) ? env : undefined;
630
+ const detachChain = detachHub !== undefined && detachRegistry !== undefined && detachEnv !== undefined && detachEnv.backgroundCapabilities.supportsDetach === true
559
631
  ? {
560
- signal: taskOpts.detachHub.signalFor(ctx.toolCallId),
632
+ signal: detachHub.signalFor(ctx.toolCallId),
561
633
  onDetached: async (shellId, stdoutSoFar, stderrSoFar, cause) => {
562
- const sessionScoped = taskOpts.sessionId !== undefined;
563
- const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
564
- const scope = ctx.principal ?? taskOpts.taskScope;
565
634
  const onNotify = taskOpts.onTaskNotification;
566
- let outputFile;
567
- if (onNotify !== undefined) {
568
- const tf = await env.createTempFile({ prefix: "bash-task-", suffix: ".output" });
569
- if (tf.ok) {
570
- const tfCanon = await env.canonicalPath(tf.value);
571
- outputFile = tfCanon.ok ? tfCanon.value : tf.value;
572
- }
573
- }
574
- let taskId;
575
- try {
576
- taskId = taskOpts.taskRegistry.registerBackgroundBash({
577
- env: env,
578
- shellId: shellId,
579
- description: description ?? command,
580
- toolUseId: ctx.toolCallId,
581
- ...(owner !== undefined ? { owner } : {}),
582
- ...(scope !== undefined ? { scope } : {}),
583
- ...(sessionScoped ? { sessionScoped: true } : {}),
584
- ...(outputFile !== undefined ? { outputFile } : {}),
585
- ...(onNotify !== undefined ? { onTerminal: (n) => onNotify(n, { priority: "next" }) } : {}),
586
- });
587
- }
588
- catch (e) {
589
- const reason = e instanceof Error ? e.message : String(e);
590
- let kill;
591
- try {
592
- kill = await env.killBackground(shellId);
593
- }
594
- catch {
595
- kill = undefined;
596
- }
597
- const killed = kill !== undefined && (kill.ok || kill.error.code === "not_found");
635
+ const adopted = await adoptBackgroundShell({
636
+ env: detachEnv,
637
+ shellId: shellId,
638
+ registry: detachRegistry,
639
+ description: description ?? command,
640
+ toolUseId: ctx.toolCallId,
641
+ ctxTaskId: ctx.taskId,
642
+ ctxPrincipal: ctx.principal,
643
+ wiring: { sessionId: taskOpts.sessionId, taskOwner: taskOpts.taskOwner, taskScope: taskOpts.taskScope, onNotify },
644
+ });
645
+ if (!adopted.ok) {
598
646
  const outTail = stdoutSoFar.slice(-1_000);
599
647
  const errTail = stderrSoFar.slice(-1_000);
600
648
  const tails = (outTail ? `\n--- output so far (tail) ---\n${outTail}` : "") +
601
649
  (errTail ? `\n--- stderr so far (tail) ---\n${errTail}` : "");
602
- if (killed) {
650
+ if (adopted.killed) {
603
651
  return {
604
- content: `Error (Bash): command was detached to the background but could not be registered (${reason}); ` +
652
+ content: `Error (Bash): command was detached to the background but could not be registered (${adopted.reason}); ` +
605
653
  `it has been terminated (an unregistered background process would have no task_id and could not be read or stopped).` +
606
654
  tails,
607
655
  details: { type: "bash", detached: true, registerFailed: true },
608
656
  isError: true,
609
657
  };
610
658
  }
611
- const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
612
659
  return {
613
- content: `Error (Bash): command was detached to the background but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
660
+ content: `Error (Bash): command was detached to the background but could not be registered (${adopted.reason}), and the cleanup kill failed (${adopted.killMsg}). ` +
614
661
  `The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
615
662
  `If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).` +
616
663
  tails,
@@ -618,8 +665,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
618
665
  isError: true,
619
666
  };
620
667
  }
668
+ const { taskId, outputFile } = adopted;
621
669
  const tail = stdoutSoFar.slice(-1_000);
622
- const adoptCaps = env.backgroundCapabilities;
670
+ const adoptCaps = detachEnv.backgroundCapabilities;
623
671
  const adoptBudgetNote = typeof adoptCaps.defaultBgTimeoutSec === "number" && typeof adoptCaps.maxBgTimeoutSec === "number"
624
672
  ? ` Time budget: auto-terminates if still running after ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
625
673
  : "";
@@ -638,6 +686,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
638
686
  ? `You will be notified when it completes — do not poll.`
639
687
  : `Poll TaskOutput until its status is no longer "running".`) +
640
688
  adoptBudgetNote +
689
+ backgroundLifetimeNote(adoptCaps) +
641
690
  (tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
642
691
  details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" ? { autoBackgrounded: true } : {}) },
643
692
  };
@@ -664,6 +713,92 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
664
713
  },
665
714
  });
666
715
  }
716
+ const GLOB_METACHAR_RE = /[*?[]/;
717
+ const MAX_GLOB_DIRS = 256;
718
+ const MAX_GLOB_MATCHES = 8192;
719
+ function compileGlobSegment(pattern) {
720
+ if (pattern.includes("\\"))
721
+ return undefined;
722
+ let src = "";
723
+ for (let i = 0; i < pattern.length; i++) {
724
+ const ch = pattern[i];
725
+ if (ch === "*") {
726
+ src += "[^/]*";
727
+ }
728
+ else if (ch === "?") {
729
+ src += "[^/]";
730
+ }
731
+ else if (ch === "[") {
732
+ const cls = parseBracketExpression(pattern, i);
733
+ if (cls === undefined)
734
+ src += "\\[";
735
+ else {
736
+ src += cls.source;
737
+ i = cls.end;
738
+ }
739
+ }
740
+ else {
741
+ src += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
742
+ }
743
+ }
744
+ try {
745
+ return new RegExp(`^${src}$`, "s");
746
+ }
747
+ catch {
748
+ return undefined;
749
+ }
750
+ }
751
+ function parseBracketExpression(pattern, start) {
752
+ let i = start + 1;
753
+ let negate = false;
754
+ if (pattern[i] === "!" || pattern[i] === "^") {
755
+ negate = true;
756
+ i++;
757
+ }
758
+ const contentStart = i;
759
+ let body = "";
760
+ let anyChar = false;
761
+ for (; i < pattern.length; i++) {
762
+ const ch = pattern[i];
763
+ if (ch === "]" && i > contentStart) {
764
+ if (body.length === 0 && !anyChar)
765
+ return undefined;
766
+ return { source: anyChar ? "[^/]" : `[${negate ? "^" : ""}${body}]`, end: i };
767
+ }
768
+ if (ch === "[" && (pattern[i + 1] === ":" || pattern[i + 1] === "." || pattern[i + 1] === "=")) {
769
+ const close = pattern.indexOf(`${pattern[i + 1]}]`, i + 2);
770
+ if (close < 0)
771
+ return undefined;
772
+ anyChar = true;
773
+ i = close + 1;
774
+ continue;
775
+ }
776
+ body += ch === "-" ? "-" : ch.replace(/[\]\\^]/g, "\\$&");
777
+ }
778
+ return undefined;
779
+ }
780
+ function globSegmentMatches(re, pattern, name) {
781
+ if (name.startsWith(".") && !pattern.startsWith("."))
782
+ return false;
783
+ return re.test(name);
784
+ }
785
+ function joinCandidate(dir, name) {
786
+ return `${dir.replace(/[\\/]+$/, "")}/${name}`;
787
+ }
788
+ function pathSegmentsFrom(p, from) {
789
+ const segs = [];
790
+ let scan = from;
791
+ while (scan < p.length) {
792
+ while (scan < p.length && (p[scan] === "/" || p[scan] === "\\"))
793
+ scan++;
794
+ const s = scan;
795
+ while (scan < p.length && p[scan] !== "/" && p[scan] !== "\\")
796
+ scan++;
797
+ if (scan > s)
798
+ segs.push({ text: p.slice(s, scan), start: s });
799
+ }
800
+ return segs;
801
+ }
667
802
  function createCanonicalReadBoundary(env, readRoots) {
668
803
  const lexicalOnly = isRemoteExecutionEnv(env);
669
804
  let rootsOnce;
@@ -690,20 +825,108 @@ function createCanonicalReadBoundary(env, readRoots) {
690
825
  }
691
826
  return undefined;
692
827
  };
828
+ const literalRootHead = (p, roots) => {
829
+ let best;
830
+ for (const r of [...readRoots, ...roots]) {
831
+ if (!r)
832
+ continue;
833
+ const withSep = r.endsWith("/") || r.endsWith("\\") ? r : `${r}/`;
834
+ if (p !== r && !p.startsWith(withSep))
835
+ continue;
836
+ if (best === undefined || r.length > best.length)
837
+ best = r;
838
+ }
839
+ return best;
840
+ };
841
+ const expandGlobCandidate = async (p, roots, signal) => {
842
+ const head = literalRootHead(p, roots);
843
+ let segs = pathSegmentsFrom(p, head?.length ?? 0);
844
+ let first = segs.findIndex((s) => GLOB_METACHAR_RE.test(s.text));
845
+ if (first < 0 && head !== undefined && GLOB_METACHAR_RE.test(head)) {
846
+ segs = pathSegmentsFrom(p, 0);
847
+ first = segs.findIndex((s) => GLOB_METACHAR_RE.test(s.text));
848
+ }
849
+ if (first < 0)
850
+ return undefined;
851
+ let frontier = [p.slice(0, segs[first].start).replace(/[\\/]+$/, "") || "/"];
852
+ let dirsUsed = 0;
853
+ let matches = 0;
854
+ for (let j = first; j < segs.length; j++) {
855
+ const seg = segs[j].text;
856
+ if (!GLOB_METACHAR_RE.test(seg)) {
857
+ frontier = frontier.map((d) => joinCandidate(d, seg));
858
+ continue;
859
+ }
860
+ const re = compileGlobSegment(seg);
861
+ if (re === undefined) {
862
+ return { unverifiable: `the pattern "${seg}" cannot be expanded to verify what it would read` };
863
+ }
864
+ const isLast = j === segs.length - 1;
865
+ const next = [];
866
+ for (const dir of frontier) {
867
+ if (++dirsUsed > MAX_GLOB_DIRS) {
868
+ return { unverifiable: `"${p}" expands over more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify` };
869
+ }
870
+ const listed = await env.listDir(dir, signal);
871
+ if (!listed.ok) {
872
+ if (listed.error.code === "not_found" || listed.error.code === "not_directory")
873
+ continue;
874
+ return { unverifiable: `"${dir}" cannot be enumerated to verify what "${seg}" would expand to (${listed.error.code})` };
875
+ }
876
+ if (seg.startsWith(".")) {
877
+ for (const dot of [".", ".."])
878
+ if (re.test(dot))
879
+ next.push(joinCandidate(dir, dot));
880
+ }
881
+ let dirInside = false;
882
+ if (isLast) {
883
+ const canonDir = await resolve(dir, signal);
884
+ if (canonDir !== undefined && !withinAnyRoot(roots, canonDir)) {
885
+ next.push(canonDir);
886
+ continue;
887
+ }
888
+ dirInside = canonDir !== undefined;
889
+ }
890
+ for (const entry of listed.value) {
891
+ if (!globSegmentMatches(re, seg, entry.name))
892
+ continue;
893
+ if (++matches > MAX_GLOB_MATCHES) {
894
+ return { unverifiable: `"${p}" expands to more than ${MAX_GLOB_MATCHES} names, too many to enumerate to verify` };
895
+ }
896
+ if (isLast && dirInside && entry.kind !== "symlink")
897
+ continue;
898
+ next.push(joinCandidate(dir, entry.name));
899
+ }
900
+ }
901
+ frontier = next;
902
+ if (frontier.length === 0)
903
+ return { paths: [] };
904
+ }
905
+ return { paths: frontier };
906
+ };
693
907
  return {
694
908
  escapesAfterResolution: async (paths, signal) => {
695
- if (lexicalOnly || paths.length === 0)
696
- return [];
909
+ if (lexicalOnly || (paths.literal.length === 0 && paths.patterns.length === 0))
910
+ return { escaping: [] };
697
911
  const roots = await canonicalRoots(signal);
698
912
  const escaping = [];
699
- for (const p of paths) {
913
+ const judge = async (p) => {
700
914
  const real = await resolve(p, signal);
701
915
  if (real === undefined)
702
- continue;
916
+ return;
703
917
  if (!withinAnyRoot(roots, real) && !escaping.includes(real))
704
918
  escaping.push(real);
919
+ };
920
+ for (const p of paths.literal)
921
+ await judge(p);
922
+ for (const p of paths.patterns) {
923
+ const expanded = await expandGlobCandidate(p, roots, signal);
924
+ if (expanded !== undefined && "unverifiable" in expanded)
925
+ return { escaping, unverifiable: expanded.unverifiable };
926
+ for (const t of expanded !== undefined && expanded.paths.length > 0 ? expanded.paths : [p])
927
+ await judge(t);
705
928
  }
706
- return escaping;
929
+ return { escaping };
707
930
  },
708
931
  allResolveInside: async (paths, signal) => {
709
932
  if (lexicalOnly || paths.length === 0)
@@ -744,7 +967,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
744
967
  "operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
745
968
  "allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
746
969
  "refused, as is one that cannot be resolved statically (e.g. a `~` path). " +
747
- (isRemoteExecutionEnv(env) ? "" : "A path that leads outside via a symlink counts as outside. ") +
970
+ (isRemoteExecutionEnv(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to. ") +
748
971
  "Still subject to the deployment's approval policy.",
749
972
  parameters: Type.Object({
750
973
  command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
@@ -763,11 +986,15 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
763
986
  return errorResult(`Error (Bash): ${boundary.reason}. bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.`, { code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
764
987
  }
765
988
  }
766
- const escaping = await canonicalBoundary.escapesAfterResolution(boundary.checkedPaths ?? [], ctx.signal);
767
- if (escaping.length > 0) {
768
- const quoted = escaping.map((p) => `"${p}"`).join(", ");
989
+ const resolved = await canonicalBoundary.escapesAfterResolution({ literal: boundary.checkedPaths ?? [], patterns: boundary.undecidedPaths ?? [] }, ctx.signal);
990
+ if (resolved.unverifiable !== undefined) {
991
+ return errorResult(`Error (Bash): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
992
+ "bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.", { code: "readonly_out_of_root", paths: [] });
993
+ }
994
+ if (resolved.escaping.length > 0) {
995
+ const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
769
996
  return errorResult(`Error (Bash): a path this command reads resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")} — not auto-allowed. ` +
770
- "bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.", { code: "readonly_out_of_root", paths: escaping });
997
+ "bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.", { code: "readonly_out_of_root", paths: resolved.escaping });
771
998
  }
772
999
  return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
773
1000
  },
@@ -776,14 +1003,13 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
776
1003
  export function createEnvTaskOutputTool(env) {
777
1004
  return defineTool({
778
1005
  name: TASK_OUTPUT_TOOL_NAME,
779
- aliases: [...TASK_OUTPUT_ALIASES],
780
1006
  contract: TASK_OUTPUT_CONTRACT,
781
1007
  description: composeTaskOutputDescription(ENV_DIRECT_TASK_TOOL_CAPS),
782
1008
  parameters: composeTaskOutputParams(ENV_DIRECT_TASK_TOOL_CAPS),
783
1009
  effect: "read",
784
1010
  execute: async (args) => {
785
- const { task_id, bash_id, runId, filter } = args;
786
- const id = resolveTaskIdArg(task_id, bash_id, runId);
1011
+ const { task_id, filter } = args;
1012
+ const id = resolveTaskIdArg(task_id);
787
1013
  if (!id)
788
1014
  return errorResult(TASK_OUTPUT_MISSING_ID_MESSAGE);
789
1015
  if (!hasBackgroundShell(env)) {
@@ -803,8 +1029,9 @@ export function createEnvTaskOutputTool(env) {
803
1029
  return errorResult(`Error (TaskOutput): ${r.error.message}`);
804
1030
  const shape = (text) => clipShellOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text);
805
1031
  const status = r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status;
806
- const dropped = r.value.bytesDroppedBeforeCursor ?? 0;
807
- const droppedNote = dropped > 0 ? `[!] ${dropped} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n` : "";
1032
+ const dropAcct = {};
1033
+ accountDroppedBytes(dropAcct, r.value);
1034
+ const droppedNote = spoolDropNote(dropAcct);
808
1035
  const body = `status: ${status}\n${droppedNote}--- new stdout ---\n${shape(r.value.stdout) || "(none)"}\n` +
809
1036
  `--- new stderr ---\n${shape(r.value.stderr) || "(none)"}`;
810
1037
  return delimitUntrusted(`TaskOutput ${id}`, body);
@@ -814,14 +1041,13 @@ export function createEnvTaskOutputTool(env) {
814
1041
  export function createEnvTaskStopTool(env, registry = defaultTaskRegistry) {
815
1042
  return defineTool({
816
1043
  name: TASK_STOP_TOOL_NAME,
817
- aliases: [...TASK_STOP_ALIASES],
818
1044
  contract: TASK_STOP_CONTRACT,
819
1045
  description: composeTaskStopDescription(ENV_DIRECT_TASK_TOOL_CAPS),
820
1046
  parameters: TASK_STOP_PARAMS,
821
1047
  effect: "write",
822
1048
  execute: async (args) => {
823
- const { task_id, shell_id, runId } = args;
824
- const id = resolveTaskIdArg(task_id, shell_id, runId);
1049
+ const { task_id, shell_id } = args;
1050
+ const id = resolveTaskIdArg(task_id, shell_id);
825
1051
  if (!id)
826
1052
  return errorResult(TASK_STOP_MISSING_ID_MESSAGE);
827
1053
  if (!hasBackgroundShell(env)) {
@@ -1,21 +1,11 @@
1
1
  import { errorResult } from "../../core/tools.js";
2
+ import type { DocumentContent, ImageContent, TextContent } from "../../internal/llm.js";
2
3
  import type { ExecutionEnv } from "../../internal/harness-types.js";
3
4
  import { type PdfModelCapabilities } from "./pdf.js";
4
5
  import { type ReadImageDownsamplerOption } from "./fs-shared.js";
5
6
  export { pdfModelCapabilitiesOf, type PdfModelCapabilities } from "./pdf.js";
6
7
  type ReadPdfReturn = string | {
7
- content: Array<{
8
- type: "text";
9
- text: string;
10
- } | {
11
- type: "image";
12
- data: string;
13
- mimeType: string;
14
- } | {
15
- type: "document";
16
- data: string;
17
- mimeType: string;
18
- }>;
8
+ content: Array<TextContent | ImageContent | DocumentContent>;
19
9
  details: unknown;
20
10
  };
21
11
  export declare function readPdfFile(env: ExecutionEnv, path: string, key: string, pages: string | undefined, signal: AbortSignal | undefined, downsamplerOpt: ReadImageDownsamplerOption, cwd: string, preRead?: Uint8Array, caps?: PdfModelCapabilities): Promise<ReadPdfReturn>;
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { clipWithFilePointer } from "../../core/tool-errors.js";
3
- import { sha256, similarNameSuggestion, OVERSIZE_READ_ESCAPE_HINT, PARTIAL_VIEW_READ_ESCAPE_HINT, } from "./safety.js";
3
+ import { normalizeAbsPathLexically, sha256, similarNameSuggestion, OVERSIZE_READ_ESCAPE_HINT, PARTIAL_VIEW_READ_ESCAPE_HINT, } from "./safety.js";
4
4
  import { decodeTextBytes, normalizeFileText } from "./encoding.js";
5
5
  import { shellQuote } from "./search.js";
6
6
  import { isNotebookPath } from "./notebook.js";
@@ -117,7 +117,7 @@ export function createShellOverflowSpoolFence(env) {
117
117
  return cut > 0 ? resolved.slice(0, cut) : undefined;
118
118
  };
119
119
  return async (path) => {
120
- const target = toPosixPathKey(path);
120
+ const target = normalizeAbsPathLexically(toPosixPathKey(path));
121
121
  const cut = target.lastIndexOf("/");
122
122
  if (cut <= 0 || !isShellOverflowFileName(target.slice(cut + 1)))
123
123
  return false;
@@ -28,7 +28,6 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
28
28
  "- `old_string` must match the file exactly, including indentation, and be unique — the edit fails otherwise. Strip the Read line prefix (line number + tab) before matching.\n" +
29
29
  "- `replace_all: true` replaces every occurrence instead.\n" +
30
30
  "- The edit also fails if the file changed on disk since you read it — Read it again first.",
31
- aliases: ["MultiEdit"],
32
31
  parameters: Type.Object({
33
32
  ...FILE_PATH_PARAMS,
34
33
  old_string: Type.Optional(Type.String({ description: "The text to replace" })),
@@ -1,3 +1,4 @@
1
+ import type { ImageContent } from "../../internal/llm.js";
1
2
  export declare function isNotebookPath(p: string): boolean;
2
3
  export declare const NOTEBOOK_CELL_OUTPUT_POINTER_CHARS = 10000;
3
4
  export declare const NOTEBOOK_OUTPUT_TEXT_CLIP = 10000;
@@ -31,11 +32,7 @@ export interface NotebookRender {
31
32
  blocks: Array<{
32
33
  type: "text";
33
34
  text: string;
34
- } | {
35
- type: "image";
36
- data: string;
37
- mimeType: string;
38
- }>;
35
+ } | ImageContent>;
39
36
  textChars: number;
40
37
  imageChars: number;
41
38
  }