@sema-agent/core 7.5.1 → 7.5.2
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 +20 -0
- package/dist/core/protocol-table.d.ts +5 -0
- package/dist/core/protocol-table.js +1 -0
- package/dist/core/runner/abort-race.d.ts +41 -0
- package/dist/core/runner/abort-race.js +38 -0
- package/dist/core/runner/checkpoint-scope.d.ts +15 -3
- package/dist/core/runner/checkpoint-scope.js +3 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/content-ask-bindings.d.ts +27 -0
- package/dist/core/runner/content-ask-bindings.js +1 -0
- package/dist/core/runner/contracts.d.ts +118 -6
- package/dist/core/runner/denial-limit-arms.d.ts +23 -0
- package/dist/core/runner/denial-limit-arms.js +21 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
- package/dist/core/runner/inherited-ask-grants.js +29 -0
- package/dist/core/runner/park-commit.d.ts +108 -0
- package/dist/core/runner/park-commit.js +32 -0
- package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
- package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +110 -0
- package/dist/core/runner/prepare-ask-lane.js +133 -0
- package/dist/core/runner/prepare-boundary-parks.d.ts +105 -0
- package/dist/core/runner/prepare-boundary-parks.js +169 -0
- package/dist/core/runner/prepare-context-lane.d.ts +119 -0
- package/dist/core/runner/prepare-context-lane.js +230 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +177 -0
- package/dist/core/runner/prepare-gate-stations.js +290 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
- package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
- package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
- package/dist/core/runner/prepare-memory-engine-session.js +233 -0
- package/dist/core/runner/prepare-park-ask.d.ts +164 -0
- package/dist/core/runner/prepare-park-ask.js +377 -0
- package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
- package/dist/core/runner/prepare-policy-chain.js +584 -0
- package/dist/core/runner/prepare-project-context.d.ts +1 -13
- package/dist/core/runner/prepare-project-context.js +1 -3
- package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
- package/dist/core/runner/prepare-prompt-assembly.js +162 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
- package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
- package/dist/core/runner/prepare-protocol-tools.js +0 -3
- package/dist/core/runner/prepare-question-face.d.ts +3 -21
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
- package/dist/core/runner/prepare-safety-scan.js +1 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
- package/dist/core/runner/prepare-suspend-saga.js +308 -0
- package/dist/core/runner/prepare-task.d.ts +9 -136
- package/dist/core/runner/prepare-task.js +44 -2741
- package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
- package/dist/core/runner/prepare-turn-wiring.js +201 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -3
- package/dist/core/runner/prepare-wiring-manifest.js +9 -2
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
- package/dist/core/runner/prepare-workspace-restore.js +3 -16
- package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
- package/dist/core/runner/prompt-hash-salt.js +2 -0
- package/dist/core/runner/remote-env-retry.d.ts +29 -0
- package/dist/core/runner/remote-env-retry.js +16 -0
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +3 -0
- package/package.json +1 -1
- /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
- /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
2
|
+
import { BINDING_CHECKPOINT_VERSION, debitLedger, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, mintCheckpointId, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, } from "../checkpoint-store.js";
|
|
3
|
+
import { defaultTaskRegistry } from "../task-registry.js";
|
|
4
|
+
import { checkpointScopeOf } from "./checkpoint-scope.js";
|
|
5
|
+
import { DEFAULT_RESOURCE_TTL_MS, publishCommittedSuspend, sanitizedTtlMs, USAGE_WINDOW_REAP_MARGIN_MS } from "./park-commit.js";
|
|
6
|
+
import { remoteEnvFailureNote } from "./remote-env-retry.js";
|
|
7
|
+
export function prepareBoundaryParks(input) {
|
|
8
|
+
const { saga, spec, checkpointStore, abortController, harness, session, sessions, sessionId, deps, priorLedger, maxSlices, maxSuspends, resourceTotal, priorSuspendCount, suspendChainBase, humanReviewRef, liveSpendRef, now, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, suspendRef, reviewRef, remoteEnvFailures, resourceSuspendEligible, durableSuspendInfraReady, incompleteSuspendAdapter } = input;
|
|
9
|
+
if (saga === undefined)
|
|
10
|
+
return { suspendForResource: undefined, suspendForPlatformLimit: undefined, suspendForReview: undefined };
|
|
11
|
+
const { inFlightSpendMicroUsd, serializeCheckpointState, commitSuspendSaga, suspendLoopCapHit, suspendableEnv, parkOnlyRemoteEnv, parkOnlyHandle } = saga;
|
|
12
|
+
const mintResourceSuspend = async (reason, sliceSpend, hint) => {
|
|
13
|
+
const rs = spec.resourceSuspend;
|
|
14
|
+
if (!checkpointStore)
|
|
15
|
+
return false;
|
|
16
|
+
const scope = rs?.scope ?? checkpointScopeOf(spec);
|
|
17
|
+
if (abortController.signal.aborted)
|
|
18
|
+
return false;
|
|
19
|
+
if (suspendLoopCapHit(priorLedger?.sliceCount ?? 0, maxSlices, " (resource slice limit)"))
|
|
20
|
+
return false;
|
|
21
|
+
const leafId = await session.getLeafId();
|
|
22
|
+
if (!leafId)
|
|
23
|
+
return false;
|
|
24
|
+
if (abortController.signal.aborted)
|
|
25
|
+
return false;
|
|
26
|
+
let remoteHandle;
|
|
27
|
+
if (suspendableEnv !== undefined) {
|
|
28
|
+
if (hasBackgroundShell(suspendableEnv))
|
|
29
|
+
await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
|
|
30
|
+
const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
|
|
31
|
+
if (!snap.ok) {
|
|
32
|
+
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
33
|
+
deps.onError?.(new Error(`resource suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
remoteHandle = { ...suspendableEnv.workspaceHandle(), snapshotId: snap.value };
|
|
37
|
+
}
|
|
38
|
+
else if (parkOnlyRemoteEnv !== undefined) {
|
|
39
|
+
if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
|
|
40
|
+
await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
|
|
41
|
+
remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
|
|
42
|
+
}
|
|
43
|
+
const token = mintCheckpointToken();
|
|
44
|
+
const gate = { kind: "resource_limit", reason, ...(hint !== undefined ? { resumeAfterMs: hint.resumeAfterMs } : {}) };
|
|
45
|
+
const resourceLedgerOut = debitLedger(priorLedger, sliceSpend, resourceTotal);
|
|
46
|
+
const mintedAt = Date.now();
|
|
47
|
+
const cp = {
|
|
48
|
+
token,
|
|
49
|
+
checkpointId: mintCheckpointId(),
|
|
50
|
+
version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
|
|
51
|
+
scope,
|
|
52
|
+
sessionId,
|
|
53
|
+
leafId,
|
|
54
|
+
gate,
|
|
55
|
+
pendingAction: { kind: "resource_limit", reason },
|
|
56
|
+
state: serializeCheckpointState(remoteHandle),
|
|
57
|
+
status: "pending",
|
|
58
|
+
createdAt: mintedAt,
|
|
59
|
+
deadline: mintedAt + Math.max(sanitizedTtlMs(rs?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS, hint !== undefined ? hint.resumeAfterMs + USAGE_WINDOW_REAP_MARGIN_MS : 0),
|
|
60
|
+
suspendCount: priorSuspendCount,
|
|
61
|
+
humanReview: humanReviewRef.count > 0
|
|
62
|
+
? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
|
|
63
|
+
: undefined,
|
|
64
|
+
resourceLedger: resourceLedgerOut,
|
|
65
|
+
...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
|
|
66
|
+
...(spec.principal ? { principal: spec.principal } : {}),
|
|
67
|
+
};
|
|
68
|
+
if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
|
|
69
|
+
return false;
|
|
70
|
+
publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
|
|
71
|
+
try {
|
|
72
|
+
await sessions.pin?.(sessionId);
|
|
73
|
+
}
|
|
74
|
+
catch (pinErr) {
|
|
75
|
+
deps.onError?.(pinErr, { phase: "config", sessionId });
|
|
76
|
+
}
|
|
77
|
+
harness.requestStopAfterTurn();
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
const suspendForResource = resourceSuspendEligible ? mintResourceSuspend : undefined;
|
|
81
|
+
const suspendForPlatformLimit = durableSuspendInfraReady ? mintResourceSuspend : undefined;
|
|
82
|
+
const suspendForReview = checkpointStore
|
|
83
|
+
? async (reason) => {
|
|
84
|
+
if (!checkpointStore)
|
|
85
|
+
return false;
|
|
86
|
+
if (abortController.signal.aborted)
|
|
87
|
+
return false;
|
|
88
|
+
if (incompleteSuspendAdapter !== undefined) {
|
|
89
|
+
try {
|
|
90
|
+
deps.onError?.(new Error(`plan_review park skipped: the per-task execution env declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")}, so its workspace could not be restored on resume (the plan-review request was dropped; the run continues without pausing)`), { phase: "config", sessionId });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (suspendLoopCapHit(suspendChainBase(), maxSuspends, " for a plan_review (likely a resume/restart loop)."))
|
|
97
|
+
return false;
|
|
98
|
+
const leafId = await session.getLeafId();
|
|
99
|
+
if (!leafId) {
|
|
100
|
+
try {
|
|
101
|
+
deps.onError?.(new Error("plan_review park skipped: no committed session leaf (the plan-review request was dropped; the run continues without pausing)"), { phase: "config", sessionId });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (abortController.signal.aborted)
|
|
108
|
+
return false;
|
|
109
|
+
let remoteHandle;
|
|
110
|
+
if (suspendableEnv !== undefined) {
|
|
111
|
+
if (hasBackgroundShell(suspendableEnv))
|
|
112
|
+
await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
|
|
113
|
+
const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
|
|
114
|
+
if (!snap.ok) {
|
|
115
|
+
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
116
|
+
deps.onError?.(new Error(`plan_review suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
remoteHandle = { ...suspendableEnv.workspaceHandle(), snapshotId: snap.value };
|
|
120
|
+
}
|
|
121
|
+
else if (parkOnlyRemoteEnv !== undefined) {
|
|
122
|
+
if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
|
|
123
|
+
await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
|
|
124
|
+
remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
|
|
125
|
+
}
|
|
126
|
+
const token = mintCheckpointToken();
|
|
127
|
+
const scope = checkpointScopeOf(spec);
|
|
128
|
+
const gate = { kind: "plan_review", reason: reason ?? `plan review requested` };
|
|
129
|
+
const reviewLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
|
|
130
|
+
const mintedAt = Date.now();
|
|
131
|
+
const cp = {
|
|
132
|
+
token,
|
|
133
|
+
checkpointId: mintCheckpointId(),
|
|
134
|
+
version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
135
|
+
scope,
|
|
136
|
+
sessionId,
|
|
137
|
+
leafId,
|
|
138
|
+
gate,
|
|
139
|
+
pendingAction: { kind: "plan_review" },
|
|
140
|
+
state: serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd()),
|
|
141
|
+
status: "pending",
|
|
142
|
+
createdAt: mintedAt,
|
|
143
|
+
suspendedAt: now(),
|
|
144
|
+
deadline: mintedAt + (sanitizedTtlMs(spec.durableApproval?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
|
|
145
|
+
suspendCount: suspendChainBase() + 1,
|
|
146
|
+
humanReview: humanReviewRef.count > 0
|
|
147
|
+
? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
|
|
148
|
+
: undefined,
|
|
149
|
+
resourceLedger: reviewLedger,
|
|
150
|
+
sourceTaskId: sessionId,
|
|
151
|
+
...(spec.principal ? { principal: spec.principal } : {}),
|
|
152
|
+
...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
|
|
153
|
+
};
|
|
154
|
+
if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
|
|
155
|
+
return false;
|
|
156
|
+
publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
|
|
157
|
+
try {
|
|
158
|
+
await sessions.pin?.(sessionId);
|
|
159
|
+
}
|
|
160
|
+
catch (pinErr) {
|
|
161
|
+
deps.onError?.(pinErr, { phase: "config", sessionId });
|
|
162
|
+
}
|
|
163
|
+
abortController.abort();
|
|
164
|
+
void harness.abort();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
: undefined;
|
|
168
|
+
return { suspendForResource, suspendForPlatformLimit, suspendForReview };
|
|
169
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/390 §1.2 M21 — prepareTask's CONTEXT-LANE phase, verbatim from the driver: the edit/guard budgets, the
|
|
3
|
+
* microCompact machine state with the trigger-window clamp's one loud outlet, the model's chars-per-token coordinate,
|
|
4
|
+
* and the `context` handler registered on the harness — heal → ledger replay → aggregate tool-result and media caps →
|
|
5
|
+
* frontier clear → the guard chain (arms A/B/C) → the git-frame guard → the orphan belt → the lossy-projection flag →
|
|
6
|
+
* the projection seat. The four module-level helpers only this lane called moved with it (the machine-state builder,
|
|
7
|
+
* the frontier-clear recorder, the observation-buffer flush, the guard chain with its two types). The interface is the
|
|
8
|
+
* dependency list the segment had implicitly (design/238 R-1).
|
|
9
|
+
*
|
|
10
|
+
* SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (the handler it registers is async and
|
|
11
|
+
* runs per provider request), so the phase adds no yield between the gate stations before it and the turn wiring after
|
|
12
|
+
* it (design/390 §1.5). Read-stability of the host handles: {@link RunInternals} (`@contract prepare.deps-read-stable`).
|
|
13
|
+
*
|
|
14
|
+
* REGISTRATION SIDE EFFECT, OWNED: this phase registers the ONE `context` handler on the harness; the harness owns the
|
|
15
|
+
* listener for the run's life (it is never unregistered — the harness is per prepare). No other station registers on
|
|
16
|
+
* that hook.
|
|
17
|
+
*/
|
|
18
|
+
import { type AgentHarness, type AgentMessage } from "../../internal/harness.js";
|
|
19
|
+
import type { Model } from "../../internal/llm.js";
|
|
20
|
+
import { type ContextEditMachine, type OccurrenceIndex } from "../context-edit.js";
|
|
21
|
+
import { type ToolResultStore } from "../tool-result-store.js";
|
|
22
|
+
import { type RunnerDeps } from "../types.js";
|
|
23
|
+
import type { Prepared, PreparedMicroCompact } from "./contracts.js";
|
|
24
|
+
import { type GitStatusLaneRef } from "./git-status-frame.js";
|
|
25
|
+
export interface PrepareContextLaneInput {
|
|
26
|
+
/** borrowed-readonly — the task model: the budgets, the trigger-window geometry and `charsPerToken` are read off it. */
|
|
27
|
+
model: Model;
|
|
28
|
+
/** borrowed-readonly — the deployment seats, whole: `onNotice` (the clamp outlet, the offload-loss announcement), `tracer`
|
|
29
|
+
* (the cap / clear / trim frames), `mediaByteCapBytes` and `onMediaStripped` (the media cap), `onError` (the belt line, the
|
|
30
|
+
* git-frame degrade) — the guard chain and the machine-state builder read through the whole record. */
|
|
31
|
+
deps: RunnerDeps;
|
|
32
|
+
/** borrowed-readonly — the acquired session id (the offload persist key; every disclosure's attribution). */
|
|
33
|
+
sessionId: string;
|
|
34
|
+
/** borrowed-readonly — the run's invocation id (the clamp notice's per-run claim). */
|
|
35
|
+
runId: string;
|
|
36
|
+
/** borrowed-readonly — the per-task identity (the trace frames' taskId). */
|
|
37
|
+
hostTaskId: string;
|
|
38
|
+
/** borrowed-readonly — the run's offload store, or undefined (the caps' and the machine's persist seat). */
|
|
39
|
+
offloadStore: ToolResultStore | undefined;
|
|
40
|
+
/** borrowed-readonly — the doors' RESOLVED microCompact knob (immutable values, never the deps bag). */
|
|
41
|
+
microCompactKnob: {
|
|
42
|
+
machine: "off" | ContextEditMachine;
|
|
43
|
+
clearOnRejection: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** borrowed-mutable — the built harness. Writer here: `on("context", …)` — the ONE registration of the request-build
|
|
46
|
+
* handler (the harness owns the listener; nothing unregisters it). */
|
|
47
|
+
harness: AgentHarness;
|
|
48
|
+
/** borrowed-mutable — the turn's halt fact (`Prepared.batchHaltRef`). Writer here: the handler CLEARS it at every new
|
|
49
|
+
* provider request (a new request means the halted boundary was legitimately passed). The judgment site sets it. */
|
|
50
|
+
batchHaltRef: Prepared["batchHaltRef"];
|
|
51
|
+
/** borrowed-mutable — the git-status lane ref. Writers here: the git-frame guard's degrade latch (`overBudgetShrunk`) inside
|
|
52
|
+
* the handler; the prompt-inputs phase seeds it. */
|
|
53
|
+
gitStatusRef: GitStatusLaneRef;
|
|
54
|
+
/** borrowed-mutable — the per-request lossy-projection flag. Writer here: the handler re-derives it at every request
|
|
55
|
+
* build; the provider-seam recording shim reads it immediately after. */
|
|
56
|
+
requestLossyRef: {
|
|
57
|
+
current: boolean;
|
|
58
|
+
};
|
|
59
|
+
/** borrowed-mutable — the trim-pressure flag (`Prepared.trimPressureRef`). Writer here: the guard chain's trim arm sets
|
|
60
|
+
* `droppedMessages`; the run loop's boundary reads and clears it. */
|
|
61
|
+
trimPressureRef: {
|
|
62
|
+
droppedMessages: boolean;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export interface PrepareContextLaneResult {
|
|
66
|
+
/** owned — the per-run microCompact machine state (`Prepared.microCompact`): machine, MC-R flag, ledger, the projection
|
|
67
|
+
* and in-turn-compaction seats the handler and the runtask arms fill. */
|
|
68
|
+
microCompact: PreparedMicroCompact;
|
|
69
|
+
/** owned — the model's structural coefficient (`charsPerToken ?? DEFAULT`), the ONE coordinate the request-layer
|
|
70
|
+
* defenses here and the prompt-overhead term downstream share with the compaction trigger estimate. */
|
|
71
|
+
charsPerToken: number;
|
|
72
|
+
}
|
|
73
|
+
/** The M21 phase body — prepareTask's context-lane stretch, verbatim (see the module header). */
|
|
74
|
+
export declare function prepareContextLane(input: PrepareContextLaneInput): PrepareContextLaneResult;
|
|
75
|
+
/** See {@link runGuardChain}. */
|
|
76
|
+
export interface GuardChainArgs {
|
|
77
|
+
/** The post-frontier view the chain starts from. */
|
|
78
|
+
edited: AgentMessage[];
|
|
79
|
+
/** The pre-cap counterpart view (arm A's clearSource — RB-212 durable composition seat). */
|
|
80
|
+
replayed: AgentMessage[];
|
|
81
|
+
index: OccurrenceIndex;
|
|
82
|
+
microCompact: PreparedMicroCompact;
|
|
83
|
+
guardAt: number;
|
|
84
|
+
charsPerToken: number;
|
|
85
|
+
offloadStore: import("../tool-result-store.js").ToolResultStore | undefined;
|
|
86
|
+
sessionId: string;
|
|
87
|
+
taskId: string;
|
|
88
|
+
deps: RunnerDeps;
|
|
89
|
+
trimPressureRef: {
|
|
90
|
+
droppedMessages: boolean;
|
|
91
|
+
};
|
|
92
|
+
recheck: boolean | undefined;
|
|
93
|
+
signal: AbortSignal | undefined;
|
|
94
|
+
/** The r7 per-request observation buffer — the chain DROPS it on its early returns (a discarded
|
|
95
|
+
* provisional projection must not narrate itself). */
|
|
96
|
+
pendingProjectionObservations: Array<() => void>;
|
|
97
|
+
}
|
|
98
|
+
export type GuardChainOutcome = {
|
|
99
|
+
earlyReturn: {
|
|
100
|
+
messages: AgentMessage[];
|
|
101
|
+
adoptSessionRebuild?: boolean;
|
|
102
|
+
};
|
|
103
|
+
} | {
|
|
104
|
+
working: AgentMessage[];
|
|
105
|
+
trimmed: AgentMessage[];
|
|
106
|
+
trimDroppedMessages: boolean;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* design/374 slice 3 — the GUARD CHAIN over one request build (D-7 case-ii transfer table),
|
|
110
|
+
* module-level per the design/238 body-span bank. design/123 D3: the chain reads the anchored
|
|
111
|
+
* coordinate re-estimated on the EDITED array (pre-anchor clears don't lower it — deliberately;
|
|
112
|
+
* trimToBudget doc). The opt-out machine = the pre-374 backstop order byte-identical
|
|
113
|
+
* (trim as the ordinary second line); otherwise arm A (blocking machine run, only when the
|
|
114
|
+
* frontier pass is off) → arm B (in-turn forced compaction behind the adopt seam) → arm C (trim,
|
|
115
|
+
* the disaster-only last resort and this chain's sole `context.trim` emitter). Returns either the
|
|
116
|
+
* post-chain view for the pipeline tail, or an EARLY hook result the context handler must return
|
|
117
|
+
* verbatim (arm-B adoption / the r8 dead-turn stand-down).
|
|
118
|
+
*/
|
|
119
|
+
export declare function runGuardChain(args: GuardChainArgs): Promise<GuardChainOutcome>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens } from "../../internal/harness.js";
|
|
2
|
+
import { clearStaleToolResults, createClearedProjectionLedger, dropEmptyFailureAssistants, editBudget, replayClearedProjection, resolveTriggerWindow } from "../context-edit.js";
|
|
3
|
+
import { dropOrphanToolResults, guardBudget, insertTrimNotice, trimToBudget } from "../context-guard.js";
|
|
4
|
+
import { AGGREGATE_MEDIA_BUDGET_BYTES, capAggregateMediaBytes } from "../media-byte-cap.js";
|
|
5
|
+
import { capAggregateToolResults } from "../tool-result-budget.js";
|
|
6
|
+
import { createOffloadPersist } from "../tool-result-store.js";
|
|
7
|
+
import { emitTrace } from "../trace.js";
|
|
8
|
+
import { deliverEngineNotice } from "../types.js";
|
|
9
|
+
import { applyGitFrameGuard } from "./git-status-frame.js";
|
|
10
|
+
export function prepareContextLane(input) {
|
|
11
|
+
const { model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, requestLossyRef, trimPressureRef } = input;
|
|
12
|
+
const editAt = editBudget(model);
|
|
13
|
+
const guardAt = guardBudget(model);
|
|
14
|
+
const microCompact = buildMicroCompactState(deps, model, sessionId, runId, offloadStore, microCompactKnob);
|
|
15
|
+
const charsPerToken = model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
|
|
16
|
+
harness.on("context", async ({ messages, recheck, signal }) => {
|
|
17
|
+
batchHaltRef.current = undefined;
|
|
18
|
+
const pendingProjectionObservations = [];
|
|
19
|
+
try {
|
|
20
|
+
const healed = dropEmptyFailureAssistants(messages);
|
|
21
|
+
const replay = replayClearedProjection(healed, microCompact.ledger);
|
|
22
|
+
const replayed = replay.messages;
|
|
23
|
+
const capped = await capAggregateToolResults(replayed, {
|
|
24
|
+
store: offloadStore,
|
|
25
|
+
sessionId,
|
|
26
|
+
onCapped: (info) => pendingProjectionObservations.push(() => emitTrace(deps.tracer, () => ({
|
|
27
|
+
kind: "tool_result.capped",
|
|
28
|
+
version: 1,
|
|
29
|
+
taskId: hostTaskId,
|
|
30
|
+
...(info.tool !== undefined ? { tool: info.tool } : {}),
|
|
31
|
+
sizeChars: info.sizeChars,
|
|
32
|
+
storeFallback: info.storeFallback,
|
|
33
|
+
ts: Date.now(),
|
|
34
|
+
}))),
|
|
35
|
+
});
|
|
36
|
+
const mediaCapped = capAggregateMediaBytes(capped, {
|
|
37
|
+
limitBytes: deps.mediaByteCapBytes ?? AGGREGATE_MEDIA_BUDGET_BYTES,
|
|
38
|
+
onStripped: (info) => {
|
|
39
|
+
const onMediaStripped = deps.onMediaStripped;
|
|
40
|
+
if (onMediaStripped !== undefined)
|
|
41
|
+
pendingProjectionObservations.push(() => onMediaStripped(info));
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
let ccPass;
|
|
45
|
+
const edited = microCompact.machine === "off" ? mediaCapped : clearStaleToolResults(mediaCapped, {
|
|
46
|
+
budgetTokens: editAt,
|
|
47
|
+
machine: microCompact.machine,
|
|
48
|
+
...(microCompact.machine === "cc" || microCompact.clearOnRejection ? { recognizeCcMarkers: true } : {}),
|
|
49
|
+
...(microCompact.machine === "cc"
|
|
50
|
+
? {
|
|
51
|
+
clearSource: replayed,
|
|
52
|
+
onCleared: (pass) => { ccPass = pass; },
|
|
53
|
+
}
|
|
54
|
+
: {}),
|
|
55
|
+
anchoredTotalTokens: estimateContextTokens(mediaCapped, charsPerToken).tokens,
|
|
56
|
+
charsPerToken,
|
|
57
|
+
...(offloadStore
|
|
58
|
+
? {
|
|
59
|
+
offload: {
|
|
60
|
+
persist: createOffloadPersist(offloadStore, sessionId, deps.onNotice),
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
: {}),
|
|
64
|
+
});
|
|
65
|
+
if (ccPass !== undefined) {
|
|
66
|
+
recordFrontierClears({ pass: ccPass, index: replay.index, edited, ledger: microCompact.ledger, tracer: deps.tracer, taskId: hostTaskId, trigger: "frontier" });
|
|
67
|
+
}
|
|
68
|
+
const chain = await runGuardChain({
|
|
69
|
+
edited, replayed, index: replay.index, microCompact, guardAt, charsPerToken, offloadStore,
|
|
70
|
+
sessionId, taskId: hostTaskId, deps, trimPressureRef, recheck, signal,
|
|
71
|
+
pendingProjectionObservations,
|
|
72
|
+
});
|
|
73
|
+
if ("earlyReturn" in chain) {
|
|
74
|
+
return chain.earlyReturn;
|
|
75
|
+
}
|
|
76
|
+
const { working, trimDroppedMessages } = chain;
|
|
77
|
+
let trimmed = chain.trimmed;
|
|
78
|
+
trimmed = applyGitFrameGuard({
|
|
79
|
+
before: working,
|
|
80
|
+
trimmed,
|
|
81
|
+
budgetTokens: guardAt,
|
|
82
|
+
ref: gitStatusRef,
|
|
83
|
+
charsPerToken,
|
|
84
|
+
onDegrade: (message) => {
|
|
85
|
+
try {
|
|
86
|
+
deps.onError?.(new Error(message), { phase: "degraded", sessionId, classification: "env-git-snapshot" });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const swept = dropOrphanToolResults(trimmed);
|
|
93
|
+
if (swept.dropped.length > 0) {
|
|
94
|
+
try {
|
|
95
|
+
deps.onError?.(new Error(`context belt: dropped ${swept.dropped.length} orphan toolResult(s) whose tool-call assistant was not in the request view — ` +
|
|
96
|
+
swept.dropped
|
|
97
|
+
.map((d) => `toolCallId=${d.toolCallId} at index ${d.index} (prev=${d.prevRole}, next=${d.nextRole})`)
|
|
98
|
+
.join("; ")), { phase: "hook", sessionId });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
requestLossyRef.current =
|
|
104
|
+
replayed !== healed ||
|
|
105
|
+
capped !== replayed ||
|
|
106
|
+
mediaCapped !== capped ||
|
|
107
|
+
edited !== mediaCapped ||
|
|
108
|
+
working !== edited ||
|
|
109
|
+
trimDroppedMessages ||
|
|
110
|
+
swept.dropped.length > 0 ||
|
|
111
|
+
gitStatusRef.overBudgetShrunk === true;
|
|
112
|
+
microCompact.projectionRef.current = { messages: swept.messages, keyOf: replay.index.keyOf };
|
|
113
|
+
return { messages: swept.messages };
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
flushProjectionObservations(pendingProjectionObservations);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return { microCompact, charsPerToken };
|
|
120
|
+
}
|
|
121
|
+
function buildMicroCompactState(deps, model, sessionId, runId, offloadStore, knob) {
|
|
122
|
+
const triggerWindow = resolveTriggerWindow(model);
|
|
123
|
+
if (triggerWindow.clamped) {
|
|
124
|
+
deliverEngineNotice(deps.onNotice, {
|
|
125
|
+
code: "config.autocompact_window_clamped",
|
|
126
|
+
message: `model "${model.id}" declares autoCompactTokens=${triggerWindow.declaredAutoCompactTokens} ABOVE its physical window ` +
|
|
127
|
+
`${triggerWindow.physicalWindow}; the autocompact window only ever LOWERS the trigger-side geometry, so the value was ` +
|
|
128
|
+
`clamped to the physical window — fix the model config (declare a value at or below the physical window, or omit it)`,
|
|
129
|
+
detail: {
|
|
130
|
+
modelId: model.id,
|
|
131
|
+
declaredAutoCompactTokens: triggerWindow.declaredAutoCompactTokens,
|
|
132
|
+
physicalWindow: triggerWindow.physicalWindow,
|
|
133
|
+
sessionId,
|
|
134
|
+
runId,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
machine: knob.machine,
|
|
140
|
+
clearOnRejection: knob.clearOnRejection,
|
|
141
|
+
ledger: createClearedProjectionLedger(),
|
|
142
|
+
projectionRef: {},
|
|
143
|
+
inTurnCompactionRef: {},
|
|
144
|
+
...(offloadStore ? { offloadPersist: createOffloadPersist(offloadStore, sessionId, deps.onNotice) } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function recordFrontierClears(args) {
|
|
148
|
+
for (const { index } of args.pass.clears) {
|
|
149
|
+
const at = args.index.keyAt(index);
|
|
150
|
+
const clearedMsg = args.edited[index];
|
|
151
|
+
const markerText = clearedMsg.content?.[0]?.text;
|
|
152
|
+
if (at !== undefined && typeof markerText === "string") {
|
|
153
|
+
args.ledger.entries.set(at.key, { marker: markerText, groupCount: at.groupCount, fp: at.fp });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
emitTrace(args.tracer, () => ({
|
|
157
|
+
kind: "context.mc_clear",
|
|
158
|
+
version: 1,
|
|
159
|
+
taskId: args.taskId,
|
|
160
|
+
clearedCount: args.pass.clears.length,
|
|
161
|
+
tokensSavedEstimate: args.pass.tokensSavedEstimate,
|
|
162
|
+
trigger: args.trigger,
|
|
163
|
+
ts: Date.now(),
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
function flushProjectionObservations(pending) {
|
|
167
|
+
for (const fire of pending.splice(0)) {
|
|
168
|
+
try {
|
|
169
|
+
fire();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function runGuardChain(args) {
|
|
176
|
+
const { edited, microCompact, guardAt, charsPerToken, deps, taskId } = args;
|
|
177
|
+
const applyGuardTrim = (view, anchoredTotal) => {
|
|
178
|
+
let trimmed = trimToBudget(view, guardAt, anchoredTotal, charsPerToken);
|
|
179
|
+
const dropped = trimmed.length < view.length;
|
|
180
|
+
if (dropped) {
|
|
181
|
+
args.trimPressureRef.droppedMessages = true;
|
|
182
|
+
const droppedCount = view.length - trimmed.length;
|
|
183
|
+
trimmed = insertTrimNotice(trimmed);
|
|
184
|
+
emitTrace(deps.tracer, () => ({
|
|
185
|
+
kind: "context.trim",
|
|
186
|
+
version: 1,
|
|
187
|
+
taskId,
|
|
188
|
+
dropped: droppedCount,
|
|
189
|
+
ts: Date.now(),
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
return { trimmed, dropped };
|
|
193
|
+
};
|
|
194
|
+
let working = edited;
|
|
195
|
+
let anchoredWorking = estimateContextTokens(working, charsPerToken).tokens;
|
|
196
|
+
if (microCompact.machine === "legacy") {
|
|
197
|
+
const t = applyGuardTrim(working, anchoredWorking);
|
|
198
|
+
return { working, trimmed: t.trimmed, trimDroppedMessages: t.dropped };
|
|
199
|
+
}
|
|
200
|
+
if (microCompact.machine === "off" && anchoredWorking > guardAt) {
|
|
201
|
+
let blockingPass;
|
|
202
|
+
const blockingEdited = clearStaleToolResults(working, {
|
|
203
|
+
budgetTokens: guardAt,
|
|
204
|
+
machine: "cc",
|
|
205
|
+
clearSource: args.replayed,
|
|
206
|
+
onCleared: (pass) => { blockingPass = pass; },
|
|
207
|
+
anchoredTotalTokens: anchoredWorking,
|
|
208
|
+
charsPerToken,
|
|
209
|
+
...(args.offloadStore ? { offload: { persist: createOffloadPersist(args.offloadStore, args.sessionId, deps.onNotice) } } : {}),
|
|
210
|
+
});
|
|
211
|
+
if (blockingPass !== undefined) {
|
|
212
|
+
recordFrontierClears({ pass: blockingPass, index: args.index, edited: blockingEdited, ledger: microCompact.ledger, tracer: deps.tracer, taskId, trigger: "blocking" });
|
|
213
|
+
working = blockingEdited;
|
|
214
|
+
anchoredWorking = estimateContextTokens(working, charsPerToken).tokens;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (anchoredWorking > guardAt && args.recheck !== true) {
|
|
218
|
+
const compacted = (await microCompact.inTurnCompactionRef.current?.(args.signal, anchoredWorking)) === true;
|
|
219
|
+
if (compacted) {
|
|
220
|
+
args.pendingProjectionObservations.length = 0;
|
|
221
|
+
return { earlyReturn: { messages: working, adoptSessionRebuild: true } };
|
|
222
|
+
}
|
|
223
|
+
if (args.signal?.aborted === true) {
|
|
224
|
+
args.pendingProjectionObservations.length = 0;
|
|
225
|
+
return { working, trimmed: working, trimDroppedMessages: false };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const t = applyGuardTrim(working, anchoredWorking);
|
|
229
|
+
return { working, trimmed: t.trimmed, trimDroppedMessages: t.dropped };
|
|
230
|
+
}
|