@sema-agent/core 7.28.1 → 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 +473 -1
- 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/brain/anthropic.js +4 -2
- package/dist/brain/open-responses.js +4 -3
- package/dist/brain/openai.js +2 -1
- package/dist/brain/stream-engine.js +6 -0
- package/dist/brain/stream-shared.d.ts +8 -0
- package/dist/brain/stream-shared.js +8 -0
- 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 +18 -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 +4 -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/message-utils.d.ts +13 -0
- package/dist/core/message-utils.js +6 -0
- 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 +3 -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 +61 -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 +2 -2
- 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/response-model-seat.d.ts +16 -0
- package/dist/core/runner/response-model-seat.js +27 -0
- 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 +9 -2
- 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 +33 -2
- package/dist/core/runner-deps.d.ts +44 -13
- package/dist/core/side-query.d.ts +6 -5
- package/dist/core/side-query.js +3 -1
- package/dist/core/swappable-deps.d.ts +20 -4
- package/dist/core/swappable-deps.js +6 -1
- package/dist/core/task-event.d.ts +13 -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 +27 -3
- 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.29.0.json +185 -0
- package/wire-changes/core@7.30.0.json +1821 -0
|
@@ -40,3 +40,11 @@ export function createRepetitionPoll() {
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
export function noteResponseModel(partial, reported, requestedId) {
|
|
44
|
+
if (typeof reported !== "string" || reported === "")
|
|
45
|
+
return;
|
|
46
|
+
if (reported === requestedId)
|
|
47
|
+
delete partial.responseModel;
|
|
48
|
+
else
|
|
49
|
+
partial.responseModel = reported;
|
|
50
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type AgentMessage, type CompactionSettings, type Session, type ThinkingLevel } from "../internal/harness.js";
|
|
2
2
|
import type { Message, Model, Tool } from "../internal/llm.js";
|
|
3
|
+
import type { ContextWindowOverlay } from "./context-settings.js";
|
|
3
4
|
import type { Hooks } from "./hooks.js";
|
|
4
5
|
import type { TraceEvent } from "./trace.js";
|
|
5
6
|
import type { Brain } from "./types.js";
|
|
6
7
|
import type { TaskEvent, TaskEventIdentity } from "./task-event.js";
|
|
8
|
+
import type { CompactionEffect } from "./frame-settlement.js";
|
|
7
9
|
/**
|
|
8
10
|
* Default summarization focus, applied when a task does not provide `compaction.instructions`.
|
|
9
11
|
* Addresses the common fidelity failure where summaries drop specific facts.
|
|
@@ -128,6 +130,12 @@ export interface MaybeCompactOptions {
|
|
|
128
130
|
};
|
|
129
131
|
/** Model whose context window bounds the budget (the model that runs the next turn). */
|
|
130
132
|
model: Model;
|
|
133
|
+
/** The deployment's LIVE per-model window overlay for THIS pass ({@link RunnerDeps.contextSettings}
|
|
134
|
+
* keyed by the leg's catalog word). Read by the caller at the moment of the pass, never snapshotted:
|
|
135
|
+
* every window number below — the trigger, the sanitize clamp, the disclosed `contextUsage` — is
|
|
136
|
+
* measured through it, so an operator who lowers a model's window reaches the legs already running.
|
|
137
|
+
* Absent ⇒ the model card's own declarations, unchanged. */
|
|
138
|
+
contextOverlay?: ContextWindowOverlay;
|
|
131
139
|
/** Optional cheaper model used to produce the summary. Falls back to `model`.
|
|
132
140
|
* design/145 §1 (semantics精确化): used only while WINDOW-SAFE and the budget gate allows — when a
|
|
133
141
|
* dry-run clamp shows the conversation would lose more than `settings.clampTolerance` of its
|
|
@@ -352,6 +360,23 @@ export interface MaybeCompactOptions {
|
|
|
352
360
|
path: string;
|
|
353
361
|
content: string;
|
|
354
362
|
}>, preserveReadState?: ReadonlyArray<string>) => void;
|
|
363
|
+
/**
|
|
364
|
+
* #1036 — THE COMMIT-BEAT EFFECT SINK: what this compaction did to the transcript, reported
|
|
365
|
+
* once, right after the `appendCompaction` that landed it. `summarizedEntryIds` = every entry id
|
|
366
|
+
* that fell into the summarized prefix (before `firstKeptEntryId`); `"unknown"` when the kept-tail
|
|
367
|
+
* anchor is not in the branch — the conservative reading (the same one
|
|
368
|
+
* {@link countSummarizedCrossSessionMessages} already takes) is that the whole branch went into
|
|
369
|
+
* the summary.
|
|
370
|
+
*
|
|
371
|
+
* Why HERE and not at the three call sites: a fact about a landed compaction has exactly one
|
|
372
|
+
* birthplace, and an effect minted per lane would be a rule written three times (and, the two
|
|
373
|
+
* times someone forgets, a lane where a summarized frame is silently never repaired). The three
|
|
374
|
+
* lanes reach it through the options assembly they already share.
|
|
375
|
+
*
|
|
376
|
+
* Observe-only, like every post-landing side effect here: a throwing sink must not fail a landed
|
|
377
|
+
* compaction.
|
|
378
|
+
*/
|
|
379
|
+
onCommitted?: (effect: CompactionEffect) => void;
|
|
355
380
|
/**
|
|
356
381
|
* env-tail migration — the git-status lane's compaction interaction (two-phase receipt). The git
|
|
357
382
|
* frame lives on the MESSAGE plane, does not enter the summary, and the projection strips it from
|
|
@@ -657,6 +682,8 @@ export interface CompactedFrameSource {
|
|
|
657
682
|
fallbackReason?: "window";
|
|
658
683
|
clampedRatio?: number;
|
|
659
684
|
clampReason?: "budget" | "tolerance";
|
|
685
|
+
/** Cross-session message frames that sat in the summarized prefix (present iff ≥ 1). */
|
|
686
|
+
summarizedCrossSessionMessages?: number;
|
|
660
687
|
}
|
|
661
688
|
/**
|
|
662
689
|
* THE mint of the `compacted` wire frame — one function, three lanes (the turn boundary, the
|
|
@@ -673,6 +700,21 @@ export interface CompactedFrameSource {
|
|
|
673
700
|
export declare function mintCompactedEvent(comp: CompactedFrameSource, trigger: "auto" | "manual" | "forced", identity: TaskEventIdentity): Extract<TaskEvent, {
|
|
674
701
|
type: "compacted";
|
|
675
702
|
}>;
|
|
703
|
+
/**
|
|
704
|
+
* How many cross-session message frames the summarized prefix held: the engine-note user messages
|
|
705
|
+
* whose text is the drain's `<cross-session-message …>` envelope, before the kept tail begins. The
|
|
706
|
+
* frames were delivered (acked from the box) and are now only whatever the summary kept of them — the
|
|
707
|
+
* count is what lets a consumer say so; `0` is reported as absence.
|
|
708
|
+
*/
|
|
709
|
+
export declare function countSummarizedCrossSessionMessages(branch: ReadonlyArray<{
|
|
710
|
+
type: string;
|
|
711
|
+
id: string;
|
|
712
|
+
message?: {
|
|
713
|
+
role: string;
|
|
714
|
+
provenance?: string;
|
|
715
|
+
content: unknown;
|
|
716
|
+
};
|
|
717
|
+
}>, firstKeptEntryId: string): number;
|
|
676
718
|
/**
|
|
677
719
|
* design/123 D4 (r5) — trim-force BACKOFF transition (pure; the run-scoped bit lives in the caller).
|
|
678
720
|
* Spiral protection replacing the §25.2 floor's role on the trim-force path: trim pressure re-arms
|
|
@@ -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
|
|
@@ -222,6 +232,13 @@ export interface EngineNotice {
|
|
|
222
232
|
* occurrenceId }` (rendered list bounded, total always exact); the `occurrenceId` is minted at this
|
|
223
233
|
* announcement, independently of the `config.models_swapped` line's own, because the bounded render
|
|
224
234
|
* makes two swaps moving the same first entries identical. Dedup key: (code, occurrenceId).
|
|
235
|
+
* - `"route.response_model_mismatch"` — the provider answered a turn naming a model that is NOT the id
|
|
236
|
+
* the request was sent with (an alias, a pool or a gateway-side substitution looks exactly like this),
|
|
237
|
+
* so it is advisory, never a refusal: the engine keeps asking for the catalog's id and reports the
|
|
238
|
+
* served name beside it (`turn_end.responseModel` / `TaskResult.responseModel`). ONCE PER LEG PER
|
|
239
|
+
* (requested, reported) PAIR — a catalog fact, not a turn fact. Audience `operator`;
|
|
240
|
+
* `detail: { requested, reported, sessionId, runId }`. Absent ⇒ the provider echoed the requested id
|
|
241
|
+
* or named nothing — one reading, not two.
|
|
225
242
|
* - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
|
|
226
243
|
* in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
|
|
227
244
|
* (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
|
|
@@ -583,7 +600,7 @@ export interface EngineNotice {
|
|
|
583
600
|
* disposition table with the error dispositions. The loser MUST NOT retry into the winner's
|
|
584
601
|
* account. A consumer diffing its own table against the catalog must not add a row for it.
|
|
585
602
|
*
|
|
586
|
-
* - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.
|
|
603
|
+
* - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.peerLane` field the
|
|
587
604
|
* resolver could not use: outside its legal range, or not a finite number at all (a string off
|
|
588
605
|
* an untyped host's wiring, `NaN`, `Infinity`). THAT FIELD falls back to its own default (never
|
|
589
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
|
+
}
|