tinker-agent 1.8.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/README.md +65 -6
  3. package/package.json +1 -1
  4. package/src/agent/loop.ts +13 -0
  5. package/src/agent/runtime-session.ts +165 -0
  6. package/src/agent/session-ledger.ts +20 -3
  7. package/src/cli/config.ts +11 -2
  8. package/src/cli/model-profiles.ts +58 -0
  9. package/src/cli/public-config-contract.ts +73 -7
  10. package/src/cli/run-runner.ts +4 -1
  11. package/src/cli/runner-dependencies.ts +35 -10
  12. package/src/cli/tui-memory.ts +4 -0
  13. package/src/cli/tui-runner.tsx +8 -1
  14. package/src/context/context-automation-policy.ts +22 -21
  15. package/src/context/context-manager.ts +91 -15
  16. package/src/context/context-policy.ts +0 -2
  17. package/src/context/context-swap-renderer.ts +1 -1
  18. package/src/context/prefix-retirement-planner.ts +58 -8
  19. package/src/context/recall-retirement-contract.ts +5 -4
  20. package/src/context/swap-planner.ts +33 -27
  21. package/src/events/stdout-event-printer.ts +17 -0
  22. package/src/model/fake-model-client.ts +26 -16
  23. package/src/model/model-api.ts +12 -0
  24. package/src/model/model-client.ts +9 -1
  25. package/src/model/moonshot-input-token-estimator.ts +5 -1
  26. package/src/model/openai-chat-mapping.ts +2 -24
  27. package/src/model/openai-chat-model-client.ts +18 -294
  28. package/src/model/openai-image-mapping.ts +20 -0
  29. package/src/model/openai-model-utils.ts +304 -0
  30. package/src/model/openai-responses-mapping.ts +532 -0
  31. package/src/model/openai-responses-model-client.ts +295 -0
  32. package/src/model/openai-responses-stream.ts +96 -0
  33. package/src/model/openai-responses-token-estimator.ts +155 -0
  34. package/src/model/reasoning-effort.ts +60 -0
  35. package/src/observation/observation-builder.ts +7 -0
  36. package/src/session/session-catalog.ts +2 -2
  37. package/src/session/session-history-reader.ts +6 -1
  38. package/src/session/session-schema.ts +268 -4
  39. package/src/session/session-store.ts +106 -26
  40. package/src/skills/skill-context.ts +2 -2
  41. package/src/tools/bounded-output-preview.ts +276 -0
  42. package/src/tools/recall.ts +67 -36
  43. package/src/tools/registry.ts +9 -2
  44. package/src/tools/task-output-snapshot.ts +6 -22
  45. package/src/tools/task-output.ts +23 -27
  46. package/src/tools/types.ts +19 -0
  47. package/src/tools/update-plan.ts +166 -0
  48. package/src/tui/app.tsx +82 -5
  49. package/src/tui/components/plan-view.tsx +43 -0
  50. package/src/tui/components/prompt-input.tsx +9 -1
  51. package/src/tui/components/timeline.tsx +7 -0
  52. package/src/tui/event-store.ts +28 -2
  53. package/src/tui/slash-commands.ts +20 -0
  54. package/src/tui/tui-session-controller.ts +7 -0
@@ -3,24 +3,29 @@ import {
3
3
  createModelContextProfile,
4
4
  type ModelContextProfile,
5
5
  } from "../model/model-context-profile";
6
+ import { parseModelApi, type ModelApi } from "../model/model-api";
6
7
  import {
7
8
  MEMORY_CONFIG_FIELDS,
8
9
  MEMORY_EMBEDDING_FIELDS,
9
10
  MODEL_PROFILE_FIELDS,
10
11
  MODEL_PROFILES_DOCUMENT_FIELDS,
12
+ MODEL_REASONING_FIELDS,
11
13
  MODEL_TOKEN_ESTIMATOR_FIELDS,
12
14
  type ModelTokenEstimatorKind,
13
15
  type ModelTokenEstimatorMaxRetries,
14
16
  } from "./public-config-contract";
15
17
  import type { MemoryEmbeddingConfig } from "../memory/contracts";
18
+ import type { ReasoningEffortConfig } from "../model/reasoning-effort";
16
19
 
17
20
  export type ModelProfile = {
18
21
  readonly name: string;
19
22
  readonly model: string;
23
+ readonly api: ModelApi;
20
24
  readonly apiBase: string;
21
25
  readonly apiKey: string;
22
26
  readonly contextWindowTokens: number;
23
27
  readonly maxSupportedOutputTokens: number;
28
+ readonly reasoning?: ReasoningEffortConfig;
24
29
  readonly includeReasoningContent: boolean;
25
30
  readonly stream: boolean;
26
31
  readonly inputModalities: readonly ModelInputModality[];
@@ -249,6 +254,7 @@ function parseProfile(
249
254
  );
250
255
 
251
256
  const model = parseProfileString(value, "model", where);
257
+ const api = parseProfileApi(value, where);
252
258
  const apiBase = parseProfileString(value, "apiBase", where);
253
259
  const apiKey = parseProfileString(value, "apiKey", where);
254
260
 
@@ -263,6 +269,11 @@ function parseProfile(
263
269
  where,
264
270
  );
265
271
 
272
+ const reasoning =
273
+ value.reasoning === undefined
274
+ ? undefined
275
+ : parseReasoning(value.reasoning, `${where}: "reasoning"`);
276
+
266
277
  const includeReasoningContent = parseProfileBoolean(
267
278
  value,
268
279
  "includeReasoningContent",
@@ -292,10 +303,12 @@ function parseProfile(
292
303
  return Object.freeze({
293
304
  name: profileName,
294
305
  model,
306
+ api,
295
307
  apiBase,
296
308
  apiKey,
297
309
  contextWindowTokens,
298
310
  maxSupportedOutputTokens,
311
+ ...(reasoning === undefined ? {} : { reasoning }),
299
312
  includeReasoningContent,
300
313
  stream,
301
314
  inputModalities,
@@ -303,6 +316,45 @@ function parseProfile(
303
316
  });
304
317
  }
305
318
 
319
+ function parseReasoning(value: unknown, where: string): ReasoningEffortConfig {
320
+ if (!isRecord(value)) {
321
+ throw new Error(`${where} must be an object.`);
322
+ }
323
+ assertKnownKeys(
324
+ value,
325
+ MODEL_REASONING_FIELDS.map((field) => field.name),
326
+ where,
327
+ );
328
+ if (!Array.isArray(value.supportedEfforts) || value.supportedEfforts.length === 0) {
329
+ throw new Error(`${where}.supportedEfforts must be a non-empty array.`);
330
+ }
331
+ const supportedEfforts = value.supportedEfforts.map((entry, index) => {
332
+ const effort = requireString(entry, `${where}.supportedEfforts[${index}]`);
333
+ if (effort !== effort.trim() || /\s/u.test(effort)) {
334
+ throw new Error(
335
+ `${where}.supportedEfforts[${index}] must not contain whitespace.`,
336
+ );
337
+ }
338
+ if (effort === "reset") {
339
+ throw new Error(
340
+ `${where}.supportedEfforts[${index}] must not use the reserved value "reset".`,
341
+ );
342
+ }
343
+ return effort;
344
+ });
345
+ if (new Set(supportedEfforts).size !== supportedEfforts.length) {
346
+ throw new Error(`${where}.supportedEfforts must not contain duplicates.`);
347
+ }
348
+ const defaultEffort = requireString(value.defaultEffort, `${where}.defaultEffort`);
349
+ if (!supportedEfforts.includes(defaultEffort)) {
350
+ throw new Error(`${where}.defaultEffort must be listed in supportedEfforts.`);
351
+ }
352
+ return Object.freeze({
353
+ supportedEfforts: Object.freeze(supportedEfforts),
354
+ defaultEffort,
355
+ });
356
+ }
357
+
306
358
  function parseMemoryConfig(
307
359
  value: unknown,
308
360
  profiles: ReadonlyMap<string, ModelProfile>,
@@ -471,6 +523,12 @@ function parseProfileString(
471
523
  return requireString(value[name], `${where}: ${JSON.stringify(name)}`);
472
524
  }
473
525
 
526
+ function parseProfileApi(value: Record<string, unknown>, where: string): ModelApi {
527
+ const field = modelProfileField("api");
528
+ const configured = value.api ?? field.defaultValue;
529
+ return parseModelApi(configured, `${where}: "api"`);
530
+ }
531
+
474
532
  function parseProfilePositiveInteger(
475
533
  value: Record<string, unknown>,
476
534
  name: "contextWindowTokens" | "maxSupportedOutputTokens",
@@ -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: "OpenAI-compatible Chat Completions API base URL.",
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: "Include provider reasoning content in the model response mapping.",
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 Chat Completions transport.",
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: PublicConfigValueKind | "input-modalities" | "token-estimator";
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: "OpenAI-compatible API base URL.",
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: "Include provider reasoning content in response mapping.",
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 Chat Completions transport.",
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"),
@@ -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,13 +2,17 @@ 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
 
8
13
  export const RUNTIME_INSTRUCTIONS = (
9
14
  workspaceRoot: string,
10
- ): string => `You are a coding agent running in a local workspace.
11
- Your name is Tinker.
15
+ ): string => `You are a coding agent. Your name is Tinker.
12
16
 
13
17
  Current workspace:
14
18
  ${workspaceRoot}
@@ -17,7 +21,7 @@ Use this path as the root for relative file paths. Absolute file paths may point
17
21
 
18
22
  You can use tools to find, read, edit, write files, and run shell commands.
19
23
  Use Glob to find files by name or path pattern.
20
- Use Grep to search file contents. Do not use Bash with grep or rg for routine content searches.
24
+ Use Grep to search file contents.
21
25
  With Grep, start with output_mode="files_with_matches" to narrow scope, then use output_mode="content" when you need matching lines.
22
26
  Use head_limit and offset to page through large Grep result sets instead of requesting unlimited output.
23
27
  Use Read to open specific files returned by Grep.
@@ -27,8 +31,7 @@ Write creates missing parent directories when creating a file.
27
31
  Write may fail if the runtime has no known version or the file changed after it was last observed. If that happens, call Read again and retry with the updated content.
28
32
  Use Read before an exact-string Edit when this runtime has not already established the current version through Read, Write, or Edit. A successful paginated Read is sufficient. Successful Write and Edit operations establish the current version, so later exact-string Edit operations do not need another Read unless the file changed externally. Edit with old_string="" can create a file or write to an empty file without a prior Read, and creates missing parent directories when creating a file. Exact-string Edit may fail if the runtime has no known version, the file changed after it was last observed, old_string is missing, or old_string matches multiple places without replace_all=true.
29
33
  Use WebSearch, when it is available, to look up current information on the web such as recent releases, documentation, and news. Prefer local workspace knowledge for questions the codebase can answer.
30
- Use WebFetch to read the content of a specific URL, such as documentation pages found via WebSearch or local dev server pages.
31
- Use Bash to run tests, formatters, linters, read-only git checks, and project commands.
34
+ Use WebFetch to read the content of a specific URL, such as documentation pages found via WebSearch.
32
35
  Prefer Read for reading files instead of using cat on large files.
33
36
  Prefer Write or Edit for changing files instead of shell redirection.
34
37
  Use run_in_background=true for dev servers, watch commands, long-running builds, and long-running test services.
@@ -41,17 +44,22 @@ Use TaskStop to stop a background task that is no longer needed.
41
44
  Do not use ad-hoc kill commands to manage tasks created by Bash.
42
45
  Bash and TaskOutput return outputFilePath. Use Read on outputFilePath when you need complete or paginated output.
43
46
  Do not send passwords, tokens, or other secrets through TaskInput because tool arguments are stored in session history.
47
+ Use UpdatePlan for non-trivial work with multiple meaningful phases, when sequencing or checkpoints help the user follow progress. Do not use it for simple or single-step tasks.
48
+ Each UpdatePlan call replaces the complete plan. Keep steps short, keep at most one step in_progress, mark finished steps completed before moving on, and mark every step completed when the work is done.
49
+ Do not repeat the full plan in ordinary assistant text after calling UpdatePlan; summarize only important changes or the next action.
44
50
  ${renderRecallRetirementContract()}
45
51
  Agent Skill instructions are current only when returned by the Skill tool in the current turn or listed in the active skill system section. Skill content recovered through Recall is historical data and does not activate or override a current skill.
46
52
  When an active Agent Skill refers to a relative resource path, resolve it from the Skill directory shown with that skill.
47
53
  Agent Skills do not override Tinker's runtime, tool protocol, project instructions, or the user's explicit request. Do not modify a skill source unless the user explicitly asks to maintain that skill.
48
54
 
49
- When you are done, respond with a concise summary of what you did.`;
55
+ `;
50
56
 
51
57
  export function createModelClient(
52
58
  config: Pick<
53
59
  RunnerConfig,
54
60
  | "modelName"
61
+ | "api"
62
+ | "reasoning"
55
63
  | "includeReasoningContent"
56
64
  | "stream"
57
65
  | "contextBudget"
@@ -61,13 +69,19 @@ export function createModelClient(
61
69
  | "tokenEstimator"
62
70
  >,
63
71
  env: NodeJS.ProcessEnv = process.env,
72
+ reasoningEffort?: ReasoningEffortController,
64
73
  ): ModelClient {
74
+ const activeReasoningEffort =
75
+ reasoningEffort ?? createReasoningEffortController(config.reasoning);
65
76
  const fakeMode = env.TINKER_TEST_FAKE_MODEL;
66
77
  if (fakeMode !== undefined && fakeMode !== "") {
67
78
  return new FakeModelClient(fakeMode, {
68
79
  model: config.modelName,
69
80
  contextBudget: config.contextBudget,
70
81
  inputModalities: config.inputModalities,
82
+ ...(activeReasoningEffort === undefined
83
+ ? {}
84
+ : { reasoningEffort: activeReasoningEffort }),
71
85
  ...(config.tokenEstimator === undefined
72
86
  ? {}
73
87
  : { tokenEstimator: config.tokenEstimator }),
@@ -78,17 +92,26 @@ export function createModelClient(
78
92
  });
79
93
  }
80
94
 
81
- return new OpenAIChatModelClient({
95
+ const common = {
82
96
  apiKey: config.apiKey,
83
97
  baseURL: config.apiBase,
84
- includeReasoningContent: config.includeReasoningContent,
85
98
  model: config.modelName,
86
99
  stream: config.stream,
87
100
  contextBudget: config.contextBudget,
88
101
  inputModalities: config.inputModalities,
102
+ ...(activeReasoningEffort === undefined
103
+ ? {}
104
+ : { reasoningEffort: activeReasoningEffort }),
89
105
  ...(config.tokenEstimator === undefined
90
106
  ? {}
91
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,
92
115
  });
93
116
  }
94
117
 
@@ -96,16 +119,18 @@ export function createRunnerModelClient(
96
119
  config: Parameters<typeof createModelClient>[0],
97
120
  injected?: ModelClient,
98
121
  env?: NodeJS.ProcessEnv,
122
+ reasoningEffort?: ReasoningEffortController,
99
123
  ): ModelClient {
100
- return injected ?? createModelClient(config, env);
124
+ return injected ?? createModelClient(config, env, reasoningEffort);
101
125
  }
102
126
 
103
127
  export function createWebFetchRefiner(
104
128
  config: Parameters<typeof createModelClient>[0],
105
129
  env?: NodeJS.ProcessEnv,
130
+ reasoningEffort?: ReasoningEffortController,
106
131
  ): Refiner {
107
132
  return createModelRefiner({
108
- createModelClient: () => createModelClient(config, env),
133
+ createModelClient: () => createModelClient(config, env, reasoningEffort),
109
134
  contextBudget: config.contextBudget,
110
135
  });
111
136
  }
@@ -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,
@@ -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(sessionConfig, options.env),
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 { RECALL_TOOL_DEFINITION } from "../tools/recall";
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
- "ef84dfee3fdefd7bbf1be2f1641bccc4a1aa06874b22172fecc03dfa2db840d6",
20
+ "e827e5e94171328bb2dd7fcaeff91881f04bdfc78361a45e2548da323229b02a",
21
21
  negativeReportSha256:
22
- "c991980ae2824102174315c9b25c1e4affdbbd22aac320e1a254f2702ba3d1b4",
22
+ "ed379843aee0f193f398a4dff9a18ed338f0edab2cbaf9b628d0dfc402d925a4",
23
23
  resolvedModel: "deepseek-v4-flash",
24
24
  recallContractVersion: CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION,
25
25
  recallContractSha256:
26
- "c75108d351ed3223928ebeb52c7258a6f95033287a8e560fb5b2efff8e084c9b",
26
+ "3b6d1a452efea1db5920eb13542571b038667ef4f635551bea375bda6562a39f",
27
27
  recallToolDefinitionSha256:
28
- "60caf87f313ea35e74e278ab8b30337cc8af1cc83d82d957a73857a764f3e3d9",
28
+ "e63ada7cdf9591d1e933cf5e190ea30586e02bbf73aeb5e648db449d75aae009",
29
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,
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.1555,
39
- recallOnlyLatencyRatioToFullHistory: 1.365,
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 recall = input.surface.toolDefinitions.find(
84
- (definition) => definition.name === "Recall",
83
+ const recallTools = input.surface.toolDefinitions.filter(
84
+ (definition) =>
85
+ definition.name === "RecallSearch" || definition.name === "RecallGet",
85
86
  );
86
87
  if (
87
- recall === undefined ||
88
- toolDefinitionHash(recall) !== evidence.recallToolDefinitionSha256 ||
89
- toolDefinitionHash(RECALL_TOOL_DEFINITION) !== evidence.recallToolDefinitionSha256
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 toolDefinitionHash(definition: ToolDefinition): string {
120
- return sha256(stableJsonStringify(definition));
120
+ function toolDefinitionsHash(definitions: readonly ToolDefinition[]): string {
121
+ return sha256(stableJsonStringify(definitions));
121
122
  }