tinker-agent 2.3.0 → 2.5.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable user-facing changes to Tinker are documented here. The project follo
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [2.5.0] - 2026-09-03
9
+
10
+ ### Added
11
+
12
+ - Proactively notify the model when input context reaches high or critical
13
+ pressure, prompting it to review and swap evictable historical tool
14
+ observations while keeping swapped content recoverable through Recall. The
15
+ runtime gives the model one iteration to act before automatic compaction
16
+ resumes, while critical pressure still triggers immediate maintenance.
17
+
18
+ ## [2.4.0] - 2026-09-02
19
+
20
+ ### Added
21
+
22
+ - Add model-directed context management. Three new built-in tools open context
23
+ maintenance to the model itself: `ContextStatus` reports live input-token
24
+ usage and pressure, `ContextSwapCandidates` lists swappable historical tool
25
+ observations with short labels and byte savings, and `ContextSwap` schedules
26
+ selected observations for eviction. Scheduled swaps are validated immediately
27
+ and committed as one context revision when the iteration's tool frames close,
28
+ leaving Recall-recoverable placeholders behind. A one-shot lease pauses
29
+ automatic compaction for exactly one iteration after a candidate listing so
30
+ the model keeps the choice under real pressure.
31
+
32
+ ### Changed
33
+
34
+ - Lower the swap eligibility floor from 8 KiB to 2 KiB per observation. Both
35
+ automatic compaction and model-directed swaps can now evict medium-sized
36
+ tool observations, individually or in batches of up to 16 per swap.
37
+
8
38
  ## [2.3.0] - 2026-08-31
9
39
 
10
40
  ### Changed
@@ -277,7 +307,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
277
307
  - First formal npm release under the `tinker-agent` package name with the `tinker`
278
308
  executable.
279
309
 
280
- [Unreleased]: https://github.com/ishowshao/tinker/compare/v2.3.0...HEAD
310
+ [Unreleased]: https://github.com/ishowshao/tinker/compare/v2.5.0...HEAD
311
+ [2.5.0]: https://github.com/ishowshao/tinker/releases/tag/v2.5.0
312
+ [2.4.0]: https://github.com/ishowshao/tinker/releases/tag/v2.4.0
281
313
  [2.3.0]: https://github.com/ishowshao/tinker/releases/tag/v2.3.0
282
314
  [2.2.0]: https://github.com/ishowshao/tinker/releases/tag/v2.2.0
283
315
  [2.1.0]: https://github.com/ishowshao/tinker/releases/tag/v2.1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinker-agent",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
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",
@@ -0,0 +1,37 @@
1
+ import type { ContextUsageSnapshot } from "./context-meter";
2
+
3
+ export const CONTEXT_PRESSURE_NOTICE_PREFIX = "[tinker context notice]";
4
+
5
+ export function isContextPressureNotice(message: {
6
+ readonly role: string;
7
+ readonly content?: unknown;
8
+ }): boolean {
9
+ return (
10
+ message.role === "user" &&
11
+ typeof message.content === "string" &&
12
+ message.content.startsWith(CONTEXT_PRESSURE_NOTICE_PREFIX)
13
+ );
14
+ }
15
+
16
+ export function contextPressureNoticeText(input: {
17
+ usage: ContextUsageSnapshot;
18
+ toolPressure: "high" | "critical";
19
+ automaticSwapEnabled: boolean;
20
+ }): string {
21
+ const base =
22
+ `${CONTEXT_PRESSURE_NOTICE_PREFIX} Input pressure is now "${input.toolPressure}" ` +
23
+ `(${input.usage.usedInputTokens} of ${input.usage.inputBudgetTokens} input tokens; ` +
24
+ `trigger at ${input.usage.triggerTokens}). ` +
25
+ "Call ContextSwapCandidates to review evictable historical tool observations, " +
26
+ "then ContextSwap to replace them with Recall-backed placeholders; " +
27
+ "swapped content stays recoverable through RecallGet.";
28
+ if (!input.automaticSwapEnabled) {
29
+ return (
30
+ `${base} Automatic compaction is disabled in this session, so pressure will ` +
31
+ "keep growing unless you swap observations or the turn ends."
32
+ );
33
+ }
34
+ return input.toolPressure === "critical"
35
+ ? `${base} Automatic compaction is running immediately because pressure exceeded the input budget.`
36
+ : `${base} Automatic compaction will resume next iteration if you do not act.`;
37
+ }
package/src/agent/loop.ts CHANGED
@@ -56,7 +56,7 @@ export type RunAgentInput = {
56
56
  preflight: ReturnType<ContextMeter["measure"]>;
57
57
  }):
58
58
  | {
59
- trigger: Exclude<SwapPlanningTrigger, "manual">;
59
+ trigger: Exclude<SwapPlanningTrigger, "manual" | "model_directed">;
60
60
  forcedTargetTokens?: number;
61
61
  }
62
62
  | undefined;
@@ -11,6 +11,7 @@ import type {
11
11
  } from "../events/types";
12
12
  import {
13
13
  runtimeIdFactory,
14
+ type MessageId,
14
15
  type RuntimeIdFactory,
15
16
  type SessionId,
16
17
  type TurnId,
@@ -27,6 +28,7 @@ import {
27
28
  type MaterializedModelRequest,
28
29
  type ModelClient,
29
30
  } from "../model/model-client";
31
+ import type { ContextPressure } from "../model/model-request-preflight";
30
32
  import { ImageAssetStore, type ImportedImageAsset } from "../image/image-asset-store";
31
33
  import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
32
34
  import {
@@ -72,7 +74,14 @@ import {
72
74
  renderedMessageHash,
73
75
  } from "../context/compiled-context-hash";
74
76
  import { createDefaultTooling, type DefaultTooling } from "../tools/registry";
75
- import type { ToolExecutor } from "../tools/types";
77
+ import {
78
+ ToolExecutionFatalError,
79
+ type ContextMaintenanceHandle,
80
+ type ContextStatusRawResult,
81
+ type ContextSwapCandidatesRawResult,
82
+ type ContextSwapRawResult,
83
+ type ToolExecutor,
84
+ } from "../tools/types";
76
85
  import type { TurnUndoResult } from "../tools/turn-undo-manager";
77
86
  import type { Refiner } from "../tools/web-fetch/refiner";
78
87
  import type { ProjectInstructionManifest } from "../instructions/project-instructions";
@@ -105,10 +114,12 @@ import type { PublicToolingConfig } from "../cli/public-config-contract";
105
114
  import type {
106
115
  IterationIdentity,
107
116
  RunAgentResult,
117
+ ToolCall,
108
118
  ToolCallIdentity,
109
119
  TurnIdentity,
110
120
  } from "./types";
111
- import { ContextMeter } from "./context-meter";
121
+ import { ContextMeter, type ContextUsageSnapshot } from "./context-meter";
122
+ import { contextPressureNoticeText } from "./context-pressure-notice";
112
123
  import { CURRENT_RECALL_RETIREMENT_CONTRACT_VERSION } from "../context/recall-retirement-contract";
113
124
  import {
114
125
  selectContextAutomation,
@@ -221,6 +232,7 @@ export type RuntimeSkillsSnapshot = {
221
232
 
222
233
  export type RuntimeSessionContext = {
223
234
  readonly sessionId: SessionId;
235
+ readonly contextMaintenance: ContextMaintenanceHandle;
224
236
  createIteration(turn: TurnIdentity, iterationNumber: number): IterationIdentity;
225
237
  createToolCall(
226
238
  iteration: IterationIdentity,
@@ -379,6 +391,8 @@ type RuntimeSessionState =
379
391
 
380
392
  type ActiveTurn = {
381
393
  turn: TurnIdentity;
394
+ ledger: AgentTurnLedger;
395
+ consumedThroughOrdinal?: number;
382
396
  controller: AbortController;
383
397
  completion: Promise<RunAgentResult>;
384
398
  };
@@ -467,6 +481,9 @@ class DefaultRuntimeSession implements RuntimeSession {
467
481
  private contextManager?: ContextManager;
468
482
  private contextAutomationDecision?: ContextAutomationDecision;
469
483
  private pendingAutomaticContextMaintenance = false;
484
+ private pendingModelDirectedSwap?: Set<MessageId>;
485
+ private modelDirectedSwapLease = false;
486
+ private pressureNoticeSentThisTurn = false;
470
487
  private readonly skillCatalog: SkillCatalogSnapshot;
471
488
  private skillCoordinator = new SkillActivationCoordinator();
472
489
  private bashGuardMode: "guard" | "yolo";
@@ -517,6 +534,11 @@ class DefaultRuntimeSession implements RuntimeSession {
517
534
  this.shadowPlanner = new SwapPlanner(input.modelClient);
518
535
  this.context = {
519
536
  sessionId: this.sessionId,
537
+ contextMaintenance: {
538
+ status: (call) => this.contextStatus(call),
539
+ candidates: (call, page) => this.contextSwapCandidates(call, page),
540
+ swap: (call, selection) => this.contextSwap(call, selection),
541
+ },
520
542
  createIteration: (turn, iterationNumber) =>
521
543
  this.createIteration(turn, iterationNumber),
522
544
  createToolCall: (iteration, toolCallNumber) =>
@@ -1248,6 +1270,133 @@ class DefaultRuntimeSession implements RuntimeSession {
1248
1270
  return this.contextAutomationDecision;
1249
1271
  }
1250
1272
 
1273
+ private async contextStatus(call: ToolCall): Promise<ContextStatusRawResult> {
1274
+ const active = this.requireActiveContextTool(call, "ContextStatus");
1275
+ try {
1276
+ const usage = this.requireContextManager().measureActive(
1277
+ active.turn.turnId,
1278
+ active.ledger,
1279
+ );
1280
+ return Object.freeze({
1281
+ ok: true,
1282
+ operation: "status",
1283
+ usedInputTokens: usage.usedInputTokens,
1284
+ inputBudgetTokens: usage.inputBudgetTokens,
1285
+ pressure: toolContextPressure(usage.pressure),
1286
+ triggerTokens: usage.triggerTokens,
1287
+ source: usage.source,
1288
+ });
1289
+ } catch (error) {
1290
+ return {
1291
+ ok: false,
1292
+ operation: "status",
1293
+ error: this.contextToolFailure("status", error),
1294
+ };
1295
+ }
1296
+ }
1297
+
1298
+ private async contextSwapCandidates(
1299
+ call: ToolCall,
1300
+ page: { readonly limit: number; readonly offset: number },
1301
+ ): Promise<ContextSwapCandidatesRawResult> {
1302
+ const active = this.requireActiveContextTool(call, "ContextSwapCandidates");
1303
+ try {
1304
+ const result = this.requireContextManager().listActiveSwapCandidates({
1305
+ turnId: active.turn.turnId,
1306
+ consumedThroughOrdinal: active.consumedThroughOrdinal,
1307
+ activeLedger: active.ledger,
1308
+ limit: page.limit,
1309
+ offset: page.offset,
1310
+ });
1311
+ if (result.total > 0 && result.usage.pressure !== "normal") {
1312
+ this.modelDirectedSwapLease = true;
1313
+ }
1314
+ return Object.freeze({
1315
+ ok: true,
1316
+ operation: "candidates",
1317
+ total: result.total,
1318
+ candidates: result.candidates,
1319
+ });
1320
+ } catch (error) {
1321
+ return {
1322
+ ok: false,
1323
+ operation: "candidates",
1324
+ error: this.contextToolFailure("candidate listing", error),
1325
+ };
1326
+ }
1327
+ }
1328
+
1329
+ private async contextSwap(
1330
+ call: ToolCall,
1331
+ selection: { readonly candidateIds: readonly MessageId[] },
1332
+ ): Promise<ContextSwapRawResult> {
1333
+ const active = this.requireActiveContextTool(call, "ContextSwap");
1334
+ try {
1335
+ const result = this.requireContextManager().validateActiveSwapSelection({
1336
+ turnId: active.turn.turnId,
1337
+ consumedThroughOrdinal: active.consumedThroughOrdinal,
1338
+ activeLedger: active.ledger,
1339
+ messageIds: selection.candidateIds,
1340
+ });
1341
+ if (result.scheduled.length === 0) {
1342
+ return Object.freeze({
1343
+ ok: false,
1344
+ operation: "swap",
1345
+ scheduled: Object.freeze([]),
1346
+ rejected: result.rejected,
1347
+ });
1348
+ }
1349
+ const pending = (this.pendingModelDirectedSwap ??= new Set<MessageId>());
1350
+ for (const candidate of result.scheduled) pending.add(candidate.candidateId);
1351
+ this.modelDirectedSwapLease = false;
1352
+ return Object.freeze({
1353
+ ok: true,
1354
+ operation: "swap",
1355
+ scheduled: result.scheduled,
1356
+ rejected: result.rejected,
1357
+ note: "Swap executes when this iteration's tool frames close.",
1358
+ });
1359
+ } catch (error) {
1360
+ return {
1361
+ ok: false,
1362
+ operation: "swap",
1363
+ scheduled: [],
1364
+ rejected: [],
1365
+ error: this.contextToolFailure("swap scheduling", error),
1366
+ };
1367
+ }
1368
+ }
1369
+
1370
+ private requireActiveContextTool(
1371
+ call: ToolCall,
1372
+ expectedName: "ContextStatus" | "ContextSwapCandidates" | "ContextSwap",
1373
+ ): ActiveTurn & { consumedThroughOrdinal: number } {
1374
+ this.requireToolCall(call);
1375
+ const active = this.activeTurn;
1376
+ if (
1377
+ call.name !== expectedName ||
1378
+ active === undefined ||
1379
+ active.turn.turnId !== call.turnId ||
1380
+ active.consumedThroughOrdinal === undefined ||
1381
+ this.state !== "executing"
1382
+ ) {
1383
+ throw new ToolExecutionFatalError(
1384
+ `${expectedName} was called outside an active model iteration.`,
1385
+ );
1386
+ }
1387
+ return active as ActiveTurn & { consumedThroughOrdinal: number };
1388
+ }
1389
+
1390
+ private contextToolFailure(operation: string, error: unknown): string {
1391
+ if (error instanceof ContextManagerError && !error.fatal) {
1392
+ return `Context ${operation} failed (${boundedContextErrorCode(error.code)}).`;
1393
+ }
1394
+ throw new ToolExecutionFatalError(
1395
+ `Context ${operation} required canonical session state that could not be read safely.`,
1396
+ { cause: error },
1397
+ );
1398
+ }
1399
+
1251
1400
  private appendSkillsCatalogLoaded(): Promise<void> {
1252
1401
  const activeNames = this.skillCoordinator
1253
1402
  .activeEntries()
@@ -1307,6 +1456,15 @@ class DefaultRuntimeSession implements RuntimeSession {
1307
1456
  iteration: IterationIdentity;
1308
1457
  built: BuiltContextRequest;
1309
1458
  }): void {
1459
+ const active = this.activeTurn;
1460
+ if (
1461
+ active === undefined ||
1462
+ active.turn.turnId !== input.iteration.turnId ||
1463
+ active.turn.sessionId !== input.iteration.sessionId
1464
+ ) {
1465
+ throw new Error("Model dispatch does not belong to the active runtime turn.");
1466
+ }
1467
+ active.consumedThroughOrdinal = input.built.canonical.messages.length;
1310
1468
  const pending = this.store.loadSkillActivations(["pending"]);
1311
1469
  if (pending.length === 0) {
1312
1470
  return;
@@ -1675,7 +1833,12 @@ class DefaultRuntimeSession implements RuntimeSession {
1675
1833
  usage: admissionSnapshot,
1676
1834
  },
1677
1835
  });
1678
- this.activeTurn = { turn, controller, completion };
1836
+ this.activeTurn = {
1837
+ turn,
1838
+ ledger: pendingLedgerTurn.agent,
1839
+ controller,
1840
+ completion,
1841
+ };
1679
1842
  this.notifyPromptScheduler();
1680
1843
  return Object.freeze({
1681
1844
  turnId: turn.turnId,
@@ -2192,6 +2355,9 @@ class DefaultRuntimeSession implements RuntimeSession {
2192
2355
  this.activeTurn = undefined;
2193
2356
  this.notifyPromptScheduler();
2194
2357
  this.pendingAutomaticContextMaintenance = false;
2358
+ this.pendingModelDirectedSwap = undefined;
2359
+ this.modelDirectedSwapLease = false;
2360
+ this.pressureNoticeSentThisTurn = false;
2195
2361
  if (this.state === "executing") {
2196
2362
  this.state = "ready";
2197
2363
  }
@@ -2229,28 +2395,81 @@ class DefaultRuntimeSession implements RuntimeSession {
2229
2395
  consumedThroughOrdinal: number;
2230
2396
  ledger: AgentTurnLedger;
2231
2397
  }): Promise<void> {
2232
- const automation = this.requireContextAutomation();
2233
- if (!automation.automaticSwapOnly) return;
2234
2398
  if (this.state !== "executing") {
2235
2399
  throw new Error(
2236
2400
  `Cannot maintain active-turn context while RuntimeSession is ${this.state}.`,
2237
2401
  );
2238
2402
  }
2239
- const manager = this.requireContextManager();
2240
- const usage = manager.measureCurrent(input.turn.turnId, input.ledger);
2241
- if (usage.pressure === "normal") return;
2403
+ const pendingModelDirectedSwap = this.pendingModelDirectedSwap;
2404
+ this.pendingModelDirectedSwap = undefined;
2242
2405
 
2243
- const qualificationId = requireAutomationQualificationId(automation);
2244
- const compactionTrigger = {
2245
- kind: "runtime_pressure",
2246
- activeTurn: {
2247
- turnId: input.turn.turnId,
2248
- consumedThroughOrdinal: input.consumedThroughOrdinal,
2249
- },
2250
- } as const;
2406
+ const automation = this.requireContextAutomation();
2407
+ const manager = this.requireContextManager();
2251
2408
  this.pendingAutomaticContextMaintenance = false;
2409
+
2410
+ let suppressAutomaticSwap = this.modelDirectedSwapLease;
2411
+ this.modelDirectedSwapLease = false;
2412
+
2413
+ if (pendingModelDirectedSwap !== undefined) {
2414
+ suppressAutomaticSwap = false;
2415
+ this.state = "maintaining_context";
2416
+ try {
2417
+ await this.performModelDirectedCompaction({
2418
+ turn: input.turn,
2419
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
2420
+ ledger: input.ledger,
2421
+ messageIds: Object.freeze([...pendingModelDirectedSwap]),
2422
+ });
2423
+ } finally {
2424
+ if (this.state === "maintaining_context") {
2425
+ this.state = "executing";
2426
+ }
2427
+ }
2428
+ }
2429
+
2430
+ let measured: ContextUsageSnapshot | undefined;
2431
+ if (
2432
+ pendingModelDirectedSwap === undefined &&
2433
+ (suppressAutomaticSwap ||
2434
+ !this.pressureNoticeSentThisTurn ||
2435
+ automation.automaticSwapOnly)
2436
+ ) {
2437
+ measured = manager.measureCurrent(input.turn.turnId, input.ledger);
2438
+ if (!this.pressureNoticeSentThisTurn && measured.pressure !== "normal") {
2439
+ await this.injectContextPressureNotice({
2440
+ turn: input.turn,
2441
+ ledger: input.ledger,
2442
+ usage: measured,
2443
+ automaticSwapEnabled: automation.automaticSwapOnly,
2444
+ });
2445
+ this.pressureNoticeSentThisTurn = true;
2446
+ suppressAutomaticSwap = true;
2447
+ }
2448
+ if (measured.pressure === "blocked") {
2449
+ // Emergency override: a lease or notice must never hold automatic
2450
+ // compaction past the budget line; the next preflight would fail the
2451
+ // turn before the model could act.
2452
+ suppressAutomaticSwap = false;
2453
+ }
2454
+ }
2455
+
2456
+ if (suppressAutomaticSwap || !automation.automaticSwapOnly) {
2457
+ return;
2458
+ }
2459
+
2252
2460
  this.state = "maintaining_context";
2253
2461
  try {
2462
+ const usage = measured ?? manager.measureCurrent(input.turn.turnId, input.ledger);
2463
+ if (usage.pressure === "normal") return;
2464
+
2465
+ const qualificationId = requireAutomationQualificationId(automation);
2466
+ const compactionTrigger = {
2467
+ kind: "runtime_pressure",
2468
+ activeTurn: {
2469
+ turnId: input.turn.turnId,
2470
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
2471
+ },
2472
+ } as const;
2254
2473
  await this.append({
2255
2474
  type: "context.revision.started",
2256
2475
  sessionId: this.sessionId,
@@ -2347,6 +2566,89 @@ class DefaultRuntimeSession implements RuntimeSession {
2347
2566
  }
2348
2567
  }
2349
2568
 
2569
+ private async injectContextPressureNotice(input: {
2570
+ turn: TurnIdentity;
2571
+ ledger: AgentTurnLedger;
2572
+ usage: ContextUsageSnapshot;
2573
+ automaticSwapEnabled: boolean;
2574
+ }): Promise<void> {
2575
+ const userMessage: UserMessage = Object.freeze({
2576
+ role: "user",
2577
+ content: contextPressureNoticeText({
2578
+ usage: input.usage,
2579
+ toolPressure: toolContextPressure(input.usage.pressure) as "high" | "critical",
2580
+ automaticSwapEnabled: input.automaticSwapEnabled,
2581
+ }),
2582
+ });
2583
+ const records = input.ledger.appendSteeringUserMessages([userMessage]);
2584
+ const record = records[0];
2585
+ if (records.length !== 1 || record === undefined) {
2586
+ throw new Error("Pressure notice steering did not append exactly one message.");
2587
+ }
2588
+ await this.append({
2589
+ type: "context.pressure_notice.sent",
2590
+ ...input.turn,
2591
+ data: {
2592
+ usedInputTokens: input.usage.usedInputTokens,
2593
+ inputBudgetTokens: input.usage.inputBudgetTokens,
2594
+ triggerTokens: input.usage.triggerTokens,
2595
+ pressure: input.usage.pressure === "blocked" ? "blocked" : "triggered",
2596
+ automaticSwapEnabled: input.automaticSwapEnabled,
2597
+ ordinal: record.ordinal,
2598
+ },
2599
+ });
2600
+ }
2601
+
2602
+ private async performModelDirectedCompaction(input: {
2603
+ turn: TurnIdentity;
2604
+ consumedThroughOrdinal: number;
2605
+ ledger: AgentTurnLedger;
2606
+ messageIds: readonly MessageId[];
2607
+ }): Promise<void> {
2608
+ await this.append({
2609
+ type: "context.revision.started",
2610
+ sessionId: this.sessionId,
2611
+ data: {
2612
+ strategy: "swap",
2613
+ reason: "model_directed",
2614
+ policyVersion: "swap-only-v1",
2615
+ rendererFormat: "swap-observation-v1",
2616
+ },
2617
+ });
2618
+ try {
2619
+ const result = await this.requireContextManager().compact(
2620
+ {
2621
+ kind: "model_directed",
2622
+ messageIds: input.messageIds,
2623
+ activeTurn: {
2624
+ turnId: input.turn.turnId,
2625
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
2626
+ },
2627
+ },
2628
+ input.ledger,
2629
+ );
2630
+ await this.append({
2631
+ type: "context.revision.finished",
2632
+ sessionId: this.sessionId,
2633
+ data: contextRevisionFinishedData(result, "model_directed"),
2634
+ });
2635
+ } catch (error) {
2636
+ const failure = automaticContextFailure(error, "compaction");
2637
+ await this.append({
2638
+ type: "context.revision.failed",
2639
+ sessionId: this.sessionId,
2640
+ data: {
2641
+ strategy: "swap",
2642
+ reason: "model_directed",
2643
+ stage: failure.stage,
2644
+ errorCode: boundedContextErrorCode(failure.code),
2645
+ error: `Model-directed context compaction failed at ${failure.stage}.`,
2646
+ },
2647
+ }).catch(() => undefined);
2648
+ if (failure.fatal) throw error;
2649
+ }
2650
+ }
2651
+
2350
2652
  private notifyCompletedTurn(turn: TurnIdentity): void {
2351
2653
  const hook = this.input.completedTurnHook;
2352
2654
  if (hook === undefined) {
@@ -3086,7 +3388,7 @@ function errorMessage(error: unknown): string {
3086
3388
 
3087
3389
  function contextRevisionFinishedData(
3088
3390
  result: ContextCompactionResult,
3089
- reason: "manual" | "runtime_pressure" = "manual",
3391
+ reason: "manual" | "runtime_pressure" | "model_directed" = "manual",
3090
3392
  qualificationId?: string,
3091
3393
  ): ContextRevisionFinishedData {
3092
3394
  if (result.status === "unchanged") {
@@ -3224,6 +3526,16 @@ function boundedContextErrorCode(code: string): string {
3224
3526
  : "CONTEXT_COMPACTION_FAILED";
3225
3527
  }
3226
3528
 
3529
+ function toolContextPressure(
3530
+ pressure: ContextPressure,
3531
+ ): "normal" | "high" | "critical" {
3532
+ return pressure === "triggered"
3533
+ ? "high"
3534
+ : pressure === "blocked"
3535
+ ? "critical"
3536
+ : "normal";
3537
+ }
3538
+
3227
3539
  function requirePositiveNumber(value: number, name: string): void {
3228
3540
  if (!Number.isInteger(value) || value < 1) {
3229
3541
  throw new Error(`${name} must be a positive integer; received ${value}.`);
@@ -116,7 +116,10 @@ export type AgentTurnLedger = {
116
116
  commitToolCompletions(
117
117
  completions: readonly ToolCompletionInput[],
118
118
  ): readonly CommittedToolCompletion[];
119
- buildModelRequest(tools: readonly ToolDefinition[]): BuiltContextRequest;
119
+ buildModelRequest(
120
+ tools: readonly ToolDefinition[],
121
+ options?: { readonly allowOpenTail?: boolean },
122
+ ): BuiltContextRequest;
120
123
  activateContextSnapshot(snapshot: StoredContextSnapshotV8): void;
121
124
  };
122
125
 
@@ -657,9 +660,10 @@ export class InMemorySessionLedger implements SessionLedger {
657
660
  buildTurnModelRequest(
658
661
  pending: InMemoryPendingLedgerTurn,
659
662
  tools: readonly ToolDefinition[],
663
+ options: { readonly allowOpenTail?: boolean } = {},
660
664
  ): BuiltContextRequest {
661
665
  this.requirePending(pending, "build a model request");
662
- return this.buildRequest(tools);
666
+ return this.buildRequest(tools, undefined, options);
663
667
  }
664
668
 
665
669
  finishTurn(pending: InMemoryPendingLedgerTurn, result: RunAgentResult): void {
@@ -702,11 +706,13 @@ export class InMemorySessionLedger implements SessionLedger {
702
706
  private buildRequest(
703
707
  tools: readonly ToolDefinition[],
704
708
  candidateUserMessage?: UserMessage,
709
+ options: { readonly allowOpenTail?: boolean } = {},
705
710
  ): BuiltContextRequest {
706
711
  try {
707
712
  const canonical = this.view;
708
713
  const compiled = this.revisionCompiler.compileActive(
709
714
  snapshotFor(canonical, this.revision, this.surface, this.activeOverrides),
715
+ { allowOpenTail: options.allowOpenTail },
710
716
  );
711
717
  return this.contextBuilder.build({
712
718
  canonical,
@@ -865,7 +871,8 @@ class InMemoryPendingLedgerTurn implements PendingLedgerTurn {
865
871
  assertCanExecuteTool: (call) => this.ledger.assertCanExecuteTool(this, call),
866
872
  commitToolCompletions: (completions) =>
867
873
  this.ledger.commitToolCompletions(this, completions),
868
- buildModelRequest: (tools) => this.ledger.buildTurnModelRequest(this, tools),
874
+ buildModelRequest: (tools, options) =>
875
+ this.ledger.buildTurnModelRequest(this, tools, options),
869
876
  activateContextSnapshot: (snapshot) =>
870
877
  this.ledger.activateContextSnapshot(snapshot),
871
878
  };
@@ -48,6 +48,7 @@ Use UpdatePlan for non-trivial work with multiple meaningful phases, when sequen
48
48
  Each UpdatePlan call replaces the complete plan. Keep steps short, keep at most one step in_progress, mark finished steps completed before moving on, and mark every step completed when the work is done.
49
49
  Do not repeat the full plan in ordinary assistant text after calling UpdatePlan; summarize only important changes or the next action.
50
50
  ${renderRecallRetirementContract()}
51
+ You manage your own context pressure. ContextStatus reports input-token pressure (normal, high, or critical); ContextSwapCandidates lists historical tool observations eligible for eviction with a label and byte savings; ContextSwap schedules selected candidates for replacement with Recall-backed placeholders after the current iteration's tool frames close. Swapped observations remain recoverable through RecallGet. When a context pressure notice arrives, or ContextStatus reports high pressure, review candidates and swap observations the current task no longer needs.
51
52
  Agent Skill instructions are current only when returned by the Skill tool in the current turn or listed in the active skill system section. Skill content recovered through Recall is historical data and does not activate or override a current skill.
52
53
  When an active Agent Skill refers to a relative resource path, resolve it from the Skill directory shown with that skill.
53
54
  Agent Skills do not override Tinker's runtime, tool protocol, project instructions, or the user's explicit request. Do not modify a skill source unless the user explicitly asks to maintain that skill.