tinker-agent 2.2.0 → 2.4.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 +36 -1
- package/README.md +18 -8
- package/package.json +1 -1
- package/src/agent/loop.ts +1 -1
- package/src/agent/runtime-session.ts +279 -28
- package/src/agent/session-ledger.ts +10 -3
- package/src/cli/config.ts +8 -12
- package/src/cli/tui-runner.tsx +3 -1
- 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 +1 -0
- package/src/events/types.ts +3 -3
- package/src/image/image-asset-store.ts +21 -8
- package/src/memory/contracts.ts +31 -0
- package/src/memory/memory-coordinator.ts +7 -0
- package/src/memory/memory-extractor.ts +26 -5
- package/src/model/fake-model-client.ts +7 -0
- package/src/model/model-client.ts +11 -0
- package/src/model/openai-chat-model-client.ts +7 -1
- package/src/model/openai-responses-model-client.ts +8 -1
- package/src/observation/observation-builder.ts +36 -0
- package/src/session/resume-projection.ts +8 -6
- package/src/session/session-catalog.ts +18 -3
- package/src/session/session-last-response-reader.ts +10 -6
- package/src/session/session-store.ts +340 -8
- package/src/session/workspace-storage.ts +84 -0
- package/src/tools/bash-task.ts +14 -12
- package/src/tools/context-maintenance.ts +266 -0
- package/src/tools/registry.ts +20 -1
- package/src/tools/types.ts +83 -1
- package/src/tui/event-store.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.4.0] - 2026-09-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add model-directed context management. Three new built-in tools open context
|
|
13
|
+
maintenance to the model itself: `ContextStatus` reports live input-token
|
|
14
|
+
usage and pressure, `ContextSwapCandidates` lists swappable historical tool
|
|
15
|
+
observations with short labels and byte savings, and `ContextSwap` schedules
|
|
16
|
+
selected observations for eviction. Scheduled swaps are validated immediately
|
|
17
|
+
and committed as one context revision when the iteration's tool frames close,
|
|
18
|
+
leaving Recall-recoverable placeholders behind. A one-shot lease pauses
|
|
19
|
+
automatic compaction for exactly one iteration after a candidate listing so
|
|
20
|
+
the model keeps the choice under real pressure.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Lower the swap eligibility floor from 8 KiB to 2 KiB per observation. Both
|
|
25
|
+
automatic compaction and model-directed swaps can now evict medium-sized
|
|
26
|
+
tool observations, individually or in batches of up to 16 per swap.
|
|
27
|
+
|
|
28
|
+
## [2.3.0] - 2026-08-31
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Store per-workspace runtime state in the global Tinker home instead of inside
|
|
33
|
+
the project. Sessions (`sessions/<id>/`), background Bash logs (`bash/`),
|
|
34
|
+
image assets (`assets/images/`), and prompt history now live under
|
|
35
|
+
`~/.tinker/projects/<workspace-slug-hash>/`, keyed by the workspace's
|
|
36
|
+
canonical absolute path. Projects no longer grow a `.tinker/` directory.
|
|
37
|
+
Existing in-project `.tinker/` state is left in place but no longer read.
|
|
38
|
+
- Add `TINKER_HOME` to relocate the global Tinker home directory (default: the
|
|
39
|
+
OS home directory).
|
|
40
|
+
|
|
8
41
|
## [2.2.0] - 2026-08-29
|
|
9
42
|
|
|
10
43
|
### Added
|
|
@@ -264,7 +297,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
264
297
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
265
298
|
executable.
|
|
266
299
|
|
|
267
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/v2.
|
|
300
|
+
[Unreleased]: https://github.com/ishowshao/tinker/compare/v2.4.0...HEAD
|
|
301
|
+
[2.4.0]: https://github.com/ishowshao/tinker/releases/tag/v2.4.0
|
|
302
|
+
[2.3.0]: https://github.com/ishowshao/tinker/releases/tag/v2.3.0
|
|
268
303
|
[2.2.0]: https://github.com/ishowshao/tinker/releases/tag/v2.2.0
|
|
269
304
|
[2.1.0]: https://github.com/ishowshao/tinker/releases/tag/v2.1.0
|
|
270
305
|
[2.0.0]: https://github.com/ishowshao/tinker/releases/tag/v2.0.0
|
package/README.md
CHANGED
|
@@ -458,13 +458,22 @@ one-shot `tinker run` command. See
|
|
|
458
458
|
[`docs/project-custom-slash-commands-design.md`](docs/project-custom-slash-commands-design.md)
|
|
459
459
|
for the full contract.
|
|
460
460
|
|
|
461
|
-
###
|
|
462
|
-
|
|
463
|
-
Tinker keeps private runtime state
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
461
|
+
### Global Runtime Data
|
|
462
|
+
|
|
463
|
+
Tinker keeps private runtime state in a global home directory instead of inside
|
|
464
|
+
your projects. Each workspace maps to
|
|
465
|
+
`~/.tinker/projects/<workspace-slug-hash>/` — the slug comes from the workspace
|
|
466
|
+
directory name and the hash from its canonical absolute path, so one project
|
|
467
|
+
always resolves to one storage directory even when reached through symlinks.
|
|
468
|
+
That directory holds per-session SQLite databases, event logs, and observation
|
|
469
|
+
logs (`sessions/<id>/`), background Bash task logs (`bash/`), imported image
|
|
470
|
+
assets (`assets/images/`), and prompt history (`prompt-history.jsonl`).
|
|
471
|
+
Workspaces stay clean: no `.tinker/` directory is created inside them.
|
|
472
|
+
|
|
473
|
+
Set `TINKER_HOME` to relocate the global home (default: the OS home directory).
|
|
474
|
+
Global memory lives in `<home>/.tinker/memory/` and the Chrome bridge host in
|
|
475
|
+
`<home>/.tinker/chrome/`. MCP server configuration is loaded from
|
|
476
|
+
`<workspace>/.mcp.json`; project slash commands are loaded from
|
|
468
477
|
`<workspace>/.tinker.json`.
|
|
469
478
|
|
|
470
479
|
`Read` has a fixed 262144-byte (256 KiB) content limit per call. A successful
|
|
@@ -494,8 +503,9 @@ tinker/
|
|
|
494
503
|
│ ├── context/ # Context protocol validation, protocol frame construction
|
|
495
504
|
│ └── ids/ # Runtime ID generation (UUID v7)
|
|
496
505
|
├── docs/ # Design notes and planning documents
|
|
497
|
-
├── .tinker/ # Runtime data (sessions, bash tasks, events)
|
|
498
506
|
└── package.json
|
|
507
|
+
|
|
508
|
+
Runtime data lives in ~/.tinker/ (sessions, bash tasks, assets), not in the repo.
|
|
499
509
|
```
|
|
500
510
|
|
|
501
511
|
## Design Philosophy
|
package/package.json
CHANGED
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,6 +114,7 @@ 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";
|
|
@@ -221,6 +231,7 @@ export type RuntimeSkillsSnapshot = {
|
|
|
221
231
|
|
|
222
232
|
export type RuntimeSessionContext = {
|
|
223
233
|
readonly sessionId: SessionId;
|
|
234
|
+
readonly contextMaintenance: ContextMaintenanceHandle;
|
|
224
235
|
createIteration(turn: TurnIdentity, iterationNumber: number): IterationIdentity;
|
|
225
236
|
createToolCall(
|
|
226
237
|
iteration: IterationIdentity,
|
|
@@ -287,6 +298,7 @@ export type CompletedTurnHook = {
|
|
|
287
298
|
|
|
288
299
|
type CommonRuntimeSessionInput = {
|
|
289
300
|
workspaceRoot: string;
|
|
301
|
+
homeRoot?: string;
|
|
290
302
|
modelName: string;
|
|
291
303
|
profileName?: string;
|
|
292
304
|
maxIterations: number;
|
|
@@ -341,7 +353,10 @@ export type RuntimeSessionFactoryDependencies = {
|
|
|
341
353
|
idFactory: RuntimeIdFactory,
|
|
342
354
|
) => Promise<SessionStore>;
|
|
343
355
|
createLedger: (store: SessionStore, idFactory: RuntimeIdFactory) => SessionLedger;
|
|
344
|
-
createEventSink: (
|
|
356
|
+
createEventSink: (
|
|
357
|
+
input: CreateRuntimeSessionInput,
|
|
358
|
+
sessionDirectory: string,
|
|
359
|
+
) => EventSink;
|
|
345
360
|
selectShadowPlanning: NonNullable<RunAgentInput["shadowPlanning"]>["select"];
|
|
346
361
|
onShadowPlanningResult?: NonNullable<RunAgentInput["shadowPlanning"]>["onResult"];
|
|
347
362
|
selectContextAutomation: typeof selectContextAutomation;
|
|
@@ -375,6 +390,8 @@ type RuntimeSessionState =
|
|
|
375
390
|
|
|
376
391
|
type ActiveTurn = {
|
|
377
392
|
turn: TurnIdentity;
|
|
393
|
+
ledger: AgentTurnLedger;
|
|
394
|
+
consumedThroughOrdinal?: number;
|
|
378
395
|
controller: AbortController;
|
|
379
396
|
completion: Promise<RunAgentResult>;
|
|
380
397
|
};
|
|
@@ -407,10 +424,12 @@ const defaultDependencies: RuntimeSessionFactoryDependencies = {
|
|
|
407
424
|
systemPrompt: input.systemPrompt,
|
|
408
425
|
projectInstruction: input.projectInstruction,
|
|
409
426
|
idFactory,
|
|
427
|
+
...(input.homeRoot === undefined ? {} : { homeRoot: input.homeRoot }),
|
|
410
428
|
})
|
|
411
429
|
: SessionStore.openExisting({
|
|
412
430
|
workspaceRoot: input.workspaceRoot,
|
|
413
431
|
sessionId: input.selection.sessionId,
|
|
432
|
+
...(input.homeRoot === undefined ? {} : { homeRoot: input.homeRoot }),
|
|
414
433
|
}),
|
|
415
434
|
createLedger: (store, idFactory) => new SqliteSessionLedger(store, idFactory),
|
|
416
435
|
createEventSink,
|
|
@@ -461,6 +480,8 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
461
480
|
private contextManager?: ContextManager;
|
|
462
481
|
private contextAutomationDecision?: ContextAutomationDecision;
|
|
463
482
|
private pendingAutomaticContextMaintenance = false;
|
|
483
|
+
private pendingModelDirectedSwap?: Set<MessageId>;
|
|
484
|
+
private modelDirectedSwapLease = false;
|
|
464
485
|
private readonly skillCatalog: SkillCatalogSnapshot;
|
|
465
486
|
private skillCoordinator = new SkillActivationCoordinator();
|
|
466
487
|
private bashGuardMode: "guard" | "yolo";
|
|
@@ -511,6 +532,11 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
511
532
|
this.shadowPlanner = new SwapPlanner(input.modelClient);
|
|
512
533
|
this.context = {
|
|
513
534
|
sessionId: this.sessionId,
|
|
535
|
+
contextMaintenance: {
|
|
536
|
+
status: (call) => this.contextStatus(call),
|
|
537
|
+
candidates: (call, page) => this.contextSwapCandidates(call, page),
|
|
538
|
+
swap: (call, selection) => this.contextSwap(call, selection),
|
|
539
|
+
},
|
|
514
540
|
createIteration: (turn, iterationNumber) =>
|
|
515
541
|
this.createIteration(turn, iterationNumber),
|
|
516
542
|
createToolCall: (iteration, toolCallNumber) =>
|
|
@@ -541,7 +567,10 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
541
567
|
const store = await dependencies.openStore(input, dependencies.idFactory);
|
|
542
568
|
let assetStore: ImageAssetStore;
|
|
543
569
|
try {
|
|
544
|
-
assetStore = await ImageAssetStore.open({
|
|
570
|
+
assetStore = await ImageAssetStore.open({
|
|
571
|
+
workspaceRoot: store.workspaceRoot,
|
|
572
|
+
...(input.homeRoot === undefined ? {} : { homeRoot: input.homeRoot }),
|
|
573
|
+
});
|
|
545
574
|
} catch (error) {
|
|
546
575
|
if (isNewSessionInput(input)) {
|
|
547
576
|
await store
|
|
@@ -557,7 +586,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
557
586
|
session = new DefaultRuntimeSession(
|
|
558
587
|
input,
|
|
559
588
|
dependencies,
|
|
560
|
-
dependencies.createEventSink(input),
|
|
589
|
+
dependencies.createEventSink(input, store.sessionDirectory),
|
|
561
590
|
dependencies.createObservationBuilder(),
|
|
562
591
|
store,
|
|
563
592
|
assetStore,
|
|
@@ -654,6 +683,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
654
683
|
|
|
655
684
|
session.tooling = dependencies.createTooling({
|
|
656
685
|
workspaceRoot: input.workspaceRoot,
|
|
686
|
+
...(input.homeRoot === undefined ? {} : { homeRoot: input.homeRoot }),
|
|
657
687
|
runtimeSession: session.context,
|
|
658
688
|
historyReader: store.historyReader(),
|
|
659
689
|
imageAssetStore: assetStore,
|
|
@@ -1238,6 +1268,133 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1238
1268
|
return this.contextAutomationDecision;
|
|
1239
1269
|
}
|
|
1240
1270
|
|
|
1271
|
+
private async contextStatus(call: ToolCall): Promise<ContextStatusRawResult> {
|
|
1272
|
+
const active = this.requireActiveContextTool(call, "ContextStatus");
|
|
1273
|
+
try {
|
|
1274
|
+
const usage = this.requireContextManager().measureActive(
|
|
1275
|
+
active.turn.turnId,
|
|
1276
|
+
active.ledger,
|
|
1277
|
+
);
|
|
1278
|
+
return Object.freeze({
|
|
1279
|
+
ok: true,
|
|
1280
|
+
operation: "status",
|
|
1281
|
+
usedInputTokens: usage.usedInputTokens,
|
|
1282
|
+
inputBudgetTokens: usage.inputBudgetTokens,
|
|
1283
|
+
pressure: toolContextPressure(usage.pressure),
|
|
1284
|
+
triggerTokens: usage.triggerTokens,
|
|
1285
|
+
source: usage.source,
|
|
1286
|
+
});
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
return {
|
|
1289
|
+
ok: false,
|
|
1290
|
+
operation: "status",
|
|
1291
|
+
error: this.contextToolFailure("status", error),
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
private async contextSwapCandidates(
|
|
1297
|
+
call: ToolCall,
|
|
1298
|
+
page: { readonly limit: number; readonly offset: number },
|
|
1299
|
+
): Promise<ContextSwapCandidatesRawResult> {
|
|
1300
|
+
const active = this.requireActiveContextTool(call, "ContextSwapCandidates");
|
|
1301
|
+
try {
|
|
1302
|
+
const result = this.requireContextManager().listActiveSwapCandidates({
|
|
1303
|
+
turnId: active.turn.turnId,
|
|
1304
|
+
consumedThroughOrdinal: active.consumedThroughOrdinal,
|
|
1305
|
+
activeLedger: active.ledger,
|
|
1306
|
+
limit: page.limit,
|
|
1307
|
+
offset: page.offset,
|
|
1308
|
+
});
|
|
1309
|
+
if (result.total > 0 && result.usage.pressure !== "normal") {
|
|
1310
|
+
this.modelDirectedSwapLease = true;
|
|
1311
|
+
}
|
|
1312
|
+
return Object.freeze({
|
|
1313
|
+
ok: true,
|
|
1314
|
+
operation: "candidates",
|
|
1315
|
+
total: result.total,
|
|
1316
|
+
candidates: result.candidates,
|
|
1317
|
+
});
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
return {
|
|
1320
|
+
ok: false,
|
|
1321
|
+
operation: "candidates",
|
|
1322
|
+
error: this.contextToolFailure("candidate listing", error),
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
private async contextSwap(
|
|
1328
|
+
call: ToolCall,
|
|
1329
|
+
selection: { readonly candidateIds: readonly MessageId[] },
|
|
1330
|
+
): Promise<ContextSwapRawResult> {
|
|
1331
|
+
const active = this.requireActiveContextTool(call, "ContextSwap");
|
|
1332
|
+
try {
|
|
1333
|
+
const result = this.requireContextManager().validateActiveSwapSelection({
|
|
1334
|
+
turnId: active.turn.turnId,
|
|
1335
|
+
consumedThroughOrdinal: active.consumedThroughOrdinal,
|
|
1336
|
+
activeLedger: active.ledger,
|
|
1337
|
+
messageIds: selection.candidateIds,
|
|
1338
|
+
});
|
|
1339
|
+
if (result.scheduled.length === 0) {
|
|
1340
|
+
return Object.freeze({
|
|
1341
|
+
ok: false,
|
|
1342
|
+
operation: "swap",
|
|
1343
|
+
scheduled: Object.freeze([]),
|
|
1344
|
+
rejected: result.rejected,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
const pending = (this.pendingModelDirectedSwap ??= new Set<MessageId>());
|
|
1348
|
+
for (const candidate of result.scheduled) pending.add(candidate.candidateId);
|
|
1349
|
+
this.modelDirectedSwapLease = false;
|
|
1350
|
+
return Object.freeze({
|
|
1351
|
+
ok: true,
|
|
1352
|
+
operation: "swap",
|
|
1353
|
+
scheduled: result.scheduled,
|
|
1354
|
+
rejected: result.rejected,
|
|
1355
|
+
note: "Swap executes when this iteration's tool frames close.",
|
|
1356
|
+
});
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
return {
|
|
1359
|
+
ok: false,
|
|
1360
|
+
operation: "swap",
|
|
1361
|
+
scheduled: [],
|
|
1362
|
+
rejected: [],
|
|
1363
|
+
error: this.contextToolFailure("swap scheduling", error),
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
private requireActiveContextTool(
|
|
1369
|
+
call: ToolCall,
|
|
1370
|
+
expectedName: "ContextStatus" | "ContextSwapCandidates" | "ContextSwap",
|
|
1371
|
+
): ActiveTurn & { consumedThroughOrdinal: number } {
|
|
1372
|
+
this.requireToolCall(call);
|
|
1373
|
+
const active = this.activeTurn;
|
|
1374
|
+
if (
|
|
1375
|
+
call.name !== expectedName ||
|
|
1376
|
+
active === undefined ||
|
|
1377
|
+
active.turn.turnId !== call.turnId ||
|
|
1378
|
+
active.consumedThroughOrdinal === undefined ||
|
|
1379
|
+
this.state !== "executing"
|
|
1380
|
+
) {
|
|
1381
|
+
throw new ToolExecutionFatalError(
|
|
1382
|
+
`${expectedName} was called outside an active model iteration.`,
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
return active as ActiveTurn & { consumedThroughOrdinal: number };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
private contextToolFailure(operation: string, error: unknown): string {
|
|
1389
|
+
if (error instanceof ContextManagerError && !error.fatal) {
|
|
1390
|
+
return `Context ${operation} failed (${boundedContextErrorCode(error.code)}).`;
|
|
1391
|
+
}
|
|
1392
|
+
throw new ToolExecutionFatalError(
|
|
1393
|
+
`Context ${operation} required canonical session state that could not be read safely.`,
|
|
1394
|
+
{ cause: error },
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1241
1398
|
private appendSkillsCatalogLoaded(): Promise<void> {
|
|
1242
1399
|
const activeNames = this.skillCoordinator
|
|
1243
1400
|
.activeEntries()
|
|
@@ -1297,6 +1454,15 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1297
1454
|
iteration: IterationIdentity;
|
|
1298
1455
|
built: BuiltContextRequest;
|
|
1299
1456
|
}): void {
|
|
1457
|
+
const active = this.activeTurn;
|
|
1458
|
+
if (
|
|
1459
|
+
active === undefined ||
|
|
1460
|
+
active.turn.turnId !== input.iteration.turnId ||
|
|
1461
|
+
active.turn.sessionId !== input.iteration.sessionId
|
|
1462
|
+
) {
|
|
1463
|
+
throw new Error("Model dispatch does not belong to the active runtime turn.");
|
|
1464
|
+
}
|
|
1465
|
+
active.consumedThroughOrdinal = input.built.canonical.messages.length;
|
|
1300
1466
|
const pending = this.store.loadSkillActivations(["pending"]);
|
|
1301
1467
|
if (pending.length === 0) {
|
|
1302
1468
|
return;
|
|
@@ -1665,7 +1831,12 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1665
1831
|
usage: admissionSnapshot,
|
|
1666
1832
|
},
|
|
1667
1833
|
});
|
|
1668
|
-
this.activeTurn = {
|
|
1834
|
+
this.activeTurn = {
|
|
1835
|
+
turn,
|
|
1836
|
+
ledger: pendingLedgerTurn.agent,
|
|
1837
|
+
controller,
|
|
1838
|
+
completion,
|
|
1839
|
+
};
|
|
1669
1840
|
this.notifyPromptScheduler();
|
|
1670
1841
|
return Object.freeze({
|
|
1671
1842
|
turnId: turn.turnId,
|
|
@@ -2182,6 +2353,8 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
2182
2353
|
this.activeTurn = undefined;
|
|
2183
2354
|
this.notifyPromptScheduler();
|
|
2184
2355
|
this.pendingAutomaticContextMaintenance = false;
|
|
2356
|
+
this.pendingModelDirectedSwap = undefined;
|
|
2357
|
+
this.modelDirectedSwapLease = false;
|
|
2185
2358
|
if (this.state === "executing") {
|
|
2186
2359
|
this.state = "ready";
|
|
2187
2360
|
}
|
|
@@ -2219,28 +2392,48 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
2219
2392
|
consumedThroughOrdinal: number;
|
|
2220
2393
|
ledger: AgentTurnLedger;
|
|
2221
2394
|
}): Promise<void> {
|
|
2222
|
-
const automation = this.requireContextAutomation();
|
|
2223
|
-
if (!automation.automaticSwapOnly) return;
|
|
2224
2395
|
if (this.state !== "executing") {
|
|
2225
2396
|
throw new Error(
|
|
2226
2397
|
`Cannot maintain active-turn context while RuntimeSession is ${this.state}.`,
|
|
2227
2398
|
);
|
|
2228
2399
|
}
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
if (
|
|
2400
|
+
const pendingModelDirectedSwap = this.pendingModelDirectedSwap;
|
|
2401
|
+
this.pendingModelDirectedSwap = undefined;
|
|
2402
|
+
if (pendingModelDirectedSwap === undefined && this.modelDirectedSwapLease) {
|
|
2403
|
+
this.modelDirectedSwapLease = false;
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2232
2406
|
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
consumedThroughOrdinal: input.consumedThroughOrdinal,
|
|
2239
|
-
},
|
|
2240
|
-
} as const;
|
|
2407
|
+
const automation = this.requireContextAutomation();
|
|
2408
|
+
if (pendingModelDirectedSwap === undefined && !automation.automaticSwapOnly) {
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
const manager = this.requireContextManager();
|
|
2241
2412
|
this.pendingAutomaticContextMaintenance = false;
|
|
2413
|
+
this.modelDirectedSwapLease = false;
|
|
2242
2414
|
this.state = "maintaining_context";
|
|
2243
2415
|
try {
|
|
2416
|
+
if (pendingModelDirectedSwap !== undefined) {
|
|
2417
|
+
await this.performModelDirectedCompaction({
|
|
2418
|
+
turn: input.turn,
|
|
2419
|
+
consumedThroughOrdinal: input.consumedThroughOrdinal,
|
|
2420
|
+
ledger: input.ledger,
|
|
2421
|
+
messageIds: Object.freeze([...pendingModelDirectedSwap]),
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
if (!automation.automaticSwapOnly) return;
|
|
2425
|
+
|
|
2426
|
+
const usage = manager.measureCurrent(input.turn.turnId, input.ledger);
|
|
2427
|
+
if (usage.pressure === "normal") return;
|
|
2428
|
+
|
|
2429
|
+
const qualificationId = requireAutomationQualificationId(automation);
|
|
2430
|
+
const compactionTrigger = {
|
|
2431
|
+
kind: "runtime_pressure",
|
|
2432
|
+
activeTurn: {
|
|
2433
|
+
turnId: input.turn.turnId,
|
|
2434
|
+
consumedThroughOrdinal: input.consumedThroughOrdinal,
|
|
2435
|
+
},
|
|
2436
|
+
} as const;
|
|
2244
2437
|
await this.append({
|
|
2245
2438
|
type: "context.revision.started",
|
|
2246
2439
|
sessionId: this.sessionId,
|
|
@@ -2337,6 +2530,56 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
2337
2530
|
}
|
|
2338
2531
|
}
|
|
2339
2532
|
|
|
2533
|
+
private async performModelDirectedCompaction(input: {
|
|
2534
|
+
turn: TurnIdentity;
|
|
2535
|
+
consumedThroughOrdinal: number;
|
|
2536
|
+
ledger: AgentTurnLedger;
|
|
2537
|
+
messageIds: readonly MessageId[];
|
|
2538
|
+
}): Promise<void> {
|
|
2539
|
+
await this.append({
|
|
2540
|
+
type: "context.revision.started",
|
|
2541
|
+
sessionId: this.sessionId,
|
|
2542
|
+
data: {
|
|
2543
|
+
strategy: "swap",
|
|
2544
|
+
reason: "model_directed",
|
|
2545
|
+
policyVersion: "swap-only-v1",
|
|
2546
|
+
rendererFormat: "swap-observation-v1",
|
|
2547
|
+
},
|
|
2548
|
+
});
|
|
2549
|
+
try {
|
|
2550
|
+
const result = await this.requireContextManager().compact(
|
|
2551
|
+
{
|
|
2552
|
+
kind: "model_directed",
|
|
2553
|
+
messageIds: input.messageIds,
|
|
2554
|
+
activeTurn: {
|
|
2555
|
+
turnId: input.turn.turnId,
|
|
2556
|
+
consumedThroughOrdinal: input.consumedThroughOrdinal,
|
|
2557
|
+
},
|
|
2558
|
+
},
|
|
2559
|
+
input.ledger,
|
|
2560
|
+
);
|
|
2561
|
+
await this.append({
|
|
2562
|
+
type: "context.revision.finished",
|
|
2563
|
+
sessionId: this.sessionId,
|
|
2564
|
+
data: contextRevisionFinishedData(result, "model_directed"),
|
|
2565
|
+
});
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
const failure = automaticContextFailure(error, "compaction");
|
|
2568
|
+
await this.append({
|
|
2569
|
+
type: "context.revision.failed",
|
|
2570
|
+
sessionId: this.sessionId,
|
|
2571
|
+
data: {
|
|
2572
|
+
strategy: "swap",
|
|
2573
|
+
reason: "model_directed",
|
|
2574
|
+
stage: failure.stage,
|
|
2575
|
+
errorCode: boundedContextErrorCode(failure.code),
|
|
2576
|
+
error: `Model-directed context compaction failed at ${failure.stage}.`,
|
|
2577
|
+
},
|
|
2578
|
+
}).catch(() => undefined);
|
|
2579
|
+
if (failure.fatal) throw error;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2340
2583
|
private notifyCompletedTurn(turn: TurnIdentity): void {
|
|
2341
2584
|
const hook = this.input.completedTurnHook;
|
|
2342
2585
|
if (hook === undefined) {
|
|
@@ -2928,21 +3171,19 @@ export async function createRuntimeSession(
|
|
|
2928
3171
|
});
|
|
2929
3172
|
}
|
|
2930
3173
|
|
|
2931
|
-
function createEventSink(
|
|
3174
|
+
function createEventSink(
|
|
3175
|
+
input: CreateRuntimeSessionInput,
|
|
3176
|
+
sessionDirectory: string,
|
|
3177
|
+
): EventSink {
|
|
2932
3178
|
const requiredSinks: EventSink[] = [];
|
|
2933
3179
|
if (input.persistence !== false) {
|
|
2934
|
-
const basePath = path.join(
|
|
2935
|
-
input.workspaceRoot,
|
|
2936
|
-
".tinker",
|
|
2937
|
-
"sessions",
|
|
2938
|
-
input.selection.sessionId,
|
|
2939
|
-
);
|
|
2940
3180
|
requiredSinks.push(
|
|
2941
3181
|
new JsonlEventLog(
|
|
2942
|
-
input.persistence?.eventLogPath ?? path.join(
|
|
3182
|
+
input.persistence?.eventLogPath ?? path.join(sessionDirectory, "events.jsonl"),
|
|
2943
3183
|
),
|
|
2944
3184
|
new ObservationTextLog(
|
|
2945
|
-
input.persistence?.observationLogPath ??
|
|
3185
|
+
input.persistence?.observationLogPath ??
|
|
3186
|
+
path.join(sessionDirectory, "observations.md"),
|
|
2946
3187
|
),
|
|
2947
3188
|
);
|
|
2948
3189
|
}
|
|
@@ -3078,7 +3319,7 @@ function errorMessage(error: unknown): string {
|
|
|
3078
3319
|
|
|
3079
3320
|
function contextRevisionFinishedData(
|
|
3080
3321
|
result: ContextCompactionResult,
|
|
3081
|
-
reason: "manual" | "runtime_pressure" = "manual",
|
|
3322
|
+
reason: "manual" | "runtime_pressure" | "model_directed" = "manual",
|
|
3082
3323
|
qualificationId?: string,
|
|
3083
3324
|
): ContextRevisionFinishedData {
|
|
3084
3325
|
if (result.status === "unchanged") {
|
|
@@ -3216,6 +3457,16 @@ function boundedContextErrorCode(code: string): string {
|
|
|
3216
3457
|
: "CONTEXT_COMPACTION_FAILED";
|
|
3217
3458
|
}
|
|
3218
3459
|
|
|
3460
|
+
function toolContextPressure(
|
|
3461
|
+
pressure: ContextPressure,
|
|
3462
|
+
): "normal" | "high" | "critical" {
|
|
3463
|
+
return pressure === "triggered"
|
|
3464
|
+
? "high"
|
|
3465
|
+
: pressure === "blocked"
|
|
3466
|
+
? "critical"
|
|
3467
|
+
: "normal";
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3219
3470
|
function requirePositiveNumber(value: number, name: string): void {
|
|
3220
3471
|
if (!Number.isInteger(value) || value < 1) {
|
|
3221
3472
|
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
|
};
|
package/src/cli/config.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type ParsedPublicEnvironment,
|
|
25
25
|
type PublicToolingConfig,
|
|
26
26
|
} from "./public-config-contract";
|
|
27
|
+
import { resolveWorkspaceStorageRoot } from "../session/workspace-storage";
|
|
27
28
|
|
|
28
29
|
export type RunnerConfig = {
|
|
29
30
|
readonly sessionId: SessionId;
|
|
@@ -227,17 +228,12 @@ function runnerConfigFromTemplate(
|
|
|
227
228
|
});
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
export function
|
|
231
|
-
return path.join(workspaceRoot, ".tinker", "sessions", sessionId, "events.jsonl");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function observationLogPath(
|
|
231
|
+
export async function promptHistoryPath(
|
|
235
232
|
workspaceRoot: string,
|
|
236
|
-
|
|
237
|
-
): string {
|
|
238
|
-
return path.join(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return path.join(workspaceRoot, ".tinker", "prompt-history.jsonl");
|
|
233
|
+
homeRoot?: string,
|
|
234
|
+
): Promise<string> {
|
|
235
|
+
return path.join(
|
|
236
|
+
await resolveWorkspaceStorageRoot(workspaceRoot, homeRoot),
|
|
237
|
+
"prompt-history.jsonl",
|
|
238
|
+
);
|
|
243
239
|
}
|
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -160,7 +160,9 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
160
160
|
config.sessionId,
|
|
161
161
|
projectionStore,
|
|
162
162
|
);
|
|
163
|
-
const promptHistory = await PromptHistory.load(
|
|
163
|
+
const promptHistory = await PromptHistory.load(
|
|
164
|
+
await promptHistoryPath(workspaceRoot),
|
|
165
|
+
);
|
|
164
166
|
const catalog = new SessionCatalog({ workspaceRoot });
|
|
165
167
|
|
|
166
168
|
const openStoredSession = async (
|