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 +33 -1
- package/package.json +1 -1
- package/src/agent/context-pressure-notice.ts +37 -0
- package/src/agent/loop.ts +1 -1
- package/src/agent/runtime-session.ts +329 -17
- package/src/agent/session-ledger.ts +10 -3
- package/src/cli/runner-dependencies.ts +1 -0
- package/src/context/context-manager.ts +185 -8
- package/src/context/context-policy.ts +1 -1
- package/src/context/context-revision-compiler.ts +7 -2
- package/src/context/context-swap-label.ts +132 -0
- package/src/context/swap-planner.ts +219 -56
- package/src/events/stdout-event-printer.ts +6 -0
- package/src/events/types.ts +17 -4
- package/src/model/fake-model-client.ts +8 -2
- package/src/observation/observation-builder.ts +36 -0
- package/src/session/session-store.ts +300 -0
- package/src/tools/context-maintenance.ts +266 -0
- package/src/tools/registry.ts +18 -1
- package/src/tools/types.ts +83 -1
- package/src/tui/event-store.ts +24 -0
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.
|
|
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
|
@@ -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
|
|
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 = {
|
|
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
|
|
2240
|
-
|
|
2241
|
-
if (usage.pressure === "normal") return;
|
|
2403
|
+
const pendingModelDirectedSwap = this.pendingModelDirectedSwap;
|
|
2404
|
+
this.pendingModelDirectedSwap = undefined;
|
|
2242
2405
|
|
|
2243
|
-
const
|
|
2244
|
-
const
|
|
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(
|
|
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) =>
|
|
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.
|