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.
- package/README.md +173 -0
- package/package.json +78 -0
- package/patches/markdansi@0.3.2.patch +37 -0
- package/src/agent/context-builder.ts +43 -0
- package/src/agent/context-meter.ts +310 -0
- package/src/agent/loop.ts +525 -0
- package/src/agent/runtime-session.ts +1212 -0
- package/src/agent/session-ledger.ts +828 -0
- package/src/agent/turn-cancellation.ts +44 -0
- package/src/agent/types.ts +77 -0
- package/src/cli/config.ts +283 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/model-profiles.ts +289 -0
- package/src/cli/run-runner.ts +107 -0
- package/src/cli/tui-runner.tsx +290 -0
- package/src/context/compiled-context-hash.ts +138 -0
- package/src/context/compiled-context-validator.ts +209 -0
- package/src/context/context-manager.ts +362 -0
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +463 -0
- package/src/context/context-revision-compiler.ts +281 -0
- package/src/context/context-revision.ts +111 -0
- package/src/context/context-source.ts +30 -0
- package/src/context/context-swap-renderer.ts +272 -0
- package/src/context/protocol-frame.ts +240 -0
- package/src/context/swap-planner.ts +725 -0
- package/src/events/append-private-file.ts +16 -0
- package/src/events/bash-result-detail.ts +70 -0
- package/src/events/composite-event-sink.ts +82 -0
- package/src/events/event-sink.ts +16 -0
- package/src/events/jsonl-event-log.ts +13 -0
- package/src/events/observation-text-log.ts +195 -0
- package/src/events/stdout-event-printer.ts +396 -0
- package/src/events/types.ts +263 -0
- package/src/ids/runtime-id.ts +68 -0
- package/src/ids/uuid-v7.ts +5 -0
- package/src/instructions/project-instructions.ts +242 -0
- package/src/mcp/mcp-config.ts +144 -0
- package/src/mcp/mcp-manager.ts +216 -0
- package/src/mcp/mcp-tool-executor.ts +178 -0
- package/src/model/committed-prefix-auditor.ts +68 -0
- package/src/model/fake-model-client.ts +280 -0
- package/src/model/model-client.ts +64 -0
- package/src/model/model-context-profile.ts +134 -0
- package/src/model/model-request-preflight.ts +120 -0
- package/src/model/openai-chat-mapping.ts +444 -0
- package/src/model/openai-chat-model-client.ts +190 -0
- package/src/model/prompt-prefix-hash.ts +47 -0
- package/src/model/token-estimator.ts +148 -0
- package/src/observation/observation-builder.ts +481 -0
- package/src/session/resume-projection.ts +616 -0
- package/src/session/session-catalog.ts +270 -0
- package/src/session/session-errors.ts +121 -0
- package/src/session/session-history-reader.ts +535 -0
- package/src/session/session-lock.ts +291 -0
- package/src/session/session-schema.ts +741 -0
- package/src/session/session-store.ts +3067 -0
- package/src/session/sqlite-session-ledger.ts +153 -0
- package/src/tools/bash-task.ts +617 -0
- package/src/tools/bash.ts +450 -0
- package/src/tools/cwd-state.ts +22 -0
- package/src/tools/edit.ts +428 -0
- package/src/tools/file-diff.ts +116 -0
- package/src/tools/glob.ts +202 -0
- package/src/tools/grep.ts +550 -0
- package/src/tools/hash.ts +9 -0
- package/src/tools/path-safety.ts +33 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/recall.ts +400 -0
- package/src/tools/registry.ts +213 -0
- package/src/tools/ripgrep.ts +220 -0
- package/src/tools/task-list.ts +59 -0
- package/src/tools/task-output-snapshot.ts +47 -0
- package/src/tools/task-output-tool.ts +62 -0
- package/src/tools/task-output.ts +159 -0
- package/src/tools/task-stop.ts +59 -0
- package/src/tools/task-tool-args.ts +29 -0
- package/src/tools/types.ts +330 -0
- package/src/tools/web-fetch/backend.ts +27 -0
- package/src/tools/web-fetch/browser-backend.ts +126 -0
- package/src/tools/web-fetch/exa-backend.ts +172 -0
- package/src/tools/web-fetch/index.ts +298 -0
- package/src/tools/web-fetch/local-backend.ts +267 -0
- package/src/tools/web-fetch/refiner.ts +78 -0
- package/src/tools/web-fetch/route.ts +95 -0
- package/src/tools/web-search.ts +300 -0
- package/src/tools/write.ts +244 -0
- package/src/tui/app.tsx +497 -0
- package/src/tui/components/assistant-markdown.tsx +47 -0
- package/src/tui/components/background-tasks.tsx +92 -0
- package/src/tui/components/bash-result-view.tsx +47 -0
- package/src/tui/components/context-status.tsx +127 -0
- package/src/tui/components/diff-view.tsx +151 -0
- package/src/tui/components/file-viewer.tsx +212 -0
- package/src/tui/components/footer.tsx +60 -0
- package/src/tui/components/header.tsx +21 -0
- package/src/tui/components/model-picker.tsx +142 -0
- package/src/tui/components/prompt-input.tsx +432 -0
- package/src/tui/components/resume-session-picker.tsx +273 -0
- package/src/tui/components/timeline.tsx +121 -0
- package/src/tui/context-format.ts +24 -0
- package/src/tui/event-store.ts +865 -0
- package/src/tui/git-branch.ts +23 -0
- package/src/tui/line-editor.ts +157 -0
- package/src/tui/prompt-history.ts +94 -0
- package/src/tui/slash-commands.ts +126 -0
- package/src/tui/tui-projection-policy.ts +35 -0
- package/src/tui/tui-projection-store.ts +123 -0
- package/src/tui/tui-session-controller.ts +170 -0
- package/src/tui/view-file.ts +122 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { ContextBuilder } from "../agent/context-builder";
|
|
2
|
+
import type { ContextUsageSnapshot } from "../agent/context-meter";
|
|
3
|
+
import type { TurnId } from "../ids/runtime-id";
|
|
4
|
+
import type { ModelClient, PreparedModelRequest } from "../model/model-client";
|
|
5
|
+
import {
|
|
6
|
+
promptPrefixFingerprint,
|
|
7
|
+
type PromptPrefixFingerprint,
|
|
8
|
+
} from "../model/prompt-prefix-hash";
|
|
9
|
+
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
10
|
+
import { estimatePromptSegments } from "../model/token-estimator";
|
|
11
|
+
import type { ToolDefinition } from "../tools/types";
|
|
12
|
+
import { activeOverrideManifestHash } from "./compiled-context-hash";
|
|
13
|
+
import { CompiledContextError } from "./compiled-context-validator";
|
|
14
|
+
import type { SwapOnlyPolicyV1 } from "./context-policy";
|
|
15
|
+
import { ContextProtocolError } from "./context-protocol-validator";
|
|
16
|
+
import type {
|
|
17
|
+
CompiledRevisionContext,
|
|
18
|
+
StoredContextRevisionV5,
|
|
19
|
+
SwapOverride,
|
|
20
|
+
} from "./context-revision";
|
|
21
|
+
import {
|
|
22
|
+
ContextRevisionCompiler,
|
|
23
|
+
ContextRevisionError,
|
|
24
|
+
} from "./context-revision-compiler";
|
|
25
|
+
import {
|
|
26
|
+
ContextSwapRenderer,
|
|
27
|
+
isSwappableRawResult,
|
|
28
|
+
SwapRenderUnsupportedError,
|
|
29
|
+
type SwappableRawKind,
|
|
30
|
+
} from "./context-swap-renderer";
|
|
31
|
+
import type {
|
|
32
|
+
CanonicalMessageRecord,
|
|
33
|
+
ProtocolContextView,
|
|
34
|
+
ToolResultRecord,
|
|
35
|
+
} from "./protocol-frame";
|
|
36
|
+
|
|
37
|
+
export type SwapPlanningTrigger = "manual" | "runtime_pressure" | "benchmark_forced";
|
|
38
|
+
|
|
39
|
+
export type SwapPlanningOutcome =
|
|
40
|
+
| "below_trigger"
|
|
41
|
+
| "below_target"
|
|
42
|
+
| "no_eligible_candidates"
|
|
43
|
+
| "target_reached"
|
|
44
|
+
| "insufficient_candidates";
|
|
45
|
+
|
|
46
|
+
export type SwapRevisionPlan = {
|
|
47
|
+
readonly version: 1;
|
|
48
|
+
readonly policyVersion: "swap-only-v1";
|
|
49
|
+
readonly baseRevisionId: CompiledRevisionContext["revisionId"];
|
|
50
|
+
readonly baseRevisionNumber: number;
|
|
51
|
+
readonly baseCanonicalThroughOrdinal: number;
|
|
52
|
+
readonly baseOverrideManifestSha256: string;
|
|
53
|
+
readonly basePrefixHash: string;
|
|
54
|
+
readonly requestConfigHash: string;
|
|
55
|
+
readonly toolSchemaHash: string;
|
|
56
|
+
readonly addedOverrides: readonly SwapOverride[];
|
|
57
|
+
readonly nextOverrideManifestSha256: string;
|
|
58
|
+
readonly targetTokens: number;
|
|
59
|
+
readonly rawTokensBefore: number;
|
|
60
|
+
readonly rawTokensAfter: number;
|
|
61
|
+
readonly guardedTokensBefore: number;
|
|
62
|
+
readonly guardedTokensAfter: number;
|
|
63
|
+
readonly projectedPrefixHash: string;
|
|
64
|
+
readonly planHash: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type SwapPlanningInput = {
|
|
68
|
+
readonly active: CompiledRevisionContext;
|
|
69
|
+
readonly revision: StoredContextRevisionV5;
|
|
70
|
+
readonly activeOverrides: readonly SwapOverride[];
|
|
71
|
+
readonly canonical: ProtocolContextView;
|
|
72
|
+
readonly activePrepared: PreparedModelRequest;
|
|
73
|
+
readonly activeUsage: ContextUsageSnapshot;
|
|
74
|
+
readonly tools: readonly ToolDefinition[];
|
|
75
|
+
readonly policy: SwapOnlyPolicyV1;
|
|
76
|
+
readonly trigger: SwapPlanningTrigger;
|
|
77
|
+
readonly forcedTargetTokens?: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type SwapPlanningResult = {
|
|
81
|
+
readonly outcome: SwapPlanningOutcome;
|
|
82
|
+
readonly canonicalMessageCount: number;
|
|
83
|
+
readonly eligibleCandidateCount: number;
|
|
84
|
+
readonly excludedByReason: Readonly<Record<string, number>>;
|
|
85
|
+
readonly selectedByRawKind: Readonly<Record<string, number>>;
|
|
86
|
+
readonly originalObservationBytes: number;
|
|
87
|
+
readonly projectedObservationBytes: number;
|
|
88
|
+
readonly rawTokensBefore: number;
|
|
89
|
+
readonly guardedTokensBefore: number;
|
|
90
|
+
readonly targetTokens: number;
|
|
91
|
+
readonly plan?: SwapRevisionPlan;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export class SwapPlanningDiagnosticError extends Error {
|
|
95
|
+
constructor(
|
|
96
|
+
readonly stage: "candidate" | "render" | "prepare" | "validate",
|
|
97
|
+
readonly code: string,
|
|
98
|
+
message: string,
|
|
99
|
+
options?: ErrorOptions,
|
|
100
|
+
) {
|
|
101
|
+
super(message, options);
|
|
102
|
+
this.name = "SwapPlanningDiagnosticError";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class SwapPlanStaleError extends Error {
|
|
107
|
+
constructor(message: string) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.name = "SwapPlanStaleError";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type ModelPreparer = Pick<ModelClient, "prepare">;
|
|
114
|
+
|
|
115
|
+
type EligibleCandidate = {
|
|
116
|
+
readonly override: SwapOverride;
|
|
117
|
+
readonly rawKind: SwappableRawKind;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type CandidateScan = {
|
|
121
|
+
readonly eligible: readonly EligibleCandidate[];
|
|
122
|
+
readonly excludedByReason: Readonly<Record<string, number>>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type Projection = {
|
|
126
|
+
readonly count: number;
|
|
127
|
+
readonly prepared: PreparedModelRequest;
|
|
128
|
+
readonly rawTokens: number;
|
|
129
|
+
readonly guardedTokens: number;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export class SwapPlanner {
|
|
133
|
+
constructor(
|
|
134
|
+
private readonly model: ModelPreparer,
|
|
135
|
+
private readonly compiler = new ContextRevisionCompiler(),
|
|
136
|
+
private readonly requestBuilder = new ContextBuilder(),
|
|
137
|
+
private readonly renderer = new ContextSwapRenderer(),
|
|
138
|
+
) {}
|
|
139
|
+
|
|
140
|
+
plan(input: SwapPlanningInput): SwapPlanningResult {
|
|
141
|
+
validatePlanningInput(input);
|
|
142
|
+
const activeFingerprint = promptPrefixFingerprint(input.activePrepared);
|
|
143
|
+
assertActiveFingerprint(input, activeFingerprint);
|
|
144
|
+
const rawTokensBefore = estimatePromptSegments(
|
|
145
|
+
input.activePrepared.promptSegments,
|
|
146
|
+
).totalTokens;
|
|
147
|
+
const guardedTokensBefore = guardTokens(
|
|
148
|
+
rawTokensBefore,
|
|
149
|
+
input.activeUsage.correctionFactor,
|
|
150
|
+
);
|
|
151
|
+
const targetTokens = planningTarget(input);
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
input.trigger === "runtime_pressure" &&
|
|
155
|
+
input.activeUsage.pressure === "normal"
|
|
156
|
+
) {
|
|
157
|
+
return emptyResult(
|
|
158
|
+
input,
|
|
159
|
+
"below_trigger",
|
|
160
|
+
rawTokensBefore,
|
|
161
|
+
guardedTokensBefore,
|
|
162
|
+
targetTokens,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (guardedTokensBefore <= targetTokens) {
|
|
166
|
+
return emptyResult(
|
|
167
|
+
input,
|
|
168
|
+
input.trigger === "runtime_pressure" ? "below_trigger" : "below_target",
|
|
169
|
+
rawTokensBefore,
|
|
170
|
+
guardedTokensBefore,
|
|
171
|
+
targetTokens,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const scan = this.scanCandidates(
|
|
176
|
+
input.canonical,
|
|
177
|
+
input.activeOverrides,
|
|
178
|
+
input.policy,
|
|
179
|
+
);
|
|
180
|
+
if (scan.eligible.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
...emptyResult(
|
|
183
|
+
input,
|
|
184
|
+
"no_eligible_candidates",
|
|
185
|
+
rawTokensBefore,
|
|
186
|
+
guardedTokensBefore,
|
|
187
|
+
targetTokens,
|
|
188
|
+
),
|
|
189
|
+
excludedByReason: scan.excludedByReason,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const projectionCache = new Map<number, Projection>();
|
|
194
|
+
const project = (count: number): Projection => {
|
|
195
|
+
const existing = projectionCache.get(count);
|
|
196
|
+
if (existing !== undefined) {
|
|
197
|
+
return existing;
|
|
198
|
+
}
|
|
199
|
+
const addedOverrides = scan.eligible
|
|
200
|
+
.slice(0, count)
|
|
201
|
+
.map((entry) => entry.override);
|
|
202
|
+
let prepared: PreparedModelRequest;
|
|
203
|
+
try {
|
|
204
|
+
const compiled = this.compiler.compileProspective({
|
|
205
|
+
active: input.active,
|
|
206
|
+
canonical: input.canonical,
|
|
207
|
+
activeOverrides: input.activeOverrides,
|
|
208
|
+
addedOverrides,
|
|
209
|
+
});
|
|
210
|
+
const built = this.requestBuilder.build({
|
|
211
|
+
canonical: input.canonical,
|
|
212
|
+
revision: input.revision,
|
|
213
|
+
activeOverrides: [...input.activeOverrides, ...addedOverrides],
|
|
214
|
+
compiled,
|
|
215
|
+
tools: input.tools,
|
|
216
|
+
});
|
|
217
|
+
prepared = this.model.prepare(built.request);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (isCanonicalPlanningError(error)) {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
throw new SwapPlanningDiagnosticError(
|
|
223
|
+
"prepare",
|
|
224
|
+
"prospective_prepare_failed",
|
|
225
|
+
"Prospective request preparation failed.",
|
|
226
|
+
{ cause: error },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
assertProspectiveConfiguration(input.activePrepared, prepared);
|
|
230
|
+
const rawTokens = estimatePromptSegments(prepared.promptSegments).totalTokens;
|
|
231
|
+
const projection = Object.freeze({
|
|
232
|
+
count,
|
|
233
|
+
prepared,
|
|
234
|
+
rawTokens,
|
|
235
|
+
guardedTokens: guardTokens(rawTokens, input.activeUsage.correctionFactor),
|
|
236
|
+
});
|
|
237
|
+
projectionCache.set(count, projection);
|
|
238
|
+
return projection;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
let previousCount = 0;
|
|
242
|
+
let count = 1;
|
|
243
|
+
let reached: Projection | undefined;
|
|
244
|
+
let last: Projection | undefined;
|
|
245
|
+
while (true) {
|
|
246
|
+
last = project(count);
|
|
247
|
+
if (last.guardedTokens <= targetTokens) {
|
|
248
|
+
reached = last;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (count === scan.eligible.length) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
previousCount = count;
|
|
255
|
+
count = Math.min(scan.eligible.length, count * 2);
|
|
256
|
+
}
|
|
257
|
+
if (last === undefined) {
|
|
258
|
+
throw new SwapPlanningDiagnosticError(
|
|
259
|
+
"validate",
|
|
260
|
+
"missing_projection",
|
|
261
|
+
"Swap planning produced no prospective projection.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let outcome: Extract<
|
|
266
|
+
SwapPlanningOutcome,
|
|
267
|
+
"target_reached" | "insufficient_candidates"
|
|
268
|
+
>;
|
|
269
|
+
let finalProjection: Projection;
|
|
270
|
+
if (reached === undefined) {
|
|
271
|
+
outcome = "insufficient_candidates";
|
|
272
|
+
finalProjection = last;
|
|
273
|
+
} else {
|
|
274
|
+
outcome = "target_reached";
|
|
275
|
+
let low = previousCount + 1;
|
|
276
|
+
let high = reached.count;
|
|
277
|
+
while (low < high) {
|
|
278
|
+
const middle = Math.floor((low + high) / 2);
|
|
279
|
+
if (project(middle).guardedTokens <= targetTokens) {
|
|
280
|
+
high = middle;
|
|
281
|
+
} else {
|
|
282
|
+
low = middle + 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
finalProjection = project(low);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
finalProjection.rawTokens >= rawTokensBefore ||
|
|
290
|
+
finalProjection.guardedTokens >= guardedTokensBefore
|
|
291
|
+
) {
|
|
292
|
+
throw new SwapPlanningDiagnosticError(
|
|
293
|
+
"validate",
|
|
294
|
+
"no_token_reduction",
|
|
295
|
+
"Prospective request did not strictly reduce raw and guarded tokens.",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const selectedCandidates = scan.eligible.slice(0, finalProjection.count);
|
|
300
|
+
const addedOverrides = Object.freeze(
|
|
301
|
+
selectedCandidates.map((candidate) => candidate.override),
|
|
302
|
+
);
|
|
303
|
+
const projectedFingerprint = promptPrefixFingerprint(finalProjection.prepared);
|
|
304
|
+
const plan = createPlan({
|
|
305
|
+
input,
|
|
306
|
+
activeFingerprint,
|
|
307
|
+
projectedFingerprint,
|
|
308
|
+
addedOverrides,
|
|
309
|
+
targetTokens,
|
|
310
|
+
rawTokensBefore,
|
|
311
|
+
rawTokensAfter: finalProjection.rawTokens,
|
|
312
|
+
guardedTokensBefore,
|
|
313
|
+
guardedTokensAfter: finalProjection.guardedTokens,
|
|
314
|
+
});
|
|
315
|
+
assertPlanBaseCurrent(plan, {
|
|
316
|
+
active: input.active,
|
|
317
|
+
revision: input.revision,
|
|
318
|
+
activeOverrides: input.activeOverrides,
|
|
319
|
+
activePrepared: input.activePrepared,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return Object.freeze({
|
|
323
|
+
outcome,
|
|
324
|
+
canonicalMessageCount: input.canonical.messages.length,
|
|
325
|
+
eligibleCandidateCount: scan.eligible.length,
|
|
326
|
+
excludedByReason: scan.excludedByReason,
|
|
327
|
+
selectedByRawKind: countRawKinds(selectedCandidates),
|
|
328
|
+
originalObservationBytes: addedOverrides.reduce(
|
|
329
|
+
(total, override) => total + override.originalBytes,
|
|
330
|
+
0,
|
|
331
|
+
),
|
|
332
|
+
projectedObservationBytes: addedOverrides.reduce(
|
|
333
|
+
(total, override) => total + override.renderedBytes,
|
|
334
|
+
0,
|
|
335
|
+
),
|
|
336
|
+
rawTokensBefore,
|
|
337
|
+
guardedTokensBefore,
|
|
338
|
+
targetTokens,
|
|
339
|
+
plan,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private scanCandidates(
|
|
344
|
+
canonical: ProtocolContextView,
|
|
345
|
+
activeOverrides: readonly SwapOverride[],
|
|
346
|
+
policy: SwapOnlyPolicyV1,
|
|
347
|
+
): CandidateScan {
|
|
348
|
+
const alreadySwapped = new Set(
|
|
349
|
+
activeOverrides.map((override) => override.messageId),
|
|
350
|
+
);
|
|
351
|
+
const closedFrames = new Set(
|
|
352
|
+
canonical.frames
|
|
353
|
+
.filter((frame) => frame.state === "closed")
|
|
354
|
+
.map((frame) => frame.frameId),
|
|
355
|
+
);
|
|
356
|
+
const resultsByMessage = new Map(
|
|
357
|
+
canonical.toolResults.map((result) => [result.toolMessageId, result] as const),
|
|
358
|
+
);
|
|
359
|
+
const protectedTurns = protectedRecentTurns(
|
|
360
|
+
canonical,
|
|
361
|
+
policy.protectedRecentTurnCount,
|
|
362
|
+
);
|
|
363
|
+
const eligible: EligibleCandidate[] = [];
|
|
364
|
+
const exclusions = new Map<string, number>();
|
|
365
|
+
|
|
366
|
+
for (const message of canonical.messages) {
|
|
367
|
+
if (message.role !== "tool") {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (alreadySwapped.has(message.messageId)) {
|
|
371
|
+
increment(exclusions, "already_swapped");
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const reason = basicExclusionReason({
|
|
375
|
+
message,
|
|
376
|
+
result: resultsByMessage.get(message.messageId),
|
|
377
|
+
closedFrames,
|
|
378
|
+
protectedTurns,
|
|
379
|
+
minimumObservationBytes: policy.minimumObservationBytes,
|
|
380
|
+
});
|
|
381
|
+
if (reason !== undefined) {
|
|
382
|
+
increment(exclusions, reason);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const result = resultsByMessage.get(message.messageId);
|
|
386
|
+
if (result === undefined || result.completion.kind !== "returned") {
|
|
387
|
+
throw new ContextRevisionError(
|
|
388
|
+
"Eligible tool message is missing its canonical returned result.",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (!isSwappableRawResult(result.completion.raw)) {
|
|
392
|
+
throw new ContextRevisionError(
|
|
393
|
+
"Eligible tool message has an unexpected raw result kind.",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
eligible.push(
|
|
398
|
+
Object.freeze({
|
|
399
|
+
override: this.renderer.render({ message, result }),
|
|
400
|
+
rawKind: result.completion.raw.kind,
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (!(error instanceof SwapRenderUnsupportedError)) {
|
|
405
|
+
throw new SwapPlanningDiagnosticError(
|
|
406
|
+
"render",
|
|
407
|
+
"renderer_failed",
|
|
408
|
+
"Swap placeholder rendering failed.",
|
|
409
|
+
{ cause: error },
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (error.code === "source_hash_mismatch") {
|
|
413
|
+
throw new ContextRevisionError(error.message);
|
|
414
|
+
}
|
|
415
|
+
increment(exclusions, error.code);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
eligible.sort(compareCandidates);
|
|
420
|
+
return Object.freeze({
|
|
421
|
+
eligible: Object.freeze(eligible),
|
|
422
|
+
excludedByReason: frozenCountRecord(exclusions),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function assertPlanBaseCurrent(
|
|
428
|
+
plan: SwapRevisionPlan,
|
|
429
|
+
current: {
|
|
430
|
+
readonly active: CompiledRevisionContext;
|
|
431
|
+
readonly revision: StoredContextRevisionV5;
|
|
432
|
+
readonly activeOverrides: readonly SwapOverride[];
|
|
433
|
+
readonly activePrepared: PreparedModelRequest;
|
|
434
|
+
},
|
|
435
|
+
): void {
|
|
436
|
+
const fingerprint = promptPrefixFingerprint(current.activePrepared);
|
|
437
|
+
if (
|
|
438
|
+
plan.baseRevisionId !== current.active.revisionId ||
|
|
439
|
+
plan.baseRevisionId !== current.revision.revisionId ||
|
|
440
|
+
plan.baseRevisionNumber !== current.revision.revisionNumber ||
|
|
441
|
+
plan.baseCanonicalThroughOrdinal !== current.active.canonicalThroughOrdinal ||
|
|
442
|
+
plan.baseOverrideManifestSha256 !==
|
|
443
|
+
activeOverrideManifestHash(current.activeOverrides) ||
|
|
444
|
+
plan.basePrefixHash !== fingerprint.prefixHash ||
|
|
445
|
+
plan.requestConfigHash !== fingerprint.requestConfigHash ||
|
|
446
|
+
plan.toolSchemaHash !== fingerprint.toolSchemaHash
|
|
447
|
+
) {
|
|
448
|
+
throw new SwapPlanStaleError(
|
|
449
|
+
"Swap revision plan base no longer matches the active request.",
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function validatePlanningInput(input: SwapPlanningInput): void {
|
|
455
|
+
if (input.policy.version !== "swap-only-v1") {
|
|
456
|
+
throw new SwapPlanningDiagnosticError(
|
|
457
|
+
"validate",
|
|
458
|
+
"unsupported_policy",
|
|
459
|
+
"Swap planning policy version is unsupported.",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (
|
|
463
|
+
input.active.sessionId !== input.canonical.sessionId ||
|
|
464
|
+
input.revision.sessionId !== input.canonical.sessionId ||
|
|
465
|
+
input.revision.revisionId !== input.active.revisionId ||
|
|
466
|
+
input.active.canonicalThroughOrdinal !== input.canonical.messages.length ||
|
|
467
|
+
input.revision.overrideManifestSha256 !==
|
|
468
|
+
activeOverrideManifestHash(input.activeOverrides)
|
|
469
|
+
) {
|
|
470
|
+
throw new ContextRevisionError(
|
|
471
|
+
"Active revision does not match canonical planning input.",
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (
|
|
475
|
+
input.forcedTargetTokens !== undefined &&
|
|
476
|
+
(!Number.isSafeInteger(input.forcedTargetTokens) || input.forcedTargetTokens < 0)
|
|
477
|
+
) {
|
|
478
|
+
throw new SwapPlanningDiagnosticError(
|
|
479
|
+
"validate",
|
|
480
|
+
"invalid_target",
|
|
481
|
+
"Forced swap target must be a non-negative safe integer.",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function assertActiveFingerprint(
|
|
487
|
+
input: SwapPlanningInput,
|
|
488
|
+
fingerprint: PromptPrefixFingerprint,
|
|
489
|
+
): void {
|
|
490
|
+
if (
|
|
491
|
+
input.activeUsage.prefixHash !== fingerprint.prefixHash ||
|
|
492
|
+
input.activeUsage.requestConfigHash !== fingerprint.requestConfigHash ||
|
|
493
|
+
input.activeUsage.toolSchemaHash !== fingerprint.toolSchemaHash
|
|
494
|
+
) {
|
|
495
|
+
throw new ContextRevisionError(
|
|
496
|
+
"Active usage fingerprint does not match the prepared request.",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function planningTarget(input: SwapPlanningInput): number {
|
|
502
|
+
if (input.trigger === "benchmark_forced") {
|
|
503
|
+
if (input.forcedTargetTokens === undefined) {
|
|
504
|
+
throw new SwapPlanningDiagnosticError(
|
|
505
|
+
"validate",
|
|
506
|
+
"missing_forced_target",
|
|
507
|
+
"Forced swap planning requires an explicit target.",
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return input.forcedTargetTokens;
|
|
511
|
+
}
|
|
512
|
+
if (input.forcedTargetTokens !== undefined) {
|
|
513
|
+
throw new SwapPlanningDiagnosticError(
|
|
514
|
+
"validate",
|
|
515
|
+
"unexpected_forced_target",
|
|
516
|
+
"Only benchmark-forced planning accepts an explicit target.",
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return Math.floor(
|
|
520
|
+
input.activeUsage.inputBudgetTokens * input.policy.targetInputRatio,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function emptyResult(
|
|
525
|
+
input: SwapPlanningInput,
|
|
526
|
+
outcome: Extract<
|
|
527
|
+
SwapPlanningOutcome,
|
|
528
|
+
"below_trigger" | "below_target" | "no_eligible_candidates"
|
|
529
|
+
>,
|
|
530
|
+
rawTokensBefore: number,
|
|
531
|
+
guardedTokensBefore: number,
|
|
532
|
+
targetTokens: number,
|
|
533
|
+
): SwapPlanningResult {
|
|
534
|
+
return Object.freeze({
|
|
535
|
+
outcome,
|
|
536
|
+
canonicalMessageCount: input.canonical.messages.length,
|
|
537
|
+
eligibleCandidateCount: 0,
|
|
538
|
+
excludedByReason: Object.freeze({}),
|
|
539
|
+
selectedByRawKind: Object.freeze({}),
|
|
540
|
+
originalObservationBytes: 0,
|
|
541
|
+
projectedObservationBytes: 0,
|
|
542
|
+
rawTokensBefore,
|
|
543
|
+
guardedTokensBefore,
|
|
544
|
+
targetTokens,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function basicExclusionReason(input: {
|
|
549
|
+
message: Extract<CanonicalMessageRecord, { role: "tool" }>;
|
|
550
|
+
result: ToolResultRecord | undefined;
|
|
551
|
+
closedFrames: ReadonlySet<string>;
|
|
552
|
+
protectedTurns: ReadonlySet<TurnId>;
|
|
553
|
+
minimumObservationBytes: number;
|
|
554
|
+
}): string | undefined {
|
|
555
|
+
if (!input.closedFrames.has(input.message.frameId)) {
|
|
556
|
+
return "frame_not_closed";
|
|
557
|
+
}
|
|
558
|
+
if (input.result === undefined) {
|
|
559
|
+
throw new ContextRevisionError(
|
|
560
|
+
"Canonical tool message is missing its tool result.",
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (input.result.completion.kind !== "returned") {
|
|
564
|
+
return "synthetic_completion";
|
|
565
|
+
}
|
|
566
|
+
if (input.message.name === "Recall") {
|
|
567
|
+
return "recall_tool";
|
|
568
|
+
}
|
|
569
|
+
if (input.protectedTurns.has(input.message.turnId)) {
|
|
570
|
+
return "protected_recent_turn";
|
|
571
|
+
}
|
|
572
|
+
if (
|
|
573
|
+
Buffer.byteLength(input.message.content, "utf8") < input.minimumObservationBytes
|
|
574
|
+
) {
|
|
575
|
+
return "observation_too_small";
|
|
576
|
+
}
|
|
577
|
+
const raw = input.result.completion.raw;
|
|
578
|
+
if (!isSwappableRawResult(raw)) {
|
|
579
|
+
return "raw_kind_not_allowlisted";
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
(raw.kind === "bash" && raw.status === "running") ||
|
|
583
|
+
(raw.kind === "task_output" &&
|
|
584
|
+
(raw.status === "running" || raw.status === "stopping"))
|
|
585
|
+
) {
|
|
586
|
+
return "running_task";
|
|
587
|
+
}
|
|
588
|
+
if (
|
|
589
|
+
input.message.contentSha256 !== input.result.observationSha256 ||
|
|
590
|
+
input.result.toolMessageId !== input.message.messageId
|
|
591
|
+
) {
|
|
592
|
+
throw new ContextRevisionError(
|
|
593
|
+
"Canonical tool result source or observation hash does not match.",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function protectedRecentTurns(
|
|
600
|
+
canonical: ProtocolContextView,
|
|
601
|
+
count: number,
|
|
602
|
+
): ReadonlySet<TurnId> {
|
|
603
|
+
const turns = new Set<TurnId>();
|
|
604
|
+
for (let index = canonical.messages.length - 1; index >= 0; index -= 1) {
|
|
605
|
+
const message = canonical.messages[index];
|
|
606
|
+
if (message === undefined || message.role === "system") {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
turns.add(message.turnId);
|
|
610
|
+
if (turns.size === count) {
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return turns;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function compareCandidates(left: EligibleCandidate, right: EligibleCandidate): number {
|
|
618
|
+
return (
|
|
619
|
+
right.override.byteSavings - left.override.byteSavings ||
|
|
620
|
+
right.override.originalBytes - left.override.originalBytes ||
|
|
621
|
+
left.override.ordinal - right.override.ordinal ||
|
|
622
|
+
left.override.messageId.localeCompare(right.override.messageId)
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function assertProspectiveConfiguration(
|
|
627
|
+
active: PreparedModelRequest,
|
|
628
|
+
prospective: PreparedModelRequest,
|
|
629
|
+
): void {
|
|
630
|
+
if (
|
|
631
|
+
prospective.provider !== active.provider ||
|
|
632
|
+
prospective.model !== active.model ||
|
|
633
|
+
prospective.requestConfigHash !== active.requestConfigHash ||
|
|
634
|
+
prospective.toolSchemaHash !== active.toolSchemaHash ||
|
|
635
|
+
prospective.requestMaxOutputTokens !== active.requestMaxOutputTokens
|
|
636
|
+
) {
|
|
637
|
+
throw new SwapPlanningDiagnosticError(
|
|
638
|
+
"validate",
|
|
639
|
+
"prospective_configuration_changed",
|
|
640
|
+
"Prospective request preparation changed its fixed configuration.",
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function createPlan(input: {
|
|
646
|
+
input: SwapPlanningInput;
|
|
647
|
+
activeFingerprint: PromptPrefixFingerprint;
|
|
648
|
+
projectedFingerprint: PromptPrefixFingerprint;
|
|
649
|
+
addedOverrides: readonly SwapOverride[];
|
|
650
|
+
targetTokens: number;
|
|
651
|
+
rawTokensBefore: number;
|
|
652
|
+
rawTokensAfter: number;
|
|
653
|
+
guardedTokensBefore: number;
|
|
654
|
+
guardedTokensAfter: number;
|
|
655
|
+
}): SwapRevisionPlan {
|
|
656
|
+
const planningInput = input.input;
|
|
657
|
+
const nextOverrideManifestSha256 = activeOverrideManifestHash([
|
|
658
|
+
...planningInput.activeOverrides,
|
|
659
|
+
...input.addedOverrides,
|
|
660
|
+
]);
|
|
661
|
+
const planIdentity = {
|
|
662
|
+
version: 1,
|
|
663
|
+
policyVersion: "swap-only-v1",
|
|
664
|
+
baseRevisionId: planningInput.active.revisionId,
|
|
665
|
+
baseRevisionNumber: planningInput.revision.revisionNumber,
|
|
666
|
+
baseCanonicalThroughOrdinal: planningInput.active.canonicalThroughOrdinal,
|
|
667
|
+
baseOverrideManifestSha256: planningInput.revision.overrideManifestSha256,
|
|
668
|
+
basePrefixHash: input.activeFingerprint.prefixHash,
|
|
669
|
+
requestConfigHash: input.activeFingerprint.requestConfigHash,
|
|
670
|
+
toolSchemaHash: input.activeFingerprint.toolSchemaHash,
|
|
671
|
+
addedOverrides: input.addedOverrides.map((override) => ({
|
|
672
|
+
messageId: override.messageId,
|
|
673
|
+
originalContentSha256: override.originalContentSha256,
|
|
674
|
+
renderedContentSha256: override.renderedContentSha256,
|
|
675
|
+
})),
|
|
676
|
+
nextOverrideManifestSha256,
|
|
677
|
+
targetTokens: input.targetTokens,
|
|
678
|
+
projectedPrefixHash: input.projectedFingerprint.prefixHash,
|
|
679
|
+
} as const;
|
|
680
|
+
return Object.freeze({
|
|
681
|
+
...planIdentity,
|
|
682
|
+
planHash: sha256(stableJsonStringify(planIdentity)),
|
|
683
|
+
addedOverrides: input.addedOverrides,
|
|
684
|
+
rawTokensBefore: input.rawTokensBefore,
|
|
685
|
+
rawTokensAfter: input.rawTokensAfter,
|
|
686
|
+
guardedTokensBefore: input.guardedTokensBefore,
|
|
687
|
+
guardedTokensAfter: input.guardedTokensAfter,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function countRawKinds(
|
|
692
|
+
candidates: readonly EligibleCandidate[],
|
|
693
|
+
): Readonly<Record<string, number>> {
|
|
694
|
+
const counts = new Map<string, number>();
|
|
695
|
+
for (const candidate of candidates) {
|
|
696
|
+
increment(counts, candidate.rawKind);
|
|
697
|
+
}
|
|
698
|
+
return frozenCountRecord(counts);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function frozenCountRecord(
|
|
702
|
+
counts: ReadonlyMap<string, number>,
|
|
703
|
+
): Readonly<Record<string, number>> {
|
|
704
|
+
return Object.freeze(
|
|
705
|
+
Object.fromEntries(
|
|
706
|
+
[...counts.entries()].sort(([left], [right]) => left.localeCompare(right)),
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function increment(counts: Map<string, number>, key: string): void {
|
|
712
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function guardTokens(rawTokens: number, correctionFactor: number): number {
|
|
716
|
+
return Math.ceil(rawTokens * correctionFactor);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function isCanonicalPlanningError(error: unknown): boolean {
|
|
720
|
+
return (
|
|
721
|
+
error instanceof ContextProtocolError ||
|
|
722
|
+
error instanceof ContextRevisionError ||
|
|
723
|
+
error instanceof CompiledContextError
|
|
724
|
+
);
|
|
725
|
+
}
|