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.
- package/CHANGELOG.md +41 -1
- package/README.md +65 -6
- package/package.json +1 -1
- package/src/agent/loop.ts +13 -0
- package/src/agent/runtime-session.ts +165 -0
- package/src/agent/session-ledger.ts +20 -3
- package/src/cli/config.ts +11 -2
- package/src/cli/model-profiles.ts +58 -0
- package/src/cli/public-config-contract.ts +73 -7
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/runner-dependencies.ts +35 -10
- package/src/cli/tui-memory.ts +4 -0
- package/src/cli/tui-runner.tsx +8 -1
- package/src/context/context-automation-policy.ts +22 -21
- package/src/context/context-manager.ts +91 -15
- package/src/context/context-policy.ts +0 -2
- package/src/context/context-swap-renderer.ts +1 -1
- package/src/context/prefix-retirement-planner.ts +58 -8
- package/src/context/recall-retirement-contract.ts +5 -4
- package/src/context/swap-planner.ts +33 -27
- package/src/events/stdout-event-printer.ts +17 -0
- package/src/model/fake-model-client.ts +26 -16
- package/src/model/model-api.ts +12 -0
- package/src/model/model-client.ts +9 -1
- package/src/model/moonshot-input-token-estimator.ts +5 -1
- package/src/model/openai-chat-mapping.ts +2 -24
- package/src/model/openai-chat-model-client.ts +18 -294
- package/src/model/openai-image-mapping.ts +20 -0
- package/src/model/openai-model-utils.ts +304 -0
- package/src/model/openai-responses-mapping.ts +532 -0
- package/src/model/openai-responses-model-client.ts +295 -0
- package/src/model/openai-responses-stream.ts +96 -0
- package/src/model/openai-responses-token-estimator.ts +155 -0
- package/src/model/reasoning-effort.ts +60 -0
- package/src/observation/observation-builder.ts +7 -0
- package/src/session/session-catalog.ts +2 -2
- package/src/session/session-history-reader.ts +6 -1
- package/src/session/session-schema.ts +268 -4
- package/src/session/session-store.ts +106 -26
- package/src/skills/skill-context.ts +2 -2
- package/src/tools/bounded-output-preview.ts +276 -0
- package/src/tools/recall.ts +67 -36
- package/src/tools/registry.ts +9 -2
- package/src/tools/task-output-snapshot.ts +6 -22
- package/src/tools/task-output.ts +23 -27
- package/src/tools/types.ts +19 -0
- package/src/tools/update-plan.ts +166 -0
- package/src/tui/app.tsx +82 -5
- package/src/tui/components/plan-view.tsx +43 -0
- package/src/tui/components/prompt-input.tsx +9 -1
- package/src/tui/components/timeline.tsx +7 -0
- package/src/tui/event-store.ts +28 -2
- package/src/tui/slash-commands.ts +20 -0
- package/src/tui/tui-session-controller.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ 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
|
+
|
|
32
|
+
## [1.9.0] - 2026-08-04
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Add an UpdatePlan tool that lets the agent maintain a visible, ordered task
|
|
37
|
+
plan for multi-phase work, rendered as a plan view in the TUI timeline and
|
|
38
|
+
printed by the one-shot CLI. Plans are validated (at most 12 steps, at most
|
|
39
|
+
one step in progress), persisted in canonical session history, and fully
|
|
40
|
+
restored when resuming a session.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Trim the runtime system prompt to keep core tool-usage guidance compact.
|
|
45
|
+
|
|
8
46
|
## [1.8.0] - 2026-08-02
|
|
9
47
|
|
|
10
48
|
### Added
|
|
@@ -161,7 +199,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
161
199
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
162
200
|
executable.
|
|
163
201
|
|
|
164
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/v1.
|
|
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
|
|
204
|
+
[1.9.0]: https://github.com/ishowshao/tinker/releases/tag/v1.9.0
|
|
165
205
|
[1.8.0]: https://github.com/ishowshao/tinker/releases/tag/v1.8.0
|
|
166
206
|
[1.7.0]: https://github.com/ishowshao/tinker/releases/tag/v1.7.0
|
|
167
207
|
[1.6.0]: https://github.com/ishowshao/tinker/releases/tag/v1.6.0
|
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Built with [Bun](https://bun.sh) + TypeScript ESM, powered by [Ink](https://gith
|
|
|
13
13
|
- `Read` / `Write` / `Edit` — File I/O with content hashing and concurrent-modification protection
|
|
14
14
|
- `Delete` — Delete one existing regular file without directory or symlink support
|
|
15
15
|
- `Bash` — Run foreground, background, and PTY shell commands with per-task working directories
|
|
16
|
+
- `UpdatePlan` — Track a complete ordered task plan and its progress
|
|
16
17
|
- `TaskList` / `TaskOutput` / `TaskInput` / `TaskStop` — Inspect, interact with, and stop long-running shell tasks
|
|
17
18
|
- `WebSearch` — Search the web via Exa API
|
|
18
19
|
- `WebFetch` — Fetch and refine web page content (local, browser, or Exa backend)
|
|
@@ -139,12 +140,13 @@ are required. Boolean environment values accept case-insensitive `true/false`,
|
|
|
139
140
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
140
141
|
| `TINKER_MODELS` | Model | All modes | No | Non-empty string | — | No | Optional model profiles JSON path. Relative paths resolve from the process cwd. |
|
|
141
142
|
| `TINKER_MODEL` | Model | Env mode | Env mode | Non-empty string | — | No | Model name used when model profiles are not configured. |
|
|
142
|
-
| `
|
|
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. |
|
|
143
145
|
| `TINKER_API_KEY` | Model | Env mode | Env mode | Non-empty string | — | Yes | API credential for the configured model endpoint. |
|
|
144
146
|
| `TINKER_CONTEXT_WINDOW_TOKENS` | Model | Env mode | Env mode | Positive integer | — | No | Model context-window size in tokens. |
|
|
145
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. |
|
|
146
|
-
| `TINKER_INCLUDE_REASONING_CONTENT` | Model | Env mode | No | Boolean | `false` | No |
|
|
147
|
-
| `TINKER_STREAM` | Model | Env mode | No | Boolean | `true` | No | Use streaming
|
|
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. |
|
|
148
150
|
| `TINKER_WEBFETCH_REFINE_MODEL` | Model | Env mode | No | Non-empty string | — | No | Optional WebFetch refiner model; currently must match TINKER_MODEL. |
|
|
149
151
|
| `TINKER_WORKSPACE` | Workspace | All modes | No | Non-empty string | Process cwd | No | Workspace path. Relative paths resolve from the process cwd. |
|
|
150
152
|
| `TINKER_MAX_ITERATIONS` | Workspace | All modes | No | Positive integer | `512` | No | Maximum agent-loop iterations per turn. |
|
|
@@ -173,21 +175,36 @@ switch to another profile. If `TINKER_MODELS` is not set, Tinker falls back to
|
|
|
173
175
|
the individual `TINKER_*` environment variables. A configured profiles file must
|
|
174
176
|
exist and be valid; Tinker does not silently fall back when it cannot be loaded.
|
|
175
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
|
+
|
|
176
182
|
<!-- BEGIN GENERATED: MODEL PROFILE FIELDS -->
|
|
177
183
|
Profile fields:
|
|
178
184
|
|
|
179
185
|
| Field | Required | Type / constraint | Default | Secret | Description |
|
|
180
186
|
| --- | --- | --- | --- | --- | --- |
|
|
181
187
|
| `model` | Yes | Non-empty string | — | No | Provider model name. |
|
|
182
|
-
| `
|
|
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. |
|
|
183
190
|
| `apiKey` | Yes | Non-empty string | — | Yes | API credential for this profile. |
|
|
184
191
|
| `contextWindowTokens` | Yes | Positive integer | — | No | Model context-window size in tokens. |
|
|
185
192
|
| `maxSupportedOutputTokens` | Yes | Positive integer | — | No | Maximum output-token count supported by the model; must not exceed contextWindowTokens. |
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
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. |
|
|
188
196
|
| `inputModalities` | No | Normalized modality array | `["text"]` | No | Accepted model input modalities; normalizes to ["text"] or ["text", "image"]. |
|
|
189
197
|
| `tokenEstimator` | With image | Object | — | Yes | Independent token estimator required for image profiles. |
|
|
190
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
|
+
|
|
191
208
|
`tokenEstimator` fields:
|
|
192
209
|
|
|
193
210
|
| Field | Type / constraint | Secret | Description |
|
|
@@ -211,6 +228,14 @@ Text-only profile example:
|
|
|
211
228
|
"apiKey": "your-model-api-key",
|
|
212
229
|
"contextWindowTokens": 128000,
|
|
213
230
|
"maxSupportedOutputTokens": 8192,
|
|
231
|
+
"reasoning": {
|
|
232
|
+
"supportedEfforts": [
|
|
233
|
+
"low",
|
|
234
|
+
"medium",
|
|
235
|
+
"high"
|
|
236
|
+
],
|
|
237
|
+
"defaultEffort": "medium"
|
|
238
|
+
},
|
|
214
239
|
"includeReasoningContent": false,
|
|
215
240
|
"stream": true,
|
|
216
241
|
"inputModalities": [
|
|
@@ -233,6 +258,14 @@ Image-capable profile example:
|
|
|
233
258
|
"apiKey": "your-model-api-key",
|
|
234
259
|
"contextWindowTokens": 128000,
|
|
235
260
|
"maxSupportedOutputTokens": 8192,
|
|
261
|
+
"reasoning": {
|
|
262
|
+
"supportedEfforts": [
|
|
263
|
+
"low",
|
|
264
|
+
"medium",
|
|
265
|
+
"high"
|
|
266
|
+
],
|
|
267
|
+
"defaultEffort": "medium"
|
|
268
|
+
},
|
|
236
269
|
"includeReasoningContent": false,
|
|
237
270
|
"stream": true,
|
|
238
271
|
"inputModalities": [
|
|
@@ -284,6 +317,14 @@ Atomic-memory profile example:
|
|
|
284
317
|
"apiKey": "your-model-api-key",
|
|
285
318
|
"contextWindowTokens": 128000,
|
|
286
319
|
"maxSupportedOutputTokens": 8192,
|
|
320
|
+
"reasoning": {
|
|
321
|
+
"supportedEfforts": [
|
|
322
|
+
"low",
|
|
323
|
+
"medium",
|
|
324
|
+
"high"
|
|
325
|
+
],
|
|
326
|
+
"defaultEffort": "medium"
|
|
327
|
+
},
|
|
287
328
|
"includeReasoningContent": false,
|
|
288
329
|
"stream": true,
|
|
289
330
|
"inputModalities": [
|
|
@@ -306,6 +347,12 @@ Atomic-memory profile example:
|
|
|
306
347
|
```
|
|
307
348
|
<!-- END GENERATED: MODEL PROFILE FIELDS -->
|
|
308
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
|
+
|
|
309
356
|
You can also select profiles explicitly for the TUI or one-shot command:
|
|
310
357
|
|
|
311
358
|
```bash
|
|
@@ -352,6 +399,7 @@ complete fixed policy and persistence contract.
|
|
|
352
399
|
| `/view <path>` | View a local UTF-8 text file |
|
|
353
400
|
| `/copy` | Copy the last response as Markdown |
|
|
354
401
|
| `/model [profile-name]` | Switch model profile (new session) |
|
|
402
|
+
| `/reasoning [effort\|reset]` | Show or change reasoning effort for this session runtime |
|
|
355
403
|
| `/resume [session-id]` | Choose or resume a session |
|
|
356
404
|
| `/session delete <session-id> --confirm` | Manage stored sessions |
|
|
357
405
|
| `/quit` | Exit the TUI |
|
|
@@ -363,6 +411,17 @@ swaps eligible historical tool output, while `/compact retire` retires a complet
|
|
|
363
411
|
cold prefix whose original history remains available through `Recall`. `/copy`
|
|
364
412
|
copies the last completed assistant response as raw Markdown.
|
|
365
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
|
+
|
|
366
425
|
### Project Custom Slash Commands
|
|
367
426
|
|
|
368
427
|
The TUI loads optional project-scoped prompt aliases from `.tinker.json` in the
|
package/package.json
CHANGED
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
|
|
192
|
-
private
|
|
193
|
-
private
|
|
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
|
-
|
|
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:
|
|
207
|
+
includeReasoningContent:
|
|
208
|
+
environment.api === "chat-completions" && environment.includeReasoningContent,
|
|
200
209
|
stream: environment.stream,
|
|
201
210
|
contextProfile,
|
|
202
211
|
contextBudget: deriveModelContextBudget(contextProfile),
|