@sema-agent/core 7.6.2 → 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.
- package/CHANGELOG.md +36 -6
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/brain/anthropic.js +8 -2
- package/dist/brain/open-responses.js +5 -3
- package/dist/brain/openai.js +31 -8
- package/dist/brain/reasoning.d.ts +32 -0
- package/dist/brain/reasoning.js +18 -0
- package/dist/core/auto-mode-defaults.d.ts +16 -0
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +19 -0
- package/dist/core/auto-mode.js +74 -56
- package/dist/core/checkpoint-execution-record.d.ts +110 -0
- package/dist/core/checkpoint-execution-record.js +49 -0
- package/dist/core/checkpoint-store.d.ts +88 -10
- package/dist/core/checkpoint-store.js +35 -2
- package/dist/core/engine-notice.d.ts +11 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/runner/clock-and-limits.d.ts +117 -0
- package/dist/core/runner/clock-and-limits.js +118 -0
- package/dist/core/runner/contracts.d.ts +10 -0
- package/dist/core/runner/decide-continuation.d.ts +98 -0
- package/dist/core/runner/decide-continuation.js +133 -0
- package/dist/core/runner/execution-record.d.ts +26 -0
- package/dist/core/runner/execution-record.js +19 -0
- package/dist/core/runner/git-leg-delivery.d.ts +28 -0
- package/dist/core/runner/git-leg-delivery.js +94 -0
- package/dist/core/runner/initial-run-state.d.ts +14 -0
- package/dist/core/runner/initial-run-state.js +11 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +17 -0
- package/dist/core/runner/prepare-run-refs.d.ts +0 -12
- package/dist/core/runner/prepare-run-refs.js +0 -5
- package/dist/core/runner/runtask.d.ts +0 -68
- package/dist/core/runner/runtask.js +28 -450
- package/dist/core/runner/steer-admission.d.ts +17 -0
- package/dist/core/runner/steer-admission.js +17 -0
- package/dist/core/runner/tool-end-body.d.ts +71 -0
- package/dist/core/runner/tool-end-body.js +74 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +85 -0
- package/dist/core/trace.d.ts +24 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/stores/file/checkpoint-store.d.ts +7 -0
- package/dist/stores/file/checkpoint-store.js +20 -1
- package/package.json +1 -1
- 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,16 +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 drainForwardedFramesBeforeDone(spec: Pick<TaskSpec, "forwardSubagentEvents">): Promise<void>;
|
|
101
89
|
export declare function prepareRunRefs(input: PrepareRunRefsInput): PrepareRunRefsResult;
|
|
@@ -2,11 +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 async function drainForwardedFramesBeforeDone(spec) {
|
|
6
|
-
if (spec.forwardSubagentEvents !== true)
|
|
7
|
-
return;
|
|
8
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
9
|
-
}
|
|
10
5
|
export function prepareRunRefs(input) {
|
|
11
6
|
const { spec, internals, executionEnv, taskRootFinal } = input;
|
|
12
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.
|