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.
Files changed (47) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +64 -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 +28 -4
  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/model/fake-model-client.ts +26 -16
  22. package/src/model/model-api.ts +12 -0
  23. package/src/model/model-client.ts +9 -1
  24. package/src/model/moonshot-input-token-estimator.ts +5 -1
  25. package/src/model/openai-chat-mapping.ts +2 -24
  26. package/src/model/openai-chat-model-client.ts +18 -294
  27. package/src/model/openai-image-mapping.ts +20 -0
  28. package/src/model/openai-model-utils.ts +304 -0
  29. package/src/model/openai-responses-mapping.ts +532 -0
  30. package/src/model/openai-responses-model-client.ts +295 -0
  31. package/src/model/openai-responses-stream.ts +96 -0
  32. package/src/model/openai-responses-token-estimator.ts +155 -0
  33. package/src/model/reasoning-effort.ts +60 -0
  34. package/src/session/session-catalog.ts +2 -2
  35. package/src/session/session-history-reader.ts +6 -1
  36. package/src/session/session-schema.ts +268 -4
  37. package/src/session/session-store.ts +105 -26
  38. package/src/skills/skill-context.ts +2 -2
  39. package/src/tools/bounded-output-preview.ts +276 -0
  40. package/src/tools/recall.ts +67 -36
  41. package/src/tools/registry.ts +7 -2
  42. package/src/tools/task-output-snapshot.ts +6 -22
  43. package/src/tools/task-output.ts +23 -27
  44. package/src/tui/app.tsx +82 -5
  45. package/src/tui/components/prompt-input.tsx +9 -1
  46. package/src/tui/slash-commands.ts +20 -0
  47. package/src/tui/tui-session-controller.ts +7 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable user-facing changes to Tinker are documented here. The project follo
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.10.1] - 2026-08-15
9
+
10
+ ### Added
11
+
12
+ - Add an OpenAI Responses API adapter selectable per model profile, including
13
+ stateless request mapping, streaming, reasoning output, tool calls, image
14
+ input, token estimation, and compatible encrypted reasoning items.
15
+ - Add per-profile reasoning effort configuration and a session-runtime
16
+ `/reasoning` control. The TUI displays the active effort, and `Ctrl+R` cycles
17
+ through supported efforts in profile order without changing configuration or
18
+ canonical history.
19
+ - Add a model provider configuration guide with ready-to-adapt examples for
20
+ OpenAI, Kimi K3, and Zhipu GLM-5.2.
21
+
22
+ ### Changed
23
+
24
+ - Split historical session retrieval into `RecallSearch` and `RecallGet`, so the
25
+ agent can locate relevant history before retrieving exact bounded content.
26
+ - Track active-turn tool-output consumption when maintaining context, allowing
27
+ already-consumed observations to be compacted safely during long-running
28
+ turns.
29
+ - Bound Bash and background-task output previews while preserving complete
30
+ output in log files for paginated inspection.
31
+
8
32
  ## [1.9.0] - 2026-08-04
9
33
 
10
34
  ### Added
@@ -175,7 +199,8 @@ All notable user-facing changes to Tinker are documented here. The project follo
175
199
  - First formal npm release under the `tinker-agent` package name with the `tinker`
176
200
  executable.
177
201
 
178
- [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.9.0...HEAD
202
+ [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.10.1...HEAD
203
+ [1.10.1]: https://github.com/ishowshao/tinker/releases/tag/v1.10.1
179
204
  [1.9.0]: https://github.com/ishowshao/tinker/releases/tag/v1.9.0
180
205
  [1.8.0]: https://github.com/ishowshao/tinker/releases/tag/v1.8.0
181
206
  [1.7.0]: https://github.com/ishowshao/tinker/releases/tag/v1.7.0
package/README.md CHANGED
@@ -140,12 +140,13 @@ are required. Boolean environment values accept case-insensitive `true/false`,
140
140
  | --- | --- | --- | --- | --- | --- | --- | --- |
141
141
  | `TINKER_MODELS` | Model | All modes | No | Non-empty string | — | No | Optional model profiles JSON path. Relative paths resolve from the process cwd. |
142
142
  | `TINKER_MODEL` | Model | Env mode | Env mode | Non-empty string | — | No | Model name used when model profiles are not configured. |
143
- | `TINKER_BASE_URL` | Model | Env mode | Env mode | Non-empty string | | No | OpenAI-compatible Chat Completions API base URL. |
143
+ | `TINKER_API` | Model | Env mode | No | Non-empty string | `"chat-completions"` | No | Model API adapter: "chat-completions" or "responses". |
144
+ | `TINKER_BASE_URL` | Model | Env mode | Env mode | Non-empty string | — | No | OpenAI-compatible API root URL; do not append /chat/completions or /responses. |
144
145
  | `TINKER_API_KEY` | Model | Env mode | Env mode | Non-empty string | — | Yes | API credential for the configured model endpoint. |
145
146
  | `TINKER_CONTEXT_WINDOW_TOKENS` | Model | Env mode | Env mode | Positive integer | — | No | Model context-window size in tokens. |
146
147
  | `TINKER_MAX_SUPPORTED_OUTPUT_TOKENS` | Model | Env mode | Env mode | Positive integer | — | No | Maximum output-token count supported by the model; must not exceed the context window. |
147
- | `TINKER_INCLUDE_REASONING_CONTENT` | Model | Env mode | No | Boolean | `false` | No | Include provider reasoning content in the model response mapping. |
148
- | `TINKER_STREAM` | Model | Env mode | No | Boolean | `true` | No | Use streaming Chat Completions transport. |
148
+ | `TINKER_INCLUDE_REASONING_CONTENT` | Model | Env mode | No | Boolean | `false` | No | Replay provider reasoning_content in Chat Completions history; ignored by Responses. |
149
+ | `TINKER_STREAM` | Model | Env mode | No | Boolean | `true` | No | Use streaming transport for the selected model API. |
149
150
  | `TINKER_WEBFETCH_REFINE_MODEL` | Model | Env mode | No | Non-empty string | — | No | Optional WebFetch refiner model; currently must match TINKER_MODEL. |
150
151
  | `TINKER_WORKSPACE` | Workspace | All modes | No | Non-empty string | Process cwd | No | Workspace path. Relative paths resolve from the process cwd. |
151
152
  | `TINKER_MAX_ITERATIONS` | Workspace | All modes | No | Positive integer | `512` | No | Maximum agent-loop iterations per turn. |
@@ -174,21 +175,36 @@ switch to another profile. If `TINKER_MODELS` is not set, Tinker falls back to
174
175
  the individual `TINKER_*` environment variables. A configured profiles file must
175
176
  exist and be valid; Tinker does not silently fall back when it cannot be loaded.
176
177
 
178
+ For provider-specific examples and guidance on API adapters, model capabilities,
179
+ reasoning efforts, and context limits, see the
180
+ [`.tinker/models.json` provider configuration guide](docs/models-json-provider-guide.md).
181
+
177
182
  <!-- BEGIN GENERATED: MODEL PROFILE FIELDS -->
178
183
  Profile fields:
179
184
 
180
185
  | Field | Required | Type / constraint | Default | Secret | Description |
181
186
  | --- | --- | --- | --- | --- | --- |
182
187
  | `model` | Yes | Non-empty string | — | No | Provider model name. |
183
- | `apiBase` | Yes | Non-empty string | | No | OpenAI-compatible API base URL. |
188
+ | `api` | No | Non-empty string | `"chat-completions"` | No | Model API adapter: "chat-completions" or "responses". |
189
+ | `apiBase` | Yes | Non-empty string | — | No | OpenAI-compatible API root URL; do not append /chat/completions or /responses. |
184
190
  | `apiKey` | Yes | Non-empty string | — | Yes | API credential for this profile. |
185
191
  | `contextWindowTokens` | Yes | Positive integer | — | No | Model context-window size in tokens. |
186
192
  | `maxSupportedOutputTokens` | Yes | Positive integer | — | No | Maximum output-token count supported by the model; must not exceed contextWindowTokens. |
187
- | `includeReasoningContent` | No | JSON boolean | `false` | No | Include provider reasoning content in response mapping. |
188
- | `stream` | No | JSON boolean | `true` | No | Use streaming Chat Completions transport. |
193
+ | `reasoning` | No | Object | | No | Provider-specific reasoning efforts and the default for each new session runtime. |
194
+ | `includeReasoningContent` | No | JSON boolean | `false` | No | Replay provider reasoning_content in Chat Completions history; ignored by Responses. |
195
+ | `stream` | No | JSON boolean | `true` | No | Use streaming transport for the selected model API. |
189
196
  | `inputModalities` | No | Normalized modality array | `["text"]` | No | Accepted model input modalities; normalizes to ["text"] or ["text", "image"]. |
190
197
  | `tokenEstimator` | With image | Object | — | Yes | Independent token estimator required for image profiles. |
191
198
 
199
+ `reasoning` fields:
200
+
201
+ | Field | Type / constraint | Description |
202
+ | --- | --- | --- |
203
+ | `supportedEfforts` | Non-empty unique string array | Provider-supported effort values exposed by the /reasoning command. |
204
+ | `defaultEffort` | Non-empty string listed above | Effort used whenever a session runtime is created or reopened. |
205
+
206
+ The optional `reasoning` object declares provider-specific effort values. Efforts must be unique non-whitespace strings, `reset` is reserved by the TUI command, and `defaultEffort` must appear in `supportedEfforts`. Omitting `reasoning` sends no effort parameter and disables `/reasoning` for that profile.
207
+
192
208
  `tokenEstimator` fields:
193
209
 
194
210
  | Field | Type / constraint | Secret | Description |
@@ -212,6 +228,14 @@ Text-only profile example:
212
228
  "apiKey": "your-model-api-key",
213
229
  "contextWindowTokens": 128000,
214
230
  "maxSupportedOutputTokens": 8192,
231
+ "reasoning": {
232
+ "supportedEfforts": [
233
+ "low",
234
+ "medium",
235
+ "high"
236
+ ],
237
+ "defaultEffort": "medium"
238
+ },
215
239
  "includeReasoningContent": false,
216
240
  "stream": true,
217
241
  "inputModalities": [
@@ -234,6 +258,14 @@ Image-capable profile example:
234
258
  "apiKey": "your-model-api-key",
235
259
  "contextWindowTokens": 128000,
236
260
  "maxSupportedOutputTokens": 8192,
261
+ "reasoning": {
262
+ "supportedEfforts": [
263
+ "low",
264
+ "medium",
265
+ "high"
266
+ ],
267
+ "defaultEffort": "medium"
268
+ },
237
269
  "includeReasoningContent": false,
238
270
  "stream": true,
239
271
  "inputModalities": [
@@ -285,6 +317,14 @@ Atomic-memory profile example:
285
317
  "apiKey": "your-model-api-key",
286
318
  "contextWindowTokens": 128000,
287
319
  "maxSupportedOutputTokens": 8192,
320
+ "reasoning": {
321
+ "supportedEfforts": [
322
+ "low",
323
+ "medium",
324
+ "high"
325
+ ],
326
+ "defaultEffort": "medium"
327
+ },
288
328
  "includeReasoningContent": false,
289
329
  "stream": true,
290
330
  "inputModalities": [
@@ -307,6 +347,12 @@ Atomic-memory profile example:
307
347
  ```
308
348
  <!-- END GENERATED: MODEL PROFILE FIELDS -->
309
349
 
350
+ Set `api` to `"responses"` to use the standard Responses API. Keep `apiBase`
351
+ at the API root—such as `https://api.openai.com/v1`—because Tinker appends the
352
+ `/responses` route. Responses requests use the stateless common subset
353
+ (`store: false` with complete input history), so the same adapter works with
354
+ OpenAI and compatible providers that do not implement stored response chaining.
355
+
310
356
  You can also select profiles explicitly for the TUI or one-shot command:
311
357
 
312
358
  ```bash
@@ -353,6 +399,7 @@ complete fixed policy and persistence contract.
353
399
  | `/view <path>` | View a local UTF-8 text file |
354
400
  | `/copy` | Copy the last response as Markdown |
355
401
  | `/model [profile-name]` | Switch model profile (new session) |
402
+ | `/reasoning [effort\|reset]` | Show or change reasoning effort for this session runtime |
356
403
  | `/resume [session-id]` | Choose or resume a session |
357
404
  | `/session delete <session-id> --confirm` | Manage stored sessions |
358
405
  | `/quit` | Exit the TUI |
@@ -364,6 +411,17 @@ swaps eligible historical tool output, while `/compact retire` retires a complet
364
411
  cold prefix whose original history remains available through `Recall`. `/copy`
365
412
  copies the last completed assistant response as raw Markdown.
366
413
 
414
+ Profiles that declare `reasoning` expose `/reasoning` as a session-runtime
415
+ control. `/reasoning <effort>` temporarily selects one of the profile's
416
+ `supportedEfforts`, and `/reasoning reset` restores `defaultEffort`. The
417
+ selection is not written to configuration or canonical history: `/clear`,
418
+ `/fork`, `/model`, `/resume`, and a TUI restart create a new runtime from the
419
+ profile default. Responses requests send `reasoning.effort`; Chat Completions
420
+ requests send `reasoning_effort`. Press `Ctrl+R` while the prompt is idle to cycle
421
+ through `supportedEfforts` in profile order, wrapping from the last effort to the
422
+ first. When configured, the TUI information line shows the live effort immediately
423
+ after the model name, for example `gpt-5.6-sol max`.
424
+
367
425
  ### Project Custom Slash Commands
368
426
 
369
427
  The TUI loads optional project-scoped prompt aliases from `.tinker.json` in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinker-agent",
3
- "version": "1.9.0",
3
+ "version": "1.10.1",
4
4
  "description": "A personal coding agent with an interactive TUI and one-shot CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/agent/loop.ts CHANGED
@@ -93,6 +93,7 @@ const MODEL_REQUEST_MAX_ATTEMPTS =
93
93
 
94
94
  export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
95
95
  let lastIteration: IterationIdentity | undefined;
96
+ let consumedThroughOrdinal = 1;
96
97
  const committedPrefixAuditor =
97
98
  input.committedPrefixAuditor ?? new CommittedPrefixAuditor();
98
99
 
@@ -154,6 +155,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
154
155
  built,
155
156
  prepared,
156
157
  preflight,
158
+ consumedThroughOrdinal,
157
159
  });
158
160
  try {
159
161
  input.contextMeter.assertWithinBudget(preflight);
@@ -365,6 +367,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
365
367
  },
366
368
  });
367
369
  const measured = input.contextMeter.recordProviderUsage(request, modelOutput);
370
+ consumedThroughOrdinal = built.canonical.messages.length;
368
371
  await input.runtimeSession.append({
369
372
  type: "context.usage.updated",
370
373
  ...iteration,
@@ -501,6 +504,11 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
501
504
  data: { outcome: "continue", toolCallCount: toolCalls.length },
502
505
  });
503
506
  input.runtimeSession.finishIterationForContinuation(iteration);
507
+ await input.runtimeSession.maintainContextAfterIteration?.({
508
+ turn: input.turn,
509
+ consumedThroughOrdinal,
510
+ ledger: input.ledger,
511
+ });
504
512
  }
505
513
 
506
514
  if (lastIteration === undefined) {
@@ -520,6 +528,7 @@ async function runShadowPlanning(input: {
520
528
  built: BuiltContextRequest;
521
529
  prepared: PreparedModelRequest;
522
530
  preflight: ReturnType<ContextMeter["measure"]>;
531
+ consumedThroughOrdinal: number;
523
532
  }): Promise<void> {
524
533
  const shadowPlanning = input.input.shadowPlanning;
525
534
  if (shadowPlanning === undefined) {
@@ -547,6 +556,10 @@ async function runShadowPlanning(input: {
547
556
  tools: input.input.tools.definitions(),
548
557
  policy: swapOnlyPolicyV1,
549
558
  trigger: decision.trigger,
559
+ activeTurn: {
560
+ turnId: input.input.turn.turnId,
561
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
562
+ },
550
563
  ...(decision.forcedTargetTokens === undefined
551
564
  ? {}
552
565
  : { forcedTargetTokens: decision.forcedTargetTokens }),
@@ -93,6 +93,7 @@ import { FatalAgentTurnError, runAgent, type RunAgentInput } from "./loop";
93
93
  import {
94
94
  AdmissionStaleError,
95
95
  SessionLedgerWriteError,
96
+ type AgentTurnLedger,
96
97
  type AdmissionBaseToken,
97
98
  type SessionLedger,
98
99
  } from "./session-ledger";
@@ -114,6 +115,7 @@ import {
114
115
  type ContextAutomationDecision,
115
116
  } from "../context/context-automation-policy";
116
117
  import type { SkillCatalogSnapshot } from "../skills/skill-loader";
118
+ import type { ReasoningEffortSnapshot } from "../model/reasoning-effort";
117
119
  import {
118
120
  activeSkillManifestEntry,
119
121
  createSkillCatalogSnapshot,
@@ -151,6 +153,9 @@ export type RuntimeSession = {
151
153
  skills(): RuntimeSkillsSnapshot;
152
154
  mcp(): McpInventorySnapshot;
153
155
  supportsImageInput(): boolean;
156
+ reasoningEffort(): ReasoningEffortSnapshot | undefined;
157
+ setReasoningEffort(effort: string): ReasoningEffortSnapshot;
158
+ resetReasoningEffort(): ReasoningEffortSnapshot;
154
159
  importImage(
155
160
  sourcePath: string,
156
161
  signal: AbortSignal,
@@ -217,6 +222,11 @@ export type RuntimeSessionContext = {
217
222
  iteration: IterationIdentity;
218
223
  built: BuiltContextRequest;
219
224
  }): void;
225
+ maintainContextAfterIteration?(input: {
226
+ turn: TurnIdentity;
227
+ consumedThroughOrdinal: number;
228
+ ledger: AgentTurnLedger;
229
+ }): Promise<void>;
220
230
  };
221
231
 
222
232
  export type ContextSurfaceRefreshSummary = {
@@ -482,6 +492,8 @@ class DefaultRuntimeSession implements RuntimeSession {
482
492
  onToolCompletionsCommitted: (completion) =>
483
493
  this.onToolCompletionsCommitted(completion),
484
494
  prepareModelDispatch: (dispatch) => this.prepareModelDispatch(dispatch),
495
+ maintainContextAfterIteration: (maintenance) =>
496
+ this.performActiveTurnContextMaintenance(maintenance),
485
497
  };
486
498
  }
487
499
 
@@ -965,6 +977,36 @@ class DefaultRuntimeSession implements RuntimeSession {
965
977
  return this.input.modelClient.inputModalities?.includes("image") === true;
966
978
  }
967
979
 
980
+ reasoningEffort(): ReasoningEffortSnapshot | undefined {
981
+ return this.input.modelClient.reasoningEffort?.snapshot();
982
+ }
983
+
984
+ setReasoningEffort(effort: string): ReasoningEffortSnapshot {
985
+ if (this.state !== "ready" || this.activeTurn !== undefined) {
986
+ throw new Error(
987
+ `Cannot change reasoning effort while RuntimeSession is ${this.state}.`,
988
+ );
989
+ }
990
+ const reasoningEffort = this.input.modelClient.reasoningEffort;
991
+ if (reasoningEffort === undefined) {
992
+ throw new Error("Current model profile does not configure reasoning effort.");
993
+ }
994
+ return reasoningEffort.set(effort);
995
+ }
996
+
997
+ resetReasoningEffort(): ReasoningEffortSnapshot {
998
+ if (this.state !== "ready" || this.activeTurn !== undefined) {
999
+ throw new Error(
1000
+ `Cannot change reasoning effort while RuntimeSession is ${this.state}.`,
1001
+ );
1002
+ }
1003
+ const reasoningEffort = this.input.modelClient.reasoningEffort;
1004
+ if (reasoningEffort === undefined) {
1005
+ throw new Error("Current model profile does not configure reasoning effort.");
1006
+ }
1007
+ return reasoningEffort.reset();
1008
+ }
1009
+
968
1010
  bashGuard(): BashGuardSnapshot {
969
1011
  return this.bashGuardSnapshot;
970
1012
  }
@@ -2016,6 +2058,129 @@ class DefaultRuntimeSession implements RuntimeSession {
2016
2058
  }
2017
2059
  }
2018
2060
 
2061
+ private async performActiveTurnContextMaintenance(input: {
2062
+ turn: TurnIdentity;
2063
+ consumedThroughOrdinal: number;
2064
+ ledger: AgentTurnLedger;
2065
+ }): Promise<void> {
2066
+ const automation = this.requireContextAutomation();
2067
+ if (!automation.automaticSwapOnly) return;
2068
+ if (this.state !== "executing") {
2069
+ throw new Error(
2070
+ `Cannot maintain active-turn context while RuntimeSession is ${this.state}.`,
2071
+ );
2072
+ }
2073
+ const manager = this.requireContextManager();
2074
+ const usage = manager.measureCurrent(input.turn.turnId, input.ledger);
2075
+ if (usage.pressure === "normal") return;
2076
+
2077
+ const qualificationId = requireAutomationQualificationId(automation);
2078
+ const compactionTrigger = {
2079
+ kind: "runtime_pressure",
2080
+ activeTurn: {
2081
+ turnId: input.turn.turnId,
2082
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
2083
+ },
2084
+ } as const;
2085
+ this.pendingAutomaticContextMaintenance = false;
2086
+ this.state = "maintaining_context";
2087
+ try {
2088
+ await this.append({
2089
+ type: "context.revision.started",
2090
+ sessionId: this.sessionId,
2091
+ data: {
2092
+ strategy: "swap",
2093
+ reason: "runtime_pressure",
2094
+ policyVersion: "swap-only-v1",
2095
+ rendererFormat: "swap-observation-v1",
2096
+ qualificationId,
2097
+ },
2098
+ });
2099
+ let swap: ContextCompactionResult;
2100
+ try {
2101
+ swap = await manager.compact(compactionTrigger, input.ledger);
2102
+ await this.append({
2103
+ type: "context.revision.finished",
2104
+ sessionId: this.sessionId,
2105
+ data: contextRevisionFinishedData(swap, "runtime_pressure", qualificationId),
2106
+ });
2107
+ } catch (error) {
2108
+ const failure = automaticContextFailure(error, "compaction");
2109
+ await this.append({
2110
+ type: "context.revision.failed",
2111
+ sessionId: this.sessionId,
2112
+ data: {
2113
+ strategy: "swap",
2114
+ reason: "runtime_pressure",
2115
+ stage: failure.stage,
2116
+ errorCode: boundedContextErrorCode(failure.code),
2117
+ error: `Automatic context compaction failed at ${failure.stage}.`,
2118
+ qualificationId,
2119
+ },
2120
+ }).catch(() => undefined);
2121
+ if (failure.fatal) throw error;
2122
+ return;
2123
+ }
2124
+
2125
+ if (
2126
+ !automation.automaticPrefixRetirement ||
2127
+ !automaticSwapNeedsRetirement(swap)
2128
+ ) {
2129
+ return;
2130
+ }
2131
+
2132
+ await this.append({
2133
+ type: "context.revision.started",
2134
+ sessionId: this.sessionId,
2135
+ data: {
2136
+ strategy: "retire_prefix",
2137
+ reason: "runtime_pressure",
2138
+ policyVersion: "recall-first-retirement-v1",
2139
+ baseRevisionNumber: this.store.loadContextSnapshot().revision.revisionNumber,
2140
+ qualificationId,
2141
+ },
2142
+ });
2143
+ try {
2144
+ const retirement = await manager.retirePrefix(
2145
+ {
2146
+ kind: "runtime_pressure",
2147
+ activeTurnId: input.turn.turnId,
2148
+ },
2149
+ input.ledger,
2150
+ );
2151
+ await this.append({
2152
+ type: "context.revision.finished",
2153
+ sessionId: this.sessionId,
2154
+ data: contextRetirementFinishedData(
2155
+ retirement,
2156
+ "runtime_pressure",
2157
+ qualificationId,
2158
+ ),
2159
+ });
2160
+ } catch (error) {
2161
+ const failure = automaticContextFailure(error, "retirement");
2162
+ await this.append({
2163
+ type: "context.revision.failed",
2164
+ sessionId: this.sessionId,
2165
+ data: {
2166
+ strategy: "retire_prefix",
2167
+ reason: "runtime_pressure",
2168
+ stage: failure.stage,
2169
+ errorCode: boundedContextErrorCode(failure.code),
2170
+ error: `Automatic context retirement failed at ${failure.stage}.`,
2171
+ committed: failure.committed,
2172
+ qualificationId,
2173
+ },
2174
+ }).catch(() => undefined);
2175
+ if (failure.fatal) throw error;
2176
+ }
2177
+ } finally {
2178
+ if (this.state === "maintaining_context") {
2179
+ this.state = "executing";
2180
+ }
2181
+ }
2182
+ }
2183
+
2019
2184
  private notifyCompletedTurn(turn: TurnIdentity): void {
2020
2185
  const hook = this.input.completedTurnHook;
2021
2186
  if (hook === undefined) {
@@ -111,6 +111,7 @@ export type AgentTurnLedger = {
111
111
  completions: readonly ToolCompletionInput[],
112
112
  ): readonly CommittedToolCompletion[];
113
113
  buildModelRequest(tools: readonly ToolDefinition[]): BuiltContextRequest;
114
+ activateContextSnapshot(snapshot: StoredContextSnapshotV8): void;
114
115
  };
115
116
 
116
117
  export type CommittedToolCompletion = {
@@ -188,9 +189,9 @@ export class InMemorySessionLedger implements SessionLedger {
188
189
  private readonly validator = new ContextProtocolValidator();
189
190
  private readonly contextBuilder: ContextBuilder;
190
191
  private readonly revisionCompiler: ContextRevisionCompiler;
191
- private readonly revision: StoredContextRevisionV8;
192
- private readonly surface: StoredContextSurfaceV8;
193
- private readonly activeOverrides: readonly StoredContextOverrideV8[];
192
+ private revision: StoredContextRevisionV8;
193
+ private surface: StoredContextSurfaceV8;
194
+ private activeOverrides: readonly StoredContextOverrideV8[];
194
195
  private readonly clock: () => string;
195
196
 
196
197
  constructor(private readonly input: CreateInMemorySessionLedgerInput) {
@@ -354,6 +355,20 @@ export class InMemorySessionLedger implements SessionLedger {
354
355
  return this.view.messages.length;
355
356
  }
356
357
 
358
+ activateContextSnapshot(snapshot: StoredContextSnapshotV8): void {
359
+ this.requireHealthy("activate a context snapshot");
360
+ if (
361
+ snapshot.meta.sessionId !== this.input.sessionId ||
362
+ stableJsonStringify(snapshot.canonical) !== stableJsonStringify(this.view)
363
+ ) {
364
+ throw new Error("Activated context snapshot does not match canonical history.");
365
+ }
366
+ this.revisionCompiler.compileActive(snapshot);
367
+ this.revision = snapshot.revision;
368
+ this.surface = snapshot.surface;
369
+ this.activeOverrides = snapshot.activeOverrides;
370
+ }
371
+
357
372
  snapshot(
358
373
  options: {
359
374
  fullIntegrity?: boolean;
@@ -766,6 +781,8 @@ class InMemoryPendingLedgerTurn implements PendingLedgerTurn {
766
781
  commitToolCompletions: (completions) =>
767
782
  this.ledger.commitToolCompletions(this, completions),
768
783
  buildModelRequest: (tools) => this.ledger.buildTurnModelRequest(this, tools),
784
+ activateContextSnapshot: (snapshot) =>
785
+ this.ledger.activateContextSnapshot(snapshot),
769
786
  };
770
787
  }
771
788
 
package/src/cli/config.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  type ModelContextBudget,
7
7
  type ModelContextProfile,
8
8
  } from "../model/model-context-profile";
9
+ import type { ModelApi } from "../model/model-api";
9
10
  import {
10
11
  loadModelProfiles,
11
12
  persistDefaultProfile,
@@ -17,6 +18,7 @@ import {
17
18
  unknownProfileError,
18
19
  } from "./model-profiles";
19
20
  import type { MemoryEmbeddingConfig } from "../memory/contracts";
21
+ import type { ReasoningEffortConfig } from "../model/reasoning-effort";
20
22
  import {
21
23
  parsePublicEnvironment,
22
24
  type ParsedPublicEnvironment,
@@ -27,9 +29,11 @@ export type RunnerConfig = {
27
29
  readonly sessionId: SessionId;
28
30
  readonly workspaceRoot: string;
29
31
  readonly modelName: string;
32
+ readonly api: ModelApi;
30
33
  readonly apiKey: string;
31
34
  readonly apiBase: string;
32
35
  readonly maxIterations: number;
36
+ readonly reasoning?: ReasoningEffortConfig;
33
37
  readonly includeReasoningContent: boolean;
34
38
  readonly stream: boolean;
35
39
  readonly contextProfile: ModelContextProfile;
@@ -166,10 +170,13 @@ function runnerConfigTemplateFromProfile(
166
170
  return Object.freeze({
167
171
  workspaceRoot: environment.workspaceRoot,
168
172
  modelName: profile.model,
173
+ api: profile.api,
169
174
  apiKey: profile.apiKey,
170
175
  apiBase: profile.apiBase,
171
176
  maxIterations: environment.maxIterations,
172
- includeReasoningContent: profile.includeReasoningContent,
177
+ ...(profile.reasoning === undefined ? {} : { reasoning: profile.reasoning }),
178
+ includeReasoningContent:
179
+ profile.api === "chat-completions" && profile.includeReasoningContent,
173
180
  stream: profile.stream,
174
181
  contextProfile,
175
182
  contextBudget: deriveModelContextBudget(contextProfile),
@@ -193,10 +200,12 @@ function runnerConfigTemplateFromEnvironment(
193
200
  return Object.freeze({
194
201
  workspaceRoot: environment.workspaceRoot,
195
202
  modelName: environment.modelName,
203
+ api: environment.api,
196
204
  apiKey: environment.apiKey,
197
205
  apiBase: environment.apiBase,
198
206
  maxIterations: environment.maxIterations,
199
- includeReasoningContent: environment.includeReasoningContent,
207
+ includeReasoningContent:
208
+ environment.api === "chat-completions" && environment.includeReasoningContent,
200
209
  stream: environment.stream,
201
210
  contextProfile,
202
211
  contextBudget: deriveModelContextBudget(contextProfile),
@@ -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",