@sema-agent/core 5.55.0 → 5.57.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 +140 -0
- package/dist/agents/send-message-tool.d.ts +11 -0
- package/dist/agents/send-message-tool.js +81 -13
- package/dist/agents/subagent.js +250 -89
- package/dist/agents/team.d.ts +10 -1
- package/dist/agents/team.js +1 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.js +4 -1
- package/dist/brain/failover.js +16 -1
- package/dist/brain/open-responses.js +15 -5
- package/dist/brain/openai.js +16 -5
- package/dist/brain/request-params.d.ts +30 -27
- package/dist/brain/request-params.js +1 -7
- package/dist/brain/route-adjudicator.d.ts +190 -0
- package/dist/brain/route-adjudicator.js +189 -0
- package/dist/brain/route-conformance.d.ts +55 -0
- package/dist/brain/route-conformance.js +136 -0
- package/dist/brain/routing.js +8 -3
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +158 -11
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/mcp.js +4 -4
- package/dist/core/memory-engine/engine.d.ts +15 -5
- package/dist/core/memory-engine/engine.js +3 -1
- package/dist/core/permission-rule-consent.d.ts +45 -0
- package/dist/core/permission-rule-consent.js +40 -11
- package/dist/core/permission-rule-model.d.ts +110 -75
- package/dist/core/permission-rule-model.js +61 -28
- package/dist/core/permission-rules.d.ts +23 -15
- package/dist/core/permission-rules.js +40 -31
- package/dist/core/runner/prepare-task.d.ts +8 -0
- package/dist/core/runner/prepare-task.js +66 -26
- package/dist/core/runner/runtask.d.ts +4 -1
- package/dist/core/runner/runtask.js +206 -21
- package/dist/core/runner/session-rule-policy.js +5 -5
- package/dist/core/scheduler.d.ts +5 -0
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/side-query.d.ts +12 -5
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +14 -9
- package/dist/core/tool-policy.js +27 -22
- package/dist/core/types.d.ts +69 -11
- package/dist/core/untrusted-text.js +8 -0
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +114 -13
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +16 -6
- package/dist/engine/llm/types.d.ts +65 -0
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +12 -6
- package/dist/prompts/default.js +2 -0
- package/dist/scenarios/scenario-registry.d.ts +5 -1
- package/dist/scenarios/scenario-registry.js +4 -2
- package/dist/tools/fs/index.js +8 -1
- package/dist/tools/scheduler-tools.js +28 -6
- package/dist/tools/web.d.ts +15 -0
- package/dist/tools/web.js +8 -2
- package/dist/tools/worktree.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -1
|
@@ -20,6 +20,7 @@ import { emitTaskOutcome } from "../task-outcome.js";
|
|
|
20
20
|
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
21
21
|
import { primaryActivityArg } from "../arg-summary.js";
|
|
22
22
|
import { resolveReasoning } from "../../brain/reasoning.js";
|
|
23
|
+
import { adjudicateDerivedRoute, authCarrierFingerprint, fallbackToPrimaryNotice, normalizeBaseUrl, sameRouteIdentity } from "../../brain/route-adjudicator.js";
|
|
23
24
|
import { readDegradation } from "../../brain/degrading.js";
|
|
24
25
|
import { runWithBrainTelemetry, runWithReasoningWireFacts, runWithStatusSink } from "../../brain/status-sink.js";
|
|
25
26
|
import { expandTiers, resolveModel, resolveTaskModel } from "../roles.js";
|
|
@@ -39,15 +40,15 @@ import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
|
39
40
|
import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
40
41
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
41
42
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
42
|
-
import { cloneObserverInput, formatHookFeedback } from "../hooks.js";
|
|
43
|
-
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
43
|
+
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
44
|
+
import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
|
|
44
45
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
45
|
-
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
46
|
+
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
46
47
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
47
48
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
48
49
|
import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
|
|
49
50
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
50
|
-
import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
51
|
+
import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
51
52
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
52
53
|
import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
|
|
53
54
|
import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
|
|
@@ -767,7 +768,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
767
768
|
const injectedThisTurn = finalVerifyInjectedThisTurn ? "final_verification" : undefined;
|
|
768
769
|
if (!boundarySteered && !prepared.abortController.signal.aborted) {
|
|
769
770
|
try {
|
|
770
|
-
const
|
|
771
|
+
const batchSeat = await runHookSeat("postToolBatch", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }, (sig) => postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity, signal: sig }));
|
|
772
|
+
if (batchSeat.expired) {
|
|
773
|
+
runnerHooks.onError?.(hookSeatExpiredError("postToolBatch", prepared.hookTimeoutMs, batchSeat.cause, "the boundary observation was abandoned and its additionalContext dropped; the turn boundary itself is unchanged"), { phase: "hook", sessionId: prepared.sessionId });
|
|
774
|
+
}
|
|
775
|
+
const r = batchSeat.expired ? undefined : batchSeat.value;
|
|
771
776
|
if (r?.additionalContext && injectedThisTurn === undefined) {
|
|
772
777
|
const body = sanitizeUntrustedText(r.additionalContext, SHELLED_BODY_ENVELOPE_TAGS);
|
|
773
778
|
const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
|
|
@@ -1504,8 +1509,25 @@ export class Runner {
|
|
|
1504
1509
|
...(typeof h.postCompact === "function" ? { postCompact: (c) => h.postCompact(c) } : {}),
|
|
1505
1510
|
...(typeof h.stopFailure === "function" ? { stopFailure: (c) => h.stopFailure(c) } : {}),
|
|
1506
1511
|
...(typeof h.permissionDenied === "function" ? { permissionDenied: (p) => h.permissionDenied(p) } : {}),
|
|
1512
|
+
...(h.timeoutMs !== undefined ? { timeoutMs: h.timeoutMs } : {}),
|
|
1507
1513
|
}
|
|
1508
1514
|
: undefined;
|
|
1515
|
+
const MIRRORED_HOOK_KEYS = [
|
|
1516
|
+
"preToolUse",
|
|
1517
|
+
"preToolUseObservational",
|
|
1518
|
+
"postToolUse",
|
|
1519
|
+
"userPromptSubmit",
|
|
1520
|
+
"stop",
|
|
1521
|
+
"postToolUseFailure",
|
|
1522
|
+
"postToolBatch",
|
|
1523
|
+
"preCompact",
|
|
1524
|
+
"postCompact",
|
|
1525
|
+
"stopFailure",
|
|
1526
|
+
"permissionDenied",
|
|
1527
|
+
"timeoutMs",
|
|
1528
|
+
];
|
|
1529
|
+
const _mirrorIsComplete = true;
|
|
1530
|
+
void _mirrorIsComplete;
|
|
1509
1531
|
return {
|
|
1510
1532
|
...(deps.toolPolicy !== undefined
|
|
1511
1533
|
? {
|
|
@@ -1547,6 +1569,17 @@ export class Runner {
|
|
|
1547
1569
|
}
|
|
1548
1570
|
const tiers = Object.hasOwn(next, "tiers") ? next.tiers : this.deps.tiers;
|
|
1549
1571
|
const expanded = tiers && Object.keys(tiers).length > 0 ? expandTiers({ ...next.models }, tiers) : { ...next.models };
|
|
1572
|
+
const movedEntries = [];
|
|
1573
|
+
for (const [name, nextModel] of Object.entries(expanded ?? {})) {
|
|
1574
|
+
const prior = this.deps.models?.[name];
|
|
1575
|
+
if (!prior)
|
|
1576
|
+
continue;
|
|
1577
|
+
const from = normalizeBaseUrl(prior.baseUrl);
|
|
1578
|
+
const to = normalizeBaseUrl(nextModel.baseUrl);
|
|
1579
|
+
if (from !== to && authCarrierFingerprint(prior.headers) === authCarrierFingerprint(nextModel.headers)) {
|
|
1580
|
+
movedEntries.push({ modelId: name, from, to });
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1550
1583
|
this.deps = { ...this.deps, models: expanded, ...(tiers !== undefined ? { tiers } : {}) };
|
|
1551
1584
|
if (tiers === undefined)
|
|
1552
1585
|
delete this.deps.tiers;
|
|
@@ -1555,6 +1588,18 @@ export class Runner {
|
|
|
1555
1588
|
message: `model catalog swapped: ${Object.keys(next.models).length} model(s), ${tiers ? Object.keys(tiers).length : 0} tier binding(s); in-flight tasks finish on their resolved models, new tasks resolve against the new catalog`,
|
|
1556
1589
|
detail: { models: Object.keys(next.models).length, tiers: tiers ? Object.keys(tiers).length : 0 },
|
|
1557
1590
|
});
|
|
1591
|
+
if (movedEntries.length > 0) {
|
|
1592
|
+
const RENDER_CAP = 8;
|
|
1593
|
+
const rendered = movedEntries.slice(0, RENDER_CAP);
|
|
1594
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
1595
|
+
code: "route.base_url_changed_key_unchanged",
|
|
1596
|
+
message: `model catalog swap moved ${movedEntries.length} entry/entries to a new baseUrl while the Model-visible credential half stayed unchanged: ` +
|
|
1597
|
+
rendered.map((e) => `"${e.modelId}" ${e.from || "(config root)"} → ${e.to || "(config root)"}`).join("; ") +
|
|
1598
|
+
(movedEntries.length > rendered.length ? `; +${movedEntries.length - rendered.length} more` : "") +
|
|
1599
|
+
" — if the provider changed (not just its domain), update the credential reference in the same step",
|
|
1600
|
+
detail: { entries: rendered, total: movedEntries.length },
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1558
1603
|
}
|
|
1559
1604
|
sideQuery(spec) {
|
|
1560
1605
|
return runWithReasoningWireFacts(() => { }, () => runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles }));
|
|
@@ -1783,6 +1828,13 @@ export class Runner {
|
|
|
1783
1828
|
const actorIn = options?.actor;
|
|
1784
1829
|
const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
|
|
1785
1830
|
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
1831
|
+
const effectiveInputId = typeof inputId === "string" ? inputId : uuidv7();
|
|
1832
|
+
const parkRecord = {
|
|
1833
|
+
text,
|
|
1834
|
+
trusted,
|
|
1835
|
+
inputId: effectiveInputId,
|
|
1836
|
+
...(actor !== undefined ? { actor } : {}),
|
|
1837
|
+
};
|
|
1786
1838
|
let payload;
|
|
1787
1839
|
let mintsAFrame;
|
|
1788
1840
|
let replay;
|
|
@@ -1797,7 +1849,7 @@ export class Runner {
|
|
|
1797
1849
|
source: "steer",
|
|
1798
1850
|
delivery: "queued",
|
|
1799
1851
|
sessionSeq: nextHumanInputSeq(h.harness),
|
|
1800
|
-
|
|
1852
|
+
inputId: effectiveInputId,
|
|
1801
1853
|
...(actor !== undefined ? { actor } : {}),
|
|
1802
1854
|
...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
|
|
1803
1855
|
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
@@ -1814,7 +1866,7 @@ export class Runner {
|
|
|
1814
1866
|
const h = handle ?? (await orTimeout(ready));
|
|
1815
1867
|
if (!h)
|
|
1816
1868
|
throw steeringError("the task is not running");
|
|
1817
|
-
payload = trusted ? formatHookFeedback(projected, h.reminderMark) : projected;
|
|
1869
|
+
payload = trusted ? formatHookFeedback(projected, h.reminderMark) : frameMidTurnUserInput(projected);
|
|
1818
1870
|
mintsAFrame = payload.trim().length !== 0;
|
|
1819
1871
|
replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
|
|
1820
1872
|
if (typeof inputId === "string") {
|
|
@@ -1830,7 +1882,7 @@ export class Runner {
|
|
|
1830
1882
|
}
|
|
1831
1883
|
}
|
|
1832
1884
|
try {
|
|
1833
|
-
await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, ...(actor !== undefined ? { actor } : {}) });
|
|
1885
|
+
await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) });
|
|
1834
1886
|
noteAccepted(h);
|
|
1835
1887
|
return;
|
|
1836
1888
|
}
|
|
@@ -1841,7 +1893,7 @@ export class Runner {
|
|
|
1841
1893
|
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
1842
1894
|
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1843
1895
|
try {
|
|
1844
|
-
await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, ...(actor !== undefined ? { actor } : {}) });
|
|
1896
|
+
await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) });
|
|
1845
1897
|
noteAccepted(h);
|
|
1846
1898
|
return;
|
|
1847
1899
|
}
|
|
@@ -1882,6 +1934,9 @@ export class Runner {
|
|
|
1882
1934
|
if (input.source !== undefined && typeof input.source !== "string") {
|
|
1883
1935
|
throw notifyError("input.source must be a string when present", "notify.invalid_payload");
|
|
1884
1936
|
}
|
|
1937
|
+
if (opts?.priority !== undefined && !isSystemInjectionPriority(opts.priority)) {
|
|
1938
|
+
throw notifyError(`opts.priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when present`, "notify.invalid_payload");
|
|
1939
|
+
}
|
|
1885
1940
|
const payload = {
|
|
1886
1941
|
task_id: input.task_id,
|
|
1887
1942
|
task_type: "external",
|
|
@@ -1945,6 +2000,8 @@ export class Runner {
|
|
|
1945
2000
|
const h = handle ?? (await orTimeout(ready));
|
|
1946
2001
|
if (!h)
|
|
1947
2002
|
return;
|
|
2003
|
+
if (!h.abortController.signal.aborted)
|
|
2004
|
+
h.loop.userInterrupted = true;
|
|
1948
2005
|
h.abortController.abort();
|
|
1949
2006
|
void h.harness.abort();
|
|
1950
2007
|
},
|
|
@@ -2029,7 +2086,18 @@ export class Runner {
|
|
|
2029
2086
|
});
|
|
2030
2087
|
const upstreamTaskNotification = internals?.onTaskNotification;
|
|
2031
2088
|
const deliveredAtTurnOpen = new Set();
|
|
2089
|
+
let priorityNowDisclosed = false;
|
|
2032
2090
|
const injectTaskNotification = (notification, opts) => {
|
|
2091
|
+
if (opts?.priority === "now" && !priorityNowDisclosed) {
|
|
2092
|
+
priorityNowDisclosed = true;
|
|
2093
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
2094
|
+
code: "task.injection_priority_unimplemented",
|
|
2095
|
+
message: `a notification was injected with priority "now", which this engine does not implement: all injection ` +
|
|
2096
|
+
`priorities deliver at the NEXT turn boundary and none aborts the running turn. The notification is ` +
|
|
2097
|
+
`delivered — only the interrupting semantics are absent.`,
|
|
2098
|
+
detail: { priority: "now", ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}) },
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2033
2101
|
if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
|
|
2034
2102
|
return Promise.resolve("dropped_duplicate");
|
|
2035
2103
|
if (!notificationLaneLive) {
|
|
@@ -2098,7 +2166,12 @@ export class Runner {
|
|
|
2098
2166
|
this.pendingSessionNotifications.pend(notificationSessionId, p);
|
|
2099
2167
|
};
|
|
2100
2168
|
prepared.harness.engineInjectionsHeld = () => prepared.batchHaltRef.current !== undefined;
|
|
2169
|
+
let undrainedUserAtEnd;
|
|
2101
2170
|
prepared.harness.onUndrainedUserInputs = (counts) => {
|
|
2171
|
+
if (prepared.suspendRef.token !== undefined || prepared.reviewRef.token !== undefined) {
|
|
2172
|
+
undrainedUserAtEnd = counts;
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2102
2175
|
for (const notice of undrainedUserInputNotices(counts, spec.taskId)) {
|
|
2103
2176
|
deliverEngineNotice(this.deps.onNotice, notice);
|
|
2104
2177
|
}
|
|
@@ -2127,7 +2200,7 @@ export class Runner {
|
|
|
2127
2200
|
}
|
|
2128
2201
|
}
|
|
2129
2202
|
}
|
|
2130
|
-
const loopLatch = { ended: false };
|
|
2203
|
+
const loopLatch = { ended: false, userInterrupted: false };
|
|
2131
2204
|
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark });
|
|
2132
2205
|
const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
|
|
2133
2206
|
prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
|
|
@@ -2809,12 +2882,21 @@ export class Runner {
|
|
|
2809
2882
|
return [];
|
|
2810
2883
|
let result;
|
|
2811
2884
|
try {
|
|
2812
|
-
|
|
2885
|
+
const stopSeat = await runHookSeat("stop", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => stopHook({
|
|
2813
2886
|
stopHookActive: consecutiveBlocks > 0,
|
|
2814
2887
|
consecutiveBlocks,
|
|
2815
2888
|
getBranch: () => prepared.session.getBranch(),
|
|
2816
2889
|
identity: prepared.hookIdentity,
|
|
2817
|
-
|
|
2890
|
+
signal: sig,
|
|
2891
|
+
}));
|
|
2892
|
+
if (stopSeat.expired) {
|
|
2893
|
+
if (stopSeat.cause === "timeout") {
|
|
2894
|
+
this.deps.onError?.(hookSeatExpiredError("stop", prepared.hookTimeoutMs, stopSeat.cause, "the run was allowed to END (the seat's own no-opinion answer); no pushback and no additional context were injected"), { phase: "hook", sessionId: prepared.sessionId });
|
|
2895
|
+
}
|
|
2896
|
+
consecutiveBlocks = 0;
|
|
2897
|
+
return [];
|
|
2898
|
+
}
|
|
2899
|
+
result = stopSeat.value;
|
|
2818
2900
|
}
|
|
2819
2901
|
catch (err) {
|
|
2820
2902
|
this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId: prepared.sessionId });
|
|
@@ -2889,7 +2971,7 @@ export class Runner {
|
|
|
2889
2971
|
...this.seamCCompactionOptions(prepared),
|
|
2890
2972
|
...gitRestateOption(prepared),
|
|
2891
2973
|
...windowSafetyOptions(prepared.harness.getModel()),
|
|
2892
|
-
...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity),
|
|
2974
|
+
...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
|
|
2893
2975
|
});
|
|
2894
2976
|
if (comp.compacted) {
|
|
2895
2977
|
compactionBreaker.failures = 0;
|
|
@@ -2954,7 +3036,7 @@ export class Runner {
|
|
|
2954
3036
|
runnerHooks: {
|
|
2955
3037
|
onError: this.deps.onError,
|
|
2956
3038
|
seamCCompactionOptions: (p) => this.seamCCompactionOptions(p),
|
|
2957
|
-
compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig, prepared.hookIdentity),
|
|
3039
|
+
compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig, prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
|
|
2958
3040
|
recordCompactionReuse: (p, c) => this.recordCompactionReuse(p, c),
|
|
2959
3041
|
},
|
|
2960
3042
|
});
|
|
@@ -3057,6 +3139,7 @@ export class Runner {
|
|
|
3057
3139
|
let final;
|
|
3058
3140
|
let threw;
|
|
3059
3141
|
let abortedLive = false;
|
|
3142
|
+
let userInterruptedLive = false;
|
|
3060
3143
|
let strandedHumanAnswers = [];
|
|
3061
3144
|
try {
|
|
3062
3145
|
if (prepared.abortController.signal.aborted) {
|
|
@@ -3184,7 +3267,23 @@ export class Runner {
|
|
|
3184
3267
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
3185
3268
|
if (userPromptSubmit) {
|
|
3186
3269
|
try {
|
|
3187
|
-
const
|
|
3270
|
+
const promptSeat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => userPromptSubmit(spec.objective, { identity: prepared.hookIdentity, signal: sig }));
|
|
3271
|
+
if (promptSeat.expired) {
|
|
3272
|
+
if (promptSeat.cause === "timeout") {
|
|
3273
|
+
try {
|
|
3274
|
+
this.deps.onError?.(hookSeatExpiredError("userPromptSubmit", prepared.hookTimeoutMs, promptSeat.cause, "the prompt was NOT submitted (fail-closed) and the task ends blocked"), { phase: "hook", sessionId: prepared.sessionId });
|
|
3275
|
+
}
|
|
3276
|
+
catch {
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
prepared.blockedRef.reason = formatHookFeedback(promptSeat.cause === "timeout"
|
|
3280
|
+
? `the deployment's userPromptSubmit hook did not answer within its ${prepared.hookTimeoutMs}ms bound while screening this prompt; ` +
|
|
3281
|
+
`the prompt was NOT submitted (fail-closed)`
|
|
3282
|
+
: `the task was cancelled while the deployment's userPromptSubmit hook was still screening this prompt; ` +
|
|
3283
|
+
`the prompt was NOT submitted (fail-closed)`, prepared.reminderMark);
|
|
3284
|
+
promptBlocked = true;
|
|
3285
|
+
}
|
|
3286
|
+
const decision = promptSeat.expired ? undefined : promptSeat.value;
|
|
3188
3287
|
if (decision?.block) {
|
|
3189
3288
|
prepared.blockedRef.reason = formatHookFeedback(decision.block, prepared.reminderMark);
|
|
3190
3289
|
promptBlocked = true;
|
|
@@ -3353,6 +3452,7 @@ export class Runner {
|
|
|
3353
3452
|
}
|
|
3354
3453
|
loopLatch.ended = true;
|
|
3355
3454
|
abortedLive = prepared.abortController.signal.aborted;
|
|
3455
|
+
userInterruptedLive = loopLatch.userInterrupted;
|
|
3356
3456
|
}
|
|
3357
3457
|
catch (err) {
|
|
3358
3458
|
loopLatch.ended = true;
|
|
@@ -3364,6 +3464,7 @@ export class Runner {
|
|
|
3364
3464
|
}
|
|
3365
3465
|
threw = err;
|
|
3366
3466
|
abortedLive = prepared.abortController.signal.aborted;
|
|
3467
|
+
userInterruptedLive = loopLatch.userInterrupted;
|
|
3367
3468
|
}
|
|
3368
3469
|
finally {
|
|
3369
3470
|
timeout.clear();
|
|
@@ -3401,13 +3502,17 @@ export class Runner {
|
|
|
3401
3502
|
const durablyPaused = suspended || reviewPaused;
|
|
3402
3503
|
const interrupted = !durablyPaused &&
|
|
3403
3504
|
(threw !== undefined || timeout.fired || rs.limits.turnsExceeded || rs.limits.budgetHit !== undefined || abortedLive || final?.stopReason === "aborted");
|
|
3505
|
+
let orphansClosed = 0;
|
|
3506
|
+
let reconcileComplete = false;
|
|
3404
3507
|
if (interrupted) {
|
|
3405
3508
|
try {
|
|
3406
3509
|
const report = await reconcileInterruptedSession(prepared.session, prepared.toolEffects, undefined, startedToolCallIds);
|
|
3510
|
+
orphansClosed = report.recovered.length;
|
|
3407
3511
|
for (const orphan of report.recovered) {
|
|
3408
3512
|
queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
|
|
3409
3513
|
emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
|
|
3410
3514
|
}
|
|
3515
|
+
reconcileComplete = true;
|
|
3411
3516
|
}
|
|
3412
3517
|
catch (reconcileErr) {
|
|
3413
3518
|
this.deps.onError?.(reconcileErr instanceof Error ? reconcileErr : new Error(String(reconcileErr)), {
|
|
@@ -3425,6 +3530,52 @@ export class Runner {
|
|
|
3425
3530
|
});
|
|
3426
3531
|
}
|
|
3427
3532
|
}
|
|
3533
|
+
if (interrupted && userInterruptedLive && reconcileComplete) {
|
|
3534
|
+
try {
|
|
3535
|
+
const entryId = await appendInterruptionMarker(prepared.session, { toolUseInFlight: orphansClosed > 0 });
|
|
3536
|
+
emitCommitted(entryId, "user");
|
|
3537
|
+
}
|
|
3538
|
+
catch (markerErr) {
|
|
3539
|
+
this.deps.onError?.(markerErr instanceof Error ? markerErr : new Error(String(markerErr)), {
|
|
3540
|
+
phase: "interrupt-reconcile",
|
|
3541
|
+
sessionId: prepared.sessionId,
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
let migratedParked = { steer: 0, followUp: 0 };
|
|
3546
|
+
const parkToken = prepared.suspendRef.token ?? prepared.reviewRef.token;
|
|
3547
|
+
const parkScope = prepared.suspendRef.token !== undefined ? prepared.suspendRef.scope : prepared.reviewRef.scope;
|
|
3548
|
+
if (durablyPaused && parkToken !== undefined && parkScope !== undefined && this.deps.checkpointStore !== undefined) {
|
|
3549
|
+
const store = this.deps.checkpointStore;
|
|
3550
|
+
const carried = [];
|
|
3551
|
+
for (const record of prepared.harness.readParkableUserInputs()) {
|
|
3552
|
+
try {
|
|
3553
|
+
if (!(await store.setPendingSteer(parkToken, parkScope, record)))
|
|
3554
|
+
break;
|
|
3555
|
+
carried.push(record);
|
|
3556
|
+
}
|
|
3557
|
+
catch (parkErr) {
|
|
3558
|
+
this.deps.onError?.(parkErr instanceof Error ? parkErr : new Error(String(parkErr)), {
|
|
3559
|
+
phase: "config",
|
|
3560
|
+
sessionId: prepared.sessionId,
|
|
3561
|
+
});
|
|
3562
|
+
const parkCode = errorCodeOf(parkErr);
|
|
3563
|
+
if (parkCode === "steering.invalid_content" || parkCode === "steering.queue_full")
|
|
3564
|
+
continue;
|
|
3565
|
+
break;
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
migratedParked = prepared.harness.dropParkedUserInputs(carried);
|
|
3569
|
+
}
|
|
3570
|
+
if (undrainedUserAtEnd !== undefined) {
|
|
3571
|
+
const remaining = {
|
|
3572
|
+
steer: Math.max(0, undrainedUserAtEnd.steer - migratedParked.steer),
|
|
3573
|
+
followUp: Math.max(0, undrainedUserAtEnd.followUp - migratedParked.followUp),
|
|
3574
|
+
};
|
|
3575
|
+
for (const notice of undrainedUserInputNotices(remaining, spec.taskId)) {
|
|
3576
|
+
deliverEngineNotice(this.deps.onNotice, notice);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3428
3579
|
if (rs.attach.attachState?.postCompactPending === true && (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true) {
|
|
3429
3580
|
emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.announce_dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
|
|
3430
3581
|
}
|
|
@@ -3434,6 +3585,7 @@ export class Runner {
|
|
|
3434
3585
|
brain: compactionBrain,
|
|
3435
3586
|
windowSafety: windowSafetyOptions(prepared.model),
|
|
3436
3587
|
onCompactionFailed: (reason) => queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "auto", reason, ...ident() }),
|
|
3588
|
+
cancelled: abortedLive,
|
|
3437
3589
|
});
|
|
3438
3590
|
try {
|
|
3439
3591
|
if (comp?.compacted) {
|
|
@@ -3596,12 +3748,20 @@ export class Runner {
|
|
|
3596
3748
|
!abortedLive &&
|
|
3597
3749
|
result.errorCode !== "conflict") {
|
|
3598
3750
|
try {
|
|
3599
|
-
await stopFailureHook({
|
|
3751
|
+
const failureSeat = await runHookSeat("stopFailure", { timeoutMs: prepared.hookTimeoutMs }, (sig) => stopFailureHook({
|
|
3600
3752
|
identity: prepared.hookIdentity,
|
|
3601
3753
|
error: result.errorMessage ?? "model error",
|
|
3602
3754
|
...(result.errorCode !== undefined ? { errorKind: result.errorCode } : {}),
|
|
3603
3755
|
turns: stats.turns,
|
|
3604
|
-
|
|
3756
|
+
signal: sig,
|
|
3757
|
+
}));
|
|
3758
|
+
if (failureSeat.expired) {
|
|
3759
|
+
try {
|
|
3760
|
+
this.deps.onError?.(hookSeatExpiredError("stopFailure", prepared.hookTimeoutMs, failureSeat.cause, "the terminal observation was abandoned; the assembled TaskResult is unchanged"), { phase: "hook", sessionId: prepared.sessionId });
|
|
3761
|
+
}
|
|
3762
|
+
catch {
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3605
3765
|
}
|
|
3606
3766
|
catch (err) {
|
|
3607
3767
|
try {
|
|
@@ -3770,6 +3930,14 @@ export class Runner {
|
|
|
3770
3930
|
}
|
|
3771
3931
|
catch {
|
|
3772
3932
|
}
|
|
3933
|
+
if (!sameRouteIdentity(model, prepared.model)) {
|
|
3934
|
+
const verdict = await adjudicateDerivedRoute({ brain: this.deps.brain, model, getApiKeyAndHeaders: spec.getApiKeyAndHeaders });
|
|
3935
|
+
if (verdict !== undefined && !verdict.ok) {
|
|
3936
|
+
deliverEngineNotice(this.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict }));
|
|
3937
|
+
model = prepared.model;
|
|
3938
|
+
thinking = prepared.thinking;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3773
3941
|
const pricing = this.deps.pricing?.[model.id] ?? modelCostToPricing(model.cost);
|
|
3774
3942
|
const ctx = await prepared.session.buildContext();
|
|
3775
3943
|
const transcript = ctx.messages.slice(-SUGGESTIONS_TRANSCRIPT_MESSAGES);
|
|
@@ -4612,10 +4780,12 @@ export class Runner {
|
|
|
4612
4780
|
consecutiveProviderReuse: prepared.compactionReuseRef.consecutive,
|
|
4613
4781
|
};
|
|
4614
4782
|
}
|
|
4615
|
-
compactionHookOptions(spec, sessionId, trigger, identity) {
|
|
4783
|
+
compactionHookOptions(spec, sessionId, trigger, identity, seatBound) {
|
|
4616
4784
|
const hooks = spec.hooks ?? this.deps.hooks;
|
|
4617
4785
|
const pre = hooks?.preCompact;
|
|
4618
4786
|
const post = hooks?.postCompact;
|
|
4787
|
+
const seatMs = seatBound?.timeoutMs ?? resolveHookTimeoutMs(hooks?.timeoutMs);
|
|
4788
|
+
const seatSignal = seatBound?.signal;
|
|
4619
4789
|
const withIdentity = (ctx) => identity !== undefined ? { ...ctx, identity } : ctx;
|
|
4620
4790
|
return {
|
|
4621
4791
|
trigger,
|
|
@@ -4631,7 +4801,12 @@ export class Runner {
|
|
|
4631
4801
|
}
|
|
4632
4802
|
};
|
|
4633
4803
|
try {
|
|
4634
|
-
const
|
|
4804
|
+
const seat = await runHookSeat("preCompact", { timeoutMs: seatMs, ...(seatSignal !== undefined ? { signal: seatSignal } : {}) }, (sig) => pre.call(hooks, { ...ctx, signal: sig }));
|
|
4805
|
+
if (seat.expired) {
|
|
4806
|
+
report(hookSeatExpiredError("preCompact", seatMs, seat.cause, `the "${ctx.trigger}" compaction PROCEEDED unblocked and with no additional instructions`));
|
|
4807
|
+
return undefined;
|
|
4808
|
+
}
|
|
4809
|
+
const r = seat.value;
|
|
4635
4810
|
if (r?.block && ctx.trigger === "forced") {
|
|
4636
4811
|
report(new Error(`a preCompact callback blocked a "forced" compaction — ignored (PTL/trim-pressure compaction is not optional): ${r.block}`));
|
|
4637
4812
|
}
|
|
@@ -4648,7 +4823,14 @@ export class Runner {
|
|
|
4648
4823
|
? {
|
|
4649
4824
|
postCompact: async (rawCtx) => {
|
|
4650
4825
|
try {
|
|
4651
|
-
await post.call(hooks, withIdentity(rawCtx));
|
|
4826
|
+
const seat = await runHookSeat("postCompact", { timeoutMs: seatMs, ...(seatSignal !== undefined ? { signal: seatSignal } : {}) }, (sig) => post.call(hooks, { ...withIdentity(rawCtx), signal: sig }));
|
|
4827
|
+
if (seat.expired) {
|
|
4828
|
+
try {
|
|
4829
|
+
this.deps.onError?.(hookSeatExpiredError("postCompact", seatMs, seat.cause, "the observation was abandoned; the compaction that already landed is unchanged"), { phase: "hook", sessionId });
|
|
4830
|
+
}
|
|
4831
|
+
catch {
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4652
4834
|
}
|
|
4653
4835
|
catch (err) {
|
|
4654
4836
|
try {
|
|
@@ -4692,7 +4874,10 @@ export class Runner {
|
|
|
4692
4874
|
...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
|
|
4693
4875
|
...this.seamCCompactionOptions(prepared),
|
|
4694
4876
|
...gitRestateOption(prepared),
|
|
4695
|
-
...this.compactionHookOptions(spec, prepared.sessionId, "auto", prepared.hookIdentity
|
|
4877
|
+
...this.compactionHookOptions(spec, prepared.sessionId, "auto", prepared.hookIdentity, {
|
|
4878
|
+
timeoutMs: prepared.hookTimeoutMs,
|
|
4879
|
+
...(opts?.cancelled === true ? { signal: prepared.abortController.signal } : {}),
|
|
4880
|
+
}),
|
|
4696
4881
|
});
|
|
4697
4882
|
this.recordCompactionReuse(prepared, finishComp);
|
|
4698
4883
|
if (finishComp.unevaluableWindow) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
|
|
2
2
|
import { isWinFormPath } from "../../tools/fs/safety.js";
|
|
3
|
-
import { createCoarseCommandNamePolicy,
|
|
3
|
+
import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
|
|
4
4
|
export const PATH_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
|
|
5
5
|
export const PATH_CONFINABLE_WRITE_TOOLS = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"]);
|
|
6
6
|
export function isWithin(root, p) {
|
|
@@ -24,8 +24,8 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
24
24
|
const { env, rootPath, toolEffects } = opts;
|
|
25
25
|
const toolDeny = new Set(rules.toolDeny ?? []);
|
|
26
26
|
const toolAllow = rules.toolAllow ? new Set(rules.toolAllow) : undefined;
|
|
27
|
-
const toolDenyCovering =
|
|
28
|
-
const toolAllowCovering =
|
|
27
|
+
const toolDenyCovering = namespacedCoveringEntries(rules.toolDeny);
|
|
28
|
+
const toolAllowCovering = namespacedCoveringEntries(rules.toolAllow);
|
|
29
29
|
const cmdPolicy = rules.commandAllow || rules.commandDeny
|
|
30
30
|
? createCoarseCommandNamePolicy({
|
|
31
31
|
...(rules.commandAllow ? { allow: rules.commandAllow } : {}),
|
|
@@ -38,9 +38,9 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
38
38
|
nameSets: [{ ...(rules.toolDeny?.length ? { deny: [...rules.toolDeny] } : {}), ...(rules.toolAllow?.length ? { allow: [...rules.toolAllow] } : {}) }],
|
|
39
39
|
async check(req, signal) {
|
|
40
40
|
const toolName = req.toolName;
|
|
41
|
-
if (toolDeny.has(toolName) ||
|
|
41
|
+
if (toolDeny.has(toolName) || namespacedCoveringHit(toolDenyCovering, toolName))
|
|
42
42
|
return deny(`tool "${req.toolName}" is denied by a session rule`);
|
|
43
|
-
if (toolAllow && !toolAllow.has(toolName) && !
|
|
43
|
+
if (toolAllow && !toolAllow.has(toolName) && !namespacedCoveringHit(toolAllowCovering, toolName)) {
|
|
44
44
|
return deny(`tool "${req.toolName}" is not in the session-rule allowlist`);
|
|
45
45
|
}
|
|
46
46
|
if (cmdPolicy) {
|
package/dist/core/scheduler.d.ts
CHANGED
|
@@ -147,6 +147,11 @@ export interface SchedulerCapability {
|
|
|
147
147
|
* 不得热翻)——工具壳的门控读与 `schedule()` 是两次操作,中途翻位会让 session intent 落到不会 reap 的
|
|
148
148
|
* backend 上。防线双置:声明面不可变 + `schedule()` 实现 MUST 自行拒绝它无法履行 reap 契约的
|
|
149
149
|
* `lifetime:"session"` intent(fail-closed 兜底,不依赖工具壳的先行探测)。
|
|
150
|
+
*
|
|
151
|
+
* 「诚实拒绝」条款的适用边界:它约束**携带 session 语义字段的调用**(CronCreate 的 `durable` 形)。
|
|
152
|
+
* 一个没有 durable 逃生口的工具(ScheduleWakeup 形)对不支持位的 backend 走的是另一臂——发送
|
|
153
|
+
* 历史 durable intent 并在回执与工具描述上**披露**该 wakeup 不随会话终结(披露≠静默;拒绝会在
|
|
154
|
+
* 零收益下拿掉该宿主的整只工具)。该臂为既裁形(capability 位 opt-out 裁定),非本条款的违例。
|
|
150
155
|
*/
|
|
151
156
|
readonly supportsSessionLifetime?: boolean;
|
|
152
157
|
/**
|
|
@@ -84,6 +84,38 @@ export interface ReconcileReport {
|
|
|
84
84
|
* `runner.resume()` uses a checkpoint-aware entry that bypasses this reconcile entirely for those calls.
|
|
85
85
|
*/
|
|
86
86
|
export declare function findOrphanToolCalls(messages: AgentMessage[], suspendedBatch?: ReadonlySet<string>): OrphanToolCall[];
|
|
87
|
+
/**
|
|
88
|
+
* backlog #389 伴生 (D-2) — CC 2.1.223's interruption markers, VERBATIM (`$U`/`CR` @ `CC:151120-151121`,
|
|
89
|
+
* minted as a USER message by `Jce` @ `CC:640141-640154`). CC mints one on every abort whose reason is
|
|
90
|
+
* outside `{"interrupt","refusal-fallback-edit"}` — and the interactive Esc / remote cancel, which is
|
|
91
|
+
* what `TaskStream.interrupt()` corresponds to, is precisely on the minting side (`CC:1033698-1033733`).
|
|
92
|
+
* The suppressed reason is the one case where the user's own replacement message is already the context.
|
|
93
|
+
*
|
|
94
|
+
* Taken verbatim rather than reworded: this string is an INPUT to later reasoning in CC (its own
|
|
95
|
+
* "interrupted then immediately retried the same action" rule reads it back), and the constitution's
|
|
96
|
+
* standing rule is that a question CC has answered is answered in CC's form.
|
|
97
|
+
*/
|
|
98
|
+
export declare const INTERRUPTED_BY_USER_MARKER = "[Request interrupted by user]";
|
|
99
|
+
/** backlog #389 伴生 — the tool-use variant (`CR`): the run was cut while a tool batch was in flight. */
|
|
100
|
+
export declare const INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER = "[Request interrupted by user for tool use]";
|
|
101
|
+
/**
|
|
102
|
+
* backlog #389 伴生 (D-2) — append the interruption marker so the SESSION records that a person stopped
|
|
103
|
+
* this run.
|
|
104
|
+
*
|
|
105
|
+
* Without it, an interrupt that lands on the model stream (no tool call in flight) leaves literally no
|
|
106
|
+
* trace: the orphan reconcile has nothing to close, and the empty aborted assistant is deliberately not
|
|
107
|
+
* persisted (`isEmptyFailureAssistant`). The next run on that session then reads a transcript in which
|
|
108
|
+
* the half-finished work simply stops, and continues as if it had ended by itself.
|
|
109
|
+
*
|
|
110
|
+
* MUST be called AFTER {@link reconcileInterruptedSession} on the same interruption: a user message
|
|
111
|
+
* placed between an assistant's tool calls and their results is exactly the invalid sequence the
|
|
112
|
+
* reconcile exists to prevent.
|
|
113
|
+
*
|
|
114
|
+
* Returns the persisted entry id so the caller can mint its `message_committed` frame.
|
|
115
|
+
*/
|
|
116
|
+
export declare function appendInterruptionMarker(session: Session, opts: {
|
|
117
|
+
toolUseInFlight: boolean;
|
|
118
|
+
}): Promise<string>;
|
|
87
119
|
/**
|
|
88
120
|
* Reconcile a resumed session's active branch: close any orphan tool calls with a synthetic
|
|
89
121
|
* interrupted `toolResult` (never re-running the tool). Returns what was recovered.
|
|
@@ -81,6 +81,21 @@ export function findOrphanToolCalls(messages, suspendedBatch) {
|
|
|
81
81
|
});
|
|
82
82
|
return orphans;
|
|
83
83
|
}
|
|
84
|
+
export const INTERRUPTED_BY_USER_MARKER = "[Request interrupted by user]";
|
|
85
|
+
export const INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER = "[Request interrupted by user for tool use]";
|
|
86
|
+
export async function appendInterruptionMarker(session, opts) {
|
|
87
|
+
return await session.appendMessage({
|
|
88
|
+
role: "user",
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: opts.toolUseInFlight ? INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER : INTERRUPTED_BY_USER_MARKER,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
provenance: "engine-note",
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
84
99
|
export async function reconcileInterruptedSession(session, toolEffects, suspendedBatch, startedToolCallIds) {
|
|
85
100
|
const { messages } = await session.buildContext();
|
|
86
101
|
const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
|
|
@@ -57,11 +57,18 @@ export interface SideQuerySpec {
|
|
|
57
57
|
/**
|
|
58
58
|
* Per-model auth — MIRRORS {@link TaskSpec.getApiKeyAndHeaders} (same signature, resolved per
|
|
59
59
|
* call against the RESOLVED model, exactly like the task path's per-call hook). The brain
|
|
60
|
-
* contract is
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
60
|
+
* contract is the route pairing law (route-adjudicator.ts): a per-model credential (this hook,
|
|
61
|
+
* or an auth header on `Model.headers`) always rides; the deployment credential rides only where
|
|
62
|
+
* its pairing is verifiable-or-unpinned — a declared `config.baseUrl` with an off-root model is
|
|
63
|
+
* refused (`route.credential_mismatch` / `route.credential_missing`), never silently followed.
|
|
64
|
+
* Historically the fallback was unconditional (`options.apiKey ?? config.apiKey`, with a model's
|
|
65
|
+
* own `baseUrl` outranking the brain's), so a side query routed to a model carrying its own
|
|
66
|
+
* `baseUrl` + per-model key sent the GATEWAY key to the per-model (possibly external) URL — the
|
|
67
|
+
* credential leak the pairing law now stops. Absent seat ⇒ no options are minted (byte-identical
|
|
68
|
+
* to before): on an UNDECLARED root the construction-time credential still applies (the
|
|
69
|
+
* quick-start posture), while a declared-root brain + off-root model hard-fails the side query
|
|
70
|
+
* with the loud refusal (`stopReason: "error"`) instead of leaking — there is no primary model
|
|
71
|
+
* for a side query to fall back to.
|
|
65
72
|
*/
|
|
66
73
|
getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
|
|
67
74
|
signal?: AbortSignal;
|