tinker-agent 2.8.0 → 2.9.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 (35) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +64 -10
  3. package/package.json +4 -3
  4. package/src/agent/runtime-context-capabilities.ts +19 -0
  5. package/src/agent/runtime-context-events.ts +127 -0
  6. package/src/agent/runtime-context-maintenance.ts +780 -0
  7. package/src/agent/runtime-interactions.ts +291 -0
  8. package/src/agent/runtime-prompt-scheduler.ts +182 -0
  9. package/src/agent/runtime-session-contracts.ts +317 -0
  10. package/src/agent/runtime-session.ts +250 -2130
  11. package/src/agent/runtime-skills.ts +544 -0
  12. package/src/cli/runner-dependencies.ts +6 -5
  13. package/src/context/context-automation-policy.ts +12 -118
  14. package/src/events/types.ts +12 -0
  15. package/src/memory/memory-get-tool.ts +1 -1
  16. package/src/observation/observation-builder.ts +41 -11
  17. package/src/session/resume-projection.ts +47 -21
  18. package/src/session/session-history-access.ts +238 -0
  19. package/src/session/session-store-context-readers.ts +183 -0
  20. package/src/session/session-store-ledger-writer.ts +315 -0
  21. package/src/session/session-store-record-writer.ts +318 -0
  22. package/src/session/session-store-recovery.ts +225 -0
  23. package/src/session/session-store-revisions.ts +1004 -0
  24. package/src/session/session-store-sql.ts +40 -0
  25. package/src/session/session-store-validation.ts +657 -0
  26. package/src/session/session-store.ts +756 -3186
  27. package/src/tools/bash-task.ts +20 -2
  28. package/src/tools/recall.ts +106 -50
  29. package/src/tools/registry.ts +4 -6
  30. package/src/tools/task-output-range.ts +146 -0
  31. package/src/tools/task-output-tool.ts +35 -5
  32. package/src/tools/task-output.ts +35 -0
  33. package/src/tools/task-tool-args.ts +34 -0
  34. package/src/tools/types.ts +9 -0
  35. package/src/tui/event-store.ts +8 -3
@@ -0,0 +1,780 @@
1
+ import { type ContextAutomationPolicy } from "../context/context-automation-policy";
2
+ import {
3
+ ContextManager,
4
+ ContextManagerError,
5
+ type ContextCompactionResult,
6
+ type ContextRetirementResult,
7
+ } from "../context/context-manager";
8
+ import type { AgentEventInput } from "../events/types";
9
+ import { type MessageId, type SessionId } from "../ids/runtime-id";
10
+ import { type UserMessage } from "../image/image-types";
11
+ import type { ContextPressure } from "../model/model-request-preflight";
12
+ import { SessionStore } from "../session/session-store";
13
+ import {
14
+ ToolExecutionFatalError,
15
+ type ContextStatusRawResult,
16
+ type ContextSwapCandidatesRawResult,
17
+ type ContextSwapRawResult,
18
+ } from "../tools/types";
19
+ import { type ContextUsageSnapshot } from "./context-meter";
20
+ import { contextPressureNoticeText } from "./context-pressure-notice";
21
+ import {
22
+ boundedContextErrorCode,
23
+ contextRetirementFinishedData,
24
+ contextRevisionFinishedData,
25
+ } from "./runtime-context-events";
26
+ import {
27
+ RuntimeEventAppendError,
28
+ type RuntimeSessionFactoryDependencies,
29
+ type RuntimeSessionState,
30
+ } from "./runtime-session-contracts";
31
+ import { type AgentTurnLedger } from "./session-ledger";
32
+ import type { ToolCall, TurnIdentity } from "./types";
33
+
34
+ type ContextMaintenanceLifecycle = {
35
+ getState(): RuntimeSessionState;
36
+ setState(state: "ready" | "executing" | "compacting" | "maintaining_context"): void;
37
+ hasActiveTurn(): boolean;
38
+ fault(error: unknown): void;
39
+ };
40
+ type ActiveContextTool = {
41
+ turn: TurnIdentity;
42
+ ledger: AgentTurnLedger;
43
+ consumedThroughOrdinal: number;
44
+ };
45
+ type MaintenanceTriggers = Pick<
46
+ RuntimeSessionFactoryDependencies,
47
+ | "manualCompactionTrigger"
48
+ | "manualRetirementTrigger"
49
+ | "automaticCompactionTrigger"
50
+ | "automaticRetirementTrigger"
51
+ >;
52
+ /** Owns maintenance scheduling; the runtime retains lifecycle state and event ordering. */
53
+ export class RuntimeContextMaintenance {
54
+ private pendingAutomaticContextMaintenance = false;
55
+ private pendingModelDirectedSwap?: Set<MessageId>;
56
+ private modelDirectedSwapLease = false;
57
+ private pressureNoticeSentThisTurn = false;
58
+ constructor(
59
+ private readonly sessionId: SessionId,
60
+ private readonly store: Pick<
61
+ SessionStore,
62
+ "assertContextRevisionIdle" | "loadContextSnapshot"
63
+ >,
64
+ private readonly requireContextManager: () => ContextManager,
65
+ private readonly requireContextAutomation: () => ContextAutomationPolicy,
66
+ private readonly requireActiveContextTool: (
67
+ call: ToolCall,
68
+ expectedName: "ContextStatus" | "ContextSwapCandidates" | "ContextSwap",
69
+ ) => ActiveContextTool,
70
+ private readonly append: (event: AgentEventInput) => Promise<void>,
71
+ private readonly lifecycle: ContextMaintenanceLifecycle,
72
+ private readonly dependencies: MaintenanceTriggers,
73
+ ) {}
74
+
75
+ scheduleAutomaticMaintenance(): void {
76
+ this.pendingAutomaticContextMaintenance = true;
77
+ }
78
+
79
+ finishTurn(): void {
80
+ this.pendingAutomaticContextMaintenance = false;
81
+ this.pendingModelDirectedSwap = undefined;
82
+ this.modelDirectedSwapLease = false;
83
+ this.pressureNoticeSentThisTurn = false;
84
+ }
85
+
86
+ async contextStatus(call: ToolCall): Promise<ContextStatusRawResult> {
87
+ const active = this.requireActiveContextTool(call, "ContextStatus");
88
+ try {
89
+ const usage = this.requireContextManager().measureActive(
90
+ active.turn.turnId,
91
+ active.ledger,
92
+ );
93
+ return Object.freeze({
94
+ ok: true,
95
+ operation: "status",
96
+ usedInputTokens: usage.usedInputTokens,
97
+ inputBudgetTokens: usage.inputBudgetTokens,
98
+ pressure: toolContextPressure(usage.pressure),
99
+ triggerTokens: usage.triggerTokens,
100
+ source: usage.source,
101
+ });
102
+ } catch (error) {
103
+ return {
104
+ ok: false,
105
+ operation: "status",
106
+ error: this.contextToolFailure("status", error),
107
+ };
108
+ }
109
+ }
110
+
111
+ async contextSwapCandidates(
112
+ call: ToolCall,
113
+ page: { readonly limit: number; readonly offset: number },
114
+ ): Promise<ContextSwapCandidatesRawResult> {
115
+ const active = this.requireActiveContextTool(call, "ContextSwapCandidates");
116
+ try {
117
+ const result = this.requireContextManager().listActiveSwapCandidates({
118
+ turnId: active.turn.turnId,
119
+ consumedThroughOrdinal: active.consumedThroughOrdinal,
120
+ activeLedger: active.ledger,
121
+ limit: page.limit,
122
+ offset: page.offset,
123
+ });
124
+ if (result.total > 0 && result.usage.pressure !== "normal") {
125
+ this.modelDirectedSwapLease = true;
126
+ }
127
+ return Object.freeze({
128
+ ok: true,
129
+ operation: "candidates",
130
+ total: result.total,
131
+ candidates: result.candidates,
132
+ });
133
+ } catch (error) {
134
+ return {
135
+ ok: false,
136
+ operation: "candidates",
137
+ error: this.contextToolFailure("candidate listing", error),
138
+ };
139
+ }
140
+ }
141
+
142
+ async contextSwap(
143
+ call: ToolCall,
144
+ selection: { readonly candidateIds: readonly MessageId[] },
145
+ ): Promise<ContextSwapRawResult> {
146
+ const active = this.requireActiveContextTool(call, "ContextSwap");
147
+ try {
148
+ const result = this.requireContextManager().validateActiveSwapSelection({
149
+ turnId: active.turn.turnId,
150
+ consumedThroughOrdinal: active.consumedThroughOrdinal,
151
+ activeLedger: active.ledger,
152
+ messageIds: selection.candidateIds,
153
+ });
154
+ if (result.scheduled.length === 0) {
155
+ return Object.freeze({
156
+ ok: false,
157
+ operation: "swap",
158
+ scheduled: Object.freeze([]),
159
+ rejected: result.rejected,
160
+ });
161
+ }
162
+ const pending = (this.pendingModelDirectedSwap ??= new Set<MessageId>());
163
+ for (const candidate of result.scheduled) pending.add(candidate.candidateId);
164
+ this.modelDirectedSwapLease = false;
165
+ return Object.freeze({
166
+ ok: true,
167
+ operation: "swap",
168
+ scheduled: result.scheduled,
169
+ rejected: result.rejected,
170
+ note: "Swap executes when this iteration's tool frames close.",
171
+ });
172
+ } catch (error) {
173
+ return {
174
+ ok: false,
175
+ operation: "swap",
176
+ scheduled: [],
177
+ rejected: [],
178
+ error: this.contextToolFailure("swap scheduling", error),
179
+ };
180
+ }
181
+ }
182
+
183
+ private contextToolFailure(operation: string, error: unknown): string {
184
+ if (error instanceof ContextManagerError && !error.fatal) {
185
+ return `Context ${operation} failed (${boundedContextErrorCode(error.code)}).`;
186
+ }
187
+ throw new ToolExecutionFatalError(
188
+ `Context ${operation} required canonical session state that could not be read safely.`,
189
+ { cause: error },
190
+ );
191
+ }
192
+
193
+ async performCompactContext(): Promise<ContextCompactionResult> {
194
+ if (this.lifecycle.getState() !== "ready") {
195
+ throw new Error(
196
+ `Cannot compact context while RuntimeSession is ${this.lifecycle.getState()}.`,
197
+ );
198
+ }
199
+ if (this.lifecycle.hasActiveTurn()) {
200
+ throw new Error("Cannot compact context while a turn is active.");
201
+ }
202
+ this.store.assertContextRevisionIdle();
203
+ this.lifecycle.setState("compacting");
204
+ let started = false;
205
+ try {
206
+ await this.append({
207
+ type: "context.revision.started",
208
+ sessionId: this.sessionId,
209
+ data: {
210
+ strategy: "swap",
211
+ reason: "manual",
212
+ policyVersion: "swap-only-v1",
213
+ rendererFormat: "swap-observation-v1",
214
+ },
215
+ });
216
+ started = true;
217
+ const result = await this.requireContextManager().compact(
218
+ this.dependencies.manualCompactionTrigger(),
219
+ );
220
+ await this.append({
221
+ type: "context.revision.finished",
222
+ sessionId: this.sessionId,
223
+ data: contextRevisionFinishedData(result),
224
+ });
225
+ if (this.lifecycle.getState() === "compacting") {
226
+ this.lifecycle.setState("ready");
227
+ }
228
+ return result;
229
+ } catch (error) {
230
+ if (started && !(error instanceof RuntimeEventAppendError)) {
231
+ const failure =
232
+ error instanceof ContextManagerError
233
+ ? error
234
+ : new ContextManagerError(
235
+ "activate",
236
+ error instanceof Error ? error.name : "CONTEXT_COMPACTION_FAILED",
237
+ true,
238
+ false,
239
+ "Context compaction failed.",
240
+ { cause: error },
241
+ );
242
+ await this.append({
243
+ type: "context.revision.failed",
244
+ sessionId: this.sessionId,
245
+ data: {
246
+ strategy: "swap",
247
+ reason: "manual",
248
+ stage: failure.stage,
249
+ errorCode: boundedContextErrorCode(failure.code),
250
+ error: `Context compaction failed at ${failure.stage}.`,
251
+ },
252
+ }).catch(() => undefined);
253
+ }
254
+ if (!(error instanceof ContextManagerError) || error.fatal) {
255
+ this.lifecycle.fault(error);
256
+ } else if (this.lifecycle.getState() === "compacting") {
257
+ this.lifecycle.setState("ready");
258
+ }
259
+ throw error;
260
+ }
261
+ }
262
+
263
+ async performRetireContext(): Promise<ContextRetirementResult> {
264
+ if (this.lifecycle.getState() !== "ready") {
265
+ throw new Error(
266
+ `Cannot retire context prefix while RuntimeSession is ${this.lifecycle.getState()}.`,
267
+ );
268
+ }
269
+ if (this.lifecycle.hasActiveTurn()) {
270
+ throw new Error("Cannot retire context prefix while a turn is active.");
271
+ }
272
+ this.store.assertContextRevisionIdle();
273
+ const baseRevisionNumber = this.store.loadContextSnapshot().revision.revisionNumber;
274
+ this.lifecycle.setState("compacting");
275
+ let started = false;
276
+ try {
277
+ await this.append({
278
+ type: "context.revision.started",
279
+ sessionId: this.sessionId,
280
+ data: {
281
+ strategy: "retire_prefix",
282
+ reason: "manual",
283
+ policyVersion: "recall-first-retirement-v1",
284
+ baseRevisionNumber,
285
+ },
286
+ });
287
+ started = true;
288
+ const result = await this.requireContextManager().retirePrefix(
289
+ this.dependencies.manualRetirementTrigger(),
290
+ );
291
+ await this.append({
292
+ type: "context.revision.finished",
293
+ sessionId: this.sessionId,
294
+ data: contextRetirementFinishedData(result),
295
+ });
296
+ if (this.lifecycle.getState() === "compacting") {
297
+ this.lifecycle.setState("ready");
298
+ }
299
+ return result;
300
+ } catch (error) {
301
+ if (started && !(error instanceof RuntimeEventAppendError)) {
302
+ const failure =
303
+ error instanceof ContextManagerError
304
+ ? error
305
+ : new ContextManagerError(
306
+ "activate",
307
+ error instanceof Error ? error.name : "CONTEXT_RETIREMENT_FAILED",
308
+ true,
309
+ false,
310
+ "Context prefix retirement failed.",
311
+ { cause: error },
312
+ );
313
+ await this.append({
314
+ type: "context.revision.failed",
315
+ sessionId: this.sessionId,
316
+ data: {
317
+ strategy: "retire_prefix",
318
+ reason: "manual",
319
+ stage: failure.stage,
320
+ errorCode: boundedContextErrorCode(failure.code),
321
+ error: `Context prefix retirement failed at ${failure.stage}.`,
322
+ committed: failure.committed,
323
+ },
324
+ }).catch(() => undefined);
325
+ }
326
+ if (!(error instanceof ContextManagerError) || error.fatal) {
327
+ this.lifecycle.fault(error);
328
+ } else if (this.lifecycle.getState() === "compacting") {
329
+ this.lifecycle.setState("ready");
330
+ }
331
+ throw error;
332
+ }
333
+ }
334
+
335
+ async evaluateClosedTurnContextPressure(): Promise<void> {
336
+ const automation = this.requireContextAutomation();
337
+ if (!automation.automaticSwap) return;
338
+
339
+ const snapshot = this.requireContextManager().measureCurrent();
340
+ await this.append({
341
+ type: "context.usage.updated",
342
+ sessionId: this.sessionId,
343
+ data: { phase: "turn_close", snapshot },
344
+ });
345
+ if (snapshot.pressure !== "normal") {
346
+ this.pendingAutomaticContextMaintenance = true;
347
+ }
348
+ }
349
+
350
+ async performAutomaticContextMaintenance(): Promise<void> {
351
+ if (!this.pendingAutomaticContextMaintenance) return;
352
+ this.pendingAutomaticContextMaintenance = false;
353
+ const automation = this.requireContextAutomation();
354
+ if (!automation.automaticSwap) return;
355
+ if (this.lifecycle.getState() !== "executing") {
356
+ throw new Error(
357
+ `Cannot run automatic context maintenance while RuntimeSession is ${this.lifecycle.getState()}.`,
358
+ );
359
+ }
360
+ this.store.assertContextRevisionIdle();
361
+ const automationPolicyId = automation.policyId;
362
+ this.lifecycle.setState("maintaining_context");
363
+ try {
364
+ const swap = await this.performAutomaticCompaction(automationPolicyId);
365
+ if (swap === undefined) return;
366
+ if (automation.automaticPrefixRetirement && automaticSwapNeedsRetirement(swap)) {
367
+ await this.performAutomaticRetirement(automationPolicyId);
368
+ }
369
+ } finally {
370
+ if (this.lifecycle.getState() === "maintaining_context") {
371
+ this.lifecycle.setState("executing");
372
+ }
373
+ }
374
+ }
375
+
376
+ async performActiveTurnContextMaintenance(input: {
377
+ turn: TurnIdentity;
378
+ consumedThroughOrdinal: number;
379
+ ledger: AgentTurnLedger;
380
+ }): Promise<void> {
381
+ if (this.lifecycle.getState() !== "executing") {
382
+ throw new Error(
383
+ `Cannot maintain active-turn context while RuntimeSession is ${this.lifecycle.getState()}.`,
384
+ );
385
+ }
386
+ const pendingModelDirectedSwap = this.pendingModelDirectedSwap;
387
+ this.pendingModelDirectedSwap = undefined;
388
+
389
+ const automation = this.requireContextAutomation();
390
+ const manager = this.requireContextManager();
391
+ this.pendingAutomaticContextMaintenance = false;
392
+
393
+ let suppressAutomaticSwap = this.modelDirectedSwapLease;
394
+ this.modelDirectedSwapLease = false;
395
+
396
+ if (pendingModelDirectedSwap !== undefined) {
397
+ suppressAutomaticSwap = false;
398
+ this.lifecycle.setState("maintaining_context");
399
+ try {
400
+ await this.performModelDirectedCompaction({
401
+ turn: input.turn,
402
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
403
+ ledger: input.ledger,
404
+ messageIds: Object.freeze([...pendingModelDirectedSwap]),
405
+ });
406
+ } finally {
407
+ if (this.lifecycle.getState() === "maintaining_context") {
408
+ this.lifecycle.setState("executing");
409
+ }
410
+ }
411
+ }
412
+
413
+ let measured: ContextUsageSnapshot | undefined;
414
+ if (
415
+ pendingModelDirectedSwap === undefined &&
416
+ (suppressAutomaticSwap ||
417
+ !this.pressureNoticeSentThisTurn ||
418
+ automation.automaticSwap)
419
+ ) {
420
+ measured = manager.measureCurrent(input.turn.turnId, input.ledger);
421
+ if (!this.pressureNoticeSentThisTurn && measured.pressure !== "normal") {
422
+ await this.injectContextPressureNotice({
423
+ turn: input.turn,
424
+ ledger: input.ledger,
425
+ usage: measured,
426
+ automaticSwapEnabled: automation.automaticSwap,
427
+ });
428
+ this.pressureNoticeSentThisTurn = true;
429
+ suppressAutomaticSwap = true;
430
+ }
431
+ if (measured.pressure === "blocked") {
432
+ // Emergency override: a lease or notice must never hold automatic
433
+ // compaction past the budget line; the next preflight would fail the
434
+ // turn before the model could act.
435
+ suppressAutomaticSwap = false;
436
+ }
437
+ }
438
+
439
+ if (suppressAutomaticSwap || !automation.automaticSwap) {
440
+ return;
441
+ }
442
+
443
+ this.lifecycle.setState("maintaining_context");
444
+ try {
445
+ const usage = measured ?? manager.measureCurrent(input.turn.turnId, input.ledger);
446
+ if (usage.pressure === "normal") return;
447
+
448
+ const automationPolicyId = automation.policyId;
449
+ const compactionTrigger = {
450
+ kind: "runtime_pressure",
451
+ activeTurn: {
452
+ turnId: input.turn.turnId,
453
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
454
+ },
455
+ } as const;
456
+ await this.append({
457
+ type: "context.revision.started",
458
+ sessionId: this.sessionId,
459
+ data: {
460
+ strategy: "swap",
461
+ reason: "runtime_pressure",
462
+ policyVersion: "swap-only-v1",
463
+ rendererFormat: "swap-observation-v1",
464
+ automationPolicyId,
465
+ },
466
+ });
467
+ let swap: ContextCompactionResult;
468
+ try {
469
+ swap = await manager.compact(compactionTrigger, input.ledger);
470
+ await this.append({
471
+ type: "context.revision.finished",
472
+ sessionId: this.sessionId,
473
+ data: contextRevisionFinishedData(
474
+ swap,
475
+ "runtime_pressure",
476
+ automationPolicyId,
477
+ ),
478
+ });
479
+ } catch (error) {
480
+ const failure = automaticContextFailure(error, "compaction");
481
+ await this.append({
482
+ type: "context.revision.failed",
483
+ sessionId: this.sessionId,
484
+ data: {
485
+ strategy: "swap",
486
+ reason: "runtime_pressure",
487
+ stage: failure.stage,
488
+ errorCode: boundedContextErrorCode(failure.code),
489
+ error: `Automatic context compaction failed at ${failure.stage}.`,
490
+ automationPolicyId,
491
+ },
492
+ }).catch(() => undefined);
493
+ if (failure.fatal) throw error;
494
+ return;
495
+ }
496
+
497
+ if (
498
+ !automation.automaticPrefixRetirement ||
499
+ !automaticSwapNeedsRetirement(swap)
500
+ ) {
501
+ return;
502
+ }
503
+
504
+ await this.append({
505
+ type: "context.revision.started",
506
+ sessionId: this.sessionId,
507
+ data: {
508
+ strategy: "retire_prefix",
509
+ reason: "runtime_pressure",
510
+ policyVersion: "recall-first-retirement-v1",
511
+ baseRevisionNumber: this.store.loadContextSnapshot().revision.revisionNumber,
512
+ automationPolicyId,
513
+ },
514
+ });
515
+ try {
516
+ const retirement = await manager.retirePrefix(
517
+ {
518
+ kind: "runtime_pressure",
519
+ activeTurnId: input.turn.turnId,
520
+ },
521
+ input.ledger,
522
+ );
523
+ await this.append({
524
+ type: "context.revision.finished",
525
+ sessionId: this.sessionId,
526
+ data: contextRetirementFinishedData(
527
+ retirement,
528
+ "runtime_pressure",
529
+ automationPolicyId,
530
+ ),
531
+ });
532
+ } catch (error) {
533
+ const failure = automaticContextFailure(error, "retirement");
534
+ await this.append({
535
+ type: "context.revision.failed",
536
+ sessionId: this.sessionId,
537
+ data: {
538
+ strategy: "retire_prefix",
539
+ reason: "runtime_pressure",
540
+ stage: failure.stage,
541
+ errorCode: boundedContextErrorCode(failure.code),
542
+ error: `Automatic context retirement failed at ${failure.stage}.`,
543
+ committed: failure.committed,
544
+ automationPolicyId,
545
+ },
546
+ }).catch(() => undefined);
547
+ if (failure.fatal) throw error;
548
+ }
549
+ } finally {
550
+ if (this.lifecycle.getState() === "maintaining_context") {
551
+ this.lifecycle.setState("executing");
552
+ }
553
+ }
554
+ }
555
+
556
+ private async injectContextPressureNotice(input: {
557
+ turn: TurnIdentity;
558
+ ledger: AgentTurnLedger;
559
+ usage: ContextUsageSnapshot;
560
+ automaticSwapEnabled: boolean;
561
+ }): Promise<void> {
562
+ const userMessage: UserMessage = Object.freeze({
563
+ role: "user",
564
+ content: contextPressureNoticeText({
565
+ usage: input.usage,
566
+ toolPressure: toolContextPressure(input.usage.pressure) as "high" | "critical",
567
+ automaticSwapEnabled: input.automaticSwapEnabled,
568
+ }),
569
+ });
570
+ const records = input.ledger.appendSteeringUserMessages([userMessage]);
571
+ const record = records[0];
572
+ if (records.length !== 1 || record === undefined) {
573
+ throw new Error("Pressure notice steering did not append exactly one message.");
574
+ }
575
+ await this.append({
576
+ type: "context.pressure_notice.sent",
577
+ ...input.turn,
578
+ data: {
579
+ usedInputTokens: input.usage.usedInputTokens,
580
+ inputBudgetTokens: input.usage.inputBudgetTokens,
581
+ triggerTokens: input.usage.triggerTokens,
582
+ pressure: input.usage.pressure === "blocked" ? "blocked" : "triggered",
583
+ automaticSwapEnabled: input.automaticSwapEnabled,
584
+ ordinal: record.ordinal,
585
+ },
586
+ });
587
+ }
588
+
589
+ private async performModelDirectedCompaction(input: {
590
+ turn: TurnIdentity;
591
+ consumedThroughOrdinal: number;
592
+ ledger: AgentTurnLedger;
593
+ messageIds: readonly MessageId[];
594
+ }): Promise<void> {
595
+ await this.append({
596
+ type: "context.revision.started",
597
+ sessionId: this.sessionId,
598
+ data: {
599
+ strategy: "swap",
600
+ reason: "model_directed",
601
+ policyVersion: "swap-only-v1",
602
+ rendererFormat: "swap-observation-v1",
603
+ },
604
+ });
605
+ try {
606
+ const result = await this.requireContextManager().compact(
607
+ {
608
+ kind: "model_directed",
609
+ messageIds: input.messageIds,
610
+ activeTurn: {
611
+ turnId: input.turn.turnId,
612
+ consumedThroughOrdinal: input.consumedThroughOrdinal,
613
+ },
614
+ },
615
+ input.ledger,
616
+ );
617
+ await this.append({
618
+ type: "context.revision.finished",
619
+ sessionId: this.sessionId,
620
+ data: contextRevisionFinishedData(result, "model_directed"),
621
+ });
622
+ } catch (error) {
623
+ const failure = automaticContextFailure(error, "compaction");
624
+ await this.append({
625
+ type: "context.revision.failed",
626
+ sessionId: this.sessionId,
627
+ data: {
628
+ strategy: "swap",
629
+ reason: "model_directed",
630
+ stage: failure.stage,
631
+ errorCode: boundedContextErrorCode(failure.code),
632
+ error: `Model-directed context compaction failed at ${failure.stage}.`,
633
+ },
634
+ }).catch(() => undefined);
635
+ if (failure.fatal) throw error;
636
+ }
637
+ }
638
+
639
+ private async performAutomaticCompaction(
640
+ automationPolicyId: string,
641
+ ): Promise<ContextCompactionResult | undefined> {
642
+ let started = false;
643
+ try {
644
+ await this.append({
645
+ type: "context.revision.started",
646
+ sessionId: this.sessionId,
647
+ data: {
648
+ strategy: "swap",
649
+ reason: "runtime_pressure",
650
+ policyVersion: "swap-only-v1",
651
+ rendererFormat: "swap-observation-v1",
652
+ automationPolicyId,
653
+ },
654
+ });
655
+ started = true;
656
+ const result = await this.requireContextManager().compact(
657
+ this.dependencies.automaticCompactionTrigger(),
658
+ );
659
+ await this.append({
660
+ type: "context.revision.finished",
661
+ sessionId: this.sessionId,
662
+ data: contextRevisionFinishedData(
663
+ result,
664
+ "runtime_pressure",
665
+ automationPolicyId,
666
+ ),
667
+ });
668
+ return result;
669
+ } catch (error) {
670
+ if (started && !(error instanceof RuntimeEventAppendError)) {
671
+ const failure = automaticContextFailure(error, "compaction");
672
+ await this.append({
673
+ type: "context.revision.failed",
674
+ sessionId: this.sessionId,
675
+ data: {
676
+ strategy: "swap",
677
+ reason: "runtime_pressure",
678
+ stage: failure.stage,
679
+ errorCode: boundedContextErrorCode(failure.code),
680
+ error: `Automatic context compaction failed at ${failure.stage}.`,
681
+ automationPolicyId,
682
+ },
683
+ }).catch(() => undefined);
684
+ }
685
+ if (error instanceof ContextManagerError && !error.fatal) {
686
+ return undefined;
687
+ }
688
+ throw error;
689
+ }
690
+ }
691
+
692
+ private async performAutomaticRetirement(
693
+ automationPolicyId: string,
694
+ ): Promise<ContextRetirementResult | undefined> {
695
+ const baseRevisionNumber = this.store.loadContextSnapshot().revision.revisionNumber;
696
+ let started = false;
697
+ try {
698
+ await this.append({
699
+ type: "context.revision.started",
700
+ sessionId: this.sessionId,
701
+ data: {
702
+ strategy: "retire_prefix",
703
+ reason: "runtime_pressure",
704
+ policyVersion: "recall-first-retirement-v1",
705
+ baseRevisionNumber,
706
+ automationPolicyId,
707
+ },
708
+ });
709
+ started = true;
710
+ const result = await this.requireContextManager().retirePrefix(
711
+ this.dependencies.automaticRetirementTrigger(),
712
+ );
713
+ await this.append({
714
+ type: "context.revision.finished",
715
+ sessionId: this.sessionId,
716
+ data: contextRetirementFinishedData(
717
+ result,
718
+ "runtime_pressure",
719
+ automationPolicyId,
720
+ ),
721
+ });
722
+ return result;
723
+ } catch (error) {
724
+ if (started && !(error instanceof RuntimeEventAppendError)) {
725
+ const failure = automaticContextFailure(error, "retirement");
726
+ await this.append({
727
+ type: "context.revision.failed",
728
+ sessionId: this.sessionId,
729
+ data: {
730
+ strategy: "retire_prefix",
731
+ reason: "runtime_pressure",
732
+ stage: failure.stage,
733
+ errorCode: boundedContextErrorCode(failure.code),
734
+ error: `Automatic context retirement failed at ${failure.stage}.`,
735
+ committed: failure.committed,
736
+ automationPolicyId,
737
+ },
738
+ }).catch(() => undefined);
739
+ }
740
+ if (error instanceof ContextManagerError && !error.fatal) {
741
+ return undefined;
742
+ }
743
+ throw error;
744
+ }
745
+ }
746
+ }
747
+ function automaticSwapNeedsRetirement(result: ContextCompactionResult): boolean {
748
+ return (
749
+ result.outcome === "no_eligible_candidates" ||
750
+ result.outcome === "insufficient_candidates"
751
+ );
752
+ }
753
+
754
+ function automaticContextFailure(
755
+ error: unknown,
756
+ strategy: "compaction" | "retirement",
757
+ ): ContextManagerError {
758
+ return error instanceof ContextManagerError
759
+ ? error
760
+ : new ContextManagerError(
761
+ "activate",
762
+ error instanceof Error
763
+ ? error.name
764
+ : `AUTOMATIC_CONTEXT_${strategy.toUpperCase()}_FAILED`,
765
+ true,
766
+ false,
767
+ `Automatic context ${strategy} failed.`,
768
+ { cause: error },
769
+ );
770
+ }
771
+
772
+ function toolContextPressure(
773
+ pressure: ContextPressure,
774
+ ): "normal" | "high" | "critical" {
775
+ return pressure === "triggered"
776
+ ? "high"
777
+ : pressure === "blocked"
778
+ ? "critical"
779
+ : "normal";
780
+ }