tinker-agent 1.0.65 → 1.2.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 +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -13,6 +13,20 @@ export type ModelProfile = {
|
|
|
13
13
|
readonly contextWindowTokens: number;
|
|
14
14
|
readonly maxSupportedOutputTokens: number;
|
|
15
15
|
readonly includeReasoningContent: boolean;
|
|
16
|
+
readonly stream: boolean;
|
|
17
|
+
readonly inputModalities: readonly ModelInputModality[];
|
|
18
|
+
readonly tokenEstimator?: ModelTokenEstimatorProfile;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ModelInputModality = "text" | "image";
|
|
22
|
+
|
|
23
|
+
export type ModelTokenEstimatorProfile = {
|
|
24
|
+
readonly kind: "moonshot-estimate-token-count-v1";
|
|
25
|
+
readonly model: string;
|
|
26
|
+
readonly apiBase: string;
|
|
27
|
+
readonly apiKey: string;
|
|
28
|
+
readonly timeoutMs: number;
|
|
29
|
+
readonly maxRetries: 0;
|
|
16
30
|
};
|
|
17
31
|
|
|
18
32
|
export type ModelProfiles = {
|
|
@@ -222,6 +236,22 @@ function parseProfile(
|
|
|
222
236
|
throw new Error(`${where} must be an object.`);
|
|
223
237
|
}
|
|
224
238
|
|
|
239
|
+
assertKnownKeys(
|
|
240
|
+
value,
|
|
241
|
+
[
|
|
242
|
+
"model",
|
|
243
|
+
"apiBase",
|
|
244
|
+
"apiKey",
|
|
245
|
+
"contextWindowTokens",
|
|
246
|
+
"maxSupportedOutputTokens",
|
|
247
|
+
"includeReasoningContent",
|
|
248
|
+
"stream",
|
|
249
|
+
"inputModalities",
|
|
250
|
+
"tokenEstimator",
|
|
251
|
+
],
|
|
252
|
+
where,
|
|
253
|
+
);
|
|
254
|
+
|
|
225
255
|
const model = requireString(value.model, `${where}: "model"`);
|
|
226
256
|
const apiBase = requireString(value.apiBase, `${where}: "apiBase"`);
|
|
227
257
|
const apiKey = requireString(value.apiKey, `${where}: "apiKey"`);
|
|
@@ -243,6 +273,25 @@ function parseProfile(
|
|
|
243
273
|
`${where}: "includeReasoningContent"`,
|
|
244
274
|
);
|
|
245
275
|
|
|
276
|
+
const stream =
|
|
277
|
+
value.stream === undefined
|
|
278
|
+
? true
|
|
279
|
+
: parseBoolean(value.stream, `${where}: "stream"`);
|
|
280
|
+
|
|
281
|
+
const inputModalities = parseInputModalities(
|
|
282
|
+
value.inputModalities,
|
|
283
|
+
`${where}: "inputModalities"`,
|
|
284
|
+
);
|
|
285
|
+
const tokenEstimator =
|
|
286
|
+
value.tokenEstimator === undefined
|
|
287
|
+
? undefined
|
|
288
|
+
: parseTokenEstimator(value.tokenEstimator, `${where}: "tokenEstimator"`);
|
|
289
|
+
if (inputModalities.includes("image") && tokenEstimator === undefined) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`${where}: image input requires a complete "tokenEstimator" configuration.`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
246
295
|
createModelContextProfile({
|
|
247
296
|
contextWindowTokens,
|
|
248
297
|
maxSupportedOutputTokens,
|
|
@@ -256,9 +305,84 @@ function parseProfile(
|
|
|
256
305
|
contextWindowTokens,
|
|
257
306
|
maxSupportedOutputTokens,
|
|
258
307
|
includeReasoningContent,
|
|
308
|
+
stream,
|
|
309
|
+
inputModalities,
|
|
310
|
+
...(tokenEstimator === undefined ? {} : { tokenEstimator }),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseInputModalities(
|
|
315
|
+
value: unknown,
|
|
316
|
+
name: string,
|
|
317
|
+
): readonly ModelInputModality[] {
|
|
318
|
+
if (value === undefined) {
|
|
319
|
+
return Object.freeze(["text"]);
|
|
320
|
+
}
|
|
321
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
322
|
+
throw new Error(`${name} must be a non-empty array.`);
|
|
323
|
+
}
|
|
324
|
+
const modalities = value.map((rawEntry): ModelInputModality => {
|
|
325
|
+
const entry: unknown = rawEntry;
|
|
326
|
+
if (entry !== "text" && entry !== "image") {
|
|
327
|
+
throw new Error(`${name} contains an unsupported modality.`);
|
|
328
|
+
}
|
|
329
|
+
return entry;
|
|
330
|
+
});
|
|
331
|
+
if (new Set(modalities).size !== modalities.length) {
|
|
332
|
+
throw new Error(`${name} must not contain duplicates.`);
|
|
333
|
+
}
|
|
334
|
+
if (!modalities.includes("text")) {
|
|
335
|
+
throw new Error(`${name} must include "text".`);
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze(
|
|
338
|
+
modalities.includes("image") ? (["text", "image"] as const) : (["text"] as const),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function parseTokenEstimator(value: unknown, name: string): ModelTokenEstimatorProfile {
|
|
343
|
+
if (!isRecord(value)) {
|
|
344
|
+
throw new Error(`${name} must be an object.`);
|
|
345
|
+
}
|
|
346
|
+
assertKnownKeys(
|
|
347
|
+
value,
|
|
348
|
+
["kind", "model", "apiBase", "apiKey", "timeoutMs", "maxRetries"],
|
|
349
|
+
name,
|
|
350
|
+
);
|
|
351
|
+
if (value.kind !== "moonshot-estimate-token-count-v1") {
|
|
352
|
+
throw new Error(`${name}.kind must be "moonshot-estimate-token-count-v1".`);
|
|
353
|
+
}
|
|
354
|
+
const model = requireString(value.model, `${name}.model`);
|
|
355
|
+
const apiBase = requireString(value.apiBase, `${name}.apiBase`);
|
|
356
|
+
const apiKey = requireString(value.apiKey, `${name}.apiKey`);
|
|
357
|
+
const timeoutMs = requirePositiveInteger(value.timeoutMs, `${name}.timeoutMs`);
|
|
358
|
+
if (timeoutMs < 1_000 || timeoutMs > 60_000) {
|
|
359
|
+
throw new Error(`${name}.timeoutMs must be between 1000 and 60000.`);
|
|
360
|
+
}
|
|
361
|
+
if (value.maxRetries !== 0) {
|
|
362
|
+
throw new Error(`${name}.maxRetries must be 0.`);
|
|
363
|
+
}
|
|
364
|
+
return Object.freeze({
|
|
365
|
+
kind: value.kind,
|
|
366
|
+
model,
|
|
367
|
+
apiBase,
|
|
368
|
+
apiKey,
|
|
369
|
+
timeoutMs,
|
|
370
|
+
maxRetries: 0,
|
|
259
371
|
});
|
|
260
372
|
}
|
|
261
373
|
|
|
374
|
+
function assertKnownKeys(
|
|
375
|
+
value: Record<string, unknown>,
|
|
376
|
+
allowed: readonly string[],
|
|
377
|
+
name: string,
|
|
378
|
+
): void {
|
|
379
|
+
const allowedSet = new Set(allowed);
|
|
380
|
+
const unknown = Object.keys(value).find((key) => !allowedSet.has(key));
|
|
381
|
+
if (unknown !== undefined) {
|
|
382
|
+
throw new Error(`${name} contains unknown field ${JSON.stringify(unknown)}.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
262
386
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
263
387
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
264
388
|
}
|
package/src/cli/run-runner.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./config";
|
|
20
20
|
import { loadModelProfiles } from "./model-profiles";
|
|
21
21
|
import { realpath } from "node:fs/promises";
|
|
22
|
+
import { loadSkillCatalog } from "../skills/skill-loader";
|
|
22
23
|
|
|
23
24
|
export type RunOneShotOptions = RunnerConfigOverrides & {
|
|
24
25
|
modelClient?: ModelClient;
|
|
@@ -44,6 +45,7 @@ export async function runOneShot(
|
|
|
44
45
|
const config = readRunnerConfig(options, profiles);
|
|
45
46
|
const workspaceRoot = await realpath(config.workspaceRoot);
|
|
46
47
|
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
48
|
+
const skillCatalog = await loadSkillCatalog({ workspaceRoot });
|
|
47
49
|
const systemPrompt = buildSystemPrompt({
|
|
48
50
|
workspaceRoot,
|
|
49
51
|
runtimeInstructions: RUNTIME_INSTRUCTIONS(workspaceRoot),
|
|
@@ -61,6 +63,7 @@ export async function runOneShot(
|
|
|
61
63
|
contextBudget: config.contextBudget,
|
|
62
64
|
systemPrompt,
|
|
63
65
|
projectInstruction: projectInstructionManifest(projectInstructions),
|
|
66
|
+
skillCatalog,
|
|
64
67
|
modelClient,
|
|
65
68
|
presentationSinks: [new StdoutEventPrinter(stdout, stderr)],
|
|
66
69
|
persistence:
|
|
@@ -69,7 +72,7 @@ export async function runOneShot(
|
|
|
69
72
|
});
|
|
70
73
|
|
|
71
74
|
const result = await session.executeTurn({
|
|
72
|
-
userPrompt,
|
|
75
|
+
userMessage: { role: "user", content: userPrompt },
|
|
73
76
|
signal: new AbortController().signal,
|
|
74
77
|
});
|
|
75
78
|
if (result.status === "completed") {
|
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
DefaultTuiSessionController,
|
|
25
25
|
managedTuiBinding,
|
|
26
26
|
type ManagedTuiSessionBinding,
|
|
27
|
+
type TuiSessionBinding,
|
|
27
28
|
} from "../tui/tui-session-controller";
|
|
28
29
|
import {
|
|
29
30
|
createRunnerModelClient,
|
|
@@ -39,6 +40,8 @@ import {
|
|
|
39
40
|
resolveSessionProfileName,
|
|
40
41
|
type ModelProfile,
|
|
41
42
|
} from "./model-profiles";
|
|
43
|
+
import { loadSkillCatalog } from "../skills/skill-loader";
|
|
44
|
+
import { loadProjectSlashCommands } from "../tui/project-slash-commands";
|
|
42
45
|
|
|
43
46
|
export async function runTui(options: { profileName?: string } = {}): Promise<void> {
|
|
44
47
|
const profiles = await loadModelProfiles();
|
|
@@ -54,6 +57,7 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
54
57
|
let quitRequested = false;
|
|
55
58
|
|
|
56
59
|
try {
|
|
60
|
+
const projectSlashCommands = await loadProjectSlashCommands(workspaceRoot);
|
|
57
61
|
const createSessionForConfig = async (
|
|
58
62
|
sessionConfig: RunnerConfig,
|
|
59
63
|
mode: "new" | "resume",
|
|
@@ -61,6 +65,8 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
61
65
|
sink: EventSink,
|
|
62
66
|
): Promise<RuntimeSession> => {
|
|
63
67
|
const modelClient = createRunnerModelClient(sessionConfig);
|
|
68
|
+
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
69
|
+
const skillCatalog = await loadSkillCatalog({ workspaceRoot });
|
|
64
70
|
const common = {
|
|
65
71
|
workspaceRoot,
|
|
66
72
|
modelName: sessionConfig.modelName,
|
|
@@ -70,6 +76,13 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
70
76
|
contextProfile: sessionConfig.contextProfile,
|
|
71
77
|
contextBudget: sessionConfig.contextBudget,
|
|
72
78
|
modelClient,
|
|
79
|
+
systemPrompt: buildSystemPrompt({
|
|
80
|
+
workspaceRoot,
|
|
81
|
+
runtimeInstructions: RUNTIME_INSTRUCTIONS(workspaceRoot),
|
|
82
|
+
projectInstructions,
|
|
83
|
+
}),
|
|
84
|
+
projectInstruction: projectInstructionManifest(projectInstructions),
|
|
85
|
+
skillCatalog,
|
|
73
86
|
presentationSinks: [sink],
|
|
74
87
|
webFetchRefiner: createWebFetchRefinerFromEnv(sessionConfig),
|
|
75
88
|
};
|
|
@@ -80,16 +93,9 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
80
93
|
});
|
|
81
94
|
}
|
|
82
95
|
|
|
83
|
-
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
84
96
|
return createRuntimeSession({
|
|
85
97
|
...common,
|
|
86
98
|
selection: { mode, sessionId },
|
|
87
|
-
systemPrompt: buildSystemPrompt({
|
|
88
|
-
workspaceRoot,
|
|
89
|
-
runtimeInstructions: RUNTIME_INSTRUCTIONS(workspaceRoot),
|
|
90
|
-
projectInstructions,
|
|
91
|
-
}),
|
|
92
|
-
projectInstruction: projectInstructionManifest(projectInstructions),
|
|
93
99
|
});
|
|
94
100
|
};
|
|
95
101
|
|
|
@@ -159,41 +165,71 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
159
165
|
});
|
|
160
166
|
};
|
|
161
167
|
|
|
162
|
-
const
|
|
163
|
-
|
|
168
|
+
const createNewSessionBinding = async (
|
|
169
|
+
sessionConfig: RunnerConfig,
|
|
164
170
|
): Promise<ManagedTuiSessionBinding> => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const profileConfig = readRunnerConfig(
|
|
169
|
-
{
|
|
170
|
-
sessionId: createUuidV7() as SessionId,
|
|
171
|
-
workspaceRoot: config.workspaceRoot,
|
|
172
|
-
profileName: profile.name,
|
|
173
|
-
maxIterations: config.maxIterations,
|
|
174
|
-
},
|
|
175
|
-
profiles,
|
|
176
|
-
);
|
|
177
|
-
const profileProjectionStore = new TuiProjectionStore({
|
|
178
|
-
sessionId: profileConfig.sessionId,
|
|
179
|
-
modelName: profileConfig.modelName,
|
|
171
|
+
const freshProjectionStore = new TuiProjectionStore({
|
|
172
|
+
sessionId: sessionConfig.sessionId,
|
|
173
|
+
modelName: sessionConfig.modelName,
|
|
180
174
|
workspaceRoot,
|
|
181
175
|
});
|
|
182
176
|
const runtimeSession = await createSessionForConfig(
|
|
183
|
-
|
|
177
|
+
sessionConfig,
|
|
184
178
|
"new",
|
|
185
|
-
|
|
186
|
-
|
|
179
|
+
sessionConfig.sessionId,
|
|
180
|
+
freshProjectionStore,
|
|
187
181
|
);
|
|
188
182
|
return managedTuiBinding({
|
|
189
183
|
runtimeSession,
|
|
190
|
-
modelName:
|
|
184
|
+
modelName: sessionConfig.modelName,
|
|
191
185
|
workspaceRoot,
|
|
192
|
-
profileName:
|
|
193
|
-
projectionStore:
|
|
186
|
+
profileName: sessionConfig.profileName,
|
|
187
|
+
projectionStore: freshProjectionStore,
|
|
194
188
|
});
|
|
195
189
|
};
|
|
196
190
|
|
|
191
|
+
const createSessionWithProfile = async (
|
|
192
|
+
profile: ModelProfile,
|
|
193
|
+
): Promise<ManagedTuiSessionBinding> => {
|
|
194
|
+
if (profiles === undefined) {
|
|
195
|
+
throw new Error("Model profiles are not configured.");
|
|
196
|
+
}
|
|
197
|
+
return createNewSessionBinding(
|
|
198
|
+
readRunnerConfig(
|
|
199
|
+
{
|
|
200
|
+
sessionId: createUuidV7() as SessionId,
|
|
201
|
+
workspaceRoot: config.workspaceRoot,
|
|
202
|
+
profileName: profile.name,
|
|
203
|
+
maxIterations: config.maxIterations,
|
|
204
|
+
},
|
|
205
|
+
profiles,
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const createFreshSession = async (
|
|
211
|
+
current: TuiSessionBinding,
|
|
212
|
+
): Promise<ManagedTuiSessionBinding> => {
|
|
213
|
+
const sessionId = createUuidV7() as SessionId;
|
|
214
|
+
if (profiles === undefined) {
|
|
215
|
+
return createNewSessionBinding({ ...config, sessionId });
|
|
216
|
+
}
|
|
217
|
+
if (current.profileName === undefined) {
|
|
218
|
+
throw new Error("Current session does not have a model profile.");
|
|
219
|
+
}
|
|
220
|
+
return createNewSessionBinding(
|
|
221
|
+
readRunnerConfig(
|
|
222
|
+
{
|
|
223
|
+
sessionId,
|
|
224
|
+
workspaceRoot: config.workspaceRoot,
|
|
225
|
+
profileName: current.profileName,
|
|
226
|
+
maxIterations: config.maxIterations,
|
|
227
|
+
},
|
|
228
|
+
profiles,
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
197
233
|
controller = new DefaultTuiSessionController(
|
|
198
234
|
managedTuiBinding({
|
|
199
235
|
runtimeSession: initialSession,
|
|
@@ -205,6 +241,7 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
205
241
|
catalog,
|
|
206
242
|
openStoredSession,
|
|
207
243
|
createSessionWithProfile,
|
|
244
|
+
createFreshSession,
|
|
208
245
|
);
|
|
209
246
|
|
|
210
247
|
instance = render(
|
|
@@ -212,6 +249,7 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
212
249
|
sessionController={controller}
|
|
213
250
|
readGitBranch={readCurrentGitBranch}
|
|
214
251
|
history={promptHistory}
|
|
252
|
+
projectSlashCommands={projectSlashCommands}
|
|
215
253
|
profiles={profiles}
|
|
216
254
|
persistDefaultProfile={persistDefaultProfile}
|
|
217
255
|
onQuit={() => {
|
|
@@ -92,7 +92,7 @@ export function activeOverrideManifestHash(overrides: readonly SwapOverride[]):
|
|
|
92
92
|
ordinal: override.ordinal,
|
|
93
93
|
originalContentSha256: override.originalContentSha256,
|
|
94
94
|
renderedContentSha256: override.renderedContentSha256,
|
|
95
|
-
rendererFormat: "swap-observation-v1",
|
|
95
|
+
rendererFormat: override.rendererFormat ?? "swap-observation-v1",
|
|
96
96
|
})),
|
|
97
97
|
),
|
|
98
98
|
);
|
|
@@ -2,6 +2,7 @@ import type { AgentMessage } from "../agent/types";
|
|
|
2
2
|
import { stableJsonStringify } from "../model/model-request-preflight";
|
|
3
3
|
import { formatMessageSource } from "./context-source";
|
|
4
4
|
import type { CompiledRevisionContext, SwapOverride } from "./context-revision";
|
|
5
|
+
import type { StoredContextSurfaceV8 } from "./context-surface";
|
|
5
6
|
import {
|
|
6
7
|
contentHash,
|
|
7
8
|
type CanonicalMessageRecord,
|
|
@@ -21,38 +22,89 @@ export class CompiledContextValidator {
|
|
|
21
22
|
compiled: CompiledRevisionContext,
|
|
22
23
|
canonical: ProtocolContextView,
|
|
23
24
|
overrides: readonly SwapOverride[],
|
|
25
|
+
surface: StoredContextSurfaceV8,
|
|
24
26
|
): void {
|
|
25
27
|
const byMessageId = overrideMap(overrides, "Active");
|
|
26
|
-
this.validate(compiled, canonical, byMessageId);
|
|
28
|
+
this.validate(compiled, canonical, byMessageId, surface);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
validateProspective(
|
|
30
32
|
compiled: CompiledRevisionContext,
|
|
31
33
|
canonical: ProtocolContextView,
|
|
32
34
|
overrides: readonly SwapOverride[],
|
|
35
|
+
surface: StoredContextSurfaceV8,
|
|
33
36
|
): void {
|
|
34
37
|
const byMessageId = overrideMap(overrides, "Prospective");
|
|
35
|
-
this.validate(compiled, canonical, byMessageId);
|
|
38
|
+
this.validate(compiled, canonical, byMessageId, surface);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
private validate(
|
|
39
42
|
compiled: CompiledRevisionContext,
|
|
40
43
|
canonical: ProtocolContextView,
|
|
41
44
|
overrides: ReadonlyMap<string, SwapOverride>,
|
|
45
|
+
surface: StoredContextSurfaceV8,
|
|
42
46
|
): void {
|
|
43
47
|
if (compiled.sessionId !== canonical.sessionId) {
|
|
44
48
|
fail("Compiled context belongs to another session.");
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
const keepFromOrdinal = compiled.manifest.keepFromOrdinal;
|
|
51
|
+
if (
|
|
52
|
+
!Number.isSafeInteger(keepFromOrdinal) ||
|
|
53
|
+
keepFromOrdinal < 1 ||
|
|
54
|
+
keepFromOrdinal > canonical.messages.length
|
|
55
|
+
) {
|
|
56
|
+
fail("Compiled context keep boundary is invalid.");
|
|
57
|
+
}
|
|
58
|
+
const activeRecords =
|
|
59
|
+
keepFromOrdinal === 1
|
|
60
|
+
? canonical.messages
|
|
61
|
+
: [
|
|
62
|
+
requireItem(canonical.messages, 0, "canonical system message"),
|
|
63
|
+
...canonical.messages.slice(keepFromOrdinal - 1),
|
|
64
|
+
];
|
|
65
|
+
const activeFrames =
|
|
66
|
+
keepFromOrdinal === 1
|
|
67
|
+
? canonical.frames
|
|
68
|
+
: [
|
|
69
|
+
requireItem(canonical.frames, 0, "canonical system frame"),
|
|
70
|
+
...canonical.frames.filter(
|
|
71
|
+
(frame) => frame.firstOrdinal >= keepFromOrdinal,
|
|
72
|
+
),
|
|
73
|
+
];
|
|
74
|
+
if (keepFromOrdinal > 1) {
|
|
75
|
+
const boundaryMessage = canonical.messages[keepFromOrdinal - 1];
|
|
76
|
+
const boundaryFrame = canonical.frames.find(
|
|
77
|
+
(frame) => frame.firstOrdinal === keepFromOrdinal,
|
|
78
|
+
);
|
|
79
|
+
if (
|
|
80
|
+
boundaryMessage?.ordinal !== keepFromOrdinal ||
|
|
81
|
+
boundaryMessage.role !== "user" ||
|
|
82
|
+
boundaryFrame?.kind !== "user" ||
|
|
83
|
+
boundaryFrame.state !== "closed" ||
|
|
84
|
+
boundaryFrame.lastOrdinal !== keepFromOrdinal
|
|
85
|
+
) {
|
|
86
|
+
fail("Compiled context keep boundary does not start a closed user frame.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (compiled.entries.length !== activeRecords.length) {
|
|
90
|
+
fail("Compiled context does not match the active message projection.");
|
|
48
91
|
}
|
|
49
92
|
if (
|
|
50
93
|
compiled.canonicalThroughOrdinal !== canonical.messages.length ||
|
|
51
|
-
compiled.manifest.
|
|
52
|
-
compiled.manifest.
|
|
94
|
+
compiled.manifest.canonicalFrameCount !== canonical.frames.length ||
|
|
95
|
+
compiled.manifest.canonicalMessageCount !== canonical.messages.length ||
|
|
96
|
+
compiled.manifest.activeFrameCount !== activeFrames.length ||
|
|
97
|
+
compiled.manifest.activeMessageCount !== compiled.entries.length
|
|
53
98
|
) {
|
|
54
99
|
fail("Compiled context manifest counts do not match canonical history.");
|
|
55
100
|
}
|
|
101
|
+
if (overrides.size > 0) {
|
|
102
|
+
for (const override of overrides.values()) {
|
|
103
|
+
if (override.ordinal < keepFromOrdinal) {
|
|
104
|
+
fail("Compiled context contains an override before its keep boundary.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
56
108
|
|
|
57
109
|
const seenFrames = new Set<string>();
|
|
58
110
|
const seenMessages = new Set<string>();
|
|
@@ -61,7 +113,7 @@ export class CompiledContextValidator {
|
|
|
61
113
|
|
|
62
114
|
for (let index = 0; index < compiled.entries.length; index += 1) {
|
|
63
115
|
const entry = requireItem(compiled.entries, index, "compiled entry");
|
|
64
|
-
const record = requireItem(
|
|
116
|
+
const record = requireItem(activeRecords, index, "active canonical message");
|
|
65
117
|
if (
|
|
66
118
|
entry.frameId !== record.frameId ||
|
|
67
119
|
entry.messageId !== record.messageId ||
|
|
@@ -79,10 +131,19 @@ export class CompiledContextValidator {
|
|
|
79
131
|
|
|
80
132
|
const override = overrides.get(entry.messageId);
|
|
81
133
|
if (override === undefined) {
|
|
82
|
-
if (entry.representation
|
|
83
|
-
|
|
134
|
+
if (entry.representation === "canonical") {
|
|
135
|
+
assertSameMessage(entry.message, record);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (
|
|
139
|
+
entry.representation !== "surface" ||
|
|
140
|
+
record.ordinal !== 1 ||
|
|
141
|
+
record.role !== "system" ||
|
|
142
|
+
entry.message.role !== "system" ||
|
|
143
|
+
entry.message.content !== surface.systemPrompt
|
|
144
|
+
) {
|
|
145
|
+
fail(`Unapproved surface entry at ordinal ${entry.ordinal}.`);
|
|
84
146
|
}
|
|
85
|
-
assertSameMessage(entry.message, record);
|
|
86
147
|
continue;
|
|
87
148
|
}
|
|
88
149
|
|
|
@@ -104,15 +165,19 @@ export class CompiledContextValidator {
|
|
|
104
165
|
}
|
|
105
166
|
}
|
|
106
167
|
|
|
107
|
-
if (
|
|
108
|
-
|
|
168
|
+
if (
|
|
169
|
+
seenFrames.size !== activeFrames.length ||
|
|
170
|
+
activeFrames.some((frame) => !seenFrames.has(frame.frameId))
|
|
171
|
+
) {
|
|
172
|
+
fail("Compiled entries do not preserve every active protocol frame.");
|
|
109
173
|
}
|
|
110
174
|
if (consumedOverrides.size !== overrides.size) {
|
|
111
175
|
fail("A prospective override does not reference canonical history.");
|
|
112
176
|
}
|
|
113
177
|
if (
|
|
114
178
|
compiled.manifest.canonicalSequenceHash !== canonicalSequenceHash(canonical) ||
|
|
115
|
-
compiled.manifest.renderedMessageHash !== renderedMessageHash(compiled.entries)
|
|
179
|
+
compiled.manifest.renderedMessageHash !== renderedMessageHash(compiled.entries) ||
|
|
180
|
+
compiled.manifest.surfaceSha256 !== surface.surfaceSha256
|
|
116
181
|
) {
|
|
117
182
|
fail("Compiled context manifest hash does not match its entries.");
|
|
118
183
|
}
|
|
@@ -136,13 +201,16 @@ function validateOverrideIdentity(
|
|
|
136
201
|
"utf8",
|
|
137
202
|
);
|
|
138
203
|
const renderedBytes = Buffer.byteLength(override.renderedContent, "utf8");
|
|
204
|
+
const rendererFormat = override.rendererFormat ?? "swap-observation-v1";
|
|
139
205
|
if (
|
|
140
206
|
override.source !== formatMessageSource(record.messageId) ||
|
|
141
207
|
override.renderedContentSha256 !== contentHash(override.renderedContent) ||
|
|
142
208
|
override.originalBytes !== originalBytes ||
|
|
143
209
|
override.renderedBytes !== renderedBytes ||
|
|
144
210
|
override.byteSavings !== originalBytes - renderedBytes ||
|
|
145
|
-
override.byteSavings <= 0
|
|
211
|
+
(rendererFormat === "swap-observation-v1" && override.byteSavings <= 0) ||
|
|
212
|
+
(rendererFormat !== "swap-observation-v1" &&
|
|
213
|
+
rendererFormat !== "skill-activation-receipt-v1")
|
|
146
214
|
) {
|
|
147
215
|
fail(`Swap override metadata is invalid at ordinal ${record.ordinal}.`);
|
|
148
216
|
}
|
|
@@ -167,11 +235,20 @@ function assertSameMessage(actual: AgentMessage, record: CanonicalMessageRecord)
|
|
|
167
235
|
}
|
|
168
236
|
switch (record.role) {
|
|
169
237
|
case "system":
|
|
170
|
-
case "user":
|
|
171
238
|
if (actual.role !== record.role || actual.content !== record.content) {
|
|
172
239
|
fail(`Canonical entry changed message content at ordinal ${record.ordinal}.`);
|
|
173
240
|
}
|
|
174
241
|
return;
|
|
242
|
+
case "user":
|
|
243
|
+
if (
|
|
244
|
+
actual.role !== "user" ||
|
|
245
|
+
actual.content !== record.content ||
|
|
246
|
+
stableJsonStringify(actual.attachments ?? null) !==
|
|
247
|
+
stableJsonStringify(record.attachments ?? null)
|
|
248
|
+
) {
|
|
249
|
+
fail(`Canonical entry changed user data at ordinal ${record.ordinal}.`);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
175
252
|
case "assistant":
|
|
176
253
|
if (
|
|
177
254
|
actual.role !== "assistant" ||
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { stableJsonStringify, sha256 } from "../model/model-request-preflight";
|
|
2
|
+
import { RECALL_TOOL_DEFINITION } from "../tools/recall";
|
|
3
|
+
import type { ToolDefinition } from "../tools/types";
|
|
4
|
+
import type { StoredContextSurfaceV8 } from "./context-surface";
|
|
5
|
+
import {
|
|
6
|
+
CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION,
|
|
7
|
+
renderRecallRetirementContract,
|
|
8
|
+
} from "./recall-retirement-contract";
|
|
9
|
+
|
|
10
|
+
export const I4_ACTIVE_RECALL_QUALIFICATION = Object.freeze({
|
|
11
|
+
qualificationId: "deepseek-v4-flash-floor-v1",
|
|
12
|
+
evaluatedProfile: "deepseek-v4-flash",
|
|
13
|
+
manifestVersion: "active-recall-manifest-v1",
|
|
14
|
+
manifestSha256: "093679e221e02b71ba5acf54693faa7a299d05dd25f6faabbeb84645d4db4d2d",
|
|
15
|
+
graderVersion: "active-recall-deterministic-grader-v1",
|
|
16
|
+
fixtureVersion: "active-recall-long-session-fixture-v1",
|
|
17
|
+
policyVersion: "active-recall-qualification-policy-v1",
|
|
18
|
+
policySha256: "77ca611594d4e9b7b5a597a3a33e35fcaaffae284dc2ac9953ce9a63cce1c009",
|
|
19
|
+
positiveReportSha256:
|
|
20
|
+
"ef84dfee3fdefd7bbf1be2f1641bccc4a1aa06874b22172fecc03dfa2db840d6",
|
|
21
|
+
negativeReportSha256:
|
|
22
|
+
"c991980ae2824102174315c9b25c1e4affdbbd22aac320e1a254f2702ba3d1b4",
|
|
23
|
+
resolvedModel: "deepseek-v4-flash",
|
|
24
|
+
recallContractVersion: CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION,
|
|
25
|
+
recallContractSha256:
|
|
26
|
+
"c75108d351ed3223928ebeb52c7258a6f95033287a8e560fb5b2efff8e084c9b",
|
|
27
|
+
recallToolDefinitionSha256:
|
|
28
|
+
"60caf87f313ea35e74e278ab8b30337cc8af1cc83d82d957a73857a764f3e3d9",
|
|
29
|
+
metrics: Object.freeze({
|
|
30
|
+
fullHistoryTaskSuccessRate: 1,
|
|
31
|
+
swapOnlyTaskSuccessRate: 1,
|
|
32
|
+
recallOnlyTaskSuccessRate: 0.9667,
|
|
33
|
+
recallOnlyActiveRecallRate: 0.9667,
|
|
34
|
+
recallOnlySearchGetSuccessRate: 0.3,
|
|
35
|
+
minimumCounterfactualGroupTaskSuccessRate: 0.8333,
|
|
36
|
+
invalidRecallCallsPerRecallOnlyTrial: 0.0333,
|
|
37
|
+
negativeUnnecessaryRecallRate: 0,
|
|
38
|
+
recallOnlyTokenRatioToFullHistory: 1.1555,
|
|
39
|
+
recallOnlyLatencyRatioToFullHistory: 1.365,
|
|
40
|
+
}),
|
|
41
|
+
passed: true,
|
|
42
|
+
} as const);
|
|
43
|
+
|
|
44
|
+
export const I4_SWAP_ONLY_QUALIFICATION_ID = "swap-only-engineering-v1";
|
|
45
|
+
|
|
46
|
+
export type ActiveRecallQualificationEvidence = {
|
|
47
|
+
readonly qualificationId: string;
|
|
48
|
+
readonly recallContractVersion: string;
|
|
49
|
+
readonly recallContractSha256: string;
|
|
50
|
+
readonly recallToolDefinitionSha256: string;
|
|
51
|
+
readonly passed: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ContextAutomationDecision = {
|
|
55
|
+
readonly automaticSwapOnly: boolean;
|
|
56
|
+
readonly automaticPrefixRetirement: boolean;
|
|
57
|
+
readonly reason:
|
|
58
|
+
| "qualified"
|
|
59
|
+
| "swap_only_qualified"
|
|
60
|
+
| "qualification_pending"
|
|
61
|
+
| "unprofiled_model"
|
|
62
|
+
| "recall_contract_mismatch"
|
|
63
|
+
| "recall_tool_mismatch";
|
|
64
|
+
readonly qualificationId?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function selectContextAutomation(
|
|
68
|
+
input: {
|
|
69
|
+
readonly profileName?: string;
|
|
70
|
+
readonly surface: StoredContextSurfaceV8;
|
|
71
|
+
},
|
|
72
|
+
evidence: ActiveRecallQualificationEvidence = I4_ACTIVE_RECALL_QUALIFICATION,
|
|
73
|
+
): ContextAutomationDecision {
|
|
74
|
+
if (input.profileName === undefined) {
|
|
75
|
+
return disabled("unprofiled_model");
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
input.surface.recallContractVersion !== evidence.recallContractVersion ||
|
|
79
|
+
sha256(renderRecallRetirementContract()) !== evidence.recallContractSha256
|
|
80
|
+
) {
|
|
81
|
+
return disabled("recall_contract_mismatch");
|
|
82
|
+
}
|
|
83
|
+
const recall = input.surface.toolDefinitions.find(
|
|
84
|
+
(definition) => definition.name === "Recall",
|
|
85
|
+
);
|
|
86
|
+
if (
|
|
87
|
+
recall === undefined ||
|
|
88
|
+
toolDefinitionHash(recall) !== evidence.recallToolDefinitionSha256 ||
|
|
89
|
+
toolDefinitionHash(RECALL_TOOL_DEFINITION) !== evidence.recallToolDefinitionSha256
|
|
90
|
+
) {
|
|
91
|
+
return disabled("recall_tool_mismatch");
|
|
92
|
+
}
|
|
93
|
+
if (!evidence.passed) {
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
automaticSwapOnly: true,
|
|
96
|
+
automaticPrefixRetirement: false,
|
|
97
|
+
reason: "swap_only_qualified",
|
|
98
|
+
qualificationId: I4_SWAP_ONLY_QUALIFICATION_ID,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
automaticSwapOnly: true,
|
|
103
|
+
automaticPrefixRetirement: true,
|
|
104
|
+
reason: "qualified",
|
|
105
|
+
qualificationId: evidence.qualificationId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function disabled(
|
|
110
|
+
reason: Exclude<ContextAutomationDecision["reason"], "qualified">,
|
|
111
|
+
): ContextAutomationDecision {
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
automaticSwapOnly: false,
|
|
114
|
+
automaticPrefixRetirement: false,
|
|
115
|
+
reason,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toolDefinitionHash(definition: ToolDefinition): string {
|
|
120
|
+
return sha256(stableJsonStringify(definition));
|
|
121
|
+
}
|