tinker-agent 1.9.0 → 1.10.1
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 +26 -1
- package/README.md +64 -6
- package/package.json +1 -1
- package/src/agent/loop.ts +13 -0
- package/src/agent/runtime-session.ts +165 -0
- package/src/agent/session-ledger.ts +20 -3
- package/src/cli/config.ts +11 -2
- package/src/cli/model-profiles.ts +58 -0
- package/src/cli/public-config-contract.ts +73 -7
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/runner-dependencies.ts +28 -4
- package/src/cli/tui-memory.ts +4 -0
- package/src/cli/tui-runner.tsx +8 -1
- package/src/context/context-automation-policy.ts +22 -21
- package/src/context/context-manager.ts +91 -15
- package/src/context/context-policy.ts +0 -2
- package/src/context/context-swap-renderer.ts +1 -1
- package/src/context/prefix-retirement-planner.ts +58 -8
- package/src/context/recall-retirement-contract.ts +5 -4
- package/src/context/swap-planner.ts +33 -27
- package/src/model/fake-model-client.ts +26 -16
- package/src/model/model-api.ts +12 -0
- package/src/model/model-client.ts +9 -1
- package/src/model/moonshot-input-token-estimator.ts +5 -1
- package/src/model/openai-chat-mapping.ts +2 -24
- package/src/model/openai-chat-model-client.ts +18 -294
- package/src/model/openai-image-mapping.ts +20 -0
- package/src/model/openai-model-utils.ts +304 -0
- package/src/model/openai-responses-mapping.ts +532 -0
- package/src/model/openai-responses-model-client.ts +295 -0
- package/src/model/openai-responses-stream.ts +96 -0
- package/src/model/openai-responses-token-estimator.ts +155 -0
- package/src/model/reasoning-effort.ts +60 -0
- package/src/session/session-catalog.ts +2 -2
- package/src/session/session-history-reader.ts +6 -1
- package/src/session/session-schema.ts +268 -4
- package/src/session/session-store.ts +105 -26
- package/src/skills/skill-context.ts +2 -2
- package/src/tools/bounded-output-preview.ts +276 -0
- package/src/tools/recall.ts +67 -36
- package/src/tools/registry.ts +7 -2
- package/src/tools/task-output-snapshot.ts +6 -22
- package/src/tools/task-output.ts +23 -27
- package/src/tui/app.tsx +82 -5
- package/src/tui/components/prompt-input.tsx +9 -1
- package/src/tui/slash-commands.ts +20 -0
- package/src/tui/tui-session-controller.ts +7 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { rgPath } from "@vscode/ripgrep";
|
|
3
|
+
import { parseModelApi, type ModelApi } from "../model/model-api";
|
|
3
4
|
|
|
4
5
|
export type PublicConfigValueKind = "non-empty-string" | "positive-integer" | "boolean";
|
|
5
6
|
|
|
@@ -39,6 +40,16 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
39
40
|
section: "model",
|
|
40
41
|
description: "Model name used when model profiles are not configured.",
|
|
41
42
|
}),
|
|
43
|
+
publicField({
|
|
44
|
+
name: "TINKER_API",
|
|
45
|
+
valueKind: "non-empty-string",
|
|
46
|
+
requiredIn: "never",
|
|
47
|
+
appliesIn: "env-mode",
|
|
48
|
+
defaultValue: "chat-completions",
|
|
49
|
+
secret: false,
|
|
50
|
+
section: "model",
|
|
51
|
+
description: 'Model API adapter: "chat-completions" or "responses".',
|
|
52
|
+
}),
|
|
42
53
|
publicField({
|
|
43
54
|
name: "TINKER_BASE_URL",
|
|
44
55
|
valueKind: "non-empty-string",
|
|
@@ -46,7 +57,8 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
46
57
|
appliesIn: "env-mode",
|
|
47
58
|
secret: false,
|
|
48
59
|
section: "model",
|
|
49
|
-
description:
|
|
60
|
+
description:
|
|
61
|
+
"OpenAI-compatible API root URL; do not append /chat/completions or /responses.",
|
|
50
62
|
}),
|
|
51
63
|
publicField({
|
|
52
64
|
name: "TINKER_API_KEY",
|
|
@@ -84,7 +96,8 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
84
96
|
defaultValue: false,
|
|
85
97
|
secret: false,
|
|
86
98
|
section: "model",
|
|
87
|
-
description:
|
|
99
|
+
description:
|
|
100
|
+
"Replay provider reasoning_content in Chat Completions history; ignored by Responses.",
|
|
88
101
|
}),
|
|
89
102
|
publicField({
|
|
90
103
|
name: "TINKER_STREAM",
|
|
@@ -94,7 +107,7 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
94
107
|
defaultValue: true,
|
|
95
108
|
secret: false,
|
|
96
109
|
section: "model",
|
|
97
|
-
description: "Use streaming
|
|
110
|
+
description: "Use streaming transport for the selected model API.",
|
|
98
111
|
}),
|
|
99
112
|
publicField({
|
|
100
113
|
name: "TINKER_WEBFETCH_REFINE_MODEL",
|
|
@@ -229,7 +242,11 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
229
242
|
|
|
230
243
|
export type ModelProfileField = {
|
|
231
244
|
readonly name: string;
|
|
232
|
-
readonly valueKind:
|
|
245
|
+
readonly valueKind:
|
|
246
|
+
| PublicConfigValueKind
|
|
247
|
+
| "input-modalities"
|
|
248
|
+
| "reasoning"
|
|
249
|
+
| "token-estimator";
|
|
233
250
|
readonly required: boolean;
|
|
234
251
|
readonly defaultValue?: string | number | boolean | readonly string[];
|
|
235
252
|
readonly secret: boolean;
|
|
@@ -248,12 +265,21 @@ export const MODEL_PROFILE_FIELDS = Object.freeze([
|
|
|
248
265
|
secret: false,
|
|
249
266
|
description: "Provider model name.",
|
|
250
267
|
}),
|
|
268
|
+
profileField({
|
|
269
|
+
name: "api",
|
|
270
|
+
valueKind: "non-empty-string",
|
|
271
|
+
required: false,
|
|
272
|
+
defaultValue: "chat-completions",
|
|
273
|
+
secret: false,
|
|
274
|
+
description: 'Model API adapter: "chat-completions" or "responses".',
|
|
275
|
+
}),
|
|
251
276
|
profileField({
|
|
252
277
|
name: "apiBase",
|
|
253
278
|
valueKind: "non-empty-string",
|
|
254
279
|
required: true,
|
|
255
280
|
secret: false,
|
|
256
|
-
description:
|
|
281
|
+
description:
|
|
282
|
+
"OpenAI-compatible API root URL; do not append /chat/completions or /responses.",
|
|
257
283
|
}),
|
|
258
284
|
profileField({
|
|
259
285
|
name: "apiKey",
|
|
@@ -277,13 +303,22 @@ export const MODEL_PROFILE_FIELDS = Object.freeze([
|
|
|
277
303
|
description:
|
|
278
304
|
"Maximum output-token count supported by the model; must not exceed contextWindowTokens.",
|
|
279
305
|
}),
|
|
306
|
+
profileField({
|
|
307
|
+
name: "reasoning",
|
|
308
|
+
valueKind: "reasoning",
|
|
309
|
+
required: false,
|
|
310
|
+
secret: false,
|
|
311
|
+
description:
|
|
312
|
+
"Provider-specific reasoning efforts and the default for each new session runtime.",
|
|
313
|
+
}),
|
|
280
314
|
profileField({
|
|
281
315
|
name: "includeReasoningContent",
|
|
282
316
|
valueKind: "boolean",
|
|
283
317
|
required: false,
|
|
284
318
|
defaultValue: false,
|
|
285
319
|
secret: false,
|
|
286
|
-
description:
|
|
320
|
+
description:
|
|
321
|
+
"Replay provider reasoning_content in Chat Completions history; ignored by Responses.",
|
|
287
322
|
}),
|
|
288
323
|
profileField({
|
|
289
324
|
name: "stream",
|
|
@@ -291,7 +326,7 @@ export const MODEL_PROFILE_FIELDS = Object.freeze([
|
|
|
291
326
|
required: false,
|
|
292
327
|
defaultValue: true,
|
|
293
328
|
secret: false,
|
|
294
|
-
description: "Use streaming
|
|
329
|
+
description: "Use streaming transport for the selected model API.",
|
|
295
330
|
}),
|
|
296
331
|
profileField({
|
|
297
332
|
name: "inputModalities",
|
|
@@ -311,6 +346,35 @@ export const MODEL_PROFILE_FIELDS = Object.freeze([
|
|
|
311
346
|
}),
|
|
312
347
|
]);
|
|
313
348
|
|
|
349
|
+
export type ModelReasoningField = {
|
|
350
|
+
readonly name: string;
|
|
351
|
+
readonly valueKind: "non-empty-string" | "non-empty-string-array";
|
|
352
|
+
readonly required: true;
|
|
353
|
+
readonly secret: false;
|
|
354
|
+
readonly description: string;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
function reasoningField<const T extends ModelReasoningField>(field: T): Readonly<T> {
|
|
358
|
+
return Object.freeze(field);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export const MODEL_REASONING_FIELDS = Object.freeze([
|
|
362
|
+
reasoningField({
|
|
363
|
+
name: "supportedEfforts",
|
|
364
|
+
valueKind: "non-empty-string-array",
|
|
365
|
+
required: true,
|
|
366
|
+
secret: false,
|
|
367
|
+
description: "Provider-supported effort values exposed by the /reasoning command.",
|
|
368
|
+
}),
|
|
369
|
+
reasoningField({
|
|
370
|
+
name: "defaultEffort",
|
|
371
|
+
valueKind: "non-empty-string",
|
|
372
|
+
required: true,
|
|
373
|
+
secret: false,
|
|
374
|
+
description: "Effort used whenever a session runtime is created or reopened.",
|
|
375
|
+
}),
|
|
376
|
+
]);
|
|
377
|
+
|
|
314
378
|
export type ModelTokenEstimatorField = {
|
|
315
379
|
readonly name: string;
|
|
316
380
|
readonly valueKind: PublicConfigValueKind | "literal-string" | "literal-number";
|
|
@@ -502,6 +566,7 @@ export type ParsedPublicEnvironment =
|
|
|
502
566
|
| (ParsedCommonEnvironment & {
|
|
503
567
|
readonly mode: "env";
|
|
504
568
|
readonly modelName: string;
|
|
569
|
+
readonly api: ModelApi;
|
|
505
570
|
readonly apiBase: string;
|
|
506
571
|
readonly apiKey: string;
|
|
507
572
|
readonly contextWindowTokens: number;
|
|
@@ -615,6 +680,7 @@ export function parsePublicEnvironment(
|
|
|
615
680
|
...common,
|
|
616
681
|
mode,
|
|
617
682
|
modelName,
|
|
683
|
+
api: parseModelApi(requiredStringValue(values, "TINKER_API"), "TINKER_API"),
|
|
618
684
|
apiBase: requiredStringValue(values, "TINKER_BASE_URL"),
|
|
619
685
|
apiKey: requiredStringValue(values, "TINKER_API_KEY"),
|
|
620
686
|
contextWindowTokens: requiredNumberValue(values, "TINKER_CONTEXT_WINDOW_TOKENS"),
|
package/src/cli/run-runner.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
createWebFetchRefiner,
|
|
17
17
|
RUNTIME_INSTRUCTIONS,
|
|
18
18
|
} from "./runner-dependencies";
|
|
19
|
+
import { createReasoningEffortController } from "../model/reasoning-effort";
|
|
19
20
|
import type { PublicToolingConfig } from "./public-config-contract";
|
|
20
21
|
import { realpath } from "node:fs/promises";
|
|
21
22
|
import { loadSkillCatalog } from "../skills/skill-loader";
|
|
@@ -51,10 +52,12 @@ export async function runOneShot(
|
|
|
51
52
|
runtimeInstructions: RUNTIME_INSTRUCTIONS(workspaceRoot),
|
|
52
53
|
projectInstructions,
|
|
53
54
|
});
|
|
55
|
+
const reasoningEffort = createReasoningEffortController(config.reasoning);
|
|
54
56
|
const modelClient = createRunnerModelClient(
|
|
55
57
|
config,
|
|
56
58
|
options.modelClient,
|
|
57
59
|
options.env,
|
|
60
|
+
reasoningEffort,
|
|
58
61
|
);
|
|
59
62
|
session = await createRuntimeSession({
|
|
60
63
|
selection: { mode: "new", sessionId: config.sessionId },
|
|
@@ -72,7 +75,7 @@ export async function runOneShot(
|
|
|
72
75
|
presentationSinks: [new StdoutEventPrinter(stdout, stderr)],
|
|
73
76
|
persistence:
|
|
74
77
|
options.eventLogPath === false ? false : { eventLogPath: options.eventLogPath },
|
|
75
|
-
webFetchRefiner: createWebFetchRefiner(config, options.env),
|
|
78
|
+
webFetchRefiner: createWebFetchRefiner(config, options.env, reasoningEffort),
|
|
76
79
|
toolingConfig: options.tooling,
|
|
77
80
|
bashGuard: {
|
|
78
81
|
mode: config.bashGuardMode,
|
|
@@ -2,6 +2,11 @@ import { renderRecallRetirementContract } from "../context/recall-retirement-con
|
|
|
2
2
|
import { FakeModelClient } from "../model/fake-model-client";
|
|
3
3
|
import type { ModelClient } from "../model/model-client";
|
|
4
4
|
import { OpenAIChatModelClient } from "../model/openai-chat-model-client";
|
|
5
|
+
import { OpenAIResponsesModelClient } from "../model/openai-responses-model-client";
|
|
6
|
+
import {
|
|
7
|
+
createReasoningEffortController,
|
|
8
|
+
type ReasoningEffortController,
|
|
9
|
+
} from "../model/reasoning-effort";
|
|
5
10
|
import { createModelRefiner, type Refiner } from "../tools/web-fetch/refiner";
|
|
6
11
|
import type { RunnerConfig } from "./config";
|
|
7
12
|
|
|
@@ -53,6 +58,8 @@ export function createModelClient(
|
|
|
53
58
|
config: Pick<
|
|
54
59
|
RunnerConfig,
|
|
55
60
|
| "modelName"
|
|
61
|
+
| "api"
|
|
62
|
+
| "reasoning"
|
|
56
63
|
| "includeReasoningContent"
|
|
57
64
|
| "stream"
|
|
58
65
|
| "contextBudget"
|
|
@@ -62,13 +69,19 @@ export function createModelClient(
|
|
|
62
69
|
| "tokenEstimator"
|
|
63
70
|
>,
|
|
64
71
|
env: NodeJS.ProcessEnv = process.env,
|
|
72
|
+
reasoningEffort?: ReasoningEffortController,
|
|
65
73
|
): ModelClient {
|
|
74
|
+
const activeReasoningEffort =
|
|
75
|
+
reasoningEffort ?? createReasoningEffortController(config.reasoning);
|
|
66
76
|
const fakeMode = env.TINKER_TEST_FAKE_MODEL;
|
|
67
77
|
if (fakeMode !== undefined && fakeMode !== "") {
|
|
68
78
|
return new FakeModelClient(fakeMode, {
|
|
69
79
|
model: config.modelName,
|
|
70
80
|
contextBudget: config.contextBudget,
|
|
71
81
|
inputModalities: config.inputModalities,
|
|
82
|
+
...(activeReasoningEffort === undefined
|
|
83
|
+
? {}
|
|
84
|
+
: { reasoningEffort: activeReasoningEffort }),
|
|
72
85
|
...(config.tokenEstimator === undefined
|
|
73
86
|
? {}
|
|
74
87
|
: { tokenEstimator: config.tokenEstimator }),
|
|
@@ -79,17 +92,26 @@ export function createModelClient(
|
|
|
79
92
|
});
|
|
80
93
|
}
|
|
81
94
|
|
|
82
|
-
|
|
95
|
+
const common = {
|
|
83
96
|
apiKey: config.apiKey,
|
|
84
97
|
baseURL: config.apiBase,
|
|
85
|
-
includeReasoningContent: config.includeReasoningContent,
|
|
86
98
|
model: config.modelName,
|
|
87
99
|
stream: config.stream,
|
|
88
100
|
contextBudget: config.contextBudget,
|
|
89
101
|
inputModalities: config.inputModalities,
|
|
102
|
+
...(activeReasoningEffort === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { reasoningEffort: activeReasoningEffort }),
|
|
90
105
|
...(config.tokenEstimator === undefined
|
|
91
106
|
? {}
|
|
92
107
|
: { tokenEstimator: config.tokenEstimator }),
|
|
108
|
+
};
|
|
109
|
+
if (config.api === "responses") {
|
|
110
|
+
return new OpenAIResponsesModelClient(common);
|
|
111
|
+
}
|
|
112
|
+
return new OpenAIChatModelClient({
|
|
113
|
+
...common,
|
|
114
|
+
includeReasoningContent: config.includeReasoningContent,
|
|
93
115
|
});
|
|
94
116
|
}
|
|
95
117
|
|
|
@@ -97,16 +119,18 @@ export function createRunnerModelClient(
|
|
|
97
119
|
config: Parameters<typeof createModelClient>[0],
|
|
98
120
|
injected?: ModelClient,
|
|
99
121
|
env?: NodeJS.ProcessEnv,
|
|
122
|
+
reasoningEffort?: ReasoningEffortController,
|
|
100
123
|
): ModelClient {
|
|
101
|
-
return injected ?? createModelClient(config, env);
|
|
124
|
+
return injected ?? createModelClient(config, env, reasoningEffort);
|
|
102
125
|
}
|
|
103
126
|
|
|
104
127
|
export function createWebFetchRefiner(
|
|
105
128
|
config: Parameters<typeof createModelClient>[0],
|
|
106
129
|
env?: NodeJS.ProcessEnv,
|
|
130
|
+
reasoningEffort?: ReasoningEffortController,
|
|
107
131
|
): Refiner {
|
|
108
132
|
return createModelRefiner({
|
|
109
|
-
createModelClient: () => createModelClient(config, env),
|
|
133
|
+
createModelClient: () => createModelClient(config, env, reasoningEffort),
|
|
110
134
|
contextBudget: config.contextBudget,
|
|
111
135
|
});
|
|
112
136
|
}
|
package/src/cli/tui-memory.ts
CHANGED
|
@@ -37,8 +37,12 @@ export async function initializeTuiMemory(input: {
|
|
|
37
37
|
return createModelClient(
|
|
38
38
|
{
|
|
39
39
|
modelName: profile.model,
|
|
40
|
+
api: profile.api,
|
|
40
41
|
apiKey: profile.apiKey,
|
|
41
42
|
apiBase: profile.apiBase,
|
|
43
|
+
...(profile.reasoning === undefined
|
|
44
|
+
? {}
|
|
45
|
+
: { reasoning: profile.reasoning }),
|
|
42
46
|
includeReasoningContent: profile.includeReasoningContent,
|
|
43
47
|
stream: profile.stream,
|
|
44
48
|
contextBudget: memoryConfig.contextBudget,
|
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -48,6 +48,7 @@ import { createWorkspaceFileLister } from "../tui/workspace-file-search";
|
|
|
48
48
|
import { clipboardWriterForEnvironment } from "../tui/clipboard";
|
|
49
49
|
import { initializeTuiMemory } from "./tui-memory";
|
|
50
50
|
import { prepareShikiHighlighter } from "../tui/shiki-highlighter";
|
|
51
|
+
import { createReasoningEffortController } from "../model/reasoning-effort";
|
|
51
52
|
|
|
52
53
|
export type RunTuiOptions = {
|
|
53
54
|
readonly publicConfig: ResolvedPublicConfig;
|
|
@@ -82,10 +83,12 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
82
83
|
sessionId: SessionId,
|
|
83
84
|
sink: EventSink & AssistantTextDeltaSink,
|
|
84
85
|
): Promise<RuntimeSession> => {
|
|
86
|
+
const reasoningEffort = createReasoningEffortController(sessionConfig.reasoning);
|
|
85
87
|
const modelClient = createRunnerModelClient(
|
|
86
88
|
sessionConfig,
|
|
87
89
|
undefined,
|
|
88
90
|
options.env,
|
|
91
|
+
reasoningEffort,
|
|
89
92
|
);
|
|
90
93
|
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
91
94
|
const skillCatalog = await loadSkillCatalog({ workspaceRoot });
|
|
@@ -107,7 +110,11 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
107
110
|
skillCatalog,
|
|
108
111
|
presentationSinks: [sink],
|
|
109
112
|
assistantTextDeltaSink: sink,
|
|
110
|
-
webFetchRefiner: createWebFetchRefiner(
|
|
113
|
+
webFetchRefiner: createWebFetchRefiner(
|
|
114
|
+
sessionConfig,
|
|
115
|
+
options.env,
|
|
116
|
+
reasoningEffort,
|
|
117
|
+
),
|
|
111
118
|
toolingConfig: options.publicConfig.tooling,
|
|
112
119
|
enableTurnUndo: true,
|
|
113
120
|
bashGuard: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stableJsonStringify, sha256 } from "../model/model-request-preflight";
|
|
2
|
-
import {
|
|
2
|
+
import { RECALL_TOOL_DEFINITIONS } from "../tools/recall";
|
|
3
3
|
import type { ToolDefinition } from "../tools/types";
|
|
4
4
|
import type { StoredContextSurfaceV8 } from "./context-surface";
|
|
5
5
|
import {
|
|
@@ -17,26 +17,26 @@ export const I4_ACTIVE_RECALL_QUALIFICATION = Object.freeze({
|
|
|
17
17
|
policyVersion: "active-recall-qualification-policy-v1",
|
|
18
18
|
policySha256: "77ca611594d4e9b7b5a597a3a33e35fcaaffae284dc2ac9953ce9a63cce1c009",
|
|
19
19
|
positiveReportSha256:
|
|
20
|
-
"
|
|
20
|
+
"e827e5e94171328bb2dd7fcaeff91881f04bdfc78361a45e2548da323229b02a",
|
|
21
21
|
negativeReportSha256:
|
|
22
|
-
"
|
|
22
|
+
"ed379843aee0f193f398a4dff9a18ed338f0edab2cbaf9b628d0dfc402d925a4",
|
|
23
23
|
resolvedModel: "deepseek-v4-flash",
|
|
24
24
|
recallContractVersion: CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION,
|
|
25
25
|
recallContractSha256:
|
|
26
|
-
"
|
|
26
|
+
"3b6d1a452efea1db5920eb13542571b038667ef4f635551bea375bda6562a39f",
|
|
27
27
|
recallToolDefinitionSha256:
|
|
28
|
-
"
|
|
28
|
+
"e63ada7cdf9591d1e933cf5e190ea30586e02bbf73aeb5e648db449d75aae009",
|
|
29
29
|
metrics: Object.freeze({
|
|
30
|
-
fullHistoryTaskSuccessRate:
|
|
31
|
-
swapOnlyTaskSuccessRate:
|
|
32
|
-
recallOnlyTaskSuccessRate:
|
|
33
|
-
recallOnlyActiveRecallRate:
|
|
34
|
-
recallOnlySearchGetSuccessRate: 0.
|
|
35
|
-
minimumCounterfactualGroupTaskSuccessRate:
|
|
36
|
-
invalidRecallCallsPerRecallOnlyTrial: 0
|
|
30
|
+
fullHistoryTaskSuccessRate: 0.9667,
|
|
31
|
+
swapOnlyTaskSuccessRate: 0.9667,
|
|
32
|
+
recallOnlyTaskSuccessRate: 1,
|
|
33
|
+
recallOnlyActiveRecallRate: 1,
|
|
34
|
+
recallOnlySearchGetSuccessRate: 0.3333,
|
|
35
|
+
minimumCounterfactualGroupTaskSuccessRate: 1,
|
|
36
|
+
invalidRecallCallsPerRecallOnlyTrial: 0,
|
|
37
37
|
negativeUnnecessaryRecallRate: 0,
|
|
38
|
-
recallOnlyTokenRatioToFullHistory: 1.
|
|
39
|
-
recallOnlyLatencyRatioToFullHistory: 1.
|
|
38
|
+
recallOnlyTokenRatioToFullHistory: 1.3739,
|
|
39
|
+
recallOnlyLatencyRatioToFullHistory: 1.207,
|
|
40
40
|
}),
|
|
41
41
|
passed: true,
|
|
42
42
|
} as const);
|
|
@@ -80,13 +80,14 @@ export function selectContextAutomation(
|
|
|
80
80
|
) {
|
|
81
81
|
return disabled("recall_contract_mismatch");
|
|
82
82
|
}
|
|
83
|
-
const
|
|
84
|
-
(definition) =>
|
|
83
|
+
const recallTools = input.surface.toolDefinitions.filter(
|
|
84
|
+
(definition) =>
|
|
85
|
+
definition.name === "RecallSearch" || definition.name === "RecallGet",
|
|
85
86
|
);
|
|
86
87
|
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
recallTools.length !== 2 ||
|
|
89
|
+
toolDefinitionsHash(recallTools) !== evidence.recallToolDefinitionSha256 ||
|
|
90
|
+
toolDefinitionsHash(RECALL_TOOL_DEFINITIONS) !== evidence.recallToolDefinitionSha256
|
|
90
91
|
) {
|
|
91
92
|
return disabled("recall_tool_mismatch");
|
|
92
93
|
}
|
|
@@ -116,6 +117,6 @@ function disabled(
|
|
|
116
117
|
});
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
function
|
|
120
|
-
return sha256(stableJsonStringify(
|
|
120
|
+
function toolDefinitionsHash(definitions: readonly ToolDefinition[]): string {
|
|
121
|
+
return sha256(stableJsonStringify(definitions));
|
|
121
122
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ContextBuilder } from "../agent/context-builder";
|
|
2
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";
|
|
3
|
+
import type { AgentTurnLedger, SessionLedger } from "../agent/session-ledger";
|
|
4
|
+
import type { RuntimeIdFactory, TurnId } from "../ids/runtime-id";
|
|
5
5
|
import {
|
|
6
6
|
CommittedPrefixAuditError,
|
|
7
7
|
type CommittedPrefixAuditor,
|
|
@@ -45,17 +45,25 @@ import {
|
|
|
45
45
|
PrefixRetirementPlanner,
|
|
46
46
|
PrefixRetirementPlanningError,
|
|
47
47
|
PrefixRetirementPlanStaleError,
|
|
48
|
+
type ActiveTurnBoundary,
|
|
49
|
+
type ClosedTurnBoundary,
|
|
48
50
|
type PrefixRetirementPlan,
|
|
49
51
|
} from "./prefix-retirement-planner";
|
|
50
52
|
|
|
51
53
|
export type ContextCompactionTrigger =
|
|
52
54
|
| { kind: "manual" }
|
|
53
|
-
| {
|
|
55
|
+
| {
|
|
56
|
+
kind: "runtime_pressure";
|
|
57
|
+
activeTurn?: {
|
|
58
|
+
turnId: TurnId;
|
|
59
|
+
consumedThroughOrdinal: number;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
54
62
|
| { kind: "benchmark_forced"; targetTokens: number };
|
|
55
63
|
|
|
56
64
|
export type ContextRetirementTrigger =
|
|
57
65
|
| { kind: "manual" }
|
|
58
|
-
| { kind: "runtime_pressure" }
|
|
66
|
+
| { kind: "runtime_pressure"; activeTurnId?: TurnId }
|
|
59
67
|
| { kind: "benchmark_forced"; targetTokens: number };
|
|
60
68
|
|
|
61
69
|
export type ContextCompactionResult =
|
|
@@ -246,15 +254,32 @@ export class ContextManager {
|
|
|
246
254
|
this.retirementPlanner = new PrefixRetirementPlanner(input.model);
|
|
247
255
|
}
|
|
248
256
|
|
|
249
|
-
|
|
257
|
+
measureCurrent(
|
|
258
|
+
activeTurnId?: TurnId,
|
|
259
|
+
activeLedger?: AgentTurnLedger,
|
|
260
|
+
): ContextUsageSnapshot {
|
|
261
|
+
assertActiveLedger(activeTurnId, activeLedger);
|
|
262
|
+
this.input.store.assertContextRevisionBoundary(activeTurnId);
|
|
263
|
+
const tools = this.input.tools();
|
|
264
|
+
const built = buildCurrentRequest(this.input.ledger, activeLedger, tools);
|
|
265
|
+
const prepared = this.input.model.prepare(built.request);
|
|
266
|
+
this.input.committedPrefixAuditor.audit(built.compiled.revisionId, prepared);
|
|
267
|
+
return this.input.contextMeter.measure(prepared);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async compact(
|
|
271
|
+
trigger: ContextCompactionTrigger,
|
|
272
|
+
activeLedger?: AgentTurnLedger,
|
|
273
|
+
): Promise<ContextCompactionResult> {
|
|
274
|
+
assertActiveLedger(activeTurnId(trigger), activeLedger);
|
|
250
275
|
const startedAt = performance.now();
|
|
251
276
|
let built;
|
|
252
277
|
let activePrepared;
|
|
253
278
|
let activeUsage;
|
|
254
279
|
const tools = this.input.tools();
|
|
255
280
|
try {
|
|
256
|
-
this.input.store.
|
|
257
|
-
built = this.input.ledger
|
|
281
|
+
this.input.store.assertContextRevisionBoundary(activeTurnId(trigger));
|
|
282
|
+
built = buildCurrentRequest(this.input.ledger, activeLedger, tools);
|
|
258
283
|
activePrepared = this.input.model.prepare(built.request);
|
|
259
284
|
activeUsage = this.input.contextMeter.measure(activePrepared);
|
|
260
285
|
} catch (error) {
|
|
@@ -274,6 +299,9 @@ export class ContextManager {
|
|
|
274
299
|
tools,
|
|
275
300
|
policy: swapOnlyPolicyV1,
|
|
276
301
|
trigger: trigger.kind,
|
|
302
|
+
...(trigger.kind === "runtime_pressure" && trigger.activeTurn !== undefined
|
|
303
|
+
? { activeTurn: trigger.activeTurn }
|
|
304
|
+
: {}),
|
|
277
305
|
...(trigger.kind === "benchmark_forced"
|
|
278
306
|
? { forcedTargetTokens: trigger.targetTokens }
|
|
279
307
|
: {}),
|
|
@@ -357,7 +385,7 @@ export class ContextManager {
|
|
|
357
385
|
candidatePrepared = this.input.model.prepare(candidate.request);
|
|
358
386
|
assertCandidatePrepared(activePrepared, candidatePrepared, plan);
|
|
359
387
|
|
|
360
|
-
const current = this.input.ledger
|
|
388
|
+
const current = buildCurrentRequest(this.input.ledger, activeLedger, tools);
|
|
361
389
|
const currentPrepared = this.input.model.prepare(current.request);
|
|
362
390
|
assertPlanBaseCurrent(plan, {
|
|
363
391
|
active: current.compiled,
|
|
@@ -365,7 +393,7 @@ export class ContextManager {
|
|
|
365
393
|
activeOverrides: current.activeOverrides,
|
|
366
394
|
activePrepared: currentPrepared,
|
|
367
395
|
});
|
|
368
|
-
this.input.store.
|
|
396
|
+
this.input.store.assertContextRevisionBoundary(activeTurnId(trigger));
|
|
369
397
|
} catch (error) {
|
|
370
398
|
throw managerError("validate", error);
|
|
371
399
|
}
|
|
@@ -394,6 +422,9 @@ export class ContextManager {
|
|
|
394
422
|
candidateCompiled.entries,
|
|
395
423
|
plan.baseCanonicalThroughOrdinal,
|
|
396
424
|
),
|
|
425
|
+
...(activeTurnId(trigger) === undefined
|
|
426
|
+
? {}
|
|
427
|
+
: { activeTurnId: activeTurnId(trigger) }),
|
|
397
428
|
});
|
|
398
429
|
} catch (error) {
|
|
399
430
|
throw managerError("commit", error);
|
|
@@ -402,6 +433,7 @@ export class ContextManager {
|
|
|
402
433
|
|
|
403
434
|
const activationStartedAt = performance.now();
|
|
404
435
|
try {
|
|
436
|
+
activeLedger?.activateContextSnapshot(this.input.store.loadContextSnapshot());
|
|
405
437
|
this.input.contextMeter.startRevision({
|
|
406
438
|
reason: "context_rebuilt",
|
|
407
439
|
requestConfigHash: candidatePrepared.requestConfigHash,
|
|
@@ -454,19 +486,26 @@ export class ContextManager {
|
|
|
454
486
|
|
|
455
487
|
async retirePrefix(
|
|
456
488
|
trigger: ContextRetirementTrigger,
|
|
489
|
+
activeLedger?: AgentTurnLedger,
|
|
457
490
|
): Promise<ContextRetirementResult> {
|
|
491
|
+
assertActiveLedger(activeTurnId(trigger), activeLedger);
|
|
458
492
|
const startedAt = performance.now();
|
|
459
493
|
const tools = this.input.tools();
|
|
460
494
|
let built;
|
|
461
495
|
let activePrepared;
|
|
462
496
|
let activeUsage;
|
|
463
|
-
let closedTurns;
|
|
497
|
+
let closedTurns: readonly ClosedTurnBoundary[];
|
|
498
|
+
let activeTurn: ActiveTurnBoundary | undefined;
|
|
464
499
|
try {
|
|
465
|
-
this.input.store.
|
|
466
|
-
built = this.input.ledger
|
|
500
|
+
this.input.store.assertContextRevisionBoundary(activeTurnId(trigger));
|
|
501
|
+
built = buildCurrentRequest(this.input.ledger, activeLedger, tools);
|
|
467
502
|
activePrepared = this.input.model.prepare(built.request);
|
|
468
503
|
activeUsage = this.input.contextMeter.measure(activePrepared);
|
|
469
|
-
|
|
504
|
+
const boundaries = this.input.store.loadRetirementBoundaries(
|
|
505
|
+
activeTurnId(trigger),
|
|
506
|
+
);
|
|
507
|
+
closedTurns = boundaries.closedTurns;
|
|
508
|
+
activeTurn = boundaries.activeTurn;
|
|
470
509
|
} catch (error) {
|
|
471
510
|
throw managerError("snapshot", error);
|
|
472
511
|
}
|
|
@@ -480,6 +519,7 @@ export class ContextManager {
|
|
|
480
519
|
activeOverrides: built.activeOverrides,
|
|
481
520
|
canonical: built.canonical,
|
|
482
521
|
closedTurns,
|
|
522
|
+
...(activeTurn === undefined ? {} : { activeTurn }),
|
|
483
523
|
activePrepared,
|
|
484
524
|
activeUsage,
|
|
485
525
|
tools,
|
|
@@ -547,7 +587,7 @@ export class ContextManager {
|
|
|
547
587
|
candidatePrepared = this.input.model.prepare(candidate.request);
|
|
548
588
|
assertRetirementCandidatePrepared(activePrepared, candidatePrepared, plan);
|
|
549
589
|
|
|
550
|
-
const current = this.input.ledger
|
|
590
|
+
const current = buildCurrentRequest(this.input.ledger, activeLedger, tools);
|
|
551
591
|
const currentPrepared = this.input.model.prepare(current.request);
|
|
552
592
|
assertRetirementPlanBaseCurrent(plan, {
|
|
553
593
|
active: current.compiled,
|
|
@@ -557,7 +597,7 @@ export class ContextManager {
|
|
|
557
597
|
canonical: current.canonical,
|
|
558
598
|
activePrepared: currentPrepared,
|
|
559
599
|
});
|
|
560
|
-
this.input.store.
|
|
600
|
+
this.input.store.assertContextRevisionBoundary(activeTurnId(trigger));
|
|
561
601
|
} catch (error) {
|
|
562
602
|
throw managerError("validate", error);
|
|
563
603
|
}
|
|
@@ -586,6 +626,9 @@ export class ContextManager {
|
|
|
586
626
|
nextActiveOverrideManifestSha256: plan.nextActiveOverrideManifestSha256,
|
|
587
627
|
canonicalSequenceSha256: plan.canonicalSequenceSha256,
|
|
588
628
|
renderedMessageSha256: plan.renderedMessageSha256,
|
|
629
|
+
...(activeTurnId(trigger) === undefined
|
|
630
|
+
? {}
|
|
631
|
+
: { activeTurnId: activeTurnId(trigger) }),
|
|
589
632
|
});
|
|
590
633
|
} catch (error) {
|
|
591
634
|
throw managerError("commit", error);
|
|
@@ -594,6 +637,7 @@ export class ContextManager {
|
|
|
594
637
|
|
|
595
638
|
const activationStartedAt = performance.now();
|
|
596
639
|
try {
|
|
640
|
+
activeLedger?.activateContextSnapshot(this.input.store.loadContextSnapshot());
|
|
597
641
|
this.input.contextMeter.startRevision({
|
|
598
642
|
reason: "context_rebuilt",
|
|
599
643
|
requestConfigHash: candidatePrepared.requestConfigHash,
|
|
@@ -739,3 +783,35 @@ function errorCode(error: unknown): string {
|
|
|
739
783
|
function elapsedMs(startedAt: number): number {
|
|
740
784
|
return Math.round((performance.now() - startedAt) * 100) / 100;
|
|
741
785
|
}
|
|
786
|
+
|
|
787
|
+
function activeTurnId(
|
|
788
|
+
trigger: ContextCompactionTrigger | ContextRetirementTrigger,
|
|
789
|
+
): TurnId | undefined {
|
|
790
|
+
if (trigger.kind !== "runtime_pressure") {
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
if ("activeTurnId" in trigger) {
|
|
794
|
+
return trigger.activeTurnId;
|
|
795
|
+
}
|
|
796
|
+
return (trigger as Extract<ContextCompactionTrigger, { kind: "runtime_pressure" }>)
|
|
797
|
+
.activeTurn?.turnId;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function assertActiveLedger(
|
|
801
|
+
activeTurnId: TurnId | undefined,
|
|
802
|
+
activeLedger: AgentTurnLedger | undefined,
|
|
803
|
+
): void {
|
|
804
|
+
if ((activeTurnId === undefined) !== (activeLedger === undefined)) {
|
|
805
|
+
throw new Error("Active context maintenance requires its active turn ledger.");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function buildCurrentRequest(
|
|
810
|
+
ledger: SessionLedger,
|
|
811
|
+
activeLedger: AgentTurnLedger | undefined,
|
|
812
|
+
tools: readonly ToolDefinition[],
|
|
813
|
+
) {
|
|
814
|
+
return activeLedger === undefined
|
|
815
|
+
? ledger.buildCommittedModelRequest(tools)
|
|
816
|
+
: activeLedger.buildModelRequest(tools);
|
|
817
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export const swapOnlyPolicyV1 = Object.freeze({
|
|
2
2
|
version: "swap-only-v1",
|
|
3
3
|
minimumObservationBytes: 8 * 1_024,
|
|
4
|
-
protectedRecentTurnCount: 8,
|
|
5
4
|
targetInputRatio: 0.3,
|
|
6
5
|
} as const);
|
|
7
6
|
|
|
@@ -9,7 +8,6 @@ export type SwapOnlyPolicyV1 = typeof swapOnlyPolicyV1;
|
|
|
9
8
|
|
|
10
9
|
export const recallFirstRetirementPolicyV1 = Object.freeze({
|
|
11
10
|
version: "recall-first-retirement-v1",
|
|
12
|
-
protectedRecentTurnCount: 8,
|
|
13
11
|
targetInputRatio: 0.3,
|
|
14
12
|
} as const);
|
|
15
13
|
|
|
@@ -73,7 +73,7 @@ export class ContextSwapRenderer {
|
|
|
73
73
|
`contentSha256=${message.contentSha256}`,
|
|
74
74
|
`tool=${stableJsonStringify(compactExternalString(message.name))}`,
|
|
75
75
|
`metadata=${metadata}`,
|
|
76
|
-
"historical=Use
|
|
76
|
+
"historical=Use RecallGet with source to recover the original observation.",
|
|
77
77
|
`current=${currentGuidance(raw.kind)}`,
|
|
78
78
|
].join("\n");
|
|
79
79
|
const originalBytes = utf8Bytes(message.content);
|