@sema-agent/core 7.11.0 → 7.11.2
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 +39 -1
- package/dist/core/auto-mode-defaults.d.ts +13 -9
- package/dist/core/auto-mode-defaults.js +1 -1
- package/dist/core/governance-codes.d.ts +1 -0
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/runner/compaction-knobs.d.ts +45 -0
- package/dist/core/runner/compaction-knobs.js +3 -0
- package/dist/core/runner/contracts.d.ts +78 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +23 -8
- package/dist/core/runner/run-attachment-seats.d.ts +20 -0
- package/dist/core/runner/run-attachment-seats.js +187 -0
- package/dist/core/runner/run-brain-sinks.d.ts +29 -0
- package/dist/core/runner/run-brain-sinks.js +61 -0
- package/dist/core/runner/run-clock-and-content.d.ts +52 -0
- package/dist/core/runner/run-clock-and-content.js +26 -0
- package/dist/core/runner/run-compaction-machinery.d.ts +35 -0
- package/dist/core/runner/run-compaction-machinery.js +98 -0
- package/dist/core/runner/run-git-lane.d.ts +64 -0
- package/dist/core/runner/run-git-lane.js +102 -0
- package/dist/core/runner/run-identity-wiring.d.ts +96 -0
- package/dist/core/runner/run-identity-wiring.js +84 -0
- package/dist/core/runner/run-reasoning-seat.d.ts +27 -0
- package/dist/core/runner/run-reasoning-seat.js +48 -0
- package/dist/core/runner/run-recovery-lanes.d.ts +54 -0
- package/dist/core/runner/run-recovery-lanes.js +180 -0
- package/dist/core/runner/run-stop-and-final-verify.d.ts +32 -0
- package/dist/core/runner/run-stop-and-final-verify.js +159 -0
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +38 -0
- package/dist/core/runner/run-telemetry-and-budget-seats.js +159 -0
- package/dist/core/runner/run-tool-mount-facts.d.ts +26 -0
- package/dist/core/runner/run-tool-mount-facts.js +58 -0
- package/dist/core/runner/run-turn-boundary.d.ts +0 -35
- package/dist/core/runner/run-turn-boundary.js +1 -3
- package/dist/core/runner/runtask.js +126 -1624
- package/dist/core/runner/stream-halt-verbs.d.ts +38 -0
- package/dist/core/runner/stream-halt-verbs.js +82 -0
- package/dist/core/runner/stream-lifecycle-verbs.d.ts +34 -0
- package/dist/core/runner/stream-lifecycle-verbs.js +126 -0
- package/dist/core/runner/stream-reap.d.ts +30 -0
- package/dist/core/runner/stream-reap.js +40 -0
- package/dist/core/runner/stream-settle-backstop.d.ts +38 -0
- package/dist/core/runner/stream-settle-backstop.js +113 -0
- package/dist/core/runner/stream-steer-verb.d.ts +30 -0
- package/dist/core/runner/stream-steer-verb.js +185 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +5 -0
- package/dist/tools/fs/bash-readonly-classifier.js +1 -0
- package/dist/tools/fs/fs-bash.js +6 -4
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { resolveReasoning } from "../../brain/reasoning.js";
|
|
2
|
+
import { emitTrace } from "../trace.js";
|
|
3
|
+
export function runReasoningSeat(input) {
|
|
4
|
+
const { prepared, rs, taskIdRef, reasoningResolution } = input;
|
|
5
|
+
reasoningResolution.current = prepared.thinking && prepared.thinking !== "off" ? resolveReasoning(prepared.thinking, prepared.model) : undefined;
|
|
6
|
+
const publishReasoningResolution = (r) => {
|
|
7
|
+
reasoningResolution.current = r;
|
|
8
|
+
if (taskIdRef)
|
|
9
|
+
taskIdRef.effectiveReasoning = r;
|
|
10
|
+
emitTrace(rs.telemetry.tracer, () => ({
|
|
11
|
+
kind: "reasoning.resolved",
|
|
12
|
+
version: 1,
|
|
13
|
+
taskId: rs.telemetry.taskId,
|
|
14
|
+
model: prepared.model.id,
|
|
15
|
+
requested: r.requested,
|
|
16
|
+
effective: r.effective,
|
|
17
|
+
graded: r.graded,
|
|
18
|
+
clamped: r.clamped,
|
|
19
|
+
format: r.format,
|
|
20
|
+
endpoint: r.endpoint,
|
|
21
|
+
...(r.dropped === true ? { dropped: true } : {}),
|
|
22
|
+
ts: Date.now(),
|
|
23
|
+
}));
|
|
24
|
+
};
|
|
25
|
+
if (reasoningResolution.current !== undefined)
|
|
26
|
+
publishReasoningResolution(reasoningResolution.current);
|
|
27
|
+
let reasoningFactsConsumed = false;
|
|
28
|
+
const observeReasoningWireFacts = (facts) => {
|
|
29
|
+
if (reasoningFactsConsumed)
|
|
30
|
+
return;
|
|
31
|
+
reasoningFactsConsumed = true;
|
|
32
|
+
if (prepared.thinking === undefined || prepared.thinking === "off")
|
|
33
|
+
return;
|
|
34
|
+
const next = resolveReasoning(prepared.thinking, prepared.model, facts);
|
|
35
|
+
const current = reasoningResolution.current;
|
|
36
|
+
if (current !== undefined &&
|
|
37
|
+
current.effective === next.effective &&
|
|
38
|
+
current.graded === next.graded &&
|
|
39
|
+
current.clamped === next.clamped &&
|
|
40
|
+
current.format === next.format &&
|
|
41
|
+
current.endpoint === next.endpoint &&
|
|
42
|
+
current.dropped === next.dropped) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
publishReasoningResolution(next);
|
|
46
|
+
};
|
|
47
|
+
return { observeReasoningWireFacts };
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Model } from "../../internal/llm.js";
|
|
2
|
+
import { type MaybeCompactOptions } from "../auto-compaction.js";
|
|
3
|
+
import type { HookInvocationIdentity } from "../hooks.js";
|
|
4
|
+
import type { PushQueue } from "../push-queue.js";
|
|
5
|
+
import type { Brain, TaskEvent, TaskSpec } from "../types.js";
|
|
6
|
+
import type { Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
|
|
7
|
+
export interface RunRecoveryLanesInput {
|
|
8
|
+
/** borrowed-readonly — the task spec: the compaction settings / instructions and the key-and-headers seat. */
|
|
9
|
+
spec: TaskSpec;
|
|
10
|
+
/** borrowed-mutable — the leg's prepared seat: the loop recovery chain is installed on its harness, the arm-B seat
|
|
11
|
+
* on `microCompact.inTurnCompactionRef`, the MC-R clears on `microCompact.ledger`; the abort signal, the pause
|
|
12
|
+
* holder, the session, the compaction model and the cache-break detector are read. */
|
|
13
|
+
prepared: Prepared;
|
|
14
|
+
/** borrowed-mutable — the run's event queue: the `compacted` and `compaction_outcome` frames of both arms. */
|
|
15
|
+
queue: PushQueue<TaskEvent>;
|
|
16
|
+
/** borrowed-mutable — the run's mutable state: the anti-thrash floor (`counters.compactionFloor`) is raised and
|
|
17
|
+
* consulted, the post-compact announce latch armed and the cadence windows rebased on `attach.attachState`. */
|
|
18
|
+
rs: RunState;
|
|
19
|
+
/** borrowed-readonly — the run's event-identity mint. */
|
|
20
|
+
ident: () => {
|
|
21
|
+
eventId: string;
|
|
22
|
+
parentToolCallId?: string;
|
|
23
|
+
sourceTaskId?: string;
|
|
24
|
+
};
|
|
25
|
+
/** borrowed-readonly — the ACCOUNTED compaction brain (the compaction-machinery lane's), handed to every pass. */
|
|
26
|
+
compactionBrain: Brain;
|
|
27
|
+
/** borrowed-mutable — the shared consecutive-failure breaker: read by the gates, zeroed on a landing (and on a
|
|
28
|
+
* noop), bumped on a burned pass. */
|
|
29
|
+
compactionBreaker: {
|
|
30
|
+
failures: number;
|
|
31
|
+
};
|
|
32
|
+
/** borrowed-readonly — the window-safety option builder (the compaction-machinery lane's), per pass. */
|
|
33
|
+
windowSafetyOptions: (mainModel: Model) => Pick<MaybeCompactOptions, "fallbackBudget" | "onWindowSafety">;
|
|
34
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`). */
|
|
35
|
+
runner: RunnerDepsSeat;
|
|
36
|
+
/** borrowed-readonly — the Runner's three compaction methods as delegates: the Seam C reuse options, the hook
|
|
37
|
+
* option wrapper and the reuse counter. The method bodies stay the Runner's; the pass calls them as the run body did. */
|
|
38
|
+
compactionSeats: {
|
|
39
|
+
seamCCompactionOptions: (prepared: Prepared) => Pick<MaybeCompactOptions, "summaryProvider" | "onCompaction" | "maxConsecutiveProviderReuse" | "consecutiveProviderReuse"> | undefined;
|
|
40
|
+
compactionHookOptions: (spec: TaskSpec, sessionId: string, trigger: "auto" | "manual" | "forced", identity?: HookInvocationIdentity, seatBound?: {
|
|
41
|
+
timeoutMs: number;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}) => Pick<MaybeCompactOptions, "trigger" | "preCompact" | "postCompact">;
|
|
44
|
+
recordCompactionReuse: (prepared: Prepared, comp: {
|
|
45
|
+
compacted: boolean;
|
|
46
|
+
reused?: boolean;
|
|
47
|
+
}) => void;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Nothing comes back: the lane's products are INSTALLED on the seats it borrowed — the harness's loop recovery
|
|
51
|
+
* chain and the microCompact arm-B seat — and the shared pass is read by those two installations alone. */
|
|
52
|
+
export interface RunRecoveryLanesResult {
|
|
53
|
+
}
|
|
54
|
+
export declare function runRecoveryLanes(input: RunRecoveryLanesInput): RunRecoveryLanesResult;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { DEFAULT_COMPACTION_SETTINGS } from "../../internal/harness.js";
|
|
2
|
+
import { planRejectionClears } from "../context-edit.js";
|
|
3
|
+
import { DEFAULT_COMPACTION_INSTRUCTIONS, maybeCompact } from "../auto-compaction.js";
|
|
4
|
+
import { emitTrace } from "../trace.js";
|
|
5
|
+
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
6
|
+
import { rebaseCadenceWindows } from "./turn-attachments.js";
|
|
7
|
+
import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
|
|
8
|
+
import { gitRestateOption } from "./git-leg-delivery.js";
|
|
9
|
+
import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
|
|
10
|
+
export function runRecoveryLanes(input) {
|
|
11
|
+
const { spec, prepared, queue, rs, ident, compactionBrain, compactionBreaker, windowSafetyOptions, runner, compactionSeats } = input;
|
|
12
|
+
const runForcedCompactionPass = async (lane, turnSignal) => {
|
|
13
|
+
if (!(spec.compaction?.enabled ?? true))
|
|
14
|
+
return false;
|
|
15
|
+
if (compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES)
|
|
16
|
+
return false;
|
|
17
|
+
const passSignal = turnSignal !== undefined ? AbortSignal.any([prepared.abortController.signal, turnSignal]) : prepared.abortController.signal;
|
|
18
|
+
try {
|
|
19
|
+
const comp = await maybeCompact({
|
|
20
|
+
session: prepared.session,
|
|
21
|
+
epochDeclaredSections: prepared.epochDeclaredSections,
|
|
22
|
+
...centerAdoptionOption(prepared),
|
|
23
|
+
model: prepared.harness.getModel(),
|
|
24
|
+
compactionModel: prepared.compModel,
|
|
25
|
+
...forkContextOption(prepared, true),
|
|
26
|
+
brain: compactionBrain,
|
|
27
|
+
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
28
|
+
thinking: prepared.thinking,
|
|
29
|
+
settings: { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction },
|
|
30
|
+
customInstructions: spec.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
|
|
31
|
+
signal: passSignal,
|
|
32
|
+
minTokens: 0,
|
|
33
|
+
force: true,
|
|
34
|
+
overheadTokens: prepared.promptOverheadTokens,
|
|
35
|
+
...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
|
|
36
|
+
onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
|
|
37
|
+
workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
|
|
38
|
+
...contextInstructionFilesOption(prepared),
|
|
39
|
+
...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
|
|
40
|
+
...compactionSeats.seamCCompactionOptions(prepared),
|
|
41
|
+
...gitRestateOption(prepared),
|
|
42
|
+
...windowSafetyOptions(prepared.harness.getModel()),
|
|
43
|
+
...compactionSeats.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: passSignal }),
|
|
44
|
+
});
|
|
45
|
+
if (comp.compacted) {
|
|
46
|
+
compactionBreaker.failures = 0;
|
|
47
|
+
compactionSeats.recordCompactionReuse(prepared, comp);
|
|
48
|
+
if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
|
|
49
|
+
const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
|
|
50
|
+
rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
|
|
51
|
+
}
|
|
52
|
+
queue.push({
|
|
53
|
+
type: "compacted",
|
|
54
|
+
trigger: "forced",
|
|
55
|
+
tokensBefore: comp.tokensBefore ?? 0,
|
|
56
|
+
...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
|
|
57
|
+
...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
|
|
58
|
+
...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
|
|
59
|
+
...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
|
|
60
|
+
...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
|
|
61
|
+
...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
|
|
62
|
+
...(comp.modelFallback ? { modelFallback: true } : {}),
|
|
63
|
+
...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
|
|
64
|
+
...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
|
|
65
|
+
...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
|
|
66
|
+
...ident(),
|
|
67
|
+
});
|
|
68
|
+
if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
|
|
69
|
+
const pd = comp.phaseDurations;
|
|
70
|
+
const pdDur = comp.durationMs;
|
|
71
|
+
emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.phase_timings", version: 1, taskId: rs.telemetry.taskId, ...pd, durationMs: pdDur, ts: Date.now() }));
|
|
72
|
+
}
|
|
73
|
+
prepared.cacheBreakDetector?.notifyCompaction();
|
|
74
|
+
if (rs.attach.attachState !== undefined) {
|
|
75
|
+
rs.attach.attachState.postCompactPending = true;
|
|
76
|
+
rebaseCadenceWindows(rs.attach.attachState, rs.counters.cadenceTurns);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (comp.noop) {
|
|
81
|
+
compactionBreaker.failures = 0;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
compactionBreaker.failures += 1;
|
|
85
|
+
}
|
|
86
|
+
const declineReason = lane === "rejection" ? "prompt-too-long recovery pass did not land" : "guard-chain forced compaction pass did not land";
|
|
87
|
+
if (!comp.noop) {
|
|
88
|
+
runner.deps.onError?.(new Error(declineReason), { phase: "compaction", sessionId: prepared.sessionId });
|
|
89
|
+
}
|
|
90
|
+
queue.push({
|
|
91
|
+
type: "compaction_outcome",
|
|
92
|
+
outcome: comp.noop ? "noop" : "failed",
|
|
93
|
+
trigger: "forced",
|
|
94
|
+
reason: declineReason,
|
|
95
|
+
...ident(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return comp.compacted === true;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (turnSignal?.aborted === true && !prepared.abortController.signal.aborted) {
|
|
102
|
+
const interruptReason = lane === "rejection"
|
|
103
|
+
? "prompt-too-long recovery pass cut short by a turn interrupt"
|
|
104
|
+
: "guard-chain forced compaction cut short by a turn interrupt";
|
|
105
|
+
queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: interruptReason, ...ident() });
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
compactionBreaker.failures += 1;
|
|
109
|
+
runner.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "compaction", sessionId: prepared.sessionId });
|
|
110
|
+
const msg = String(err instanceof Error ? err.message : err);
|
|
111
|
+
queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: msg.length > 512 ? `${msg.slice(0, 512)}…` : msg, ...ident() });
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
prepared.harness.setLoopRecovery({
|
|
116
|
+
truncatedOutput: {},
|
|
117
|
+
malformedToolUse: {},
|
|
118
|
+
thinkingOnly: {},
|
|
119
|
+
degenerateOutput: { detect: (m) => isDegenerateCutMessage(m) },
|
|
120
|
+
promptTooLong: {
|
|
121
|
+
recover: async (attempt, turnSignal) => {
|
|
122
|
+
if (prepared.abortController.signal.aborted || prepared.pausedRef.current !== undefined)
|
|
123
|
+
return false;
|
|
124
|
+
if (turnSignal?.aborted === true)
|
|
125
|
+
return false;
|
|
126
|
+
if (prepared.microCompact.clearOnRejection && attempt === 1) {
|
|
127
|
+
const proj = prepared.microCompact.projectionRef.current;
|
|
128
|
+
const compactionAvailable = (spec.compaction?.enabled ?? true) && compactionBreaker.failures < MAX_CONSECUTIVE_COMPACTION_FAILURES;
|
|
129
|
+
const declineArm = compactionAvailable ? "forced_compaction" : "none";
|
|
130
|
+
const plan = proj === undefined
|
|
131
|
+
? { declined: "no_candidates" }
|
|
132
|
+
: planRejectionClears(proj.messages, {
|
|
133
|
+
keyOf: proj.keyOf,
|
|
134
|
+
...(prepared.microCompact.offloadPersist ? { offload: { persist: prepared.microCompact.offloadPersist } } : {}),
|
|
135
|
+
});
|
|
136
|
+
if ("declined" in plan) {
|
|
137
|
+
emitTrace(rs.telemetry.tracer, () => ({
|
|
138
|
+
kind: "context.mc_null",
|
|
139
|
+
version: 1,
|
|
140
|
+
taskId: rs.telemetry.taskId,
|
|
141
|
+
reason: plan.declined,
|
|
142
|
+
nextArm: declineArm,
|
|
143
|
+
ts: Date.now(),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
for (const e of plan.cleared) {
|
|
148
|
+
prepared.microCompact.ledger.entries.set(e.key, { marker: e.marker, groupCount: e.groupCount, fp: e.fp });
|
|
149
|
+
}
|
|
150
|
+
emitTrace(rs.telemetry.tracer, () => ({
|
|
151
|
+
kind: "context.mc_clear",
|
|
152
|
+
version: 1,
|
|
153
|
+
taskId: rs.telemetry.taskId,
|
|
154
|
+
clearedCount: plan.cleared.length,
|
|
155
|
+
tokensSavedEstimate: plan.tokensSavedEstimate,
|
|
156
|
+
trigger: "refusal",
|
|
157
|
+
ts: Date.now(),
|
|
158
|
+
}));
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return runForcedCompactionPass("rejection", turnSignal);
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
prepared.microCompact.inTurnCompactionRef.current = async (turnSignal, anchoredEstimate) => {
|
|
167
|
+
if (prepared.abortController.signal.aborted || prepared.pausedRef.current !== undefined)
|
|
168
|
+
return false;
|
|
169
|
+
if (turnSignal?.aborted === true)
|
|
170
|
+
return false;
|
|
171
|
+
if (anchoredEstimate !== undefined && rs.counters.compactionFloor > 0 && anchoredEstimate < rs.counters.compactionFloor) {
|
|
172
|
+
const floor = rs.counters.compactionFloor;
|
|
173
|
+
emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.suppressed", version: 1, taskId: rs.telemetry.taskId, estTokens: anchoredEstimate, floor, ts: Date.now() }));
|
|
174
|
+
queue.push({ type: "compaction_outcome", outcome: "suppressed", trigger: "forced", ...ident() });
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return runForcedCompactionPass("guard", turnSignal);
|
|
178
|
+
};
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PushQueue } from "../push-queue.js";
|
|
2
|
+
import type { TaskEvent, TaskSpec } from "../types.js";
|
|
3
|
+
import type { Stats } from "./assemble-result.js";
|
|
4
|
+
import type { Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
|
|
5
|
+
export interface RunStopAndFinalVerifyInput {
|
|
6
|
+
/** borrowed-readonly — the task spec: the hooks slot and the `finalVerification` opt-in. */
|
|
7
|
+
spec: TaskSpec;
|
|
8
|
+
/** borrowed-mutable — the leg's prepared seat: the stop gate is installed on its harness; the abort signal, the
|
|
9
|
+
* pause and halt holders, the output seat, the session, the hook bound and identity are read inside the gate. */
|
|
10
|
+
prepared: Prepared;
|
|
11
|
+
/** borrowed-mutable — the run's event queue: the injection echo frames. */
|
|
12
|
+
queue: PushQueue<TaskEvent>;
|
|
13
|
+
/** borrowed-mutable — the run's mutable state: the injection counter (`counters.finalVerifyInjections`) is bumped;
|
|
14
|
+
* the grounding latches, the write latch and the turn cap are read. */
|
|
15
|
+
rs: RunState;
|
|
16
|
+
/** borrowed-readonly — the run's usage counters, read by the headroom guards. */
|
|
17
|
+
stats: Stats;
|
|
18
|
+
/** borrowed-readonly — the run's event-identity mint (the echo frames). */
|
|
19
|
+
ident: () => {
|
|
20
|
+
eventId: string;
|
|
21
|
+
parentToolCallId?: string;
|
|
22
|
+
sourceTaskId?: string;
|
|
23
|
+
};
|
|
24
|
+
/** borrowed-readonly — the monotonic walltime deadline (the clock lane's), read by the budget fill. */
|
|
25
|
+
walltimeMonotonicDeadline: number | undefined;
|
|
26
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`hooks` at resolution, `onError` in the gate). */
|
|
27
|
+
runner: RunnerDepsSeat;
|
|
28
|
+
}
|
|
29
|
+
/** Nothing comes back: the gate is INSTALLED on the borrowed harness, and the seat's closures are read by it alone. */
|
|
30
|
+
export interface RunStopAndFinalVerifyResult {
|
|
31
|
+
}
|
|
32
|
+
export declare function runStopAndFinalVerify(input: RunStopAndFinalVerifyInput): RunStopAndFinalVerifyResult;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { openSystemReminder } from "../reminder-mint.js";
|
|
2
|
+
import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
|
|
3
|
+
const STOP_HOOK_BLOCK_CAP = 8;
|
|
4
|
+
export function runStopAndFinalVerify(input) {
|
|
5
|
+
const { spec, prepared, queue, rs, stats, ident, walltimeMonotonicDeadline, runner } = input;
|
|
6
|
+
const stopHook = (spec.hooks ?? runner.deps.hooks)?.stop;
|
|
7
|
+
const finalVerificationOn = spec.finalVerification === true;
|
|
8
|
+
const finalVerifyBudgetFill = () => {
|
|
9
|
+
let worst = 0;
|
|
10
|
+
if (rs.budget.maxTokensWindow !== undefined && rs.budget.maxTokensWindow > 0)
|
|
11
|
+
worst = Math.max(worst, stats.tokens / rs.budget.maxTokensWindow);
|
|
12
|
+
if (rs.budget.maxCostMicroUsd !== undefined && rs.budget.maxCostMicroUsd > 0)
|
|
13
|
+
worst = Math.max(worst, stats.costMicroUsd / rs.budget.maxCostMicroUsd);
|
|
14
|
+
if (walltimeMonotonicDeadline !== undefined && prepared.suspendForResource === undefined) {
|
|
15
|
+
const windowMs = walltimeMonotonicDeadline - rs.telemetry.taskStartMonotonic;
|
|
16
|
+
if (windowMs > 0)
|
|
17
|
+
worst = Math.max(worst, (performance.now() - rs.telemetry.taskStartMonotonic) / windowMs);
|
|
18
|
+
}
|
|
19
|
+
return worst;
|
|
20
|
+
};
|
|
21
|
+
const emitFinalVerifyEcho = (body) => {
|
|
22
|
+
queue.push({ type: "steering_injected", source: "final_verification", preview: body.slice(0, 220), ...ident() });
|
|
23
|
+
};
|
|
24
|
+
if (stopHook || finalVerificationOn) {
|
|
25
|
+
let consecutiveBlocks = 0;
|
|
26
|
+
prepared.harness.setStopGate(async () => {
|
|
27
|
+
if (prepared.abortController.signal.aborted || prepared.pausedRef.current !== undefined || prepared.batchHaltRef.current !== undefined)
|
|
28
|
+
return [];
|
|
29
|
+
if (finalVerificationOn &&
|
|
30
|
+
(rs.counters.finalVerifyInjections === 0 || (rs.counters.finalVerifyInjections === 1 && rs.counters.groundingSignalPreR9 && !rs.counters.groundingSignalPostR9)) &&
|
|
31
|
+
rs.counters.wroteThisRun &&
|
|
32
|
+
prepared.outputRef.set !== true &&
|
|
33
|
+
!(rs.limits.effectiveMaxTurns !== undefined && rs.limits.effectiveMaxTurns > 0 && stats.turns >= rs.limits.effectiveMaxTurns - 1) &&
|
|
34
|
+
finalVerifyBudgetFill() < 0.9) {
|
|
35
|
+
rs.counters.finalVerifyInjections += 1;
|
|
36
|
+
if (rs.counters.finalVerifyInjections === 2) {
|
|
37
|
+
const reentryBody = openSystemReminder(prepared.reminderMark) +
|
|
38
|
+
"[final verification] Your tool calls in this run worked with raw bytes, structural parsing, " +
|
|
39
|
+
"or checksum/digest computation — the deliverable very likely embeds verifiable structure (structural fields, an " +
|
|
40
|
+
"embedded checksum-family value, reference data it must match, or a replayable deterministic path). You MUST " +
|
|
41
|
+
"execute the grounding check that structure supports — recompute the embedded value and compare it against the " +
|
|
42
|
+
"declared one, re-parse the structure from the raw bytes and reconcile it with your output, compare against the " +
|
|
43
|
+
"reference data, or replay the deterministic path — and REPORT the check's concrete result before finishing. " +
|
|
44
|
+
"A closing statement without a reported check result is not verification. If you already ran such a check, state " +
|
|
45
|
+
"its concrete result now; if the check mismatches, fix the deliverable first. This is the final reminder from " +
|
|
46
|
+
"this verification gate — it will not intervene again.</system-reminder>";
|
|
47
|
+
emitFinalVerifyEcho(reentryBody);
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
role: "user",
|
|
51
|
+
engineMinted: true,
|
|
52
|
+
content: reentryBody,
|
|
53
|
+
timestamp: Date.now(),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
const nudgeBody = openSystemReminder(prepared.reminderMark) +
|
|
58
|
+
"[final verification] Before finishing: re-verify the FINAL deliverable through its REAL entry point, " +
|
|
59
|
+
"exactly as the acceptance criteria would exercise it — execute the binary/function/endpoint directly and read the ACTUAL " +
|
|
60
|
+
"output and exit code. Do NOT rely on earlier self-tests, shell redirections, or assumptions (a program that prints to " +
|
|
61
|
+
"stdout is not a program that writes the required file). If anything mismatches the task's requirements, fix it before " +
|
|
62
|
+
"finishing. " +
|
|
63
|
+
"Treat verification writes as state-harmless: when the deliverable itself is a persisted final " +
|
|
64
|
+
"state (for example a committed or pushed file, a deployed artifact, or a required output file), " +
|
|
65
|
+
"do not change that state merely to test it. This constrains HOW you verify — it is never a " +
|
|
66
|
+
"license to skip the real acceptance path or to check a substitute of your own making: expected " +
|
|
67
|
+
"values must come from the task's requirements, never from content you generated. If the real " +
|
|
68
|
+
"acceptance path requires a write, use disposable inputs or an isolated target, end in the exact " +
|
|
69
|
+
"required final state, and verify that final state before finishing. " +
|
|
70
|
+
"If the work relied on a third-party API, library, or model, check the usage contract the object itself declares " +
|
|
71
|
+
"(docstrings, metadata, configuration — e.g. prompt conventions shipped with a model) and confirm your calls follow " +
|
|
72
|
+
"it rather than a default symmetric usage. Verify not only that the deliverable EXISTS but that the METHOD that " +
|
|
73
|
+
"produced it matches the task's requirements. " +
|
|
74
|
+
"Choose the verification SURFACE deliberately: check against the reference data, oracle, or evaluation tooling the " +
|
|
75
|
+
"task itself provides — re-running your own implementation and getting the same answer is self-consistency, not " +
|
|
76
|
+
"correctness — and cross-check through an independent second path where the task or environment offers one " +
|
|
77
|
+
"(checksums, runtime artifacts). Verify the PERSISTED artifact — re-read what is actually on disk or committed, " +
|
|
78
|
+
"not in-memory state — against every hard constraint from the original task text (numeric bounds, allowed-value " +
|
|
79
|
+
"lists, naming semantics, required files), reconciling whole-set completeness: nothing missing, nothing duplicated. " +
|
|
80
|
+
"If the deliverable embeds verifiable structure — structural fields, an embedded checksum-family value, " +
|
|
81
|
+
"reference data it must match, or a replayable deterministic path — you MUST execute the grounding check " +
|
|
82
|
+
"that structure supports and REPORT its concrete result in your closing summary: for such a deliverable, " +
|
|
83
|
+
"no reported check result means the work is not finished. " +
|
|
84
|
+
"If the task produced neither an executable deliverable nor any verifiable structure or acceptance " +
|
|
85
|
+
"oracle to check against, briefly confirm completion and stop. " +
|
|
86
|
+
"Residue YOUR OWN testing created (scratch files, running processes, generated outputs the task does not ask for) " +
|
|
87
|
+
"is not protected state — if the task's required final state is a clean target, removing your own residue is part " +
|
|
88
|
+
"of delivering it. " +
|
|
89
|
+
"Verification must never LAUNDER uncertainty: if part of your conclusion was uncertain before this check, keep " +
|
|
90
|
+
"reporting it as uncertain unless the check you actually ran resolved it — a re-stated conclusion is not new " +
|
|
91
|
+
"evidence.</system-reminder>";
|
|
92
|
+
emitFinalVerifyEcho(nudgeBody);
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
role: "user",
|
|
96
|
+
engineMinted: true,
|
|
97
|
+
content: nudgeBody,
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
if (!stopHook)
|
|
103
|
+
return [];
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
const stopSeat = await runHookSeat("stop", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => stopHook({
|
|
107
|
+
stopHookActive: consecutiveBlocks > 0,
|
|
108
|
+
consecutiveBlocks,
|
|
109
|
+
getBranch: () => prepared.session.getBranch(),
|
|
110
|
+
identity: prepared.hookIdentity,
|
|
111
|
+
signal: sig,
|
|
112
|
+
}));
|
|
113
|
+
if (stopSeat.expired) {
|
|
114
|
+
if (stopSeat.cause === "timeout") {
|
|
115
|
+
runner.deps.onError?.(hookSeatExpiredError("stop", prepared.hookTimeoutMs, stopSeat.cause, "the run was allowed to END (the seat's own no-opinion answer); no pushback and no additional context were injected"), { phase: "hook", sessionId: prepared.sessionId });
|
|
116
|
+
}
|
|
117
|
+
consecutiveBlocks = 0;
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
result = stopSeat.value;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
runner.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId: prepared.sessionId });
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const messages = [];
|
|
127
|
+
if (result?.additionalContext) {
|
|
128
|
+
messages.push({
|
|
129
|
+
role: "user",
|
|
130
|
+
engineMinted: true,
|
|
131
|
+
content: formatHookFeedback(`Stop hook additional context: ${result.additionalContext}`, prepared.reminderMark),
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (messages.length === 0 && !result?.block) {
|
|
136
|
+
consecutiveBlocks = 0;
|
|
137
|
+
return messages;
|
|
138
|
+
}
|
|
139
|
+
consecutiveBlocks++;
|
|
140
|
+
const cap = STOP_HOOK_BLOCK_CAP;
|
|
141
|
+
if (consecutiveBlocks > cap) {
|
|
142
|
+
runner.deps.onError?.(new Error(`a Stop hook kept the turn from ending ${consecutiveBlocks} consecutive times — overriding and ending the run. ` +
|
|
143
|
+
`Both a block and an additionalContext-only push-back count (CC 2.1.220). ` +
|
|
144
|
+
`Check ctx.stopHookActive in the hook and return success while it's true.`), { phase: "hook", sessionId: prepared.sessionId });
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
if (result?.block) {
|
|
148
|
+
messages.push({
|
|
149
|
+
role: "user",
|
|
150
|
+
engineMinted: true,
|
|
151
|
+
content: formatHookFeedback(`Stop hook stopped continuation: ${result.block}`, prepared.reminderMark),
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return messages;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ModelPricing } from "../pricing.js";
|
|
2
|
+
import type { SessionStore } from "../session.js";
|
|
3
|
+
import type { RunnerDeps, TaskSpec } from "../types.js";
|
|
4
|
+
import type { Stats } from "./assemble-result.js";
|
|
5
|
+
import type { Prepared, ResumeRun, RunnerDepsSeat, RunState, TaskIdRef } from "./contracts.js";
|
|
6
|
+
export interface RunTelemetryAndBudgetSeatsInput {
|
|
7
|
+
/** borrowed-readonly — the task spec: limits, degrade, tracer, output retries, the resource-suspend totals. */
|
|
8
|
+
spec: TaskSpec;
|
|
9
|
+
/** borrowed-mutable — the leg's prepared seat: `liveSpendRef.get` is installed, `humanReviewRef` and `planModeRef`
|
|
10
|
+
* are written on a resume; the model, the ledger, the session id and the usage governance are read. */
|
|
11
|
+
prepared: Prepared;
|
|
12
|
+
/** borrowed-readonly — the resume plan when this leg resumes a checkpoint (the human-review latency, the plan re-arm). */
|
|
13
|
+
resume: ResumeRun | undefined;
|
|
14
|
+
/** borrowed-readonly — the run's usage counters (this leg's own spend), read by the live-spend and budget closures. */
|
|
15
|
+
stats: Stats;
|
|
16
|
+
/** borrowed-mutable — the stream layer's backstop carrier: the effective task id and session id are published here. */
|
|
17
|
+
taskIdRef: TaskIdRef | undefined;
|
|
18
|
+
/** borrowed-readonly — the invocation's frozen tracer (#499), absence included; undefined when the seat was omitted. */
|
|
19
|
+
entryTracer: {
|
|
20
|
+
tracer: TaskSpec["tracer"];
|
|
21
|
+
} | undefined;
|
|
22
|
+
/** borrowed-readonly — the model catalog generation this task resolved against (pinned before prepare for swapDeps). */
|
|
23
|
+
modelCatalog: RunnerDeps["models"];
|
|
24
|
+
/** borrowed-readonly — the run's canonical task id (`spec.taskId ?? sessionId`), shared with the identity mint. */
|
|
25
|
+
runSourceTaskId: string;
|
|
26
|
+
/** borrowed-mutable — the run's mutable state: this lane is the WRITER of the `telemetry`, `degrade`, `limits` and
|
|
27
|
+
* `budget` groups' initial values and function members. */
|
|
28
|
+
rs: RunState;
|
|
29
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`pricing`, `onError`, `tracer`). */
|
|
30
|
+
runner: RunnerDepsSeat;
|
|
31
|
+
/** borrowed-readonly — the Runner's session store: the run is recorded on it (`noteTaskRun`, best-effort, loud). */
|
|
32
|
+
sessions: SessionStore;
|
|
33
|
+
}
|
|
34
|
+
export interface RunTelemetryAndBudgetSeatsResult {
|
|
35
|
+
/** #462: the price-table door — a table adopted MID-RUN that cannot price marks the spend unpriced. */
|
|
36
|
+
noteUnevaluablePriceTable: (p: ModelPricing) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare function runTelemetryAndBudgetSeats(input: RunTelemetryAndBudgetSeatsInput): RunTelemetryAndBudgetSeatsResult;
|