tinker-agent 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +64 -6
  3. package/package.json +1 -1
  4. package/src/agent/loop.ts +17 -0
  5. package/src/agent/runtime-session.ts +341 -1
  6. package/src/agent/session-ledger.ts +100 -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/events/observation-text-log.ts +4 -0
  22. package/src/events/stdout-event-printer.ts +5 -0
  23. package/src/events/types.ts +5 -1
  24. package/src/model/fake-model-client.ts +55 -16
  25. package/src/model/model-api.ts +12 -0
  26. package/src/model/model-client.ts +9 -1
  27. package/src/model/moonshot-input-token-estimator.ts +5 -1
  28. package/src/model/openai-chat-mapping.ts +2 -24
  29. package/src/model/openai-chat-model-client.ts +18 -294
  30. package/src/model/openai-image-mapping.ts +20 -0
  31. package/src/model/openai-model-utils.ts +304 -0
  32. package/src/model/openai-responses-mapping.ts +532 -0
  33. package/src/model/openai-responses-model-client.ts +295 -0
  34. package/src/model/openai-responses-stream.ts +96 -0
  35. package/src/model/openai-responses-token-estimator.ts +155 -0
  36. package/src/model/reasoning-effort.ts +60 -0
  37. package/src/session/session-catalog.ts +2 -2
  38. package/src/session/session-history-reader.ts +6 -1
  39. package/src/session/session-schema.ts +268 -4
  40. package/src/session/session-store.ts +134 -26
  41. package/src/skills/skill-context.ts +2 -2
  42. package/src/tools/bounded-output-preview.ts +276 -0
  43. package/src/tools/recall.ts +67 -36
  44. package/src/tools/registry.ts +7 -2
  45. package/src/tools/task-output-snapshot.ts +6 -22
  46. package/src/tools/task-output.ts +23 -27
  47. package/src/tui/app.tsx +153 -11
  48. package/src/tui/components/footer.tsx +6 -1
  49. package/src/tui/components/prompt-input.tsx +9 -1
  50. package/src/tui/event-store.ts +15 -0
  51. package/src/tui/slash-commands.ts +20 -0
  52. package/src/tui/tui-session-controller.ts +14 -0
@@ -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,
@@ -137,6 +139,18 @@ export type AcceptedTurn = {
137
139
  readonly completion: Promise<RunAgentResult>;
138
140
  };
139
141
 
142
+ export type PromptSchedulerSnapshot = {
143
+ readonly state: "idle" | "running";
144
+ readonly activeTurnId?: TurnIdentity["turnId"];
145
+ readonly pendingCount: number;
146
+ };
147
+
148
+ export type QueueFollowUpResult = {
149
+ readonly kind: "queued";
150
+ readonly pendingCount: number;
151
+ readonly activeTurnId?: TurnIdentity["turnId"];
152
+ };
153
+
140
154
  export type SessionDisposeReason =
141
155
  | { type: "oneshot_complete" }
142
156
  | { type: "tui_exit" }
@@ -151,6 +165,9 @@ export type RuntimeSession = {
151
165
  skills(): RuntimeSkillsSnapshot;
152
166
  mcp(): McpInventorySnapshot;
153
167
  supportsImageInput(): boolean;
168
+ reasoningEffort(): ReasoningEffortSnapshot | undefined;
169
+ setReasoningEffort(effort: string): ReasoningEffortSnapshot;
170
+ resetReasoningEffort(): ReasoningEffortSnapshot;
154
171
  importImage(
155
172
  sourcePath: string,
156
173
  signal: AbortSignal,
@@ -162,6 +179,9 @@ export type RuntimeSession = {
162
179
  ): Promise<void>;
163
180
  admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn>;
164
181
  executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult>;
182
+ promptScheduler(): PromptSchedulerSnapshot;
183
+ subscribePromptScheduler(listener: () => void): () => void;
184
+ queueFollowUp(userMessage: UserMessage): QueueFollowUpResult;
165
185
  compactContext(): Promise<ContextCompactionResult>;
166
186
  retireContext(): Promise<ContextRetirementResult>;
167
187
  undoLatestFileMutationTurn(): Promise<TurnUndoResult>;
@@ -217,6 +237,15 @@ export type RuntimeSessionContext = {
217
237
  iteration: IterationIdentity;
218
238
  built: BuiltContextRequest;
219
239
  }): void;
240
+ maintainContextAfterIteration?(input: {
241
+ turn: TurnIdentity;
242
+ consumedThroughOrdinal: number;
243
+ ledger: AgentTurnLedger;
244
+ }): Promise<void>;
245
+ applyQueuedSteering?(input: {
246
+ turn: TurnIdentity;
247
+ ledger: AgentTurnLedger;
248
+ }): Promise<number>;
220
249
  };
221
250
 
222
251
  export type ContextSurfaceRefreshSummary = {
@@ -344,10 +373,18 @@ type RuntimeSessionState =
344
373
  | "disposed";
345
374
 
346
375
  type ActiveTurn = {
376
+ turn: TurnIdentity;
347
377
  controller: AbortController;
348
378
  completion: Promise<RunAgentResult>;
349
379
  };
350
380
 
381
+ type QueuedPrompt = {
382
+ readonly userMessage: UserMessage;
383
+ };
384
+
385
+ const MAX_QUEUED_PROMPTS = 8;
386
+ const MAX_QUEUED_PROMPT_TEXT_BYTES = 64 * 1024;
387
+
351
388
  type ActiveAdmission = {
352
389
  controller: AbortController;
353
390
  settled: Promise<void>;
@@ -405,6 +442,13 @@ class DefaultRuntimeSession implements RuntimeSession {
405
442
  private ledger?: SessionLedger;
406
443
  private activeAdmission?: ActiveAdmission;
407
444
  private activeTurn?: ActiveTurn;
445
+ private executionChainRunning = false;
446
+ private readonly queuedPrompts: QueuedPrompt[] = [];
447
+ private promptSchedulerSnapshot: PromptSchedulerSnapshot = Object.freeze({
448
+ state: "idle",
449
+ pendingCount: 0,
450
+ });
451
+ private readonly promptSchedulerListeners = new Set<() => void>();
408
452
  private activeContextRevision?: Promise<
409
453
  ContextCompactionResult | ContextRetirementResult
410
454
  >;
@@ -482,6 +526,9 @@ class DefaultRuntimeSession implements RuntimeSession {
482
526
  onToolCompletionsCommitted: (completion) =>
483
527
  this.onToolCompletionsCommitted(completion),
484
528
  prepareModelDispatch: (dispatch) => this.prepareModelDispatch(dispatch),
529
+ maintainContextAfterIteration: (maintenance) =>
530
+ this.performActiveTurnContextMaintenance(maintenance),
531
+ applyQueuedSteering: (steering) => this.applyQueuedSteering(steering),
485
532
  };
486
533
  }
487
534
 
@@ -965,6 +1012,36 @@ class DefaultRuntimeSession implements RuntimeSession {
965
1012
  return this.input.modelClient.inputModalities?.includes("image") === true;
966
1013
  }
967
1014
 
1015
+ reasoningEffort(): ReasoningEffortSnapshot | undefined {
1016
+ return this.input.modelClient.reasoningEffort?.snapshot();
1017
+ }
1018
+
1019
+ setReasoningEffort(effort: string): ReasoningEffortSnapshot {
1020
+ if (this.state !== "ready" || this.activeTurn !== undefined) {
1021
+ throw new Error(
1022
+ `Cannot change reasoning effort while RuntimeSession is ${this.state}.`,
1023
+ );
1024
+ }
1025
+ const reasoningEffort = this.input.modelClient.reasoningEffort;
1026
+ if (reasoningEffort === undefined) {
1027
+ throw new Error("Current model profile does not configure reasoning effort.");
1028
+ }
1029
+ return reasoningEffort.set(effort);
1030
+ }
1031
+
1032
+ resetReasoningEffort(): ReasoningEffortSnapshot {
1033
+ if (this.state !== "ready" || this.activeTurn !== undefined) {
1034
+ throw new Error(
1035
+ `Cannot change reasoning effort while RuntimeSession is ${this.state}.`,
1036
+ );
1037
+ }
1038
+ const reasoningEffort = this.input.modelClient.reasoningEffort;
1039
+ if (reasoningEffort === undefined) {
1040
+ throw new Error("Current model profile does not configure reasoning effort.");
1041
+ }
1042
+ return reasoningEffort.reset();
1043
+ }
1044
+
968
1045
  bashGuard(): BashGuardSnapshot {
969
1046
  return this.bashGuardSnapshot;
970
1047
  }
@@ -1467,11 +1544,71 @@ class DefaultRuntimeSession implements RuntimeSession {
1467
1544
  }
1468
1545
  }
1469
1546
 
1547
+ promptScheduler(): PromptSchedulerSnapshot {
1548
+ return this.promptSchedulerSnapshot;
1549
+ }
1550
+
1551
+ subscribePromptScheduler(listener: () => void): () => void {
1552
+ this.promptSchedulerListeners.add(listener);
1553
+ return () => this.promptSchedulerListeners.delete(listener);
1554
+ }
1555
+
1556
+ queueFollowUp(userMessage: UserMessage): QueueFollowUpResult {
1557
+ if (!this.executionChainRunning) {
1558
+ throw new Error("Cannot queue a follow-up while no execution chain is running.");
1559
+ }
1560
+ validateUserMessage(userMessage);
1561
+ if (userMessage.attachments !== undefined) {
1562
+ throw new Error("Active-turn follow-ups do not support image attachments.");
1563
+ }
1564
+ if (this.queuedPrompts.length >= MAX_QUEUED_PROMPTS) {
1565
+ throw new Error(`At most ${MAX_QUEUED_PROMPTS} follow-ups may be queued.`);
1566
+ }
1567
+ const queuedBytes = this.queuedPrompts.reduce(
1568
+ (total, entry) => total + Buffer.byteLength(entry.userMessage.content, "utf8"),
1569
+ 0,
1570
+ );
1571
+ const nextBytes = Buffer.byteLength(userMessage.content, "utf8");
1572
+ if (queuedBytes + nextBytes > MAX_QUEUED_PROMPT_TEXT_BYTES) {
1573
+ throw new Error("Queued follow-ups exceed the 64 KiB text limit.");
1574
+ }
1575
+ this.queuedPrompts.push({
1576
+ userMessage: Object.freeze({ ...userMessage }),
1577
+ });
1578
+ this.notifyPromptScheduler();
1579
+ return Object.freeze({
1580
+ kind: "queued",
1581
+ pendingCount: this.queuedPrompts.length,
1582
+ ...(this.activeTurn === undefined
1583
+ ? {}
1584
+ : { activeTurnId: this.activeTurn.turn.turnId }),
1585
+ });
1586
+ }
1587
+
1470
1588
  async executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult> {
1471
1589
  return (await this.admitTurn(input)).completion;
1472
1590
  }
1473
1591
 
1474
1592
  async admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
1593
+ if (this.executionChainRunning) {
1594
+ throw new Error(
1595
+ `Cannot execute a turn while RuntimeSession is ${this.state}; a prompt chain is already executing.`,
1596
+ );
1597
+ }
1598
+ this.executionChainRunning = true;
1599
+ this.notifyPromptScheduler();
1600
+ try {
1601
+ const accepted = await this.admitSingleTurn(input);
1602
+ const completion = this.continueExecutionChain(accepted.completion, input.signal);
1603
+ return Object.freeze({ ...accepted, completion });
1604
+ } catch (error) {
1605
+ this.executionChainRunning = false;
1606
+ this.notifyPromptScheduler();
1607
+ throw error;
1608
+ }
1609
+ }
1610
+
1611
+ private async admitSingleTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
1475
1612
  if (this.state !== "ready") {
1476
1613
  throw new Error(`Cannot execute a turn while RuntimeSession is ${this.state}.`);
1477
1614
  }
@@ -1544,7 +1681,8 @@ class DefaultRuntimeSession implements RuntimeSession {
1544
1681
  usage: admissionSnapshot,
1545
1682
  },
1546
1683
  });
1547
- this.activeTurn = { controller, completion };
1684
+ this.activeTurn = { turn, controller, completion };
1685
+ this.notifyPromptScheduler();
1548
1686
  return Object.freeze({
1549
1687
  turnId: turn.turnId,
1550
1688
  userMessage: input.userMessage,
@@ -1566,6 +1704,79 @@ class DefaultRuntimeSession implements RuntimeSession {
1566
1704
  }
1567
1705
  }
1568
1706
 
1707
+ private async continueExecutionChain(
1708
+ initialCompletion: Promise<RunAgentResult>,
1709
+ signal: AbortSignal,
1710
+ ): Promise<RunAgentResult> {
1711
+ let completion = initialCompletion;
1712
+ let finalResult: RunAgentResult;
1713
+ try {
1714
+ for (;;) {
1715
+ finalResult = await completion;
1716
+ if (finalResult.status !== "completed" || this.queuedPrompts.length === 0) {
1717
+ return finalResult;
1718
+ }
1719
+ const next = this.queuedPrompts[0];
1720
+ if (next === undefined) {
1721
+ return finalResult;
1722
+ }
1723
+ const accepted = await this.admitSingleTurn({
1724
+ userMessage: next.userMessage,
1725
+ signal,
1726
+ });
1727
+ this.queuedPrompts.shift();
1728
+ this.notifyPromptScheduler();
1729
+ completion = accepted.completion;
1730
+ }
1731
+ } finally {
1732
+ this.queuedPrompts.splice(0);
1733
+ this.executionChainRunning = false;
1734
+ this.notifyPromptScheduler();
1735
+ }
1736
+ }
1737
+
1738
+ private notifyPromptScheduler(): void {
1739
+ this.promptSchedulerSnapshot = Object.freeze({
1740
+ state: this.executionChainRunning ? "running" : "idle",
1741
+ ...(this.activeTurn === undefined
1742
+ ? {}
1743
+ : { activeTurnId: this.activeTurn.turn.turnId }),
1744
+ pendingCount: this.queuedPrompts.length,
1745
+ });
1746
+ for (const listener of this.promptSchedulerListeners) listener();
1747
+ }
1748
+
1749
+ private async applyQueuedSteering(input: {
1750
+ turn: TurnIdentity;
1751
+ ledger: AgentTurnLedger;
1752
+ }): Promise<number> {
1753
+ if (this.activeTurn?.turn.turnId !== input.turn.turnId) {
1754
+ throw new Error("Cannot apply steering outside the active turn.");
1755
+ }
1756
+ if (this.queuedPrompts.length === 0) return 0;
1757
+ const drained = this.queuedPrompts.splice(0);
1758
+ const records = input.ledger.appendSteeringUserMessages(
1759
+ drained.map((entry) => entry.userMessage),
1760
+ );
1761
+ this.notifyPromptScheduler();
1762
+ for (let index = 0; index < records.length; index += 1) {
1763
+ const record = records[index];
1764
+ const queued = drained[index];
1765
+ if (record === undefined || queued === undefined) {
1766
+ throw new Error("Steering ledger result did not match the drained queue.");
1767
+ }
1768
+ await this.append({
1769
+ type: "turn.steering.applied",
1770
+ ...input.turn,
1771
+ data: {
1772
+ userPrompt: projectUserMessage(queued.userMessage),
1773
+ ordinal: record.ordinal,
1774
+ },
1775
+ });
1776
+ }
1777
+ return records.length;
1778
+ }
1779
+
1569
1780
  private settleAdmission(admission: ActiveAdmission): void {
1570
1781
  if (this.activeAdmission !== admission) {
1571
1782
  throw new Error("Runtime admission ownership was lost.");
@@ -1796,6 +2007,8 @@ class DefaultRuntimeSession implements RuntimeSession {
1796
2007
  canSwitchSession(): boolean {
1797
2008
  return (
1798
2009
  this.state === "ready" &&
2010
+ !this.executionChainRunning &&
2011
+ this.queuedPrompts.length === 0 &&
1799
2012
  this.activeTurn === undefined &&
1800
2013
  (this.tooling?.taskManager
1801
2014
  .listBackgroundTasks()
@@ -1983,6 +2196,7 @@ class DefaultRuntimeSession implements RuntimeSession {
1983
2196
  } finally {
1984
2197
  removeExternalAbortListener();
1985
2198
  this.activeTurn = undefined;
2199
+ this.notifyPromptScheduler();
1986
2200
  this.pendingAutomaticContextMaintenance = false;
1987
2201
  if (this.state === "executing") {
1988
2202
  this.state = "ready";
@@ -2016,6 +2230,129 @@ class DefaultRuntimeSession implements RuntimeSession {
2016
2230
  }
2017
2231
  }
2018
2232
 
2233
+ private async performActiveTurnContextMaintenance(input: {
2234
+ turn: TurnIdentity;
2235
+ consumedThroughOrdinal: number;
2236
+ ledger: AgentTurnLedger;
2237
+ }): Promise<void> {
2238
+ const automation = this.requireContextAutomation();
2239
+ if (!automation.automaticSwapOnly) return;
2240
+ if (this.state !== "executing") {
2241
+ throw new Error(
2242
+ `Cannot maintain active-turn context while RuntimeSession is ${this.state}.`,
2243
+ );
2244
+ }
2245
+ const manager = this.requireContextManager();
2246
+ const usage = manager.measureCurrent(input.turn.turnId, input.ledger);
2247
+ if (usage.pressure === "normal") return;
2248
+
2249
+ const qualificationId = requireAutomationQualificationId(automation);
2250
+ const compactionTrigger = {
2251
+ kind: "runtime_pressure",
2252
+ activeTurn: {
2253
+ turnId: input.turn.turnId,
2254
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
2255
+ },
2256
+ } as const;
2257
+ this.pendingAutomaticContextMaintenance = false;
2258
+ this.state = "maintaining_context";
2259
+ try {
2260
+ await this.append({
2261
+ type: "context.revision.started",
2262
+ sessionId: this.sessionId,
2263
+ data: {
2264
+ strategy: "swap",
2265
+ reason: "runtime_pressure",
2266
+ policyVersion: "swap-only-v1",
2267
+ rendererFormat: "swap-observation-v1",
2268
+ qualificationId,
2269
+ },
2270
+ });
2271
+ let swap: ContextCompactionResult;
2272
+ try {
2273
+ swap = await manager.compact(compactionTrigger, input.ledger);
2274
+ await this.append({
2275
+ type: "context.revision.finished",
2276
+ sessionId: this.sessionId,
2277
+ data: contextRevisionFinishedData(swap, "runtime_pressure", qualificationId),
2278
+ });
2279
+ } catch (error) {
2280
+ const failure = automaticContextFailure(error, "compaction");
2281
+ await this.append({
2282
+ type: "context.revision.failed",
2283
+ sessionId: this.sessionId,
2284
+ data: {
2285
+ strategy: "swap",
2286
+ reason: "runtime_pressure",
2287
+ stage: failure.stage,
2288
+ errorCode: boundedContextErrorCode(failure.code),
2289
+ error: `Automatic context compaction failed at ${failure.stage}.`,
2290
+ qualificationId,
2291
+ },
2292
+ }).catch(() => undefined);
2293
+ if (failure.fatal) throw error;
2294
+ return;
2295
+ }
2296
+
2297
+ if (
2298
+ !automation.automaticPrefixRetirement ||
2299
+ !automaticSwapNeedsRetirement(swap)
2300
+ ) {
2301
+ return;
2302
+ }
2303
+
2304
+ await this.append({
2305
+ type: "context.revision.started",
2306
+ sessionId: this.sessionId,
2307
+ data: {
2308
+ strategy: "retire_prefix",
2309
+ reason: "runtime_pressure",
2310
+ policyVersion: "recall-first-retirement-v1",
2311
+ baseRevisionNumber: this.store.loadContextSnapshot().revision.revisionNumber,
2312
+ qualificationId,
2313
+ },
2314
+ });
2315
+ try {
2316
+ const retirement = await manager.retirePrefix(
2317
+ {
2318
+ kind: "runtime_pressure",
2319
+ activeTurnId: input.turn.turnId,
2320
+ },
2321
+ input.ledger,
2322
+ );
2323
+ await this.append({
2324
+ type: "context.revision.finished",
2325
+ sessionId: this.sessionId,
2326
+ data: contextRetirementFinishedData(
2327
+ retirement,
2328
+ "runtime_pressure",
2329
+ qualificationId,
2330
+ ),
2331
+ });
2332
+ } catch (error) {
2333
+ const failure = automaticContextFailure(error, "retirement");
2334
+ await this.append({
2335
+ type: "context.revision.failed",
2336
+ sessionId: this.sessionId,
2337
+ data: {
2338
+ strategy: "retire_prefix",
2339
+ reason: "runtime_pressure",
2340
+ stage: failure.stage,
2341
+ errorCode: boundedContextErrorCode(failure.code),
2342
+ error: `Automatic context retirement failed at ${failure.stage}.`,
2343
+ committed: failure.committed,
2344
+ qualificationId,
2345
+ },
2346
+ }).catch(() => undefined);
2347
+ if (failure.fatal) throw error;
2348
+ }
2349
+ } finally {
2350
+ if (this.state === "maintaining_context") {
2351
+ this.state = "executing";
2352
+ }
2353
+ }
2354
+ }
2355
+
2019
2356
  private notifyCompletedTurn(turn: TurnIdentity): void {
2020
2357
  const hook = this.input.completedTurnHook;
2021
2358
  if (hook === undefined) {
@@ -2233,6 +2570,9 @@ class DefaultRuntimeSession implements RuntimeSession {
2233
2570
  }
2234
2571
 
2235
2572
  this.state = "disposing";
2573
+ this.queuedPrompts.splice(0);
2574
+ this.executionChainRunning = false;
2575
+ this.notifyPromptScheduler();
2236
2576
  const errors: unknown[] = this.faultCause === undefined ? [] : [this.faultCause];
2237
2577
  const activeAdmission = this.activeAdmission;
2238
2578
  if (activeAdmission !== undefined) {
@@ -100,6 +100,9 @@ export type PendingLedgerTurn = {
100
100
  };
101
101
 
102
102
  export type AgentTurnLedger = {
103
+ appendSteeringUserMessages(
104
+ messages: readonly UserMessage[],
105
+ ): readonly CanonicalMessageRecord[];
103
106
  appendAssistant(input: {
104
107
  iteration: IterationIdentity;
105
108
  message: AssistantMessage;
@@ -111,6 +114,7 @@ export type AgentTurnLedger = {
111
114
  completions: readonly ToolCompletionInput[],
112
115
  ): readonly CommittedToolCompletion[];
113
116
  buildModelRequest(tools: readonly ToolDefinition[]): BuiltContextRequest;
117
+ activateContextSnapshot(snapshot: StoredContextSnapshotV8): void;
114
118
  };
115
119
 
116
120
  export type CommittedToolCompletion = {
@@ -128,6 +132,13 @@ export type LedgerMutation =
128
132
  admissionBase?: AdmissionBaseToken;
129
133
  next: ProtocolContextView;
130
134
  }
135
+ | {
136
+ kind: "append_steering_users";
137
+ turn: TurnIdentity;
138
+ frames: readonly ProtocolFrame[];
139
+ messages: readonly CanonicalMessageRecord[];
140
+ next: ProtocolContextView;
141
+ }
131
142
  | {
132
143
  kind: "append_assistant";
133
144
  iteration: IterationIdentity;
@@ -188,9 +199,9 @@ export class InMemorySessionLedger implements SessionLedger {
188
199
  private readonly validator = new ContextProtocolValidator();
189
200
  private readonly contextBuilder: ContextBuilder;
190
201
  private readonly revisionCompiler: ContextRevisionCompiler;
191
- private readonly revision: StoredContextRevisionV8;
192
- private readonly surface: StoredContextSurfaceV8;
193
- private readonly activeOverrides: readonly StoredContextOverrideV8[];
202
+ private revision: StoredContextRevisionV8;
203
+ private surface: StoredContextSurfaceV8;
204
+ private activeOverrides: readonly StoredContextOverrideV8[];
194
205
  private readonly clock: () => string;
195
206
 
196
207
  constructor(private readonly input: CreateInMemorySessionLedgerInput) {
@@ -354,6 +365,20 @@ export class InMemorySessionLedger implements SessionLedger {
354
365
  return this.view.messages.length;
355
366
  }
356
367
 
368
+ activateContextSnapshot(snapshot: StoredContextSnapshotV8): void {
369
+ this.requireHealthy("activate a context snapshot");
370
+ if (
371
+ snapshot.meta.sessionId !== this.input.sessionId ||
372
+ stableJsonStringify(snapshot.canonical) !== stableJsonStringify(this.view)
373
+ ) {
374
+ throw new Error("Activated context snapshot does not match canonical history.");
375
+ }
376
+ this.revisionCompiler.compileActive(snapshot);
377
+ this.revision = snapshot.revision;
378
+ this.surface = snapshot.surface;
379
+ this.activeOverrides = snapshot.activeOverrides;
380
+ }
381
+
357
382
  snapshot(
358
383
  options: {
359
384
  fullIntegrity?: boolean;
@@ -381,6 +406,74 @@ export class InMemorySessionLedger implements SessionLedger {
381
406
  this.pending = undefined;
382
407
  }
383
408
 
409
+ appendSteeringUserMessages(
410
+ pending: InMemoryPendingLedgerTurn,
411
+ userMessages: readonly UserMessage[],
412
+ ): readonly CanonicalMessageRecord[] {
413
+ this.requirePending(pending, "append steering user messages");
414
+ this.assertNoOpenFrame();
415
+ if (userMessages.length === 0) {
416
+ return Object.freeze([]);
417
+ }
418
+ for (const userMessage of userMessages) {
419
+ validateUserMessage(userMessage);
420
+ }
421
+
422
+ const frames: ProtocolFrame[] = [];
423
+ const messages: CanonicalMessageRecord[] = [];
424
+ for (const userMessage of userMessages) {
425
+ const createdAt = this.clock();
426
+ const ordinal = this.view.messages.length + messages.length + 1;
427
+ const frameId = this.input.idFactory.createProtocolFrameId();
428
+ messages.push(
429
+ immutableRecord<CanonicalMessageRecord>({
430
+ messageId: this.input.idFactory.createMessageId(),
431
+ sessionId: this.input.sessionId,
432
+ frameId,
433
+ ordinal,
434
+ contentSha256: userMessageHash(userMessage),
435
+ createdAt,
436
+ role: "user",
437
+ turnId: pending.turn.turnId,
438
+ content: userMessage.content,
439
+ ...(userMessage.attachments === undefined
440
+ ? {}
441
+ : {
442
+ attachments: Object.freeze(
443
+ immutableCanonicalClone(userMessage.attachments),
444
+ ),
445
+ }),
446
+ origin: "user",
447
+ }),
448
+ );
449
+ frames.push(
450
+ immutableRecord<ProtocolFrame>({
451
+ frameId,
452
+ sessionId: this.input.sessionId,
453
+ turnId: pending.turn.turnId,
454
+ kind: "user",
455
+ state: "closed",
456
+ firstOrdinal: ordinal,
457
+ lastOrdinal: ordinal,
458
+ createdAt,
459
+ closedAt: createdAt,
460
+ }),
461
+ );
462
+ }
463
+ const immutableFrames = Object.freeze(frames);
464
+ const immutableMessages = Object.freeze(messages);
465
+ const next = appendView(this.view, immutableFrames, immutableMessages, []);
466
+ this.validator.validate(next, { fullIntegrity: true });
467
+ this.commit({
468
+ kind: "append_steering_users",
469
+ turn: pending.turn,
470
+ frames: immutableFrames,
471
+ messages: immutableMessages,
472
+ next,
473
+ });
474
+ return immutableMessages;
475
+ }
476
+
384
477
  appendAssistant(
385
478
  pending: InMemoryPendingLedgerTurn,
386
479
  input: {
@@ -761,11 +854,15 @@ class InMemoryPendingLedgerTurn implements PendingLedgerTurn {
761
854
  readonly turn: TurnIdentity,
762
855
  ) {
763
856
  this.agent = {
857
+ appendSteeringUserMessages: (messages) =>
858
+ this.ledger.appendSteeringUserMessages(this, messages),
764
859
  appendAssistant: (input) => this.ledger.appendAssistant(this, input),
765
860
  assertCanExecuteTool: (call) => this.ledger.assertCanExecuteTool(this, call),
766
861
  commitToolCompletions: (completions) =>
767
862
  this.ledger.commitToolCompletions(this, completions),
768
863
  buildModelRequest: (tools) => this.ledger.buildTurnModelRequest(this, tools),
864
+ activateContextSnapshot: (snapshot) =>
865
+ this.ledger.activateContextSnapshot(snapshot),
769
866
  };
770
867
  }
771
868
 
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),