tinker-agent 1.0.65

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.
Files changed (110) hide show
  1. package/README.md +173 -0
  2. package/package.json +78 -0
  3. package/patches/markdansi@0.3.2.patch +37 -0
  4. package/src/agent/context-builder.ts +43 -0
  5. package/src/agent/context-meter.ts +310 -0
  6. package/src/agent/loop.ts +525 -0
  7. package/src/agent/runtime-session.ts +1212 -0
  8. package/src/agent/session-ledger.ts +828 -0
  9. package/src/agent/turn-cancellation.ts +44 -0
  10. package/src/agent/types.ts +77 -0
  11. package/src/cli/config.ts +283 -0
  12. package/src/cli/index.ts +29 -0
  13. package/src/cli/model-profiles.ts +289 -0
  14. package/src/cli/run-runner.ts +107 -0
  15. package/src/cli/tui-runner.tsx +290 -0
  16. package/src/context/compiled-context-hash.ts +138 -0
  17. package/src/context/compiled-context-validator.ts +209 -0
  18. package/src/context/context-manager.ts +362 -0
  19. package/src/context/context-policy.ts +8 -0
  20. package/src/context/context-protocol-validator.ts +463 -0
  21. package/src/context/context-revision-compiler.ts +281 -0
  22. package/src/context/context-revision.ts +111 -0
  23. package/src/context/context-source.ts +30 -0
  24. package/src/context/context-swap-renderer.ts +272 -0
  25. package/src/context/protocol-frame.ts +240 -0
  26. package/src/context/swap-planner.ts +725 -0
  27. package/src/events/append-private-file.ts +16 -0
  28. package/src/events/bash-result-detail.ts +70 -0
  29. package/src/events/composite-event-sink.ts +82 -0
  30. package/src/events/event-sink.ts +16 -0
  31. package/src/events/jsonl-event-log.ts +13 -0
  32. package/src/events/observation-text-log.ts +195 -0
  33. package/src/events/stdout-event-printer.ts +396 -0
  34. package/src/events/types.ts +263 -0
  35. package/src/ids/runtime-id.ts +68 -0
  36. package/src/ids/uuid-v7.ts +5 -0
  37. package/src/instructions/project-instructions.ts +242 -0
  38. package/src/mcp/mcp-config.ts +144 -0
  39. package/src/mcp/mcp-manager.ts +216 -0
  40. package/src/mcp/mcp-tool-executor.ts +178 -0
  41. package/src/model/committed-prefix-auditor.ts +68 -0
  42. package/src/model/fake-model-client.ts +280 -0
  43. package/src/model/model-client.ts +64 -0
  44. package/src/model/model-context-profile.ts +134 -0
  45. package/src/model/model-request-preflight.ts +120 -0
  46. package/src/model/openai-chat-mapping.ts +444 -0
  47. package/src/model/openai-chat-model-client.ts +190 -0
  48. package/src/model/prompt-prefix-hash.ts +47 -0
  49. package/src/model/token-estimator.ts +148 -0
  50. package/src/observation/observation-builder.ts +481 -0
  51. package/src/session/resume-projection.ts +616 -0
  52. package/src/session/session-catalog.ts +270 -0
  53. package/src/session/session-errors.ts +121 -0
  54. package/src/session/session-history-reader.ts +535 -0
  55. package/src/session/session-lock.ts +291 -0
  56. package/src/session/session-schema.ts +741 -0
  57. package/src/session/session-store.ts +3067 -0
  58. package/src/session/sqlite-session-ledger.ts +153 -0
  59. package/src/tools/bash-task.ts +617 -0
  60. package/src/tools/bash.ts +450 -0
  61. package/src/tools/cwd-state.ts +22 -0
  62. package/src/tools/edit.ts +428 -0
  63. package/src/tools/file-diff.ts +116 -0
  64. package/src/tools/glob.ts +202 -0
  65. package/src/tools/grep.ts +550 -0
  66. package/src/tools/hash.ts +9 -0
  67. package/src/tools/path-safety.ts +33 -0
  68. package/src/tools/read.ts +319 -0
  69. package/src/tools/recall.ts +400 -0
  70. package/src/tools/registry.ts +213 -0
  71. package/src/tools/ripgrep.ts +220 -0
  72. package/src/tools/task-list.ts +59 -0
  73. package/src/tools/task-output-snapshot.ts +47 -0
  74. package/src/tools/task-output-tool.ts +62 -0
  75. package/src/tools/task-output.ts +159 -0
  76. package/src/tools/task-stop.ts +59 -0
  77. package/src/tools/task-tool-args.ts +29 -0
  78. package/src/tools/types.ts +330 -0
  79. package/src/tools/web-fetch/backend.ts +27 -0
  80. package/src/tools/web-fetch/browser-backend.ts +126 -0
  81. package/src/tools/web-fetch/exa-backend.ts +172 -0
  82. package/src/tools/web-fetch/index.ts +298 -0
  83. package/src/tools/web-fetch/local-backend.ts +267 -0
  84. package/src/tools/web-fetch/refiner.ts +78 -0
  85. package/src/tools/web-fetch/route.ts +95 -0
  86. package/src/tools/web-search.ts +300 -0
  87. package/src/tools/write.ts +244 -0
  88. package/src/tui/app.tsx +497 -0
  89. package/src/tui/components/assistant-markdown.tsx +47 -0
  90. package/src/tui/components/background-tasks.tsx +92 -0
  91. package/src/tui/components/bash-result-view.tsx +47 -0
  92. package/src/tui/components/context-status.tsx +127 -0
  93. package/src/tui/components/diff-view.tsx +151 -0
  94. package/src/tui/components/file-viewer.tsx +212 -0
  95. package/src/tui/components/footer.tsx +60 -0
  96. package/src/tui/components/header.tsx +21 -0
  97. package/src/tui/components/model-picker.tsx +142 -0
  98. package/src/tui/components/prompt-input.tsx +432 -0
  99. package/src/tui/components/resume-session-picker.tsx +273 -0
  100. package/src/tui/components/timeline.tsx +121 -0
  101. package/src/tui/context-format.ts +24 -0
  102. package/src/tui/event-store.ts +865 -0
  103. package/src/tui/git-branch.ts +23 -0
  104. package/src/tui/line-editor.ts +157 -0
  105. package/src/tui/prompt-history.ts +94 -0
  106. package/src/tui/slash-commands.ts +126 -0
  107. package/src/tui/tui-projection-policy.ts +35 -0
  108. package/src/tui/tui-projection-store.ts +123 -0
  109. package/src/tui/tui-session-controller.ts +170 -0
  110. package/src/tui/view-file.ts +122 -0
@@ -0,0 +1,138 @@
1
+ import { sha256, stableJsonStringify } from "../model/model-request-preflight";
2
+ import type { CompiledContextEntry, SwapOverride } from "./context-revision";
3
+ import {
4
+ contentHash,
5
+ materializeAgentMessages,
6
+ type ProtocolContextView,
7
+ } from "./protocol-frame";
8
+
9
+ export function canonicalSequenceHash(
10
+ canonical: ProtocolContextView,
11
+ throughOrdinal = canonical.messages.length,
12
+ ): string {
13
+ return sha256(
14
+ stableJsonStringify({
15
+ sessionId: canonical.sessionId,
16
+ frames: canonical.frames
17
+ .filter((frame) => frame.firstOrdinal <= throughOrdinal)
18
+ .map((frame) => ({
19
+ frameId: frame.frameId,
20
+ kind: frame.kind,
21
+ state: frame.state,
22
+ firstOrdinal: frame.firstOrdinal,
23
+ lastOrdinal: frame.lastOrdinal,
24
+ })),
25
+ messages: canonical.messages
26
+ .filter((message) => message.ordinal <= throughOrdinal)
27
+ .map((message) => ({
28
+ frameId: message.frameId,
29
+ messageId: message.messageId,
30
+ ordinal: message.ordinal,
31
+ role: message.role,
32
+ contentSha256: message.contentSha256,
33
+ })),
34
+ }),
35
+ );
36
+ }
37
+
38
+ export function renderedMessageHash(
39
+ entries: readonly CompiledContextEntry[],
40
+ throughOrdinal = Number.POSITIVE_INFINITY,
41
+ ): string {
42
+ return sha256(
43
+ stableJsonStringify(
44
+ entries
45
+ .filter((entry) => entry.ordinal <= throughOrdinal)
46
+ .map((entry) => ({
47
+ frameId: entry.frameId,
48
+ messageId: entry.messageId,
49
+ ordinal: entry.ordinal,
50
+ representation: entry.representation,
51
+ sourceContentSha256: entry.sourceContentSha256,
52
+ rendered: renderedMessageDescriptor(entry),
53
+ })),
54
+ ),
55
+ );
56
+ }
57
+
58
+ export function canonicalRenderedMessageHash(
59
+ canonical: ProtocolContextView,
60
+ throughOrdinal = canonical.messages.length,
61
+ ): string {
62
+ const messages = materializeAgentMessages(canonical.messages);
63
+ const entries = canonical.messages.map((record, index) => {
64
+ const message = messages[index];
65
+ if (message === undefined) {
66
+ throw new Error(`Missing materialized message at index ${index}.`);
67
+ }
68
+ return {
69
+ frameId: record.frameId,
70
+ messageId: record.messageId,
71
+ ordinal: record.ordinal,
72
+ representation: "canonical" as const,
73
+ sourceContentSha256: record.contentSha256,
74
+ message,
75
+ };
76
+ });
77
+ return renderedMessageHash(entries, throughOrdinal);
78
+ }
79
+
80
+ export function activeOverrideManifestHash(overrides: readonly SwapOverride[]): string {
81
+ return sha256(
82
+ stableJsonStringify(
83
+ [...overrides]
84
+ .sort(
85
+ (left, right) =>
86
+ left.ordinal - right.ordinal ||
87
+ left.messageId.localeCompare(right.messageId),
88
+ )
89
+ .map((override) => ({
90
+ messageId: override.messageId,
91
+ frameId: override.frameId,
92
+ ordinal: override.ordinal,
93
+ originalContentSha256: override.originalContentSha256,
94
+ renderedContentSha256: override.renderedContentSha256,
95
+ rendererFormat: "swap-observation-v1",
96
+ })),
97
+ ),
98
+ );
99
+ }
100
+
101
+ function renderedMessageDescriptor(entry: CompiledContextEntry): unknown {
102
+ const message = entry.message;
103
+ switch (message.role) {
104
+ case "system":
105
+ case "user":
106
+ return {
107
+ role: message.role,
108
+ contentSha256:
109
+ entry.representation === "canonical"
110
+ ? entry.sourceContentSha256
111
+ : contentHash(message.content),
112
+ };
113
+ case "assistant":
114
+ return {
115
+ role: message.role,
116
+ contentSha256: entry.sourceContentSha256,
117
+ reasoningContentSha256:
118
+ message.reasoningContent === undefined
119
+ ? undefined
120
+ : contentHash(message.reasoningContent ?? null),
121
+ toolCallsSha256:
122
+ message.toolCalls === undefined
123
+ ? undefined
124
+ : sha256(stableJsonStringify(message.toolCalls)),
125
+ };
126
+ case "tool":
127
+ return {
128
+ role: message.role,
129
+ toolCallId: message.toolCallId,
130
+ providerToolCallId: message.providerToolCallId,
131
+ name: message.name,
132
+ contentSha256:
133
+ entry.representation === "canonical"
134
+ ? entry.sourceContentSha256
135
+ : contentHash(message.content),
136
+ };
137
+ }
138
+ }
@@ -0,0 +1,209 @@
1
+ import type { AgentMessage } from "../agent/types";
2
+ import { stableJsonStringify } from "../model/model-request-preflight";
3
+ import { formatMessageSource } from "./context-source";
4
+ import type { CompiledRevisionContext, SwapOverride } from "./context-revision";
5
+ import {
6
+ contentHash,
7
+ type CanonicalMessageRecord,
8
+ type ProtocolContextView,
9
+ } from "./protocol-frame";
10
+ import { canonicalSequenceHash, renderedMessageHash } from "./compiled-context-hash";
11
+
12
+ export class CompiledContextError extends Error {
13
+ constructor(message: string) {
14
+ super(message);
15
+ this.name = "CompiledContextError";
16
+ }
17
+ }
18
+
19
+ export class CompiledContextValidator {
20
+ validateActive(
21
+ compiled: CompiledRevisionContext,
22
+ canonical: ProtocolContextView,
23
+ overrides: readonly SwapOverride[],
24
+ ): void {
25
+ const byMessageId = overrideMap(overrides, "Active");
26
+ this.validate(compiled, canonical, byMessageId);
27
+ }
28
+
29
+ validateProspective(
30
+ compiled: CompiledRevisionContext,
31
+ canonical: ProtocolContextView,
32
+ overrides: readonly SwapOverride[],
33
+ ): void {
34
+ const byMessageId = overrideMap(overrides, "Prospective");
35
+ this.validate(compiled, canonical, byMessageId);
36
+ }
37
+
38
+ private validate(
39
+ compiled: CompiledRevisionContext,
40
+ canonical: ProtocolContextView,
41
+ overrides: ReadonlyMap<string, SwapOverride>,
42
+ ): void {
43
+ if (compiled.sessionId !== canonical.sessionId) {
44
+ fail("Compiled context belongs to another session.");
45
+ }
46
+ if (compiled.entries.length !== canonical.messages.length) {
47
+ fail("Compiled context changed the canonical message count.");
48
+ }
49
+ if (
50
+ compiled.canonicalThroughOrdinal !== canonical.messages.length ||
51
+ compiled.manifest.frameCount !== canonical.frames.length ||
52
+ compiled.manifest.messageCount !== compiled.entries.length
53
+ ) {
54
+ fail("Compiled context manifest counts do not match canonical history.");
55
+ }
56
+
57
+ const seenFrames = new Set<string>();
58
+ const seenMessages = new Set<string>();
59
+ const seenOrdinals = new Set<number>();
60
+ const consumedOverrides = new Set<string>();
61
+
62
+ for (let index = 0; index < compiled.entries.length; index += 1) {
63
+ const entry = requireItem(compiled.entries, index, "compiled entry");
64
+ const record = requireItem(canonical.messages, index, "canonical message");
65
+ if (
66
+ entry.frameId !== record.frameId ||
67
+ entry.messageId !== record.messageId ||
68
+ entry.ordinal !== record.ordinal ||
69
+ entry.sourceContentSha256 !== record.contentSha256
70
+ ) {
71
+ fail(`Compiled entry at ordinal ${record.ordinal} changed canonical identity.`);
72
+ }
73
+ if (seenMessages.has(entry.messageId) || seenOrdinals.has(entry.ordinal)) {
74
+ fail(`Compiled entry identity is duplicated at ordinal ${entry.ordinal}.`);
75
+ }
76
+ seenFrames.add(entry.frameId);
77
+ seenMessages.add(entry.messageId);
78
+ seenOrdinals.add(entry.ordinal);
79
+
80
+ const override = overrides.get(entry.messageId);
81
+ if (override === undefined) {
82
+ if (entry.representation !== "canonical") {
83
+ fail(`Unapproved swapped entry at ordinal ${entry.ordinal}.`);
84
+ }
85
+ assertSameMessage(entry.message, record);
86
+ continue;
87
+ }
88
+
89
+ consumedOverrides.add(entry.messageId);
90
+ validateOverrideIdentity(override, record);
91
+ if (record.role !== "tool" || entry.message.role !== "tool") {
92
+ fail(`Only tool messages may be swapped at ordinal ${entry.ordinal}.`);
93
+ }
94
+ if (
95
+ entry.representation !== "swapped" ||
96
+ entry.message.toolCallId !== record.toolCallId ||
97
+ entry.message.providerToolCallId !== record.providerToolCallId ||
98
+ entry.message.name !== record.name ||
99
+ entry.message.content !== override.renderedContent
100
+ ) {
101
+ fail(
102
+ `Swapped tool entry changed protocol identity at ordinal ${entry.ordinal}.`,
103
+ );
104
+ }
105
+ }
106
+
107
+ if (seenFrames.size !== canonical.frames.length) {
108
+ fail("Compiled entries do not preserve every protocol frame.");
109
+ }
110
+ if (consumedOverrides.size !== overrides.size) {
111
+ fail("A prospective override does not reference canonical history.");
112
+ }
113
+ if (
114
+ compiled.manifest.canonicalSequenceHash !== canonicalSequenceHash(canonical) ||
115
+ compiled.manifest.renderedMessageHash !== renderedMessageHash(compiled.entries)
116
+ ) {
117
+ fail("Compiled context manifest hash does not match its entries.");
118
+ }
119
+ }
120
+ }
121
+
122
+ function validateOverrideIdentity(
123
+ override: SwapOverride,
124
+ record: CanonicalMessageRecord,
125
+ ): void {
126
+ if (
127
+ override.frameId !== record.frameId ||
128
+ override.messageId !== record.messageId ||
129
+ override.ordinal !== record.ordinal ||
130
+ override.originalContentSha256 !== record.contentSha256
131
+ ) {
132
+ fail(`Swap override changed canonical identity at ordinal ${record.ordinal}.`);
133
+ }
134
+ const originalBytes = Buffer.byteLength(
135
+ record.role === "assistant" ? (record.content ?? "") : record.content,
136
+ "utf8",
137
+ );
138
+ const renderedBytes = Buffer.byteLength(override.renderedContent, "utf8");
139
+ if (
140
+ override.source !== formatMessageSource(record.messageId) ||
141
+ override.renderedContentSha256 !== contentHash(override.renderedContent) ||
142
+ override.originalBytes !== originalBytes ||
143
+ override.renderedBytes !== renderedBytes ||
144
+ override.byteSavings !== originalBytes - renderedBytes ||
145
+ override.byteSavings <= 0
146
+ ) {
147
+ fail(`Swap override metadata is invalid at ordinal ${record.ordinal}.`);
148
+ }
149
+ }
150
+
151
+ function overrideMap(
152
+ overrides: readonly SwapOverride[],
153
+ label: string,
154
+ ): ReadonlyMap<string, SwapOverride> {
155
+ const byMessageId = new Map(
156
+ overrides.map((override) => [override.messageId, override] as const),
157
+ );
158
+ if (byMessageId.size !== overrides.length) {
159
+ fail(`${label} compiled context contains duplicate overrides.`);
160
+ }
161
+ return byMessageId;
162
+ }
163
+
164
+ function assertSameMessage(actual: AgentMessage, record: CanonicalMessageRecord): void {
165
+ if (actual.role !== record.role) {
166
+ fail(`Canonical entry changed message role at ordinal ${record.ordinal}.`);
167
+ }
168
+ switch (record.role) {
169
+ case "system":
170
+ case "user":
171
+ if (actual.role !== record.role || actual.content !== record.content) {
172
+ fail(`Canonical entry changed message content at ordinal ${record.ordinal}.`);
173
+ }
174
+ return;
175
+ case "assistant":
176
+ if (
177
+ actual.role !== "assistant" ||
178
+ actual.content !== record.content ||
179
+ actual.reasoningContent !== record.reasoningContent ||
180
+ stableJsonStringify(actual.toolCalls ?? null) !==
181
+ stableJsonStringify(record.toolCalls ?? null)
182
+ ) {
183
+ fail(`Canonical entry changed assistant data at ordinal ${record.ordinal}.`);
184
+ }
185
+ return;
186
+ case "tool":
187
+ if (
188
+ actual.role !== "tool" ||
189
+ actual.toolCallId !== record.toolCallId ||
190
+ actual.providerToolCallId !== record.providerToolCallId ||
191
+ actual.name !== record.name ||
192
+ actual.content !== record.content
193
+ ) {
194
+ fail(`Canonical entry changed tool data at ordinal ${record.ordinal}.`);
195
+ }
196
+ }
197
+ }
198
+
199
+ function requireItem<T>(items: readonly T[], index: number, name: string): T {
200
+ const item = items[index];
201
+ if (item === undefined) {
202
+ fail(`Missing ${name} at index ${index}.`);
203
+ }
204
+ return item;
205
+ }
206
+
207
+ function fail(message: string): never {
208
+ throw new CompiledContextError(message);
209
+ }
@@ -0,0 +1,362 @@
1
+ import { ContextBuilder } from "../agent/context-builder";
2
+ import type { ContextMeter, ContextUsageSnapshot } from "../agent/context-meter";
3
+ import type { SessionLedger } from "../agent/session-ledger";
4
+ import type { RuntimeIdFactory } from "../ids/runtime-id";
5
+ import {
6
+ CommittedPrefixAuditError,
7
+ type CommittedPrefixAuditor,
8
+ } from "../model/committed-prefix-auditor";
9
+ import type { ModelClient } from "../model/model-client";
10
+ import { promptPrefixFingerprint } from "../model/prompt-prefix-hash";
11
+ import { estimatePromptSegments } from "../model/token-estimator";
12
+ import { SessionError } from "../session/session-errors";
13
+ import type { SessionStore } from "../session/session-store";
14
+ import type { ToolDefinition } from "../tools/types";
15
+ import { canonicalSequenceHash, renderedMessageHash } from "./compiled-context-hash";
16
+ import { CompiledContextError } from "./compiled-context-validator";
17
+ import { swapOnlyPolicyV1 } from "./context-policy";
18
+ import { ContextProtocolError } from "./context-protocol-validator";
19
+ import {
20
+ ContextRevisionCompiler,
21
+ ContextRevisionError,
22
+ } from "./context-revision-compiler";
23
+ import {
24
+ assertPlanBaseCurrent,
25
+ SwapPlanner,
26
+ SwapPlanningDiagnosticError,
27
+ SwapPlanStaleError,
28
+ } from "./swap-planner";
29
+
30
+ export type ContextCompactionTrigger =
31
+ | { kind: "manual" }
32
+ | { kind: "runtime_pressure" }
33
+ | { kind: "benchmark_forced"; targetTokens: number };
34
+
35
+ export type ContextCompactionResult =
36
+ | {
37
+ status: "unchanged";
38
+ outcome: "below_target" | "no_eligible_candidates";
39
+ revisionId: ReturnType<RuntimeIdFactory["createContextRevisionId"]>;
40
+ revisionNumber: number;
41
+ totalOverrideCount: number;
42
+ rawTokensBefore: number;
43
+ guardedTokensBefore: number;
44
+ targetTokens: number;
45
+ planningDurationMs: number;
46
+ durationMs: number;
47
+ }
48
+ | {
49
+ status: "compacted";
50
+ outcome: "target_reached" | "insufficient_candidates";
51
+ previousRevisionId: ReturnType<RuntimeIdFactory["createContextRevisionId"]>;
52
+ revisionId: ReturnType<RuntimeIdFactory["createContextRevisionId"]>;
53
+ previousRevisionNumber: number;
54
+ revisionNumber: number;
55
+ addedOverrideCount: number;
56
+ totalOverrideCount: number;
57
+ originalObservationBytes: number;
58
+ projectedObservationBytes: number;
59
+ rawTokensBefore: number;
60
+ rawTokensAfter: number;
61
+ guardedTokensBefore: number;
62
+ guardedTokensAfter: number;
63
+ targetTokens: number;
64
+ planHash: string;
65
+ planningDurationMs: number;
66
+ validationDurationMs: number;
67
+ transactionDurationMs: number;
68
+ activationDurationMs: number;
69
+ durationMs: number;
70
+ };
71
+
72
+ export class ContextManagerError extends Error {
73
+ constructor(
74
+ readonly stage: "snapshot" | "plan" | "validate" | "commit" | "activate",
75
+ readonly code: string,
76
+ readonly fatal: boolean,
77
+ readonly committed: boolean,
78
+ message: string,
79
+ options?: ErrorOptions,
80
+ ) {
81
+ super(message, options);
82
+ this.name = "ContextManagerError";
83
+ }
84
+ }
85
+
86
+ type ModelPreparer = Pick<ModelClient, "prepare">;
87
+
88
+ export class ContextManager {
89
+ private readonly planner: SwapPlanner;
90
+ private readonly compiler = new ContextRevisionCompiler();
91
+ private readonly requestBuilder = new ContextBuilder();
92
+
93
+ constructor(
94
+ private readonly input: {
95
+ store: SessionStore;
96
+ ledger: SessionLedger;
97
+ model: ModelPreparer;
98
+ contextMeter: ContextMeter;
99
+ committedPrefixAuditor: CommittedPrefixAuditor;
100
+ idFactory: RuntimeIdFactory;
101
+ tools: () => readonly ToolDefinition[];
102
+ onUsageUpdated: (snapshot: ContextUsageSnapshot) => Promise<void>;
103
+ },
104
+ ) {
105
+ this.planner = new SwapPlanner(input.model);
106
+ }
107
+
108
+ async compact(trigger: ContextCompactionTrigger): Promise<ContextCompactionResult> {
109
+ const startedAt = performance.now();
110
+ let built;
111
+ let activePrepared;
112
+ let activeUsage;
113
+ const tools = this.input.tools();
114
+ try {
115
+ this.input.store.assertContextRevisionIdle();
116
+ built = this.input.ledger.buildCommittedModelRequest(tools);
117
+ activePrepared = this.input.model.prepare(built.request);
118
+ activeUsage = this.input.contextMeter.measure(activePrepared);
119
+ } catch (error) {
120
+ throw managerError("snapshot", error);
121
+ }
122
+
123
+ let planning;
124
+ try {
125
+ planning = this.planner.plan({
126
+ active: built.compiled,
127
+ revision: built.revision,
128
+ activeOverrides: built.activeOverrides,
129
+ canonical: built.canonical,
130
+ activePrepared,
131
+ activeUsage,
132
+ tools,
133
+ policy: swapOnlyPolicyV1,
134
+ trigger: trigger.kind,
135
+ ...(trigger.kind === "benchmark_forced"
136
+ ? { forcedTargetTokens: trigger.targetTokens }
137
+ : {}),
138
+ });
139
+ } catch (error) {
140
+ throw managerError("plan", error);
141
+ }
142
+ const planningDurationMs = elapsedMs(startedAt);
143
+
144
+ if (
145
+ planning.outcome === "below_target" ||
146
+ planning.outcome === "no_eligible_candidates"
147
+ ) {
148
+ return Object.freeze({
149
+ status: "unchanged",
150
+ outcome: planning.outcome,
151
+ revisionId: built.revision.revisionId,
152
+ revisionNumber: built.revision.revisionNumber,
153
+ totalOverrideCount: built.revision.totalOverrideCount,
154
+ rawTokensBefore: planning.rawTokensBefore,
155
+ guardedTokensBefore: planning.guardedTokensBefore,
156
+ targetTokens: planning.targetTokens,
157
+ planningDurationMs,
158
+ durationMs: elapsedMs(startedAt),
159
+ });
160
+ }
161
+ if (planning.outcome === "below_trigger") {
162
+ throw new ContextManagerError(
163
+ "plan",
164
+ "manual_below_trigger",
165
+ true,
166
+ false,
167
+ "Active compaction cannot finish with a shadow-only below-trigger outcome.",
168
+ );
169
+ }
170
+ const plan = planning.plan;
171
+ if (plan === undefined || plan.addedOverrides.length === 0) {
172
+ throw new ContextManagerError(
173
+ "validate",
174
+ "missing_swap_plan",
175
+ true,
176
+ false,
177
+ "Committable swap planning produced no revision plan.",
178
+ );
179
+ }
180
+
181
+ const candidateOverrides = [...built.activeOverrides, ...plan.addedOverrides];
182
+ const validationStartedAt = performance.now();
183
+ let candidatePrepared;
184
+ let candidateCompiled;
185
+ try {
186
+ candidateCompiled = this.compiler.compileProspective({
187
+ active: built.compiled,
188
+ canonical: built.canonical,
189
+ activeOverrides: built.activeOverrides,
190
+ addedOverrides: plan.addedOverrides,
191
+ });
192
+ const candidate = this.requestBuilder.build({
193
+ canonical: built.canonical,
194
+ revision: built.revision,
195
+ activeOverrides: candidateOverrides,
196
+ compiled: candidateCompiled,
197
+ tools,
198
+ });
199
+ candidatePrepared = this.input.model.prepare(candidate.request);
200
+ assertCandidatePrepared(activePrepared, candidatePrepared, plan);
201
+
202
+ const current = this.input.ledger.buildCommittedModelRequest(tools);
203
+ const currentPrepared = this.input.model.prepare(current.request);
204
+ assertPlanBaseCurrent(plan, {
205
+ active: current.compiled,
206
+ revision: current.revision,
207
+ activeOverrides: current.activeOverrides,
208
+ activePrepared: currentPrepared,
209
+ });
210
+ this.input.store.assertContextRevisionIdle();
211
+ } catch (error) {
212
+ throw managerError("validate", error);
213
+ }
214
+ const validationDurationMs = elapsedMs(validationStartedAt);
215
+
216
+ const revisionId = this.input.idFactory.createContextRevisionId();
217
+ const transactionStartedAt = performance.now();
218
+ let revision;
219
+ try {
220
+ revision = this.input.store.commitSwapRevision({
221
+ revisionId,
222
+ expectedBaseRevisionId: plan.baseRevisionId,
223
+ expectedBaseRevisionNumber: plan.baseRevisionNumber,
224
+ expectedCanonicalThroughOrdinal: plan.baseCanonicalThroughOrdinal,
225
+ expectedBaseOverrideManifestSha256: plan.baseOverrideManifestSha256,
226
+ policyVersion: plan.policyVersion,
227
+ rendererFormat: "swap-observation-v1",
228
+ planHash: plan.planHash,
229
+ addedOverrides: plan.addedOverrides,
230
+ nextOverrideManifestSha256: plan.nextOverrideManifestSha256,
231
+ canonicalSequenceSha256: canonicalSequenceHash(
232
+ built.canonical,
233
+ plan.baseCanonicalThroughOrdinal,
234
+ ),
235
+ renderedMessageSha256: renderedMessageHash(
236
+ candidateCompiled.entries,
237
+ plan.baseCanonicalThroughOrdinal,
238
+ ),
239
+ });
240
+ } catch (error) {
241
+ throw managerError("commit", error);
242
+ }
243
+ const transactionDurationMs = elapsedMs(transactionStartedAt);
244
+
245
+ const activationStartedAt = performance.now();
246
+ try {
247
+ this.input.contextMeter.startRevision({
248
+ reason: "context_rebuilt",
249
+ requestConfigHash: candidatePrepared.requestConfigHash,
250
+ toolSchemaHash: candidatePrepared.toolSchemaHash,
251
+ });
252
+ this.input.committedPrefixAuditor.audit(revisionId, candidatePrepared);
253
+ const usage = this.input.contextMeter.measure(candidatePrepared);
254
+ if (usage.source !== "estimated_full") {
255
+ throw new Error(
256
+ "First context measurement after revision activation was not a full estimate.",
257
+ );
258
+ }
259
+ await this.input.onUsageUpdated(usage);
260
+ } catch (error) {
261
+ throw new ContextManagerError(
262
+ "activate",
263
+ errorCode(error),
264
+ true,
265
+ true,
266
+ "Context revision committed but runtime activation failed.",
267
+ { cause: error },
268
+ );
269
+ }
270
+ const activationDurationMs = elapsedMs(activationStartedAt);
271
+
272
+ return Object.freeze({
273
+ status: "compacted",
274
+ outcome: planning.outcome,
275
+ previousRevisionId: built.revision.revisionId,
276
+ revisionId,
277
+ previousRevisionNumber: built.revision.revisionNumber,
278
+ revisionNumber: revision.revisionNumber,
279
+ addedOverrideCount: plan.addedOverrides.length,
280
+ totalOverrideCount: revision.totalOverrideCount,
281
+ originalObservationBytes: planning.originalObservationBytes,
282
+ projectedObservationBytes: planning.projectedObservationBytes,
283
+ rawTokensBefore: plan.rawTokensBefore,
284
+ rawTokensAfter: plan.rawTokensAfter,
285
+ guardedTokensBefore: plan.guardedTokensBefore,
286
+ guardedTokensAfter: plan.guardedTokensAfter,
287
+ targetTokens: plan.targetTokens,
288
+ planHash: plan.planHash,
289
+ planningDurationMs,
290
+ validationDurationMs,
291
+ transactionDurationMs,
292
+ activationDurationMs,
293
+ durationMs: elapsedMs(startedAt),
294
+ });
295
+ }
296
+ }
297
+
298
+ function assertCandidatePrepared(
299
+ active: ReturnType<ModelPreparer["prepare"]>,
300
+ candidate: ReturnType<ModelPreparer["prepare"]>,
301
+ plan: NonNullable<ReturnType<SwapPlanner["plan"]>["plan"]>,
302
+ ): void {
303
+ const rawTokens = estimatePromptSegments(candidate.promptSegments).totalTokens;
304
+ const fingerprint = promptPrefixFingerprint(candidate);
305
+ if (
306
+ candidate.provider !== active.provider ||
307
+ candidate.model !== active.model ||
308
+ candidate.requestConfigHash !== active.requestConfigHash ||
309
+ candidate.toolSchemaHash !== active.toolSchemaHash ||
310
+ candidate.requestMaxOutputTokens !== active.requestMaxOutputTokens ||
311
+ rawTokens !== plan.rawTokensAfter ||
312
+ fingerprint.prefixHash !== plan.projectedPrefixHash ||
313
+ plan.rawTokensAfter >= plan.rawTokensBefore ||
314
+ plan.guardedTokensAfter >= plan.guardedTokensBefore
315
+ ) {
316
+ throw new Error("Prepared candidate does not match its validated swap plan.");
317
+ }
318
+ }
319
+
320
+ function managerError(
321
+ stage: ContextManagerError["stage"],
322
+ error: unknown,
323
+ ): ContextManagerError {
324
+ if (error instanceof ContextManagerError) {
325
+ return error;
326
+ }
327
+ const fatal = isFatalContextError(error) || stage === "commit";
328
+ const diagnostic = error instanceof SwapPlanningDiagnosticError;
329
+ return new ContextManagerError(
330
+ stage,
331
+ diagnostic ? error.code : errorCode(error),
332
+ fatal,
333
+ false,
334
+ error instanceof Error ? error.message : `Context compaction failed at ${stage}.`,
335
+ { cause: error },
336
+ );
337
+ }
338
+
339
+ function isFatalContextError(error: unknown): boolean {
340
+ return (
341
+ error instanceof ContextProtocolError ||
342
+ error instanceof ContextRevisionError ||
343
+ error instanceof CompiledContextError ||
344
+ error instanceof SwapPlanStaleError ||
345
+ error instanceof CommittedPrefixAuditError ||
346
+ error instanceof SessionError
347
+ );
348
+ }
349
+
350
+ function errorCode(error: unknown): string {
351
+ if (error instanceof SessionError) {
352
+ return error.code;
353
+ }
354
+ if (error instanceof Error) {
355
+ return error.name;
356
+ }
357
+ return "CONTEXT_COMPACTION_FAILED";
358
+ }
359
+
360
+ function elapsedMs(startedAt: number): number {
361
+ return Math.round((performance.now() - startedAt) * 100) / 100;
362
+ }