@sema-agent/core 5.13.0 → 5.14.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 (86) hide show
  1. package/CHANGELOG.md +296 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +4 -0
  4. package/dist/agents/subagent.js +133 -41
  5. package/dist/brain/anthropic.js +33 -10
  6. package/dist/brain/context-overflow.d.ts +20 -0
  7. package/dist/brain/context-overflow.js +58 -0
  8. package/dist/brain/open-responses.js +24 -10
  9. package/dist/brain/openai.js +29 -11
  10. package/dist/brain/request-params.d.ts +2 -0
  11. package/dist/brain/request-params.js +16 -0
  12. package/dist/brain/stream-engine.d.ts +9 -1
  13. package/dist/brain/stream-engine.js +256 -27
  14. package/dist/brain/timeout.d.ts +1 -0
  15. package/dist/brain/timeout.js +1 -0
  16. package/dist/core/a2a.d.ts +2 -2
  17. package/dist/core/a2a.js +3 -3
  18. package/dist/core/ask-question.d.ts +47 -2
  19. package/dist/core/ask-question.js +209 -28
  20. package/dist/core/background-agent-store.d.ts +2 -0
  21. package/dist/core/checkpoint-store.d.ts +41 -17
  22. package/dist/core/checkpoint-store.js +114 -3
  23. package/dist/core/hooks.d.ts +24 -2
  24. package/dist/core/hooks.js +97 -10
  25. package/dist/core/human-input-projection.d.ts +12 -0
  26. package/dist/core/human-input-projection.js +27 -0
  27. package/dist/core/mcp.d.ts +7 -2
  28. package/dist/core/mcp.js +7 -7
  29. package/dist/core/memory-admission.d.ts +4 -0
  30. package/dist/core/memory-admission.js +3 -0
  31. package/dist/core/runner/assemble-result.d.ts +1 -0
  32. package/dist/core/runner/assemble-result.js +1 -1
  33. package/dist/core/runner/prepare-task.d.ts +16 -6
  34. package/dist/core/runner/prepare-task.js +301 -24
  35. package/dist/core/runner/runtask.d.ts +3 -6
  36. package/dist/core/runner/runtask.js +186 -36
  37. package/dist/core/runner/tool-output-projection.js +1 -0
  38. package/dist/core/session-store.d.ts +3 -0
  39. package/dist/core/session-store.js +4 -0
  40. package/dist/core/session.d.ts +1 -0
  41. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  42. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  43. package/dist/core/task-notification.d.ts +2 -0
  44. package/dist/core/task-notification.js +5 -3
  45. package/dist/core/task-registry-agent.d.ts +1 -0
  46. package/dist/core/task-registry-agent.js +6 -0
  47. package/dist/core/task-registry.d.ts +1 -0
  48. package/dist/core/task-registry.js +4 -1
  49. package/dist/core/tool-policy.d.ts +5 -0
  50. package/dist/core/tool-policy.js +2 -1
  51. package/dist/core/types.d.ts +32 -1
  52. package/dist/core/wiring-manifest.d.ts +97 -0
  53. package/dist/core/wiring-manifest.js +186 -0
  54. package/dist/engine/compaction/compaction.js +2 -2
  55. package/dist/engine/harness/agent-harness.d.ts +2 -1
  56. package/dist/engine/harness/agent-harness.js +8 -1
  57. package/dist/engine/harness/types.d.ts +3 -1
  58. package/dist/engine/llm/types.d.ts +7 -0
  59. package/dist/engine/llm/types.js +8 -1
  60. package/dist/engine/session/import-validate.d.ts +6 -1
  61. package/dist/engine/session/import-validate.js +29 -6
  62. package/dist/engine/session/memory-repo.d.ts +3 -1
  63. package/dist/engine/session/memory-repo.js +2 -2
  64. package/dist/index.d.ts +7 -4
  65. package/dist/index.js +7 -4
  66. package/dist/internal/harness-types.d.ts +1 -1
  67. package/dist/internal/llm.d.ts +2 -2
  68. package/dist/internal/llm.js +1 -1
  69. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  70. package/dist/orchestration/run-workflow-tool.js +3 -0
  71. package/dist/orchestration/workflow-types.d.ts +8 -0
  72. package/dist/orchestration/workflow-types.js +14 -0
  73. package/dist/orchestration/workflow.d.ts +4 -0
  74. package/dist/orchestration/workflow.js +134 -5
  75. package/dist/prompts/default.js +1 -1
  76. package/dist/stores/file/checkpoint-store.d.ts +3 -5
  77. package/dist/stores/file/checkpoint-store.js +31 -2
  78. package/dist/stores/file/index.js +1 -1
  79. package/dist/stores/file/session-store.d.ts +3 -1
  80. package/dist/stores/file/session-store.js +2 -2
  81. package/dist/stores/file/shared-ledger.js +8 -1
  82. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  83. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  84. package/dist/tools/fs/fs-bash.js +31 -12
  85. package/dist/tools/fs/safety.js +34 -10
  86. package/package.json +1 -1
@@ -17,6 +17,7 @@ import { uuidv7 } from "../internal/harness.js";
17
17
  import { addWorktree } from "../core/git-worktree-env.js";
18
18
  import { shellQuote } from "../tools/fs/search.js";
19
19
  import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
20
+ import { readDurableOrgAdmission } from "../core/memory-admission.js";
20
21
  import { extractErrorCode } from "../brain/errors.js";
21
22
  import { governanceRetryClass } from "../core/governance-codes.js";
22
23
  import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
@@ -53,6 +54,39 @@ export function notifyResultField(result) {
53
54
  ? `${result.slice(0, BG_AGENT_NOTIFY_RESULT_MAX)}\n[result truncated: ${result.length} chars total — call TaskOutput for the full text]`
54
55
  : result;
55
56
  }
57
+ const HANDBACK_ASK_MESSAGE = "Subagent has finished and is handing back control to the main agent. Review the subagent's work and flag if any action may violate security policy.";
58
+ async function reviewHandback(opts) {
59
+ const { review, evidence, signal } = opts;
60
+ if (review === undefined)
61
+ return undefined;
62
+ if (signal?.aborted === true)
63
+ return undefined;
64
+ if (!evidence.result && !evidence.steps && !evidence.edits && !evidence.partialText)
65
+ return undefined;
66
+ const verdict = await review.decider
67
+ .decide({
68
+ req: {
69
+ toolName: opts.toolName,
70
+ args: {
71
+ result: evidence.result?.slice(0, 2_000),
72
+ toolSteps: evidence.steps,
73
+ editedFiles: evidence.edits,
74
+ ...(evidence.partialText ? { partialFindings: evidence.partialText.slice(0, 2_000) } : {}),
75
+ },
76
+ toolCallId: opts.toolCallId,
77
+ },
78
+ askMessage: HANDBACK_ASK_MESSAGE,
79
+ }, signal)
80
+ .catch(() => ({ kind: "unavailable", cause: "error" }));
81
+ if (verdict.kind !== "block")
82
+ return undefined;
83
+ return `SECURITY WARNING: This subagent performed actions that may violate security policy. Reason: ${inlineUntrusted(verdict.reason ?? "(no reason given)")}. Review the subagent's actions carefully before acting on its output.`;
84
+ }
85
+ function withHandbackWarning(warning, result) {
86
+ if (warning === undefined)
87
+ return result;
88
+ return result ? `${warning}\n\n${result}` : warning;
89
+ }
56
90
  export function inheritedManifestScopeFor(snapshot) {
57
91
  if (!snapshot || snapshot.length === 0)
58
92
  return undefined;
@@ -251,6 +285,7 @@ export function completedAgentCard(child, extras) {
251
285
  ...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}),
252
286
  ...(child.retryAfterMs !== undefined ? { retryAfterMs: child.retryAfterMs } : {}),
253
287
  ...(child.degraded !== undefined ? { degraded: child.degraded } : {}),
288
+ ...(extras.handbackWarning !== undefined ? { handbackWarning: extras.handbackWarning } : {}),
254
289
  ...(extras.error ? { error_kind: extras.error.errorKind, retryable: extras.error.retryable } : {}),
255
290
  stats: child.stats,
256
291
  ...(child.model !== undefined ? { model: child.model, resolvedModel: child.model } : {}),
@@ -286,7 +321,7 @@ function durablePauseDelegationReport(reportLabel, child) {
286
321
  function unparkedDurablePauseReason(d) {
287
322
  return d.kind === "no_park_lane"
288
323
  ? `the agent hit an approval gate and paused durably, but this delegation cannot be parked ` +
289
- `(parking needs an explicitly named child plus a durable agent row, a checkpoint store and the ` +
324
+ `(parking needs a durable agent row, a checkpoint store and the ` +
290
325
  `durable-session capability). Its checkpoint ${d.checkpointToken} is still committed and the ` +
291
326
  `approval must be resolved through the deployment's approval channel; the gated action never ran.`
292
327
  : `the agent hit an approval gate and paused durably, but the park did not take (` +
@@ -297,6 +332,10 @@ function unparkedDurablePauseReason(d) {
297
332
  function errorKindClause(c) {
298
333
  return c !== undefined ? ` (error_kind: ${c.errorKind}, retryable: ${c.retryable})` : "";
299
334
  }
335
+ function stripSpawnTurnQuestionFace(spec) {
336
+ const { onQuestion: spawnTurnQuestionFace, ...rest } = spec;
337
+ return { spec: rest, internalsFlag: spawnTurnQuestionFace !== undefined ? { questionFaceStripped: true } : {} };
338
+ }
300
339
  const FAILED_SESSION_RETAIN_TTL_MS = 15 * 60 * 1000;
301
340
  const PARTIAL_FINDINGS_MAX_CHARS = 1200;
302
341
  const BG_NOTIFY_DRAIN_WINDOW_MS = 2_000;
@@ -503,6 +542,13 @@ export function createSubagentResume(deps) {
503
542
  }
504
543
  catch {
505
544
  }
545
+ try {
546
+ const resumedOrgVerdict = entry.internalsSnapshot.ownOrgAdmissionRef?.current;
547
+ if (resumedOrgVerdict !== undefined)
548
+ deps.registry.recordBackgroundAgentOrgAdmission(deps.taskId, resumedOrgVerdict);
549
+ }
550
+ catch {
551
+ }
506
552
  },
507
553
  }
508
554
  : {}),
@@ -530,6 +576,14 @@ export function createSubagentResume(deps) {
530
576
  void _;
531
577
  const child = await stream.result();
532
578
  let status = child.status === "completed" ? "completed" : abort.signal.aborted ? "killed" : "failed";
579
+ const resumeHandbackWarning = await reviewHandback({
580
+ review: deps.currentAutoModeReview,
581
+ toolCallId: deps.parentToolCallId,
582
+ toolName: "Agent(handback)",
583
+ evidence: { result: child.result, steps: resumeStepRecorder.recentSteps(), edits: resumeStepRecorder.editedFiles() },
584
+ signal: abort.signal,
585
+ });
586
+ const resumeHandbackResult = withHandbackWarning(resumeHandbackWarning, child.result);
533
587
  const errCodeRevive = status === "failed" ? (child.errorCode ?? extractErrorCode(child.errorMessage)) : undefined;
534
588
  const errClassRevive = status === "failed"
535
589
  ? classifySubagentError({ status: "failed", ...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}), ...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}) })
@@ -538,7 +592,7 @@ export function createSubagentResume(deps) {
538
592
  try {
539
593
  const winner = deps.registry.settleRevivedAgent(deps.taskId, reviveCycle, {
540
594
  status,
541
- ...resultSettleFields(child.result),
595
+ ...resultSettleFields(resumeHandbackResult),
542
596
  ...(status !== "completed" ? { error: (child.errorMessage ?? `resumed run ${status}`).slice(0, 500) } : {}),
543
597
  ...(errCodeRevive !== undefined ? { errorCode: errCodeRevive } : {}),
544
598
  ...(errClassRevive !== undefined ? { retryable: errClassRevive.retryable, errorKind: errClassRevive.errorKind } : {}),
@@ -585,7 +639,7 @@ export function createSubagentResume(deps) {
585
639
  summary: reviveTerminalSummary,
586
640
  ...(failReasonRevive !== undefined ? { error: failReasonRevive.slice(0, REPORT_FIELD_MAX) } : {}),
587
641
  ...(status === "failed" && errCodeRevive !== undefined ? { errorCode: errCodeRevive } : {}),
588
- ...(child.result ? { result: notifyResultField(child.result) } : {}),
642
+ ...(resumeHandbackResult ? { result: notifyResultField(resumeHandbackResult) } : {}),
589
643
  ...(status === "killed" && child.result ? { partial: true } : {}),
590
644
  ...resumeResidual(),
591
645
  resumable: status !== "killed",
@@ -1304,7 +1358,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1304
1358
  const runObserverLeg = async (spec) => {
1305
1359
  if (!isObserverTaskId(sid))
1306
1360
  throw new ObserverStoppedByUserError("observer identity revoked — delivery refused");
1307
- const r = await opts.runner.runTask(spec);
1361
+ const r = await opts.runner.runTask(spec, ctx.interactionPosture !== undefined ? { parentInteractionPosture: ctx.interactionPosture } : undefined);
1308
1362
  if (r.status !== "completed") {
1309
1363
  if (r.errorCode === "resume.session_not_found") {
1310
1364
  throw new ObserverResumeStateError(`observer session no longer exists${r.errorMessage ? `: ${r.errorMessage}` : ""}`);
@@ -1443,8 +1497,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1443
1497
  ...(inheritedManifestScope ? { inheritedManifestScope } : {}),
1444
1498
  ...(treeScope !== undefined ? { registryScope: treeScope } : {}),
1445
1499
  ...(ctx.inheritedGateForChildren ? { inheritedGate: ctx.inheritedGateForChildren() } : {}),
1500
+ ownOrgAdmissionRef: { current: reviveClaim !== undefined ? readDurableOrgAdmission(reviveClaim.row) : undefined },
1446
1501
  ...(childDefaultPersona !== undefined ? { defaultSystemPrompt: childDefaultPersona } : {}),
1447
1502
  isDelegatedChild: true,
1503
+ ...(ctx.interactionPosture !== undefined ? { parentInteractionPosture: ctx.interactionPosture } : {}),
1448
1504
  parentToolCallId: ctx.toolCallId,
1449
1505
  ...(childAgentName ? { agentName: childAgentName } : {}),
1450
1506
  ...(agentName !== undefined ? { explicitAgentName: agentName } : {}),
@@ -1513,11 +1569,12 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1513
1569
  limits: childLimits,
1514
1570
  signal,
1515
1571
  });
1516
- const tryRetainChild = async (ledger, entryTtlMs, preMintedSessionId, durableProbe, releaseAnchor) => {
1572
+ const tryRetainChild = async (ledger, entryTtlMs, preMintedSessionId, durableProbe, releaseAnchor, internalsExtra) => {
1517
1573
  if (!ledger)
1518
1574
  return undefined;
1519
1575
  const childSessionId = preMintedSessionId ?? uuidv7();
1520
- const { signal: _spawnSignal, onQuestion: _spawnTurnQuestionFace, ...plainSpec } = buildChildSpec(undefined);
1576
+ const retainStrip = stripSpawnTurnQuestionFace(buildChildSpec(undefined));
1577
+ const { signal: _spawnSignal, ...plainSpec } = retainStrip.spec;
1521
1578
  let entry = ledger.register(ctx.toolCallId, {
1522
1579
  childSessionId,
1523
1580
  runner: opts.runner,
@@ -1534,7 +1591,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1534
1591
  ...(plainSpec.deferTools !== undefined ? { deferTools: [...plainSpec.deferTools] } : {}),
1535
1592
  ...(plainSpec.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...plainSpec.alwaysLoadTools] } : {}),
1536
1593
  }),
1537
- internalsSnapshot: { ...childInternals },
1594
+ internalsSnapshot: { ...childInternals, ...retainStrip.internalsFlag, ...(internalsExtra ?? {}) },
1538
1595
  release: async () => {
1539
1596
  try {
1540
1597
  await opts.runner.sessions.unpin?.(childSessionId);
@@ -1597,14 +1654,14 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1597
1654
  if (ledger !== undefined && !ledger.disposed) {
1598
1655
  if (opts.background)
1599
1656
  ensureSessionReapHook(opts.background.registry);
1600
- const { signal: _spawnSignal, onQuestion: _spawnTurnQuestionFace, ...plainSpec } = buildChildSpec(undefined);
1657
+ const failedRetainStrip = stripSpawnTurnQuestionFace(buildChildSpec(undefined));
1658
+ const { signal: _spawnSignal, ...plainSpec } = failedRetainStrip.spec;
1601
1659
  const entry = ledger.register(ctx.toolCallId, {
1602
1660
  childSessionId,
1603
1661
  runner: opts.runner,
1604
1662
  ttlMs: FAILED_SESSION_RETAIN_TTL_MS,
1605
1663
  ...(childAgentName !== undefined ? { agentName: childAgentName } : {}),
1606
1664
  ...(agentName !== undefined ? { explicitAgentName: agentName } : {}),
1607
- ...(agentName !== undefined ? { explicitAgentName: agentName } : {}),
1608
1665
  specSnapshot: Object.freeze({
1609
1666
  ...plainSpec,
1610
1667
  tools: [...(plainSpec.tools ?? [])],
@@ -1615,7 +1672,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1615
1672
  ...(plainSpec.deferTools !== undefined ? { deferTools: [...plainSpec.deferTools] } : {}),
1616
1673
  ...(plainSpec.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...plainSpec.alwaysLoadTools] } : {}),
1617
1674
  }),
1618
- internalsSnapshot: { ...childInternals },
1675
+ internalsSnapshot: { ...childInternals, ...failedRetainStrip.internalsFlag },
1619
1676
  release: releaseChild,
1620
1677
  });
1621
1678
  if (entry) {
@@ -1715,7 +1772,6 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1715
1772
  else if (!sessionScopedBg)
1716
1773
  ctx.signal.addEventListener("abort", onHostAbort, { once: true });
1717
1774
  }
1718
- const dropTurnBoundQuestionFace = sessionScopedBg ? { onQuestion: undefined } : {};
1719
1775
  const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
1720
1776
  const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
1721
1777
  const bgScope = treeScope;
@@ -1808,7 +1864,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1808
1864
  startedAt: Date.now(),
1809
1865
  });
1810
1866
  const forkBgStartedAt = Date.now();
1811
- const bgForkSpec = { ...buildChildSpec(abort.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective, ...dropTurnBoundQuestionFace };
1867
+ const bgForkSpecBase = { ...buildChildSpec(abort.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective };
1868
+ const forkQuestionStrip = sessionScopedBg ? stripSpawnTurnQuestionFace(bgForkSpecBase) : undefined;
1869
+ const bgForkSpec = forkQuestionStrip !== undefined ? forkQuestionStrip.spec : bgForkSpecBase;
1812
1870
  const s2ForkNotifyReady = (inject) => {
1813
1871
  bg.registry.attachAgentNotify(taskId, inject);
1814
1872
  };
@@ -1832,7 +1890,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1832
1890
  const bgForkInternals = bgSink
1833
1891
  ? {
1834
1892
  ...forkInternals,
1893
+ ...(forkQuestionStrip !== undefined ? forkQuestionStrip.internalsFlag : {}),
1835
1894
  onNotifyInjectorReady: s2ForkNotifyReady,
1895
+ delegationTaskType: "background_agent",
1836
1896
  onForwardEvent: (e) => {
1837
1897
  try {
1838
1898
  recordingForward(e, { bgAgentId: taskId });
@@ -1863,7 +1923,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1863
1923
  }
1864
1924
  : {
1865
1925
  ...forkInternals,
1926
+ ...(forkQuestionStrip !== undefined ? forkQuestionStrip.internalsFlag : {}),
1866
1927
  onNotifyInjectorReady: s2ForkNotifyReady,
1928
+ delegationTaskType: "background_agent",
1867
1929
  onForwardEvent: (e) => {
1868
1930
  try {
1869
1931
  recordingForward(e, { bgAgentId: taskId });
@@ -1888,6 +1950,14 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1888
1950
  }
1889
1951
  catch {
1890
1952
  }
1953
+ const forkBgWarning = await reviewHandback({
1954
+ review: ctx.autoModeReview,
1955
+ toolCallId: ctx.toolCallId,
1956
+ toolName: "Agent(fork handback)",
1957
+ evidence: { result: child.result, steps: stepRecorder.recentSteps(), edits: stepRecorder.editedFiles() },
1958
+ signal: abort.signal,
1959
+ });
1960
+ const forkBgResult = withHandbackWarning(forkBgWarning, child.result);
1891
1961
  const okBg = child.status === "completed";
1892
1962
  const reapedBg = !okBg && abort.signal.aborted;
1893
1963
  const collateralBg = reapedBg && bg.registry.getAccessibleTask(taskId, { ...(bgOwner !== undefined ? { owner: bgOwner } : {}), ...(bgScope !== undefined ? { scope: bgScope } : {}) })?.status === "running";
@@ -1899,7 +1969,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1899
1969
  const settledBg = bg.registry.settleBackgroundAgent(taskId, {
1900
1970
  cycle: 0,
1901
1971
  status: okBg ? "completed" : reapedBg ? "killed" : "failed",
1902
- ...resultSettleFields(child.result),
1972
+ ...resultSettleFields(forkBgResult),
1903
1973
  ...(!okBg ? { error: reapedBg ? (collateralBg ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : child.errorMessage ?? String(child.status) } : {}),
1904
1974
  ...(errCodeFork !== undefined ? { errorCode: errCodeFork } : {}),
1905
1975
  ...(errClassFork !== undefined ? { retryable: errClassFork.retryable, errorKind: errClassFork.errorKind } : {}),
@@ -1950,7 +2020,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1950
2020
  ...(failReasonFork !== undefined ? { error: failReasonFork.slice(0, REPORT_FIELD_MAX) } : {}),
1951
2021
  ...(settledBg === "failed" && errCodeFork !== undefined ? { errorCode: errCodeFork } : {}),
1952
2022
  ...(child.sessionId ? { sessionId: child.sessionId } : {}),
1953
- ...(child.result ? { result: notifyResultField(child.result) } : {}),
2023
+ ...(forkBgResult ? { result: notifyResultField(forkBgResult) } : {}),
1954
2024
  ...(settledBg === "killed" && child.result ? { partial: true } : {}),
1955
2025
  ...residualFork,
1956
2026
  resumable: resumableFork,
@@ -2053,6 +2123,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2053
2123
  const wt = await finishWorktree();
2054
2124
  return { isError: true, content: `Fork failed: the forked run threw (${e instanceof Error ? e.message : String(e)}).${wt ? `\n${wt}` : ""}`, details: { error: "fork run failed" } };
2055
2125
  }
2126
+ const forkHandbackWarning = await reviewHandback({
2127
+ review: ctx.autoModeReview,
2128
+ toolCallId: ctx.toolCallId,
2129
+ toolName: "Agent(fork handback)",
2130
+ evidence: { result: forkChild.result, steps: stepRecorder.recentSteps(), edits: stepRecorder.editedFiles() },
2131
+ signal: ctx.signal,
2132
+ });
2056
2133
  const nsf = forkChild.stats.nested;
2057
2134
  ctx.reportUsage?.({
2058
2135
  tokens: forkChild.stats.tokens + (nsf?.tokens ?? 0),
@@ -2066,6 +2143,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2066
2143
  const wtLine = await finishWorktree();
2067
2144
  const forkErr = classifySubagentError(forkChild);
2068
2145
  const forkLines = [
2146
+ ...(forkHandbackWarning ? [forkHandbackWarning, ""] : []),
2069
2147
  `[Sub-agent report · ${FORK_SUBAGENT_TYPE}${a.description ? ` · ${String(a.description)}` : ""}] (forked session …${forkedId.slice(-8)})`,
2070
2148
  `status: ${forkChild.status}`,
2071
2149
  modelNote ?? "",
@@ -2087,6 +2165,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2087
2165
  details: completedAgentCard(forkChild, {
2088
2166
  subagentType: FORK_SUBAGENT_TYPE,
2089
2167
  error: forkErr,
2168
+ ...(forkHandbackWarning !== undefined ? { handbackWarning: forkHandbackWarning } : {}),
2090
2169
  ...(worktreeKeptPath !== undefined ? { worktreePath: worktreeKeptPath } : {}),
2091
2170
  ...(() => {
2092
2171
  const ts = toolStatsCounter.snapshot();
@@ -2255,7 +2334,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2255
2334
  : ctx.subagentRetain;
2256
2335
  if (sessionScopedBg && bgRetainLedger)
2257
2336
  ensureSessionReapHook(bg.registry);
2258
- const bgRetain = await tryRetainChild(bgRetainLedger, sessionScopedBg && ctx.subagentRetain ? ctx.subagentRetain.ttlMs : undefined, bgChildSessionId, bgDurableProbe, () => bg.registry.releaseDurableTranscriptAnchor(taskId));
2337
+ const bgRetain = await tryRetainChild(bgRetainLedger, sessionScopedBg && ctx.subagentRetain ? ctx.subagentRetain.ttlMs : undefined, bgChildSessionId, bgDurableProbe, () => bg.registry.releaseDurableTranscriptAnchor(taskId), {
2338
+ delegationTaskType: "background_agent",
2339
+ });
2259
2340
  if (bgRetain) {
2260
2341
  bg.registry.markRetainedContinuation(taskId);
2261
2342
  const origRelease = bgRetain.release;
@@ -2265,23 +2346,26 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2265
2346
  };
2266
2347
  }
2267
2348
  const forwardDurableApproval = ctx.durableApprovalForChildren !== undefined &&
2268
- agentName !== undefined &&
2269
2349
  bg.agentStore !== undefined &&
2270
2350
  bg.checkpointStore !== undefined &&
2271
2351
  bg.ensureChildSessionDurable !== undefined;
2272
- const bgSpec = {
2352
+ const bgSpecBase = {
2273
2353
  ...buildChildSpec(abort.signal),
2274
2354
  sessionId: bgChildSessionId,
2275
2355
  ...(forwardDurableApproval ? { durableApproval: { ...ctx.durableApprovalForChildren } } : {}),
2276
2356
  ...(reviveRow !== undefined ? { requireExistingSession: true } : {}),
2277
- ...(outlivesThisRequest ? { onQuestion: undefined } : {}),
2278
2357
  };
2358
+ const bgQuestionStrip = outlivesThisRequest ? stripSpawnTurnQuestionFace(bgSpecBase) : undefined;
2359
+ const bgSpec = bgQuestionStrip !== undefined ? bgQuestionStrip.spec : bgSpecBase;
2279
2360
  let reviveAttachedResolve;
2280
2361
  const reviveAttached = reviveRow !== undefined ? new Promise((r) => (reviveAttachedResolve = r)) : undefined;
2281
2362
  const s2NotifyReady = (inject) => {
2282
2363
  bg.registry.attachAgentNotify(taskId, inject);
2283
2364
  if (parkedResume !== undefined)
2284
2365
  bg.registry.finalizeParkedResume(taskId);
2366
+ const legOrgVerdict = childInternals.ownOrgAdmissionRef?.current;
2367
+ if (legOrgVerdict !== undefined)
2368
+ bg.registry.recordBackgroundAgentOrgAdmission(taskId, legOrgVerdict);
2285
2369
  reviveAttachedResolve?.();
2286
2370
  };
2287
2371
  const bgActivityBeat = createBgActivityBeat(ctx.toolCallId, (toolStarts) => {
@@ -2304,7 +2388,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2304
2388
  const bgInternals = bgSink
2305
2389
  ? {
2306
2390
  ...childInternals,
2391
+ ...(bgQuestionStrip !== undefined ? bgQuestionStrip.internalsFlag : {}),
2307
2392
  onNotifyInjectorReady: s2NotifyReady,
2393
+ delegationTaskType: "background_agent",
2308
2394
  onForwardEvent: (e) => {
2309
2395
  try {
2310
2396
  recordingForward(e, { bgAgentId: taskId });
@@ -2335,7 +2421,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2335
2421
  }
2336
2422
  : {
2337
2423
  ...childInternals,
2424
+ ...(bgQuestionStrip !== undefined ? bgQuestionStrip.internalsFlag : {}),
2338
2425
  onNotifyInjectorReady: s2NotifyReady,
2426
+ delegationTaskType: "background_agent",
2339
2427
  onForwardEvent: (e) => {
2340
2428
  try {
2341
2429
  recordingForward(e, { bgAgentId: taskId });
@@ -2487,6 +2575,16 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2487
2575
  catch {
2488
2576
  }
2489
2577
  const seqAtSettle = bgRetain?.cycleSeq ?? (parkedResume !== undefined ? (reviveRow?.seq ?? 1) + 1 : reviveRow?.seq);
2578
+ const bgHandbackWarning = isDurablePause(child.status)
2579
+ ? undefined
2580
+ : await reviewHandback({
2581
+ review: ctx.autoModeReview,
2582
+ toolCallId: ctx.toolCallId,
2583
+ toolName: "Agent(handback)",
2584
+ evidence: { result: child.result, steps: stepRecorder.recentSteps(), edits: stepRecorder.editedFiles() },
2585
+ signal: abort.signal,
2586
+ });
2587
+ const bgHandbackResult = withHandbackWarning(bgHandbackWarning, child.result);
2490
2588
  if (bgRetain) {
2491
2589
  if (isDurablePause(child.status))
2492
2590
  bgRetainLedger?.abandon(ctx.toolCallId);
@@ -2509,7 +2607,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2509
2607
  return false;
2510
2608
  }
2511
2609
  };
2512
- const wired = agentName !== undefined && bg.agentStore !== undefined && cpStore !== undefined && bg.ensureChildSessionDurable !== undefined;
2610
+ const wired = bg.agentStore !== undefined && cpStore !== undefined && bg.ensureChildSessionDurable !== undefined;
2513
2611
  if (wired) {
2514
2612
  const token = child.checkpointToken;
2515
2613
  if (abort.signal.aborted) {
@@ -2568,7 +2666,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2568
2666
  cycle: 0,
2569
2667
  status: ok ? "completed" : reaped ? "killed" : "failed",
2570
2668
  seq: seqAtSettle ?? 1,
2571
- ...resultSettleFields(child.result),
2669
+ ...resultSettleFields(bgHandbackResult),
2572
2670
  ...(!ok ? { error: reaped ? (collateral ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : unparkedPauseReason ?? child.errorMessage ?? String(child.status) } : {}),
2573
2671
  ...(errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
2574
2672
  ...(errClassBg !== undefined ? { retryable: errClassBg.retryable, errorKind: errClassBg.errorKind } : {}),
@@ -2620,7 +2718,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2620
2718
  ...(failReasonBg !== undefined ? { error: failReasonBg.slice(0, REPORT_FIELD_MAX) } : {}),
2621
2719
  ...(settled === "failed" && errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
2622
2720
  ...(child.sessionId ? { sessionId: child.sessionId } : {}),
2623
- ...(child.result ? { result: notifyResultField(child.result) } : {}),
2721
+ ...(bgHandbackResult ? { result: notifyResultField(bgHandbackResult) } : {}),
2624
2722
  ...(settled === "killed" && child.result ? { partial: true } : {}),
2625
2723
  ...residual,
2626
2724
  resumable: resumableBg,
@@ -2777,6 +2875,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2777
2875
  ...(opts.background ? { registry: opts.background.registry } : {}),
2778
2876
  ...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
2779
2877
  ...(ctx.onQuestion !== undefined ? { currentOnQuestion: ctx.onQuestion } : {}),
2878
+ ...(ctx.autoModeReview !== undefined ? { currentAutoModeReview: ctx.autoModeReview } : {}),
2780
2879
  });
2781
2880
  notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
2782
2881
  resume,
@@ -2801,26 +2900,18 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2801
2900
  child = await opts.runner.runTask(buildChildSpec(ctx.signal), childInternals);
2802
2901
  }
2803
2902
  await settleObserver(child.status);
2804
- let handbackWarning;
2805
- if (ctx.autoModeReview) {
2806
- const handbackSteps = stepRecorder.recentSteps();
2807
- const handbackEdits = stepRecorder.editedFiles();
2808
- if (child.result || handbackSteps || handbackEdits) {
2809
- const handbackVerdict = await ctx.autoModeReview.decider
2810
- .decide({
2811
- req: {
2812
- toolName: "Agent(handback)",
2813
- args: { result: child.result?.slice(0, 2_000), toolSteps: handbackSteps, editedFiles: handbackEdits },
2814
- toolCallId: ctx.toolCallId,
2815
- },
2816
- askMessage: "Subagent has finished and is handing back control to the main agent. Review the subagent's work and flag if any action may violate security policy.",
2817
- }, ctx.signal)
2818
- .catch(() => ({ kind: "unavailable", cause: "error" }));
2819
- if (handbackVerdict.kind === "block") {
2820
- handbackWarning = `SECURITY WARNING: This subagent performed actions that may violate security policy. Reason: ${inlineUntrusted(handbackVerdict.reason ?? "(no reason given)")}. Review the subagent's actions carefully before acting on its output.`;
2821
- }
2822
- }
2823
- }
2903
+ const handbackWarning = await reviewHandback({
2904
+ review: ctx.autoModeReview,
2905
+ toolCallId: ctx.toolCallId,
2906
+ toolName: "Agent(handback)",
2907
+ evidence: {
2908
+ result: child.result,
2909
+ steps: stepRecorder.recentSteps(),
2910
+ edits: stepRecorder.editedFiles(),
2911
+ ...(child.result || child.status === "completed" ? {} : { partialText: assistantTextTail.trim() }),
2912
+ },
2913
+ signal: ctx.signal,
2914
+ });
2824
2915
  const ns = child.stats.nested;
2825
2916
  ctx.reportUsage?.({
2826
2917
  tokens: child.stats.tokens + (ns?.tokens ?? 0),
@@ -2905,6 +2996,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2905
2996
  details: completedAgentCard(child, {
2906
2997
  ...(def !== undefined ? { subagentType: def.name } : {}),
2907
2998
  error: errClass,
2999
+ ...(handbackWarning !== undefined ? { handbackWarning } : {}),
2908
3000
  ...(modelFallback !== undefined ? { modelFallback } : {}),
2909
3001
  ...(worktreeKeptPath !== undefined ? { worktreePath: worktreeKeptPath } : {}),
2910
3002
  ...(() => {
@@ -5,7 +5,7 @@ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js"
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
8
- import { ANTHROPIC_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
8
+ import { ANTHROPIC_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
9
9
  import { isThinkingLevel, reasoningBudgetShare, resolveEffort } from "./reasoning.js";
10
10
  import { runStreamingBrain } from "./stream-engine.js";
11
11
  const MIN_THINKING_TOKENS = 1024;
@@ -233,7 +233,7 @@ export function createAnthropicBrain(config = {}) {
233
233
  config,
234
234
  httpLabel: "anthropic",
235
235
  stallTimeouts: options?.stallTimeouts,
236
- buildRequest: () => {
236
+ buildRequest: (overrides) => {
237
237
  const apiKey = options?.apiKey ?? config.apiKey;
238
238
  const root = (model.baseUrl || config.baseUrl || "https://api.anthropic.com").replace(/\/+$/, "");
239
239
  const nonEmptyBlocks = context.systemBlocks?.filter((b) => b.text.length > 0);
@@ -248,7 +248,7 @@ export function createAnthropicBrain(config = {}) {
248
248
  : undefined;
249
249
  const body = {
250
250
  model: model.id,
251
- max_tokens: options?.maxTokens ?? model.maxTokens ?? config.defaultMaxTokens ?? 4096,
251
+ max_tokens: overrides?.maxOutputTokens ?? options?.maxTokens ?? model.maxTokens ?? config.defaultMaxTokens ?? 4096,
252
252
  messages: toAnthropicMessages(context, model),
253
253
  stream: true,
254
254
  };
@@ -266,7 +266,7 @@ export function createAnthropicBrain(config = {}) {
266
266
  body.thinking = { type: "adaptive" };
267
267
  }
268
268
  else {
269
- const hardCap = options?.maxTokens !== undefined;
269
+ const hardCap = overrides?.maxOutputTokens !== undefined || options?.maxTokens !== undefined;
270
270
  if (hardCap && body.max_tokens < MIN_THINKING_TOKENS * 2) {
271
271
  body.thinking = undefined;
272
272
  delete body.thinking;
@@ -322,11 +322,28 @@ export function createAnthropicBrain(config = {}) {
322
322
  headers["anthropic-version"] = config.version ?? "2023-06-01";
323
323
  if (apiKey)
324
324
  headers["x-api-key"] = apiKey;
325
- thinkingRequested = body.thinking !== undefined;
326
- sentMaxTokens = body.max_tokens;
327
- sentMaxTokensLane =
328
- options?.maxTokens !== undefined ? "options" : model.maxTokens !== undefined ? "model" : "config-default";
329
- return { url: `${root}/v1/messages`, headers, body: JSON.stringify(applyExtraBody(body, model.extraBody, ANTHROPIC_RESERVED)) };
325
+ const builtThinkingRequested = body.thinking !== undefined;
326
+ const builtMaxTokens = body.max_tokens;
327
+ const builtMaxTokensLane = overrides?.maxOutputTokens !== undefined
328
+ ? "engine"
329
+ : options?.maxTokens !== undefined
330
+ ? "options"
331
+ : model.maxTokens !== undefined
332
+ ? "model"
333
+ : "config-default";
334
+ const wire = applyExtraBody(body, model.extraBody, ANTHROPIC_RESERVED);
335
+ return {
336
+ url: `${root}/v1/messages`,
337
+ headers,
338
+ body: JSON.stringify(wire),
339
+ outputCapTokens: effectiveOutputCap(wire, OUTPUT_CAP_KEYS.anthropic),
340
+ capDerivedPaths: [...OUTPUT_CAP_KEYS.anthropic, "thinking.budget_tokens"],
341
+ onCommitted: () => {
342
+ thinkingRequested = builtThinkingRequested;
343
+ sentMaxTokens = builtMaxTokens;
344
+ sentMaxTokensLane = builtMaxTokensLane;
345
+ },
346
+ };
330
347
  },
331
348
  makeParser: (ctrl) => {
332
349
  const { out, partial } = ctrl;
@@ -588,7 +605,13 @@ export function createAnthropicBrain(config = {}) {
588
605
  const safetyCut = stopReason === "refusal";
589
606
  const contextWindowExceeded = stopReason === "model_context_window_exceeded";
590
607
  const emptyNoFinish = noUsableContent && stopReason == null && !degenerate;
591
- const maxTokensNote = `effective max_tokens=${sentMaxTokens ?? "?"} (from ${sentMaxTokensLane === "options" ? "options.maxTokens" : sentMaxTokensLane === "model" ? "model.maxTokens" : "config default"})`;
608
+ const maxTokensNote = `effective max_tokens=${sentMaxTokens ?? "?"} (from ${sentMaxTokensLane === "options"
609
+ ? "options.maxTokens"
610
+ : sentMaxTokensLane === "engine"
611
+ ? "the engine's context-overflow recovery"
612
+ : sentMaxTokensLane === "model"
613
+ ? "model.maxTokens"
614
+ : "config default"})`;
592
615
  const errorMessage = degenerate
593
616
  ? DEGENERATE_MESSAGE
594
617
  : safetyCut
@@ -0,0 +1,20 @@
1
+ export declare const FLOOR_OUTPUT_TOKENS = 3000;
2
+ export interface ContextOverflowReport {
3
+ inputTokens: number;
4
+ maxTokens: number;
5
+ contextLimit: number;
6
+ }
7
+ export declare function parseContextOverflow(status: number, body: string): ContextOverflowReport | undefined;
8
+ export type OutputCapAdjustment = {
9
+ outcome: "adjust";
10
+ maxOutputTokens: number;
11
+ current: number;
12
+ } | {
13
+ outcome: "below_floor";
14
+ available: number;
15
+ } | {
16
+ outcome: "no_progress";
17
+ available: number;
18
+ current: number;
19
+ };
20
+ export declare function planOutputCapAdjustment(report: ContextOverflowReport, otherCapsInEffect?: readonly (number | undefined)[]): OutputCapAdjustment;
@@ -0,0 +1,58 @@
1
+ export const FLOOR_OUTPUT_TOKENS = 3000;
2
+ const CONTEXT_HEADROOM_TOKENS = 1000;
3
+ const CONTEXT_OVERFLOW_RE = /^input length and `max_tokens` exceed context limit: (\d+) \+ (\d+) > (\d+)(?!\w|[.,]\d|\s\d|[+\-*/])/;
4
+ const CONTEXT_OVERFLOW_PREFIX = "input length and `max_tokens` exceed context limit:";
5
+ function providerDiagnostic(body) {
6
+ const trimmed = body.trim();
7
+ if (!trimmed.startsWith("{"))
8
+ return undefined;
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(trimmed);
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
17
+ return undefined;
18
+ const err = parsed.error;
19
+ if (typeof err !== "object" || err === null || Array.isArray(err))
20
+ return undefined;
21
+ const message = err.message;
22
+ return typeof message === "string" ? message : undefined;
23
+ }
24
+ function reportFrom(diagnostic) {
25
+ const message = diagnostic.trimStart();
26
+ const m = CONTEXT_OVERFLOW_RE.exec(message);
27
+ if (m === null)
28
+ return undefined;
29
+ if (message.slice(m[0].length).includes(CONTEXT_OVERFLOW_PREFIX))
30
+ return undefined;
31
+ const [, rawInput, rawMax, rawLimit] = m;
32
+ if (rawInput === undefined || rawMax === undefined || rawLimit === undefined)
33
+ return undefined;
34
+ const inputTokens = Number.parseInt(rawInput, 10);
35
+ const maxTokens = Number.parseInt(rawMax, 10);
36
+ const contextLimit = Number.parseInt(rawLimit, 10);
37
+ if (!Number.isSafeInteger(inputTokens) || !Number.isSafeInteger(maxTokens) || !Number.isSafeInteger(contextLimit)) {
38
+ return undefined;
39
+ }
40
+ if (inputTokens <= contextLimit - maxTokens)
41
+ return undefined;
42
+ return { inputTokens, maxTokens, contextLimit };
43
+ }
44
+ export function parseContextOverflow(status, body) {
45
+ if (status !== 400)
46
+ return undefined;
47
+ const diagnostic = providerDiagnostic(body);
48
+ return diagnostic === undefined ? undefined : reportFrom(diagnostic);
49
+ }
50
+ export function planOutputCapAdjustment(report, otherCapsInEffect = []) {
51
+ const available = Math.max(0, report.contextLimit - report.inputTokens - CONTEXT_HEADROOM_TOKENS);
52
+ if (available < FLOOR_OUTPUT_TOKENS)
53
+ return { outcome: "below_floor", available };
54
+ const current = Math.min(report.maxTokens, ...otherCapsInEffect.filter((c) => c !== undefined));
55
+ if (available >= current)
56
+ return { outcome: "no_progress", available, current };
57
+ return { outcome: "adjust", maxOutputTokens: available, current };
58
+ }