@sema-agent/core 5.13.0 → 5.15.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 +418 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +133 -41
- 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/errors.js +21 -1
- 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/retry.d.ts +5 -0
- package/dist/brain/retry.js +16 -4
- package/dist/brain/stream-engine.d.ts +9 -1
- package/dist/brain/stream-engine.js +261 -28
- 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 +41 -17
- package/dist/core/checkpoint-store.js +137 -6
- 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/mcp.d.ts +7 -2
- package/dist/core/mcp.js +7 -7
- package/dist/core/memory-admission.d.ts +4 -0
- package/dist/core/memory-admission.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +3 -1
- package/dist/core/memory-engine/engine.js +4 -3
- package/dist/core/memory-recall.d.ts +1 -1
- package/dist/core/memory-recall.js +3 -2
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-memory.d.ts +1 -0
- package/dist/core/runner/prepare-memory.js +3 -3
- package/dist/core/runner/prepare-task.d.ts +19 -6
- package/dist/core/runner/prepare-task.js +343 -35
- package/dist/core/runner/runtask.d.ts +3 -6
- package/dist/core/runner/runtask.js +290 -64
- package/dist/core/runner/tool-disclosure.d.ts +5 -0
- package/dist/core/runner/tool-disclosure.js +65 -16
- package/dist/core/runner/tool-output-projection.js +1 -0
- package/dist/core/runner/turn-attachments.d.ts +4 -0
- package/dist/core/runner/turn-attachments.js +15 -2
- package/dist/core/session-store.d.ts +3 -0
- package/dist/core/session-store.js +4 -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/task-tool-shape.js +4 -3
- package/dist/core/tool-policy.d.ts +5 -0
- package/dist/core/tool-policy.js +2 -1
- package/dist/core/trace.d.ts +6 -0
- package/dist/core/types.d.ts +32 -1
- 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 +8 -1
- 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/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 +10 -7
- package/dist/index.js +8 -5
- 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/prompts/supervisor.d.ts +2 -2
- package/dist/prompts/supervisor.js +5 -4
- 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.d.ts +1 -0
- package/dist/tools/fs/fs-bash.js +32 -13
- package/dist/tools/fs/gh-rate-limit.d.ts +1 -1
- package/dist/tools/fs/gh-rate-limit.js +4 -3
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +1 -0
- 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")
|
|
@@ -188,6 +215,14 @@ function toolResultMsg(toolCallId, toolName, text, isError) {
|
|
|
188
215
|
timestamp: Date.now(),
|
|
189
216
|
};
|
|
190
217
|
}
|
|
218
|
+
function describeSuppliedValue(value) {
|
|
219
|
+
return typeof value === "string" ? value : value === null ? "null" : typeof value;
|
|
220
|
+
}
|
|
221
|
+
function assertReviewText(value, field) {
|
|
222
|
+
if (value !== undefined && typeof value !== "string") {
|
|
223
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume \`${field}\` is not a plain string (got ${typeof value}) — a review verdict's text is an operator's plain data, not a live object; refusing pre-CAS, the checkpoint stays pending`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
191
226
|
function resumeContinuation(resume) {
|
|
192
227
|
if (resume.outcome.gate === "wake") {
|
|
193
228
|
return formatHookFeedback("You were WOKEN from a parked pause by an operator. Before continuing, re-orient from the workspace: " +
|
|
@@ -548,6 +583,12 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
548
583
|
...(bgTasks !== undefined ? { backgroundTasks: bgTasks } : {}),
|
|
549
584
|
...(pendingTools !== undefined ? { newTools: pendingTools } : {}),
|
|
550
585
|
...(prepared.toolMaterializeStatic ? { newToolsStaticFace: true } : {}),
|
|
586
|
+
...(prepared.toolMaterializeStatic && pendingTools !== undefined
|
|
587
|
+
? { newToolsSwappedUnderStatic: pendingTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
|
|
588
|
+
: {}),
|
|
589
|
+
...(prepared.toolMaterializeStatic && pendingReaddedTools !== undefined
|
|
590
|
+
? { readdedToolsSwappedUnderStatic: pendingReaddedTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
|
|
591
|
+
: {}),
|
|
551
592
|
...(mcpToolsDelta !== undefined ? { mcpToolsDelta } : {}),
|
|
552
593
|
...(rs.attach.agentListingOn && prepared.agentListing !== undefined
|
|
553
594
|
? {
|
|
@@ -566,6 +607,12 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
566
607
|
const exact = renderToolsDelta({
|
|
567
608
|
...(pendingTools !== undefined ? { added: pendingTools } : {}),
|
|
568
609
|
...(prepared.toolMaterializeStatic ? { staticFace: true } : {}),
|
|
610
|
+
...(prepared.toolMaterializeStatic && pendingTools !== undefined
|
|
611
|
+
? { swappedUnderStatic: pendingTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
|
|
612
|
+
: {}),
|
|
613
|
+
...(prepared.toolMaterializeStatic && pendingReaddedTools !== undefined
|
|
614
|
+
? { readdedSwappedUnderStatic: pendingReaddedTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
|
|
615
|
+
: {}),
|
|
569
616
|
...(mcpToolsDelta ?? {}),
|
|
570
617
|
});
|
|
571
618
|
if (exact !== undefined && due.some((a) => a.source === "tools_delta" && a.body === exact)) {
|
|
@@ -1166,6 +1213,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1166
1213
|
const progress = {
|
|
1167
1214
|
type: "task_progress",
|
|
1168
1215
|
taskId: rs.telemetry.taskId,
|
|
1216
|
+
...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
|
|
1169
1217
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
1170
1218
|
...(subagentName ? { name: subagentName } : {}),
|
|
1171
1219
|
usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
|
|
@@ -1332,6 +1380,7 @@ export class Runner {
|
|
|
1332
1380
|
return runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles });
|
|
1333
1381
|
}
|
|
1334
1382
|
runTaskStream(spec, resume, internals) {
|
|
1383
|
+
const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
|
|
1335
1384
|
const queue = new PushQueue();
|
|
1336
1385
|
const detachHub = new ToolDetachHub();
|
|
1337
1386
|
let resultValue;
|
|
@@ -1386,7 +1435,7 @@ export class Runner {
|
|
|
1386
1435
|
publishReady(h);
|
|
1387
1436
|
}, (s) => {
|
|
1388
1437
|
reapHandle = s;
|
|
1389
|
-
}, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef);
|
|
1438
|
+
}, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
|
|
1390
1439
|
}
|
|
1391
1440
|
finally {
|
|
1392
1441
|
releaseLock?.();
|
|
@@ -1523,7 +1572,29 @@ export class Runner {
|
|
|
1523
1572
|
if (options?.trusted && sanitizeUntrustedText(text) !== text) {
|
|
1524
1573
|
throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
|
|
1525
1574
|
}
|
|
1526
|
-
const
|
|
1575
|
+
const actor = options?.actor === undefined ? undefined : snapshotActorAssertion(options.actor);
|
|
1576
|
+
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
1577
|
+
const payload = options?.trusted ? formatHookFeedback(projected) : projected;
|
|
1578
|
+
const mintsAFrame = payload.trim().length !== 0;
|
|
1579
|
+
const emitAccepted = (h) => {
|
|
1580
|
+
if (!mintsAFrame)
|
|
1581
|
+
return;
|
|
1582
|
+
queue.push({
|
|
1583
|
+
...buildHumanInputEvent({
|
|
1584
|
+
carrier: "steer",
|
|
1585
|
+
source: "steer",
|
|
1586
|
+
delivery: "queued",
|
|
1587
|
+
sessionSeq: nextHumanInputSeq(h.harness),
|
|
1588
|
+
...(actor !== undefined ? { actor } : {}),
|
|
1589
|
+
...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
|
|
1590
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
1591
|
+
}),
|
|
1592
|
+
eventId: uuidv7(),
|
|
1593
|
+
...(internals?.parentToolCallId !== undefined
|
|
1594
|
+
? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) }
|
|
1595
|
+
: {}),
|
|
1596
|
+
});
|
|
1597
|
+
};
|
|
1527
1598
|
const deliver = async () => {
|
|
1528
1599
|
if (resultValue)
|
|
1529
1600
|
throw steeringError("the task has already finished");
|
|
@@ -1531,7 +1602,8 @@ export class Runner {
|
|
|
1531
1602
|
if (!h)
|
|
1532
1603
|
throw steeringError("the task is not running");
|
|
1533
1604
|
try {
|
|
1534
|
-
await h.harness.steer(payload, { provenance: "engine-note" });
|
|
1605
|
+
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1606
|
+
emitAccepted(h);
|
|
1535
1607
|
return;
|
|
1536
1608
|
}
|
|
1537
1609
|
catch (e) {
|
|
@@ -1541,7 +1613,8 @@ export class Runner {
|
|
|
1541
1613
|
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
1542
1614
|
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1543
1615
|
try {
|
|
1544
|
-
await h.harness.steer(payload, { provenance: "engine-note" });
|
|
1616
|
+
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1617
|
+
emitAccepted(h);
|
|
1545
1618
|
return;
|
|
1546
1619
|
}
|
|
1547
1620
|
catch (e2) {
|
|
@@ -1658,7 +1731,7 @@ export class Runner {
|
|
|
1658
1731
|
})()),
|
|
1659
1732
|
};
|
|
1660
1733
|
}
|
|
1661
|
-
async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef) {
|
|
1734
|
+
async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, entryActor) {
|
|
1662
1735
|
const prepareResume = resume
|
|
1663
1736
|
? {
|
|
1664
1737
|
leafId: resume.cp.leafId,
|
|
@@ -1671,6 +1744,12 @@ export class Runner {
|
|
|
1671
1744
|
executesApprovedAction: resume.cp.pendingAction.kind === "tool_approval" &&
|
|
1672
1745
|
resume.outcome.gate !== "wake" &&
|
|
1673
1746
|
resume.outcome.decision === "allow",
|
|
1747
|
+
...(resume.outcome.gate !== "wake" && resume.outcome.decision === "allow" && pendingContentAskCallId(resume.cp) !== undefined
|
|
1748
|
+
? {
|
|
1749
|
+
redeemedContentAskCallId: pendingContentAskCallId(resume.cp),
|
|
1750
|
+
redeemedContentAskQuestionsHash: boundInputHashOf(resume.cp.pendingAction.args?.questions),
|
|
1751
|
+
}
|
|
1752
|
+
: {}),
|
|
1674
1753
|
}
|
|
1675
1754
|
: undefined;
|
|
1676
1755
|
const taskNotificationQueue = new SystemInjectionQueue();
|
|
@@ -1759,6 +1838,7 @@ export class Runner {
|
|
|
1759
1838
|
const parentToolCallId = internals?.parentToolCallId;
|
|
1760
1839
|
const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
|
|
1761
1840
|
notificationIdent = ident;
|
|
1841
|
+
queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
|
|
1762
1842
|
manualCompactRef.emitMooted = (reason) => {
|
|
1763
1843
|
queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
|
|
1764
1844
|
};
|
|
@@ -2133,6 +2213,7 @@ export class Runner {
|
|
|
2133
2213
|
...(prepared.promptManifest.tools ? { tools: prepared.promptManifest.tools } : {}),
|
|
2134
2214
|
...(prepared.promptManifest.snapshot ? { snapshot: prepared.promptManifest.snapshot } : {}),
|
|
2135
2215
|
...(prepared.promptManifest.lowering ? { lowering: prepared.promptManifest.lowering } : {}),
|
|
2216
|
+
...(prepared.promptManifest.toolDisclosure ? { toolDisclosure: prepared.promptManifest.toolDisclosure } : {}),
|
|
2136
2217
|
totalChars: prepared.promptManifest.blocks.reduce((n, b) => n + b.chars, 0),
|
|
2137
2218
|
ts: rs.telemetry.taskStart,
|
|
2138
2219
|
}));
|
|
@@ -2595,6 +2676,7 @@ export class Runner {
|
|
|
2595
2676
|
let final;
|
|
2596
2677
|
let threw;
|
|
2597
2678
|
let abortedLive = false;
|
|
2679
|
+
let strandedHumanAnswers = [];
|
|
2598
2680
|
try {
|
|
2599
2681
|
if (prepared.abortController.signal.aborted) {
|
|
2600
2682
|
const e = new Error("run aborted");
|
|
@@ -2647,15 +2729,31 @@ export class Runner {
|
|
|
2647
2729
|
}
|
|
2648
2730
|
}
|
|
2649
2731
|
const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2732
|
+
const resumeFrames = [
|
|
2733
|
+
...readPendingSteerQueue(resume.cp.state).map((entry) => ({ entry, source: "steer" })),
|
|
2734
|
+
...(resume.wakeMessage !== undefined ? [{ entry: resume.wakeMessage, source: "wake" }] : []),
|
|
2735
|
+
];
|
|
2736
|
+
for (const { entry: steer, source } of resumeFrames) {
|
|
2653
2737
|
const start = continuation.length;
|
|
2738
|
+
const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
|
|
2654
2739
|
continuation +=
|
|
2655
2740
|
"\n\n" +
|
|
2656
|
-
(steer.trusted ? formatHookFeedback(
|
|
2741
|
+
(steer.trusted ? formatHookFeedback(projected) : delimitUntrusted("supervisor steering message", projected));
|
|
2657
2742
|
if (steer.trusted)
|
|
2658
2743
|
engineSegments.push({ start, end: continuation.length });
|
|
2744
|
+
queue.push({
|
|
2745
|
+
...buildHumanInputEvent({
|
|
2746
|
+
carrier: source,
|
|
2747
|
+
source,
|
|
2748
|
+
delivery: "applied",
|
|
2749
|
+
sessionSeq: nextHumanInputSeq(prepared.harness),
|
|
2750
|
+
inputId: steer.inputId,
|
|
2751
|
+
...(steer.actor !== undefined ? { actor: steer.actor } : {}),
|
|
2752
|
+
...(steer.actor?.issuer !== undefined ? { issuer: steer.actor.issuer } : {}),
|
|
2753
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
2754
|
+
}),
|
|
2755
|
+
...ident(),
|
|
2756
|
+
});
|
|
2659
2757
|
}
|
|
2660
2758
|
if (resume !== undefined)
|
|
2661
2759
|
resume.decisionDelivered = true;
|
|
@@ -2663,7 +2761,9 @@ export class Runner {
|
|
|
2663
2761
|
}
|
|
2664
2762
|
}
|
|
2665
2763
|
else {
|
|
2666
|
-
|
|
2764
|
+
const objectiveActor = entryActor;
|
|
2765
|
+
const projectedObjective = projectHumanInput({ text: spec.objective, actor: objectiveActor, source: "objective" });
|
|
2766
|
+
let effectiveObjective = projectedObjective;
|
|
2667
2767
|
let promptBlocked = false;
|
|
2668
2768
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
2669
2769
|
if (userPromptSubmit) {
|
|
@@ -2674,7 +2774,7 @@ export class Runner {
|
|
|
2674
2774
|
promptBlocked = true;
|
|
2675
2775
|
}
|
|
2676
2776
|
else if (decision?.additionalContext) {
|
|
2677
|
-
effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${
|
|
2777
|
+
effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${projectedObjective}`;
|
|
2678
2778
|
}
|
|
2679
2779
|
}
|
|
2680
2780
|
catch (hookErr) {
|
|
@@ -2775,10 +2875,23 @@ export class Runner {
|
|
|
2775
2875
|
rs.attach.attachmentsInjected += firstFrames.length;
|
|
2776
2876
|
if (firstFrames.length > 0)
|
|
2777
2877
|
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
2778
|
-
const enginePrefixChars = effectiveObjective.length -
|
|
2878
|
+
const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
|
|
2879
|
+
queue.push({
|
|
2880
|
+
...buildHumanInputEvent({
|
|
2881
|
+
carrier: "objective",
|
|
2882
|
+
source: "objective",
|
|
2883
|
+
delivery: "applied",
|
|
2884
|
+
sessionSeq: nextHumanInputSeq(prepared.harness),
|
|
2885
|
+
...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
|
|
2886
|
+
...(objectiveActor?.issuer !== undefined ? { issuer: objectiveActor.issuer } : {}),
|
|
2887
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
2888
|
+
}),
|
|
2889
|
+
...ident(),
|
|
2890
|
+
});
|
|
2779
2891
|
final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
|
|
2780
2892
|
...(images && images.length > 0 ? { images } : {}),
|
|
2781
2893
|
...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
|
|
2894
|
+
...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
|
|
2782
2895
|
}));
|
|
2783
2896
|
}
|
|
2784
2897
|
}
|
|
@@ -2801,6 +2914,7 @@ export class Runner {
|
|
|
2801
2914
|
prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
|
|
2802
2915
|
prepared.abortController.abort();
|
|
2803
2916
|
prepared.releaseSignal();
|
|
2917
|
+
strandedHumanAnswers = prepared.settleContentAskBindings();
|
|
2804
2918
|
try {
|
|
2805
2919
|
prepared.harness.recoverUndrainedEngineNotes();
|
|
2806
2920
|
}
|
|
@@ -2939,6 +3053,7 @@ export class Runner {
|
|
|
2939
3053
|
model: prepared.model.id,
|
|
2940
3054
|
unpricedSpend: rs.telemetry.unpricedSpend,
|
|
2941
3055
|
rewindNotes: prepared.rewindNotes,
|
|
3056
|
+
strandedHumanAnswers,
|
|
2942
3057
|
remoteEnvFailures: prepared.remoteEnvFailures,
|
|
2943
3058
|
retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
|
|
2944
3059
|
abortedForTimeout: timeout.fired,
|
|
@@ -3075,6 +3190,7 @@ export class Runner {
|
|
|
3075
3190
|
const terminalTick = {
|
|
3076
3191
|
type: "task_progress",
|
|
3077
3192
|
taskId: rs.telemetry.taskId,
|
|
3193
|
+
...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
|
|
3078
3194
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
3079
3195
|
...(subagentName ? { name: subagentName } : {}),
|
|
3080
3196
|
usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
|
|
@@ -3281,6 +3397,9 @@ export class Runner {
|
|
|
3281
3397
|
return stream.result();
|
|
3282
3398
|
}
|
|
3283
3399
|
async resumeStream(token, outcome, taskConfig, internals) {
|
|
3400
|
+
if (outcome === null || typeof outcome !== "object") {
|
|
3401
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
|
|
3402
|
+
}
|
|
3284
3403
|
const store = resolveCheckpointStore(taskConfig, this.deps);
|
|
3285
3404
|
if (!store) {
|
|
3286
3405
|
throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
|
|
@@ -3299,7 +3418,10 @@ export class Runner {
|
|
|
3299
3418
|
}
|
|
3300
3419
|
}
|
|
3301
3420
|
let wakeMessage;
|
|
3302
|
-
|
|
3421
|
+
const outcomeGate = outcome.gate;
|
|
3422
|
+
let suppliedMessage;
|
|
3423
|
+
if (outcomeGate === "wake") {
|
|
3424
|
+
suppliedMessage = outcome.message;
|
|
3303
3425
|
const gk = cp.gate.kind;
|
|
3304
3426
|
const decideEntry = gk === "human" || gk === "irreversible_ask"
|
|
3305
3427
|
? 'a `policy_ask` outcome (decision allow/deny bound to the pending tool call)'
|
|
@@ -3332,56 +3454,147 @@ export class Runner {
|
|
|
3332
3454
|
`(newer-version row?) — fail-closed: wake only serves a pure park (pendingAction "task_done")`);
|
|
3333
3455
|
}
|
|
3334
3456
|
}
|
|
3335
|
-
if (
|
|
3336
|
-
wakeMessage = validatePendingSteer(
|
|
3457
|
+
if (suppliedMessage !== undefined) {
|
|
3458
|
+
wakeMessage = validatePendingSteer(suppliedMessage);
|
|
3337
3459
|
}
|
|
3338
|
-
else if (cp.state.
|
|
3460
|
+
else if (readPendingSteerQueue(cp.state).length === 0) {
|
|
3339
3461
|
throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
|
|
3340
3462
|
"wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
|
|
3341
3463
|
}
|
|
3342
3464
|
}
|
|
3343
|
-
const gateMatch =
|
|
3344
|
-
(cp.gate.kind === "human" &&
|
|
3345
|
-
(cp.gate.kind === "irreversible_ask" &&
|
|
3346
|
-
(cp.gate.kind === "resource_limit" &&
|
|
3347
|
-
(cp.gate.kind === "needs_review" &&
|
|
3348
|
-
(cp.gate.kind === "plan_review" &&
|
|
3465
|
+
const gateMatch = outcomeGate === "wake" ||
|
|
3466
|
+
(cp.gate.kind === "human" && outcomeGate === "policy_ask") ||
|
|
3467
|
+
(cp.gate.kind === "irreversible_ask" && outcomeGate === "policy_ask") ||
|
|
3468
|
+
(cp.gate.kind === "resource_limit" && outcomeGate === "resource_limit") ||
|
|
3469
|
+
(cp.gate.kind === "needs_review" && outcomeGate === "dry_run_review") ||
|
|
3470
|
+
(cp.gate.kind === "plan_review" && outcomeGate === "plan_review");
|
|
3349
3471
|
if (!gateMatch) {
|
|
3350
|
-
throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${
|
|
3351
|
-
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3472
|
+
throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") 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`);
|
|
3473
|
+
}
|
|
3474
|
+
let plainReviewOutcome;
|
|
3475
|
+
if (cp.gate.kind === "plan_review") {
|
|
3476
|
+
const decide = outcome;
|
|
3477
|
+
const decision = decide.decision;
|
|
3478
|
+
const editedPlan = decide.editedPlan;
|
|
3479
|
+
const reason = decide.reason;
|
|
3480
|
+
if (decision !== "approve" && decision !== "edit" && decision !== "reject") {
|
|
3481
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the plan_review domain — a plan review is exactly "approve", "edit" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3482
|
+
}
|
|
3483
|
+
assertReviewText(editedPlan, "editedPlan");
|
|
3484
|
+
assertReviewText(reason, "reason");
|
|
3485
|
+
plainReviewOutcome = {
|
|
3486
|
+
gate: "plan_review",
|
|
3487
|
+
decision,
|
|
3488
|
+
...(editedPlan !== undefined ? { editedPlan } : {}),
|
|
3489
|
+
...(reason !== undefined ? { reason } : {}),
|
|
3490
|
+
};
|
|
3357
3491
|
}
|
|
3358
|
-
if (
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3492
|
+
else if (cp.gate.kind === "needs_review") {
|
|
3493
|
+
const decide = outcome;
|
|
3494
|
+
const decision = decide.decision;
|
|
3495
|
+
const reason = decide.reason;
|
|
3496
|
+
if (decision !== "approve" && decision !== "reject") {
|
|
3497
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the dry_run_review domain — a dry-run review is exactly "approve" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3498
|
+
}
|
|
3499
|
+
assertReviewText(reason, "reason");
|
|
3500
|
+
plainReviewOutcome = {
|
|
3501
|
+
gate: "dry_run_review",
|
|
3502
|
+
decision,
|
|
3503
|
+
...(reason !== undefined ? { reason } : {}),
|
|
3504
|
+
};
|
|
3363
3505
|
}
|
|
3364
|
-
if (
|
|
3365
|
-
const
|
|
3366
|
-
if (
|
|
3367
|
-
throw new CheckpointError("checkpoint.
|
|
3506
|
+
if (plainReviewOutcome !== undefined) {
|
|
3507
|
+
const reason = plainReviewOutcome.reason;
|
|
3508
|
+
if (plainReviewOutcome.decision === "reject" && reason && sanitizeUntrustedText(reason) !== reason) {
|
|
3509
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
3510
|
+
}
|
|
3511
|
+
const capturedPlan = plainReviewOutcome.gate === "plan_review" ? plainReviewOutcome.editedPlan : undefined;
|
|
3512
|
+
if (plainReviewOutcome.decision === "edit" &&
|
|
3513
|
+
capturedPlan &&
|
|
3514
|
+
sanitizeUntrustedText(capturedPlan) !== capturedPlan) {
|
|
3515
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume editedPlan must not contain a </system-reminder> tag");
|
|
3516
|
+
}
|
|
3517
|
+
if (cp.reopenReason === "env_failed") {
|
|
3518
|
+
const winner = cp.resolvedOutcome;
|
|
3519
|
+
if (winner === undefined) {
|
|
3520
|
+
throw new CheckpointError("checkpoint.reopen_revote", "review checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
|
|
3521
|
+
}
|
|
3522
|
+
if (winner.decision !== plainReviewOutcome.decision) {
|
|
3523
|
+
throw new CheckpointError("checkpoint.reopen_revote", `an env_failed reopen replays the ALREADY-RECORDED review decision ("${winner.decision}") — refusing a re-vote with a different decision ("${plainReviewOutcome.decision}")`);
|
|
3524
|
+
}
|
|
3525
|
+
if (winner.updatedInput !== capturedPlan) {
|
|
3526
|
+
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");
|
|
3527
|
+
}
|
|
3368
3528
|
}
|
|
3369
|
-
|
|
3370
|
-
|
|
3529
|
+
}
|
|
3530
|
+
let plainParkOutcome;
|
|
3531
|
+
if (outcomeGate === "wake") {
|
|
3532
|
+
plainParkOutcome = {
|
|
3533
|
+
gate: "wake",
|
|
3534
|
+
...(wakeMessage !== undefined ? { message: wakeMessage } : {}),
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
else if (outcomeGate === "resource_limit") {
|
|
3538
|
+
const decision = outcome.decision;
|
|
3539
|
+
if (decision !== "continue") {
|
|
3540
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the resource_limit domain — a slice resume is exactly "continue"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3541
|
+
}
|
|
3542
|
+
plainParkOutcome = { gate: "resource_limit", decision };
|
|
3543
|
+
}
|
|
3544
|
+
let suppliedAnswer;
|
|
3545
|
+
let redeemedAnswer;
|
|
3546
|
+
let plainPolicyOutcome;
|
|
3547
|
+
if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
|
|
3548
|
+
const decide = outcome;
|
|
3549
|
+
suppliedAnswer = decide.answer;
|
|
3550
|
+
if (suppliedAnswer !== undefined) {
|
|
3551
|
+
const reading = classifyQuestionOutcome(suppliedAnswer);
|
|
3552
|
+
if (reading.shape !== "answered") {
|
|
3553
|
+
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) — " +
|
|
3554
|
+
"refusing pre-CAS rather than fabricating an empty human response; the checkpoint stays pending", { field: "answer" });
|
|
3555
|
+
}
|
|
3556
|
+
redeemedAnswer = reading.answer;
|
|
3557
|
+
}
|
|
3558
|
+
const updatedInputRaw = decide.updatedInput;
|
|
3559
|
+
let updatedInput;
|
|
3560
|
+
try {
|
|
3561
|
+
updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
|
|
3371
3562
|
}
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3563
|
+
catch {
|
|
3564
|
+
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");
|
|
3565
|
+
}
|
|
3566
|
+
if (Object.is(updatedInput, -0))
|
|
3567
|
+
updatedInput = 0;
|
|
3568
|
+
if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
|
|
3569
|
+
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) — " +
|
|
3570
|
+
"such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
|
|
3571
|
+
}
|
|
3572
|
+
const decision = decide.decision;
|
|
3573
|
+
if (decision !== "allow" && decision !== "deny") {
|
|
3574
|
+
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`);
|
|
3575
|
+
}
|
|
3576
|
+
const reason = decide.reason;
|
|
3577
|
+
plainPolicyOutcome = {
|
|
3578
|
+
gate: "policy_ask",
|
|
3579
|
+
boundCallId: decide.boundCallId,
|
|
3580
|
+
boundInputHash: decide.boundInputHash,
|
|
3581
|
+
decision,
|
|
3582
|
+
...(updatedInput !== undefined ? { updatedInput } : {}),
|
|
3583
|
+
...(reason !== undefined ? { reason } : {}),
|
|
3584
|
+
...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
|
|
3585
|
+
};
|
|
3586
|
+
if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
|
|
3587
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
3375
3588
|
}
|
|
3376
3589
|
}
|
|
3377
|
-
if (
|
|
3590
|
+
if (plainPolicyOutcome !== undefined) {
|
|
3378
3591
|
const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
|
|
3379
|
-
if (
|
|
3380
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${
|
|
3592
|
+
if (plainPolicyOutcome.boundCallId !== boundTo) {
|
|
3593
|
+
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" });
|
|
3381
3594
|
}
|
|
3382
3595
|
const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
|
|
3383
3596
|
if (boundHash !== undefined) {
|
|
3384
|
-
if (
|
|
3597
|
+
if (plainPolicyOutcome.boundInputHash !== boundHash) {
|
|
3385
3598
|
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" });
|
|
3386
3599
|
}
|
|
3387
3600
|
}
|
|
@@ -3392,20 +3605,26 @@ export class Runner {
|
|
|
3392
3605
|
if (cp.resolvedOutcome === undefined) {
|
|
3393
3606
|
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)");
|
|
3394
3607
|
}
|
|
3395
|
-
const incoming = winnerFromOutcome(
|
|
3396
|
-
|
|
3608
|
+
const incoming = winnerFromOutcome(plainPolicyOutcome);
|
|
3609
|
+
const persistedWinner = (() => {
|
|
3610
|
+
const w = cp.resolvedOutcome;
|
|
3611
|
+
if (w?.answer === undefined)
|
|
3612
|
+
return w;
|
|
3613
|
+
const reading = classifyQuestionOutcome(w.answer);
|
|
3614
|
+
return reading.shape === "answered" ? { ...w, answer: reading.answer } : w;
|
|
3615
|
+
})();
|
|
3616
|
+
if (incoming === undefined || !sameWinner(incoming, persistedWinner)) {
|
|
3397
3617
|
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`);
|
|
3398
3618
|
}
|
|
3399
3619
|
}
|
|
3400
3620
|
const contentAskCallId = pendingContentAskCallId(cp);
|
|
3401
|
-
if (
|
|
3621
|
+
if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
|
|
3402
3622
|
throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
|
|
3403
3623
|
? "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"
|
|
3404
3624
|
: "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" });
|
|
3405
3625
|
}
|
|
3406
|
-
if (contentAskCallId !== undefined &&
|
|
3407
|
-
|
|
3408
|
-
if (liveFace === undefined || liveFace === QUESTION_AWAITS_RESUME) {
|
|
3626
|
+
if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
|
|
3627
|
+
if (!isLiveQuestionFace(taskConfig.onQuestion ?? this.deps.onQuestion)) {
|
|
3409
3628
|
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 — " +
|
|
3410
3629
|
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
|
|
3411
3630
|
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
|
|
@@ -3442,7 +3661,11 @@ export class Runner {
|
|
|
3442
3661
|
"rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
|
|
3443
3662
|
}
|
|
3444
3663
|
}
|
|
3445
|
-
const
|
|
3664
|
+
const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
|
|
3665
|
+
if (outcomeForStore === undefined) {
|
|
3666
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") matched the checkpoint gate but no lane captured it — refusing pre-CAS rather than passing the caller's live object to the store and the resumed run`);
|
|
3667
|
+
}
|
|
3668
|
+
const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
|
|
3446
3669
|
if (!won) {
|
|
3447
3670
|
const live = await store.get(token);
|
|
3448
3671
|
if (live?.status === "pending") {
|
|
@@ -3456,8 +3679,8 @@ export class Runner {
|
|
|
3456
3679
|
await internals.afterCheckpointResolve();
|
|
3457
3680
|
consumeFlipDone = true;
|
|
3458
3681
|
}
|
|
3459
|
-
const answerFace =
|
|
3460
|
-
? answerFaceForRedeemedCall(
|
|
3682
|
+
const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined
|
|
3683
|
+
? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
|
|
3461
3684
|
: undefined;
|
|
3462
3685
|
const spec = {
|
|
3463
3686
|
...taskConfig,
|
|
@@ -3496,7 +3719,7 @@ export class Runner {
|
|
|
3496
3719
|
? 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")
|
|
3497
3720
|
: 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)");
|
|
3498
3721
|
}
|
|
3499
|
-
return this.runTaskStream(spec, { cp, outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
|
|
3722
|
+
return this.runTaskStream(spec, { cp, outcome: outcomeForStore, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
|
|
3500
3723
|
}
|
|
3501
3724
|
async applyResumeDecision(prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
|
|
3502
3725
|
const { pendingAction } = resume.cp;
|
|
@@ -3552,7 +3775,7 @@ export class Runner {
|
|
|
3552
3775
|
const { outcome } = resume;
|
|
3553
3776
|
if (pendingAction.kind !== "tool_approval" || outcome.gate !== "policy_ask")
|
|
3554
3777
|
return;
|
|
3555
|
-
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput
|
|
3778
|
+
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
|
|
3556
3779
|
const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
|
|
3557
3780
|
emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
|
|
3558
3781
|
const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
|
|
@@ -3563,6 +3786,9 @@ export class Runner {
|
|
|
3563
3786
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
3564
3787
|
return;
|
|
3565
3788
|
}
|
|
3789
|
+
if (outcome.decision !== "allow") {
|
|
3790
|
+
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`);
|
|
3791
|
+
}
|
|
3566
3792
|
if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
|
|
3567
3793
|
const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
|
|
3568
3794
|
if (rechecked.action === "deny") {
|