@sema-agent/core 5.12.0 → 5.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +320 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +137 -43
- package/dist/brain/anthropic.js +33 -10
- package/dist/brain/context-overflow.d.ts +20 -0
- package/dist/brain/context-overflow.js +58 -0
- package/dist/brain/open-responses.js +24 -10
- package/dist/brain/openai.js +29 -11
- package/dist/brain/request-params.d.ts +2 -0
- package/dist/brain/request-params.js +16 -0
- package/dist/brain/stream-engine.d.ts +9 -1
- package/dist/brain/stream-engine.js +256 -27
- package/dist/brain/timeout.d.ts +1 -0
- package/dist/brain/timeout.js +1 -0
- package/dist/core/a2a.d.ts +2 -2
- package/dist/core/a2a.js +3 -3
- package/dist/core/ask-question.d.ts +47 -2
- package/dist/core/ask-question.js +209 -28
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/checkpoint-store.d.ts +49 -18
- package/dist/core/checkpoint-store.js +117 -4
- package/dist/core/compliance.d.ts +11 -0
- package/dist/core/compliance.js +34 -0
- package/dist/core/governance-codes.d.ts +12 -0
- package/dist/core/governance-codes.js +24 -0
- package/dist/core/hooks.d.ts +24 -2
- package/dist/core/hooks.js +97 -10
- package/dist/core/human-input-projection.d.ts +12 -0
- package/dist/core/human-input-projection.js +27 -0
- package/dist/core/locked-config.d.ts +27 -0
- package/dist/core/locked-config.js +42 -0
- package/dist/core/mcp.d.ts +7 -2
- package/dist/core/mcp.js +7 -7
- package/dist/core/memory-admission.d.ts +51 -0
- package/dist/core/memory-admission.js +159 -0
- package/dist/core/memory.d.ts +2 -0
- package/dist/core/memory.js +3 -2
- package/dist/core/retention.d.ts +36 -0
- package/dist/core/retention.js +31 -0
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +2 -2
- package/dist/core/runner/prepare-memory.d.ts +9 -0
- package/dist/core/runner/prepare-memory.js +28 -2
- package/dist/core/runner/prepare-task.d.ts +18 -6
- package/dist/core/runner/prepare-task.js +428 -38
- package/dist/core/runner/runtask.d.ts +3 -6
- package/dist/core/runner/runtask.js +258 -77
- package/dist/core/runner/tool-output-projection.js +1 -0
- package/dist/core/session-store.d.ts +4 -0
- package/dist/core/session-store.js +5 -0
- package/dist/core/session.d.ts +1 -0
- package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-notification.js +5 -3
- package/dist/core/task-registry-agent.d.ts +1 -0
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/tool-policy.d.ts +5 -0
- package/dist/core/tool-policy.js +2 -1
- package/dist/core/tool-result-store.d.ts +2 -0
- package/dist/core/tool-result-store.js +1 -0
- package/dist/core/types.d.ts +38 -2
- package/dist/core/wiring-manifest.d.ts +97 -0
- package/dist/core/wiring-manifest.js +186 -0
- package/dist/engine/compaction/compaction.js +2 -2
- package/dist/engine/harness/agent-harness.d.ts +2 -1
- package/dist/engine/harness/agent-harness.js +19 -2
- package/dist/engine/harness/types.d.ts +3 -1
- package/dist/engine/llm/types.d.ts +7 -0
- package/dist/engine/llm/types.js +8 -1
- package/dist/engine/llm/validation.js +11 -1
- package/dist/engine/session/import-validate.d.ts +6 -1
- package/dist/engine/session/import-validate.js +29 -6
- package/dist/engine/session/memory-repo.d.ts +3 -1
- package/dist/engine/session/memory-repo.js +2 -2
- package/dist/index.d.ts +12 -4
- package/dist/index.js +12 -4
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/llm.d.ts +2 -2
- package/dist/internal/llm.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +4 -0
- package/dist/orchestration/run-workflow-tool.js +3 -0
- package/dist/orchestration/workflow-types.d.ts +8 -0
- package/dist/orchestration/workflow-types.js +14 -0
- package/dist/orchestration/workflow.d.ts +4 -0
- package/dist/orchestration/workflow.js +134 -5
- package/dist/prompts/default.js +1 -1
- package/dist/stores/file/checkpoint-store.d.ts +3 -5
- package/dist/stores/file/checkpoint-store.js +31 -2
- package/dist/stores/file/index.js +1 -1
- package/dist/stores/file/session-store.d.ts +3 -1
- package/dist/stores/file/session-store.js +2 -2
- package/dist/stores/file/shared-ledger.js +8 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
- package/dist/tools/fs/bash-readonly-classifier.js +94 -0
- package/dist/tools/fs/fs-bash.js +31 -12
- package/dist/tools/fs/safety.js +34 -10
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
2
|
-
import {
|
|
2
|
+
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
3
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
3
4
|
import { engineVersion } from "../version.js";
|
|
4
5
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
5
6
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
6
7
|
import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill, sanitizeCompactionSettings } from "../auto-compaction.js";
|
|
7
|
-
import { ASK_USER_QUESTION_TOOL_NAME,
|
|
8
|
+
import { ASK_USER_QUESTION_TOOL_NAME, canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace, markBoundOnlyQuestionFace } from "../ask-question.js";
|
|
9
|
+
import { boundInputHashOf } from "../canonical-json.js";
|
|
8
10
|
import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
|
|
9
11
|
import { emitTrace } from "../trace.js";
|
|
10
12
|
import { emitTaskOutcome } from "../task-outcome.js";
|
|
@@ -31,6 +33,7 @@ import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
|
31
33
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
32
34
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
33
35
|
import { formatHookFeedback } from "../hooks.js";
|
|
36
|
+
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
34
37
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
35
38
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
36
39
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
@@ -52,6 +55,15 @@ function createRunState() {
|
|
|
52
55
|
attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, sizeGuidelineState: undefined, attachmentsInjected: 0 },
|
|
53
56
|
};
|
|
54
57
|
}
|
|
58
|
+
const humanInputOrdinals = new WeakMap();
|
|
59
|
+
function nextHumanInputSeq(key) {
|
|
60
|
+
let box = humanInputOrdinals.get(key);
|
|
61
|
+
if (box === undefined) {
|
|
62
|
+
box = { n: 0 };
|
|
63
|
+
humanInputOrdinals.set(key, box);
|
|
64
|
+
}
|
|
65
|
+
return ++box.n;
|
|
66
|
+
}
|
|
55
67
|
const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
|
|
56
68
|
const STOP_HOOK_BLOCK_CAP = 8;
|
|
57
69
|
const COMPACTION_REGROWTH_FACTOR = 1.5;
|
|
@@ -139,7 +151,15 @@ function deepJsonEqual(a, b) {
|
|
|
139
151
|
if (aArr && bArr) {
|
|
140
152
|
if (a.length !== b.length)
|
|
141
153
|
return false;
|
|
142
|
-
|
|
154
|
+
for (let i = 0; i < a.length; i++) {
|
|
155
|
+
if (!deepJsonEqual(a[i], b[i]))
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const aExtra = Object.keys(a).filter((k) => !isCanonicalIndexKey(k, a.length));
|
|
159
|
+
const bExtra = Object.keys(b).filter((k) => !isCanonicalIndexKey(k, b.length));
|
|
160
|
+
if (aExtra.length !== bExtra.length)
|
|
161
|
+
return false;
|
|
162
|
+
return aExtra.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepJsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
|
|
143
163
|
}
|
|
144
164
|
const ao = a;
|
|
145
165
|
const bo = b;
|
|
@@ -149,6 +169,9 @@ function deepJsonEqual(a, b) {
|
|
|
149
169
|
return false;
|
|
150
170
|
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k]));
|
|
151
171
|
}
|
|
172
|
+
function isCanonicalIndexKey(key, length) {
|
|
173
|
+
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < length;
|
|
174
|
+
}
|
|
152
175
|
function sameWinner(a, b) {
|
|
153
176
|
return (a.boundCallId === b.boundCallId &&
|
|
154
177
|
a.decision === b.decision &&
|
|
@@ -160,14 +183,18 @@ function pendingContentAskCallId(cp) {
|
|
|
160
183
|
? cp.pendingAction.toolCallId
|
|
161
184
|
: undefined;
|
|
162
185
|
}
|
|
163
|
-
function answerFaceForRedeemedCall(answer, redeemedCallId, base) {
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
function answerFaceForRedeemedCall(answer, redeemedCallId, base, redeemedQuestionsHash) {
|
|
187
|
+
let consumed = false;
|
|
188
|
+
const face = async (req, signal) => {
|
|
189
|
+
if (!consumed && req.toolCallId === redeemedCallId && boundInputHashOf(req.questions) === redeemedQuestionsHash) {
|
|
190
|
+
consumed = true;
|
|
166
191
|
return answer;
|
|
192
|
+
}
|
|
167
193
|
if (base !== undefined)
|
|
168
194
|
return base(req, signal);
|
|
169
195
|
throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg");
|
|
170
196
|
};
|
|
197
|
+
return isLiveQuestionFace(base) ? face : markBoundOnlyQuestionFace(face);
|
|
171
198
|
}
|
|
172
199
|
function writeFamilyOfCanonical(name) {
|
|
173
200
|
if (name === "TaskCreate" || name === "TaskUpdate")
|
|
@@ -1166,6 +1193,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1166
1193
|
const progress = {
|
|
1167
1194
|
type: "task_progress",
|
|
1168
1195
|
taskId: rs.telemetry.taskId,
|
|
1196
|
+
...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
|
|
1169
1197
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
1170
1198
|
...(subagentName ? { name: subagentName } : {}),
|
|
1171
1199
|
usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
|
|
@@ -1332,6 +1360,7 @@ export class Runner {
|
|
|
1332
1360
|
return runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles });
|
|
1333
1361
|
}
|
|
1334
1362
|
runTaskStream(spec, resume, internals) {
|
|
1363
|
+
const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
|
|
1335
1364
|
const queue = new PushQueue();
|
|
1336
1365
|
const detachHub = new ToolDetachHub();
|
|
1337
1366
|
let resultValue;
|
|
@@ -1386,7 +1415,7 @@ export class Runner {
|
|
|
1386
1415
|
publishReady(h);
|
|
1387
1416
|
}, (s) => {
|
|
1388
1417
|
reapHandle = s;
|
|
1389
|
-
}, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef);
|
|
1418
|
+
}, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
|
|
1390
1419
|
}
|
|
1391
1420
|
finally {
|
|
1392
1421
|
releaseLock?.();
|
|
@@ -1450,6 +1479,12 @@ export class Runner {
|
|
|
1450
1479
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
1451
1480
|
errorCode: code,
|
|
1452
1481
|
...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
|
|
1482
|
+
...(() => {
|
|
1483
|
+
const hinted = err.retryAfterMs;
|
|
1484
|
+
return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
|
|
1485
|
+
? { retryAfterMs: hinted }
|
|
1486
|
+
: {};
|
|
1487
|
+
})(),
|
|
1453
1488
|
stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
|
|
1454
1489
|
};
|
|
1455
1490
|
emitTrace(spec.tracer ?? this.deps.tracer, () => ({
|
|
@@ -1517,7 +1552,29 @@ export class Runner {
|
|
|
1517
1552
|
if (options?.trusted && sanitizeUntrustedText(text) !== text) {
|
|
1518
1553
|
throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
|
|
1519
1554
|
}
|
|
1520
|
-
const
|
|
1555
|
+
const actor = options?.actor === undefined ? undefined : snapshotActorAssertion(options.actor);
|
|
1556
|
+
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
1557
|
+
const payload = options?.trusted ? formatHookFeedback(projected) : projected;
|
|
1558
|
+
const mintsAFrame = payload.trim().length !== 0;
|
|
1559
|
+
const emitAccepted = (h) => {
|
|
1560
|
+
if (!mintsAFrame)
|
|
1561
|
+
return;
|
|
1562
|
+
queue.push({
|
|
1563
|
+
...buildHumanInputEvent({
|
|
1564
|
+
carrier: "steer",
|
|
1565
|
+
source: "steer",
|
|
1566
|
+
delivery: "queued",
|
|
1567
|
+
sessionSeq: nextHumanInputSeq(h.harness),
|
|
1568
|
+
...(actor !== undefined ? { actor } : {}),
|
|
1569
|
+
...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
|
|
1570
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
1571
|
+
}),
|
|
1572
|
+
eventId: uuidv7(),
|
|
1573
|
+
...(internals?.parentToolCallId !== undefined
|
|
1574
|
+
? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) }
|
|
1575
|
+
: {}),
|
|
1576
|
+
});
|
|
1577
|
+
};
|
|
1521
1578
|
const deliver = async () => {
|
|
1522
1579
|
if (resultValue)
|
|
1523
1580
|
throw steeringError("the task has already finished");
|
|
@@ -1525,7 +1582,8 @@ export class Runner {
|
|
|
1525
1582
|
if (!h)
|
|
1526
1583
|
throw steeringError("the task is not running");
|
|
1527
1584
|
try {
|
|
1528
|
-
await h.harness.steer(payload, { provenance: "engine-note" });
|
|
1585
|
+
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1586
|
+
emitAccepted(h);
|
|
1529
1587
|
return;
|
|
1530
1588
|
}
|
|
1531
1589
|
catch (e) {
|
|
@@ -1535,7 +1593,8 @@ export class Runner {
|
|
|
1535
1593
|
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
1536
1594
|
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1537
1595
|
try {
|
|
1538
|
-
await h.harness.steer(payload, { provenance: "engine-note" });
|
|
1596
|
+
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1597
|
+
emitAccepted(h);
|
|
1539
1598
|
return;
|
|
1540
1599
|
}
|
|
1541
1600
|
catch (e2) {
|
|
@@ -1652,7 +1711,7 @@ export class Runner {
|
|
|
1652
1711
|
})()),
|
|
1653
1712
|
};
|
|
1654
1713
|
}
|
|
1655
|
-
async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef) {
|
|
1714
|
+
async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, entryActor) {
|
|
1656
1715
|
const prepareResume = resume
|
|
1657
1716
|
? {
|
|
1658
1717
|
leafId: resume.cp.leafId,
|
|
@@ -1665,6 +1724,12 @@ export class Runner {
|
|
|
1665
1724
|
executesApprovedAction: resume.cp.pendingAction.kind === "tool_approval" &&
|
|
1666
1725
|
resume.outcome.gate !== "wake" &&
|
|
1667
1726
|
resume.outcome.decision === "allow",
|
|
1727
|
+
...(resume.outcome.gate !== "wake" && resume.outcome.decision === "allow" && pendingContentAskCallId(resume.cp) !== undefined
|
|
1728
|
+
? {
|
|
1729
|
+
redeemedContentAskCallId: pendingContentAskCallId(resume.cp),
|
|
1730
|
+
redeemedContentAskQuestionsHash: boundInputHashOf(resume.cp.pendingAction.args?.questions),
|
|
1731
|
+
}
|
|
1732
|
+
: {}),
|
|
1668
1733
|
}
|
|
1669
1734
|
: undefined;
|
|
1670
1735
|
const taskNotificationQueue = new SystemInjectionQueue();
|
|
@@ -1703,9 +1768,7 @@ export class Runner {
|
|
|
1703
1768
|
queue.push({ type: "task_notification", notification: item.payload, ...notificationIdent() });
|
|
1704
1769
|
if (notificationHarness) {
|
|
1705
1770
|
const xml = renderTaskNotificationXml(item.payload);
|
|
1706
|
-
const deliver =
|
|
1707
|
-
? notificationHarness.followUp(xml, { provenance: "engine-note", enginePayload: item.payload })
|
|
1708
|
-
: notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
|
|
1771
|
+
const deliver = notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
|
|
1709
1772
|
void deliver.then(() => item.onDisposition?.("queued"), () => {
|
|
1710
1773
|
parkTaskNotification(item.payload, item.priority);
|
|
1711
1774
|
item.onDisposition?.("parked");
|
|
@@ -1755,6 +1818,7 @@ export class Runner {
|
|
|
1755
1818
|
const parentToolCallId = internals?.parentToolCallId;
|
|
1756
1819
|
const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
|
|
1757
1820
|
notificationIdent = ident;
|
|
1821
|
+
queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
|
|
1758
1822
|
manualCompactRef.emitMooted = (reason) => {
|
|
1759
1823
|
queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
|
|
1760
1824
|
};
|
|
@@ -2323,6 +2387,22 @@ export class Runner {
|
|
|
2323
2387
|
});
|
|
2324
2388
|
const stopHook = (spec.hooks ?? this.deps.hooks)?.stop;
|
|
2325
2389
|
const finalVerificationOn = spec.finalVerification === true;
|
|
2390
|
+
const finalVerifyBudgetFill = () => {
|
|
2391
|
+
let worst = 0;
|
|
2392
|
+
if (rs.budget.maxTokensWindow !== undefined && rs.budget.maxTokensWindow > 0)
|
|
2393
|
+
worst = Math.max(worst, stats.tokens / rs.budget.maxTokensWindow);
|
|
2394
|
+
if (rs.budget.maxCostMicroUsd !== undefined && rs.budget.maxCostMicroUsd > 0)
|
|
2395
|
+
worst = Math.max(worst, stats.costMicroUsd / rs.budget.maxCostMicroUsd);
|
|
2396
|
+
if (walltimeMonotonicDeadline !== undefined && prepared.suspendForResource === undefined) {
|
|
2397
|
+
const windowMs = walltimeMonotonicDeadline - rs.telemetry.taskStartMonotonic;
|
|
2398
|
+
if (windowMs > 0)
|
|
2399
|
+
worst = Math.max(worst, (performance.now() - rs.telemetry.taskStartMonotonic) / windowMs);
|
|
2400
|
+
}
|
|
2401
|
+
return worst;
|
|
2402
|
+
};
|
|
2403
|
+
const emitFinalVerifyEcho = (body) => {
|
|
2404
|
+
queue.push({ type: "steering_injected", source: "final_verification", preview: body.slice(0, 220), ...ident() });
|
|
2405
|
+
};
|
|
2326
2406
|
if (stopHook || finalVerificationOn) {
|
|
2327
2407
|
let consecutiveBlocks = 0;
|
|
2328
2408
|
prepared.harness.setStopGate(async () => {
|
|
@@ -2332,58 +2412,69 @@ export class Runner {
|
|
|
2332
2412
|
(rs.counters.finalVerifyInjections === 0 || (rs.counters.finalVerifyInjections === 1 && rs.counters.groundingSignalPreR9 && !rs.counters.groundingSignalPostR9)) &&
|
|
2333
2413
|
rs.counters.wroteThisRun &&
|
|
2334
2414
|
prepared.outputRef.set !== true &&
|
|
2335
|
-
!(rs.limits.effectiveMaxTurns !== undefined && rs.limits.effectiveMaxTurns > 0 && stats.turns >= rs.limits.effectiveMaxTurns - 1)
|
|
2415
|
+
!(rs.limits.effectiveMaxTurns !== undefined && rs.limits.effectiveMaxTurns > 0 && stats.turns >= rs.limits.effectiveMaxTurns - 1) &&
|
|
2416
|
+
finalVerifyBudgetFill() < 0.9) {
|
|
2336
2417
|
rs.counters.finalVerifyInjections += 1;
|
|
2337
2418
|
if (rs.counters.finalVerifyInjections === 2) {
|
|
2419
|
+
const reentryBody = "<system-reminder>[final verification] Your tool calls in this run worked with raw bytes, structural parsing, " +
|
|
2420
|
+
"or checksum/digest computation — the deliverable very likely embeds verifiable structure (structural fields, an " +
|
|
2421
|
+
"embedded checksum-family value, reference data it must match, or a replayable deterministic path). You MUST " +
|
|
2422
|
+
"execute the grounding check that structure supports — recompute the embedded value and compare it against the " +
|
|
2423
|
+
"declared one, re-parse the structure from the raw bytes and reconcile it with your output, compare against the " +
|
|
2424
|
+
"reference data, or replay the deterministic path — and REPORT the check's concrete result before finishing. " +
|
|
2425
|
+
"A closing statement without a reported check result is not verification. If you already ran such a check, state " +
|
|
2426
|
+
"its concrete result now; if the check mismatches, fix the deliverable first. This is the final reminder from " +
|
|
2427
|
+
"this verification gate — it will not intervene again.</system-reminder>";
|
|
2428
|
+
emitFinalVerifyEcho(reentryBody);
|
|
2338
2429
|
return [
|
|
2339
2430
|
{
|
|
2340
2431
|
role: "user",
|
|
2341
2432
|
engineMinted: true,
|
|
2342
|
-
content:
|
|
2343
|
-
"or checksum/digest computation — the deliverable very likely embeds verifiable structure (structural fields, an " +
|
|
2344
|
-
"embedded checksum-family value, reference data it must match, or a replayable deterministic path). You MUST " +
|
|
2345
|
-
"execute the grounding check that structure supports — recompute the embedded value and compare it against the " +
|
|
2346
|
-
"declared one, re-parse the structure from the raw bytes and reconcile it with your output, compare against the " +
|
|
2347
|
-
"reference data, or replay the deterministic path — and REPORT the check's concrete result before finishing. " +
|
|
2348
|
-
"A closing statement without a reported check result is not verification. If you already ran such a check, state " +
|
|
2349
|
-
"its concrete result now; if the check mismatches, fix the deliverable first. This is the final reminder from " +
|
|
2350
|
-
"this verification gate — it will not intervene again.</system-reminder>",
|
|
2433
|
+
content: reentryBody,
|
|
2351
2434
|
timestamp: Date.now(),
|
|
2352
2435
|
},
|
|
2353
2436
|
];
|
|
2354
2437
|
}
|
|
2438
|
+
const nudgeBody = "<system-reminder>[final verification] Before finishing: re-verify the FINAL deliverable through its REAL entry point, " +
|
|
2439
|
+
"exactly as the acceptance criteria would exercise it — execute the binary/function/endpoint directly and read the ACTUAL " +
|
|
2440
|
+
"output and exit code. Do NOT rely on earlier self-tests, shell redirections, or assumptions (a program that prints to " +
|
|
2441
|
+
"stdout is not a program that writes the required file). If anything mismatches the task's requirements, fix it before " +
|
|
2442
|
+
"finishing. " +
|
|
2443
|
+
"Treat verification writes as state-harmless: when the deliverable itself is a persisted final " +
|
|
2444
|
+
"state (for example a committed or pushed file, a deployed artifact, or a required output file), " +
|
|
2445
|
+
"do not change that state merely to test it. This constrains HOW you verify — it is never a " +
|
|
2446
|
+
"license to skip the real acceptance path or to check a substitute of your own making: expected " +
|
|
2447
|
+
"values must come from the task's requirements, never from content you generated. If the real " +
|
|
2448
|
+
"acceptance path requires a write, use disposable inputs or an isolated target, end in the exact " +
|
|
2449
|
+
"required final state, and verify that final state before finishing. " +
|
|
2450
|
+
"If the work relied on a third-party API, library, or model, check the usage contract the object itself declares " +
|
|
2451
|
+
"(docstrings, metadata, configuration — e.g. prompt conventions shipped with a model) and confirm your calls follow " +
|
|
2452
|
+
"it rather than a default symmetric usage. Verify not only that the deliverable EXISTS but that the METHOD that " +
|
|
2453
|
+
"produced it matches the task's requirements. " +
|
|
2454
|
+
"Choose the verification SURFACE deliberately: check against the reference data, oracle, or evaluation tooling the " +
|
|
2455
|
+
"task itself provides — re-running your own implementation and getting the same answer is self-consistency, not " +
|
|
2456
|
+
"correctness — and cross-check through an independent second path where the task or environment offers one " +
|
|
2457
|
+
"(checksums, runtime artifacts). Verify the PERSISTED artifact — re-read what is actually on disk or committed, " +
|
|
2458
|
+
"not in-memory state — against every hard constraint from the original task text (numeric bounds, allowed-value " +
|
|
2459
|
+
"lists, naming semantics, required files), reconciling whole-set completeness: nothing missing, nothing duplicated. " +
|
|
2460
|
+
"If the deliverable embeds verifiable structure — structural fields, an embedded checksum-family value, " +
|
|
2461
|
+
"reference data it must match, or a replayable deterministic path — you MUST execute the grounding check " +
|
|
2462
|
+
"that structure supports and REPORT its concrete result in your closing summary: for such a deliverable, " +
|
|
2463
|
+
"no reported check result means the work is not finished. " +
|
|
2464
|
+
"If the task produced neither an executable deliverable nor any verifiable structure or acceptance " +
|
|
2465
|
+
"oracle to check against, briefly confirm completion and stop. " +
|
|
2466
|
+
"Residue YOUR OWN testing created (scratch files, running processes, generated outputs the task does not ask for) " +
|
|
2467
|
+
"is not protected state — if the task's required final state is a clean target, removing your own residue is part " +
|
|
2468
|
+
"of delivering it. " +
|
|
2469
|
+
"Verification must never LAUNDER uncertainty: if part of your conclusion was uncertain before this check, keep " +
|
|
2470
|
+
"reporting it as uncertain unless the check you actually ran resolved it — a re-stated conclusion is not new " +
|
|
2471
|
+
"evidence.</system-reminder>";
|
|
2472
|
+
emitFinalVerifyEcho(nudgeBody);
|
|
2355
2473
|
return [
|
|
2356
2474
|
{
|
|
2357
2475
|
role: "user",
|
|
2358
2476
|
engineMinted: true,
|
|
2359
|
-
content:
|
|
2360
|
-
"exactly as the acceptance criteria would exercise it — execute the binary/function/endpoint directly and read the ACTUAL " +
|
|
2361
|
-
"output and exit code. Do NOT rely on earlier self-tests, shell redirections, or assumptions (a program that prints to " +
|
|
2362
|
-
"stdout is not a program that writes the required file). If anything mismatches the task's requirements, fix it before " +
|
|
2363
|
-
"finishing. " +
|
|
2364
|
-
"Treat verification writes as state-harmless: when the deliverable itself is a persisted final " +
|
|
2365
|
-
"state (for example a committed or pushed file, a deployed artifact, or a required output file), " +
|
|
2366
|
-
"do not change that state merely to test it. This constrains HOW you verify — it is never a " +
|
|
2367
|
-
"license to skip the real acceptance path or to check a substitute of your own making: expected " +
|
|
2368
|
-
"values must come from the task's requirements, never from content you generated. If the real " +
|
|
2369
|
-
"acceptance path requires a write, use disposable inputs or an isolated target, end in the exact " +
|
|
2370
|
-
"required final state, and verify that final state before finishing. " +
|
|
2371
|
-
"If the work relied on a third-party API, library, or model, check the usage contract the object itself declares " +
|
|
2372
|
-
"(docstrings, metadata, configuration — e.g. prompt conventions shipped with a model) and confirm your calls follow " +
|
|
2373
|
-
"it rather than a default symmetric usage. Verify not only that the deliverable EXISTS but that the METHOD that " +
|
|
2374
|
-
"produced it matches the task's requirements. " +
|
|
2375
|
-
"Choose the verification SURFACE deliberately: check against the reference data, oracle, or evaluation tooling the " +
|
|
2376
|
-
"task itself provides — re-running your own implementation and getting the same answer is self-consistency, not " +
|
|
2377
|
-
"correctness — and cross-check through an independent second path where the task or environment offers one " +
|
|
2378
|
-
"(checksums, runtime artifacts). Verify the PERSISTED artifact — re-read what is actually on disk or committed, " +
|
|
2379
|
-
"not in-memory state — against every hard constraint from the original task text (numeric bounds, allowed-value " +
|
|
2380
|
-
"lists, naming semantics, required files), reconciling whole-set completeness: nothing missing, nothing duplicated. " +
|
|
2381
|
-
"If the deliverable embeds verifiable structure — structural fields, an embedded checksum-family value, " +
|
|
2382
|
-
"reference data it must match, or a replayable deterministic path — you MUST execute the grounding check " +
|
|
2383
|
-
"that structure supports and REPORT its concrete result in your closing summary: for such a deliverable, " +
|
|
2384
|
-
"no reported check result means the work is not finished. " +
|
|
2385
|
-
"If the task produced neither an executable deliverable nor any verifiable structure or acceptance " +
|
|
2386
|
-
"oracle to check against, briefly confirm completion and stop.</system-reminder>",
|
|
2477
|
+
content: nudgeBody,
|
|
2387
2478
|
timestamp: Date.now(),
|
|
2388
2479
|
},
|
|
2389
2480
|
];
|
|
@@ -2564,6 +2655,7 @@ export class Runner {
|
|
|
2564
2655
|
let final;
|
|
2565
2656
|
let threw;
|
|
2566
2657
|
let abortedLive = false;
|
|
2658
|
+
let strandedHumanAnswers = [];
|
|
2567
2659
|
try {
|
|
2568
2660
|
if (prepared.abortController.signal.aborted) {
|
|
2569
2661
|
const e = new Error("run aborted");
|
|
@@ -2616,15 +2708,31 @@ export class Runner {
|
|
|
2616
2708
|
}
|
|
2617
2709
|
}
|
|
2618
2710
|
const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2711
|
+
const resumeFrames = [
|
|
2712
|
+
...readPendingSteerQueue(resume.cp.state).map((entry) => ({ entry, source: "steer" })),
|
|
2713
|
+
...(resume.wakeMessage !== undefined ? [{ entry: resume.wakeMessage, source: "wake" }] : []),
|
|
2714
|
+
];
|
|
2715
|
+
for (const { entry: steer, source } of resumeFrames) {
|
|
2622
2716
|
const start = continuation.length;
|
|
2717
|
+
const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
|
|
2623
2718
|
continuation +=
|
|
2624
2719
|
"\n\n" +
|
|
2625
|
-
(steer.trusted ? formatHookFeedback(
|
|
2720
|
+
(steer.trusted ? formatHookFeedback(projected) : delimitUntrusted("supervisor steering message", projected));
|
|
2626
2721
|
if (steer.trusted)
|
|
2627
2722
|
engineSegments.push({ start, end: continuation.length });
|
|
2723
|
+
queue.push({
|
|
2724
|
+
...buildHumanInputEvent({
|
|
2725
|
+
carrier: source,
|
|
2726
|
+
source,
|
|
2727
|
+
delivery: "applied",
|
|
2728
|
+
sessionSeq: nextHumanInputSeq(prepared.harness),
|
|
2729
|
+
inputId: steer.inputId,
|
|
2730
|
+
...(steer.actor !== undefined ? { actor: steer.actor } : {}),
|
|
2731
|
+
...(steer.actor?.issuer !== undefined ? { issuer: steer.actor.issuer } : {}),
|
|
2732
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
2733
|
+
}),
|
|
2734
|
+
...ident(),
|
|
2735
|
+
});
|
|
2628
2736
|
}
|
|
2629
2737
|
if (resume !== undefined)
|
|
2630
2738
|
resume.decisionDelivered = true;
|
|
@@ -2632,7 +2740,9 @@ export class Runner {
|
|
|
2632
2740
|
}
|
|
2633
2741
|
}
|
|
2634
2742
|
else {
|
|
2635
|
-
|
|
2743
|
+
const objectiveActor = entryActor;
|
|
2744
|
+
const projectedObjective = projectHumanInput({ text: spec.objective, actor: objectiveActor, source: "objective" });
|
|
2745
|
+
let effectiveObjective = projectedObjective;
|
|
2636
2746
|
let promptBlocked = false;
|
|
2637
2747
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
2638
2748
|
if (userPromptSubmit) {
|
|
@@ -2643,7 +2753,7 @@ export class Runner {
|
|
|
2643
2753
|
promptBlocked = true;
|
|
2644
2754
|
}
|
|
2645
2755
|
else if (decision?.additionalContext) {
|
|
2646
|
-
effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${
|
|
2756
|
+
effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${projectedObjective}`;
|
|
2647
2757
|
}
|
|
2648
2758
|
}
|
|
2649
2759
|
catch (hookErr) {
|
|
@@ -2744,10 +2854,23 @@ export class Runner {
|
|
|
2744
2854
|
rs.attach.attachmentsInjected += firstFrames.length;
|
|
2745
2855
|
if (firstFrames.length > 0)
|
|
2746
2856
|
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
2747
|
-
const enginePrefixChars = effectiveObjective.length -
|
|
2857
|
+
const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
|
|
2858
|
+
queue.push({
|
|
2859
|
+
...buildHumanInputEvent({
|
|
2860
|
+
carrier: "objective",
|
|
2861
|
+
source: "objective",
|
|
2862
|
+
delivery: "applied",
|
|
2863
|
+
sessionSeq: nextHumanInputSeq(prepared.harness),
|
|
2864
|
+
...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
|
|
2865
|
+
...(objectiveActor?.issuer !== undefined ? { issuer: objectiveActor.issuer } : {}),
|
|
2866
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
2867
|
+
}),
|
|
2868
|
+
...ident(),
|
|
2869
|
+
});
|
|
2748
2870
|
final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
|
|
2749
2871
|
...(images && images.length > 0 ? { images } : {}),
|
|
2750
2872
|
...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
|
|
2873
|
+
...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
|
|
2751
2874
|
}));
|
|
2752
2875
|
}
|
|
2753
2876
|
}
|
|
@@ -2770,6 +2893,7 @@ export class Runner {
|
|
|
2770
2893
|
prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
|
|
2771
2894
|
prepared.abortController.abort();
|
|
2772
2895
|
prepared.releaseSignal();
|
|
2896
|
+
strandedHumanAnswers = prepared.settleContentAskBindings();
|
|
2773
2897
|
try {
|
|
2774
2898
|
prepared.harness.recoverUndrainedEngineNotes();
|
|
2775
2899
|
}
|
|
@@ -2908,6 +3032,7 @@ export class Runner {
|
|
|
2908
3032
|
model: prepared.model.id,
|
|
2909
3033
|
unpricedSpend: rs.telemetry.unpricedSpend,
|
|
2910
3034
|
rewindNotes: prepared.rewindNotes,
|
|
3035
|
+
strandedHumanAnswers,
|
|
2911
3036
|
remoteEnvFailures: prepared.remoteEnvFailures,
|
|
2912
3037
|
retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
|
|
2913
3038
|
abortedForTimeout: timeout.fired,
|
|
@@ -3044,6 +3169,7 @@ export class Runner {
|
|
|
3044
3169
|
const terminalTick = {
|
|
3045
3170
|
type: "task_progress",
|
|
3046
3171
|
taskId: rs.telemetry.taskId,
|
|
3172
|
+
...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
|
|
3047
3173
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
3048
3174
|
...(subagentName ? { name: subagentName } : {}),
|
|
3049
3175
|
usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
|
|
@@ -3304,7 +3430,7 @@ export class Runner {
|
|
|
3304
3430
|
if (outcome.message !== undefined) {
|
|
3305
3431
|
wakeMessage = validatePendingSteer(outcome.message);
|
|
3306
3432
|
}
|
|
3307
|
-
else if (cp.state.
|
|
3433
|
+
else if (readPendingSteerQueue(cp.state).length === 0) {
|
|
3308
3434
|
throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
|
|
3309
3435
|
"wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
|
|
3310
3436
|
}
|
|
@@ -3318,8 +3444,7 @@ export class Runner {
|
|
|
3318
3444
|
if (!gateMatch) {
|
|
3319
3445
|
throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${outcome.gate}") does not match checkpoint gate "${cp.gate.kind}" — resume serves human/policy_ask, irreversible_ask/policy_ask, resource_limit/resource_limit, needs_review/dry_run_review, and plan_review/plan_review`);
|
|
3320
3446
|
}
|
|
3321
|
-
const reasonBearing = (outcome.gate === "
|
|
3322
|
-
(outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
|
|
3447
|
+
const reasonBearing = (outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
|
|
3323
3448
|
(outcome.gate === "plan_review" && outcome.decision === "reject");
|
|
3324
3449
|
if (reasonBearing && outcome.reason && sanitizeUntrustedText(outcome.reason) !== outcome.reason) {
|
|
3325
3450
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
@@ -3343,14 +3468,60 @@ export class Runner {
|
|
|
3343
3468
|
throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose edited plan differs from the recorded one");
|
|
3344
3469
|
}
|
|
3345
3470
|
}
|
|
3346
|
-
|
|
3471
|
+
let suppliedAnswer;
|
|
3472
|
+
let redeemedAnswer;
|
|
3473
|
+
let plainPolicyOutcome;
|
|
3474
|
+
if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
|
|
3475
|
+
const decide = outcome;
|
|
3476
|
+
suppliedAnswer = decide.answer;
|
|
3477
|
+
if (suppliedAnswer !== undefined) {
|
|
3478
|
+
const reading = classifyQuestionOutcome(suppliedAnswer);
|
|
3479
|
+
if (reading.shape !== "answered") {
|
|
3480
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried a content-ask `answer` that does not read as an answer (malformed, sparse, or not capturable as plain data) — " +
|
|
3481
|
+
"refusing pre-CAS rather than fabricating an empty human response; the checkpoint stays pending", { field: "answer" });
|
|
3482
|
+
}
|
|
3483
|
+
redeemedAnswer = reading.answer;
|
|
3484
|
+
}
|
|
3485
|
+
const updatedInputRaw = decide.updatedInput;
|
|
3486
|
+
let updatedInput;
|
|
3487
|
+
try {
|
|
3488
|
+
updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
|
|
3489
|
+
}
|
|
3490
|
+
catch {
|
|
3491
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` that could not be captured as plain data — refusing pre-CAS; the checkpoint stays pending");
|
|
3492
|
+
}
|
|
3493
|
+
if (Object.is(updatedInput, -0))
|
|
3494
|
+
updatedInput = 0;
|
|
3495
|
+
if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
|
|
3496
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` outside the plain-JSON value domain (a Map/Set/Date/buffer, a sparse or expando-carrying array, a non-finite number, or a cycle) — " +
|
|
3497
|
+
"such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
|
|
3498
|
+
}
|
|
3499
|
+
const decision = decide.decision;
|
|
3500
|
+
if (decision !== "allow" && decision !== "deny") {
|
|
3501
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${String(decision)}" is outside the policy_ask domain — a decide is exactly "allow" or "deny"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3502
|
+
}
|
|
3503
|
+
const reason = decide.reason;
|
|
3504
|
+
plainPolicyOutcome = {
|
|
3505
|
+
gate: "policy_ask",
|
|
3506
|
+
boundCallId: decide.boundCallId,
|
|
3507
|
+
boundInputHash: decide.boundInputHash,
|
|
3508
|
+
decision,
|
|
3509
|
+
...(updatedInput !== undefined ? { updatedInput } : {}),
|
|
3510
|
+
...(reason !== undefined ? { reason } : {}),
|
|
3511
|
+
...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
|
|
3512
|
+
};
|
|
3513
|
+
if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
|
|
3514
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
if (plainPolicyOutcome !== undefined) {
|
|
3347
3518
|
const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
|
|
3348
|
-
if (
|
|
3349
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${
|
|
3519
|
+
if (plainPolicyOutcome.boundCallId !== boundTo) {
|
|
3520
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${plainPolicyOutcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
|
|
3350
3521
|
}
|
|
3351
3522
|
const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
|
|
3352
3523
|
if (boundHash !== undefined) {
|
|
3353
|
-
if (
|
|
3524
|
+
if (plainPolicyOutcome.boundInputHash !== boundHash) {
|
|
3354
3525
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
|
|
3355
3526
|
}
|
|
3356
3527
|
}
|
|
@@ -3361,20 +3532,26 @@ export class Runner {
|
|
|
3361
3532
|
if (cp.resolvedOutcome === undefined) {
|
|
3362
3533
|
throw new CheckpointError("checkpoint.reopen_revote", "checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
|
|
3363
3534
|
}
|
|
3364
|
-
const incoming = winnerFromOutcome(
|
|
3365
|
-
|
|
3535
|
+
const incoming = winnerFromOutcome(plainPolicyOutcome);
|
|
3536
|
+
const persistedWinner = (() => {
|
|
3537
|
+
const w = cp.resolvedOutcome;
|
|
3538
|
+
if (w?.answer === undefined)
|
|
3539
|
+
return w;
|
|
3540
|
+
const reading = classifyQuestionOutcome(w.answer);
|
|
3541
|
+
return reading.shape === "answered" ? { ...w, answer: reading.answer } : w;
|
|
3542
|
+
})();
|
|
3543
|
+
if (incoming === undefined || !sameWinner(incoming, persistedWinner)) {
|
|
3366
3544
|
throw new CheckpointError("checkpoint.reopen_revote", `this checkpoint was reopened after an env-restore failure (env_failed) — a re-resume is a system retry of the already-approved action, not a re-vote; the supplied decision must replay the persisted winner (boundCallId "${cp.resolvedOutcome.boundCallId}", decision "${cp.resolvedOutcome.decision}"), refusing a different decision`);
|
|
3367
3545
|
}
|
|
3368
3546
|
}
|
|
3369
3547
|
const contentAskCallId = pendingContentAskCallId(cp);
|
|
3370
|
-
if (
|
|
3548
|
+
if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
|
|
3371
3549
|
throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
|
|
3372
3550
|
? "resume carried a content-ask `answer` but the checkpoint's pending call is not the reserved question tool — an answer has no consumer on a side-effecting tool's approval; refusing rather than dropping it silently"
|
|
3373
3551
|
: "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
|
|
3374
3552
|
}
|
|
3375
|
-
if (contentAskCallId !== undefined &&
|
|
3376
|
-
|
|
3377
|
-
if (liveFace === undefined || liveFace === QUESTION_AWAITS_RESUME) {
|
|
3553
|
+
if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
|
|
3554
|
+
if (!isLiveQuestionFace(taskConfig.onQuestion ?? this.deps.onQuestion)) {
|
|
3378
3555
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
|
|
3379
3556
|
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
|
|
3380
3557
|
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
|
|
@@ -3411,7 +3588,8 @@ export class Runner {
|
|
|
3411
3588
|
"rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
|
|
3412
3589
|
}
|
|
3413
3590
|
}
|
|
3414
|
-
const
|
|
3591
|
+
const outcomeForStore = plainPolicyOutcome ?? outcome;
|
|
3592
|
+
const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
|
|
3415
3593
|
if (!won) {
|
|
3416
3594
|
const live = await store.get(token);
|
|
3417
3595
|
if (live?.status === "pending") {
|
|
@@ -3425,8 +3603,8 @@ export class Runner {
|
|
|
3425
3603
|
await internals.afterCheckpointResolve();
|
|
3426
3604
|
consumeFlipDone = true;
|
|
3427
3605
|
}
|
|
3428
|
-
const answerFace =
|
|
3429
|
-
? answerFaceForRedeemedCall(
|
|
3606
|
+
const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined
|
|
3607
|
+
? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
|
|
3430
3608
|
: undefined;
|
|
3431
3609
|
const spec = {
|
|
3432
3610
|
...taskConfig,
|
|
@@ -3465,7 +3643,7 @@ export class Runner {
|
|
|
3465
3643
|
? new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run AND the store refused to reopen the checkpoint — the approval is terminally consumed and the suspended work was not executed; a retry needs a fresh approval")
|
|
3466
3644
|
: new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run and the reopen attempt FAILED IN FLIGHT — the checkpoint's state is unprovable from here: it may already be pending again. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead (the approved action did NOT run either way)");
|
|
3467
3645
|
}
|
|
3468
|
-
return this.runTaskStream(spec, { cp, outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
|
|
3646
|
+
return this.runTaskStream(spec, { cp, outcome: plainPolicyOutcome ?? outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
|
|
3469
3647
|
}
|
|
3470
3648
|
async applyResumeDecision(prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
|
|
3471
3649
|
const { pendingAction } = resume.cp;
|
|
@@ -3521,7 +3699,7 @@ export class Runner {
|
|
|
3521
3699
|
const { outcome } = resume;
|
|
3522
3700
|
if (pendingAction.kind !== "tool_approval" || outcome.gate !== "policy_ask")
|
|
3523
3701
|
return;
|
|
3524
|
-
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput
|
|
3702
|
+
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
|
|
3525
3703
|
const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
|
|
3526
3704
|
emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
|
|
3527
3705
|
const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
|
|
@@ -3532,6 +3710,9 @@ export class Runner {
|
|
|
3532
3710
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
3533
3711
|
return;
|
|
3534
3712
|
}
|
|
3713
|
+
if (outcome.decision !== "allow") {
|
|
3714
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `pending action reached the resolver with decision "${String(outcome.decision)}" — only "allow" executes and only "deny" injects a denial; refusing to execute`);
|
|
3715
|
+
}
|
|
3535
3716
|
if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
|
|
3536
3717
|
const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
|
|
3537
3718
|
if (rechecked.action === "deny") {
|
|
@@ -57,6 +57,7 @@ export const toolOutputFrom = (result) => {
|
|
|
57
57
|
return { output: raw, truncated: true, totalChars };
|
|
58
58
|
};
|
|
59
59
|
const CC_DETAIL_TYPES = new Set([
|
|
60
|
+
"ask-question",
|
|
60
61
|
"edit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
|
|
61
62
|
"agent", "task", "task-list", "task-output", "workflow-run",
|
|
62
63
|
"web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
|