@sema-agent/core 7.29.0 → 7.30.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 +361 -0
- package/dist/agents/child-model-seat.d.ts +59 -7
- package/dist/agents/child-model-seat.js +33 -7
- package/dist/agents/cross-session-judge.d.ts +27 -5
- package/dist/agents/cross-session-judge.js +23 -12
- package/dist/agents/list-agents-tool.d.ts +31 -6
- package/dist/agents/list-agents-tool.js +66 -14
- package/dist/agents/peer-admission.d.ts +20 -8
- package/dist/agents/peer-admission.js +21 -12
- package/dist/agents/peer-directory.d.ts +29 -0
- package/dist/agents/peer-directory.js +20 -0
- package/dist/agents/peer-held-queue.d.ts +31 -4
- package/dist/agents/peer-held-queue.js +32 -1
- package/dist/agents/peer-session-drain.d.ts +91 -13
- package/dist/agents/peer-session-drain.js +409 -102
- package/dist/agents/read-inbox-tool.d.ts +22 -0
- package/dist/agents/read-inbox-tool.js +39 -0
- package/dist/agents/roster-store.d.ts +2 -4
- package/dist/agents/roster-store.js +1 -1
- package/dist/agents/send-message-tool.d.ts +4 -4
- package/dist/agents/send-message-tool.js +22 -2
- package/dist/agents/subagent.js +62 -44
- package/dist/agents/team-role.d.ts +34 -0
- package/dist/agents/team-role.js +17 -0
- package/dist/agents/verify.js +2 -2
- package/dist/core/auto-compaction.d.ts +42 -0
- package/dist/core/auto-compaction.js +35 -2
- package/dist/core/child-clamp-seats.d.ts +11 -0
- package/dist/core/child-clamp-seats.js +3 -0
- package/dist/core/context-edit.d.ts +3 -2
- package/dist/core/context-edit.js +6 -5
- package/dist/core/context-guard.d.ts +2 -1
- package/dist/core/context-guard.js +3 -2
- package/dist/core/context-settings.d.ts +103 -0
- package/dist/core/context-settings.js +60 -0
- package/dist/core/engine-notice.d.ts +11 -1
- package/dist/core/frame-settlement.d.ts +97 -0
- package/dist/core/frame-settlement.js +36 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +10 -10
- package/dist/core/mailbox-store.d.ts +41 -7
- package/dist/core/mailbox-store.js +9 -3
- package/dist/core/parent-spec-seat.d.ts +14 -1
- package/dist/core/parent-spec-seat.js +6 -0
- package/dist/core/permission-rule-model.js +1 -1
- package/dist/core/roles.d.ts +20 -0
- package/dist/core/roles.js +35 -16
- package/dist/core/runner/assemble-result.d.ts +2 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/compaction-call-options.d.ts +26 -2
- package/dist/core/runner/compaction-call-options.js +9 -0
- package/dist/core/runner/compaction-seams.d.ts +4 -3
- package/dist/core/runner/compaction-seams.js +13 -6
- package/dist/core/runner/contracts.d.ts +56 -7
- package/dist/core/runner/decide-continuation.d.ts +2 -1
- package/dist/core/runner/decide-continuation.js +11 -10
- package/dist/core/runner/initial-run-state.js +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
- package/dist/core/runner/prepare-config-doors.js +4 -1
- package/dist/core/runner/prepare-context-lane.d.ts +9 -2
- package/dist/core/runner/prepare-context-lane.js +15 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
- package/dist/core/runner/prepare-delegation-surface.js +9 -3
- package/dist/core/runner/prepare-file-history.js +10 -8
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
- package/dist/core/runner/prepare-prompt-inputs.js +2 -1
- package/dist/core/runner/prepare-task.js +22 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
- package/dist/core/runner/prepare-wiring-manifest.js +6 -1
- package/dist/core/runner/retired-runner-deps.d.ts +3 -1
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
- package/dist/core/runner/run-compaction-machinery.js +5 -3
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/run-identity-wiring.js +19 -1
- package/dist/core/runner/run-leg.js +1 -1
- package/dist/core/runner/run-notification-lane.js +8 -2
- package/dist/core/runner/run-recovery-lanes.js +9 -5
- package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
- package/dist/core/runner/run-settle-and-teardown.js +6 -10
- package/dist/core/runner/run-stop-and-final-verify.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +5 -5
- package/dist/core/runner/run-turn-boundary.js +32 -5
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +32 -1
- package/dist/core/runner-deps.d.ts +44 -13
- package/dist/core/swappable-deps.d.ts +20 -4
- package/dist/core/swappable-deps.js +6 -1
- package/dist/core/task-event.d.ts +5 -0
- package/dist/core/task-notification.d.ts +144 -2
- package/dist/core/task-notification.js +154 -5
- package/dist/core/task-result.d.ts +14 -0
- package/dist/core/task-spec.d.ts +7 -0
- package/dist/core/tool-catalog-entries.js +2 -1
- package/dist/core/tool-conformance.js +1 -0
- package/dist/core/tool-policy.d.ts +9 -4
- package/dist/core/tool-spec.d.ts +14 -0
- package/dist/core/wiring-manifest.d.ts +12 -0
- package/dist/core/wiring-manifest.js +5 -1
- package/dist/engine/harness/agent-harness.d.ts +14 -1
- package/dist/engine/harness/agent-harness.js +28 -3
- package/dist/engine/harness/types.d.ts +3 -1
- package/dist/engine/llm/types.d.ts +8 -7
- package/dist/engine/llm/types.js +1 -1
- package/dist/engine/session/import-validate.js +4 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +7 -5
- package/dist/orchestration/run-workflow-tool.d.ts +4 -4
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/orchestration/workflow.js +8 -5
- package/dist/shell/lex.d.ts +30 -51
- package/dist/shell/lex.js +237 -284
- package/dist/stores/cc/mailbox-store.js +3 -2
- package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
- package/dist/stores/file/file-history-store.d.ts +2 -4
- package/dist/stores/file/file-history-store.js +1 -1
- package/dist/stores/file/fs-atomic.d.ts +16 -11
- package/dist/stores/file/fs-atomic.js +17 -6
- package/dist/stores/file/index.d.ts +5 -6
- package/dist/stores/file/index.js +1 -1
- package/dist/stores/file/mailbox-store.d.ts +4 -9
- package/dist/stores/file/mailbox-store.js +29 -30
- package/dist/stores/file/session-policy-store.d.ts +2 -0
- package/dist/stores/file/session-policy-store.js +1 -1
- package/dist/stores/file/session-store.d.ts +11 -7
- package/dist/stores/file/session-store.js +23 -16
- package/dist/stores/file/shared-ledger.js +3 -14
- package/dist/stores/file/tool-result-store.d.ts +2 -0
- package/dist/stores/file/tool-result-store.js +5 -2
- package/dist/stores/file/workflow-journal-store.js +14 -9
- package/dist/tools/fs/bash-program-position.d.ts +15 -24
- package/dist/tools/fs/bash-program-position.js +11 -29
- package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
- package/dist/tools/fs/bash-readonly-classifier.js +400 -272
- package/dist/tools/fs/shell-read-check.js +3 -3
- package/dist/tools/fs/shell-read-establish.js +1 -1
- package/dist/tools/fs/shell-read-plan.d.ts +11 -6
- package/dist/tools/fs/shell-read-plan.js +167 -183
- package/dist/tools/fs/shell-read-word.d.ts +50 -0
- package/dist/tools/fs/shell-read-word.js +59 -0
- package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
- package/dist/tools/fs/shell-reentry-text.js +73 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +58 -8
- package/wire-changes/core@7.30.0.json +1821 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CROSS_SESSION_MESSAGE_TAG } from "../agents/cross-session-envelope.js";
|
|
1
2
|
import { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, CompactionError, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, formatFileOperations, formatPersistedOutputRefs, prepareCompaction, shouldCompact, } from "../internal/harness.js";
|
|
2
3
|
import { fileArgPath } from "../tools/fs/safety.js";
|
|
3
4
|
import { contextEditFrontier, resolveTriggerWindow } from "./context-edit.js";
|
|
@@ -81,9 +82,13 @@ export async function maybeCompact(opts) {
|
|
|
81
82
|
const structuralTokens = ctx.messages.reduce((sum, m) => sum + estimateTokens(m, cpt), 0);
|
|
82
83
|
const anchorStale = est.usageTokens > 0 && est.tokens > structuralTokens * STALE_ANCHOR_STRUCTURAL_MARGIN + overhead;
|
|
83
84
|
const tokens = anchorStale || est.usageTokens === 0 ? structuralTokens + overhead : est.tokens;
|
|
84
|
-
const window = resolveTriggerWindow(opts.model).window;
|
|
85
|
+
const window = resolveTriggerWindow(opts.model, opts.contextOverlay).window;
|
|
85
86
|
const windowKnown = Number.isFinite(window) && window > 0;
|
|
86
|
-
const windowField = opts.
|
|
87
|
+
const windowField = (opts.contextOverlay?.autoCompactTokens ?? opts.model.autoCompactTokens) != null
|
|
88
|
+
? "autoCompactTokens"
|
|
89
|
+
: (opts.contextOverlay?.contextTokens ?? opts.model.contextTokens) != null
|
|
90
|
+
? "contextTokens"
|
|
91
|
+
: "contextWindow";
|
|
87
92
|
const settings = sanitizeCompactionSettings(rawSettings, window);
|
|
88
93
|
const contextUsage = windowKnown
|
|
89
94
|
? { usedTokens: tokens, windowTokens: window, compactAtTokens: window - settings.reserveTokens }
|
|
@@ -459,6 +464,17 @@ export async function maybeCompact(opts) {
|
|
|
459
464
|
...(opts.activeTools !== undefined && opts.activeTools.length > 0 ? { activeTools: [...opts.activeTools] } : {}),
|
|
460
465
|
...(opts.gitRestate !== undefined ? { gitAnnouncement: { kind: opts.gitRestate.pending.kind, hash: opts.gitRestate.pending.hash, pending: true } } : {}),
|
|
461
466
|
}, false);
|
|
467
|
+
if (opts.onCommitted !== undefined) {
|
|
468
|
+
const firstKeptIdx = branch.findIndex((e) => e.id === firstKeptEntryId);
|
|
469
|
+
const effect = firstKeptIdx >= 0
|
|
470
|
+
? { summarizedEntryIds: new Set(branch.slice(0, firstKeptIdx).map((e) => e.id)) }
|
|
471
|
+
: { summarizedEntryIds: "unknown" };
|
|
472
|
+
try {
|
|
473
|
+
opts.onCommitted(effect);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
462
478
|
if (opts.gitRestate !== undefined) {
|
|
463
479
|
try {
|
|
464
480
|
await opts.gitRestate.land();
|
|
@@ -514,6 +530,7 @@ export async function maybeCompact(opts) {
|
|
|
514
530
|
}), "compaction.postCompact");
|
|
515
531
|
}
|
|
516
532
|
const endAt = Date.now();
|
|
533
|
+
const summarizedCrossSessionMessages = countSummarizedCrossSessionMessages(branch, firstKeptEntryId);
|
|
517
534
|
return {
|
|
518
535
|
contextUsage,
|
|
519
536
|
compacted: true,
|
|
@@ -522,6 +539,7 @@ export async function maybeCompact(opts) {
|
|
|
522
539
|
triggerTokens: tokens,
|
|
523
540
|
postTriggerTokens,
|
|
524
541
|
firstKeptEntryId,
|
|
542
|
+
...(summarizedCrossSessionMessages > 0 ? { summarizedCrossSessionMessages } : {}),
|
|
525
543
|
reused,
|
|
526
544
|
naturalTrigger,
|
|
527
545
|
durationMs: endAt - compactStartAt,
|
|
@@ -558,9 +576,24 @@ export function mintCompactedEvent(comp, trigger, identity) {
|
|
|
558
576
|
...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
|
|
559
577
|
...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
|
|
560
578
|
...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
|
|
579
|
+
...(comp.summarizedCrossSessionMessages !== undefined ? { summarizedCrossSessionMessages: comp.summarizedCrossSessionMessages } : {}),
|
|
561
580
|
...identity,
|
|
562
581
|
};
|
|
563
582
|
}
|
|
583
|
+
export function countSummarizedCrossSessionMessages(branch, firstKeptEntryId) {
|
|
584
|
+
const firstKeptIdx = branch.findIndex((e) => e.id === firstKeptEntryId);
|
|
585
|
+
const summarized = firstKeptIdx >= 0 ? branch.slice(0, firstKeptIdx) : branch;
|
|
586
|
+
let n = 0;
|
|
587
|
+
for (const e of summarized) {
|
|
588
|
+
if (e.type !== "message" || e.message === undefined || e.message.role !== "user" || e.message.provenance !== "engine-note")
|
|
589
|
+
continue;
|
|
590
|
+
const c = e.message.content;
|
|
591
|
+
const text = typeof c === "string" ? c : Array.isArray(c) ? c.map((b) => (b !== null && typeof b === "object" && b.type === "text" ? String(b.text ?? "") : "")).join("") : "";
|
|
592
|
+
if (text.startsWith(`<${CROSS_SESSION_MESSAGE_TAG} `) || text.startsWith(`<${CROSS_SESSION_MESSAGE_TAG}>`))
|
|
593
|
+
n += 1;
|
|
594
|
+
}
|
|
595
|
+
return n;
|
|
596
|
+
}
|
|
564
597
|
export function nextTrimForceBackoff(prev, comp, trimForced = false) {
|
|
565
598
|
if (trimForced && comp.naturalTrigger !== true && comp.compacted && comp.freedTokens !== undefined && comp.freedTokens <= 0) {
|
|
566
599
|
return true;
|
|
@@ -77,3 +77,14 @@ export interface ChildClampSource {
|
|
|
77
77
|
* — so a clamp can never end one level deep, and a new seat cannot reach some constructors and not others.
|
|
78
78
|
*/
|
|
79
79
|
export declare function childClampSeats(source: ChildClampSource): Partial<TaskSpec>;
|
|
80
|
+
/**
|
|
81
|
+
* The OWN posture of an engine-built leg that never asks the person (the observer sidecar, the verifier): no
|
|
82
|
+
* human-facing tool is mounted AND the leg declares itself headless. Both, always together — the spec's posture
|
|
83
|
+
* outranks the one a leg inherits from its parent or its deployment, and the prepare door refuses an
|
|
84
|
+
* "interactive" posture paired with `interactiveTools: false`, so a leg that stated only the clamp was refused
|
|
85
|
+
* under every interactive parent. Spread ABOVE {@link childClampSeats}: a caller clamp can only tighten it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function engineHeadlessLegSeats(): {
|
|
88
|
+
interactiveTools: false;
|
|
89
|
+
interactionPosture: "headless";
|
|
90
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "../internal/harness.js";
|
|
2
2
|
import type { Model } from "../internal/llm.js";
|
|
3
|
+
import { type ContextWindowOverlay } from "./context-settings.js";
|
|
3
4
|
/**
|
|
4
5
|
* Small-window FLOOR fraction of the clearStale/compaction frontier (see {@link contextEditFrontier},
|
|
5
6
|
* which is the single source for both the clearStale budget and the compaction-threshold clamp).
|
|
@@ -224,7 +225,7 @@ export interface TriggerWindowResolution {
|
|
|
224
225
|
* chain is the drift this function exists to end. Non-finite/≤0 declared values keep their historic
|
|
225
226
|
* meaning (each consumer's own unknown-window sentinel), deliberately untouched here.
|
|
226
227
|
*/
|
|
227
|
-
export declare function resolveTriggerWindow(model: Model): TriggerWindowResolution;
|
|
228
|
+
export declare function resolveTriggerWindow(model: Model, overlay?: ContextWindowOverlay): TriggerWindowResolution;
|
|
228
229
|
/** The token budget at which context-editing begins for a model (below the harder guard trim).
|
|
229
230
|
* = {@link contextEditFrontier} of the model's AUTOCOMPACT window (CC-parity fixed deduction with
|
|
230
231
|
* a 0.7 small-window floor) — the same point the compaction threshold is clamped to. Dual-window
|
|
@@ -232,7 +233,7 @@ export declare function resolveTriggerWindow(model: Model): TriggerWindowResolut
|
|
|
232
233
|
* trim (`guardBudget`) stays on the physical window — which is why the window is read through
|
|
233
234
|
* {@link resolveTriggerWindow}: an autocompact window declared ABOVE the physical one would put
|
|
234
235
|
* this frontier above the guard (inverted geometry), so it is clamped there (slice 1b, X5). */
|
|
235
|
-
export declare function editBudget(model: Model): number;
|
|
236
|
+
export declare function editBudget(model: Model, overlay?: ContextWindowOverlay): number;
|
|
236
237
|
/** Content fingerprint of an occurrence — a sha-256 digest over the FULL canonical content form
|
|
237
238
|
* (per block: type + complete text bytes, and for non-text blocks the mime type + payload length
|
|
238
239
|
* + a payload head), replay's mis-paint guard (adversarial rounds 1+2): ordinal mapping alone
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { DEFAULT_CHARS_PER_TOKEN, estimateTokens } from "../internal/harness.js";
|
|
3
3
|
import { isToolResult } from "./message-utils.js";
|
|
4
|
+
import { physicalWindowOf } from "./context-settings.js";
|
|
4
5
|
import { PERSISTED_OUTPUT_PREFIX, offloadPagebackHint } from "./tool-result-store.js";
|
|
5
6
|
import { assertNoRetiredKeys } from "./retired-keys.js";
|
|
6
7
|
import { compactableToolNames } from "./tool-registry.js";
|
|
@@ -179,9 +180,9 @@ export function clearStaleToolResults(messages, opts) {
|
|
|
179
180
|
reportCleared(opts.onCleared, clears, tokensSaved);
|
|
180
181
|
return out;
|
|
181
182
|
}
|
|
182
|
-
export function resolveTriggerWindow(model) {
|
|
183
|
-
const physical = model
|
|
184
|
-
const declared = model.autoCompactTokens;
|
|
183
|
+
export function resolveTriggerWindow(model, overlay) {
|
|
184
|
+
const physical = physicalWindowOf(model, overlay);
|
|
185
|
+
const declared = overlay?.autoCompactTokens ?? model.autoCompactTokens;
|
|
185
186
|
if (declared !== undefined &&
|
|
186
187
|
Number.isFinite(declared) &&
|
|
187
188
|
declared > 0 &&
|
|
@@ -192,8 +193,8 @@ export function resolveTriggerWindow(model) {
|
|
|
192
193
|
}
|
|
193
194
|
return { window: declared ?? physical, clamped: false };
|
|
194
195
|
}
|
|
195
|
-
export function editBudget(model) {
|
|
196
|
-
const window = resolveTriggerWindow(model).window;
|
|
196
|
+
export function editBudget(model, overlay) {
|
|
197
|
+
const window = resolveTriggerWindow(model, overlay).window;
|
|
197
198
|
if (!Number.isFinite(window) || window <= 0) {
|
|
198
199
|
return Number.POSITIVE_INFINITY;
|
|
199
200
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "../internal/harness.js";
|
|
2
2
|
import type { Model } from "../internal/llm.js";
|
|
3
|
+
import { type ContextWindowOverlay } from "./context-settings.js";
|
|
3
4
|
/**
|
|
4
5
|
* Last-resort, in-task context trim. Proper compaction (LLM summary) runs *between* tasks;
|
|
5
6
|
* this prevents a single task whose internal tool-loop balloons from overflowing the window.
|
|
@@ -99,4 +100,4 @@ charsPerToken?: number): GitFrameGuardOutcome;
|
|
|
99
100
|
* above the compaction/clearStale frontier. Dual-window semantics: deliberately the PHYSICAL
|
|
100
101
|
* window (`contextTokens ?? contextWindow`), NOT `autoCompactTokens` — the guard protects the
|
|
101
102
|
* real request limit; a lowered autocompact window only moves the trigger-side geometry. */
|
|
102
|
-
export declare function guardBudget(model: Model): number;
|
|
103
|
+
export declare function guardBudget(model: Model, overlay?: ContextWindowOverlay): number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "../internal/harness.js";
|
|
2
2
|
import { isToolResult, messageRole } from "./message-utils.js";
|
|
3
|
+
import { physicalWindowOf } from "./context-settings.js";
|
|
3
4
|
const GUARD_FRACTION = 0.85;
|
|
4
5
|
const GUARD_HEADROOM_TOKENS = 23000;
|
|
5
6
|
function isSummary(m) {
|
|
@@ -190,8 +191,8 @@ export function protectGitFrame(before, trimmed, budgetTokens, frame, charsPerTo
|
|
|
190
191
|
}
|
|
191
192
|
return { action: "over_budget" };
|
|
192
193
|
}
|
|
193
|
-
export function guardBudget(model) {
|
|
194
|
-
const window = model
|
|
194
|
+
export function guardBudget(model, overlay) {
|
|
195
|
+
const window = physicalWindowOf(model, overlay);
|
|
195
196
|
if (!Number.isFinite(window) || window <= 0) {
|
|
196
197
|
return Number.POSITIVE_INFINITY;
|
|
197
198
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment's per-model CONTEXT-WINDOW overlay — the one deployment seat an IN-FLIGHT leg re-reads.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT IS NOT ON THE MODEL. `Model.contextTokens` / `Model.autoCompactTokens` are two things at once: the
|
|
5
|
+
* geometry a leg measures itself against, and part of the immutable routing card a leg was handed at its
|
|
6
|
+
* prepare. The hot-swap law (`swappable-deps.ts`) is right about the card — nothing is re-routed, re-priced or
|
|
7
|
+
* re-fenced mid-run, because a running request must reach the endpoint it was priced and credentialed for.
|
|
8
|
+
* It was wrong about the geometry: an operator who lowers a model's usable window is not re-routing anything,
|
|
9
|
+
* they are changing a SETTING, and a leg that keeps filling the retired window until it ends is the one thing
|
|
10
|
+
* they were trying to prevent. The reference implementation keeps that setting off the model card for exactly
|
|
11
|
+
* this reason and re-reads it on every request, clamping it to the model's physical window.
|
|
12
|
+
*
|
|
13
|
+
* So the overlay lives here, keyed by CATALOG WORD — the same word `ModelRef` already speaks. A leg carries
|
|
14
|
+
* the ref that chose its model (`ResolvedRole.modelRef` → the harness → `ToolExecuteContext.modelRef`), so
|
|
15
|
+
* the lookup is a plain read with no reverse search: an id→word search cannot answer (one model is reachable
|
|
16
|
+
* under several words, two words may route to one id, and an object-pinned model has no word at all), and a
|
|
17
|
+
* leg that knows its own word needs no search.
|
|
18
|
+
*
|
|
19
|
+
* WHAT IT DOES NOT DO. It carries no routing, credential, price or fence field, and never will: those are the
|
|
20
|
+
* card, and the card stays snapshotted. An OBJECT-pinned leg (a spec that named a `Model`, a `select` criteria)
|
|
21
|
+
* has no word and therefore no overlay — the pin is the caller saying "this exact model, as declared".
|
|
22
|
+
*/
|
|
23
|
+
import type { Model } from "../internal/llm.js";
|
|
24
|
+
import type { ModelRef } from "./model-seat.js";
|
|
25
|
+
/**
|
|
26
|
+
* One model's overlay. Every member is a DECLARATION the deployment may change between requests; an absent
|
|
27
|
+
* member means "whatever the model card says", never a value.
|
|
28
|
+
* · `contextTokens` — the usable physical window (the model card's `contextTokens ?? contextWindow`).
|
|
29
|
+
* · `autoCompactTokens` — the trigger-side window (dual-window semantics: lowers the compaction trigger and
|
|
30
|
+
* the context-editing frontier without touching the physical request budget). Clamped to the physical
|
|
31
|
+
* window by the one resolver, exactly as a model-card declaration is.
|
|
32
|
+
* · `enabled` — compaction for legs on this model, the deployment's twin of `TaskSpec.compaction.enabled`
|
|
33
|
+
* and with the same reach (the routine boundary pass, the recovery pass and the end-of-task summary —
|
|
34
|
+
* one gate, inside `maybeCompact`, fed by one settings assembly). A request that states its own
|
|
35
|
+
* `compaction.enabled` outranks it: the request is the author, this seat is the deployment's default —
|
|
36
|
+
* the same "the spec's own value wins, the deployment fills an unset one" law every other seat follows.
|
|
37
|
+
*/
|
|
38
|
+
export interface ContextWindowOverlay {
|
|
39
|
+
readonly contextTokens?: number;
|
|
40
|
+
readonly autoCompactTokens?: number;
|
|
41
|
+
readonly enabled?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The seat: catalog word → overlay. Words absent from the table are "nothing declared".
|
|
45
|
+
*
|
|
46
|
+
* @contract context_settings.geometry_is_read_live — this is the ONE deployment seat a leg re-reads while it
|
|
47
|
+
* runs: every window number a leg is measured against (the compaction trigger, the sanitize clamp, the
|
|
48
|
+
* context-editing frontier, the request guard, the disclosed `contextUsage`) is taken from the overlay in
|
|
49
|
+
* force at the moment of THAT question, keyed by the word that chose the leg's current model, so a swap
|
|
50
|
+
* reaches boundaries of legs already in flight; the leg's ROUTING — endpoint, credentials, price, fence, the
|
|
51
|
+
* `Model` object itself — stays the generation the leg prepared under, and this seat can never carry any of
|
|
52
|
+
* it. A leg whose model was pinned as an object has no word and is never overlaid.
|
|
53
|
+
*/
|
|
54
|
+
export type ContextSettings = Readonly<Record<string, ContextWindowOverlay>>;
|
|
55
|
+
/**
|
|
56
|
+
* The overlay in force for a leg, read at the moment of the question. `ref` is the leg's own catalog word
|
|
57
|
+
* (`AgentHarness.getModelRef()` / `ResolvedRole.modelRef`); a `Model` OBJECT answers `undefined` because a
|
|
58
|
+
* pinned model has no word for the table to key on. OWN-property read: a word like `"constructor"` must mean
|
|
59
|
+
* "a key this deployment declared", never `Object.prototype`'s answer.
|
|
60
|
+
*
|
|
61
|
+
* The leg's own word first; when it declares nothing, the words it STANDS IN FOR — a tier word the default
|
|
62
|
+
* role answers with, a CC alias, `best` — as the tier expansion recorded them ({@link tierAliasVia}): only
|
|
63
|
+
* words that name the very model the leg's word resolves to, so a deployment key that a tier word passes by
|
|
64
|
+
* but that names ANOTHER model is never read. Nothing is found by searching for a model.
|
|
65
|
+
*/
|
|
66
|
+
export declare function contextOverlayFor(settings: ContextSettings | undefined, ref: ModelRef | undefined, catalog?: {
|
|
67
|
+
models?: Record<string, Model>;
|
|
68
|
+
}): ContextWindowOverlay | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Every word the overlay declares must be a word of the model catalog (the deployment's keys, the tier words
|
|
71
|
+
* and aliases its `tiers` expanded) — `config.context_settings_unknown_word`. A misspelled word keys a leg
|
|
72
|
+
* nobody can run, so it would govern nothing while reading as in force. Checked at construction and on every
|
|
73
|
+
* swap against the catalog the swap leaves in force, so a catalog swap that would orphan a declared word is
|
|
74
|
+
* refused too. Returns `settings`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function assertContextSettingsWords<T extends ContextSettings | undefined>(settings: T, models: Record<string, Model> | undefined, where: string): T;
|
|
77
|
+
/**
|
|
78
|
+
* The seat's VALUE GATE — the same one the hot-swap door and the construction path apply, so a value the door
|
|
79
|
+
* admits is a value every later read admits. A malformed overlay is refused LOUDLY rather than repaired
|
|
80
|
+
* quietly: a window silently dropped back to the model card's is the misconfiguration this seat exists to make
|
|
81
|
+
* visible. `undefined` (the seat cleared) passes through.
|
|
82
|
+
*
|
|
83
|
+
* `where` names the caller in the message (`"swapDeps"` / `"RunnerDeps"`), since the two paths are the two
|
|
84
|
+
* moments an operator can get it wrong.
|
|
85
|
+
*/
|
|
86
|
+
export declare function assertContextSettingsValue(v: unknown, where: string): ContextSettings | undefined;
|
|
87
|
+
/** Does a REQUEST's own `compaction.enabled` stand, or does the deployment's overlay decide? One law, stated
|
|
88
|
+
* once: the request is the author, the deployment fills an unset one, and nothing declared means ON. */
|
|
89
|
+
export declare function compactionEnabled(specEnabled: boolean | undefined, overlay: ContextWindowOverlay | undefined): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* The overlay's view of a model's PHYSICAL window — the model card's chain with the deployment's live
|
|
92
|
+
* declaration CLAMPED over it. The single expression `guardBudget` and `resolveTriggerWindow` share.
|
|
93
|
+
*
|
|
94
|
+
* The clamp is the whole difference between a setting and a claim. An overlay says how much of a model a
|
|
95
|
+
* deployment wants used; it cannot say what the endpoint will accept, and the endpoint a leg talks to is the
|
|
96
|
+
* one it prepared under. Letting a declaration RAISE the window would let an operator publishing a larger
|
|
97
|
+
* model and its larger window in one step hand the bigger number to legs still routed at the smaller
|
|
98
|
+
* endpoint — the guard would stop protecting the limit it exists for. So: lower freely, never raise. (Same
|
|
99
|
+
* clamp the reference implementation applies to its configured window: `min(model window, configured)`.)
|
|
100
|
+
* A card with no usable window of its own has nothing to clamp against, and the declaration is then the only
|
|
101
|
+
* number there is.
|
|
102
|
+
*/
|
|
103
|
+
export declare function physicalWindowOf(model: Model, overlay: ContextWindowOverlay | undefined): number;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { tierAliasVia } from "./roles.js";
|
|
2
|
+
export function contextOverlayFor(settings, ref, catalog) {
|
|
3
|
+
if (settings === undefined || typeof ref !== "string")
|
|
4
|
+
return undefined;
|
|
5
|
+
for (const word of [ref, ...tierAliasVia(ref, catalog?.models)])
|
|
6
|
+
if (Object.hasOwn(settings, word))
|
|
7
|
+
return settings[word];
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
export function assertContextSettingsWords(settings, models, where) {
|
|
11
|
+
const unknown = Object.keys(settings ?? {}).filter((w) => models === undefined || !Object.hasOwn(models, w));
|
|
12
|
+
if (unknown.length === 0)
|
|
13
|
+
return settings;
|
|
14
|
+
const e = new Error(`${where}: contextSettings declares ${unknown.map((w) => JSON.stringify(w)).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a word of the model catalog (its keys, and the tier words and aliases \`tiers\` expanded) — an overlay no leg can be keyed by governs nothing; the current settings stay in force`);
|
|
15
|
+
e.code = "config.context_settings_unknown_word";
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
export function assertContextSettingsValue(v, where) {
|
|
19
|
+
if (v === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) {
|
|
22
|
+
throw new Error(`${where}: contextSettings must be a plain Record<string, {contextTokens?, autoCompactTokens?, enabled?}> (got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v})`);
|
|
23
|
+
}
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [word, raw] of Object.entries(v)) {
|
|
26
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
27
|
+
throw new Error(`${where}: contextSettings[${JSON.stringify(word)}] must be a plain object (got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw})`);
|
|
28
|
+
}
|
|
29
|
+
const o = raw;
|
|
30
|
+
const unknown = Object.keys(o).filter((k) => k !== "contextTokens" && k !== "autoCompactTokens" && k !== "enabled");
|
|
31
|
+
if (unknown.length > 0) {
|
|
32
|
+
throw new Error(`${where}: contextSettings[${JSON.stringify(word)}] carries ${unknown.map((k) => JSON.stringify(k)).join(", ")} — this seat declares window geometry only (contextTokens | autoCompactTokens | enabled); routing, pricing and fencing stay on the model card`);
|
|
33
|
+
}
|
|
34
|
+
for (const k of ["contextTokens", "autoCompactTokens"]) {
|
|
35
|
+
const n = o[k];
|
|
36
|
+
if (n !== undefined && (typeof n !== "number" || !Number.isFinite(n) || n <= 0)) {
|
|
37
|
+
throw new Error(`${where}: contextSettings[${JSON.stringify(word)}].${k} must be a positive finite number (got ${JSON.stringify(n)}) — an unusable window is a configuration defect, not a request to fall back to the model's own`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (o.enabled !== undefined && typeof o.enabled !== "boolean") {
|
|
41
|
+
throw new Error(`${where}: contextSettings[${JSON.stringify(word)}].enabled must be a boolean (got ${JSON.stringify(o.enabled)})`);
|
|
42
|
+
}
|
|
43
|
+
out[word] = Object.freeze({
|
|
44
|
+
...(o.contextTokens !== undefined ? { contextTokens: o.contextTokens } : {}),
|
|
45
|
+
...(o.autoCompactTokens !== undefined ? { autoCompactTokens: o.autoCompactTokens } : {}),
|
|
46
|
+
...(o.enabled !== undefined ? { enabled: o.enabled } : {}),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze(out);
|
|
50
|
+
}
|
|
51
|
+
export function compactionEnabled(specEnabled, overlay) {
|
|
52
|
+
return specEnabled ?? overlay?.enabled ?? true;
|
|
53
|
+
}
|
|
54
|
+
export function physicalWindowOf(model, overlay) {
|
|
55
|
+
const card = model.contextTokens ?? model.contextWindow;
|
|
56
|
+
const declared = overlay?.contextTokens;
|
|
57
|
+
if (declared === undefined)
|
|
58
|
+
return card;
|
|
59
|
+
return Number.isFinite(card) && card > 0 ? Math.min(declared, card) : declared;
|
|
60
|
+
}
|
|
@@ -172,6 +172,16 @@ export interface EngineNotice {
|
|
|
172
172
|
* - `"config.write_face_swapped"` — the same line for the WRITE seat (`RunnerDeps.writeFace`),
|
|
173
173
|
* same payload, same laws. A SEPARATE code rather than a face field on the line above: the audience
|
|
174
174
|
* table and every consumer table are keyed by code.
|
|
175
|
+
* - `"config.context_settings_swapped"` — `Runner.swapDeps` replaced the deployment's per-model
|
|
176
|
+
* context-window overlay (`RunnerDeps.contextSettings`, the `contextSettings` seat of the swappable
|
|
177
|
+
* set) without a restart. Unlike every other swappable seat this one is read LIVE: a running leg
|
|
178
|
+
* measures its next boundary (the compaction trigger, the sanitize clamp, the request guard, the
|
|
179
|
+
* disclosed `contextUsage`) against the new windows, while its routing stays on the generation it
|
|
180
|
+
* prepared under. `detail: { words, declared, previousDeclared, occurrenceId }` — the declared model
|
|
181
|
+
* WORDS (the first eight) and the two counts, never the numbers: an overlay's values are deployment
|
|
182
|
+
* configuration and this line is an operator breadcrumb, not a config dump. A candidate outside the
|
|
183
|
+
* value gate (`assertContextSettingsValue`) throws atomically and mints nothing. Own `occurrenceId`
|
|
184
|
+
* for the same reason as the catalog line. Dedup key: (code, occurrenceId).
|
|
175
185
|
* - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
|
|
176
186
|
* DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
|
|
177
187
|
* the pairing pre-flight and the seat fell back to the primary model instead of sinking the
|
|
@@ -590,7 +600,7 @@ export interface EngineNotice {
|
|
|
590
600
|
* disposition table with the error dispositions. The loser MUST NOT retry into the winner's
|
|
591
601
|
* account. A consumer diffing its own table against the catalog must not add a row for it.
|
|
592
602
|
*
|
|
593
|
-
* - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.
|
|
603
|
+
* - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.peerLane` field the
|
|
594
604
|
* resolver could not use: outside its legal range, or not a finite number at all (a string off
|
|
595
605
|
* an untyped host's wiring, `NaN`, `Infinity`). THAT FIELD falls back to its own default (never
|
|
596
606
|
* a clamp to the nearest edge, never a whole-config reject) and every other field is unaffected.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** What ONE landed compaction did to the transcript, recorded at its commit point. */
|
|
2
|
+
export interface CompactionEffect {
|
|
3
|
+
/** The entry ids that went into the summarized prefix (everything before `firstKeptEntryId`), or
|
|
4
|
+
* `"unknown"` when the kept-tail anchor could not be located in the branch — the conservative
|
|
5
|
+
* reading is that the whole branch was summarized, so a reader treats `"unknown"` as "every
|
|
6
|
+
* receipt this run holds is invalidated" (the same direction
|
|
7
|
+
* {@link import("./auto-compaction.js").countSummarizedCrossSessionMessages} already takes). */
|
|
8
|
+
readonly summarizedEntryIds: ReadonlySet<string> | "unknown";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One receipt ASK: the carrier a frame rides, and — for a frame that rides a TOOL RESULT — the
|
|
12
|
+
* text {@link frameSurvivesInToolResult} must find in that result as committed. An injected frame is its
|
|
13
|
+
* own carrier (the committed message IS the frame), so it carries no text to check.
|
|
14
|
+
*/
|
|
15
|
+
export interface FrameReceiptAsk {
|
|
16
|
+
/** The injection queue's dedup key of the frame itself, or {@link toolResultReceiptKey} of the call whose result carries it. */
|
|
17
|
+
readonly carrier: string;
|
|
18
|
+
readonly frameText?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The leg's settlement seam, one cell per run.
|
|
22
|
+
*
|
|
23
|
+
* `awaiting` is an ASK, not a log: only keys a consumer registered are ever given a receipt, an ask
|
|
24
|
+
* leaves when its carrier commits, so a run that delivers thousands of ordinary notification frames
|
|
25
|
+
* does not accumulate one map entry per frame. The key is the consumer's per-frame key.
|
|
26
|
+
*/
|
|
27
|
+
export interface FrameSettlement {
|
|
28
|
+
/** Receipt asks a consumer is waiting on (key → ask). The receipt sink records nothing for any other key. */
|
|
29
|
+
readonly awaiting: Map<string, FrameReceiptAsk>;
|
|
30
|
+
/** Receipts collected for awaited keys: key → the transcript entry id the frame landed in. */
|
|
31
|
+
readonly receipts: Map<string, string>;
|
|
32
|
+
/** Awaited keys whose carrier COMMITTED WITHOUT the frame and nothing the HOST did explains it (an engine-side
|
|
33
|
+
* crop): shown nothing, so owed again — never a receipt. */
|
|
34
|
+
readonly withheld: Set<string>;
|
|
35
|
+
/** Carriers whose result a HOST hook rewrote (`postToolUse.updatedOutput`) before the commit — recorded only
|
|
36
|
+
* for a carrier some ask rides ({@link noteHostRewrite}), consumed by the receipt at its commit: a frame the
|
|
37
|
+
* rewrite kept gets an ordinary receipt, a frame it removed is host-withheld. */
|
|
38
|
+
readonly hostRewritten: Set<string>;
|
|
39
|
+
/** Awaited keys whose frame is not in the host's rewrite (removed, truncated, or reshaped beyond whitespace): the
|
|
40
|
+
* host decided what the model reads, so the consumer settles the record as the host's decision (terminal),
|
|
41
|
+
* discloses it loudly, and never hands it over again. */
|
|
42
|
+
readonly hostWithheld: Set<string>;
|
|
43
|
+
/** Compactions that LANDED this run and have not been consumed by a settlement pass yet, in landing
|
|
44
|
+
* order. Recorded only while {@link FrameSettlement.wantsEffects} says a record could still be
|
|
45
|
+
* broken by one, and cleared by the consumer's LOCAL settlement half — which needs no lease, so an
|
|
46
|
+
* unclaimable or empty box cannot leave them piling up for the whole leg. */
|
|
47
|
+
readonly compactions: CompactionEffect[];
|
|
48
|
+
/**
|
|
49
|
+
* #1036 — "is this engine frame's record still parked in a DURABLE box?" Set by the
|
|
50
|
+
* session-box drain when it binds. The terminal sweep hands back engine-note payloads that never
|
|
51
|
+
* drained and the runner pends them PER SESSION (an in-process copy); for a frame whose record the
|
|
52
|
+
* drain no longer acks on delivery, that copy would be a second source of truth — the box already
|
|
53
|
+
* re-serves it to the session's next run. Absent (no drain bound) ⇒ the pre-1036 behavior, which is
|
|
54
|
+
* correct there: nothing else holds those frames.
|
|
55
|
+
*/
|
|
56
|
+
boxBacked?: (payload: unknown) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* #1036 — "is there a record a landed compaction could still break?" Asked at the compaction
|
|
59
|
+
* COMMIT beat, before an effect is recorded at all. Absent (no drain bound) or `false` ⇒ the effect
|
|
60
|
+
* is not recorded: nothing would ever read it, and a leg that compacts many times would otherwise
|
|
61
|
+
* hold every landing's entry-id set until it ended.
|
|
62
|
+
*/
|
|
63
|
+
wantsEffects?: () => boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function createFrameSettlement(): FrameSettlement;
|
|
66
|
+
/** The carrier key of a TOOL RESULT (the `ReadInbox` pull face): its own domain, so it can never
|
|
67
|
+
* collide with an injection queue dedup key. One tool result can carry SEVERAL frames, each asked for
|
|
68
|
+
* under its own key with this carrier. */
|
|
69
|
+
export declare function toolResultReceiptKey(toolCallId: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* THE rule for a frame that rides a TOOL RESULT: the result can be replaced (`postToolUse.updatedOutput`)
|
|
72
|
+
* or cropped (the aggregate result budget) after the frame was handed to it, so handing it over proves
|
|
73
|
+
* nothing — the frame counts as delivered only when its text is present in the result AS COMMITTED
|
|
74
|
+
* (a string, or the text blocks, each read whole), compared with every whitespace run on both sides folded
|
|
75
|
+
* to one space: a rewrite that only re-spaces, re-indents or re-wraps the frame kept it. Any other change
|
|
76
|
+
* (a word, an escape, the case) did not. Before the commit, nothing is proven. The inbox receipt
|
|
77
|
+
* below reads it; the listing ride (`prepare-listings.ts`) does not call it — it never commits announced
|
|
78
|
+
* state at ride time, the conservative answer for a frame whose commit has not happened yet.
|
|
79
|
+
*/
|
|
80
|
+
export declare function frameSurvivesInToolResult(committedContent: unknown, frameText: string): boolean;
|
|
81
|
+
/** The station that applies a HOST hook's `updatedOutput` marks the carrier — only when an ask rides it, so an
|
|
82
|
+
* ordinary rewritten result leaves nothing behind. */
|
|
83
|
+
export declare function noteHostRewrite(seam: FrameSettlement, toolCallId: string): void;
|
|
84
|
+
/** The receipt sink's ONE writer: every ask riding `carrier` is answered — a receipt when its frame is in
|
|
85
|
+
* the committed content (or it is its own carrier); when it is not, host-withheld if a host hook rewrote the
|
|
86
|
+
* carrier (the host's decision), withheld otherwise (owed again). */
|
|
87
|
+
export declare function recordFrameReceipt(seam: FrameSettlement, carrier: string, committedContent: unknown, entryId: string): void;
|
|
88
|
+
/** The carrier a committed frame was riding, as the harness reports it at the transcript append — a tool
|
|
89
|
+
* result with the content it was committed with (after every result transformation). */
|
|
90
|
+
export type CommittedFrameCarrier = {
|
|
91
|
+
kind: "engine_note";
|
|
92
|
+
payload: unknown;
|
|
93
|
+
} | {
|
|
94
|
+
kind: "tool_result";
|
|
95
|
+
toolCallId: string;
|
|
96
|
+
content: unknown;
|
|
97
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function createFrameSettlement() {
|
|
2
|
+
return { awaiting: new Map(), receipts: new Map(), withheld: new Set(), hostRewritten: new Set(), hostWithheld: new Set(), compactions: [] };
|
|
3
|
+
}
|
|
4
|
+
export function toolResultReceiptKey(toolCallId) {
|
|
5
|
+
return `tool_result:${toolCallId}`;
|
|
6
|
+
}
|
|
7
|
+
const foldWhitespace = (text) => text.replace(/\s+/g, " ");
|
|
8
|
+
export function frameSurvivesInToolResult(committedContent, frameText) {
|
|
9
|
+
const frame = foldWhitespace(frameText);
|
|
10
|
+
if (typeof committedContent === "string")
|
|
11
|
+
return foldWhitespace(committedContent).includes(frame);
|
|
12
|
+
if (!Array.isArray(committedContent))
|
|
13
|
+
return false;
|
|
14
|
+
return committedContent.some((b) => b.type === "text" && typeof b.text === "string" && foldWhitespace(b.text).includes(frame));
|
|
15
|
+
}
|
|
16
|
+
export function noteHostRewrite(seam, toolCallId) {
|
|
17
|
+
const carrier = toolResultReceiptKey(toolCallId);
|
|
18
|
+
for (const ask of seam.awaiting.values()) {
|
|
19
|
+
if (ask.carrier === carrier) {
|
|
20
|
+
seam.hostRewritten.add(carrier);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function recordFrameReceipt(seam, carrier, committedContent, entryId) {
|
|
26
|
+
const hostRewrote = seam.hostRewritten.delete(carrier);
|
|
27
|
+
for (const [key, ask] of seam.awaiting) {
|
|
28
|
+
if (ask.carrier !== carrier)
|
|
29
|
+
continue;
|
|
30
|
+
seam.awaiting.delete(key);
|
|
31
|
+
if (ask.frameText === undefined || frameSurvivesInToolResult(committedContent, ask.frameText))
|
|
32
|
+
seam.receipts.set(key, entryId);
|
|
33
|
+
else
|
|
34
|
+
(hostRewrote ? seam.hostWithheld : seam.withheld).add(key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -119,7 +119,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
119
119
|
* the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
|
|
120
120
|
* so the catalog is not written a second time here and the two cannot drift.
|
|
121
121
|
*/
|
|
122
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
|
|
122
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
|
|
123
123
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
124
124
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
125
125
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -105,6 +105,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
105
105
|
"config.env_timeout_discarded",
|
|
106
106
|
"config.materialize_env_discarded",
|
|
107
107
|
"config.models_swapped",
|
|
108
|
+
"config.context_settings_swapped",
|
|
108
109
|
"config.read_face_swapped",
|
|
109
110
|
"config.read_face_deployment_clamped",
|
|
110
111
|
"config.write_face_swapped",
|
|
@@ -198,6 +199,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
198
199
|
"config.env_timeout_discarded": "operator",
|
|
199
200
|
"config.materialize_env_discarded": "operator",
|
|
200
201
|
"config.models_swapped": "operator",
|
|
202
|
+
"config.context_settings_swapped": "operator",
|
|
201
203
|
"config.read_face_swapped": "operator",
|
|
202
204
|
"config.read_face_deployment_clamped": "operator",
|
|
203
205
|
"config.write_face_swapped": "operator",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -540,16 +540,8 @@ export interface StopHookContext {
|
|
|
540
540
|
/** Consecutive blocks so far in this run (resets when a stop() call lets the run proceed). */
|
|
541
541
|
consecutiveBlocks: number;
|
|
542
542
|
/**
|
|
543
|
-
* A view of the conversation this hook is being asked to judge
|
|
544
|
-
*
|
|
545
|
-
* Until now a Stop hook received two counters and nothing else. That is enough for a hook that asks
|
|
546
|
-
* "have I blocked too often?", and structurally insufficient for the entire class of hooks that ask
|
|
547
|
-
* anything ABOUT THE RUN — which is what the prompt-shaped hooks a settings file can declare are for.
|
|
548
|
-
* Measured end to end downstream: the carrier was constructed, invoked, and returned without error, and
|
|
549
|
-
* the model answered "I don't have direct access to your file system…" — because nobody had given the
|
|
550
|
-
* evaluator the session. Every layer reported success and the feature could not work. A hook cannot
|
|
551
|
-
* judge a run it cannot see, so the gap belonged here rather than in any of the layers that were
|
|
552
|
-
* dutifully passing an empty view along.
|
|
543
|
+
* A view of the conversation this hook is being asked to judge — the counters alone cannot serve a
|
|
544
|
+
* hook that asks anything ABOUT THE RUN (the prompt-shaped hooks a settings file declares).
|
|
553
545
|
*
|
|
554
546
|
* LAZY on purpose: a hook that only reads the counters pays nothing, and a hook that needs the
|
|
555
547
|
* conversation gets it in full rather than through a truncation policy the engine would have to invent
|
|
@@ -559,6 +551,14 @@ export interface StopHookContext {
|
|
|
559
551
|
* Absent when the runner cannot supply one (a stop gate wired without a session).
|
|
560
552
|
*/
|
|
561
553
|
getBranch?: () => Promise<readonly SessionTreeEntry[]>;
|
|
554
|
+
/** The run's background tasks still in flight (pending / running / parked under this run's owner triple — the
|
|
555
|
+
* post-compaction reminder's projection; CC `SubagentStop.background_tasks`). LAZY like {@link getBranch};
|
|
556
|
+
* `[]` = none in that projection, ABSENT = no view. A parked child here waits on an approval out of band. */
|
|
557
|
+
backgroundTasks?: () => ReadonlyArray<{
|
|
558
|
+
id: string;
|
|
559
|
+
description?: string;
|
|
560
|
+
status: string;
|
|
561
|
+
}>;
|
|
562
562
|
/** {@link HookSeatSignal} — this invocation's own abort signal. */
|
|
563
563
|
signal?: AbortSignal;
|
|
564
564
|
}
|