@sema-agent/core 5.8.0 → 5.9.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 (37) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/agents/cascade.js +24 -0
  3. package/dist/agents/subagent.d.ts +3 -0
  4. package/dist/agents/subagent.js +64 -14
  5. package/dist/brain/open-responses.d.ts +11 -0
  6. package/dist/brain/open-responses.js +721 -0
  7. package/dist/brain/request-params.d.ts +1 -0
  8. package/dist/brain/request-params.js +16 -0
  9. package/dist/core/fs-write-gate-policy.js +2 -2
  10. package/dist/core/lsp-diagnostics.d.ts +3 -2
  11. package/dist/core/lsp-diagnostics.js +20 -7
  12. package/dist/core/runner/assemble-result.d.ts +1 -0
  13. package/dist/core/runner/assemble-result.js +14 -7
  14. package/dist/core/runner/prepare-task.d.ts +1 -0
  15. package/dist/core/runner/prepare-task.js +7 -4
  16. package/dist/core/runner/runtask.js +28 -22
  17. package/dist/core/runner/session-rule-policy.d.ts +1 -0
  18. package/dist/core/runner/session-rule-policy.js +4 -3
  19. package/dist/core/task-registry-shared.d.ts +0 -1
  20. package/dist/core/tool-policy.d.ts +8 -0
  21. package/dist/core/tool-policy.js +11 -0
  22. package/dist/core/trace.d.ts +0 -2
  23. package/dist/core/types.d.ts +3 -0
  24. package/dist/engine/harness/agent-harness.d.ts +1 -0
  25. package/dist/engine/harness/agent-harness.js +3 -0
  26. package/dist/engine/harness/types.d.ts +1 -0
  27. package/dist/engine/llm/types.d.ts +2 -73
  28. package/dist/engine/loop/agent-loop.js +6 -6
  29. package/dist/engine/loop/types.d.ts +1 -0
  30. package/dist/engine/session/repo-utils.d.ts +1 -2
  31. package/dist/engine/session/repo-utils.js +0 -7
  32. package/dist/index.d.ts +3 -2
  33. package/dist/index.js +2 -1
  34. package/dist/internal/llm.d.ts +1 -1
  35. package/dist/orchestration/workflow.js +14 -5
  36. package/dist/tools/web.js +20 -19
  37. package/package.json +4 -2
@@ -1,5 +1,6 @@
1
1
  export declare const OPENAI_RESERVED: ReadonlySet<string>;
2
2
  export declare const ANTHROPIC_RESERVED: ReadonlySet<string>;
3
+ export declare const RESPONSES_RESERVED: ReadonlySet<string>;
3
4
  export declare function reservedFor(api: string): ReadonlySet<string>;
4
5
  export declare function applyExtraBody(body: Record<string, unknown>, extraBody: Record<string, unknown> | undefined, reserved: ReadonlySet<string>): Record<string, unknown>;
5
6
  export declare function stripAuthHeaders(headers: Record<string, string>): void;
@@ -25,7 +25,23 @@ export const ANTHROPIC_RESERVED = new Set([
25
25
  "output_config",
26
26
  "context_management",
27
27
  ]);
28
+ export const RESPONSES_RESERVED = new Set([
29
+ "model",
30
+ "input",
31
+ "stream",
32
+ "instructions",
33
+ "tools",
34
+ "temperature",
35
+ "max_output_tokens",
36
+ "reasoning",
37
+ "store",
38
+ "previous_response_id",
39
+ "conversation",
40
+ ]);
41
+ const RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses", "openai-chatgpt-responses"]);
28
42
  export function reservedFor(api) {
43
+ if (RESPONSES_APIS.has(api))
44
+ return RESPONSES_RESERVED;
29
45
  return api === "anthropic-messages" ? ANTHROPIC_RESERVED : OPENAI_RESERVED;
30
46
  }
31
47
  export function applyExtraBody(body, extraBody, reserved) {
@@ -1,9 +1,9 @@
1
1
  import { canonicalizeTarget, writeTargetPath } from "../tools/fs/safety.js";
2
- import { PATH_WRITE_TOOLS, isWithin } from "./runner/session-rule-policy.js";
2
+ import { PATH_CONFINABLE_WRITE_TOOLS, isWithin } from "./runner/session-rule-policy.js";
3
3
  const ask = (message) => ({ action: "ask", message, decisionReason: "rule" });
4
4
  export function createFsWriteGatePolicy(opts) {
5
5
  const { env, rootPath, defaultWrite } = opts;
6
- const gated = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"]);
6
+ const gated = PATH_CONFINABLE_WRITE_TOOLS;
7
7
  const acceptDirs = opts.acceptDirs && opts.acceptDirs.length > 0 ? opts.acceptDirs : undefined;
8
8
  const exemptDirs = opts.exemptDirs && opts.exemptDirs.length > 0 ? opts.exemptDirs : undefined;
9
9
  return {
@@ -22,9 +22,10 @@ export declare class LspDiagnosticsRegistry {
22
22
  private readonly pending;
23
23
  private readonly delivered;
24
24
  publish(uri: string, diagnostics: LspDiagnostic[]): void;
25
- fileEdited(uri: string): void;
25
+ fileEdited(runIdent: string, uri: string): void;
26
+ releaseRun(runIdent: string): void;
26
27
  isEmpty(): boolean;
27
- drain(): LspFileDiagnostics[];
28
+ drain(runIdent: string): LspFileDiagnostics[];
28
29
  }
29
30
  export declare function formatDiagnosticsSummary(files: LspFileDiagnostics[]): string;
30
31
  export declare function formatDiagnosticsBlock(files: LspFileDiagnostics[]): string;
@@ -16,30 +16,39 @@ function diagnosticKey(uri, d) {
16
16
  }
17
17
  export class LspDiagnosticsRegistry {
18
18
  pending = new Map();
19
- delivered = new Set();
19
+ delivered = new Map();
20
20
  publish(uri, diagnostics) {
21
21
  if (diagnostics.length === 0)
22
22
  this.pending.delete(uri);
23
23
  else
24
24
  this.pending.set(uri, diagnostics);
25
25
  }
26
- fileEdited(uri) {
26
+ fileEdited(runIdent, uri) {
27
+ const forRun = this.delivered.get(runIdent);
28
+ if (forRun === undefined)
29
+ return;
27
30
  const prefix = `${uri}|`;
28
- for (const key of this.delivered) {
31
+ for (const key of forRun) {
29
32
  if (key.startsWith(prefix))
30
- this.delivered.delete(key);
33
+ forRun.delete(key);
31
34
  }
35
+ if (forRun.size === 0)
36
+ this.delivered.delete(runIdent);
37
+ }
38
+ releaseRun(runIdent) {
39
+ this.delivered.delete(runIdent);
32
40
  }
33
41
  isEmpty() {
34
42
  return this.pending.size === 0;
35
43
  }
36
- drain() {
44
+ drain(runIdent) {
37
45
  const out = [];
38
46
  const requeued = new Map();
47
+ let delivered = this.delivered.get(runIdent);
39
48
  let total = 0;
40
49
  for (const [uri, all] of this.pending) {
41
50
  const fresh = all
42
- .filter((d) => !this.delivered.has(diagnosticKey(uri, d)))
51
+ .filter((d) => delivered?.has(diagnosticKey(uri, d)) !== true)
43
52
  .sort((a, b) => (a.severity ?? 99) - (b.severity ?? 99));
44
53
  if (fresh.length === 0)
45
54
  continue;
@@ -49,8 +58,12 @@ export class LspDiagnosticsRegistry {
49
58
  requeued.set(uri, fresh.slice(take.length));
50
59
  if (take.length === 0)
51
60
  continue;
61
+ if (delivered === undefined) {
62
+ delivered = new Set();
63
+ this.delivered.set(runIdent, delivered);
64
+ }
52
65
  for (const d of take)
53
- this.delivered.add(diagnosticKey(uri, d));
66
+ delivered.add(diagnosticKey(uri, d));
54
67
  total += take.length;
55
68
  out.push({ uri, diagnostics: take });
56
69
  }
@@ -59,6 +59,7 @@ export interface ResultFlags {
59
59
  unpricedSpend?: boolean;
60
60
  rewindNotes?: TaskResult["rewindNotes"];
61
61
  remoteEnvFailures?: TaskResult["remoteEnvFailures"];
62
+ retryAfterMs?: number;
62
63
  abortedForTimeout: boolean;
63
64
  abortedForTurns: boolean;
64
65
  abortedLive?: boolean;
@@ -1,5 +1,14 @@
1
1
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
+ const SALVAGE_ELIGIBLE_TERMINALS = new Set([
4
+ "output.degenerate",
5
+ "limits.max_tokens_exceeded",
6
+ "limits.max_cost_exceeded",
7
+ "limits.max_turns_exceeded",
8
+ "limits.max_walltime_exceeded",
9
+ "env.lifetime_expired",
10
+ "usage.window_exhausted",
11
+ ]);
3
12
  export function errorCodeOf(err) {
4
13
  let cur = err;
5
14
  let fallback;
@@ -63,7 +72,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
63
72
  status = "failed";
64
73
  errorCode = "output.degenerate";
65
74
  errorMessage = final?.errorMessage;
66
- salvagedOutput = text.trim() || undefined;
67
75
  }
68
76
  else if (flags.budgetHit) {
69
77
  status = "failed";
@@ -73,7 +81,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
73
81
  flags.budgetHit === "precall"
74
82
  ? `the estimated cost of the first call already exceeds ${axisName}; the task was not started`
75
83
  : `cumulative usage exceeded ${axisName}`;
76
- salvagedOutput = text.trim() || undefined;
77
84
  }
78
85
  else if (flags.suspendLoop) {
79
86
  status = "failed";
@@ -90,8 +97,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
90
97
  : flags.conflict
91
98
  ? "conflict"
92
99
  : errorCodeOf(flags.threw);
93
- if (flags.abortedForTimeout || flags.abortedForTurns)
94
- salvagedOutput = text.trim() || undefined;
95
100
  }
96
101
  else if (flags.blockedReason) {
97
102
  status = "blocked";
@@ -114,8 +119,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
114
119
  status = "failed";
115
120
  errorCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
116
121
  errorMessage = final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted");
117
- if (flags.abortedForTimeout || flags.abortedForTurns)
118
- salvagedOutput = text.trim() || undefined;
119
122
  }
120
123
  else if (!final) {
121
124
  status = "failed";
@@ -139,9 +142,13 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
139
142
  else {
140
143
  status = "completed";
141
144
  }
145
+ if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
146
+ salvagedOutput = text.trim() || undefined;
147
+ }
148
+ const retryAfterMs = errorCode === "usage.window_exhausted" ? flags.retryAfterMs : undefined;
142
149
  const { compactionMicroUsd: _internalCompaction, ...publicStats } = stats;
143
150
  void _internalCompaction;
144
151
  if (flags.unpricedSpend)
145
152
  delete publicStats.costMicroUsd;
146
- return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), stats: publicStats };
153
+ return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), stats: publicStats };
147
154
  }
@@ -199,6 +199,7 @@ export interface Prepared {
199
199
  lspDiagnostics?: {
200
200
  registry: import("../lsp-diagnostics.js").LspDiagnosticsRegistry;
201
201
  nudge: (rawPath: string) => void;
202
+ runIdent: string;
202
203
  };
203
204
  planModeRef: {
204
205
  active: boolean;
@@ -17,7 +17,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
17
17
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
18
18
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
19
19
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
20
- import { combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
20
+ import { askApproverIdentity, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
21
21
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
22
22
  import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentListingDelta } from "./turn-attachments.js";
23
23
  import { inlineUntrusted } from "../untrusted-text.js";
@@ -944,6 +944,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
944
944
  model: harnessRef.current?.getModel(),
945
945
  thinkingLevel: harnessRef.current?.getThinkingLevel(),
946
946
  principal: spec.principal,
947
+ ...(frozenOnAsk !== undefined ? { onAsk: frozenOnAsk } : {}),
947
948
  oneShot: spec.oneShot,
948
949
  clientContext: spec.clientContext,
949
950
  excludeTools: toolFaceSnapshot.exclude,
@@ -1562,11 +1563,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1562
1563
  const lspDiagnostics = spec.lspDiagnostics !== false && lspManager?.diagnostics !== undefined && handsEnabled && spec.handsReadOnly !== true
1563
1564
  ? lspManager.diagnostics
1564
1565
  : undefined;
1566
+ const lspRunIdent = sessionId;
1565
1567
  const nudgeLspOnEdit = lspDiagnostics
1566
1568
  ? (rawPath) => {
1567
1569
  const baseDir = handsCwdRef?.current ?? taskRootPath;
1568
1570
  const filePath = resolveLspPath(rawPath, baseDir);
1569
- lspDiagnostics.fileEdited(pathToUri(filePath));
1571
+ lspDiagnostics.fileEdited(lspRunIdent, pathToUri(filePath));
1570
1572
  void lspManager
1571
1573
  .sessionFor(filePath, undefined, executionEnv)
1572
1574
  .then((session) => session?.notifyFileChanged?.(filePath))
@@ -2203,6 +2205,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2203
2205
  ? { legDate: envFacts.date, today: () => formatLocalDate(new Date(), tzValid ? userTz : undefined) }
2204
2206
  : undefined;
2205
2207
  const harness = new AgentHarness({
2208
+ abortResultDetails: () => suspendRef.token !== undefined || reviewRef.token !== undefined ? { code: "gate.parked" } : undefined,
2206
2209
  ...(spec.limits?.maxOutputTokens !== undefined && spec.limits.maxOutputTokens > 0
2207
2210
  ? { maxOutputTokens: spec.limits.maxOutputTokens }
2208
2211
  : {}),
@@ -2774,7 +2777,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2774
2777
  }
2775
2778
  if (decision.decisionReason === undefined || decision.decisionReason === "rule") {
2776
2779
  const grant = inheritedAskGrants.get(req.toolCallId);
2777
- if (grant !== undefined && grant.approver === onAsk && grant.argsJson === askGrantShapeOf(req.args)) {
2780
+ if (grant !== undefined && askApproverIdentity(grant.approver) === askApproverIdentity(onAsk) && grant.argsJson === askGrantShapeOf(req.args)) {
2778
2781
  inheritedAskGrants.delete(req.toolCallId);
2779
2782
  humanReviewRef.count += 1;
2780
2783
  humanReviewRef.totalWaitMs += grant.waitMs;
@@ -3599,7 +3602,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3599
3602
  : undefined;
3600
3603
  overheadState.promptChars = systemPrompt.length;
3601
3604
  const preparedHolder = {};
3602
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3605
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3603
3606
  const prepared = buildPrepared();
3604
3607
  preparedHolder.current = prepared;
3605
3608
  return prepared;
@@ -113,6 +113,16 @@ function resumeDecisionWasNegative(resume) {
113
113
  }
114
114
  const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
115
115
  "NOT executed on resume. If you still need it, issue it again now.";
116
+ function toolEndBodyFrom(result, isError) {
117
+ const o = toolOutputFrom(result);
118
+ const st = structuredFrom(result);
119
+ const code = isError ? result?.details?.code : undefined;
120
+ return {
121
+ ...(o !== undefined ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) } : {}),
122
+ ...(st !== undefined ? { structured: st } : {}),
123
+ ...(typeof code === "string" ? { errorCode: code } : {}),
124
+ };
125
+ }
116
126
  function deepJsonEqual(a, b) {
117
127
  if (a === b)
118
128
  return true;
@@ -380,7 +390,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
380
390
  emitTrace(rs.telemetry.tracer, () => ({ kind: "review.dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
381
391
  }
382
392
  if (prepared.lspDiagnostics && !prepared.lspDiagnostics.registry.isEmpty() && !prepared.abortController.signal.aborted) {
383
- const files = prepared.lspDiagnostics.registry.drain();
393
+ const files = prepared.lspDiagnostics.registry.drain(prepared.lspDiagnostics.runIdent);
384
394
  if (files.length > 0) {
385
395
  queue.push({ type: "diagnostics", files, isNew: true, ...ident() });
386
396
  const block = formatDiagnosticsBlock(files);
@@ -1049,16 +1059,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1049
1059
  toolName: event.toolName,
1050
1060
  ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
1051
1061
  isError: event.isError,
1052
- ...(() => {
1053
- const o = toolOutputFrom(event.result);
1054
- return o !== undefined
1055
- ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) }
1056
- : {};
1057
- })(),
1058
- ...(() => {
1059
- const st = structuredFrom(event.result);
1060
- return st !== undefined ? { structured: st } : {};
1061
- })(),
1062
+ ...toolEndBodyFrom(event.result, event.isError),
1062
1063
  ...ident(),
1063
1064
  });
1064
1065
  announceWorkspaceMove();
@@ -2446,7 +2447,7 @@ export class Runner {
2446
2447
  if (resume) {
2447
2448
  const walltimeExhaustedResume = effectiveTimeoutMs !== undefined && effectiveTimeoutMs <= 0;
2448
2449
  if (!walltimeExhaustedResume && resume.outcome.gate !== "wake") {
2449
- await this.applyResumeDecision(prepared, resume, (e) => queue.push(e), emitCommitted, (toolName, details) => {
2450
+ await this.applyResumeDecision(prepared, resume, (e) => pushContent({ ...e, ...ident() }), emitCommitted, (toolName, details) => {
2450
2451
  if (rs.attach.attachState === undefined)
2451
2452
  return;
2452
2453
  const family = writeFamilyOf(toolName);
@@ -2638,6 +2639,7 @@ export class Runner {
2638
2639
  }
2639
2640
  finally {
2640
2641
  timeout.clear();
2642
+ prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
2641
2643
  prepared.abortController.abort();
2642
2644
  prepared.releaseSignal();
2643
2645
  try {
@@ -2779,6 +2781,7 @@ export class Runner {
2779
2781
  unpricedSpend: rs.telemetry.unpricedSpend,
2780
2782
  rewindNotes: prepared.rewindNotes,
2781
2783
  remoteEnvFailures: prepared.remoteEnvFailures,
2784
+ retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
2782
2785
  abortedForTimeout: timeout.fired,
2783
2786
  abortedForTurns: rs.limits.turnsExceeded,
2784
2787
  abortedLive,
@@ -3366,7 +3369,7 @@ export class Runner {
3366
3369
  }
3367
3370
  else if (!completed.has(id)) {
3368
3371
  const name = names.get(id) ?? "unknown";
3369
- emit({ type: "tool_end", toolCallId: id, toolName: name, ...((() => { const l = prepared.tools.find((t) => t.name === name)?.label; return l !== undefined && l !== name ? { label: l } : {}; })()), isError: true });
3372
+ emit({ type: "tool_end", toolCallId: id, toolName: name, ...((() => { const l = prepared.tools.find((t) => t.name === name)?.label; return l !== undefined && l !== name ? { label: l } : {}; })()), isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE) }, true) });
3370
3373
  const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE), true));
3371
3374
  emitCommitted(eid, "toolResult", id);
3372
3375
  }
@@ -3380,10 +3383,10 @@ export class Runner {
3380
3383
  const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput ?? pendingAction.args);
3381
3384
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
3382
3385
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
3383
- const emitEnd = (isError) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError });
3386
+ const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
3384
3387
  if (outcome.decision === "deny") {
3385
- emitEnd(true);
3386
3388
  const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason) : `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
3389
+ emitEnd(true, { content: formatHookFeedback(reason) });
3387
3390
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason), true));
3388
3391
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3389
3392
  return;
@@ -3391,8 +3394,9 @@ export class Runner {
3391
3394
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3392
3395
  const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3393
3396
  if (rechecked.action === "deny") {
3394
- emitEnd(true);
3395
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.message ? `: ${rechecked.message}` : ""}.`), true));
3397
+ const editedDenial = formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.message ? `: ${rechecked.message}` : ""}.`);
3398
+ emitEnd(true, { content: editedDenial });
3399
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, editedDenial, true));
3396
3400
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3397
3401
  return;
3398
3402
  }
@@ -3400,8 +3404,9 @@ export class Runner {
3400
3404
  if (prepared.denyNarrowingPolicy) {
3401
3405
  const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3402
3406
  if (narrowed.action === "deny") {
3403
- emitEnd(true);
3404
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`), true));
3407
+ const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`);
3408
+ emitEnd(true, { content: narrowedDenial });
3409
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, narrowedDenial, true));
3405
3410
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3406
3411
  return;
3407
3412
  }
@@ -3419,13 +3424,14 @@ export class Runner {
3419
3424
  res = await tool.execute(pendingAction.toolCallId, args, prepared.abortController.signal);
3420
3425
  }
3421
3426
  catch (err) {
3422
- emitEnd(true);
3423
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, `Error: ${err instanceof Error ? err.message : String(err)}`, true));
3427
+ const execError = `Error: ${err instanceof Error ? err.message : String(err)}`;
3428
+ emitEnd(true, { content: execError });
3429
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, execError, true));
3424
3430
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3425
3431
  return;
3426
3432
  }
3427
3433
  const executedIsError = res.isError === true;
3428
- emitEnd(executedIsError);
3434
+ emitEnd(executedIsError, res);
3429
3435
  onResolvedToolSuccess?.(pendingAction.toolName, executedIsError ? undefined : res.details);
3430
3436
  const eid = await prepared.session.appendMessage({
3431
3437
  role: "toolResult",
@@ -3,6 +3,7 @@ import type { ToolEffect } from "../types.js";
3
3
  import { type NamedToolPolicy } from "../tool-policy.js";
4
4
  import type { SessionPermissionRules } from "../session-policy-store.js";
5
5
  export declare const PATH_WRITE_TOOLS: ReadonlySet<string>;
6
+ export declare const PATH_CONFINABLE_WRITE_TOOLS: ReadonlySet<string>;
6
7
  export declare function isWithin(root: string, p: string): boolean;
7
8
  export declare function createSessionRulePolicy(rules: SessionPermissionRules, opts: {
8
9
  env: ExecutionEnv;
@@ -1,7 +1,8 @@
1
- import { canonicalizeTarget, fileArgPath } from "../../tools/fs/safety.js";
1
+ import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
2
2
  import { isWinFormPath } from "../../tools/fs/safety.js";
3
3
  import { createCoarseCommandNamePolicy } from "../tool-policy.js";
4
4
  export const PATH_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
5
+ export const PATH_CONFINABLE_WRITE_TOOLS = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"]);
5
6
  export function isWithin(root, p) {
6
7
  if (!root)
7
8
  return false;
@@ -46,14 +47,14 @@ export function createSessionRulePolicy(rules, opts) {
46
47
  return d;
47
48
  }
48
49
  if (allowDirs) {
49
- if (!PATH_WRITE_TOOLS.has(toolName)) {
50
+ if (!PATH_CONFINABLE_WRITE_TOOLS.has(toolName)) {
50
51
  const eff = toolEffects?.get(toolName) ?? "write";
51
52
  if (eff !== "read") {
52
53
  return deny(`write-capable tool "${req.toolName}" denied: session rule confines writes to allowDirs but this tool cannot be path-confined`);
53
54
  }
54
55
  }
55
56
  else {
56
- const path = fileArgPath(req.args);
57
+ const path = writeTargetPath(toolName, req.args);
57
58
  if (typeof path !== "string" || path.length === 0) {
58
59
  return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the call has no resolvable path`);
59
60
  }
@@ -40,7 +40,6 @@ export interface SemaTaskHandle {
40
40
  createdAt: number;
41
41
  updatedAt: number;
42
42
  outputFile?: string;
43
- outputOffset?: number;
44
43
  completionId?: string;
45
44
  }
46
45
  export declare function mintCompletionId(target: {
@@ -70,6 +70,11 @@ export declare function createTranscriptIntegrityPolicy(opts?: {
70
70
  readAllow?: readonly string[];
71
71
  tools?: string[];
72
72
  }): ToolPolicy;
73
+ export interface AskDelegationProvenance {
74
+ readonly parentToolCallId: string;
75
+ readonly depth: number;
76
+ readonly agentName?: string;
77
+ }
73
78
  export interface AskRequest {
74
79
  toolName: string;
75
80
  toolCallId: string;
@@ -81,12 +86,15 @@ export interface AskRequest {
81
86
  readonly fromSubagent?: true;
82
87
  readonly sourceAgentName?: string;
83
88
  readonly requiresRealApproval?: boolean;
89
+ readonly delegation?: AskDelegationProvenance;
84
90
  }
85
91
  export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal) => AskOutcome | Promise<AskOutcome>);
86
92
  export type AskOutcome = boolean | "unavailable" | {
87
93
  allow: boolean;
88
94
  updatedInput?: unknown;
89
95
  };
96
+ export declare function withDelegationProvenance(onAsk: OnAsk, delegation: AskDelegationProvenance): OnAsk;
97
+ export declare function askApproverIdentity(onAsk: unknown): unknown;
90
98
  export type ResolvedAsk = PermissionResult & {
91
99
  approverUnavailable?: true;
92
100
  presentedInput?: unknown;
@@ -516,6 +516,17 @@ export function createTranscriptIntegrityPolicy(opts) {
516
516
  },
517
517
  };
518
518
  }
519
+ const delegatedApproverRoot = new WeakMap();
520
+ export function withDelegationProvenance(onAsk, delegation) {
521
+ if (typeof onAsk !== "function")
522
+ return onAsk;
523
+ const wrapper = (req, signal) => req.delegation !== undefined ? onAsk(req, signal) : onAsk({ ...req, delegation }, signal);
524
+ delegatedApproverRoot.set(wrapper, askApproverIdentity(onAsk));
525
+ return wrapper;
526
+ }
527
+ export function askApproverIdentity(onAsk) {
528
+ return typeof onAsk === "function" ? (delegatedApproverRoot.get(onAsk) ?? onAsk) : onAsk;
529
+ }
519
530
  function tryCloneArgs(v) {
520
531
  try {
521
532
  const value = structuredClone(v);
@@ -134,8 +134,6 @@ export type TraceEvent = {
134
134
  firstTokenMs?: number;
135
135
  callStartedAt?: number;
136
136
  costMicroUsd?: number;
137
- callCap?: number;
138
- capThinkingSkipped?: boolean;
139
137
  stopReason?: string;
140
138
  ts: number;
141
139
  } | {
@@ -98,6 +98,7 @@ export interface ToolExecuteContext {
98
98
  model?: Model;
99
99
  thinkingLevel?: ThinkingLevel;
100
100
  principal?: string;
101
+ onAsk?: import("./tool-policy.js").OnAsk;
101
102
  oneShot?: boolean;
102
103
  clientContext?: TaskSpec["clientContext"];
103
104
  excludeTools?: readonly string[];
@@ -410,6 +411,7 @@ export interface TaskResult {
410
411
  remoteEnvFailures?: RemoteEnvFailureNote[];
411
412
  errorMessage?: string;
412
413
  errorCode?: string;
414
+ retryAfterMs?: number;
413
415
  degraded?: {
414
416
  from: string;
415
417
  to: string;
@@ -523,6 +525,7 @@ export type TaskEvent = ({
523
525
  isError: boolean;
524
526
  output?: unknown;
525
527
  structured?: unknown;
528
+ errorCode?: string;
526
529
  truncated?: boolean;
527
530
  totalChars?: number;
528
531
  } & TaskEventIdentity) | ({
@@ -50,6 +50,7 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
50
50
  private maxOutputTokens?;
51
51
  private maxOutputTokensPerCall?;
52
52
  private stallTimeoutsPerCall?;
53
+ private abortResultDetails?;
53
54
  private loopTrace?;
54
55
  private resilience?;
55
56
  private maxToolConcurrency?;
@@ -186,6 +186,7 @@ export class AgentHarness {
186
186
  maxOutputTokens;
187
187
  maxOutputTokensPerCall;
188
188
  stallTimeoutsPerCall;
189
+ abortResultDetails;
189
190
  loopTrace;
190
191
  resilience;
191
192
  maxToolConcurrency;
@@ -216,6 +217,7 @@ export class AgentHarness {
216
217
  this.maxOutputTokens = options.maxOutputTokens;
217
218
  this.maxOutputTokensPerCall = options.maxOutputTokensPerCall;
218
219
  this.stallTimeoutsPerCall = options.stallTimeoutsPerCall;
220
+ this.abortResultDetails = options.abortResultDetails;
219
221
  this.loopTrace = options.loopTrace;
220
222
  this.resilience = options.resilience;
221
223
  this.maxToolConcurrency = options.maxToolConcurrency;
@@ -440,6 +442,7 @@ export class AgentHarness {
440
442
  ...(this.maxOutputTokens !== undefined ? { maxTokens: this.maxOutputTokens } : {}),
441
443
  ...(this.maxOutputTokensPerCall !== undefined ? { maxTokensPerCall: this.maxOutputTokensPerCall } : {}),
442
444
  ...(this.stallTimeoutsPerCall !== undefined ? { stallTimeoutsPerCall: this.stallTimeoutsPerCall } : {}),
445
+ ...(this.abortResultDetails !== undefined ? { abortResultDetails: this.abortResultDetails } : {}),
443
446
  ...(this.resilience !== undefined ? { resilience: this.resilience } : {}),
444
447
  ...(this.maxToolConcurrency !== undefined ? { maxToolConcurrency: this.maxToolConcurrency } : {}),
445
448
  ...(this.streamingToolExecution === true && (this.getHandlers("tool_call")?.size ?? 0) === 0
@@ -542,6 +542,7 @@ export interface AgentHarnessOptions<TSkill extends Skill = Skill, TPromptTempla
542
542
  maxOutputTokens?: number;
543
543
  maxOutputTokensPerCall?: () => number | undefined;
544
544
  stallTimeoutsPerCall?: () => import("../llm/types.js").StallTimeouts | undefined;
545
+ abortResultDetails?: () => Record<string, unknown> | undefined;
545
546
  loopTrace?: (step: import("../loop/agent-loop.js").LoopStep) => void;
546
547
  resilience?: import("../llm/types.js").ResilienceOptions;
547
548
  maxToolConcurrency?: number;