@sema-agent/core 2.8.0 → 2.10.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 (44) hide show
  1. package/dist/agents/send-message-tool.js +37 -29
  2. package/dist/agents/subagent.js +91 -4
  3. package/dist/brain/circuit-breaker.js +18 -8
  4. package/dist/brain/retry.d.ts +1 -0
  5. package/dist/brain/retry.js +29 -7
  6. package/dist/brain/stream-engine.d.ts +1 -0
  7. package/dist/brain/stream-engine.js +74 -12
  8. package/dist/config/defaults.d.ts +1 -0
  9. package/dist/config/defaults.js +1 -0
  10. package/dist/core/auto-compaction.js +9 -1
  11. package/dist/core/background-agent-store.d.ts +2 -0
  12. package/dist/core/background-agent-store.js +20 -0
  13. package/dist/core/mcp.js +8 -5
  14. package/dist/core/runner/assemble-result.d.ts +1 -0
  15. package/dist/core/runner/assemble-result.js +1 -1
  16. package/dist/core/runner/prepare-task.d.ts +2 -1
  17. package/dist/core/runner/prepare-task.js +61 -20
  18. package/dist/core/runner/runtask.js +38 -12
  19. package/dist/core/runner/tool-disclosure.d.ts +8 -3
  20. package/dist/core/runner/tool-disclosure.js +39 -10
  21. package/dist/core/skills-directory.d.ts +1 -1
  22. package/dist/core/skills-directory.js +257 -28
  23. package/dist/core/task-registry-agent.d.ts +2 -1
  24. package/dist/core/task-registry-agent.js +50 -54
  25. package/dist/core/task-registry.d.ts +1 -0
  26. package/dist/core/task-registry.js +1 -1
  27. package/dist/core/types.d.ts +9 -1
  28. package/dist/engine/compaction/compaction.js +71 -20
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/internal/harness-types.d.ts +1 -1
  32. package/dist/internal/harness.d.ts +1 -1
  33. package/dist/internal/harness.js +1 -1
  34. package/dist/orchestration/run-workflow-tool.d.ts +1 -0
  35. package/dist/orchestration/run-workflow-tool.js +4 -1
  36. package/dist/orchestration/workflow.d.ts +1 -1
  37. package/dist/orchestration/workflow.js +20 -12
  38. package/dist/tools/fs/bash-readonly-classifier.js +83 -17
  39. package/dist/tools/fs/fs-bash.js +17 -11
  40. package/dist/tools/fs/fs-shared.d.ts +1 -0
  41. package/dist/tools/fs/fs-shared.js +44 -2
  42. package/dist/tools/web.d.ts +15 -0
  43. package/dist/tools/web.js +42 -0
  44. package/package.json +1 -1
@@ -256,7 +256,7 @@ export function findTurnStartIndex(entries, entryIndex, startIndex) {
256
256
  }
257
257
  return -1;
258
258
  }
259
- function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
259
+ function collectToolCallSites(entries, startIndex, endIndex) {
260
260
  const callSites = new Map();
261
261
  for (let i = startIndex; i < endIndex; i++) {
262
262
  const entry = entries[i];
@@ -275,6 +275,22 @@ function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
275
275
  }
276
276
  }
277
277
  }
278
+ return callSites;
279
+ }
280
+ function emittingCallSite(callSites, toolCallId, resultIndex) {
281
+ const sites = callSites.get(toolCallId);
282
+ if (sites === undefined)
283
+ return -1;
284
+ let site = -1;
285
+ for (const s of sites) {
286
+ if (s < resultIndex)
287
+ site = s;
288
+ else
289
+ break;
290
+ }
291
+ return site;
292
+ }
293
+ function enforceToolPairContainment(entries, callSites, endIndex, cutIndex) {
278
294
  if (callSites.size === 0)
279
295
  return cutIndex;
280
296
  for (let i = cutIndex; i < endIndex; i++) {
@@ -284,16 +300,7 @@ function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
284
300
  const msg = entry.message;
285
301
  if (msg.role !== "toolResult")
286
302
  continue;
287
- const sites = callSites.get(msg.toolCallId);
288
- if (sites === undefined)
289
- continue;
290
- let site = -1;
291
- for (const s of sites) {
292
- if (s < i)
293
- site = s;
294
- else
295
- break;
296
- }
303
+ const site = emittingCallSite(callSites, msg.toolCallId, i);
297
304
  if (site === -1 || site >= cutIndex)
298
305
  continue;
299
306
  cutIndex = site;
@@ -301,6 +308,49 @@ function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
301
308
  }
302
309
  return cutIndex;
303
310
  }
311
+ function foldToolPairsForward(entries, callSites, cutPoints, endIndex, cutIndex) {
312
+ for (let i = cutIndex; i < endIndex; i++) {
313
+ const entry = entries[i];
314
+ if (entry.type !== "message")
315
+ continue;
316
+ const msg = entry.message;
317
+ if (msg.role !== "toolResult")
318
+ continue;
319
+ const site = emittingCallSite(callSites, msg.toolCallId, i);
320
+ if (site === -1 || site >= cutIndex)
321
+ continue;
322
+ const next = cutPoints.find((c) => c > i);
323
+ if (next === undefined)
324
+ return -1;
325
+ cutIndex = next;
326
+ i = cutIndex - 1;
327
+ }
328
+ return cutIndex;
329
+ }
330
+ function deriveCutPoint(entries, startIndex, cutIndex) {
331
+ const cutEntry = entries[cutIndex];
332
+ const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
333
+ const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
334
+ return {
335
+ firstKeptEntryIndex: cutIndex,
336
+ turnStartIndex,
337
+ isSplitTurn: !isUserMessage && turnStartIndex !== -1,
338
+ };
339
+ }
340
+ function leavesSummarizableHistory(entries, startIndex, cut) {
341
+ const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
342
+ for (let i = startIndex; i < historyEnd; i++) {
343
+ if (getMessageFromEntryForCompaction(entries[i]) !== undefined)
344
+ return true;
345
+ }
346
+ if (!cut.isSplitTurn)
347
+ return false;
348
+ for (let i = cut.turnStartIndex; i < cut.firstKeptEntryIndex; i++) {
349
+ if (getMessageFromEntryForCompaction(entries[i]) !== undefined)
350
+ return true;
351
+ }
352
+ return false;
353
+ }
304
354
  export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
305
355
  const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
306
356
  if (cutPoints.length === 0) {
@@ -335,15 +385,16 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, ch
335
385
  break;
336
386
  }
337
387
  }
338
- cutIndex = enforceToolPairContainment(entries, startIndex, endIndex, cutIndex);
339
- const cutEntry = entries[cutIndex];
340
- const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
341
- const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
342
- return {
343
- firstKeptEntryIndex: cutIndex,
344
- turnStartIndex,
345
- isSplitTurn: !isUserMessage && turnStartIndex !== -1,
346
- };
388
+ const callSites = collectToolCallSites(entries, startIndex, endIndex);
389
+ const budgetCutIndex = cutIndex;
390
+ cutIndex = enforceToolPairContainment(entries, callSites, endIndex, cutIndex);
391
+ const kept = deriveCutPoint(entries, startIndex, cutIndex);
392
+ if (cutIndex !== budgetCutIndex && !leavesSummarizableHistory(entries, startIndex, kept)) {
393
+ const folded = foldToolPairsForward(entries, callSites, cutPoints, endIndex, budgetCutIndex);
394
+ if (folded !== -1)
395
+ return deriveCutPoint(entries, startIndex, folded);
396
+ }
397
+ return kept;
347
398
  }
348
399
  export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
349
400
 
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { createSkillsFromDirectory, type SkillsDirectoryOptions, type SkillsDire
7
7
  export { REPORT_FINDINGS_TOOL_NAME, type ReportedFinding } from "./core/runner/synthetic-tools.js";
8
8
  export { formatToolError, formatZodValidationError, formatValidationPath, truncateError, errorClassOf } from "./core/tool-errors.js";
9
9
  export type { WorkerErrorClass } from "./core/tool-errors.js";
10
- export { createWebFetchTool, webFetchToolSpec, htmlToText, type WebFetchConfig, createWebSearchTool, type WebSearchConfig, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
10
+ export { createWebFetchTool, webFetchToolSpec, htmlToText, type WebFetchConfig, createWebSearchTool, type WebSearchConfig, createSearxngSearchBackend, probeSearchBackend, type SearxngBackendOptions, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
11
11
  export { createTodoWriteTool } from "./tools/todo.js";
12
12
  export { createTaskListTools, createMemoryTaskListStore, assertJsonMetadata, type TaskListItem, type TaskListStore } from "./tools/task-list.js";
13
13
  export { assembleCodeTools, type CodeToolsConfig, CODE_ROLE } from "./scenarios/full-body.js";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { SKILL_TOOL_NAME } from "./core/runner/synthetic-tools.js";
5
5
  export { createSkillsFromDirectory, } from "./core/skills-directory.js";
6
6
  export { REPORT_FINDINGS_TOOL_NAME } from "./core/runner/synthetic-tools.js";
7
7
  export { formatToolError, formatZodValidationError, formatValidationPath, truncateError, errorClassOf } from "./core/tool-errors.js";
8
- export { createWebFetchTool, webFetchToolSpec, htmlToText, createWebSearchTool, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
8
+ export { createWebFetchTool, webFetchToolSpec, htmlToText, createWebSearchTool, createSearxngSearchBackend, probeSearchBackend, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
9
9
  export { createTodoWriteTool } from "./tools/todo.js";
10
10
  export { createTaskListTools, createMemoryTaskListStore, assertJsonMetadata } from "./tools/task-list.js";
11
11
  export { assembleCodeTools, CODE_ROLE } from "./scenarios/full-body.js";
@@ -1,7 +1,7 @@
1
1
  export type { CompactionPreparation, SummarizationClampDryRun } from "../engine/compaction/compaction.js";
2
2
  export type { InvokedSkillRetention } from "../engine/compaction/utils.js";
3
3
  export type { AgentCoreRuntimeDeps } from "../engine/loop/runtime-deps.js";
4
- export type { AgentMessage, AgentTool, AgentToolResult, ThinkingLevel, } from "../engine/loop/types.js";
4
+ export type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode, } from "../engine/loop/types.js";
5
5
  export type { AgentHarnessEvent, CompactionSettings, ExecutionEnv, ExecutionErrorCode, FileErrorCode, FileInfo, Result, Session, SessionMetadata, SessionRepo, SessionStorage, SessionTreeEntry, Skill, } from "../engine/harness/types.js";
6
6
  export type { ExecutionEnvExecOptions } from "../engine/harness/types.js";
7
7
  export type { SessionWriteOptions, CompactionEntry } from "../engine/harness/types.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -58,6 +58,7 @@ export interface RunWorkflowToolDeps {
58
58
  governanceBaseline: WorkflowGovernanceBaseline;
59
59
  parentExcludeTools?: readonly string[];
60
60
  parentDeferTools?: readonly string[];
61
+ parentAlwaysLoadTools?: readonly string[];
61
62
  parentPromptProfile?: "simple" | "classic";
62
63
  models?: Record<string, Model>;
63
64
  agents?: import("../core/types.js").AgentDefinition[];
@@ -154,9 +154,12 @@ export async function createRunWorkflowTool(d) {
154
154
  ...(d.parentDeferTools?.length
155
155
  ? { deferTools: [...new Set([...(base.deferTools ?? []), ...d.parentDeferTools])] }
156
156
  : {}),
157
+ ...(d.parentAlwaysLoadTools?.length
158
+ ? { alwaysLoadTools: [...new Set([...(base.alwaysLoadTools ?? []), ...d.parentAlwaysLoadTools])] }
159
+ : {}),
157
160
  });
158
161
  const withParentProfile = (base) => d.parentPromptProfile !== undefined && base.promptProfile === undefined ? { ...base, promptProfile: d.parentPromptProfile } : base;
159
- const baselineWithParentFace = (d.parentExcludeTools?.length ?? 0) > 0 || (d.parentDeferTools?.length ?? 0) > 0 || d.parentPromptProfile !== undefined
162
+ const baselineWithParentFace = (d.parentExcludeTools?.length ?? 0) > 0 || (d.parentDeferTools?.length ?? 0) > 0 || (d.parentAlwaysLoadTools?.length ?? 0) > 0 || d.parentPromptProfile !== undefined
160
163
  ? {
161
164
  ...d.governanceBaseline,
162
165
  base: withParentProfile(withParentFace(d.governanceBaseline.base)),
@@ -18,7 +18,7 @@ export declare function workflowAgentCallKey(ordinal: number, spec: TaskSpec, op
18
18
  schema?: TSchema;
19
19
  isolation?: "worktree";
20
20
  }): string;
21
- export declare function assertSupportedAgentIsolation(isolation: unknown): asserts isolation is "worktree" | undefined;
21
+ export declare function assertSupportedAgentIsolation(isolation: unknown): void;
22
22
  export interface WorkflowFanOutSlotError {
23
23
  index: number;
24
24
  kind: string;
@@ -9,6 +9,7 @@ import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
9
9
  import { callKeyOrdinal, oversizeJournalResult, journalOversizeTombstone, JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES } from "../core/workflow-journal-store.js";
10
10
  import { isWorkflowRunActive, closeWorkflowChannel, markWorkflowActive, publishWorkflowEvent } from "./workflow-observe.js";
11
11
  import { isDurablePause, mapNestedSuspend } from "../agents/suspend-guard.js";
12
+ import { RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
12
13
  import { boundInputHashOf } from "../core/canonical-json.js";
13
14
  import { boundedRedactedSummary } from "../core/untrusted-egress.js";
14
15
  import { delimitUntrusted } from "../core/untrusted-text.js";
@@ -20,7 +21,6 @@ const MAX_TRANSCRIPT_CHARS = 4000;
20
21
  const WORKFLOW_RESULT_MAX = 4000;
21
22
  const WORKFLOW_RESULT_FULL_MAX = 200_000;
22
23
  const MAX_ACTIVITY = 30;
23
- const RUNNING_AGENT_PERSIST_EVERY_BEATS = 4;
24
24
  function workflowModelLabel(spec) {
25
25
  const model = spec.model;
26
26
  if (model === undefined)
@@ -100,7 +100,7 @@ export function workflowAgentCallKey(ordinal, spec, opts) {
100
100
  tools: spec.tools?.map((t) => t.name).slice().sort(),
101
101
  mcp: spec.mcp?.map((m) => m.name).slice().sort(),
102
102
  outputSchema: opts.schema ?? spec.outputSchema,
103
- isolation: opts.isolation,
103
+ isolation: opts.isolation ? opts.isolation : undefined,
104
104
  };
105
105
  return `${ordinal}:${boundInputHashOf(identity)}`;
106
106
  }
@@ -112,7 +112,7 @@ function resolveChildSessionIdAtSpawn(spec) {
112
112
  return randomUUID();
113
113
  }
114
114
  export function assertSupportedAgentIsolation(isolation) {
115
- if (isolation !== undefined && isolation !== "worktree") {
115
+ if (isolation && isolation !== "worktree") {
116
116
  const shown = typeof isolation === "string" ? JSON.stringify(isolation) : String(isolation);
117
117
  const e = new Error(`isolation ${shown} is not supported in workflow agents — only "worktree" (omit the option to run in the shared working tree). The agent was not started (fail-closed: an unrecognized isolation value must never silently run in the shared working tree).`);
118
118
  e.code = "isolation.invalid";
@@ -453,16 +453,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
453
453
  };
454
454
  const waIdOf = (callKey) => `wa${createHash("sha256").update(`${runId}:${callKey}`).digest("hex").slice(0, 16)}`;
455
455
  const bceLive = new Map();
456
- const bceSpawn = (callKey, label, agentType, replayed) => {
456
+ const bceSpawn = (callKey, label, agentType, replayed, sessionId) => {
457
457
  if (!bceSink)
458
458
  return;
459
459
  const id = waIdOf(callKey);
460
- bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}) });
460
+ bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}), ...(sessionId !== undefined ? { sessionId } : {}) });
461
461
  bceEmit({
462
462
  kind: "spawn",
463
463
  taskId: id,
464
464
  sessionScoped: false,
465
465
  owner: runId,
466
+ ...(sessionId !== undefined ? { sessionId, transcriptId: sessionId } : {}),
466
467
  ...(scope !== undefined ? { scope } : {}),
467
468
  description: replayed ? `${label} (replayed)` : label,
468
469
  agentType: agentType ?? "workflow-agent",
@@ -474,6 +475,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
474
475
  startedAt: Date.now(),
475
476
  });
476
477
  };
478
+ const bceBindSession = (callKey, sessionId) => {
479
+ const row = bceLive.get(waIdOf(callKey));
480
+ if (row !== undefined)
481
+ row.sessionId = sessionId;
482
+ };
477
483
  const bceTick = (callKey, e) => {
478
484
  if (!bceSink)
479
485
  return;
@@ -489,6 +495,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
489
495
  workflowRunId: runId,
490
496
  ...(scope !== undefined ? { scope } : {}),
491
497
  ...(row.agentType !== undefined ? { agentType: row.agentType } : { agentType: "workflow-agent" }),
498
+ ...(row.sessionId !== undefined ? { sessionId: row.sessionId, transcriptId: row.sessionId } : {}),
492
499
  name: e.name ?? row.label,
493
500
  progressTaskId: e.taskId,
494
501
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
@@ -635,7 +642,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
635
642
  rec.toolCalls = (rec.toolCalls ?? 0) + 1;
636
643
  rec.activity = tail;
637
644
  beatCount += 1;
638
- if (beatCount === 1 || beatCount % RUNNING_AGENT_PERSIST_EVERY_BEATS === 0) {
645
+ if (beatCount === 1 || beatCount % RUNNING_AGENT_OBSERVE_EVERY_BEATS === 0) {
639
646
  void persist("update");
640
647
  }
641
648
  };
@@ -768,7 +775,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
768
775
  agentPhaseOf.set(replayRec, phaseInstance);
769
776
  emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(model !== undefined ? { model } : {}), replayed: true, ts: at });
770
777
  emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: replayRec.status, output: cachedOutput, ...(rs.toolCalls !== undefined ? { toolCalls: rs.toolCalls } : {}), replayed: true, ts: at });
771
- bceSpawn(callKey, label, agentOpts.agentType, true);
778
+ bceSpawn(callKey, label, agentOpts.agentType, true, r.sessionId || undefined);
772
779
  bceTerminal(callKey, replayRec.status === "completed" ? "completed" : "failed", cachedOutput, r.sessionId || undefined, replayRec.stats);
773
780
  accumulateStats(r, false);
774
781
  void persist("update");
@@ -810,7 +817,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
810
817
  if (finalized)
811
818
  throw new Error("workflow run already finalized — ctx.agent cannot spawn after the run ended");
812
819
  rec.startedAt = now();
813
- bceSpawn(callKey, label, agentOpts.agentType, false);
820
+ const bornChildSessionId = resolveChildSessionIdAtSpawn(spec);
821
+ bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId);
814
822
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
815
823
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
816
824
  const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
@@ -852,10 +860,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
852
860
  const onCallerAbort = () => attemptCtl.abort(new Error("workflow aborted"));
853
861
  effectiveSignal?.addEventListener("abort", onCallerAbort, { once: true });
854
862
  armWatchdog();
855
- const attemptSessionId = resolveChildSessionIdAtSpawn(runSpec);
863
+ const attemptSessionId = attempts === 1 ? bornChildSessionId : resolveChildSessionIdAtSpawn(runSpec);
856
864
  const attemptSpec = attemptSessionId !== undefined ? { ...runSpec, sessionId: attemptSessionId, signal: attemptCtl.signal } : { ...runSpec, signal: attemptCtl.signal };
857
865
  if (attemptSessionId !== undefined && !finalized) {
858
866
  rec.sessionId = attemptSessionId;
867
+ bceBindSession(callKey, attemptSessionId);
859
868
  void persist("update");
860
869
  }
861
870
  const attemptInternals = {
@@ -1143,9 +1152,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1143
1152
  throw new Error(finalized ? "workflow run already finalized — ctx.agentStream cannot spawn after the run ended" : "workflow aborted");
1144
1153
  }
1145
1154
  rec.startedAt = now();
1146
- bceSpawn(callKey, label, agentOpts.agentType, false);
1155
+ const childSessionId = resolveChildSessionIdAtSpawn(spec);
1156
+ bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId);
1147
1157
  let stream;
1148
- let childSessionId;
1149
1158
  try {
1150
1159
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
1151
1160
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
@@ -1154,7 +1163,6 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1154
1163
  const baseRunSpec = agentOpts.schema
1155
1164
  ? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
1156
1165
  : { ...framedSpec, ...authInherit, signal: effectiveSignal };
1157
- childSessionId = resolveChildSessionIdAtSpawn(baseRunSpec);
1158
1166
  const runSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
1159
1167
  const enrichedForwardS = opts.onForwardEvent !== undefined
1160
1168
  ? (e) => {
@@ -83,19 +83,80 @@ function resolveOperandLexically(base, operand, homeDir) {
83
83
  return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
84
84
  }
85
85
  function tokenizeSegment(segment) {
86
- return segment
86
+ const raw = segment
87
87
  .trim()
88
88
  .split(/\s+/)
89
- .filter((t) => t.length > 0)
90
- .map(foldQuoteRemovalToken);
89
+ .filter((t) => t.length > 0);
90
+ return { folded: raw.map(foldQuoteRemovalToken), raw };
91
91
  }
92
- function collectSegmentBoundaryFindings(toks, boundary) {
93
- const name = toks[0];
92
+ function hasUnquotedExpansionMetachar(rawToken) {
93
+ let open;
94
+ for (const ch of rawToken) {
95
+ if (open === undefined && (ch === '"' || ch === "'")) {
96
+ open = ch;
97
+ continue;
98
+ }
99
+ if (open === ch) {
100
+ open = undefined;
101
+ continue;
102
+ }
103
+ if (open === undefined && (ch === "{" || ch === "}" || ch === "$" || ch === "`"))
104
+ return true;
105
+ if (open === '"' && (ch === "$" || ch === "`"))
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+ function isGrepPatternFlagToken(tok) {
111
+ if (tok.startsWith("--"))
112
+ return tok.startsWith("--regexp") || tok.startsWith("--file");
113
+ return /^-[A-Za-z]*[ef]/.test(tok);
114
+ }
115
+ function grepClusterValueOwner(tok) {
116
+ if (!/^-[A-Za-z]/.test(tok) || tok.startsWith("--"))
117
+ return undefined;
118
+ for (const ch of tok.slice(1)) {
119
+ if (ch === "e" || ch === "f")
120
+ return ch;
121
+ if (!/[A-Za-z]/.test(ch))
122
+ return undefined;
123
+ }
124
+ return undefined;
125
+ }
126
+ function attachedOptionPayloads(tok) {
127
+ const out = [];
128
+ if (tok.startsWith("--")) {
129
+ const eq = tok.indexOf("=");
130
+ if (eq > 0 && eq + 1 < tok.length)
131
+ out.push(tok.slice(eq + 1));
132
+ return out;
133
+ }
134
+ for (let k = 1; k <= tok.length; k++) {
135
+ if (!/^[A-Za-z]*$/.test(tok.slice(1, k)))
136
+ break;
137
+ const payload = tok.slice(k);
138
+ if (payload.length > 0)
139
+ out.push(payload);
140
+ }
141
+ return out;
142
+ }
143
+ function collectSegmentBoundaryFindings(tokens, boundary) {
144
+ const name = tokens.folded[0];
94
145
  if (NO_PATH_OPERAND_COMMANDS.has(name))
95
146
  return [];
96
- const args = toks.slice(1);
147
+ const args = tokens.folded.slice(1);
97
148
  const findings = [];
98
149
  const candidates = [];
150
+ for (const rawArg of tokens.raw.slice(1)) {
151
+ if (!hasUnquotedExpansionMetachar(rawArg))
152
+ continue;
153
+ return [
154
+ {
155
+ kind: "unresolvable",
156
+ reason: `"${name}" is given the argument "${rawArg}", which the shell expands (brace/variable/command expansion) before the command runs — the path it would actually read cannot be resolved statically, so it is not auto-allowed`,
157
+ },
158
+ ];
159
+ }
99
160
  if (name === "cd") {
100
161
  const target = args.find((t) => !t.startsWith("-") || t === "-");
101
162
  if (target === undefined) {
@@ -107,7 +168,7 @@ function collectSegmentBoundaryFindings(toks, boundary) {
107
168
  candidates.push(target);
108
169
  }
109
170
  else {
110
- const patternSuppliedByFlag = name === "grep" && args.some((t) => t === "-e" || t.startsWith("-e") || t === "-f" || t.startsWith("-f") || t.startsWith("--regexp") || t.startsWith("--file"));
171
+ const patternSuppliedByFlag = name === "grep" && args.some(isGrepPatternFlagToken);
111
172
  let sawOperand = false;
112
173
  for (let k = 0; k < args.length; k++) {
113
174
  const t = args[k];
@@ -115,17 +176,21 @@ function collectSegmentBoundaryFindings(toks, boundary) {
115
176
  k++;
116
177
  continue;
117
178
  }
179
+ if (name === "cut" && (/^-d./.test(t) || /^--(output-)?delimiter=/.test(t)))
180
+ continue;
181
+ if (name === "grep" && (grepClusterValueOwner(t) === "e" || /^--regexp=/.test(t)))
182
+ continue;
118
183
  if (t.startsWith("-") && t !== "-") {
119
- const eq = t.indexOf("=");
120
- const value = eq > 0 ? t.slice(eq + 1) : "";
121
- if (value.length > 0 && isAbsolutePathToken(value))
122
- candidates.push(value);
184
+ for (const payload of attachedOptionPayloads(t)) {
185
+ if (isAbsolutePathToken(payload) || isPathShapedToken(payload))
186
+ candidates.push(payload);
187
+ }
123
188
  continue;
124
189
  }
125
- if (t === "-")
126
- continue;
127
190
  const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
128
191
  sawOperand = true;
192
+ if (t === "-")
193
+ continue;
129
194
  if (isGrepPatternSlot)
130
195
  continue;
131
196
  if (isPathShapedToken(t))
@@ -227,16 +292,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
227
292
  const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity };
228
293
  const foldedSegments = [];
229
294
  for (let si = 0; si < segments.length; si++) {
230
- const toks = tokenizeSegment(segments[si]);
295
+ const segmentTokens = tokenizeSegment(segments[si]);
296
+ const toks = segmentTokens.folded;
231
297
  if (toks.length === 0)
232
298
  continue;
233
- foldedSegments.push(toks);
299
+ foldedSegments.push(segmentTokens);
234
300
  const name = toks[0];
235
301
  const nonOption = toks.slice(1).filter((t) => !t.startsWith("-"));
236
302
  if (!(pipeFed[si] ?? false)) {
237
303
  const floor = STDIN_FILE_FLOOR[name];
238
304
  const restArgs = toks.slice(1);
239
- const grepPatternSuppliedByFlag = name === "grep" && restArgs.some((t) => t === "-e" || t.startsWith("-e") || t === "-f" || t.startsWith("--regexp") || t.startsWith("--file"));
305
+ const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(isGrepPatternFlagToken);
240
306
  let sawNonFlagOperand = false;
241
307
  let hasStdinDash = false;
242
308
  if (name !== "tr") {
@@ -305,7 +371,7 @@ function evaluateReadBoundary(foldedSegments, boundary) {
305
371
  }
306
372
  export function classifySimpleCommandReadBoundary(command, boundary) {
307
373
  const toks = tokenizeSegment(command);
308
- if (toks.length === 0)
374
+ if (toks.folded.length === 0)
309
375
  return {};
310
376
  return evaluateReadBoundary([toks], boundary);
311
377
  }
@@ -7,7 +7,7 @@ import { delimitUntrusted } from "../../core/untrusted-text.js";
7
7
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
8
8
  import { imageMagicMatches, withinAnyRoot } from "./safety.js";
9
9
  import { ghRateLimitHint } from "./gh-rate-limit.js";
10
- import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
10
+ import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
11
11
  import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
12
12
  export function bashReversibilityProbe(allow, boundary) {
13
13
  const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
@@ -632,7 +632,19 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
632
632
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
633
633
  const sample = [...allow].slice(0, 6).join(", ");
634
634
  const readRoots = [rootCanonical, ...(opts?.additionalRoots ?? [])];
635
- const mintedOutputFiles = new Set();
635
+ const overflowSpoolFence = createShellOverflowSpoolFence(env);
636
+ const readsOnlyEngineOverflowSpool = async (verdict) => {
637
+ if (verdict.outOfRootRead !== true)
638
+ return false;
639
+ const paths = verdict.outOfRootPaths ?? [];
640
+ if (paths.length === 0)
641
+ return false;
642
+ for (const p of paths) {
643
+ if (!(await overflowSpoolFence(p)))
644
+ return false;
645
+ }
646
+ return true;
647
+ };
636
648
  return defineTool({
637
649
  name: "Bash",
638
650
  contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
@@ -651,17 +663,11 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
651
663
  const reason = coarseReadonlyCheck(command, allow);
652
664
  if (reason)
653
665
  return errorResult(`Error (Bash): ${reason}`);
654
- const boundary = classifySimpleCommandReadBoundary(command, { roots: [...readRoots, ...mintedOutputFiles], cwd: rootCanonical });
655
- if (boundary.reason !== undefined) {
666
+ const boundary = classifySimpleCommandReadBoundary(command, { roots: readRoots, cwd: rootCanonical });
667
+ if (boundary.reason !== undefined && !(await readsOnlyEngineOverflowSpool(boundary))) {
656
668
  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 ?? [] });
657
669
  }
658
- const result = await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
659
- if (typeof result !== "string") {
660
- const outputFile = result.details.output_file;
661
- if (typeof outputFile === "string")
662
- mintedOutputFiles.add(outputFile);
663
- }
664
- return result;
670
+ return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
665
671
  },
666
672
  });
667
673
  }
@@ -47,6 +47,7 @@ export declare const FILE_PATH_PARAMS: {
47
47
  };
48
48
  export declare function clipShellOutput(s: string): string;
49
49
  export declare function writeShellOverflowFile(env: ExecutionEnv, stdout: string, stderr: string): Promise<string | undefined>;
50
+ export declare function createShellOverflowSpoolFence(env: ExecutionEnv): (path: string) => Promise<boolean>;
50
51
  export declare function shellRecoveryHint(path: string, readOnly: boolean | undefined): string;
51
52
  export declare const FILE_STATE_TRAILER = " (file state is current in your context \u2014 no need to Read it back)";
52
53
  export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
@@ -52,7 +52,10 @@ export const BASH_MAX_TIMEOUT_SEC = 600;
52
52
  export const BASH_DEFAULT_TIMEOUT_MS = BASH_DEFAULT_TIMEOUT_SEC * 1000;
53
53
  export const BASH_MAX_TIMEOUT_MS = BASH_MAX_TIMEOUT_SEC * 1000;
54
54
  function validTimeoutMs(n) {
55
- return n !== undefined && Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
55
+ if (n === undefined || !Number.isFinite(n))
56
+ return undefined;
57
+ const floored = Math.floor(n);
58
+ return floored >= 1 ? floored : undefined;
56
59
  }
57
60
  export function resolveBashTimeoutCaps(opts) {
58
61
  const defaultMs = validTimeoutMs(opts?.bashDefaultTimeoutMs) ??
@@ -80,7 +83,7 @@ export function clipShellOutput(s) {
80
83
  return clipWithFilePointer(s, bashMaxOutputChars());
81
84
  }
82
85
  export async function writeShellOverflowFile(env, stdout, stderr) {
83
- const tf = await env.createTempFile({ prefix: "bash-output-", suffix: ".log" });
86
+ const tf = await env.createTempFile({ prefix: SHELL_OVERFLOW_FILE_PREFIX, suffix: SHELL_OVERFLOW_FILE_SUFFIX });
84
87
  if (!tf.ok)
85
88
  return undefined;
86
89
  const body = stderr.length > 0 ? `${stdout}${stdout.length > 0 && !stdout.endsWith("\n") ? "\n" : ""}--- stderr ---\n${stderr}` : stdout;
@@ -90,6 +93,45 @@ export async function writeShellOverflowFile(env, stdout, stderr) {
90
93
  const canon = await env.canonicalPath(tf.value);
91
94
  return canon.ok ? canon.value : tf.value;
92
95
  }
96
+ const SHELL_OVERFLOW_FILE_PREFIX = "bash-output-";
97
+ const SHELL_OVERFLOW_FILE_SUFFIX = ".log";
98
+ function toPosixPathKey(p) {
99
+ return p.replace(/\\/g, "/").replace(/(.)\/+$/, "$1");
100
+ }
101
+ function isShellOverflowFileName(base) {
102
+ if (!base.startsWith(SHELL_OVERFLOW_FILE_PREFIX) || !base.endsWith(SHELL_OVERFLOW_FILE_SUFFIX))
103
+ return false;
104
+ const middle = base.slice(SHELL_OVERFLOW_FILE_PREFIX.length, base.length - SHELL_OVERFLOW_FILE_SUFFIX.length);
105
+ return /^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(middle);
106
+ }
107
+ export function createShellOverflowSpoolFence(env) {
108
+ let tempArea;
109
+ const learnTempArea = async () => {
110
+ const probe = await env.createTempDir();
111
+ if (!probe.ok)
112
+ return undefined;
113
+ const canon = await env.canonicalPath(probe.value);
114
+ const resolved = toPosixPathKey(canon.ok ? canon.value : probe.value);
115
+ await env.remove(probe.value, { recursive: true, force: true });
116
+ const cut = resolved.lastIndexOf("/");
117
+ return cut > 0 ? resolved.slice(0, cut) : undefined;
118
+ };
119
+ return async (path) => {
120
+ const target = toPosixPathKey(path);
121
+ const cut = target.lastIndexOf("/");
122
+ if (cut <= 0 || !isShellOverflowFileName(target.slice(cut + 1)))
123
+ return false;
124
+ tempArea ??= learnTempArea();
125
+ const area = await tempArea;
126
+ if (area === undefined)
127
+ return false;
128
+ const parent = target.slice(0, cut);
129
+ if (parent === area)
130
+ return true;
131
+ const up = parent.lastIndexOf("/");
132
+ return up > 0 && parent.slice(0, up) === area;
133
+ };
134
+ }
93
135
  export function shellRecoveryHint(path, readOnly) {
94
136
  const quoted = shellQuote(path);
95
137
  const example = readOnly ? `tail -c 50000 ${quoted}` : `sed -n 'START,ENDp' ${quoted}`;
@@ -29,3 +29,18 @@ export interface WebSearchConfig {
29
29
  }
30
30
  export declare function clipCodePoints(s: string, max: number): string;
31
31
  export declare function createWebSearchTool(config: WebSearchConfig): ToolSpec;
32
+ export interface SearxngBackendOptions {
33
+ fetchImpl?: typeof fetch;
34
+ timeoutMs?: number;
35
+ extraParams?: Record<string, string>;
36
+ }
37
+ export declare function createSearxngSearchBackend(baseUrl: string, options?: SearxngBackendOptions): WebSearchConfig["search"];
38
+ export declare function probeSearchBackend(search: WebSearchConfig["search"], options?: {
39
+ timeoutMs?: number;
40
+ }): Promise<{
41
+ ok: true;
42
+ results: number;
43
+ } | {
44
+ ok: false;
45
+ error: string;
46
+ }>;