@sema-agent/core 7.36.0 → 7.37.1
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 +116 -0
- package/dist/brain/anthropic.js +7 -14
- package/dist/brain/open-responses.js +6 -13
- package/dist/brain/openai.js +7 -14
- package/dist/brain/stream-shared.d.ts +21 -0
- package/dist/brain/stream-shared.js +13 -0
- package/dist/core/ask-origin.d.ts +35 -5
- package/dist/core/ask-origin.js +23 -4
- package/dist/core/checkpoint-store.d.ts +13 -3
- package/dist/core/decision-receipt.d.ts +21 -0
- package/dist/core/decision-receipt.js +63 -0
- package/dist/core/engine-notice.d.ts +17 -5
- package/dist/core/gate-fold.js +2 -1
- package/dist/core/gate-lanes.js +8 -13
- package/dist/core/gate-outcome.d.ts +7 -5
- package/dist/core/gate-outcome.js +5 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +11 -2
- package/dist/core/hooks.js +3 -1
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/runner/denial-limit-arms.d.ts +28 -18
- package/dist/core/runner/denial-limit-arms.js +11 -7
- package/dist/core/runner/inherited-ask-grants.d.ts +32 -31
- package/dist/core/runner/inherited-ask-grants.js +5 -0
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +1 -5
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -8
- package/dist/core/runner/prepare-policy-chain.js +43 -25
- package/dist/core/runner/run-identity-wiring.d.ts +1 -1
- package/dist/core/runner/run-identity-wiring.js +2 -0
- package/dist/core/runner/tool-turn-repair-seat.d.ts +8 -0
- package/dist/core/runner/tool-turn-repair-seat.js +28 -0
- package/dist/core/session-reconcile.d.ts +35 -0
- package/dist/core/session-reconcile.js +31 -20
- package/dist/core/side-query.js +4 -2
- package/dist/core/store-contracts/checkpoint-store-contract.js +18 -0
- package/dist/core/tool-name-readings.d.ts +4 -0
- package/dist/core/tool-name-readings.js +1 -0
- package/dist/core/tool-policy.d.ts +74 -84
- package/dist/core/tool-policy.js +71 -36
- package/dist/engine/harness/agent-harness.d.ts +7 -0
- package/dist/engine/harness/agent-harness.js +14 -2
- package/dist/engine/llm/index.d.ts +1 -0
- package/dist/engine/llm/index.js +1 -0
- package/dist/engine/llm/tool-turns.d.ts +20 -0
- package/dist/engine/llm/tool-turns.js +91 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/llm.d.ts +2 -2
- package/dist/internal/llm.js +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
- package/wire-changes/core@7.37.0.json +405 -0
- package/wire-changes/core@7.37.1.json +122 -0
|
@@ -1,41 +1,42 @@
|
|
|
1
|
+
import type { AskClass } from "../ask-origin.js";
|
|
1
2
|
import type { HumanReviewGateRow } from "../task-result.js";
|
|
2
3
|
import { type OnAsk, type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
* gate just re-asks — the safe direction, never a gate failure). */
|
|
7
|
-
export declare function askGrantShapeOf(args: unknown): string | undefined;
|
|
8
|
-
/**
|
|
9
|
-
* Duplicate-frame collapse (single-Runner embedded shape), the CONSUME half: an inherited
|
|
10
|
-
* layer already resolved THIS call as a clean allow at the SAME approver function, on EXACTLY the
|
|
11
|
-
* byte-shape this caller-slot ask now presents. The human's consent covers this consultation
|
|
12
|
-
* verbatim — asking again is the 2-frame UX debt, not a second authority. Guards, all mismatches
|
|
13
|
-
* re-ask (⇒ `undefined`):
|
|
14
|
-
* · SOURCE: only a caller-slot POLICY ask may reuse — a safety tighten
|
|
15
|
-
* (egress/irreversible/shellGate stamp `decisionReason:"safety"`), a hook-promoted ask
|
|
16
|
-
* ("hook"), or a classifier ask carries a DIFFERENT authority/context and keeps its frame.
|
|
17
|
-
* · IDENTITY: the approver FUNCTION reference (never a name) — compared through
|
|
18
|
-
* `askApproverIdentity`, not by raw reference (ruled 2026-08-04): a delegated child's approver
|
|
19
|
-
* is the ancestor's own, wrapped with delegation provenance — a DIFFERENT function object, the
|
|
20
|
-
* SAME operator. Bare `===` therefore stopped collapsing the frame the moment the ask seam
|
|
21
|
-
* started forwarding approvers, and one tool call presented the same person the same card
|
|
22
|
-
* twice. The wrapper adds metadata and delegates every decision, so it grants no authority of
|
|
23
|
-
* its own; identity here only answers "same consultation".
|
|
24
|
-
* · SHAPE: byte-equality against the presented SNAPSHOT; reuse re-emits that snapshot
|
|
25
|
-
* as the executed shape via `presentedInput` — the same schema-revalidated binding
|
|
26
|
-
* as a main-gate approval (shown == executed, by construction).
|
|
27
|
-
* · LEDGER: the ONE real consultation (timed on the wrapper arm) is recorded here —
|
|
28
|
-
* one frame happened, one frame is booked.
|
|
29
|
-
* The grant lives only within one gate evaluation (consumed on reuse, swept at the call site) — a
|
|
30
|
-
* REOPENED checkpoint's re-adjudication never sees a stale grant.
|
|
31
|
-
*/
|
|
32
|
-
export declare function consumeInheritedAskGrant(grants: Map<string, {
|
|
4
|
+
/** One inherited ask grant: a person's clean allow at an ancestor's live approver, on ONE presented shape, to ONE question
|
|
5
|
+
* (`classes`, the request's class set in `ASK_CLASSES` order) — it answers no stricter question. */
|
|
6
|
+
export interface InheritedAskGrant {
|
|
33
7
|
approver: unknown;
|
|
34
8
|
argsJson: string;
|
|
35
9
|
presented: unknown;
|
|
36
10
|
waitMs: number;
|
|
37
11
|
settled?: object;
|
|
38
|
-
|
|
12
|
+
classes: readonly AskClass[];
|
|
13
|
+
}
|
|
14
|
+
/** Does a grant for `granted` cover a question the gate classified as `asked`? `ordinary` — the plain hesitation every other
|
|
15
|
+
* class is a stricter form of — is covered by any card answered; every other class must have been on the card (the gate reads
|
|
16
|
+
* facts the station cannot hold, and a class those add was asked of nobody). Unclassified, or a card with no class ⇒ never. */
|
|
17
|
+
export declare function askGrantCovers(granted: readonly AskClass[], asked: readonly AskClass[] | undefined): boolean;
|
|
18
|
+
/** JSON.stringify throws on a cyclic value (a rewrite object carries no acyclicity guarantee) — `undefined` means "no
|
|
19
|
+
* comparable shape", which every consumer treats as a mismatch (skip the record / reuse ⇒ re-ask, the safe direction). */
|
|
20
|
+
export declare function askGrantShapeOf(args: unknown): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Duplicate-frame collapse, the CONSUME half: an inherited layer already resolved THIS call as a clean allow at the
|
|
23
|
+
* SAME approver function, on EXACTLY the byte-shape this caller-slot ask now presents, for a question that covers this
|
|
24
|
+
* one — asking again is the 2-frame UX debt, not a second authority. Guards, all mismatches re-ask (⇒ `undefined`):
|
|
25
|
+
* · SOURCE: only a caller-slot POLICY ask may reuse — a safety tighten (`decisionReason:"safety"`), a hook-promoted
|
|
26
|
+
* ask ("hook") or a classifier ask carries a DIFFERENT authority and keeps its frame.
|
|
27
|
+
* · IDENTITY: the approver FUNCTION reference (never a name), compared through `askApproverIdentity`: a delegated
|
|
28
|
+
* child's approver is the ancestor's own wrapped with provenance — a different function object, the same operator
|
|
29
|
+
* (the wrapper grants no authority of its own); identity answers only "same consultation".
|
|
30
|
+
* · SHAPE: byte-equality against the presented SNAPSHOT; reuse re-emits that snapshot as the executed shape via
|
|
31
|
+
* `presentedInput` — the same binding as a main-gate approval (shown == executed, by construction).
|
|
32
|
+
* · CLASS: the gate's stamped `classes` is covered by the set the person answered ({@link askGrantCovers}); a class
|
|
33
|
+
* the gate's own judges add (a protected target, an out-of-root read) was not on the card. Not covered ⇒ the gate
|
|
34
|
+
* asks its own question and the grant stays (it answers what it answered; the call-site sweep ends it).
|
|
35
|
+
* · LEDGER: the ONE real consultation (timed on the wrapper arm) is booked here — one frame happened, one is booked.
|
|
36
|
+
* The grant lives within one gate evaluation (consumed on reuse, swept at the call site): a REOPENED checkpoint's
|
|
37
|
+
* re-adjudication never sees a stale grant.
|
|
38
|
+
*/
|
|
39
|
+
export declare function consumeInheritedAskGrant(grants: Map<string, InheritedAskGrant>, onAsk: OnAsk | undefined, humanReviewRef: {
|
|
39
40
|
count: number;
|
|
40
41
|
totalWaitMs: number;
|
|
41
42
|
gates: HumanReviewGateRow[];
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { primaryActivityArg } from "../arg-summary.js";
|
|
2
2
|
import { askApproverIdentity, carryEngineAttestations } from "../tool-policy.js";
|
|
3
|
+
export function askGrantCovers(granted, asked) {
|
|
4
|
+
return asked !== undefined && asked.length > 0 && granted.length > 0 && asked.every((c) => c === "ordinary" || granted.includes(c));
|
|
5
|
+
}
|
|
3
6
|
export function askGrantShapeOf(args) {
|
|
4
7
|
try {
|
|
5
8
|
return JSON.stringify(args ?? null);
|
|
@@ -14,6 +17,8 @@ export function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision
|
|
|
14
17
|
const grant = grants.get(req.toolCallId);
|
|
15
18
|
if (grant === undefined || askApproverIdentity(grant.approver) !== askApproverIdentity(onAsk) || grant.argsJson !== askGrantShapeOf(req.args))
|
|
16
19
|
return undefined;
|
|
20
|
+
if (!askGrantCovers(grant.classes, decision.action === "ask" ? decision.classes : undefined))
|
|
21
|
+
return undefined;
|
|
17
22
|
grants.delete(req.toolCallId);
|
|
18
23
|
humanReviewRef.count += 1;
|
|
19
24
|
humanReviewRef.totalWaitMs += grant.waitMs;
|
|
@@ -36,7 +36,7 @@ export function prepareInheritedGate(input) {
|
|
|
36
36
|
if (modeSeed.migrated !== undefined) {
|
|
37
37
|
deliverEngineNotice(deps.onNotice, {
|
|
38
38
|
code: "resume.legacy_mode_seed_migrated",
|
|
39
|
-
message: `the resumed checkpoint row carried pre-mode keys (${modeSeed.migrated.droppedKeys.join(", ")}); read once into ${modeSeed.migrated.permissionMode !== undefined ? `permissionMode: "${modeSeed.migrated.permissionMode}"` : "no mode of its own (the chain's ceiling stands)"}${modeSeed.shellGate !== undefined ? ' with the shell floor "always" kept' : ""} — the next suspend writes the one key;
|
|
39
|
+
message: `the resumed checkpoint row carried pre-mode keys (${modeSeed.migrated.droppedKeys.join(", ")}); read once into ${modeSeed.migrated.permissionMode !== undefined ? `permissionMode: "${modeSeed.migrated.permissionMode}"` : "no mode of its own (the chain's ceiling stands)"}${modeSeed.shellGate !== undefined ? ' with the shell floor "always" kept' : ""} — the next suspend writes the one key; this migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys (resume or abandon such rows before upgrading)`,
|
|
40
40
|
detail: { migrated: { ...(modeSeed.migrated.permissionMode !== undefined ? { permissionMode: modeSeed.migrated.permissionMode } : {}), droppedKeys: [...modeSeed.migrated.droppedKeys] }, sessionId, runId },
|
|
41
41
|
});
|
|
42
42
|
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* decomposition exists to make explicit.
|
|
10
10
|
*
|
|
11
11
|
* The thirteen-argument park closure is unchanged: twelve positional seats then the structural {@link AskCarry}, the
|
|
12
|
-
* shape the gate's one invocation site (`parkWith`) holds. The ask's origin word and
|
|
13
|
-
* carry onto the row
|
|
12
|
+
* shape the gate's one invocation site (`parkWith`) holds. The ask's origin word, class pair and denial-limit fallback
|
|
13
|
+
* ride the carry onto the row — this phase mints no origin or class, changes no word, adds no arm to the closed set.
|
|
14
14
|
*
|
|
15
15
|
* SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (both closures run per tool call), so the
|
|
16
16
|
* phase adds no yield between the boundary parks before it and the gate station after it.
|
|
@@ -173,11 +173,7 @@ export function prepareParkAsk(input) {
|
|
|
173
173
|
if (reprojectedRaced.tag === "threw")
|
|
174
174
|
throw reprojectedRaced.error;
|
|
175
175
|
const reprojected = refuseOutOfContractDecision(reprojectedRaced.value);
|
|
176
|
-
const
|
|
177
|
-
const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
|
|
178
|
-
!(demanded?.ok === true &&
|
|
179
|
-
samePlainValue(demanded.value, parkedArgs) &&
|
|
180
|
-
samePlainValue(reprojected.updatedInput, parkedArgs));
|
|
176
|
+
const rewroteTheFiledValue = reprojected.updatedInput !== undefined && !samePlainValue(reprojected.updatedInput, parkedArgs);
|
|
181
177
|
if (reprojected.action === "deny" || rewroteTheFiledValue) {
|
|
182
178
|
throw new ParkRefusal(`the arguments of "${req.toolName}" change shape when stored by this checkpoint backend, and the deployment's tool policy does not accept the stored shape` +
|
|
183
179
|
`${reprojected.action === "deny" && reprojected.message ? `: ${reprojected.message}` : ""} — ` +
|
|
@@ -34,6 +34,7 @@ import { type ActiveSkillScope } from "./active-skill-scope.js";
|
|
|
34
34
|
import type { InheritedGate, IrreversibilityTier, PrepareResume, RunInternals, StopForDenialLimit } from "./contracts.js";
|
|
35
35
|
import { type Arbitration } from "../ask-origin.js";
|
|
36
36
|
import { type ModeGateSeats, type PermissionModeController } from "../mode-policy.js";
|
|
37
|
+
import { type InheritedAskGrant } from "./inherited-ask-grants.js";
|
|
37
38
|
import { createRuleOffersOf, type PermissionRuleLanes } from "./permission-rule-lanes.js";
|
|
38
39
|
import type { ToolCallLike } from "../tool-registry.js";
|
|
39
40
|
export interface PreparePolicyChainInput {
|
|
@@ -45,7 +46,7 @@ export interface PreparePolicyChainInput {
|
|
|
45
46
|
* `permissionRuleStore` / `localOwnerRules` (the lanes and the offer factory), `onError` / `onNotice` (the advisories,
|
|
46
47
|
* the refusal disclosures, the late-ask observer), `tracer` (the admission and rule-store frames), `checkpointStore` (the park facility). Receiver preserved;
|
|
47
48
|
* the advisory dedupe is KEYED on this object's identity. */
|
|
48
|
-
deps: Pick<RunnerDeps, "sessionPolicyStore" | "hooks" | "permissionRuleStore" | "localOwnerRules" | "onError" | "onNotice" | "tracer" | "checkpointStore">;
|
|
49
|
+
deps: Pick<RunnerDeps, "sessionPolicyStore" | "hooks" | "permissionRuleStore" | "localOwnerRules" | "onError" | "onNotice" | "tracer" | "checkpointStore" | "a2a">;
|
|
49
50
|
/** borrowed-readonly — the trusted spawn-side channel: `parentTaskId` (the from-subagent bit), `explicitAgentName` /
|
|
50
51
|
* `agentName` (the source agent name) — the ask source identity's local, trusted facts. */
|
|
51
52
|
internals: Pick<RunInternals, "parentTaskId" | "explicitAgentName" | "agentName"> | undefined;
|
|
@@ -186,13 +187,7 @@ export interface PreparePolicyChainResult {
|
|
|
186
187
|
}>>;
|
|
187
188
|
/** borrowed-mutable — the grant record. Writers: the inherited arms here (record a clean allow at a live approver);
|
|
188
189
|
* the gate station (consume on reuse, sweep at settlement). */
|
|
189
|
-
inheritedAskGrants: Map<string,
|
|
190
|
-
approver: unknown;
|
|
191
|
-
argsJson: string;
|
|
192
|
-
presented: unknown;
|
|
193
|
-
waitMs: number;
|
|
194
|
-
settled?: object;
|
|
195
|
-
}>;
|
|
190
|
+
inheritedAskGrants: Map<string, InheritedAskGrant>;
|
|
196
191
|
/** owned — the SINGLE AskRequest source-identity factory every resolveAsk construction site spreads. */
|
|
197
192
|
askSourceIdentity: () => {
|
|
198
193
|
principal?: string;
|
|
@@ -6,12 +6,12 @@ import { cloneObserverInput, resolveHookTimeoutMs } from "../hooks.js";
|
|
|
6
6
|
import { assertHookSeatsCallable } from "../hook-seats.js";
|
|
7
7
|
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "../permission-rules.js";
|
|
8
8
|
import { isIsolated } from "../remote-env.js";
|
|
9
|
-
import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, durableParkOptInDeclared, isLiveApproverSeat, resolveAsk, toolPolicyNameSets, wrapToolPolicy, attestAskUnresolvableCause } from "../tool-policy.js";
|
|
9
|
+
import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, durableParkOptInDeclared, isLiveApproverSeat, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, wrapToolPolicy, attestAskUnresolvableCause } from "../tool-policy.js";
|
|
10
10
|
import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
|
|
11
11
|
import { NAMESPACED_NAME_SHAPES, protocolOf } from "../protocol-table.js";
|
|
12
12
|
import { emitTrace } from "../trace.js";
|
|
13
13
|
import { createActiveSkillScopePolicy } from "./active-skill-scope.js";
|
|
14
|
-
import { arbitrateByMode,
|
|
14
|
+
import { arbitrateByMode, askClassMembersOf, askMandateMember } from "../ask-origin.js";
|
|
15
15
|
import { createModeGatePolicy, mountedToolDeclaresReadOnly } from "../mode-policy.js";
|
|
16
16
|
const ancestorFacts = (ask) => ({ ...(ask ?? {}), ancestorResolved: true });
|
|
17
17
|
import { headlessDenyAtFold, headlessDenyAtRecheck, INHERITED_STATION_FACTS, inheritedAskCarry, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
|
|
@@ -20,12 +20,12 @@ import { askGrantShapeOf } from "./inherited-ask-grants.js";
|
|
|
20
20
|
import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
|
|
21
21
|
import { createLiveSessionRules, createSessionRulePolicy } from "./session-rule-policy.js";
|
|
22
22
|
import { thrownText } from "../thrown-value.js";
|
|
23
|
-
import { heldBy, seatOf, wireOf } from "../tool-name-readings.js";
|
|
23
|
+
import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
|
|
24
24
|
export async function preparePolicyChain(input) {
|
|
25
25
|
const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
|
|
26
26
|
const ancestorArbitrations = new Map();
|
|
27
|
-
const arbitrateInherited = (ask, toolCallId) => {
|
|
28
|
-
const verdict = arbitrateByMode(permissionMode.current,
|
|
27
|
+
const arbitrateInherited = (ask, toolCallId, classification) => {
|
|
28
|
+
const verdict = arbitrateByMode(permissionMode.current, classification, { ...(ask.origin !== undefined ? { origin: ask.origin } : {}), ...(ask.mandate !== undefined ? { mandate: ask.mandate } : {}) });
|
|
29
29
|
if (verdict.kind === "ask" || verdict.kind === "accepted")
|
|
30
30
|
return undefined;
|
|
31
31
|
ancestorArbitrations.set(toolCallId, verdict);
|
|
@@ -188,7 +188,7 @@ export async function preparePolicyChain(input) {
|
|
|
188
188
|
}, "mandate_unreconstructible", creq);
|
|
189
189
|
const markerChannelFullDeny = (creq) => attestAskUnresolvableCause({ action: "deny", message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed` }, "approver_unavailable", creq);
|
|
190
190
|
const inheritedAskGrants = new Map();
|
|
191
|
-
const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs, settled) => {
|
|
191
|
+
const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs, settled, classes) => {
|
|
192
192
|
if (typeof approver !== "function")
|
|
193
193
|
return;
|
|
194
194
|
if (presented === undefined)
|
|
@@ -198,7 +198,7 @@ export async function preparePolicyChain(input) {
|
|
|
198
198
|
const argsJson = askGrantShapeOf(presented);
|
|
199
199
|
if (argsJson === undefined)
|
|
200
200
|
return;
|
|
201
|
-
inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs, settled });
|
|
201
|
+
inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs, settled, classes });
|
|
202
202
|
};
|
|
203
203
|
const sandboxDeclared = isIsolated(executionEnv);
|
|
204
204
|
const sandboxAdmissionArmed = (() => {
|
|
@@ -259,7 +259,7 @@ export async function preparePolicyChain(input) {
|
|
|
259
259
|
};
|
|
260
260
|
const callerPolicyLayer = policy !== undefined && sameInstanceAncestorCount > 0
|
|
261
261
|
? wrapToolPolicy(policy, async (creq, csignal) => {
|
|
262
|
-
const decision = await policy.check(creq, csignal ?? abortController.signal);
|
|
262
|
+
const decision = refuseOutOfContractDecision(await policy.check(creq, csignal ?? abortController.signal));
|
|
263
263
|
sharedFirstDecision.set(creq.toolCallId, decision);
|
|
264
264
|
if (sharedFirstDecision.size > SHARED_FIRST_DECISION_CAP) {
|
|
265
265
|
const oldest = sharedFirstDecision.keys().next().value;
|
|
@@ -278,15 +278,33 @@ export async function preparePolicyChain(input) {
|
|
|
278
278
|
: {}),
|
|
279
279
|
...(delegation.isDelegatedChild ? { isDelegatedChild: true } : {}),
|
|
280
280
|
});
|
|
281
|
+
const seatMarksOf = (call) => {
|
|
282
|
+
const tierSeat = seatOf("report.riskAxes", irreversibilityTier, call);
|
|
283
|
+
return {
|
|
284
|
+
tier: tierSeat?.value,
|
|
285
|
+
egress: heldBy("report.riskAxes.egress", egressTools, call),
|
|
286
|
+
shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
|
|
287
|
+
sources: new Set(heldValues("report.markSources", toolMarkSources, call).flatMap((s) => [...s])),
|
|
288
|
+
};
|
|
289
|
+
};
|
|
281
290
|
const riskAxesOf = (call) => {
|
|
282
|
-
const
|
|
291
|
+
const seat = seatMarksOf(call);
|
|
283
292
|
const negatives = seatOf("report.riskAxes.negatives", axisExplicitNegatives, call)?.value;
|
|
284
|
-
const irreversible = tier === "always" || tier === "maybe" ? true : tier === "never" ? false : negatives?.irreversible;
|
|
285
|
-
const egress =
|
|
293
|
+
const irreversible = seat.tier === "always" || seat.tier === "maybe" ? true : seat.tier === "never" ? false : negatives?.irreversible;
|
|
294
|
+
const egress = seat.egress ? true : negatives?.egress;
|
|
286
295
|
if (irreversible === undefined && egress === undefined)
|
|
287
296
|
return {};
|
|
288
297
|
return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
|
|
289
298
|
};
|
|
299
|
+
const inheritedStationFacts = (call) => {
|
|
300
|
+
const seat = seatMarksOf(call);
|
|
301
|
+
return {
|
|
302
|
+
...INHERITED_STATION_FACTS,
|
|
303
|
+
mandateMarks: { egress: seat.egress, shellGated: seat.shellGated, ...(seat.tier !== undefined ? { irreversibility: seat.tier } : {}) },
|
|
304
|
+
...(seat.sources.size > 0 ? { toolMarkSources: seat.sources } : {}),
|
|
305
|
+
...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}),
|
|
306
|
+
};
|
|
307
|
+
};
|
|
290
308
|
const inheritedAskEvidence = inheritedAskRuleEvidence(deps.permissionRuleStore);
|
|
291
309
|
const { personal: permissionRuleLane, org: permissionRuleOrgLane } = createPermissionRuleLanes({
|
|
292
310
|
provider: deps.permissionRuleStore,
|
|
@@ -335,7 +353,7 @@ export async function preparePolicyChain(input) {
|
|
|
335
353
|
}
|
|
336
354
|
let re;
|
|
337
355
|
try {
|
|
338
|
-
re = await pol.check({ ...creq, args: editArgs }, csignal ?? abortController.signal);
|
|
356
|
+
re = refuseOutOfContractDecision(await pol.check({ ...creq, args: editArgs }, csignal ?? abortController.signal));
|
|
339
357
|
}
|
|
340
358
|
catch (err) {
|
|
341
359
|
return vetoed({
|
|
@@ -353,7 +371,7 @@ export async function preparePolicyChain(input) {
|
|
|
353
371
|
}
|
|
354
372
|
if (re.updatedInput !== undefined)
|
|
355
373
|
editArgs = re.updatedInput;
|
|
356
|
-
const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: ancestorAutoMode, ask: re, req: { ...creq, args: editArgs }, signal: csignal ?? abortController.signal, subject: "the approved edit", allowRewrite: editArgs, unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(ancestorDecider?.timeoutMs !== undefined ? { budgetMs: ancestorDecider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(onAskOf), onNotice: deps.onNotice, sessionId, runId }) });
|
|
374
|
+
const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: ancestorAutoMode, ask: re, req: { ...creq, args: editArgs }, signal: csignal ?? abortController.signal, subject: "the approved edit", allowRewrite: editArgs, facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(ancestorDecider?.timeoutMs !== undefined ? { budgetMs: ancestorDecider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(onAskOf), onNotice: deps.onNotice, sessionId, runId }) });
|
|
357
375
|
if (judged.kind === "allow") {
|
|
358
376
|
const cleared = { ...judged.result };
|
|
359
377
|
carryEngineAttestations(judged.result, cleared);
|
|
@@ -364,7 +382,7 @@ export async function preparePolicyChain(input) {
|
|
|
364
382
|
return vetoed(judged.result);
|
|
365
383
|
const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere } = judged;
|
|
366
384
|
re = editAsk;
|
|
367
|
-
const editArbitrated = arbitrateInherited(re, creq.toolCallId);
|
|
385
|
+
const editArbitrated = arbitrateInherited(re, creq.toolCallId, judged.classification);
|
|
368
386
|
if (editArbitrated !== undefined) {
|
|
369
387
|
const out = editArbitrated.action === "allow" ? { action: "allow", updatedInput: editArgs } : editArbitrated;
|
|
370
388
|
carryPrecedingSettlement(settledBy, out);
|
|
@@ -435,17 +453,17 @@ export async function preparePolicyChain(input) {
|
|
|
435
453
|
return first;
|
|
436
454
|
return mandateUnreconstructibleDeny(creq);
|
|
437
455
|
}
|
|
438
|
-
const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: first, req: creq, signal: csignal ?? abortController.signal, subject: "this call", unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
|
|
456
|
+
const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: first, req: creq, signal: csignal ?? abortController.signal, subject: "this call", facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
|
|
439
457
|
if (judged.kind === "allow")
|
|
440
458
|
return judged.result;
|
|
441
459
|
if (judged.kind === "deny")
|
|
442
460
|
return judged.result;
|
|
443
461
|
const { ask: inheritedAsk, fallback, mintedHere } = judged;
|
|
444
|
-
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) &&
|
|
462
|
+
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
|
|
445
463
|
recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
|
|
446
464
|
return { action: "allow" };
|
|
447
465
|
}
|
|
448
|
-
const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId);
|
|
466
|
+
const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId, judged.classification);
|
|
449
467
|
if (arbitrated !== undefined)
|
|
450
468
|
return arbitrated;
|
|
451
469
|
const presentedArgs = creq.args;
|
|
@@ -477,14 +495,14 @@ export async function preparePolicyChain(input) {
|
|
|
477
495
|
if (resolved.updatedInput !== undefined) {
|
|
478
496
|
return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode);
|
|
479
497
|
}
|
|
480
|
-
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
|
|
498
|
+
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved, askClassMembersOf(judged.classification).classes);
|
|
481
499
|
return { action: "allow" };
|
|
482
500
|
},
|
|
483
501
|
}
|
|
484
502
|
: wrapToolPolicy(pc.policy, async (creq, csignal) => {
|
|
485
503
|
let decision;
|
|
486
504
|
try {
|
|
487
|
-
decision = await pc.policy.check(creq, csignal ?? abortController.signal);
|
|
505
|
+
decision = refuseOutOfContractDecision(await pc.policy.check(creq, csignal ?? abortController.signal));
|
|
488
506
|
}
|
|
489
507
|
catch (err) {
|
|
490
508
|
return {
|
|
@@ -509,17 +527,17 @@ export async function preparePolicyChain(input) {
|
|
|
509
527
|
return mandateUnreconstructibleDeny(creq);
|
|
510
528
|
}
|
|
511
529
|
const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
|
|
512
|
-
const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: decision, req: { ...creq, args: presentedArgs }, signal: csignal ?? abortController.signal, subject: "this call", allowRewrite: decision.updatedInput, unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
|
|
530
|
+
const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: decision, req: { ...creq, args: presentedArgs }, signal: csignal ?? abortController.signal, subject: "this call", allowRewrite: decision.updatedInput, facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
|
|
513
531
|
if (judged.kind === "allow")
|
|
514
532
|
return judged.result;
|
|
515
533
|
if (judged.kind === "deny")
|
|
516
534
|
return judged.result;
|
|
517
535
|
const { ask: inheritedAsk, fallback, mintedHere } = judged;
|
|
518
|
-
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) &&
|
|
536
|
+
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
|
|
519
537
|
recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
|
|
520
538
|
return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
|
|
521
539
|
}
|
|
522
|
-
const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId);
|
|
540
|
+
const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId, judged.classification);
|
|
523
541
|
if (arbitrated !== undefined)
|
|
524
542
|
return arbitrated;
|
|
525
543
|
const askT0 = now();
|
|
@@ -551,7 +569,7 @@ export async function preparePolicyChain(input) {
|
|
|
551
569
|
return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode);
|
|
552
570
|
}
|
|
553
571
|
if (pc.preToolUse === undefined) {
|
|
554
|
-
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
|
|
572
|
+
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved, askClassMembersOf(judged.classification).classes);
|
|
555
573
|
}
|
|
556
574
|
if (decision.updatedInput === undefined)
|
|
557
575
|
return resolved;
|
|
@@ -572,7 +590,7 @@ export async function preparePolicyChain(input) {
|
|
|
572
590
|
sameInstanceAdapterIndices.add(j + callerSlotOffset);
|
|
573
591
|
});
|
|
574
592
|
const instrumentedChain = rewriteCapableChain.map((layer, idx) => wrapToolPolicy(layer, async (creq, csignal) => {
|
|
575
|
-
const d = await layer.check(creq, csignal);
|
|
593
|
+
const d = refuseOutOfContractDecision(await layer.check(creq, csignal));
|
|
576
594
|
if (d.updatedInput !== undefined)
|
|
577
595
|
rewriteEmitterIndex.set(creq.toolCallId, idx);
|
|
578
596
|
if (d.action === "ask") {
|
|
@@ -619,7 +637,7 @@ export async function preparePolicyChain(input) {
|
|
|
619
637
|
const revalidated = {};
|
|
620
638
|
try {
|
|
621
639
|
for (const layer of recheckLayers) {
|
|
622
|
-
const d = await layer.check(finalReq, signal);
|
|
640
|
+
const d = refuseOutOfContractDecision(await layer.check(finalReq, signal));
|
|
623
641
|
if (d.action === "deny") {
|
|
624
642
|
const carried = { ...d, updatedInput: decision.updatedInput };
|
|
625
643
|
carryEngineAttestations(d, carried);
|
|
@@ -12,7 +12,7 @@ export interface RunIdentityWiringInput {
|
|
|
12
12
|
* task_notification frames. */
|
|
13
13
|
queue: PushQueue<TaskEvent>;
|
|
14
14
|
/** borrowed-mutable — the leg's prepared seat: the harness sinks (`onUndrainedEngineNotes`, `engineInjectionsHeld`,
|
|
15
|
-
* `onUndrainedUserInputs`, `onEngineNoteConsumed`) are installed, `nextTurn` is appended to; the ids, the identity
|
|
15
|
+
* `onUndrainedUserInputs`, `onEngineNoteConsumed`, `onToolTurnRepaired`) are installed, `nextTurn` is appended to; the ids, the identity
|
|
16
16
|
* envelope, the hold refs, the memory session and the listing / roster faces are read. */
|
|
17
17
|
prepared: Prepared;
|
|
18
18
|
/** borrowed-readonly — the trusted run-scoped channel: `parentToolCallId`, `onNotifyInjectorReady`. */
|
|
@@ -3,6 +3,7 @@ import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js"
|
|
|
3
3
|
import { recordFrameReceipt, toolResultReceiptKey } from "../frame-settlement.js";
|
|
4
4
|
import { discloseDroppedPending, renderTaskNotificationXml, taskNotificationDedupKey } from "../task-notification.js";
|
|
5
5
|
import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
|
|
6
|
+
import { createToolTurnRepairSeat } from "./tool-turn-repair-seat.js";
|
|
6
7
|
export function runIdentityWiring(input) {
|
|
7
8
|
const { spec, queue, prepared, internals, taskIdRef, onReady, manualCompactRef, notifyRef, captureOptOutRef, mcpRedialRef, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, notificationLane, pendingSessionNotifications, runner } = input;
|
|
8
9
|
notificationLane.harness = prepared.harness;
|
|
@@ -84,6 +85,7 @@ export function runIdentityWiring(input) {
|
|
|
84
85
|
if (key !== undefined)
|
|
85
86
|
recordFrameReceipt(seam, key, carrier.kind === "tool_result" ? carrier.content : undefined, entryId);
|
|
86
87
|
};
|
|
88
|
+
prepared.harness.onToolTurnRepaired = createToolTurnRepairSeat((notice) => runner.deps.onNotice?.(notice), { sessionId: prepared.sessionId, runId: prepared.runId });
|
|
87
89
|
if (notifyRef)
|
|
88
90
|
notifyRef.inject = injectTaskNotification;
|
|
89
91
|
if (captureOptOutRef && prepared.memoryEngineSession?.captureOptOut !== undefined) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EngineNotice } from "../engine-notice.js";
|
|
2
|
+
import type { ToolTurnRepair } from "../../internal/llm.js";
|
|
3
|
+
/** The notice's attribution ids — the same pair, for the same reason, as the fold seat's. */
|
|
4
|
+
export interface ToolTurnRepairSeatIds {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createToolTurnRepairSeat(onNotice: ((notice: EngineNotice) => void) | undefined, ids: ToolTurnRepairSeatIds): (repairs: readonly ToolTurnRepair[]) => void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
2
|
+
const FORM_SENTENCE = {
|
|
3
|
+
trailing_text: "a block after an assistant's tool calls was moved ahead of them",
|
|
4
|
+
late_result: "a result recorded later was moved up behind its call",
|
|
5
|
+
missing_result: "a call with no result was closed as interrupted (outcome unknown)",
|
|
6
|
+
duplicate_result: "a second result recorded for one call was folded into the first (an interrupted close beside a recorded result is left out)",
|
|
7
|
+
};
|
|
8
|
+
export function createToolTurnRepairSeat(onNotice, ids) {
|
|
9
|
+
const announced = new Set();
|
|
10
|
+
return (repairs) => {
|
|
11
|
+
const fresh = repairs.filter((repair) => {
|
|
12
|
+
const key = `${repair.form}\u0000${[...repair.toolCallIds].sort().join("\u0000")}`;
|
|
13
|
+
if (announced.has(key))
|
|
14
|
+
return false;
|
|
15
|
+
announced.add(key);
|
|
16
|
+
return true;
|
|
17
|
+
});
|
|
18
|
+
if (fresh.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
deliverEngineNotice(onNotice, {
|
|
21
|
+
code: "context.tool_turn_repaired",
|
|
22
|
+
message: `The session transcript carries ${fresh.length === 1 ? "a tool turn" : `${fresh.length} tool turns`} in a shape a provider lane ` +
|
|
23
|
+
`rejects, so the request was repaired on its way out: ${[...new Set(fresh.map((r) => FORM_SENTENCE[r.form]))].join("; ")}. ` +
|
|
24
|
+
`The transcript is unchanged and every later request is repaired the same way; announced once per run.`,
|
|
25
|
+
detail: { repairs: fresh.map((r) => ({ form: r.form, toolCallIds: [...r.toolCallIds] })), sessionId: ids.sessionId, runId: ids.runId },
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "../internal/harness-types.js";
|
|
2
|
+
import type { ToolResultMessage } from "../internal/llm.js";
|
|
2
3
|
import type { Session } from "./session.js";
|
|
3
4
|
import type { ToolEffect } from "./types.js";
|
|
4
5
|
/**
|
|
@@ -116,6 +117,40 @@ export declare const INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER = "[Request interru
|
|
|
116
117
|
export declare function appendInterruptionMarker(session: Session, opts: {
|
|
117
118
|
toolUseInFlight: boolean;
|
|
118
119
|
}): Promise<string>;
|
|
120
|
+
/** One synthetic interrupted result: what a call with no recorded result is closed with. */
|
|
121
|
+
export interface InterruptedResult {
|
|
122
|
+
toolCallId: string;
|
|
123
|
+
toolName: string;
|
|
124
|
+
/** The model-facing text. */
|
|
125
|
+
text: string;
|
|
126
|
+
/** Which of the two honest states the call is closed in. */
|
|
127
|
+
errorKind: ReconciledErrorKind;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The ONE mint of interrupted-result content for a batch of calls that have no recorded result. Two readers: the
|
|
131
|
+
* persisted reconcile ({@link reconcileInterruptedSession}) and the request view's tool-turn law
|
|
132
|
+
* (`engine/llm/tool-turns.ts`, which closes a call on the wire only and never writes the transcript) — so a call
|
|
133
|
+
* closed either way reads the same to the model.
|
|
134
|
+
*
|
|
135
|
+
* `toolEffects` / `startedToolCallIds` as on {@link reconcileInterruptedSession}; absent ⇒ every call gets the
|
|
136
|
+
* conservative unknown-outcome wording. The first call of each class in the batch states it in full and the rest
|
|
137
|
+
* point back at it (scoped to this call, so a later batch always starts from the full wording again).
|
|
138
|
+
*/
|
|
139
|
+
export declare function interruptedResults(calls: readonly {
|
|
140
|
+
toolCallId: string;
|
|
141
|
+
toolName: string;
|
|
142
|
+
}[], toolEffects?: Map<string, ToolEffect>, startedToolCallIds?: ReadonlySet<string>): InterruptedResult[];
|
|
143
|
+
/** Is `message` an INTERRUPTED CLOSE — a result minted to stand in for one that was never recorded (this family's
|
|
144
|
+
* two-valued `details.errorKind`, written by the reconcile here and by the loop's turn-scoped interrupt)? A close
|
|
145
|
+
* says only "no result was recorded", so it is void beside a recorded result for the same call. Total: a tool's
|
|
146
|
+
* `details` is the tool's value, so an unreadable one reads as "not a close" (one guarded read per member). */
|
|
147
|
+
export declare function isInterruptedClose(message: {
|
|
148
|
+
details?: unknown;
|
|
149
|
+
}): boolean;
|
|
150
|
+
/** The `toolResult` message for one {@link InterruptedResult} — the persisted and the request-view closes are this one shape. */
|
|
151
|
+
export declare function interruptedResultMessage(result: InterruptedResult, timestamp: number): ToolResultMessage<{
|
|
152
|
+
errorKind: ReconciledErrorKind;
|
|
153
|
+
}>;
|
|
119
154
|
/**
|
|
120
155
|
* Reconcile a resumed session's active branch: close any orphan tool calls with a synthetic
|
|
121
156
|
* interrupted `toolResult` (never re-running the tool). Returns what was recovered.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readMember } from "./thrown-value.js";
|
|
1
2
|
const INTERRUPTED_UNKNOWN = "[INTERRUPTED] The previous run ended before this tool call's result was recorded. Its outcome " +
|
|
2
3
|
"is UNKNOWN — the action may have completed, partially completed, or never run. Do not assume " +
|
|
3
4
|
"success or failure. Before relying on it: if it is safe to read, query the current state to " +
|
|
@@ -96,30 +97,40 @@ export async function appendInterruptionMarker(session, opts) {
|
|
|
96
97
|
timestamp: Date.now(),
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
|
-
export
|
|
100
|
-
const { messages } = await session.buildContext();
|
|
101
|
-
const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
|
|
102
|
-
const recovered = [];
|
|
100
|
+
export function interruptedResults(calls, toolEffects, startedToolCallIds) {
|
|
103
101
|
const statedInFull = new Set();
|
|
104
|
-
|
|
105
|
-
const effect = toolEffects?.get(
|
|
102
|
+
return calls.map(({ toolCallId, toolName }) => {
|
|
103
|
+
const effect = toolEffects?.get(toolName) ?? "write";
|
|
106
104
|
const effectText = effect === "read" ? INTERRUPTED_SAFE : effect === "idempotent" ? INTERRUPTED_IDEMPOTENT : INTERRUPTED_UNKNOWN;
|
|
107
|
-
const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(
|
|
108
|
-
const fullText = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
|
|
105
|
+
const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(toolCallId);
|
|
109
106
|
const cls = neverStarted ? "never_started" : effect === "read" ? "read" : effect === "idempotent" ? "idempotent" : "unknown";
|
|
110
|
-
const text = statedInFull.has(cls) ? REPEAT_TEXT[cls] :
|
|
107
|
+
const text = statedInFull.has(cls) ? REPEAT_TEXT[cls] : neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
|
|
111
108
|
statedInFull.add(cls);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
109
|
+
return { toolCallId, toolName, text, errorKind: neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown" };
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function isInterruptedClose(message) {
|
|
113
|
+
const kind = readMember(readMember(message, "details"), "errorKind");
|
|
114
|
+
return kind === "interrupted_never_started" || kind === "interrupted_outcome_unknown";
|
|
115
|
+
}
|
|
116
|
+
export function interruptedResultMessage(result, timestamp) {
|
|
117
|
+
return {
|
|
118
|
+
role: "toolResult",
|
|
119
|
+
toolCallId: result.toolCallId,
|
|
120
|
+
toolName: result.toolName,
|
|
121
|
+
content: [{ type: "text", text: result.text }],
|
|
122
|
+
details: { errorKind: result.errorKind },
|
|
123
|
+
isError: true,
|
|
124
|
+
timestamp,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export async function reconcileInterruptedSession(session, toolEffects, suspendedBatch, startedToolCallIds) {
|
|
128
|
+
const { messages } = await session.buildContext();
|
|
129
|
+
const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
|
|
130
|
+
const recovered = [];
|
|
131
|
+
for (const result of interruptedResults(orphans, toolEffects, startedToolCallIds)) {
|
|
132
|
+
const entryId = await session.appendMessage(interruptedResultMessage(result, Date.now()));
|
|
133
|
+
recovered.push({ toolCallId: result.toolCallId, toolName: result.toolName, entryId, text: result.text, errorKind: result.errorKind });
|
|
123
134
|
}
|
|
124
135
|
return { recovered };
|
|
125
136
|
}
|
package/dist/core/side-query.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureToolTurnInvariants } from "../internal/llm.js";
|
|
1
2
|
import { resolveTaskModel } from "./roles.js";
|
|
2
3
|
import { readDegradation } from "../brain/degrading.js";
|
|
3
4
|
import { cacheFamilyOf, uncachedInputTokensOf } from "./runner/usage-accounting.js";
|
|
@@ -33,14 +34,15 @@ export async function runSideQuery(spec, deps) {
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
if (m.role === "toolResult") {
|
|
36
|
-
|
|
37
|
+
const details = m.details;
|
|
38
|
+
return { role: "toolResult", toolCallId: m.toolCallId, toolName: m.toolName, content: m.content, isError: m.isError ?? false, timestamp, ...(details !== undefined ? { details } : {}) };
|
|
37
39
|
}
|
|
38
40
|
return { role: "user", content: m.content, timestamp };
|
|
39
41
|
};
|
|
40
42
|
const effectiveSystemPrompt = spec.systemPrompt ?? resolved.systemPrompt;
|
|
41
43
|
const context = {
|
|
42
44
|
...(effectiveSystemPrompt !== undefined ? { systemPrompt: effectiveSystemPrompt } : {}),
|
|
43
|
-
messages: spec.messages.map(normalize),
|
|
45
|
+
messages: ensureToolTurnInvariants(spec.messages.map(normalize)).messages,
|
|
44
46
|
...(spec.tools !== undefined && spec.tools.length > 0
|
|
45
47
|
? { tools: spec.tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })) }
|
|
46
48
|
: {}),
|
|
@@ -381,6 +381,24 @@ export async function checkpointStoreContract(make, runAssertion) {
|
|
|
381
381
|
assert.equal("mandate" in byId.get("mw-absent"), false, "absent must project the key OMITTED");
|
|
382
382
|
assert.equal("mandate" in byId.get("mw-bogus"), false, "a word outside the closed set must read as absent, never project verbatim");
|
|
383
383
|
});
|
|
384
|
+
run("#1214 ask class pair: a tool_approval row's `askClass` / `classes` survive the round-trip verbatim (the array whole, in its order); an absent pair stays absent", async () => {
|
|
385
|
+
const store = make();
|
|
386
|
+
const single = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "ac-single" });
|
|
387
|
+
Object.assign(single.pendingAction, { askClass: "denial_limit_fallback", classes: ["denial_limit_fallback"] });
|
|
388
|
+
const several = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "ac-several" });
|
|
389
|
+
Object.assign(several.pendingAction, { askClass: "transcript_write", classes: ["transcript_write", "org_rule"] });
|
|
390
|
+
const absent = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "ac-absent" });
|
|
391
|
+
for (const cp of [single, several, absent])
|
|
392
|
+
await store.put(cp.token, cp);
|
|
393
|
+
for (const [cp, askClass, classes] of [[single, "denial_limit_fallback", ["denial_limit_fallback"]], [several, "transcript_write", ["transcript_write", "org_rule"]]]) {
|
|
394
|
+
const back = (await store.get(cp.token)).pendingAction;
|
|
395
|
+
assert.equal(back.askClass, askClass, "the row's askClass must survive the round-trip verbatim");
|
|
396
|
+
assert.deepEqual(back.classes, classes, "the row's classes must survive the round-trip as the same array, in the same order");
|
|
397
|
+
}
|
|
398
|
+
const bare = (await store.get(absent.token)).pendingAction;
|
|
399
|
+
assert.equal("askClass" in bare, false, "an absent askClass must stay OMITTED on the row");
|
|
400
|
+
assert.equal("classes" in bare, false, "an absent classes must stay OMITTED on the row");
|
|
401
|
+
});
|
|
384
402
|
await settle();
|
|
385
403
|
}
|
|
386
404
|
const sortByToken = (a, b) => a.token.localeCompare(b.token);
|
|
@@ -203,6 +203,10 @@ export declare const NAME_READINGS: {
|
|
|
203
203
|
readonly rank: "member";
|
|
204
204
|
readonly file: string;
|
|
205
205
|
};
|
|
206
|
+
readonly "report.markSources": {
|
|
207
|
+
readonly kind: "any";
|
|
208
|
+
readonly file: string;
|
|
209
|
+
};
|
|
206
210
|
readonly "verify.wroteThisRun": {
|
|
207
211
|
readonly kind: "wire";
|
|
208
212
|
readonly file: string;
|