@sema-agent/core 7.12.0 → 7.13.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 +44 -0
- package/dist/core/ask-origin.d.ts +55 -0
- package/dist/core/ask-origin.js +21 -0
- package/dist/core/engine-notice.d.ts +18 -0
- package/dist/core/gate-lanes.d.ts +0 -40
- package/dist/core/gate-lanes.js +3 -17
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/hooks.js +1 -1
- package/dist/core/runner/assemble-result.d.ts +45 -1
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/compaction-seams.d.ts +42 -0
- package/dist/core/runner/compaction-seams.js +80 -0
- package/dist/core/runner/contracts.d.ts +44 -6
- package/dist/core/runner/denial-limit-arms.js +3 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
- package/dist/core/runner/permission-rule-lanes.js +2 -1
- package/dist/core/runner/prepare-policy-chain.js +3 -2
- package/dist/core/runner/prepare-protocol-tools.js +5 -0
- package/dist/core/runner/prepare-safety-scan.js +11 -16
- package/dist/core/runner/prepare-task.d.ts +9 -1
- package/dist/core/runner/prepare-task.js +6 -1
- package/dist/core/runner/resume-claim.d.ts +2 -2
- package/dist/core/runner/resume-preflight.d.ts +2 -2
- package/dist/core/runner/run-attachment-seats.d.ts +2 -2
- package/dist/core/runner/run-git-lane.d.ts +1 -1
- package/dist/core/runner/run-harness-handlers.js +2 -0
- package/dist/core/runner/run-identity-wiring.d.ts +7 -0
- package/dist/core/runner/run-identity-wiring.js +3 -1
- package/dist/core/runner/run-leg.d.ts +7 -5
- package/dist/core/runner/run-leg.js +256 -5
- package/dist/core/runner/run-notification-lane.d.ts +4 -3
- package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
- package/dist/core/runner/run-recovery-lanes.js +5 -4
- package/dist/core/runner/run-settle-and-teardown.d.ts +14 -10
- package/dist/core/runner/run-settle-and-teardown.js +112 -4
- package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
- package/dist/core/runner/run-terminal-adoption.d.ts +16 -16
- package/dist/core/runner/run-terminal-adoption.js +87 -7
- package/dist/core/runner/run-turn-boundary.js +5 -4
- package/dist/core/runner/runtask.d.ts +10 -90
- package/dist/core/runner/runtask.js +77 -596
- package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
- package/dist/core/task-stream.d.ts +10 -1
- package/dist/core/tool-face.d.ts +8 -0
- package/dist/core/tool-face.js +1 -0
- package/dist/core/tool-policy.d.ts +5 -3
- package/dist/core/tool-registry.d.ts +11 -3
- package/dist/core/tool-registry.js +7 -1
- package/dist/core/tool-roster.d.ts +26 -0
- package/dist/core/tool-roster.js +38 -7
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/server/http.js +2 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +11 -1
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
2
2
|
import { redactThenCut } from "../../agents/subagent-steps.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
|
|
4
|
+
import { CheckpointError, readPendingSteerQueue, realApprovalOrgFact, remainingBudgetMicroUsd } from "../checkpoint-store.js";
|
|
5
|
+
import { normalizePersistedRuleHit } from "../gate-lanes.js";
|
|
4
6
|
import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
|
|
5
7
|
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
8
|
+
import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
|
|
9
|
+
import { applyPersistedTightening, disclosedRuleSet } from "../persisted-rule-arms.js";
|
|
6
10
|
import { computeCostMicroUsd } from "../pricing.js";
|
|
7
11
|
import { mintSystemReminder } from "../reminder-mint.js";
|
|
8
12
|
import { hasDestroy } from "../remote-env.js";
|
|
13
|
+
import { refuseOutOfContractDecision } from "../tool-policy.js";
|
|
14
|
+
import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
|
|
9
15
|
import { deliverEngineNotice } from "../types.js";
|
|
10
|
-
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
16
|
+
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
11
17
|
import { errorCodeOf } from "./assemble-result.js";
|
|
12
18
|
import { DEFAULT_PRECALL_OUTPUT_TOKENS, platformLimitTerminal } from "./clock-and-limits.js";
|
|
13
|
-
import { resumeContinuation } from "./decide-continuation.js";
|
|
19
|
+
import { deepJsonEqual, DEFERRED_REISSUE, resumeContinuation } from "./decide-continuation.js";
|
|
20
|
+
import { settleExecutionRecord } from "./execution-record.js";
|
|
21
|
+
import { mintGateOutcome } from "./gate-exit.js";
|
|
14
22
|
import { resolveGitLegDelivery, wrapGitFrame } from "./git-leg-delivery.js";
|
|
15
23
|
import { GIT_STATUS_ECHO_PREVIEW } from "./git-status-frame.js";
|
|
16
24
|
import { toImageContent } from "./image.js";
|
|
17
25
|
import { nextHumanInputSeq } from "./steer-admission.js";
|
|
18
26
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
27
|
+
import { toolEndBodyFrom, toolResultMsg } from "./tool-end-body.js";
|
|
19
28
|
import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingDelta, renderOrphanedBackgroundTasks, renderSkillsListingDelta, stampWriteAnchor } from "./turn-attachments.js";
|
|
20
29
|
export async function runLeg(input) {
|
|
21
|
-
const { spec, queue, prepared, resume, rs, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner,
|
|
30
|
+
const { spec, queue, prepared, resume, rs, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
|
|
22
31
|
let final;
|
|
23
32
|
let threw;
|
|
24
33
|
let abortedLive = false;
|
|
@@ -55,7 +64,7 @@ export async function runLeg(input) {
|
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
if (resumeWindowRetryAfterMs === undefined && !walltimeExhaustedResume && resume.outcome.gate !== "wake") {
|
|
58
|
-
await applyResumeDecision(prepared, resume, (e) => pushContent({ ...e, ...ident() }), emitCommitted, (toolName, details) => {
|
|
67
|
+
await applyResumeDecision(runner, prepared, resume, (e) => pushContent({ ...e, ...ident() }), emitCommitted, (toolName, details) => {
|
|
59
68
|
if (rs.attach.attachState === undefined)
|
|
60
69
|
return;
|
|
61
70
|
const family = writeFamilyOf(toolName);
|
|
@@ -460,3 +469,245 @@ export async function runLeg(input) {
|
|
|
460
469
|
}
|
|
461
470
|
return await next({ final, threw, abortedLive, userInterruptedLive, strandedHumanAnswers });
|
|
462
471
|
}
|
|
472
|
+
const ORG_DISCLOSURE_MAX_CHARS = 600;
|
|
473
|
+
async function applyResumeDecision(runner, prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
|
|
474
|
+
const { pendingAction } = resume.cp;
|
|
475
|
+
if (resume.outcome.gate === "dry_run_review")
|
|
476
|
+
return;
|
|
477
|
+
if (resume.outcome.gate === "plan_review")
|
|
478
|
+
return;
|
|
479
|
+
if (pendingAction.kind !== "tool_approval")
|
|
480
|
+
return;
|
|
481
|
+
if (resume.outcome.gate === "policy_ask" &&
|
|
482
|
+
resume.outcome.decision === "allow" &&
|
|
483
|
+
!prepared.tools.some((t) => t.name === pendingAction.toolName)) {
|
|
484
|
+
const e = new Error(`the approved tool "${pendingAction.toolName}" is no longer available on resume — refusing to continue as if it ran; the checkpoint is reopened for a retry with the tool present`);
|
|
485
|
+
e.code = "resume.tool_unavailable";
|
|
486
|
+
throw e;
|
|
487
|
+
}
|
|
488
|
+
const liveRoster = prepared.toolRosterDeltas.current;
|
|
489
|
+
if (pendingAction.kind === "tool_approval" && liveRoster !== undefined && judgeParkedToolIdentity(pendingAction, rosterEntryNamed(liveRoster, pendingAction.toolName)) === "mismatch") {
|
|
490
|
+
const e = new Error(`the approved tool "${pendingAction.toolName}" on this leg is not the tool the approval was minted against (its contract / shape / revision differ) — refusing to execute a replacement wearing the name; the checkpoint is reopened for a retry with the original tool present`);
|
|
491
|
+
e.code = "resume.tool_contract_mismatch";
|
|
492
|
+
throw e;
|
|
493
|
+
}
|
|
494
|
+
const completed = new Set(pendingAction.completedCallIds);
|
|
495
|
+
const deferredIds = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !completed.has(id));
|
|
496
|
+
const names = new Map();
|
|
497
|
+
const deferredArgs = new Map();
|
|
498
|
+
if (deferredIds.length > 0) {
|
|
499
|
+
const { messages } = await prepared.session.buildContext();
|
|
500
|
+
for (const m of messages) {
|
|
501
|
+
if (m.role === "assistant") {
|
|
502
|
+
for (const c of m.content) {
|
|
503
|
+
if (c.type === "toolCall") {
|
|
504
|
+
names.set(c.id, c.name);
|
|
505
|
+
deferredArgs.set(c.id, c.arguments);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
for (const id of pendingAction.batchToolCallIds) {
|
|
512
|
+
if (id === pendingAction.toolCallId) {
|
|
513
|
+
await resolvePendingCall(runner, prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart);
|
|
514
|
+
}
|
|
515
|
+
else if (!completed.has(id)) {
|
|
516
|
+
const name = names.get(id) ?? "unknown";
|
|
517
|
+
const displayLabel = (() => {
|
|
518
|
+
const l = prepared.tools.find((t) => t.name === name)?.label;
|
|
519
|
+
return l !== undefined && l !== name ? { label: l } : {};
|
|
520
|
+
})();
|
|
521
|
+
emit({ type: "tool_start", toolCallId: id, toolName: name, ...displayLabel, args: deferredArgs.get(id) ?? {} });
|
|
522
|
+
emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark) }, true, prepared.structuredProjector) });
|
|
523
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark), true));
|
|
524
|
+
emitCommitted(eid, "toolResult", id);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
|
|
529
|
+
const { pendingAction } = resume.cp;
|
|
530
|
+
const { outcome } = resume;
|
|
531
|
+
if (pendingAction.kind !== "tool_approval" || outcome.gate !== "policy_ask")
|
|
532
|
+
return;
|
|
533
|
+
const decidedGate = outcome.gateOutcome;
|
|
534
|
+
if (decidedGate === undefined) {
|
|
535
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "pending action reached the resolver with no settlement record on its decision (gateOutcome) — refusing to execute or settle a decision that carries no record");
|
|
536
|
+
}
|
|
537
|
+
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
|
|
538
|
+
const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
|
|
539
|
+
emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
|
|
540
|
+
const settleEnd = async (isError, result, gate = decidedGate) => {
|
|
541
|
+
emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, prepared.structuredProjector, gate) });
|
|
542
|
+
const word = await resume.recordExecutionOutcome(gate);
|
|
543
|
+
settleExecutionRecord(runner.deps.onNotice, word, { toolName: pendingAction.toolName, gate, sessionId: prepared.sessionId, runId: prepared.runId, scope: resume.cp.scope, ...(resume.cp.checkpointId !== undefined ? { checkpointId: resume.cp.checkpointId } : {}) });
|
|
544
|
+
};
|
|
545
|
+
const vetoed = (layer) => mintGateOutcome({
|
|
546
|
+
deniedBy: layer,
|
|
547
|
+
...(outcome.gateOutcome?.settlement !== undefined && outcome.gateOutcome.origin !== undefined ? { settled: { settlement: outcome.gateOutcome.settlement, origin: outcome.gateOutcome.origin } } : {}),
|
|
548
|
+
});
|
|
549
|
+
if (outcome.decision === "deny") {
|
|
550
|
+
const defaultDenial = outcome.gateOutcome?.settlement?.kind === "park_sla_expired"
|
|
551
|
+
? `No one answered the approval request for the pending tool call "${pendingAction.toolName}" — the approval window elapsed with no answer, so it was not executed.`
|
|
552
|
+
: `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
|
|
553
|
+
const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason, REVIEWER_NOTE_MAX_BODY) : defaultDenial;
|
|
554
|
+
await settleEnd(true, { content: formatHookFeedback(reason, prepared.reminderMark) });
|
|
555
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason, prepared.reminderMark), true));
|
|
556
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (outcome.decision !== "allow") {
|
|
560
|
+
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`);
|
|
561
|
+
}
|
|
562
|
+
const beltBudget = {
|
|
563
|
+
resourceRemainingMicroUsd: remainingBudgetMicroUsd(prepared.resourceLedger),
|
|
564
|
+
resourceSpentMicroUsd: prepared.resourceLedger?.spentMicroUsd ?? 0,
|
|
565
|
+
suspendCount: resume.cp.suspendCount ?? 0,
|
|
566
|
+
};
|
|
567
|
+
if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
|
|
568
|
+
const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({
|
|
569
|
+
toolName: pendingAction.toolName,
|
|
570
|
+
args: resolvedArgs,
|
|
571
|
+
toolCallId: pendingAction.toolCallId,
|
|
572
|
+
budget: beltBudget,
|
|
573
|
+
...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
|
|
574
|
+
...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
|
|
575
|
+
}, prepared.abortController.signal));
|
|
576
|
+
const beltUnsafe = rechecked.action === "deny" ||
|
|
577
|
+
(rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, resolvedArgs));
|
|
578
|
+
if (beltUnsafe) {
|
|
579
|
+
const editedDenial = formatHookFeedback(`The approver EDITED this call's input; the edited call is refused by the deployment's tool policy and was not executed${rechecked.action === "deny" && rechecked.message ? `: ${rechecked.message}` : " (the policy rewrote the edit to a value that was not approved)"}.`, prepared.reminderMark);
|
|
580
|
+
await settleEnd(true, { content: editedDenial }, vetoed("policy"));
|
|
581
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, editedDenial, true));
|
|
582
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const gateRealApproval = resume.cp.gate.kind === "irreversible_ask" ? resume.cp.gate.realApproval : undefined;
|
|
587
|
+
const gateOrgGoverned = realApprovalOrgFact(gateRealApproval) !== undefined;
|
|
588
|
+
if (gateOrgGoverned && prepared.permissionRuleOrg === undefined) {
|
|
589
|
+
const unwiredDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: its approval was minted under organization governance (${gateRealApproval.origin}), and this worker has no org adjudication wiring — a governed approval may only be redeemed where governance can be enforced. This approval is spent; re-issue the call on an org-wired worker.`, prepared.reminderMark);
|
|
590
|
+
await settleEnd(true, { content: unwiredDenial }, vetoed("org"));
|
|
591
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, unwiredDenial, true));
|
|
592
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (prepared.permissionRuleOrg !== undefined) {
|
|
596
|
+
const orgVerdict = prepared.permissionRuleOrg
|
|
597
|
+
.adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
|
|
598
|
+
.catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
|
|
599
|
+
let orgWaitCancelled = false;
|
|
600
|
+
const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, {
|
|
601
|
+
signal: prepared.abortController.signal,
|
|
602
|
+
timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS,
|
|
603
|
+
onFallback: (cause) => {
|
|
604
|
+
orgWaitCancelled = cause === "aborted";
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
const blocked = org.status === "unavailable"
|
|
608
|
+
? realApprovalOrgFact(gateRealApproval) === "unavailable"
|
|
609
|
+
? undefined
|
|
610
|
+
: orgWaitCancelled
|
|
611
|
+
? "this deployment is org-governed and the task was cancelled before the organization policy snapshot could be adjudicated"
|
|
612
|
+
: "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
|
|
613
|
+
: org.verdict?.behavior === "deny"
|
|
614
|
+
? `an organization policy rule (${org.verdict.rule}) denies it`
|
|
615
|
+
: undefined;
|
|
616
|
+
if (blocked !== undefined) {
|
|
617
|
+
const orgDisclosures = org.status === "unavailable" && org.disclosures.length > 0
|
|
618
|
+
? ` Governance disclosures: ${inlineUntrusted(org.disclosures.join("; "), ORG_DISCLOSURE_MAX_CHARS)}.`
|
|
619
|
+
: "";
|
|
620
|
+
const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`, prepared.reminderMark);
|
|
621
|
+
await settleEnd(true, { content: orgDenial }, vetoed("org"));
|
|
622
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
|
|
623
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (prepared.denyNarrowingPolicy) {
|
|
628
|
+
const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({
|
|
629
|
+
toolName: pendingAction.toolName,
|
|
630
|
+
args: resolvedArgs,
|
|
631
|
+
toolCallId: pendingAction.toolCallId,
|
|
632
|
+
...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
|
|
633
|
+
budget: beltBudget,
|
|
634
|
+
...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
|
|
635
|
+
}, prepared.abortController.signal));
|
|
636
|
+
if (narrowed.action === "deny") {
|
|
637
|
+
const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`, prepared.reminderMark);
|
|
638
|
+
await settleEnd(true, { content: narrowedDenial }, vetoed("policy"));
|
|
639
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, narrowedDenial, true));
|
|
640
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (prepared.persistedRuleLane !== undefined) {
|
|
645
|
+
const unreadable = { unreadable: true };
|
|
646
|
+
const read = await settleOrgVerdictWithin(Promise.resolve()
|
|
647
|
+
.then(() => prepared.persistedRuleLane.adjudicate({
|
|
648
|
+
toolName: pendingAction.toolName,
|
|
649
|
+
args: resolvedArgs,
|
|
650
|
+
toolCallId: pendingAction.toolCallId,
|
|
651
|
+
...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
|
|
652
|
+
budget: beltBudget,
|
|
653
|
+
...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
|
|
654
|
+
}))
|
|
655
|
+
.then((answer) => normalizePersistedRuleHit(answer)), unreadable, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
656
|
+
const tightened = applyPersistedTightening({ action: "allow" }, read);
|
|
657
|
+
if (tightened.tightened === "deny" || tightened.tightened === "unreadable") {
|
|
658
|
+
const why = tightened.tightened === "deny"
|
|
659
|
+
? `a standing deny rule of yours (${disclosedRuleSet(read.hit?.rules ?? [])}) now refuses it`
|
|
660
|
+
: "your persisted permission rules could not be read on this leg";
|
|
661
|
+
const ruleDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${why}. This approval is spent — the call has to be re-issued and approved again.`, prepared.reminderMark);
|
|
662
|
+
await settleEnd(true, { content: ruleDenial }, vetoed("persisted_rule"));
|
|
663
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, ruleDenial, true));
|
|
664
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const tool = prepared.tools.find((t) => t.name === pendingAction.toolName);
|
|
669
|
+
if (!tool) {
|
|
670
|
+
const e = new Error(`the approved tool "${pendingAction.toolName}" is no longer available on resume`);
|
|
671
|
+
e.code = "resume.tool_unavailable";
|
|
672
|
+
throw e;
|
|
673
|
+
}
|
|
674
|
+
const args = resolvedArgs;
|
|
675
|
+
onExecuteStart?.(pendingAction.toolCallId);
|
|
676
|
+
prepared.suspendProgressRef.executedApproved = true;
|
|
677
|
+
let res;
|
|
678
|
+
try {
|
|
679
|
+
res = await tool.execute(pendingAction.toolCallId, args, prepared.abortController.signal);
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
const execError = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
683
|
+
const marks = (() => {
|
|
684
|
+
if (err === null || typeof err !== "object")
|
|
685
|
+
return undefined;
|
|
686
|
+
const src = err;
|
|
687
|
+
let d;
|
|
688
|
+
if (src.details !== null && typeof src.details === "object" && !Array.isArray(src.details)) {
|
|
689
|
+
d = { ...src.details };
|
|
690
|
+
}
|
|
691
|
+
if (typeof src.errorKind === "string")
|
|
692
|
+
d = { ...(d ?? {}), errorKind: src.errorKind };
|
|
693
|
+
return d;
|
|
694
|
+
})();
|
|
695
|
+
await settleEnd(true, { content: execError, ...(marks !== undefined ? { details: marks } : {}) });
|
|
696
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, execError, true));
|
|
697
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const executedIsError = res.isError === true;
|
|
701
|
+
await settleEnd(executedIsError, res);
|
|
702
|
+
onResolvedToolSuccess?.(pendingAction.toolName, executedIsError ? undefined : res.details);
|
|
703
|
+
const eid = await prepared.session.appendMessage({
|
|
704
|
+
role: "toolResult",
|
|
705
|
+
toolCallId: pendingAction.toolCallId,
|
|
706
|
+
toolName: pendingAction.toolName,
|
|
707
|
+
content: res.content,
|
|
708
|
+
details: res.details,
|
|
709
|
+
isError: executedIsError,
|
|
710
|
+
timestamp: Date.now(),
|
|
711
|
+
});
|
|
712
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
713
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { SessionStore } from "../session.js";
|
|
|
4
4
|
import { type PendingSessionNotifications, type SystemInjectionPriority, type TaskNotificationPayload } from "../task-notification.js";
|
|
5
5
|
import type { RunnerDeps, TaskEvent, TaskSpec } from "../types.js";
|
|
6
6
|
import type { SessionReadFileStates } from "./prepare-hands-readface.js";
|
|
7
|
-
import type { NotificationLaneBindings, Prepared,
|
|
7
|
+
import type { NotificationLaneBindings, Prepared, PrepareTaskFn, ResumeRun, RunInternals, RunnerDepsSeat, RunnerSelfSeat, TaskIdRef } from "./contracts.js";
|
|
8
8
|
export interface RunNotificationLaneInput<R> {
|
|
9
9
|
/** borrowed-readonly — the task spec: prepare's input, and the principal the peer scope is minted from. */
|
|
10
10
|
spec: TaskSpec;
|
|
@@ -28,8 +28,9 @@ export interface RunNotificationLaneInput<R> {
|
|
|
28
28
|
sessionReadStates: SessionReadFileStates;
|
|
29
29
|
/** borrowed-mutable — the Runner's per-session parked-notification store: the torn-down lane and the escalation pend here. */
|
|
30
30
|
pendingSessionNotifications: PendingSessionNotifications;
|
|
31
|
-
/** borrowed-readonly — the orchestrator's entry (a lane may not name the orchestrator; the driver hands it in)
|
|
32
|
-
|
|
31
|
+
/** borrowed-readonly — the orchestrator's entry (a lane may not name the orchestrator; the driver hands it in), as the ONE
|
|
32
|
+
* contract the orchestrator's own declaration is pinned to ({@link PrepareTaskFn}, contracts.ts — design/393 S7, #675). */
|
|
33
|
+
prepareTask: PrepareTaskFn;
|
|
33
34
|
/** borrowed-readonly — the lanes after this one, entered in this lane's last continuation with what it prepared and bound. */
|
|
34
35
|
next: (lane: RunNotificationLaneResult) => Promise<R>;
|
|
35
36
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Model } from "../../internal/llm.js";
|
|
2
2
|
import { type MaybeCompactOptions } from "../auto-compaction.js";
|
|
3
|
-
import type { HookInvocationIdentity } from "../hooks.js";
|
|
4
3
|
import type { PushQueue } from "../push-queue.js";
|
|
5
4
|
import type { Brain, TaskEvent, TaskSpec } from "../types.js";
|
|
6
|
-
import type { Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
|
|
5
|
+
import type { InstallingPhaseResult, Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
|
|
7
6
|
export interface RunRecoveryLanesInput {
|
|
8
7
|
/** borrowed-readonly — the task spec: the compaction settings / instructions and the key-and-headers seat. */
|
|
9
8
|
spec: TaskSpec;
|
|
@@ -31,24 +30,12 @@ export interface RunRecoveryLanesInput {
|
|
|
31
30
|
};
|
|
32
31
|
/** borrowed-readonly — the window-safety option builder (the compaction-machinery lane's), per pass. */
|
|
33
32
|
windowSafetyOptions: (mainModel: Model) => Pick<MaybeCompactOptions, "fallbackBudget" | "onWindowSafety">;
|
|
34
|
-
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError
|
|
33
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`; the compaction seams the pass reaches down
|
|
34
|
+
* for — compaction-seams.ts — read the summary provider and the hooks slot through it on every pass). */
|
|
35
35
|
runner: RunnerDepsSeat;
|
|
36
|
-
/** borrowed-readonly — the Runner's three compaction methods as delegates: the Seam C reuse options, the hook
|
|
37
|
-
* option wrapper and the reuse counter. The method bodies stay the Runner's; the pass calls them as the run body did. */
|
|
38
|
-
compactionSeats: {
|
|
39
|
-
seamCCompactionOptions: (prepared: Prepared) => Pick<MaybeCompactOptions, "summaryProvider" | "onCompaction" | "maxConsecutiveProviderReuse" | "consecutiveProviderReuse"> | undefined;
|
|
40
|
-
compactionHookOptions: (spec: TaskSpec, sessionId: string, trigger: "auto" | "manual" | "forced", identity?: HookInvocationIdentity, seatBound?: {
|
|
41
|
-
timeoutMs: number;
|
|
42
|
-
signal?: AbortSignal;
|
|
43
|
-
}) => Pick<MaybeCompactOptions, "trigger" | "preCompact" | "postCompact">;
|
|
44
|
-
recordCompactionReuse: (prepared: Prepared, comp: {
|
|
45
|
-
compacted: boolean;
|
|
46
|
-
reused?: boolean;
|
|
47
|
-
}) => void;
|
|
48
|
-
};
|
|
49
36
|
}
|
|
50
37
|
/** Nothing comes back: the lane's products are INSTALLED on the seats it borrowed — the harness's loop recovery
|
|
51
38
|
* chain and the microCompact arm-B seat — and the shared pass is read by those two installations alone. */
|
|
52
|
-
export interface RunRecoveryLanesResult {
|
|
39
|
+
export interface RunRecoveryLanesResult extends InstallingPhaseResult {
|
|
53
40
|
}
|
|
54
41
|
export declare function runRecoveryLanes(input: RunRecoveryLanesInput): RunRecoveryLanesResult;
|
|
@@ -7,8 +7,9 @@ import { rebaseCadenceWindows } from "./turn-attachments.js";
|
|
|
7
7
|
import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
|
|
8
8
|
import { gitRestateOption } from "./git-leg-delivery.js";
|
|
9
9
|
import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
|
|
10
|
+
import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions } from "./compaction-seams.js";
|
|
10
11
|
export function runRecoveryLanes(input) {
|
|
11
|
-
const { spec, prepared, queue, rs, ident, compactionBrain, compactionBreaker, windowSafetyOptions, runner
|
|
12
|
+
const { spec, prepared, queue, rs, ident, compactionBrain, compactionBreaker, windowSafetyOptions, runner } = input;
|
|
12
13
|
const runForcedCompactionPass = async (lane, turnSignal) => {
|
|
13
14
|
if (!(spec.compaction?.enabled ?? true))
|
|
14
15
|
return false;
|
|
@@ -37,14 +38,14 @@ export function runRecoveryLanes(input) {
|
|
|
37
38
|
workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
|
|
38
39
|
...contextInstructionFilesOption(prepared),
|
|
39
40
|
...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
|
|
40
|
-
...
|
|
41
|
+
...seamCCompactionOptions(runner, prepared),
|
|
41
42
|
...gitRestateOption(prepared),
|
|
42
43
|
...windowSafetyOptions(prepared.harness.getModel()),
|
|
43
|
-
...
|
|
44
|
+
...compactionHookOptions(runner, spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: passSignal }),
|
|
44
45
|
});
|
|
45
46
|
if (comp.compacted) {
|
|
46
47
|
compactionBreaker.failures = 0;
|
|
47
|
-
|
|
48
|
+
recordCompactionReuse(runner, prepared, comp);
|
|
48
49
|
if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
|
|
49
50
|
const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
|
|
50
51
|
rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* compaction, the forwarded-frame drain and the queue's `done` + close, the file-history boundary settle and the
|
|
10
10
|
* prompt-suggestion kickoff — then, in its finally, the four teardown legs in the order they were: the shell-lane
|
|
11
11
|
* children settle, the background-shell sweep, the registry clears / the background-agent abort / the retained
|
|
12
|
-
* child sessions, and the owned env's teardown.
|
|
12
|
+
* child sessions, and the owned env's teardown. The suggestion pass (`suggestNextPrompts`, with its four bounds) and the
|
|
13
|
+
* owned-env teardown (`teardownOwnedEnv`) are the lane's own module-private functions since design/393 S7, verbatim from
|
|
14
|
+
* the Runner's private methods: each has this one caller, and a sibling file would be a lane-to-lane reach (the S6 legs' reading).
|
|
13
15
|
*
|
|
14
16
|
* Async (eleven first-level awaits, all of them the same awaits at the same positions). The `queue.push("done")` and
|
|
15
17
|
* the teardown legs' order are contract (the forwarded-frame drain lands before `done`; the env is torn down after
|
|
@@ -21,9 +23,9 @@ import type { AssistantMessage } from "../../internal/llm.js";
|
|
|
21
23
|
import type { ResolvedReasoning } from "../../brain/reasoning.js";
|
|
22
24
|
import type { PushQueue } from "../push-queue.js";
|
|
23
25
|
import type { SessionStore } from "../session.js";
|
|
24
|
-
import type
|
|
26
|
+
import { type CompactOutcome, type DelegationLifecycleEvent, type TaskEvent, type TaskResult, type TaskSpec } from "../types.js";
|
|
25
27
|
import { type Stats } from "./assemble-result.js";
|
|
26
|
-
import type { EndOfTaskCompaction, ManualCompactRef, Prepared, ResumeRun, RunInternals, RunnerDepsSeat, RunState, SuspendReap, TaskIdRef } from "./contracts.js";
|
|
28
|
+
import type { EndOfTaskCompaction, InstallingPhaseResult, ManualCompactRef, Prepared, ResumeRun, RunInternals, RunnerDepsSeat, RunState, SuspendReap, TaskIdRef } from "./contracts.js";
|
|
27
29
|
export interface RunSettleAndTeardownInput {
|
|
28
30
|
/** borrowed-readonly — the task spec: the hooks slot, the suggestion opt-in, the retain / forward flags, the task id and principal. */
|
|
29
31
|
spec: TaskSpec;
|
|
@@ -76,6 +78,11 @@ export interface RunSettleAndTeardownInput {
|
|
|
76
78
|
threw: unknown;
|
|
77
79
|
/** borrowed-readonly — the live-abort flag captured before the leg's release-abort. */
|
|
78
80
|
abortedLive: boolean;
|
|
81
|
+
/** borrowed-readonly — the leg's snapshot of `loopLatch.userInterrupted`: names the origin of a stop that landed after the answer settled. */
|
|
82
|
+
userInterruptedLive: boolean;
|
|
83
|
+
/** borrowed-readonly — #677: the terminal-adoption lane's ONE evaluation of `answerSettled` (a clean `"stop"` final, no continuation
|
|
84
|
+
* lost). Read by the arbitration (`ResultFlags.answerSettled`), the teardown's stop-source attribution and the notice. */
|
|
85
|
+
answerSettled: boolean;
|
|
79
86
|
/** borrowed-readonly — design/174: the answered-but-never-collected deliveries the leg's finally settled. */
|
|
80
87
|
strandedHumanAnswers: ReadonlyArray<{
|
|
81
88
|
deliveryId: string;
|
|
@@ -93,17 +100,14 @@ export interface RunSettleAndTeardownInput {
|
|
|
93
100
|
onSuggestions: (p: Promise<string[]>) => void;
|
|
94
101
|
/** borrowed-readonly — the stream's reap-stash for a committed pause (what `destroy()` reaps when the run SUSPENDED). */
|
|
95
102
|
onSuspend: (reap: SuspendReap) => void;
|
|
96
|
-
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `hooks`, `checkpointStore`, `executionEnv`, `onNotice
|
|
97
|
-
*
|
|
103
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `hooks`, `checkpointStore`, `executionEnv`, `onNotice`; the
|
|
104
|
+
* suggestion pass reads `brain` / `pricing` / `roles` and the owned-env teardown `onError` through it too); the one home of why it
|
|
105
|
+
* is a seat and not a captured object is {@link RunnerDepsSeat}. */
|
|
98
106
|
runner: RunnerDepsSeat;
|
|
99
107
|
/** borrowed-readonly — the session store: the resumed-terminal leg unpins the checkpoint's session through it. */
|
|
100
108
|
sessions: SessionStore;
|
|
101
|
-
/** borrowed-readonly — the Runner's post-completion prompt-suggestion pass (its private method, as a delegate). */
|
|
102
|
-
suggestNextPrompts: (spec: TaskSpec, prepared: Prepared, result: TaskResult) => Promise<string[]>;
|
|
103
|
-
/** borrowed-readonly — the Runner's owned-env teardown (its private method, as a delegate; its gate mirrors the reap-stash's). */
|
|
104
|
-
teardownOwnedEnv: (prepared: Prepared) => Promise<void>;
|
|
105
109
|
}
|
|
106
110
|
/** Nothing comes back: the result is published through `setResult`, the queue is closed, the teardown legs ran. */
|
|
107
|
-
export interface RunSettleAndTeardownResult {
|
|
111
|
+
export interface RunSettleAndTeardownResult extends InstallingPhaseResult {
|
|
108
112
|
}
|
|
109
113
|
export declare function runSettleAndTeardown(input: RunSettleAndTeardownInput): Promise<RunSettleAndTeardownResult>;
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
+
import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
|
|
2
|
+
import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
|
|
1
3
|
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
2
4
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
3
5
|
import { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
4
6
|
import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
|
|
7
|
+
import { modelCostToPricing } from "../pricing.js";
|
|
5
8
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
9
|
+
import { resolveTaskModel } from "../roles.js";
|
|
6
10
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
7
11
|
import { emitTrace } from "../trace.js";
|
|
12
|
+
import { deliverEngineNotice } from "../types.js";
|
|
8
13
|
import { assembleResult } from "./assemble-result.js";
|
|
9
14
|
import { TIMER_LATENESS_REPORT_MS } from "./clock-and-limits.js";
|
|
10
15
|
import { resumeDecisionWasNegative } from "./decide-continuation.js";
|
|
11
16
|
import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
|
|
17
|
+
import { generatePromptSuggestions } from "./prompt-suggestions.js";
|
|
12
18
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
13
19
|
import { amendTerminal, terminalProjection } from "./terminal-projection.js";
|
|
14
20
|
export async function runSettleAndTeardown(input) {
|
|
15
|
-
const { spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, strandedHumanAnswers, reasoningResolution, comp, setResult, onSuggestions, onSuspend, runner, sessions
|
|
21
|
+
const { spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, setResult, onSuggestions, onSuspend, runner, sessions } = input;
|
|
16
22
|
try {
|
|
17
23
|
if (comp?.compacted) {
|
|
18
24
|
queue.push({
|
|
@@ -107,6 +113,7 @@ export async function runSettleAndTeardown(input) {
|
|
|
107
113
|
abortedForTimeout: timeout.fired,
|
|
108
114
|
abortedForTurns: rs.limits.turnsExceeded,
|
|
109
115
|
abortedLive,
|
|
116
|
+
answerSettled,
|
|
110
117
|
budgetHit: rs.limits.budgetHit,
|
|
111
118
|
budgetAxis: rs.limits.budgetAxis,
|
|
112
119
|
blockedReason: prepared.blockedRef.reason,
|
|
@@ -116,6 +123,19 @@ export async function runSettleAndTeardown(input) {
|
|
|
116
123
|
suspendLoop: prepared.suspendLoopRef.hit,
|
|
117
124
|
...(prepared.pausedRef.current !== undefined ? { paused: prepared.pausedRef.current.cause } : {}),
|
|
118
125
|
});
|
|
126
|
+
if (abortedLive && answerSettled && result.terminal.kind === "completed") {
|
|
127
|
+
deliverEngineNotice(runner.deps.onNotice, {
|
|
128
|
+
code: "task.interrupt_unconsumed",
|
|
129
|
+
message: "a stop arrived after the run had already given its answer: nothing was cut or stopped by it — the answer stands " +
|
|
130
|
+
"as the result, the run ends completed, and no interruption marker or limit code records the stop.",
|
|
131
|
+
detail: {
|
|
132
|
+
sessionId: prepared.sessionId,
|
|
133
|
+
runId: rs.telemetry.runId,
|
|
134
|
+
...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
|
|
135
|
+
origin: userInterruptedLive ? "interrupt" : timeout.fired ? "walltime" : rs.limits.turnsExceeded ? "turns" : "signal",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
119
139
|
if (resume !== undefined &&
|
|
120
140
|
resume.pendingActionStarted !== true &&
|
|
121
141
|
result.terminal.kind !== "completed" &&
|
|
@@ -269,8 +289,8 @@ export async function runSettleAndTeardown(input) {
|
|
|
269
289
|
queue.push({ type: "done", result });
|
|
270
290
|
queue.close();
|
|
271
291
|
await prepared.fileHistoryBoundary?.settle();
|
|
272
|
-
if (spec.suggestNextPrompts && result.terminal.kind === "completed") {
|
|
273
|
-
onSuggestions(suggestNextPrompts(spec, prepared, result));
|
|
292
|
+
if (spec.suggestNextPrompts && result.terminal.kind === "completed" && !abortedLive) {
|
|
293
|
+
onSuggestions(suggestNextPrompts(spec, prepared, result, runner));
|
|
274
294
|
}
|
|
275
295
|
}
|
|
276
296
|
finally {
|
|
@@ -280,6 +300,7 @@ export async function runSettleAndTeardown(input) {
|
|
|
280
300
|
prepared.pausedRef.current === undefined;
|
|
281
301
|
await settleTeardownLeg(() => defaultTaskRegistry.settleKilledForOwner({ owner: spec.taskId ?? prepared.sessionId, scope: spec.principal ?? "default", sessionId: prepared.sessionId }, {
|
|
282
302
|
source: abortedLive &&
|
|
303
|
+
!answerSettled &&
|
|
283
304
|
!timeout.fired &&
|
|
284
305
|
!rs.limits.turnsExceeded &&
|
|
285
306
|
rs.limits.budgetHit === undefined &&
|
|
@@ -318,7 +339,94 @@ export async function runSettleAndTeardown(input) {
|
|
|
318
339
|
if (prepared.subagentRetain) {
|
|
319
340
|
await settleTeardownLeg(() => prepared.subagentRetain.disposeAll(), "subagentRetain.disposeAll", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
320
341
|
}
|
|
321
|
-
await settleTeardownLeg(() => teardownOwnedEnv(prepared), "teardownOwnedEnv", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
342
|
+
await settleTeardownLeg(() => teardownOwnedEnv(prepared, runner), "teardownOwnedEnv", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
322
343
|
}
|
|
323
344
|
return {};
|
|
324
345
|
}
|
|
346
|
+
const SUGGESTIONS_DEFAULT_COUNT = 3;
|
|
347
|
+
const SUGGESTIONS_MAX_COUNT = 8;
|
|
348
|
+
const SUGGESTIONS_TIMEOUT_MS = 30_000;
|
|
349
|
+
const SUGGESTIONS_TRANSCRIPT_MESSAGES = 12;
|
|
350
|
+
async function suggestNextPrompts(spec, prepared, result, runner) {
|
|
351
|
+
const cfg = typeof spec.suggestNextPrompts === "object" ? spec.suggestNextPrompts : {};
|
|
352
|
+
const count = cfg.count && cfg.count > 0 ? Math.min(cfg.count, SUGGESTIONS_MAX_COUNT) : SUGGESTIONS_DEFAULT_COUNT;
|
|
353
|
+
const ac = new AbortController();
|
|
354
|
+
let timer;
|
|
355
|
+
const timeout = new Promise((resolve) => {
|
|
356
|
+
timer = setTimeout(() => {
|
|
357
|
+
ac.abort();
|
|
358
|
+
resolve([]);
|
|
359
|
+
}, SUGGESTIONS_TIMEOUT_MS);
|
|
360
|
+
});
|
|
361
|
+
const work = (async () => {
|
|
362
|
+
let model = prepared.model;
|
|
363
|
+
let thinking = prepared.thinking;
|
|
364
|
+
try {
|
|
365
|
+
const resolved = resolveTaskModel({ modelRole: cfg.role ?? "summarize", roles: spec.roles }, runner.deps);
|
|
366
|
+
model = resolved.model;
|
|
367
|
+
thinking = resolved.thinking;
|
|
368
|
+
}
|
|
369
|
+
catch (roleErr) {
|
|
370
|
+
if (cfg.role !== undefined) {
|
|
371
|
+
let asked;
|
|
372
|
+
try {
|
|
373
|
+
asked = String(cfg.role);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
asked = `<unrenderable ${typeof cfg.role}>`;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
runner.deps.onError?.(new Error(`suggestNextPrompts.role ${JSON.stringify(asked.length > 80 ? `${asked.slice(0, 80)}…` : asked)} did not resolve to a model — the prompt-suggestion pass ran on the task's own model (${prepared.model.id}) instead. Configure that role in RunnerDeps.roles / TaskSpec.roles, or omit the field to use the "summarize" role.`, { cause: roleErr }), { phase: "suggestions", sessionId: prepared.sessionId });
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!sameRouteIdentity(model, prepared.model)) {
|
|
386
|
+
const verdict = await adjudicateDerivedRoute({ brain: runner.deps.brain, model, getApiKeyAndHeaders: spec.getApiKeyAndHeaders });
|
|
387
|
+
if (verdict !== undefined && !verdict.ok) {
|
|
388
|
+
deliverEngineNotice(runner.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict, sessionId: prepared.sessionId, runId: prepared.runId }));
|
|
389
|
+
model = prepared.model;
|
|
390
|
+
thinking = prepared.thinking;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const pricing = runner.deps.pricing?.[model.id] ?? modelCostToPricing(model.cost);
|
|
394
|
+
const ctx = await prepared.session.buildContext();
|
|
395
|
+
const transcript = ctx.messages.filter((m) => !isSyntheticApiErrorMessage(m)).slice(-SUGGESTIONS_TRANSCRIPT_MESSAGES);
|
|
396
|
+
const out = await generatePromptSuggestions({ brain: runner.deps.brain, model, pricing, thinking, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, signal: ac.signal }, transcript, count);
|
|
397
|
+
if (out.tokens > 0 || out.costMicroUsd > 0) {
|
|
398
|
+
result.stats.suggestions = { tokens: out.tokens, costMicroUsd: out.costMicroUsd };
|
|
399
|
+
if (result.stats.costBreakdown) {
|
|
400
|
+
result.stats.costBreakdown.suggestionsMicroUsd = out.costMicroUsd;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return out.suggestions;
|
|
404
|
+
})();
|
|
405
|
+
work.catch(() => { });
|
|
406
|
+
try {
|
|
407
|
+
return await Promise.race([work, timeout]);
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
try {
|
|
411
|
+
runner.deps.onError?.(err, { phase: "suggestions", sessionId: prepared.sessionId });
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
}
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function teardownOwnedEnv(prepared, runner) {
|
|
422
|
+
if (prepared.ownedEnv &&
|
|
423
|
+
hasDestroy(prepared.ownedEnv) &&
|
|
424
|
+
prepared.pausedRef.current === undefined) {
|
|
425
|
+
try {
|
|
426
|
+
await prepared.ownedEnv.destroy();
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
runner.deps.onError?.(err, { phase: "config", sessionId: prepared.sessionId });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PushQueue } from "../push-queue.js";
|
|
2
2
|
import type { TaskEvent, TaskSpec } from "../types.js";
|
|
3
3
|
import type { Stats } from "./assemble-result.js";
|
|
4
|
-
import type { Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
|
|
4
|
+
import type { InstallingPhaseResult, Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
|
|
5
5
|
export interface RunStopAndFinalVerifyInput {
|
|
6
6
|
/** borrowed-readonly — the task spec: the hooks slot and the `finalVerification` opt-in. */
|
|
7
7
|
spec: TaskSpec;
|
|
@@ -27,6 +27,6 @@ export interface RunStopAndFinalVerifyInput {
|
|
|
27
27
|
runner: RunnerDepsSeat;
|
|
28
28
|
}
|
|
29
29
|
/** Nothing comes back: the gate is INSTALLED on the borrowed harness, and the seat's closures are read by it alone. */
|
|
30
|
-
export interface RunStopAndFinalVerifyResult {
|
|
30
|
+
export interface RunStopAndFinalVerifyResult extends InstallingPhaseResult {
|
|
31
31
|
}
|
|
32
32
|
export declare function runStopAndFinalVerify(input: RunStopAndFinalVerifyInput): RunStopAndFinalVerifyResult;
|