@sema-agent/core 7.6.3 → 7.7.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 (47) hide show
  1. package/CHANGELOG.md +33 -10
  2. package/dist/agents/peer-admission.d.ts +1 -1
  3. package/dist/brain/anthropic.js +8 -2
  4. package/dist/brain/open-responses.js +5 -3
  5. package/dist/brain/openai.js +31 -8
  6. package/dist/brain/reasoning.d.ts +32 -0
  7. package/dist/brain/reasoning.js +18 -0
  8. package/dist/core/auto-mode-defaults.d.ts +16 -0
  9. package/dist/core/auto-mode-defaults.js +1 -0
  10. package/dist/core/auto-mode.d.ts +19 -0
  11. package/dist/core/auto-mode.js +74 -56
  12. package/dist/core/checkpoint-execution-record.d.ts +110 -0
  13. package/dist/core/checkpoint-execution-record.js +49 -0
  14. package/dist/core/checkpoint-store.d.ts +88 -10
  15. package/dist/core/checkpoint-store.js +35 -2
  16. package/dist/core/engine-notice.d.ts +11 -0
  17. package/dist/core/governance-codes.d.ts +1 -1
  18. package/dist/core/governance-codes.js +2 -0
  19. package/dist/core/runner/clock-and-limits.d.ts +117 -0
  20. package/dist/core/runner/clock-and-limits.js +118 -0
  21. package/dist/core/runner/contracts.d.ts +10 -0
  22. package/dist/core/runner/decide-continuation.d.ts +98 -0
  23. package/dist/core/runner/decide-continuation.js +133 -0
  24. package/dist/core/runner/execution-record.d.ts +26 -0
  25. package/dist/core/runner/execution-record.js +19 -0
  26. package/dist/core/runner/git-leg-delivery.d.ts +28 -0
  27. package/dist/core/runner/git-leg-delivery.js +94 -0
  28. package/dist/core/runner/initial-run-state.d.ts +14 -0
  29. package/dist/core/runner/initial-run-state.js +11 -0
  30. package/dist/core/runner/prepare-caps-and-workflow.js +17 -0
  31. package/dist/core/runner/prepare-run-refs.d.ts +0 -16
  32. package/dist/core/runner/prepare-run-refs.js +0 -6
  33. package/dist/core/runner/runtask.d.ts +0 -68
  34. package/dist/core/runner/runtask.js +28 -452
  35. package/dist/core/runner/steer-admission.d.ts +17 -0
  36. package/dist/core/runner/steer-admission.js +17 -0
  37. package/dist/core/runner/tool-end-body.d.ts +71 -0
  38. package/dist/core/runner/tool-end-body.js +74 -0
  39. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
  40. package/dist/core/store-contracts/checkpoint-store-contract.js +85 -0
  41. package/dist/core/trace.d.ts +24 -0
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.js +4 -3
  44. package/dist/stores/file/checkpoint-store.d.ts +7 -0
  45. package/dist/stores/file/checkpoint-store.js +20 -1
  46. package/package.json +1 -1
  47. package/test/export-surface.snapshot.json +37 -1
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The resumed leg's DECIDE vocabulary — everything the resume engine needs to read a decide outcome and
3
+ * carry it into the continuation, none of which is a step the driver calls with an Input:
4
+ * · was the decision NEGATIVE (a delivered refusal consumes its gate; no reopen compensation may fire);
5
+ * · is a replayed winner the SAME decision as the persisted one (design/80 D-1 reopen-by-reason guard,
6
+ * with the structural JSON equality it needs);
7
+ * · which call a CONTENT-ASK checkpoint parks on, and the one-shot answering face a decide's answer is
8
+ * bound to for the redeemed call;
9
+ * · the capture-time domain checks on a decide's TEXT payloads, and the label a rejected value gets;
10
+ * · the model-facing text for a deferred batch sibling, and the continuation prompt itself.
11
+ *
12
+ * Pure over its arguments (the answering face is the one closure, and it closes over its own arguments
13
+ * only). Run-loop machinery (layer 1), named without the `resume-` family prefix: that prefix marks a
14
+ * driven rung of the resume decision ladder, which this is not.
15
+ */
16
+ import { type OnQuestion, type QuestionAnswer } from "../ask-question.js";
17
+ import { type Checkpoint, type ResolvedOutcome } from "../checkpoint-store.js";
18
+ import type { ResumeRun } from "./contracts.js";
19
+ /**
20
+ * RB-471 ([2315]) — was this resume driven by a NEGATIVE human decision (plan_review/dry_run_review
21
+ * `reject`, policy_ask `deny`)? Such a decision consumes its gate by being DELIVERED: there is no owed
22
+ * action whose non-start could waste it, so the reopen compensation (both the throw-path and the
23
+ * settle-path arm) must never fire for it — reopening re-asks a gate the human already refused, and on
24
+ * the plan_review reject leg it minted a zombie gate beside the re-plan's NEW park (the session then
25
+ * reported the same decidePath locked no matter how many times the human decided).
26
+ */
27
+ export declare function resumeDecisionWasNegative(resume: ResumeRun): boolean;
28
+ /** Model-facing result for a batch sibling of a suspended call that v1 does not blind-run (§4.ter). */
29
+ export declare const DEFERRED_REISSUE: string;
30
+ /**
31
+ * Structural deep equality of two JSON-serializable values (order-independent over object keys). Used ONLY
32
+ * by the Runner's `resumeStream` design/80 D-1 reopen-by-reason guard to confirm an `env_failed` re-resume
33
+ * replays the EXACT persisted winner (its `updatedInput` payload included). This is a same-runtime
34
+ * value-vs-value comparison of two payloads that already round-trip JSON in the durable store — it is NOT
35
+ * the cross-runtime `boundInputHash` canonicalization (design/80 D-1 §2, out of scope for slice 1b), which
36
+ * the spec deliberately keeps server-minted-opaque to avoid false mismatches.
37
+ */
38
+ export declare function deepJsonEqual(a: unknown, b: unknown): boolean;
39
+ /** design/80 D-1 (reopen-by-reason): two resume winners are the SAME decision iff they bind the same call,
40
+ * the same allow/deny verdict, the same (deep-equal) `updatedInput` rewrite AND the same (deep-equal)
41
+ * content-ask `answer`. An `env_failed` re-resume must replay an identical winner; any divergence is a
42
+ * re-vote (rejected).
43
+ *
44
+ * ORDER MATTERS — `incoming` is the decide being replayed, `persisted` is the row's recorded winner. The
45
+ * `reason` dimension is compared ASYMMETRICALLY (see below), so the two arguments are not interchangeable. */
46
+ export declare function sameWinner(incoming: ResolvedOutcome, persisted: ResolvedOutcome): boolean;
47
+ /** The tool call a CONTENT-ASK checkpoint parks on — the pending `tool_approval` call of the reserved
48
+ * question tool — or `undefined` when this checkpoint parks on anything else (a side-effecting tool's
49
+ * approval, a resource slice, a review pause). The single predicate both the answer-binding guard and the
50
+ * answering-face injection key on, so "is this decide answering a question?" is asked exactly one way. */
51
+ export declare function pendingContentAskCallId(cp: Checkpoint): string | undefined;
52
+ /**
53
+ * Bind a decide's content-ask answer as the resumed leg's answering face (ruled 2026-08-04) — the same
54
+ * shape the documented live arm builds by hand (`{ ...taskConfig, onQuestion: async () => answer }`), so
55
+ * the redeemed call executes against the SAME tool and the SAME untrusted-answer fence rather than a
56
+ * second, parallel answer path.
57
+ *
58
+ * ONE-SHOT, bound to the redeemed call id: a decide answers the question the operator was shown, and
59
+ * nothing else. Should the resumed leg ask a NEW question, this face must not replay the previous answer
60
+ * to it (the questions differ; the fence's `selected ⊆ options` would strip the selections and the
61
+ * operator's note would be re-delivered out of context). It falls through to whatever answering face the
62
+ * resume already had, and — with none — refuses, which the tool reports to the model as "no answer was
63
+ * obtained", its honest headless outcome. In the durable topology this fallthrough is normally
64
+ * unreachable: a second question is adjudicated `ask` and parks for its own decide before executing.
65
+ */
66
+ export declare function answerFaceForRedeemedCall(answer: QuestionAnswer, redeemedCallId: string, base: OnQuestion | undefined,
67
+ /** Digest of the question batch the operator was shown, so redemption cannot be claimed by a
68
+ * DIFFERENT question that happens to arrive under the same call id. */
69
+ redeemedQuestionsHash: string): OnQuestion;
70
+ /**
71
+ * A label for a REJECTED deployment-supplied value, safe to put in an error message. Never coerces:
72
+ * `String(x)` on an object with no `toPrimitive` path (a null-prototype record, a throwing
73
+ * `Symbol.toPrimitive`) throws, which would replace this module's typed refusal with a raw TypeError —
74
+ * the malformed input escaping the very classification the refusal exists to give.
75
+ */
76
+ export declare function describeSuppliedValue(value: unknown): string;
77
+ /**
78
+ * Capture-time domain check for a decide outcome's TEXT payloads — the review lanes' `editedPlan` /
79
+ * `reason` and the approval lane's deny `reason`. All are persisted (the recorded winner's payload,
80
+ * compared when an `env_failed` reopen replays it) and all are fenced into the model-facing
81
+ * continuation reminder. The declared wire type is a string; a value outside it reads one way in this
82
+ * process and another after a JSON round-trip, and reaches the reminder as whatever its stringification
83
+ * happens to be. Worse, the pre-CAS tag-hygiene check and the fence BOTH reach the value through
84
+ * `String.prototype.replace`: an object carrying its own `replace` is therefore read twice and may
85
+ * answer differently each time — the check passing on text it never inspected. Refused pre-CAS so the
86
+ * row stays `pending` for a well-formed decide, exactly like the other capture-time domain refusals,
87
+ * and BEFORE the hygiene check so no guard ever consults a value outside the domain. The rejected field
88
+ * is named in the message only — `CheckpointError`'s structured `detail.field` is a closed set covering
89
+ * the approval lane's payload names, and widening it is a wire-surface change of its own.
90
+ */
91
+ export declare function assertOutcomeText(value: string | undefined, field: "editedPlan" | "reason"): void;
92
+ /**
93
+ * The continuation prompt for a resumed run (design/45 §B6). The vendored harness only exposes
94
+ * `prompt(text)` (it always appends a user message — there is no "continue from tool results" entry), so
95
+ * resume re-enters via a `<system-reminder>`-wrapped user message that tells the model the gated decision
96
+ * was applied. The "user message wart" is accepted (API-valid, and clearer than a silent continuation).
97
+ */
98
+ export declare function resumeContinuation(resume: ResumeRun, mark: string | undefined): string;
@@ -0,0 +1,133 @@
1
+ import { ASK_USER_QUESTION_TOOL_NAME, isLiveQuestionFace, markBoundOnlyQuestionFace } from "../ask-question.js";
2
+ import { boundInputHashOf } from "../canonical-json.js";
3
+ import { CheckpointError } from "../checkpoint-store.js";
4
+ import { formatHookFeedback } from "../hooks.js";
5
+ import { delimitUntrusted, REVIEWER_NOTE_MAX_BODY } from "../untrusted-text.js";
6
+ export function resumeDecisionWasNegative(resume) {
7
+ const o = resume.outcome;
8
+ return o.decision === "reject" || o.decision === "deny";
9
+ }
10
+ export const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
11
+ "NOT executed on resume. If you still need it, issue it again now.";
12
+ export function deepJsonEqual(a, b) {
13
+ if (a === b)
14
+ return true;
15
+ if (typeof a !== typeof b || a === null || b === null || typeof a !== "object") {
16
+ return false;
17
+ }
18
+ const aArr = Array.isArray(a);
19
+ const bArr = Array.isArray(b);
20
+ if (aArr !== bArr)
21
+ return false;
22
+ if (aArr && bArr) {
23
+ if (a.length !== b.length)
24
+ return false;
25
+ for (let i = 0; i < a.length; i++) {
26
+ if (!deepJsonEqual(a[i], b[i]))
27
+ return false;
28
+ }
29
+ const aExtra = Object.keys(a).filter((k) => !isCanonicalIndexKey(k, a.length));
30
+ const bExtra = Object.keys(b).filter((k) => !isCanonicalIndexKey(k, b.length));
31
+ if (aExtra.length !== bExtra.length)
32
+ return false;
33
+ return aExtra.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepJsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
34
+ }
35
+ const ao = a;
36
+ const bo = b;
37
+ const aKeys = Object.keys(ao);
38
+ const bKeys = Object.keys(bo);
39
+ if (aKeys.length !== bKeys.length)
40
+ return false;
41
+ return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k]));
42
+ }
43
+ function isCanonicalIndexKey(key, length) {
44
+ return /^(0|[1-9]\d*)$/.test(key) && Number(key) < length;
45
+ }
46
+ export function sameWinner(incoming, persisted) {
47
+ return (incoming.boundCallId === persisted.boundCallId &&
48
+ incoming.decision === persisted.decision &&
49
+ deepJsonEqual(incoming.updatedInput, persisted.updatedInput) &&
50
+ deepJsonEqual(incoming.answer, persisted.answer) &&
51
+ (persisted.reason === undefined || incoming.reason === persisted.reason));
52
+ }
53
+ export function pendingContentAskCallId(cp) {
54
+ return cp.pendingAction.kind === "tool_approval" && cp.pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME
55
+ ? cp.pendingAction.toolCallId
56
+ : undefined;
57
+ }
58
+ export function answerFaceForRedeemedCall(answer, redeemedCallId, base, redeemedQuestionsHash) {
59
+ let consumed = false;
60
+ const face = async (req, signal) => {
61
+ if (!consumed && req.toolCallId === redeemedCallId && boundInputHashOf(req.questions) === redeemedQuestionsHash) {
62
+ consumed = true;
63
+ return answer;
64
+ }
65
+ if (base !== undefined)
66
+ return base(req, signal);
67
+ throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg");
68
+ };
69
+ return isLiveQuestionFace(base) ? face : markBoundOnlyQuestionFace(face);
70
+ }
71
+ export function describeSuppliedValue(value) {
72
+ return typeof value === "string" ? value : value === null ? "null" : typeof value;
73
+ }
74
+ export function assertOutcomeText(value, field) {
75
+ if (value !== undefined && typeof value !== "string") {
76
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume \`${field}\` is not a plain string (got ${typeof value}) — a decide's text payload is an operator's plain data, not a live object; refusing pre-CAS, the checkpoint stays pending`);
77
+ }
78
+ }
79
+ export function resumeContinuation(resume, mark) {
80
+ if (resume.outcome.gate === "wake") {
81
+ return formatHookFeedback("You were WOKEN from a parked pause by an operator. Before continuing, re-orient from the workspace: " +
82
+ "run `git status` and review your recent changes / last commits to confirm what is already done, then " +
83
+ "continue the remaining work. Do NOT restart the task or re-run work that is already committed.", mark);
84
+ }
85
+ if (resume.outcome.gate === "resource_limit") {
86
+ return formatHookFeedback("You were resumed after a pause. Before continuing, re-orient from the workspace: run `git status` and " +
87
+ "review your recent changes / last commits to confirm what is already done, then continue the " +
88
+ "remaining work. Do NOT restart the task or re-run work that is already committed.", mark);
89
+ }
90
+ if (resume.outcome.gate === "dry_run_review") {
91
+ const verdict = resume.outcome.decision === "approve"
92
+ ? "Your predicted change was REVIEWED and APPROVED; it has been applied"
93
+ : `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}; it was NOT applied`;
94
+ return formatHookFeedback(`You were resumed after a dry-run review. ${verdict}. Before continuing, re-orient from the workspace ` +
95
+ "(run `git status` and review your recent changes) to confirm the current state, then continue the " +
96
+ "remaining work. Do NOT restart the task or re-run work that is already done.", mark);
97
+ }
98
+ if (resume.outcome.gate === "plan_review") {
99
+ if (resume.outcome.decision === "approve") {
100
+ return formatHookFeedback("Your proposed PLAN was REVIEWED and APPROVED. Proceed with that plan now — begin executing it. " +
101
+ "This is a RESUMED task; do NOT re-plan or restart from scratch, just carry out the approved plan.", mark);
102
+ }
103
+ if (resume.outcome.decision === "edit") {
104
+ if (!resume.outcome.editedPlan) {
105
+ return formatHookFeedback("Your proposed PLAN was REVIEWED and EDITED, but no revised plan text was supplied — proceed with your " +
106
+ "ORIGINAL plan as-is, begin executing it now. This is a RESUMED task; do NOT re-plan or restart from scratch.", mark);
107
+ }
108
+ return formatHookFeedback("Your proposed PLAN was REVIEWED and EDITED by a human reviewer. Proceed with the REVISED plan below " +
109
+ "(it supersedes your earlier plan); begin executing it now. This is a RESUMED task; do NOT re-plan " +
110
+ "or restart from scratch, just carry out the revised plan.\n\nThe REVISED plan to follow is:\n" +
111
+ delimitUntrusted("revised plan", resume.outcome.editedPlan), mark);
112
+ }
113
+ const why = resume.outcome.reason ? ` Reviewer note: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : "";
114
+ return formatHookFeedback(`Your proposed PLAN was REVIEWED and REJECTED; it was NOT executed.${why} Produce a NEW plan that ` +
115
+ "addresses the concern, then continue. This is a RESUMED task — re-plan from the current state; do " +
116
+ "NOT execute the rejected plan.", mark);
117
+ }
118
+ const { pendingAction } = resume.cp;
119
+ if (pendingAction.kind !== "tool_approval") {
120
+ return formatHookFeedback("This is a RESUMED task — continue from where you left off without restarting or re-running prior work.", mark);
121
+ }
122
+ const decided = resume.outcome.decision === "allow"
123
+ ? "was APPROVED and has now been executed — its result is in the tool results above"
124
+ : `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}`;
125
+ const remaining = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !pendingAction.completedCallIds.includes(id)).length;
126
+ const batchNote = remaining > 0
127
+ ? ` ${remaining} other tool call(s) from that batch were returned as deferred and not run — re-issue any you still need.`
128
+ : "";
129
+ return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}.${batchNote} ` +
130
+ `This is a RESUMED task — every step before this point is already done and its results are in the ` +
131
+ `conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` +
132
+ `exact point, building on the existing results, and finish the remaining work.`, mark);
133
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The execution-record settle half of the pending-call resolver's belt (run-loop machinery, layer 1):
3
+ * what the resolver does with the word the row's `recordExecutionOutcome` verb answered. The verb itself
4
+ * is bound by `resumeStream` (ResumeRun.recordExecutionOutcome); the resolver calls it once per
5
+ * resolution, right after the resolved call's `tool_end` frame, and hands the answer here. The rule is a
6
+ * table lookup, not a condition list: the words that leave the row unrecorded
7
+ * (`EXECUTION_RECORD_LEAVES_ROW_UNRECORDED`) announce `checkpoint.execution_outcome_unrecorded`; the
8
+ * others say nothing. The execution result is never changed by the answer — the record is an account of
9
+ * what ran, not a gate on it — so this module has no return value and no throw of its own (a conflict is
10
+ * the verb's own throw and propagates past the resolver untouched).
11
+ */
12
+ import { type ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
13
+ import type { GateOutcome } from "../gate-outcome.js";
14
+ import { type RunnerDeps } from "../types.js";
15
+ /** The facts the unrecorded notice carries — all from the resumed leg; the secret token never rides. */
16
+ export interface ExecutionRecordFacts {
17
+ toolName: string;
18
+ gate: GateOutcome;
19
+ sessionId: string;
20
+ runId: string;
21
+ scope: string;
22
+ checkpointId?: string;
23
+ }
24
+ /** Settle the record verb's answer: announce the unrecorded words through the deployment's notice sink;
25
+ * the recorded words are silent. */
26
+ export declare function settleExecutionRecord(onNotice: RunnerDeps["onNotice"], word: ExecutionOutcomeRecordWord, facts: ExecutionRecordFacts): void;
@@ -0,0 +1,19 @@
1
+ import { EXECUTION_RECORD_LEAVES_ROW_UNRECORDED } from "../checkpoint-execution-record.js";
2
+ import { deliverEngineNotice } from "../types.js";
3
+ export function settleExecutionRecord(onNotice, word, facts) {
4
+ if (!EXECUTION_RECORD_LEAVES_ROW_UNRECORDED[word])
5
+ return;
6
+ deliverEngineNotice(onNotice, {
7
+ code: "checkpoint.execution_outcome_unrecorded",
8
+ message: `The resolved tool call "${facts.toolName}" was disposed (${facts.gate.disposition.kind}) but its execution record could not be filed on the checkpoint: ` +
9
+ `the store answered "${word}" — the row is no longer a resolved row of this scope (reopened, expired or removed under this leg). ` +
10
+ `The execution result stands as delivered; the row reads unknown on its execution axis.`,
11
+ detail: {
12
+ sessionId: facts.sessionId,
13
+ runId: facts.runId,
14
+ scope: facts.scope,
15
+ ...(facts.checkpointId !== undefined ? { checkpointId: facts.checkpointId } : {}),
16
+ word,
17
+ },
18
+ });
19
+ }
@@ -0,0 +1,28 @@
1
+ import type { MaybeCompactOptions } from "../auto-compaction.js";
2
+ import type { Prepared } from "./contracts.js";
3
+ /** env-tail migration — the git frame's authority shell: the single-point F3 wrap the first-frame
4
+ * listings use (one `<system-reminder>` per body, body neutralized — idempotent over the already-
5
+ * sanitized snapshot; the S2 breakout posture). */
6
+ export declare function wrapGitFrame(body: string, mark: string | undefined): string;
7
+ /**
8
+ * env-tail migration — the git lane's leg-boundary decision: read ladder (BRANCH AUTHORITY) then
9
+ * the `(kind, hash)` compare. Ladder rungs:
10
+ * ① nearest mirror on the active session branch — a PENDING carrier (or an announced carrier
11
+ * whose frame entry is off the branch) is a MANDATORY re-announce gate, overriding everything;
12
+ * ② the checkpoint mirror, only when the branch carries no mirror at all — its own `entryId`
13
+ * must sit on the active branch to count as announced (a rewound frame under a descendant
14
+ * checkpoint reads as pending);
15
+ * ③ full absence ⇒ conservative re-announce (duplicate-tolerant by design).
16
+ * Seeds `ref.announced` with the resolved prior (pendingness preserved — a suspend before the
17
+ * receipt then mirrors the honest state) and returns the frame BODY to deliver, or undefined when
18
+ * the announced view already matches (or the lane has nothing to say this leg). Tombstone kinds
19
+ * fire on the availability FLIP EDGE only: with no prior view at all there is nothing to disown —
20
+ * no frame (the probe's own onError already told the deployment).
21
+ */
22
+ export declare function resolveGitLegDelivery(prepared: Prepared, cpMirror: unknown, report: (err: Error) => void): Promise<string | undefined>;
23
+ /** env-tail migration — the compaction interaction option for one `maybeCompact` invocation (ALL
24
+ * THREE routes — turn-boundary auto, prompt-too-long recovery, end-of-task finish — thread this
25
+ * same builder, the single-source requirement): the current tuple restated as PENDING on the
26
+ * compaction entry (same CAS as the new baseline) + the run's re-assert closure as the landing
27
+ * hook. Empty when the lane never announced anything (nothing to preserve across the summary). */
28
+ export declare function gitRestateOption(prepared: Prepared): Pick<MaybeCompactOptions, "gitRestate">;
@@ -0,0 +1,94 @@
1
+ import { gitFrameContextVisible, normalizeGitAnnouncement } from "../../internal/harness.js";
2
+ import { mintSystemReminder } from "../reminder-mint.js";
3
+ import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
4
+ import { branchCarriesVisiblePositiveGitFrame, newestEngineGitFrame } from "./git-status-frame.js";
5
+ export function wrapGitFrame(body, mark) {
6
+ return mintSystemReminder(sanitizeUntrustedText(body, SHELLED_BODY_ENVELOPE_TAGS), mark);
7
+ }
8
+ export async function resolveGitLegDelivery(prepared, cpMirror, report) {
9
+ const ref = prepared.gitStatusRef;
10
+ const frame = ref.frame;
11
+ if (frame === undefined)
12
+ return undefined;
13
+ let prior;
14
+ let pending = false;
15
+ try {
16
+ const branchReadout = await prepared.session.getGitAnnouncement?.();
17
+ if (branchReadout !== undefined) {
18
+ if (branchReadout.status === "pending") {
19
+ pending = true;
20
+ prior = { kind: branchReadout.kind, hash: branchReadout.hash };
21
+ }
22
+ else {
23
+ prior = { kind: branchReadout.kind, hash: branchReadout.hash, ...(branchReadout.entryId !== undefined ? { entryId: branchReadout.entryId } : {}) };
24
+ }
25
+ }
26
+ else {
27
+ const shaped = normalizeGitAnnouncement(cpMirror);
28
+ if (shaped !== undefined) {
29
+ if (shaped.pending === true || shaped.entryId === undefined) {
30
+ pending = true;
31
+ prior = { kind: shaped.kind, hash: shaped.hash };
32
+ }
33
+ else if (gitFrameContextVisible(await prepared.session.getBranch(), shaped.entryId)) {
34
+ prior = { kind: shaped.kind, hash: shaped.hash, entryId: shaped.entryId };
35
+ }
36
+ else {
37
+ pending = true;
38
+ prior = { kind: shaped.kind, hash: shaped.hash };
39
+ }
40
+ }
41
+ }
42
+ }
43
+ catch (err) {
44
+ pending = true;
45
+ report(err instanceof Error ? err : new Error(String(err)));
46
+ }
47
+ if (prior !== undefined && !pending && prior.entryId !== undefined) {
48
+ try {
49
+ const newest = newestEngineGitFrame(await prepared.session.getBranch(), prepared.reminderMark);
50
+ if (newest === undefined || newest.entryId !== prior.entryId)
51
+ pending = true;
52
+ }
53
+ catch (err) {
54
+ pending = true;
55
+ report(err instanceof Error ? err : new Error(String(err)));
56
+ }
57
+ }
58
+ if (prior !== undefined)
59
+ ref.announced = pending ? { ...prior, pending: true } : { ...prior };
60
+ const negative = frame.kind === "unavailable" || frame.kind === "non-repo";
61
+ if (negative) {
62
+ if (prior === undefined && !pending) {
63
+ let mustDisown;
64
+ try {
65
+ mustDisown = branchCarriesVisiblePositiveGitFrame(await prepared.session.getBranch(), prepared.reminderMark);
66
+ }
67
+ catch (err) {
68
+ mustDisown = true;
69
+ report(err instanceof Error ? err : new Error(String(err)));
70
+ }
71
+ return mustDisown ? frame.body : undefined;
72
+ }
73
+ if (prior !== undefined && !pending && prior.kind === frame.kind && prior.hash === frame.hash)
74
+ return undefined;
75
+ return frame.body;
76
+ }
77
+ if (!pending && prior !== undefined && prior.kind === frame.kind && prior.hash === frame.hash) {
78
+ ref.protectedText = wrapGitFrame(frame.body, prepared.reminderMark);
79
+ if (frame.kind === "full" && frame.shrunk !== undefined) {
80
+ ref.wrappedShrink = { find: ref.protectedText, replace: wrapGitFrame(frame.shrunk.body, prepared.reminderMark) };
81
+ }
82
+ return undefined;
83
+ }
84
+ return frame.body;
85
+ }
86
+ export function gitRestateOption(prepared) {
87
+ const ref = prepared.gitStatusRef;
88
+ if (ref.frame === undefined || ref.announced === undefined || ref.reassert === undefined)
89
+ return {};
90
+ const use = ref.overBudgetShrunk && ref.frame.shrunk !== undefined
91
+ ? { kind: "degraded", hash: ref.frame.shrunk.hash }
92
+ : { kind: ref.frame.kind, hash: ref.frame.hash };
93
+ return { gitRestate: { pending: use, land: ref.reassert } };
94
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The zero-value constructor of {@link RunState}, the run loop's ONE shared mutable record (seven groups,
3
+ * sixty fields; its writers are the run loop's three top-level machines, see the interface's own doc in
4
+ * contracts.ts). The driver mints one per run leg and hands it to every machine as a `borrowed-mutable`
5
+ * seat; nothing here reads or writes host state, so the skeleton is machinery, not a phase.
6
+ *
7
+ * Named without the `run-` family prefix on purpose: that prefix marks a DRIVEN run lane (one the driver
8
+ * calls with an Input and reads a Result from — the phase-api gate judges every such file), and the
9
+ * layering registry's run-lanes stratum is a glob over the same prefix. A machinery module the lanes stand
10
+ * on sits one layer below them under a name of its own, exactly as the prepare-path machinery does.
11
+ */
12
+ import type { RunState } from "./contracts.js";
13
+ /** Zero-value skeleton — every REAL value is assigned at the field's original declaration site. */
14
+ export declare function createRunState(): RunState;
@@ -0,0 +1,11 @@
1
+ export function createRunState() {
2
+ return {
3
+ telemetry: { cacheFamily: "input-excludes-cached", pricing: { inputPer1M: 0, outputPer1M: 0 }, pricingConfigured: true, unpricedSpend: false, tracer: undefined, taskId: "", runId: "", taskStart: 0, taskStartMonotonic: 0, cacheBreakReported: false },
4
+ degrade: { degradeToModel: undefined, degraded: undefined, outputErrorStreak: 0, outputInvalid: false, recordDegraded: () => { } },
5
+ limits: { turnsExceeded: false, budgetHit: undefined, budgetAxis: undefined, platformTerminal: undefined, outputRetryCap: 0, effectiveMaxTurns: undefined },
6
+ budget: { remainingMicroUsd: undefined, maxCostMicroUsd: undefined, remainingTokens: undefined, maxTokensWindow: undefined, overBudget: () => undefined, streamCancel: false, callOutputChars: 0, lastStreamBudgetCheck: 0, projectedOverBudget: () => undefined },
7
+ turn: { callStartAt: undefined, firstTokenAt: undefined, turnUsage: undefined, turnUsageMissing: false, turnStopReason: undefined, lastTurnHadToolCalls: false, toolBatch: [] },
8
+ counters: { approachNoticesSent: 0, walltimeSyncBackstopFired: false, compactionFloor: 0, trimForceBackoff: false, repetitionCuts: 0, repetitionSpared: 0, repetitionEvents: [], REPETITION_EVENTS_CAP: 0, preemptIgnoredReported: false, wroteThisRun: false, finalVerifyInjections: 0, groundingSignalPreR9: false, groundingSignalPostR9: false, cadenceTurns: 0 },
9
+ attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, sizeGuidelineState: undefined, attachmentsInjected: 0 },
10
+ };
11
+ }
@@ -5,7 +5,9 @@ import { CROSS_SESSION_CLASSIFIER_RULE } from "../../agents/cross-session-envelo
5
5
  import { createRunWorkflowTool, RUN_WORKFLOW_TOOL_NAME } from "../../orchestration/run-workflow-tool.js";
6
6
  import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-runner.js";
7
7
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
8
+ import { thinkingOffExpressible } from "../../brain/reasoning.js";
8
9
  import { autoModeArmingRecipeOf } from "../auto-mode-arming.js";
10
+ import { AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "../auto-mode-defaults.js";
9
11
  import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
10
12
  import { createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
11
13
  import { resolveCheckpointStore } from "../checkpoint-store.js";
@@ -14,6 +16,7 @@ import { createPresentPlanTool, createEnterPlanModeTool } from "../present-plan-
14
16
  import { resolveTaskModel } from "../roles.js";
15
17
  import { brainToRuntime } from "../runtime.js";
16
18
  import { defaultTaskRegistry } from "../task-registry.js";
19
+ import { emitTrace } from "../trace.js";
17
20
  import { defineTool, isDefineToolProduct } from "../tools.js";
18
21
  import { derivedRouteFallsBack } from "./derived-route-fallback.js";
19
22
  import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
@@ -156,11 +159,23 @@ export async function prepareCapsAndWorkflow(input) {
156
159
  const classifierLaneRule = peerLaneActive && peerSendMessageBuiltIn;
157
160
  const classifierSystemPrompt = buildAutoModePrompt(classifierLaneRule ? { ...am, crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : am);
158
161
  const classifierRuntime = brainToRuntime(deps.brain);
162
+ const classifierCap = thinkingOffExpressible(classifierModel) ? { maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS } : {};
159
163
  autoModeDenialTracking = createAutoModeDenialTracker(am.denialLimit);
160
164
  autoModeDecider = createAutoModeDecider({
161
165
  ...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
162
166
  ...(am.failureThreshold !== undefined ? { failureThreshold: am.failureThreshold } : {}),
163
167
  ...(am.onBreakerOpen !== undefined ? { onBreakerOpen: am.onBreakerOpen } : {}),
168
+ onClassified: (info) => emitTrace(deps.tracer, () => ({
169
+ kind: "auto_mode.classified",
170
+ version: 1,
171
+ taskId: hostTaskId,
172
+ toolCallId: info.toolCallId,
173
+ model: classifierModel.id,
174
+ ms: info.ms,
175
+ verdict: info.verdict,
176
+ ...(info.cause !== undefined ? { cause: info.cause } : {}),
177
+ ts: Date.now(),
178
+ })),
164
179
  classify: async (input, signal) => {
165
180
  const ctx = await session.buildContext();
166
181
  const known = ctx.messages.filter((m) => !isSyntheticApiErrorMessage(m) &&
@@ -169,6 +184,8 @@ export async function prepareCapsAndWorkflow(input) {
169
184
  const classifierAuth = await spec.getApiKeyAndHeaders?.(classifierModel);
170
185
  const response = await classifierRuntime.completeSimple(classifierModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] }, {
171
186
  signal,
187
+ reasoning: "off",
188
+ ...classifierCap,
172
189
  ...(classifierAuth?.apiKey !== undefined ? { apiKey: classifierAuth.apiKey } : {}),
173
190
  ...(classifierAuth?.headers !== undefined ? { headers: classifierAuth.headers } : {}),
174
191
  });
@@ -86,20 +86,4 @@ export interface PrepareRunRefsResult {
86
86
  worktreeIsolation: SubagentWorktreeIsolation | undefined;
87
87
  }
88
88
  /** The M3a phase body — prepareTask's run-refs stretch, verbatim (see the module header). */
89
- /**
90
- * A run that opted into `forwardSubagentEvents` may have BACKGROUND children whose frames are forwarded to the
91
- * deployment's sink from the child's own run. Those forwards are scheduled on later microtasks than the parent's
92
- * turn, so "the parent stream ended" and "the child's already-emitted frames reached the sink" are two different
93
- * moments — the gap is one microtask ladder deep and moves whenever the gate's station structure changes (design/393
94
- * / #594: the three-layer gate added three async frames to a child's gated call, and a consumer that read the parent
95
- * to `done` then inspected the sink lost the child's `tool_end`). The rule is not a tick count: **before the parent
96
- * says `done`, every frame a child had already handed to the forward channel is delivered.** One macrotask turn is
97
- * the smallest boundary that strictly orders after all pending microtasks; it is taken only on the opted-in run,
98
- * only once, right before `done`.
99
- */
100
- export declare function forwardsSubagentEvents(spec: Pick<TaskSpec, "forwardSubagentEvents">): boolean;
101
- /** The drain itself — one macrotask turn. Callers gate it with {@link forwardsSubagentEvents} SYNCHRONOUSLY so a
102
- * non-forwarding run's terminal push is not one microtask later than it was (an `await` of an early-returning
103
- * async function still yields once; the black-box round measured that tick). */
104
- export declare function drainForwardedFramesBeforeDone(): Promise<void>;
105
89
  export declare function prepareRunRefs(input: PrepareRunRefsInput): PrepareRunRefsResult;
@@ -2,12 +2,6 @@ import { createSubagentWorktreeHelper } from "../../agents/subagent.js";
2
2
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
3
3
  import { ActiveSkillScope } from "./active-skill-scope.js";
4
4
  import { createEditedFilesLedger } from "./edited-files-ledger.js";
5
- export function forwardsSubagentEvents(spec) {
6
- return spec.forwardSubagentEvents === true;
7
- }
8
- export function drainForwardedFramesBeforeDone() {
9
- return new Promise((resolve) => setImmediate(resolve));
10
- }
11
5
  export function prepareRunRefs(input) {
12
6
  const { spec, internals, executionEnv, taskRootFinal } = input;
13
7
  const { note: noteFileEdited, snapshot: editedFilesSnapshot } = createEditedFilesLedger();
@@ -3,77 +3,9 @@ import { type CheckpointToken, type ResumeOutcome } from "../checkpoint-store.js
3
3
  import { type TaskOutcome } from "../task-outcome.js";
4
4
  import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
5
5
  import type { SessionStore } from "../session.js";
6
- import { type RecoveredOrphan } from "../session-reconcile.js";
7
- import type { GateOutcome } from "../gate-outcome.js";
8
- import { type McpDelivered } from "../mcp-failure.js";
9
6
  import type { AgentDefinition, ModelRef, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
10
7
  import type { ResumeRun, ResumeTaskConfig, RunInternals, RunnerSelfSeat } from "./contracts.js";
11
8
  export type { ResumeTaskConfig } from "./contracts.js";
12
- /** The `tool_end` body fields projected from a harness tool result — output/truncated/totalChars via
13
- * {@link toolOutputFrom} and the CC card via {@link structuredFrom}. Single construction point for BOTH
14
- * the live loop's frames and the resumed batch's frames (`resolvePendingCall` + the deferred-sibling
15
- * close): the resumed frames used to carry only `isError`, so a client rendering tool output from frames
16
- * showed an empty body for every durable-approved call. Same projection = same source as the transcript.
17
- * Also the one place the gate outcome reaches a frame — see the parameter. */
18
- declare function toolEndBodyFrom(result: unknown, isError: boolean,
19
- /** The gate's record of the pass that admitted or refused this call, supplied by the CALLER of this
20
- * projection — the live loop reads it off the gate's per-call sideband, the resumed leg off the decide's
21
- * minted record. Deliberately a parameter and never derived from `result`: a tool's own `details`
22
- * (which post-tool hooks may also replace) is writable by layers that adjudicate nothing, so reading
23
- * provenance out of it would let a failing tool claim a person approved it. Omitted ⇒ the call never
24
- * went through the gate (see the `tool_end.gate` doc, the one home). */
25
- gate?: GateOutcome,
26
- /** WHICH call this run's committed durable park is holding ({@link gatedCallIdOf}), for the frames the
27
- * abort short-circuits. Same never-derived-from-`result` posture as the outcome above, and for the
28
- * sharpest version of that reason: this one is an assertion about a DIFFERENT call, so a tool able to
29
- * author it could point an approval UI at a call nobody is waiting on. Omitted ⇒ no park is holding a
30
- * call (nothing parked, or the park that did holds none), and the frame then carries no id at all. */
31
- gatedCallIdOfRun?: string): {
32
- output?: unknown;
33
- truncated?: boolean;
34
- totalChars?: number;
35
- structured?: unknown;
36
- errorCode?: string;
37
- delivered?: McpDelivered;
38
- gatedCallId?: string;
39
- gate?: GateOutcome;
40
- };
41
- /**
42
- * scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
43
- * construction point for BOTH minting legs (the live-abort loop at the end of a run, and the wake/crash
44
- * leg's replay at run open), so the two can never disagree about the shape of the same event.
45
- *
46
- * Both frames used to carry `isError:true` and NOTHING else: a consumer rendering tool output from the
47
- * event stream showed an EMPTY body for every interrupted call, even though the persisted transcript
48
- * (which the model reads) carried the full `[INTERRUPTED]` explanation — the two faces of one call
49
- * disagreed. The projection goes through the same {@link toolEndBodyFrom} every live tool result uses, so
50
- * `output` = the persisted model-facing text and `errorCode` = the persisted `details.errorKind`
51
- * (`interrupted_never_started` / `interrupted_outcome_unknown`) — a consumer discriminates on the code
52
- * instead of prose-matching. No `structured`: the reconcile mints no CC card (no `details.type`), which
53
- * `structuredFrom`'s allowlist already enforces.
54
- */
55
- export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind">): ReturnType<typeof toolEndBodyFrom>;
56
- export declare const GOVERNANCE_READ_STALLED: unique symbol;
57
- /** Await a ledger charge, firing `onSlow` ONCE if it has not settled after
58
- * {@link CHARGE_SETTLE_DISCLOSE_MS}. The charge itself is always awaited to completion. */
59
- export declare function awaitChargeWithSlowDisclosure<T>(charge: Promise<T>, onSlow: () => void, discloseAfterMs?: number): Promise<T>;
60
- /**
61
- * Await `p` until the absolute epoch `deadline`, then give up with {@link GOVERNANCE_READ_STALLED}.
62
- *
63
- * Chunked, and deliberately so: the deadline is derived from a caller-declared environment lifetime, so
64
- * the distance to it can exceed {@link MAX_TIMER_DELAY_MS} — a single timer armed for that distance
65
- * would fire at once and abandon a perfectly healthy read. Each chunk re-reads the wall clock, so the
66
- * decision is always made against the deadline itself rather than against an allowance computed once.
67
- *
68
- * Exported for its own unit pin (the same posture as the brain-call guardrail primitive): the
69
- * behaviors below are clock behaviors, and only a virtual clock can assert them without spending the
70
- * wall-clock time they describe. Not re-exported from the package index.
71
- *
72
- * An already-overdue deadline still gets ONE zero-delay pass: a promise that is settled (or settles in a
73
- * microtask, which is every in-process store) must be allowed to win, because giving up on an answer we
74
- * already hold would be a fabricated stall. The second pass is what makes the loop terminate.
75
- */
76
- export declare function raceUntilDeadline<T>(p: Promise<T>, deadline: number): Promise<T | typeof GOVERNANCE_READ_STALLED>;
77
9
  /**
78
10
  * A stateless task runner. Holds shared deps (the external brain, model catalog) and an
79
11
  * in-memory session store so that passing a `sessionId` continues a prior conversation.