@sema-agent/core 7.49.5 → 7.50.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 (193) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/dist/agents/observer.js +3 -10
  3. package/dist/agents/subagent-steps.d.ts +6 -14
  4. package/dist/agents/subagent-steps.js +3 -17
  5. package/dist/brain/stream-shared.d.ts +1 -1
  6. package/dist/core/allow-entry.d.ts +1 -1
  7. package/dist/core/allow-entry.js +1 -1
  8. package/dist/core/ask-origin.d.ts +15 -11
  9. package/dist/core/ask-origin.js +6 -14
  10. package/dist/core/auto-mode-prompt.js +6 -2
  11. package/dist/core/checkpoint-store.d.ts +6 -4
  12. package/dist/core/checkpoint-store.js +1 -1
  13. package/dist/core/context-edit.d.ts +2 -2
  14. package/dist/core/decision-receipt.d.ts +2 -2
  15. package/dist/core/decision-receipt.js +30 -11
  16. package/dist/core/delegation-frames.d.ts +66 -18
  17. package/dist/core/delegation-frames.js +22 -0
  18. package/dist/core/engine-notice.d.ts +7 -5
  19. package/dist/core/frame-settlement.d.ts +6 -1
  20. package/dist/core/gate-fold.js +19 -13
  21. package/dist/core/gate-lanes.js +12 -11
  22. package/dist/core/gate-outcome.d.ts +2 -2
  23. package/dist/core/governance-codes.js +2 -0
  24. package/dist/core/hooks.d.ts +28 -20
  25. package/dist/core/human-input-projection.js +2 -0
  26. package/dist/core/lsp-diagnostics.d.ts +3 -0
  27. package/dist/core/lsp-diagnostics.js +6 -0
  28. package/dist/core/lsp-session.d.ts +39 -17
  29. package/dist/core/lsp-session.js +118 -58
  30. package/dist/core/lsp.d.ts +83 -3
  31. package/dist/core/lsp.js +146 -16
  32. package/dist/core/mcp.js +2 -1
  33. package/dist/core/media-byte-cap.d.ts +14 -0
  34. package/dist/core/media-byte-cap.js +15 -3
  35. package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
  36. package/dist/core/memory-engine/consolidation-driver.js +54 -6
  37. package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
  38. package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
  39. package/dist/core/memory-engine/consolidation.d.ts +17 -0
  40. package/dist/core/memory-engine/consolidation.js +1 -0
  41. package/dist/core/memory-engine/distiller.d.ts +8 -2
  42. package/dist/core/memory-engine/distiller.js +8 -6
  43. package/dist/core/memory-engine/engine.d.ts +37 -4
  44. package/dist/core/memory-engine/engine.js +127 -38
  45. package/dist/core/memory-engine/file-backend.d.ts +8 -3
  46. package/dist/core/memory-engine/file-backend.js +44 -16
  47. package/dist/core/memory-engine/index.d.ts +2 -2
  48. package/dist/core/memory-engine/index.js +1 -1
  49. package/dist/core/memory-engine/landing-admission.d.ts +65 -0
  50. package/dist/core/memory-engine/landing-admission.js +68 -0
  51. package/dist/core/memory-engine/types.d.ts +23 -0
  52. package/dist/core/mode-policy.d.ts +3 -2
  53. package/dist/core/mode-policy.js +2 -0
  54. package/dist/core/permission-rule-model.d.ts +19 -13
  55. package/dist/core/permission-rule-model.js +7 -4
  56. package/dist/core/permission-rule-provider.d.ts +11 -0
  57. package/dist/core/permission-rules.d.ts +1 -1
  58. package/dist/core/permission-rules.js +5 -2
  59. package/dist/core/persisted-rule-arms.d.ts +15 -7
  60. package/dist/core/persisted-rule-arms.js +13 -6
  61. package/dist/core/reminder-mint.d.ts +5 -0
  62. package/dist/core/reminder-mint.js +1 -0
  63. package/dist/core/retired-keys.d.ts +8 -0
  64. package/dist/core/retired-keys.js +4 -0
  65. package/dist/core/rule-lane-reads.d.ts +4 -4
  66. package/dist/core/rule-lane-reads.js +10 -1
  67. package/dist/core/runner/construction-doors.d.ts +10 -4
  68. package/dist/core/runner/construction-doors.js +25 -2
  69. package/dist/core/runner/contracts.d.ts +20 -3
  70. package/dist/core/runner/env-capability-census.js +1 -1
  71. package/dist/core/runner/gate-exit.js +2 -1
  72. package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
  73. package/dist/core/runner/memory-consolidation-driver.js +5 -2
  74. package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
  75. package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
  76. package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
  77. package/dist/core/runner/permission-rule-lanes.js +13 -43
  78. package/dist/core/runner/person-path-rows.d.ts +244 -0
  79. package/dist/core/runner/person-path-rows.js +387 -0
  80. package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
  81. package/dist/core/runner/prepare-artifact.d.ts +5 -3
  82. package/dist/core/runner/prepare-artifact.js +2 -8
  83. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  84. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  85. package/dist/core/runner/prepare-context-lane.js +2 -2
  86. package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
  87. package/dist/core/runner/prepare-gate-stations.js +1 -1
  88. package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
  89. package/dist/core/runner/prepare-hands-readface.js +57 -43
  90. package/dist/core/runner/prepare-lsp.d.ts +15 -13
  91. package/dist/core/runner/prepare-lsp.js +11 -11
  92. package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
  93. package/dist/core/runner/prepare-policy-chain.js +6 -7
  94. package/dist/core/runner/prepare-run-refs.js +1 -1
  95. package/dist/core/runner/prepare-task.js +24 -16
  96. package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
  97. package/dist/core/runner/prepare-turn-wiring.js +19 -8
  98. package/dist/core/runner/read-root-grant.d.ts +1 -25
  99. package/dist/core/runner/read-root-grant.js +1 -17
  100. package/dist/core/runner/retired-runner-deps.d.ts +2 -2
  101. package/dist/core/runner/run-brain-sinks.d.ts +1 -5
  102. package/dist/core/runner/run-brain-sinks.js +2 -4
  103. package/dist/core/runner/run-harness-handlers.js +5 -4
  104. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  105. package/dist/core/runner/run-identity-wiring.js +10 -1
  106. package/dist/core/runner/run-leg.d.ts +1 -1
  107. package/dist/core/runner/run-leg.js +22 -9
  108. package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
  109. package/dist/core/runner/run-settle-and-teardown.js +7 -4
  110. package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
  111. package/dist/core/runner/run-terminal-adoption.js +1 -1
  112. package/dist/core/runner/run-turn-boundary.js +2 -2
  113. package/dist/core/runner/runtask.js +8 -8
  114. package/dist/core/runner/session-file-state-replay.d.ts +14 -4
  115. package/dist/core/runner/session-file-state-replay.js +8 -0
  116. package/dist/core/runner/stream-steer-verb.js +3 -0
  117. package/dist/core/runner/tool-end-body.d.ts +5 -4
  118. package/dist/core/runner/tool-end-body.js +4 -4
  119. package/dist/core/runner/tool-output-projection.d.ts +6 -4
  120. package/dist/core/runner/tool-output-projection.js +66 -47
  121. package/dist/core/runner-deps.d.ts +6 -3
  122. package/dist/core/shared-memory/types.d.ts +2 -2
  123. package/dist/core/shell-read-silence.d.ts +23 -0
  124. package/dist/core/shell-read-silence.js +18 -0
  125. package/dist/core/synthetic-read-verdict.d.ts +80 -21
  126. package/dist/core/synthetic-read-verdict.js +89 -18
  127. package/dist/core/task-event.d.ts +113 -25
  128. package/dist/core/task-spec.d.ts +44 -0
  129. package/dist/core/task-tool-shape.d.ts +1 -1
  130. package/dist/core/tool-catalog-entries.js +16 -2
  131. package/dist/core/tool-policy.d.ts +25 -15
  132. package/dist/core/tool-policy.js +9 -13
  133. package/dist/core/trace.d.ts +3 -2
  134. package/dist/core/types.d.ts +3 -3
  135. package/dist/core/types.js +1 -1
  136. package/dist/core/untrusted-text.d.ts +2 -2
  137. package/dist/core/user-attachment.d.ts +164 -0
  138. package/dist/core/user-attachment.js +437 -0
  139. package/dist/engine/compaction/compaction.js +10 -3
  140. package/dist/engine/compaction/utils.js +6 -0
  141. package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
  142. package/dist/engine/execution-env/node-execution-env.js +125 -0
  143. package/dist/engine/harness/agent-harness.d.ts +12 -2
  144. package/dist/engine/harness/agent-harness.js +15 -3
  145. package/dist/engine/harness/types.d.ts +21 -0
  146. package/dist/engine/llm/content-block-render.d.ts +15 -0
  147. package/dist/engine/llm/content-block-render.js +10 -0
  148. package/dist/engine/llm/index.d.ts +1 -0
  149. package/dist/engine/llm/index.js +1 -0
  150. package/dist/engine/llm/types.d.ts +80 -4
  151. package/dist/engine/llm/types.js +43 -1
  152. package/dist/engine/session/import-validate.js +2 -0
  153. package/dist/index.d.ts +6 -4
  154. package/dist/index.js +2 -1
  155. package/dist/internal/harness-types.d.ts +1 -1
  156. package/dist/internal/llm.d.ts +2 -2
  157. package/dist/internal/llm.js +1 -1
  158. package/dist/orchestration/workflow.d.ts +4 -5
  159. package/dist/orchestration/workflow.js +2 -2
  160. package/dist/prompts/default.d.ts +1 -1
  161. package/dist/tools/artifact/artifact-tool.js +1 -1
  162. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  163. package/dist/tools/fs/dir-entries.d.ts +4 -10
  164. package/dist/tools/fs/dir-entries.js +4 -11
  165. package/dist/tools/fs/file-read-check.js +3 -3
  166. package/dist/tools/fs/fs-pdf.js +2 -4
  167. package/dist/tools/fs/fs-read.d.ts +47 -4
  168. package/dist/tools/fs/fs-read.js +63 -19
  169. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  170. package/dist/tools/fs/fs-search-tools.js +7 -2
  171. package/dist/tools/fs/fs-write.d.ts +1 -1
  172. package/dist/tools/fs/fs-write.js +7 -6
  173. package/dist/tools/fs/index.d.ts +7 -1
  174. package/dist/tools/fs/index.js +56 -34
  175. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  176. package/dist/tools/fs/read-deny-home.js +32 -22
  177. package/dist/tools/fs/read-deny.d.ts +14 -0
  178. package/dist/tools/fs/read-deny.js +8 -0
  179. package/dist/tools/fs/repo-map.d.ts +3 -2
  180. package/dist/tools/fs/repo-map.js +11 -3
  181. package/dist/tools/fs/safety.d.ts +41 -2
  182. package/dist/tools/fs/safety.js +12 -3
  183. package/dist/tools/fs/shell-read-check.d.ts +12 -3
  184. package/dist/tools/fs/shell-read-check.js +28 -15
  185. package/dist/tools/fs/shell-read-device.d.ts +1 -1
  186. package/dist/tools/fs/shell-read-judge.d.ts +9 -4
  187. package/dist/tools/fs/shell-read-judge.js +1 -1
  188. package/dist/tools/loop-tick.d.ts +2 -2
  189. package/package.json +3 -1
  190. package/test/export-surface.snapshot.json +39 -1
  191. package/wire-changes/core@7.50.0.json +1699 -0
  192. package/dist/core/runner/persisted-path-rows.d.ts +0 -88
  193. package/dist/core/runner/persisted-path-rows.js +0 -108
@@ -6,6 +6,8 @@ import { normalizePersistedRuleHit } from "../rule-lane-reads.js";
6
6
  import { decidersNote, SETTLEMENT_NOBODY_JUDGED } from "../gate-outcome.js";
7
7
  import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
8
8
  import { buildAppliedHumanInputEvent, buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
9
+ import { humanInputAttachmentsOf, readObjectiveAttachments, userAttachmentDeclarations } from "../user-attachment.js";
10
+ import { uuidv7 } from "../../internal/harness.js";
9
11
  import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
10
12
  import { applyPersistedTightening, disclosedRuleSet } from "../persisted-rule-arms.js";
11
13
  import { parkedSkillApprovalRefusal, skillRowRefusal } from "../skill-grant.js";
@@ -13,7 +15,8 @@ import { SKILL_TOOL_NAME } from "./synthetic-tools.js";
13
15
  import { computeCostMicroUsd } from "../pricing.js";
14
16
  import { mintSystemReminder } from "../reminder-mint.js";
15
17
  import { hasDestroy } from "../remote-env.js";
16
- import { advertisedWritableRootsSeat, exemptRootsSeat, humanRefusalSentence, policyCannotAnswer, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
18
+ import { advertisedWritableRootsSeat, attestationOf, exemptRootsSeat, humanRefusalSentence, policyCannotAnswer, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
19
+ import { pathDenialCardOf } from "../../tools/fs/safety.js";
17
20
  import { callFaceSeat, judgeParkedToolIdentity, parkedRowLiveFace, rosterEntryNamed } from "../tool-roster.js";
18
21
  import { deliverEngineNotice } from "../types.js";
19
22
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
@@ -288,7 +291,7 @@ export async function runLeg(input) {
288
291
  const userPromptSubmit = (spec.hooks ?? runner.deps.hooks)?.userPromptSubmit;
289
292
  if (userPromptSubmit) {
290
293
  try {
291
- const promptSeat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => userPromptSubmit(spec.objective, { identity: prepared.hookIdentity, permissionMode: prepared.permissionMode.observed, signal: sig, source: "objective", ...(objectiveActor !== undefined ? { actor: snapshotActorAssertion(objectiveActor) } : {}) }));
294
+ const promptSeat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => userPromptSubmit(spec.objective, { identity: prepared.hookIdentity, permissionMode: prepared.permissionMode.observed, signal: sig, source: "objective", ...(objectiveActor !== undefined ? { actor: snapshotActorAssertion(objectiveActor) } : {}), ...(prepared.userAttachments !== undefined ? { userAttachments: userAttachmentDeclarations(prepared.userAttachments) } : {}) }));
292
295
  if (promptSeat.expired) {
293
296
  if (promptSeat.cause === "timeout") {
294
297
  try {
@@ -326,6 +329,8 @@ export async function runLeg(input) {
326
329
  promptBlocked = true;
327
330
  }
328
331
  }
332
+ const objectiveInputId = uuidv7();
333
+ const { frames: attachmentFrames, chars: attachmentChars } = await readObjectiveAttachments(prepared, objectiveInputId, promptBlocked);
329
334
  let gitLegDelivered;
330
335
  if (!promptBlocked) {
331
336
  const gitBody = await resolveGitLegDelivery(prepared, undefined, (err) => {
@@ -401,14 +406,14 @@ export async function runLeg(input) {
401
406
  const precallMicroUsd = rs.budget.maxCostMicroUsd === undefined
402
407
  ? 0
403
408
  : computeCostMicroUsd({
404
- totalInputTokens: Math.ceil((effectiveObjective.length + gitQueuedChars) / 4),
409
+ totalInputTokens: Math.ceil((effectiveObjective.length + gitQueuedChars + attachmentChars) / 4),
405
410
  cacheReadTokens: 0,
406
411
  cacheWriteTokens: 0,
407
412
  cacheWriteTokensLong: 0,
408
413
  outputTokens: prepared.model.maxTokens ?? DEFAULT_PRECALL_OUTPUT_TOKENS,
409
414
  }, rs.telemetry.pricing);
410
415
  const precallCeilingMicroUsd = prepared.suspendForResource !== undefined ? rs.budget.remainingMicroUsd : rs.budget.maxCostMicroUsd;
411
- const precallTokens = Math.ceil((effectiveObjective.length + gitQueuedChars) / 4) + (prepared.model.maxTokens ?? DEFAULT_PRECALL_OUTPUT_TOKENS);
416
+ const precallTokens = Math.ceil((effectiveObjective.length + gitQueuedChars + attachmentChars) / 4) + (prepared.model.maxTokens ?? DEFAULT_PRECALL_OUTPUT_TOKENS);
412
417
  const precallCeilingTokens = prepared.suspendForResource !== undefined ? rs.budget.remainingTokens : rs.budget.maxTokensWindow;
413
418
  const entryUsageRetryAfterMs = prepared.usageGovernance !== undefined ? await prepared.usageGovernance.check(Date.now()) : undefined;
414
419
  if (promptBlocked) {
@@ -470,21 +475,25 @@ export async function runLeg(input) {
470
475
  }
471
476
  const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
472
477
  if (!prepared.abortController.signal.aborted) {
478
+ prepared.attachmentDelivery.stage(objectiveInputId, attachmentFrames);
473
479
  queue.push({
474
480
  ...buildHumanInputEvent({
475
481
  carrier: "objective",
476
482
  source: "objective",
477
483
  delivery: "applied",
484
+ inputId: objectiveInputId,
478
485
  sessionSeq: nextHumanInputSeq(prepared.harness),
479
486
  ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
480
487
  ...(objectiveActor?.issuer !== undefined ? { issuer: objectiveActor.issuer } : {}),
481
488
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
489
+ ...(attachmentFrames.length > 0 ? { attachments: humanInputAttachmentsOf(attachmentFrames) } : {}),
482
490
  }),
483
491
  ...ident(),
484
492
  });
485
493
  final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
486
494
  ...(images && images.length > 0 ? { images } : {}),
487
495
  ...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
496
+ ...(attachmentFrames.length > 0 ? { trailing: attachmentFrames.map((f) => f.seed) } : {}),
488
497
  ...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
489
498
  }));
490
499
  await flushGitMirror();
@@ -579,7 +588,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
579
588
  const settleEnd = async (isError, result, refusedBy) => {
580
589
  if (refusedBy !== undefined)
581
590
  occurrences.refuse(pendingAction.toolCallId, refusedBy);
582
- const gate = emitSettled(occurrences, callStart, { result, isError }, { push: emit, structured: prepared.structuredProjector }).gate ?? decidedGate;
591
+ const gate = emitSettled(occurrences, callStart, { result, isError }, { push: emit, structured: prepared.structuredProjector, mediaByteCapBytes: runner.deps.mediaByteCapBytes }).gate ?? decidedGate;
583
592
  const word = await resume.recordExecutionOutcome(gate);
584
593
  settleExecutionRecord(runner.deps.onNotice, word, { toolName: pendingAction.toolName, gate, sessionId: prepared.sessionId, runId: prepared.runId, scope: resume.cp.scope, ...(resume.cp.checkpointId !== undefined ? { checkpointId: resume.cp.checkpointId } : {}) });
585
594
  };
@@ -627,6 +636,10 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
627
636
  resourceSpentMicroUsd: prepared.resourceLedger?.spentMicroUsd ?? 0,
628
637
  suspendCount: resume.cp.suspendCount ?? 0,
629
638
  };
639
+ const denialCard = (d) => {
640
+ const card = pathDenialCardOf(attestationOf(d, { toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName })?.pathDenial);
641
+ return card !== undefined ? { details: card } : {};
642
+ };
630
643
  const beltStamped = prepared.stampCallTarget?.(beltReq(), prepared.abortController.signal);
631
644
  if (beltStamped !== undefined)
632
645
  beltReading = (await beltStamped).targetReading;
@@ -643,7 +656,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
643
656
  ? " (the call as it would run requires a person's approval, and this decision was the deployment's policy)"
644
657
  : " (the policy rewrote these bytes to a value that was not approved)";
645
658
  const editedDenial = formatHookFeedback(`${outcome.updatedInput !== undefined ? "The approver EDITED this call's input; the edited call" : "A PreToolUse hook REWROTE this approved call's input; the rewritten call"} is refused by the deployment's tool policy and was not executed${why}.`, prepared.reminderMark);
646
- await settleEnd(true, { content: editedDenial }, "policy");
659
+ await settleEnd(true, { content: editedDenial, ...denialCard(rechecked) }, "policy");
647
660
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, editedDenial, true));
648
661
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
649
662
  return;
@@ -697,7 +710,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
697
710
  const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check(beltReq(), prepared.abortController.signal));
698
711
  if (narrowed.action === "deny") {
699
712
  const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by ${ruleSourcePhrase("narrowing")} and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`, prepared.reminderMark);
700
- await settleEnd(true, { content: narrowedDenial }, "policy");
713
+ await settleEnd(true, { content: narrowedDenial, ...denialCard(narrowed) }, "policy");
701
714
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, narrowedDenial, true));
702
715
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
703
716
  return;
@@ -721,7 +734,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
721
734
  laneRead = Promise.resolve(unreadable);
722
735
  }
723
736
  const read = await settleOrgVerdictWithin(laneRead, unreadable, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS, onFallback: (cause) => void (laneWaitCancelled = cause === "aborted") });
724
- const tightened = applyPersistedTightening({ action: "allow" }, read);
737
+ const tightened = applyPersistedTightening({ action: "allow" }, read, beltReq());
725
738
  if (laneWaitCancelled || tightened.tightened === "deny" || tightened.tightened === "unreadable") {
726
739
  const why = laneWaitCancelled
727
740
  ? "the task was cancelled before your persisted permission rules could be read"
@@ -729,7 +742,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
729
742
  ? `a standing deny rule of yours (${disclosedRuleSet(read.hit?.rules ?? [])}) now refuses it`
730
743
  : `it could not be read against your standing deny/ask rules (${read.reason})`;
731
744
  const ruleDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${why}. This approval is spent — the call has to be re-issued and approved again.`, prepared.reminderMark);
732
- await settleEnd(true, { content: ruleDenial }, "persisted_rule");
745
+ await settleEnd(true, { content: ruleDenial, ...denialCard(tightened.decision) }, "persisted_rule");
733
746
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, ruleDenial, true));
734
747
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
735
748
  return;
@@ -26,10 +26,6 @@ export interface RunSettleAndTeardownInput {
26
26
  parentToolCallId?: string;
27
27
  sourceTaskId?: string;
28
28
  };
29
- /** borrowed-readonly — the spawning tool call's id when this task runs AS A SUB-AGENT (gates the terminal tick). */
30
- parentToolCallId: string | undefined;
31
- /** borrowed-readonly — the sub-agent display name for the terminal tick. */
32
- subagentName: string | undefined;
33
29
  /** borrowed-readonly — the loop-liveness latch: `userHalted` = the halt verb was ACCEPTED (the unconsumed-halt notice's input; it does not sign the seat). */
34
30
  loopLatch: {
35
31
  readonly userHalted: boolean;
@@ -1,4 +1,5 @@
1
1
  import { MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
2
+ import { emitsChildRowFrames } from "../delegation-frames.js";
2
3
  import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
3
4
  import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
4
5
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
@@ -21,11 +22,12 @@ import { generatePromptSuggestions } from "./prompt-suggestions.js";
21
22
  import { settleTeardownLeg } from "./teardown-bounded.js";
22
23
  import { owedDateAnchor } from "./announce-once-ledger.js";
23
24
  import { autoRunConsolidationFromRecommendation, autoRunOnRecommendationEffective, scopeHasUnconsumedRecommendation } from "./memory-consolidation-driver.js";
25
+ import { consolidationWriteJudgeOfLeg } from "./memory-consolidation-write-judge.js";
24
26
  import { amendTerminal, terminalProjection } from "./terminal-projection.js";
25
27
  import { mintCompactedEvent } from "../auto-compaction.js";
26
28
  import { asThrownError } from "../thrown-value.js";
27
29
  export async function runSettleAndTeardown(input) {
28
- const { spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, compactionBreaker, owedUserInput, setResult, onSuggestions, onSuspend, runner, sessions } = input;
30
+ const { spec, queue, prepared, resume, internals, rs, stats, ident, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, compactionBreaker, owedUserInput, setResult, onSuggestions, onSuspend, runner, sessions } = input;
29
31
  try {
30
32
  if (comp?.compacted) {
31
33
  queue.push(mintCompactedEvent(comp, "auto", ident()));
@@ -237,7 +239,8 @@ export async function runSettleAndTeardown(input) {
237
239
  await settleTeardownLeg(() => mes.harvest(), "memoryEngineSession.harvest (settle leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
238
240
  const scope = mes.handle.writeScope;
239
241
  if (scope !== null && autoRunOnRecommendationEffective(runner.deps) && scopeHasUnconsumedRecommendation(mes.engine, scope)) {
240
- void autoRunConsolidationFromRecommendation({ deps: runner.deps, engine: mes.engine, scope, sessionId: prepared.sessionId, onNotice: runner.deps.onNotice, onError: runner.deps.onError });
242
+ const writeJudge = consolidationWriteJudgeOfLeg(prepared.syntheticWriteVerdict);
243
+ void autoRunConsolidationFromRecommendation({ deps: runner.deps, engine: mes.engine, scope, sessionId: prepared.sessionId, writeJudge, onNotice: runner.deps.onNotice, onError: runner.deps.onError });
241
244
  }
242
245
  }
243
246
  emitTrace(rs.telemetry.tracer, () => ({
@@ -262,14 +265,14 @@ export async function runSettleAndTeardown(input) {
262
265
  ts: Date.now(),
263
266
  }));
264
267
  setResult(result);
265
- if (parentToolCallId !== undefined && result.terminal.kind !== "paused") {
268
+ if (emitsChildRowFrames(prepared.hookIdentity) && result.terminal.kind !== "paused") {
266
269
  const terminalTick = {
267
270
  type: "task_progress",
268
271
  taskId: rs.telemetry.taskId,
269
272
  ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
270
273
  ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
271
274
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
272
- ...(subagentName ? { name: subagentName } : {}),
275
+ ...(prepared.hookIdentity.agentName ? { name: prepared.hookIdentity.agentName } : {}),
273
276
  ...(internals?.explicitAgentName !== undefined ? { spawnName: internals.explicitAgentName } : {}),
274
277
  model: prepared.model.id,
275
278
  usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
@@ -78,7 +78,7 @@ export interface RunTerminalAdoptionInput<R> {
78
78
  abortedLive: boolean;
79
79
  /** borrowed-readonly — the leg's snapshot of `loopLatch.userInterrupted` (gates the interruption marker). */
80
80
  userInterruptedLive: boolean;
81
- /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `checkpointStore`, `onNotice`); the one home of why it is
81
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `checkpointStore`, `onNotice`, `mediaByteCapBytes`); the one home of why it is
82
82
  * a seat and not a captured object is {@link RunnerDepsSeat}. */
83
83
  runner: RunnerDepsSeat;
84
84
  /** borrowed-readonly — the session store: the end-of-task pass touches the leg's session through it (`finish`'s teardown legs). */
@@ -43,7 +43,7 @@ export async function runTerminalAdoption(input) {
43
43
  const report = await reconcileInterruptedSession(prepared.session, prepared.toolEffects, undefined, startedToolCallIds);
44
44
  orphansClosed = report.recovered.length;
45
45
  for (const orphan of report.recovered) {
46
- emitSettled(prepared.toolOccurrences, { toolCallId: orphan.toolCallId, spelledName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), args: orphan.args }, reconciledEnd(orphan), { push: (f) => queue.push({ ...f, ...ident() }), structured: prepared.structuredProjector });
46
+ emitSettled(prepared.toolOccurrences, { toolCallId: orphan.toolCallId, spelledName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), args: orphan.args }, reconciledEnd(orphan), { push: (f) => queue.push({ ...f, ...ident() }), structured: prepared.structuredProjector, mediaByteCapBytes: runner.deps.mediaByteCapBytes });
47
47
  emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
48
48
  }
49
49
  reconcileComplete = true;
@@ -10,7 +10,7 @@ import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFi
10
10
  import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
11
11
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
12
12
  import { droppedToolDigest, withAnnounceDigests } from "./announce-once-ledger.js";
13
- import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
13
+ import { formatDiagnosticsBlock, withholdListedDiagnostics } from "../lsp-diagnostics.js";
14
14
  import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
15
15
  import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow, ENV_DUE_GOVERNANCE_READ_BUDGET_MS, GOVERNANCE_READ_STALLED, LIMIT_APPROACH_DEFAULT_THRESHOLDS, limitApproachFrames, platformLimitTerminal, raceUntilDeadline } from "./clock-and-limits.js";
16
16
  import { appendBoundaryFrames, compactionRestateOptions } from "./compaction-restate.js";
@@ -203,7 +203,7 @@ export function createTurnBoundary(input) {
203
203
  !prepared.lspDiagnostics.registry.isEmpty() &&
204
204
  !prepared.abortController.signal.aborted &&
205
205
  prepared.batchHaltRef.current === undefined) {
206
- const files = prepared.lspDiagnostics.registry.drain(prepared.lspDiagnostics.runIdent);
206
+ const files = await withholdListedDiagnostics(prepared.lspDiagnostics.registry.drain(prepared.lspDiagnostics.runIdent), prepared.lspDiagnostics.withheldOf);
207
207
  if (files.length > 0) {
208
208
  queue.push({ type: "diagnostics", files, isNew: true, ...ident() });
209
209
  const block = mintSystemReminder(formatDiagnosticsBlock(files), prepared.reminderMark);
@@ -45,7 +45,7 @@ import { streamLifecycleVerbs } from "./stream-lifecycle-verbs.js";
45
45
  import { streamHaltVerbs } from "./stream-halt-verbs.js";
46
46
  import { streamModeVerb } from "./stream-mode-verb.js";
47
47
  import { runNotificationLane } from "./run-notification-lane.js";
48
- import { assertRunnerDepsDoors } from "./construction-doors.js";
48
+ import { retainRunnerDeps } from "./construction-doors.js";
49
49
  import { runLeg } from "./run-leg.js";
50
50
  import { runTerminalAdoption } from "./run-terminal-adoption.js";
51
51
  import { runSettleAndTeardown } from "./run-settle-and-teardown.js";
@@ -77,10 +77,10 @@ export class Runner {
77
77
  static PARENT_CONSTRAINT_REGISTRY_CAP = 1024;
78
78
  depsSeat;
79
79
  constructor(deps) {
80
- assertRunnerDepsDoors(deps);
80
+ const retained = retainRunnerDeps(deps);
81
81
  const runner = this;
82
82
  this.depsSeat = { get deps() { return runner.deps; } };
83
- this.deps = withDeploymentAllowedTools(deps);
83
+ this.deps = withDeploymentAllowedTools(retained);
84
84
  if (this.deps.tiers) {
85
85
  this.deps = { ...this.deps, models: expandTiers(this.deps.models, this.deps.tiers) };
86
86
  }
@@ -121,7 +121,7 @@ export class Runner {
121
121
  if (!this.deps.toolResultStore) {
122
122
  this.deps = { ...this.deps, toolResultStore: new RunnerSharedToolResultStore({ maxTotalChars: 64_000_000 }) };
123
123
  }
124
- this.sessions = deps.sessionStore ?? new TtlSessionStore();
124
+ this.sessions = this.deps.sessionStore ?? new TtlSessionStore();
125
125
  }
126
126
  emitTaskOutcome(outcome) {
127
127
  emitTaskOutcome(this.deps, outcome);
@@ -465,10 +465,10 @@ export class Runner {
465
465
  const { effectiveTimeoutMs, walltimeMonotonicDeadline, timeout, pushContent, ownCommittedTailRef, emitCommitted } = runClockAndContent({ spec, prepared, queue, internals, rs, ident, parentToolCallId, startedToolCallIds });
466
466
  const toolProgress = createToolProgressLane(pushContent, ident, prepared.toolOccurrences);
467
467
  prepared.toolProgressRef.current = toolProgress;
468
- const { subagentName, withBrainSinks, withExtraBodyDroppedSink } = runBrainSinks({ queue, internals, rs, ident, parentToolCallId, observeReasoningWireFacts, notice: { onNotice: this.deps.onNotice, sessionId: prepared.sessionId, runId: prepared.runId } });
468
+ const { withBrainSinks, withExtraBodyDroppedSink } = runBrainSinks({ queue, internals, rs, ident, observeReasoningWireFacts, notice: { onNotice: this.deps.onNotice, sessionId: prepared.sessionId, runId: prepared.runId } });
469
469
  const toolLabels = new Map(prepared.tools.flatMap((t) => (t.label !== undefined && t.label !== t.name ? [[t.name, t.label]] : [])));
470
470
  for (const orphan of prepared.wakeRecovered) {
471
- emitSettled(prepared.toolOccurrences, { toolCallId: orphan.toolCallId, spelledName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), args: orphan.args }, reconciledEnd(orphan), { push: (f) => queue.push({ ...f, ...ident() }), structured: prepared.structuredProjector });
471
+ emitSettled(prepared.toolOccurrences, { toolCallId: orphan.toolCallId, spelledName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), args: orphan.args }, reconciledEnd(orphan), { push: (f) => queue.push({ ...f, ...ident() }), structured: prepared.structuredProjector, mediaByteCapBytes: this.deps.mediaByteCapBytes });
472
472
  emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
473
473
  }
474
474
  const postToolBatchHook = (spec.hooks ?? this.deps.hooks)?.postToolBatch;
@@ -477,7 +477,7 @@ export class Runner {
477
477
  const { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, announceWorkspaceMove } = createHarnessHandlers({
478
478
  prepared, stats, rs,
479
479
  deps: {
480
- spec, queue, internals, ident, parentToolCallId, subagentName,
480
+ spec, queue, internals, ident, mediaByteCapBytes: this.deps.mediaByteCapBytes,
481
481
  pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf,
482
482
  toolLabels, postToolBatchHook, batchArgs, internalsNotifier,
483
483
  runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info), onNotice: this.deps.onNotice },
@@ -512,7 +512,7 @@ export class Runner {
512
512
  spec, queue, prepared, rs, stats, ident, emitCommitted, startedToolCallIds, toolLabels, timeout, compactionBrain, compactionBreaker, windowSafetyOptions,
513
513
  undrainedUserAtEnd, userInputLostAtEnd, final, threw, abortedLive, userInterruptedLive, runner: this.depsSeat, sessions: this.sessions,
514
514
  next: ({ threw, comp, answerSettled, owedUserInput }) => runSettleAndTeardown({
515
- spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef,
515
+ spec, queue, prepared, resume, internals, rs, stats, ident, loopLatch, timeout, ownCommittedTailRef,
516
516
  emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, compactionBreaker, owedUserInput,
517
517
  setResult, onSuggestions, onSuspend, runner: this.depsSeat, sessions: this.sessions,
518
518
  }),
@@ -31,15 +31,22 @@
31
31
  * (the model re-reads), which is the pre-existing cost, not a new failure.
32
32
  */
33
33
  import type { AgentMessage } from "../../internal/harness.js";
34
- /** The whole text of one file at the moment the transcript recorded it. */
35
- export interface TranscriptFileRecord {
34
+ import { type UserAttachmentObservation } from "../../internal/llm.js";
35
+ /** What the transcript proves about one file: its whole text at that moment (a tool's card), or the read-state entry an
36
+ * attachment frame's delivery committed (its record's observation, verbatim — hash, view and partial-view flag). */
37
+ export type TranscriptFileRecord = {
36
38
  /** The path as the tool recorded it — absolute (see {@link wholeFileRecordsFromTranscript}). */
37
39
  path: string;
38
40
  /** The file's ENTIRE text at that moment, in the decoded coordinate the fs tools hash. */
39
41
  content: string;
40
- /** The recording tool result's own timestamp — the most honest "last read at" available. */
42
+ /** The recording message's own timestamp — the most honest "last read at" available. */
41
43
  at: number;
42
- }
44
+ } | {
45
+ path: string;
46
+ /** The entry the delivery committed. */
47
+ observation: UserAttachmentObservation["entry"];
48
+ at: number;
49
+ };
43
50
  /**
44
51
  * Per-file, the last whole-file content the transcript proves — in transcript order, last record wins.
45
52
  *
@@ -57,6 +64,9 @@ export interface TranscriptFileRecord {
57
64
  * the narrowed model-facing union: this reads the branch as the session stores it, and every lane that
58
65
  * is not a tool result falls out on the first line of the loop.
59
66
  *
67
+ * A user-attachment frame (`UserMessage.attachment` with an observation) proves the read state its delivery committed,
68
+ * keyed by the canonical key it carries — the same entry the live commit wrote, so a rebuilt partial view stays one.
69
+ *
60
70
  * Paths are reported only when the tool recorded an ABSOLUTE one. A relative spelling was resolved
61
71
  * against the tracked working directory of that moment, which this task cannot reconstruct; re-
62
72
  * resolving it here could key the entry to a different file.
@@ -1,3 +1,4 @@
1
+ import { userAttachmentOf } from "../../internal/llm.js";
1
2
  import { isAbsoluteForFamily, pathFamilyOf } from "../../tools/fs/safety.js";
2
3
  import { applyRecordedEdit } from "../../tools/fs/fs-shared.js";
3
4
  import { normalizeFileText } from "../../tools/fs/encoding.js";
@@ -64,6 +65,13 @@ function wholeFileFromReadCard(rest) {
64
65
  export function wholeFileRecordsFromTranscript(messages) {
65
66
  const byPath = new Map();
66
67
  for (const m of messages) {
68
+ const attachment = userAttachmentOf(m);
69
+ if (attachment?.observation !== undefined) {
70
+ const { key, entry } = attachment.observation;
71
+ if (isAbsoluteForFamily(pathFamilyOf({ root: key }), key))
72
+ byPath.set(key, { path: key, observation: entry, at: m.timestamp });
73
+ continue;
74
+ }
67
75
  if (m.role !== "toolResult" || m.isError)
68
76
  continue;
69
77
  const card = cardOf(m.details);
@@ -13,6 +13,9 @@ export function streamSteerVerb(input) {
13
13
  const { spec, internals, queue, acceptedSteerInputs, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, steeringError, runner } = input;
14
14
  let steerChain = Promise.resolve();
15
15
  const steer = async (text, options) => {
16
+ if (options?.userAttachments !== undefined) {
17
+ throw steeringError("a steer carries no attachments — declare them in TaskSpec.userAttachments on the next objective", "steering.invalid_content");
18
+ }
16
19
  const trusted = options?.trusted ? true : false;
17
20
  if (trusted && sanitizeUntrustedText(text) !== text) {
18
21
  throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
@@ -32,9 +32,8 @@ export declare function batchResponseDigest(result: unknown): string | undefined
32
32
  * showed an empty body for every durable-approved call. Same projection = same source as the transcript.
33
33
  * Also the one place the gate outcome reaches a frame — see the parameter. */
34
34
  export declare function toolEndBodyFrom(result: unknown, isError: boolean,
35
- /** The leg's card projector — see the doc above. A parameter, never a module default: a body minted with the engine
36
- * set alone would silently drop every declared caller card and never announce. */
37
- structured: StructuredProjector,
35
+ /** The leg's card projector (never a module default: the engine set alone drops every declared caller card unannounced) and media budget. */
36
+ leg: Pick<OccurrenceFrames, "structured" | "mediaByteCapBytes">,
38
37
  /** The gate's record of the pass that admitted or refused this call, supplied by the CALLER of this
39
38
  * projection — the end projection ({@link emitSettled}) reads it off the call's occurrence (its latest gate
40
39
  * record). Deliberately a parameter and never derived from `result`: a tool's own `details`
@@ -63,10 +62,12 @@ export type OccurrenceFrame = Omit<Extract<TaskEvent, {
63
62
  }>, keyof TaskEventIdentity> | Omit<Extract<TaskEvent, {
64
63
  type: "tool_end";
65
64
  }>, keyof TaskEventIdentity>;
66
- /** Where an occurrence's frames go: the leg's push (it stamps the identity and picks the queue) and its card projector. */
65
+ /** Where an occurrence's frames go: the leg's push (it stamps the identity and picks the queue), its card projector and its media budget. */
67
66
  export interface OccurrenceFrames {
68
67
  push(frame: OccurrenceFrame): void;
69
68
  structured: StructuredProjector;
69
+ /** `RunnerDeps.mediaByteCapBytes` as given — the request strip's budget, read through the same `mediaBudgetBytes`; required, so no site reads a second one. */
70
+ mediaByteCapBytes: number | undefined;
70
71
  }
71
72
  /** The START projection: the call's occurrence opens and its start frame goes out. Every leg that announces a call
72
73
  * comes through here (the live loop's `tool_execution_start`, the resumed leg's approved call). */
@@ -33,9 +33,9 @@ export function batchResponseDigest(result) {
33
33
  ? `${text.slice(0, BATCH_RESPONSE_MAX_CHARS)}…[+${text.length - BATCH_RESPONSE_MAX_CHARS} chars truncated]`
34
34
  : text;
35
35
  }
36
- export function toolEndBodyFrom(result, isError, structured, gate, gatedCallIdOfRun) {
37
- const o = toolOutputFrom(result);
38
- const st = structured(result);
36
+ export function toolEndBodyFrom(result, isError, leg, gate, gatedCallIdOfRun) {
37
+ const o = toolOutputFrom(result, leg.mediaByteCapBytes);
38
+ const st = leg.structured(result);
39
39
  const det = isError ? readMember(result, "details") : undefined;
40
40
  const codeRaw = readMember(det, "code");
41
41
  const kindRaw = readMember(det, "errorKind");
@@ -71,7 +71,7 @@ export function emitSettled(table, start, end, frames) {
71
71
  toolName: occ.spelledName,
72
72
  ...(occ.label !== undefined ? { label: occ.label } : {}),
73
73
  isError,
74
- ...toolEndBodyFrom(end.result, isError, frames.structured, gate, end.gatedCallIdOfRun),
74
+ ...toolEndBodyFrom(end.result, isError, frames, gate, end.gatedCallIdOfRun),
75
75
  ...(gate?.disposition.kind === "denied" && occ.wireName !== undefined ? { wireName: occ.wireName } : {}),
76
76
  ...(end.notExecuted === true ? { notExecuted: true } : {}),
77
77
  });
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * The `tool_end` output/structured projection, verbatim from
3
3
  * `runtask.ts`'s runLocked R4 phase. Pure functions over the harness `AgentToolResult`: zero closure
4
- * state, zero runtask coupling — the ONLY module-level input is the size cap below. Event-sequence
5
- * snapshot pin (recorded before/after the move, byte-equal) guards the projection semantics.
4
+ * state, zero runtask coupling — the module-level inputs are the text cap below and the shared media budget default
5
+ * (`mediaBudgetBytes`); the leg hands in its own `mediaByteCapBytes`.
6
6
  */
7
- export declare const toolOutputFrom: (result: unknown) => {
7
+ type ToolOutput = {
8
8
  output: unknown;
9
9
  truncated: boolean;
10
10
  totalChars?: number;
11
- } | undefined;
11
+ };
12
+ export declare const toolOutputFrom: (result: unknown, mediaByteCapBytes: number | undefined) => ToolOutput | undefined;
12
13
  /** The `tool_end.structured` projection over a harness tool result: the CC card when `details.type` names a card
13
14
  * the leg admits, else undefined. Minted per leg by {@link createStructuredProjector}; every body projection takes it
14
15
  * as a parameter — there is no module-level card set to fall back to. */
@@ -23,3 +24,4 @@ export declare function createStructuredProjector(opts?: {
23
24
  declaredCards?: Iterable<string>;
24
25
  onUndeclared?: (type: string) => void;
25
26
  }): StructuredProjector;
27
+ export {};
@@ -1,62 +1,81 @@
1
+ import { isTextBlock, renderContentBlock } from "../../internal/llm.js";
2
+ import { imageBytes, mediaBudgetBytes } from "../media-byte-cap.js";
1
3
  import { engineCardTypes } from "../tool-registry.js";
2
4
  import { readMember } from "../thrown-value.js";
3
5
  const MAX_TOOL_END_OUTPUT_CHARS = 16384;
4
- const safeJsonLength = (v) => {
5
- try {
6
- return JSON.stringify(v)?.length;
6
+ const marker = (n) => `…[+${n} chars truncated]`;
7
+ const isMediaBlock = (b) => {
8
+ const block = b;
9
+ return (block?.type === "image" || block?.type === "document") && typeof block.data === "string";
10
+ };
11
+ const ownChars = (b) => isTextBlock(b) ? b.text.length + (JSON.stringify({ ...b, type: undefined, text: undefined })?.length ?? 2) - 2 : isMediaBlock(b) ? b.data.length : (JSON.stringify(b)?.length ?? 0);
12
+ const boundTextLane = (blocks, lane) => {
13
+ const sized = lane.map((i) => ({ i, size: ownChars(blocks[i]) }));
14
+ const stream = sized.reduce((n, { size }, k) => n + size + (k > 0 ? 1 : 0), 0);
15
+ const forms = new Map();
16
+ let used = 0;
17
+ for (const [k, { i, size }] of sized.entries()) {
18
+ const sep = k > 0 ? 1 : 0;
19
+ if (used + sep + size <= MAX_TOOL_END_OUTPUT_CHARS) {
20
+ used += sep + size;
21
+ forms.set(i, blocks[i]);
22
+ continue;
23
+ }
24
+ const shown = renderContentBlock(blocks[i]).slice(0, Math.max(0, MAX_TOOL_END_OUTPUT_CHARS - used - sep));
25
+ const notShown = stream - Math.min(MAX_TOOL_END_OUTPUT_CHARS, used + sep) - (isTextBlock(blocks[i]) ? shown.length : 0);
26
+ forms.set(i, { type: "text", text: shown + marker(notShown) });
27
+ return { forms, cut: true };
7
28
  }
8
- catch {
9
- return undefined;
29
+ return { forms, cut: false };
30
+ };
31
+ const wireBytes = (b) => Buffer.byteLength(JSON.stringify(b)) + 1;
32
+ const SUMMARY_RESERVE = wireBytes({ type: "omitted", blocks: Number.MAX_SAFE_INTEGER, bytes: Number.MAX_SAFE_INTEGER, truncated: true }) + 2;
33
+ const fitMedia = (media, budget) => {
34
+ let used = 0;
35
+ let degraded = false;
36
+ const blocks = [];
37
+ for (const [i, m] of media.entries()) {
38
+ const cost = used + m.data.length >= budget ? Infinity : wireBytes(m);
39
+ const kept = used + cost <= budget;
40
+ const out = kept ? m : { type: m.type, ...(typeof m.mimeType === "string" && m.mimeType.length <= 255 ? { mimeType: m.mimeType } : {}), bytes: imageBytes(m), truncated: true };
41
+ const outCost = kept ? cost : wireBytes(out);
42
+ degraded ||= !kept;
43
+ if (used + outCost > budget) {
44
+ const rest = media.slice(i);
45
+ blocks.push({ type: "omitted", blocks: rest.length, bytes: rest.reduce((n, r) => n + imageBytes(r), 0), truncated: true });
46
+ break;
47
+ }
48
+ used += outCost;
49
+ blocks.push(out);
10
50
  }
51
+ return { blocks, degraded };
11
52
  };
12
- const trunc = (s) => ({
13
- text: s.slice(0, MAX_TOOL_END_OUTPUT_CHARS) + `…[+${s.length - MAX_TOOL_END_OUTPUT_CHARS} chars truncated]`,
14
- totalChars: s.length,
15
- });
16
- export const toolOutputFrom = (result) => {
17
- const content = result !== null && typeof result === "object" ? result.content : result;
18
- if (content === undefined)
19
- return undefined;
20
- if (typeof content === "string") {
21
- if (content.length <= MAX_TOOL_END_OUTPUT_CHARS)
53
+ const project = (content, mediaByteCapBytes) => {
54
+ if (!Array.isArray(content)) {
55
+ const s = typeof content === "string" ? content : (JSON.stringify(content) ?? "");
56
+ if (s.length <= MAX_TOOL_END_OUTPUT_CHARS)
22
57
  return { output: content, truncated: false };
23
- const t = trunc(content);
24
- return { output: t.text, truncated: true, totalChars: t.totalChars };
58
+ return { output: s.slice(0, MAX_TOOL_END_OUTPUT_CHARS) + marker(s.length - MAX_TOOL_END_OUTPUT_CHARS), truncated: true, totalChars: s.length };
25
59
  }
26
- let serialized;
60
+ const media = content.map(isMediaBlock);
61
+ const text = boundTextLane(content, content.flatMap((_, i) => (media[i] ? [] : [i])));
62
+ const limit = mediaBudgetBytes(mediaByteCapBytes);
63
+ const textBytes = [...text.forms.values()].reduce((n, f) => n + wireBytes(f), 0);
64
+ const fit = fitMedia(content.filter((_, i) => media[i]), limit === Infinity ? Infinity : Math.ceil((limit * 4) / 3) - SUMMARY_RESERVE - textBytes);
65
+ if (!text.cut && !fit.degraded)
66
+ return { output: content, truncated: false };
67
+ let next = 0;
68
+ const output = content.flatMap((_, i) => (media[i] ? (fit.blocks[next++] ?? []) : text.forms.has(i) ? [text.forms.get(i)] : []));
69
+ return { output, truncated: true, totalChars: content.reduce((n, b) => n + ownChars(b), 0) };
70
+ };
71
+ export const toolOutputFrom = (result, mediaByteCapBytes) => {
27
72
  try {
28
- serialized = JSON.stringify(content) ?? "";
73
+ const content = result !== null && typeof result === "object" ? result.content : result;
74
+ return content === undefined ? undefined : project(content, mediaByteCapBytes);
29
75
  }
30
76
  catch {
31
- return { output: "[unserializable tool output]", truncated: false };
32
- }
33
- if (serialized.length <= MAX_TOOL_END_OUTPUT_CHARS)
34
- return { output: content, truncated: false };
35
- let anyNonText = false;
36
- let trueChars = 0;
37
- const raw = Array.isArray(content)
38
- ? content
39
- .map((b) => {
40
- const block = b;
41
- if (block?.type === "text" && typeof block.text === "string") {
42
- trueChars += block.text.length;
43
- return block.text;
44
- }
45
- anyNonText = true;
46
- trueChars += typeof block?.data === "string" ? block.data.length : (safeJsonLength(block) ?? 0);
47
- return `[${typeof block?.type === "string" ? block.type : "block"}${typeof block?.mimeType === "string" ? `: ${block.mimeType}` : ""}]`;
48
- })
49
- .join("\n")
50
- : serialized;
51
- const totalChars = Array.isArray(content) ? trueChars : serialized.length;
52
- if (raw.length > MAX_TOOL_END_OUTPUT_CHARS) {
53
- const t = trunc(raw);
54
- return { output: t.text, truncated: true, totalChars };
55
- }
56
- if (!anyNonText) {
57
- return { output: raw, truncated: false };
77
+ return { output: "[unserializable tool output]", truncated: true };
58
78
  }
59
- return { output: raw, truncated: true, totalChars };
60
79
  };
61
80
  const CC_DETAIL_TYPES = engineCardTypes();
62
81
  export function createStructuredProjector(opts = {}) {
@@ -852,8 +852,9 @@ export interface RunnerDeps {
852
852
  * family this seat complements, not replaces — BCE stays the fleet-row lane with registry `a*`
853
853
  * handles and ticks; this seat is the leg-identity lane), and workflow-spawned agents. Frames are
854
854
  * minted at the child leg's own runner chokepoint and carry the #281 件A identity envelope plus a
855
- * terminal status summary — see {@link DelegationLifecycleEvent} for the emission points, the
856
- * per-leg semantics and the recorded honest absences. Wire it once at Runner construction; a
855
+ * terminal status summary; the `spawn` phase is the projection of the child's `task_started` frame
856
+ * (once per cycle), the `terminal` phase is per leg — see {@link DelegationLifecycleEvent} for the
857
+ * emission points and the recorded honest absences. Wire it once at Runner construction; a
857
858
  * deployment needs NO RunInternals access to observe delegation any more. Observation only — a
858
859
  * throwing or rejecting observer is contained ({@link deliverDelegationLifecycle}) and never
859
860
  * faults the child run; a PRESENT non-function value here is announced once per process and the
@@ -1446,7 +1447,9 @@ export interface RunnerDeps {
1446
1447
  * CC 2.1.196 `tengu_media_byte_cap` parity). Image reads (`Read` → ImageContent) are per-image-capped but
1447
1448
  * accumulate across turns; this bounds the SUM in the request by stripping the OLDEST images (replaced with a
1448
1449
  * marker) — request-only, non-destructive (the durable session keeps the image; the model can re-Read it).
1449
- * Default {@link AGGREGATE_MEDIA_BUDGET_BYTES}; set `Infinity`/`0` to disable.
1450
+ * The same budget bounds the media of one `tool_end.output` (spent there as base64 on the wire, x 4/3); both readers
1451
+ * read it through `mediaBudgetBytes`. Default {@link AGGREGATE_MEDIA_BUDGET_BYTES}; set `Infinity`/`0` to disable. A
1452
+ * non-number, `NaN` or a negative is refused at `new Runner` (`config.invalid_argument`), never read as "no budget".
1450
1453
  */
1451
1454
  mediaByteCapBytes?: number;
1452
1455
  /** Optional telemetry fired when {@link mediaByteCapBytes} strips media in a request (CC's
@@ -2,8 +2,8 @@
2
2
  * design/177 — the SHARED memory-store seam (connected team/deployment memory libraries the model may
3
3
  * BROWSE and READ through two tools) and its model-facing constants.
4
4
  *
5
- * Upstream anchor: CC 2.1.223's `memory_list` / `memory_read` pair. The defining property of that face
6
- * is READ/WRITE ASYMMETRY: the model side has no write channel at all — write governance is an
5
+ * Upstream anchor: CC 2.1.223's `memory_list` / `memory_read` pair, whose defining property was READ/WRITE
6
+ * ASYMMETRY (no model-side write channel); CC no longer has that shape — 2.1.296 also mounts `yd` (cli296.js:242348) "memory_write" — and sema keeps the 223 one (KL-878): write governance is an
7
7
  * environment concern, and the tool face's entire participation in it is truthfully reporting each
8
8
  * store's `writable` bit. sema keeps that structure and pushes the whole credential/consent half onto
9
9
  * the HOST (BYOM: core holds no identity face), so this module's seam is a single pull-shaped snapshot