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.
Files changed (208) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +90 -9
  5. package/bin/tinker.js +39 -0
  6. package/node_modules/ansi-regex/index.d.ts +33 -0
  7. package/node_modules/ansi-regex/index.js +14 -0
  8. package/node_modules/ansi-regex/license +9 -0
  9. package/node_modules/ansi-regex/package.json +61 -0
  10. package/node_modules/ansi-regex/readme.md +66 -0
  11. package/node_modules/ansi-styles/index.d.ts +236 -0
  12. package/node_modules/ansi-styles/index.js +223 -0
  13. package/node_modules/ansi-styles/license +9 -0
  14. package/node_modules/ansi-styles/package.json +54 -0
  15. package/node_modules/ansi-styles/readme.md +173 -0
  16. package/node_modules/chalk/license +9 -0
  17. package/node_modules/chalk/package.json +83 -0
  18. package/node_modules/chalk/readme.md +297 -0
  19. package/node_modules/chalk/source/index.d.ts +325 -0
  20. package/node_modules/chalk/source/index.js +225 -0
  21. package/node_modules/chalk/source/utilities.js +33 -0
  22. package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  23. package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  24. package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  25. package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  26. package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  27. package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  28. package/node_modules/character-entities/index.d.ts +6 -0
  29. package/node_modules/character-entities/index.js +2132 -0
  30. package/node_modules/character-entities/license +22 -0
  31. package/node_modules/character-entities/package.json +78 -0
  32. package/node_modules/character-entities/readme.md +152 -0
  33. package/node_modules/decode-named-character-reference/index.d.ts +13 -0
  34. package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
  35. package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
  36. package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
  37. package/node_modules/decode-named-character-reference/index.dom.js +32 -0
  38. package/node_modules/decode-named-character-reference/index.js +19 -0
  39. package/node_modules/decode-named-character-reference/license +22 -0
  40. package/node_modules/decode-named-character-reference/package.json +90 -0
  41. package/node_modules/decode-named-character-reference/readme.md +136 -0
  42. package/node_modules/get-east-asian-width/index.d.ts +60 -0
  43. package/node_modules/get-east-asian-width/index.js +30 -0
  44. package/node_modules/get-east-asian-width/license +9 -0
  45. package/node_modules/get-east-asian-width/lookup-data.js +21 -0
  46. package/node_modules/get-east-asian-width/lookup.js +138 -0
  47. package/node_modules/get-east-asian-width/package.json +71 -0
  48. package/node_modules/get-east-asian-width/readme.md +65 -0
  49. package/node_modules/get-east-asian-width/utilities.js +24 -0
  50. package/node_modules/has-flag/index.d.ts +39 -0
  51. package/node_modules/has-flag/index.js +8 -0
  52. package/node_modules/has-flag/license +9 -0
  53. package/node_modules/has-flag/package.json +49 -0
  54. package/node_modules/has-flag/readme.md +74 -0
  55. package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
  56. package/node_modules/is-fullwidth-code-point/index.js +12 -0
  57. package/node_modules/is-fullwidth-code-point/license +9 -0
  58. package/node_modules/is-fullwidth-code-point/package.json +53 -0
  59. package/node_modules/is-fullwidth-code-point/readme.md +31 -0
  60. package/node_modules/markdansi/LICENSE +21 -0
  61. package/node_modules/markdansi/README.md +167 -0
  62. package/node_modules/markdansi/dist/ast.d.ts +99 -0
  63. package/node_modules/markdansi/dist/ast.js +1 -0
  64. package/node_modules/markdansi/dist/cli.d.ts +17 -0
  65. package/node_modules/markdansi/dist/cli.js +208 -0
  66. package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
  67. package/node_modules/markdansi/dist/hyperlink.js +26 -0
  68. package/node_modules/markdansi/dist/index.d.ts +10 -0
  69. package/node_modules/markdansi/dist/index.js +16 -0
  70. package/node_modules/markdansi/dist/parser.d.ts +2 -0
  71. package/node_modules/markdansi/dist/parser.js +205 -0
  72. package/node_modules/markdansi/dist/render.d.ts +9 -0
  73. package/node_modules/markdansi/dist/render.js +680 -0
  74. package/node_modules/markdansi/dist/stream.d.ts +39 -0
  75. package/node_modules/markdansi/dist/stream.js +192 -0
  76. package/node_modules/markdansi/dist/theme.d.ts +18 -0
  77. package/node_modules/markdansi/dist/theme.js +105 -0
  78. package/node_modules/markdansi/dist/types.d.ts +58 -0
  79. package/node_modules/markdansi/dist/types.js +1 -0
  80. package/node_modules/markdansi/dist/wrap.d.ts +10 -0
  81. package/node_modules/markdansi/dist/wrap.js +73 -0
  82. package/node_modules/markdansi/docs/spec.md +110 -0
  83. package/node_modules/markdansi/package.json +78 -0
  84. package/node_modules/markdansi/tsconfig.json +18 -0
  85. package/node_modules/marked/LICENSE +44 -0
  86. package/node_modules/marked/README.md +115 -0
  87. package/node_modules/marked/bin/main.js +284 -0
  88. package/node_modules/marked/bin/marked.js +16 -0
  89. package/node_modules/marked/lib/marked.d.ts +760 -0
  90. package/node_modules/marked/lib/marked.esm.js +77 -0
  91. package/node_modules/marked/lib/marked.esm.js.map +7 -0
  92. package/node_modules/marked/lib/marked.umd.js +79 -0
  93. package/node_modules/marked/lib/marked.umd.js.map +7 -0
  94. package/node_modules/marked/man/marked.1 +113 -0
  95. package/node_modules/marked/man/marked.1.md +93 -0
  96. package/node_modules/marked/package.json +103 -0
  97. package/node_modules/slice-ansi/index.d.ts +19 -0
  98. package/node_modules/slice-ansi/index.js +317 -0
  99. package/node_modules/slice-ansi/license +10 -0
  100. package/node_modules/slice-ansi/package.json +59 -0
  101. package/node_modules/slice-ansi/readme.md +55 -0
  102. package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
  103. package/node_modules/string-width/index.d.ts +39 -0
  104. package/node_modules/string-width/index.js +203 -0
  105. package/node_modules/string-width/license +9 -0
  106. package/node_modules/string-width/package.json +65 -0
  107. package/node_modules/string-width/readme.md +66 -0
  108. package/node_modules/strip-ansi/index.d.ts +15 -0
  109. package/node_modules/strip-ansi/index.js +19 -0
  110. package/node_modules/strip-ansi/license +9 -0
  111. package/node_modules/strip-ansi/package.json +59 -0
  112. package/node_modules/strip-ansi/readme.md +37 -0
  113. package/node_modules/supports-color/browser.d.ts +1 -0
  114. package/node_modules/supports-color/browser.js +35 -0
  115. package/node_modules/supports-color/index.d.ts +55 -0
  116. package/node_modules/supports-color/index.js +202 -0
  117. package/node_modules/supports-color/license +9 -0
  118. package/node_modules/supports-color/package.json +64 -0
  119. package/node_modules/supports-color/readme.md +75 -0
  120. package/node_modules/supports-hyperlinks/browser.js +10 -0
  121. package/node_modules/supports-hyperlinks/index.d.ts +25 -0
  122. package/node_modules/supports-hyperlinks/index.js +163 -0
  123. package/node_modules/supports-hyperlinks/license +10 -0
  124. package/node_modules/supports-hyperlinks/package.json +47 -0
  125. package/node_modules/supports-hyperlinks/readme.md +45 -0
  126. package/package.json +41 -10
  127. package/patches/markdansi@0.3.2.patch +22 -2
  128. package/src/agent/context-builder.ts +24 -10
  129. package/src/agent/context-meter.ts +89 -5
  130. package/src/agent/loop.ts +107 -10
  131. package/src/agent/runtime-session.ts +1437 -82
  132. package/src/agent/session-ledger.ts +169 -29
  133. package/src/agent/types.ts +12 -1
  134. package/src/agent/user-prompt-projection.ts +62 -0
  135. package/src/cli/config.ts +57 -14
  136. package/src/cli/model-profiles.ts +124 -0
  137. package/src/cli/run-runner.ts +4 -1
  138. package/src/cli/tui-runner.tsx +68 -30
  139. package/src/context/compiled-context-hash.ts +1 -1
  140. package/src/context/compiled-context-validator.ts +92 -15
  141. package/src/context/context-automation-policy.ts +121 -0
  142. package/src/context/context-manager.ts +390 -11
  143. package/src/context/context-policy.ts +8 -0
  144. package/src/context/context-protocol-validator.ts +11 -1
  145. package/src/context/context-revision-compiler.ts +190 -27
  146. package/src/context/context-revision.ts +105 -19
  147. package/src/context/context-surface.ts +329 -0
  148. package/src/context/prefix-retirement-planner.ts +570 -0
  149. package/src/context/protocol-frame.ts +30 -3
  150. package/src/context/recall-retirement-contract.ts +27 -0
  151. package/src/context/swap-planner.ts +34 -10
  152. package/src/events/observation-text-log.ts +3 -3
  153. package/src/events/stdout-event-printer.ts +39 -3
  154. package/src/events/types.ts +172 -35
  155. package/src/ids/runtime-id.ts +6 -0
  156. package/src/ids/uuid-v7.ts +7 -0
  157. package/src/image/image-asset-store.ts +319 -0
  158. package/src/image/image-input-policy.ts +14 -0
  159. package/src/image/image-probe.ts +233 -0
  160. package/src/image/image-types.ts +262 -0
  161. package/src/instructions/project-instructions.ts +4 -1
  162. package/src/mcp/mcp-manager.ts +83 -19
  163. package/src/model/fake-model-client.ts +9 -1
  164. package/src/model/input-token-estimator.ts +25 -0
  165. package/src/model/model-client.ts +60 -0
  166. package/src/model/model-request-preflight.ts +1 -0
  167. package/src/model/moonshot-input-token-estimator.ts +107 -0
  168. package/src/model/openai-chat-mapping.ts +77 -1
  169. package/src/model/openai-chat-model-client.ts +389 -59
  170. package/src/model/openai-chat-stream.ts +272 -0
  171. package/src/model/prompt-prefix-hash.ts +2 -4
  172. package/src/model/token-estimator.ts +4 -0
  173. package/src/observation/observation-builder.ts +29 -0
  174. package/src/session/resume-projection.ts +94 -34
  175. package/src/session/session-catalog.ts +4 -4
  176. package/src/session/session-errors.ts +1 -1
  177. package/src/session/session-history-reader.ts +151 -4
  178. package/src/session/session-last-response-reader.ts +189 -0
  179. package/src/session/session-schema.ts +349 -35
  180. package/src/session/session-store.ts +3266 -255
  181. package/src/session/sqlite-session-ledger.ts +9 -4
  182. package/src/skills/skill-catalog.ts +182 -0
  183. package/src/skills/skill-context.ts +364 -0
  184. package/src/skills/skill-loader.ts +779 -0
  185. package/src/skills/skill-tool.ts +297 -0
  186. package/src/tools/edit.ts +51 -64
  187. package/src/tools/ensure-parent-directory.ts +6 -0
  188. package/src/tools/read.ts +2 -2
  189. package/src/tools/recall.ts +43 -40
  190. package/src/tools/registry.ts +24 -3
  191. package/src/tools/ripgrep.ts +3 -2
  192. package/src/tools/types.ts +43 -4
  193. package/src/tools/write.ts +20 -31
  194. package/src/tui/app.tsx +333 -96
  195. package/src/tui/clipboard.ts +5 -0
  196. package/src/tui/components/mcp-panel.tsx +28 -0
  197. package/src/tui/components/prompt-input.tsx +790 -257
  198. package/src/tui/components/skills-panel.tsx +31 -0
  199. package/src/tui/components/timeline.tsx +30 -1
  200. package/src/tui/event-store.ts +85 -2
  201. package/src/tui/file-mention.ts +234 -0
  202. package/src/tui/line-editor.ts +1 -0
  203. package/src/tui/project-slash-commands.ts +229 -0
  204. package/src/tui/prompt-draft.ts +510 -0
  205. package/src/tui/prompt-history.ts +294 -22
  206. package/src/tui/slash-commands.ts +46 -2
  207. package/src/tui/tui-session-controller.ts +110 -48
  208. 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
  }
@@ -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") {
@@ -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 createSessionWithProfile = async (
163
- profile: ModelProfile,
168
+ const createNewSessionBinding = async (
169
+ sessionConfig: RunnerConfig,
164
170
  ): Promise<ManagedTuiSessionBinding> => {
165
- if (profiles === undefined) {
166
- throw new Error("Model profiles are not configured.");
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
- profileConfig,
177
+ sessionConfig,
184
178
  "new",
185
- profileConfig.sessionId,
186
- profileProjectionStore,
179
+ sessionConfig.sessionId,
180
+ freshProjectionStore,
187
181
  );
188
182
  return managedTuiBinding({
189
183
  runtimeSession,
190
- modelName: profileConfig.modelName,
184
+ modelName: sessionConfig.modelName,
191
185
  workspaceRoot,
192
- profileName: profile.name,
193
- projectionStore: profileProjectionStore,
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
- if (compiled.entries.length !== canonical.messages.length) {
47
- fail("Compiled context changed the canonical message count.");
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.frameCount !== canonical.frames.length ||
52
- compiled.manifest.messageCount !== compiled.entries.length
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(canonical.messages, index, "canonical message");
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 !== "canonical") {
83
- fail(`Unapproved swapped entry at ordinal ${entry.ordinal}.`);
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 (seenFrames.size !== canonical.frames.length) {
108
- fail("Compiled entries do not preserve every protocol frame.");
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
+ }