@aragon-agent/cli 0.6.1 → 0.6.3

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 (127) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/README.md +241 -18
  3. package/dist/agent/agent-mode.d.ts +39 -0
  4. package/dist/agent/agent-mode.js +36 -0
  5. package/dist/agent/controller.d.ts +74 -1
  6. package/dist/agent/controller.js +200 -1
  7. package/dist/agent/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +47 -0
  9. package/dist/agent/reducer.d.ts +120 -0
  10. package/dist/agent/reducer.js +201 -2
  11. package/dist/cli.js +297 -6
  12. package/dist/commands/builtins.js +73 -23
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +113 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +342 -0
  20. package/dist/compaction/compactor.d.ts +268 -0
  21. package/dist/compaction/compactor.js +689 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +179 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +255 -0
  37. package/dist/compaction/wiring.js +556 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +4 -1
  43. package/dist/config/env.js +92 -2
  44. package/dist/config/load.d.ts +31 -0
  45. package/dist/config/load.js +63 -1
  46. package/dist/config/schema.d.ts +159 -0
  47. package/dist/config/schema.js +137 -0
  48. package/dist/config/store.js +17 -1
  49. package/dist/config/ui-state.d.ts +44 -6
  50. package/dist/config/ui-state.js +21 -12
  51. package/dist/diagnostics/doctor.js +41 -1
  52. package/dist/exec/events.d.ts +50 -1
  53. package/dist/exec/index.js +8 -4
  54. package/dist/exec/runner.js +44 -2
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/stdin-mouse-filter.d.ts +8 -3
  58. package/dist/logging/install.d.ts +51 -1
  59. package/dist/logging/install.js +28 -0
  60. package/dist/logging/logger.d.ts +1 -1
  61. package/dist/session/cli-commands.js +23 -1
  62. package/dist/session/persist.js +19 -0
  63. package/dist/session/store.d.ts +18 -0
  64. package/dist/session/store.js +59 -2
  65. package/dist/team/report.js +7 -1
  66. package/dist/team/runtime.d.ts +10 -0
  67. package/dist/team/runtime.js +7 -0
  68. package/dist/team/subagent.d.ts +31 -1
  69. package/dist/team/subagent.js +42 -15
  70. package/dist/team/types.d.ts +8 -0
  71. package/dist/ui/ActivityLine.d.ts +19 -1
  72. package/dist/ui/ActivityLine.js +10 -2
  73. package/dist/ui/App.d.ts +48 -1
  74. package/dist/ui/App.js +317 -17
  75. package/dist/ui/Composer.d.ts +26 -1
  76. package/dist/ui/Composer.js +6 -6
  77. package/dist/ui/PromptInput.d.ts +34 -1
  78. package/dist/ui/PromptInput.js +54 -3
  79. package/dist/ui/StatusBar.d.ts +40 -0
  80. package/dist/ui/StatusBar.js +13 -4
  81. package/dist/ui/TeamPanel.js +7 -1
  82. package/dist/ui/Transcript.d.ts +21 -1
  83. package/dist/ui/Transcript.js +32 -2
  84. package/dist/ui/clipboard.d.ts +52 -0
  85. package/dist/ui/clipboard.js +91 -0
  86. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  87. package/dist/ui/entries/CompactionCard.js +158 -0
  88. package/dist/ui/frame-differ.d.ts +46 -0
  89. package/dist/ui/frame-differ.js +94 -10
  90. package/dist/ui/gauge.d.ts +16 -1
  91. package/dist/ui/gauge.js +25 -6
  92. package/dist/ui/glyphs.d.ts +11 -0
  93. package/dist/ui/glyphs.js +2 -0
  94. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  95. package/dist/ui/layout/ScrollViewport.js +128 -21
  96. package/dist/ui/layout/budget.d.ts +10 -3
  97. package/dist/ui/layout/budget.js +10 -3
  98. package/dist/ui/layout/follow-state.d.ts +137 -0
  99. package/dist/ui/layout/follow-state.js +150 -0
  100. package/dist/ui/layout/virtual-window.d.ts +11 -0
  101. package/dist/ui/layout/virtual-window.js +34 -0
  102. package/dist/ui/overlays/HelpOverlay.js +18 -2
  103. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  104. package/dist/ui/overlays/SettingsScreen.js +60 -1
  105. package/dist/ui/palettes.d.ts +16 -0
  106. package/dist/ui/palettes.js +6 -0
  107. package/dist/ui/screen.d.ts +22 -0
  108. package/dist/ui/screen.js +70 -8
  109. package/dist/ui/selection/highlight.d.ts +39 -0
  110. package/dist/ui/selection/highlight.js +64 -0
  111. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  112. package/dist/ui/selection/screen-mirror.js +35 -0
  113. package/dist/ui/selection/selection-controller.d.ts +88 -0
  114. package/dist/ui/selection/selection-controller.js +282 -0
  115. package/dist/ui/selection/selection.d.ts +90 -0
  116. package/dist/ui/selection/selection.js +223 -0
  117. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  118. package/dist/ui/stdout-frame-writer.js +15 -0
  119. package/dist/ui/theme.d.ts +11 -0
  120. package/dist/ui/theme.js +2 -0
  121. package/dist/ui/transcript-text.js +23 -0
  122. package/dist/ui/use-startup-notices.d.ts +62 -5
  123. package/dist/ui/use-startup-notices.js +81 -17
  124. package/dist/ui/use-wheel-routing.js +8 -0
  125. package/dist/ui/win-vt-force.d.ts +136 -0
  126. package/dist/ui/win-vt-force.js +237 -0
  127. package/package.json +4 -2
@@ -9,7 +9,7 @@
9
9
  * from the event stream by the reducer (TUI) and the headless writer.
10
10
  */
11
11
  import { ModelRegistry, type AgentEvent, type AgentTool, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ThinkingLevel } from '@aragon-agent/core';
12
- import type { CliConfig, FastConfig, RetryConfig, TeamConfig, ThemeName, TodoConfig } from '../config/schema.js';
12
+ import type { CliConfig, CompactionConfig, FastConfig, RetryConfig, TeamConfig, ThemeName, TodoConfig } from '../config/schema.js';
13
13
  import { type ConfirmRequest } from '../tools/index.js';
14
14
  import type { ToolPermission } from '../exec/permission.js';
15
15
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
@@ -17,6 +17,7 @@ import type { FilePatch } from '../tools/patch.js';
17
17
  import type { TeamEvent, TeamSnapshot } from '../team/types.js';
18
18
  import { type FastStatus } from '../fast/wiring.js';
19
19
  import type { FastEventListener } from '../fast/types.js';
20
+ import type { CompactionEventListener, CompactionSnapshot } from '../compaction/types.js';
20
21
  import type { TodoEventListener, TodoSnapshot } from '../todo/types.js';
21
22
  import { type HumanInputGate } from '../tools/human-input.js';
22
23
  import { SkillService, type ApprovalGate } from '../skills/service.js';
@@ -174,6 +175,7 @@ export declare class AgentController {
174
175
  private readonly toolOutputs;
175
176
  private readonly outputListeners;
176
177
  private readonly fast;
178
+ private readonly compaction;
177
179
  /**
178
180
  * Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
179
181
  * Cleared at the next `agent_start`.
@@ -491,6 +493,77 @@ export declare class AgentController {
491
493
  * `subscribeTodos` already set.
492
494
  */
493
495
  subscribeFast(listener: FastEventListener): () => void;
496
+ /** Whether a `ContextManager` exists at all. Fixed at construction. */
497
+ isCompactionRegistered(): boolean;
498
+ /** Whether it will actually fire. Flipped live by `/compact on|off`. */
499
+ isCompactionEnabled(): boolean;
500
+ /**
501
+ * Flip the live switch.
502
+ *
503
+ * A NO-OP WHEN NOTHING IS REGISTERED, and `/compact` reports that honestly by
504
+ * checking `isCompactionRegistered()` first — the `/fast on` and `/team on`
505
+ * shape. `enabled` decides whether the wiring EXISTS, which happens once at
506
+ * construction and cannot be undone: a session that started with no manager
507
+ * would have to be given one mid-flight, and the `Agent` has no setter for it
508
+ * on purpose (§3.2).
509
+ */
510
+ setCompactionEnabled(enabled: boolean): void;
511
+ getCompactionConfig(): CompactionConfig;
512
+ /**
513
+ * Adopt a new compaction config for this session.
514
+ *
515
+ * THE `onConfigChanged` CALL IS NOT OPTIONAL, for the reason `setFastConfig`
516
+ * records: a persist without it reports success and changes nothing visible
517
+ * until relaunch, because the chip and `/compact status` read a snapshot.
518
+ */
519
+ setCompactionConfig(patch: Partial<CompactionConfig>): CompactionConfig;
520
+ /** Everything `/compact status`, the settings row and the chip read. */
521
+ getCompactionSnapshot(): CompactionSnapshot;
522
+ /**
523
+ * The `ModelRef` the next compaction would summarize with, or `null`.
524
+ *
525
+ * IT IS CARRIED, NEVER RE-DERIVED, and that is the whole reason it exists. The
526
+ * summarizer is the fast tier when `useFastTier` is on AND the tier resolves,
527
+ * and the session's own model otherwise — a combination no caller can
528
+ * reconstruct from `CliConfig` without re-implementing `resolveFastTier`, and
529
+ * one that a plausible guess (`fast.enabled ? fast.provider : provider`) gets
530
+ * WRONG whenever `fast.enabled` is true and `compaction.useFastTier` is false.
531
+ * Pricing the spend against the wrong provider looks up a model the static
532
+ * table has never seen, which prices it at `$0.00` — the C-11 / RV-4 lie, on
533
+ * the one feature that spends money without being asked (AC-15).
534
+ */
535
+ getCompactionSummarizerRef(): ModelRef | null;
536
+ /**
537
+ * This run's archive key, or `null` when compaction is unregistered
538
+ * (context-auto-compaction-hardening §3.5 / W4).
539
+ *
540
+ * `/compact history` and `/compact show` scope themselves to it, so a second
541
+ * `aragon` in another terminal is invisible here rather than interleaved with
542
+ * this one's compactions.
543
+ */
544
+ getCompactionRunId(): string | null;
545
+ /** Ask for a compaction at the next turn boundary (§4.4 / D-17). */
546
+ queueCompaction(instructions?: string): void;
547
+ /**
548
+ * The IDLE `/compact` path (§4.4 / D-25).
549
+ *
550
+ * IT VALIDATES BEFORE IT SPLICES. `CompactionWiring.compactNow` runs
551
+ * `validateHistory` itself and refuses on failure, because with no loop running
552
+ * the engine's own gate is unreachable and D-4 declares that gate
553
+ * unbypassable. `replaceMessages` validates NOTHING.
554
+ */
555
+ compactNow(instructions?: string): Promise<{
556
+ ok: boolean;
557
+ reason?: string;
558
+ }>;
559
+ /**
560
+ * Subscribe to the CLI-local compaction event stream.
561
+ *
562
+ * Returns a no-op unsubscribe when compaction was off at construction, so
563
+ * every caller can subscribe unconditionally — the contract `subscribeTeam`,
564
+ * `subscribeTodos` and `subscribeFast` already set.
565
+ */
566
+ subscribeCompaction(listener: CompactionEventListener): () => void;
494
567
  /** List usable tools (name + description) for the /tools command. */
495
568
  listTools(): AgentTool[];
496
569
  }
@@ -9,7 +9,7 @@
9
9
  * from the event stream by the reducer (TUI) and the headless writer.
10
10
  */
11
11
  import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
12
- import { clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
12
+ import { clampCompactionConfig, clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
13
13
  import { makeGetApiKey } from '../config/load.js';
14
14
  import { updatePersistedConfig } from '../config/store.js';
15
15
  import { createBuiltinTools } from '../tools/index.js';
@@ -22,6 +22,7 @@ import { buildTeamBlock } from '../team/prompt.js';
22
22
  import { buildFastBlock } from '../fast/prompt.js';
23
23
  import { describeFastTierProblem, fastProviderOf, resolveFastTier } from '../fast/resolve.js';
24
24
  import { FastWiring, offFastStatus } from '../fast/wiring.js';
25
+ import { CompactionWiring, offCompactionSnapshot } from '../compaction/wiring.js';
25
26
  import { TodoStore } from '../todo/store.js';
26
27
  import { createTodoTool } from '../todo/todo-tool.js';
27
28
  import { buildTodoBlock } from '../todo/prompt.js';
@@ -153,6 +154,24 @@ export class AgentController {
153
154
  // property, and `<fast_tier>` is never spliced (I-2).
154
155
  // -----------------------------------------------------------------------
155
156
  fast;
157
+ // -----------------------------------------------------------------------
158
+ // Context compaction (context-auto-compaction §3.2.1)
159
+ //
160
+ // ONE FIELD AND THIN FORWARDERS, the `fast` shape one feature later, and for
161
+ // the same budget reason (C-16): every piece of state — the guards, the
162
+ // ladder, the manual queue, the session totals — lives in
163
+ // `compaction/wiring.ts` and `compaction/compactor.ts`.
164
+ //
165
+ // `null` when `compaction.enabled` was false at construction, which is the
166
+ // byte-identity branch: no manager is passed to `new Agent({...})`, nothing is
167
+ // subscribed, no registry is allocated, and the engine loop's checkpoint is one
168
+ // `if` against a field that is genuinely absent (AC-1).
169
+ //
170
+ // CONSTRUCTED ABOVE `new Agent({...})` AND ATTACHED BELOW IT (C-12 / P0-2).
171
+ // The port has to be inside the Agent's constructor argument and the wiring has
172
+ // to subscribe to the Agent, and this class cannot do both in one step.
173
+ // -----------------------------------------------------------------------
174
+ compaction;
156
175
  /**
157
176
  * Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
158
177
  * Cleared at the next `agent_start`.
@@ -217,6 +236,11 @@ export class AgentController {
217
236
  this.teamRuntime = this.teamRegistered
218
237
  ? new TeamRuntime({
219
238
  getConfig: () => this.config,
239
+ // Read LIVE through the wiring, exactly as `resolveTier` below is: the
240
+ // closure is only ever called from inside a dispatch, and
241
+ // `childFactory()` re-reads `compaction.subagents` on every call so a
242
+ // settings-screen edit reaches the next dispatch.
243
+ contextManagerFor: (req) => this.compaction?.childFactory()?.(req),
220
244
  // Read LIVE through the wiring, which does not exist yet — the closure
221
245
  // is only ever called from inside a dispatch, long after construction.
222
246
  resolveTier: (tier) => this.fast
@@ -379,6 +403,25 @@ export class AgentController {
379
403
  modelId: config.model,
380
404
  ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
381
405
  };
406
+ // --- Context compaction, part 1: BEFORE the Agent ----------------------
407
+ //
408
+ // It needs no agent reference to EXIST — only live config, the key
409
+ // resolvers, the model-info lookup and a notifier — which is exactly what
410
+ // makes the C-12 split work. `getMessages` / `getSystemPrompt` are LAZY for
411
+ // the same reason: `this.agent` is assigned on the very next statement, and
412
+ // they are only ever called from inside an event handler.
413
+ this.compaction = config.compaction.enabled
414
+ ? new CompactionWiring({
415
+ getConfig: () => this.config,
416
+ hasKey: (id) => this.hasApiKey(id),
417
+ getApiKey: (id) => this.resolveKey(id),
418
+ getModelInfoFor: (ref) => this.getModelInfoFor(ref),
419
+ isPricedModel: (ref) => this.isPricedModel(ref),
420
+ getMessages: () => this.agent.state.messages,
421
+ getSystemPrompt: () => this.agent.state.systemPrompt,
422
+ notify: (level, text) => deps.notify?.(level, text),
423
+ })
424
+ : null;
382
425
  this.agent = new Agent({
383
426
  systemPrompt: this.composeSystemPrompt(),
384
427
  model,
@@ -389,6 +432,13 @@ export class AgentController {
389
432
  // without rebuilding the Agent — the closure reads the current config.
390
433
  getApiKey: (id) => this.resolveKey(id),
391
434
  maxTokens: config.maxTokens,
435
+ // SPREAD CONDITIONALLY, never `contextManager: this.compaction?.manager()`.
436
+ // With the feature off the options bag has no such key, so the loop's gate
437
+ // tests a field that is genuinely `undefined` rather than a property
438
+ // holding `undefined` — which is what makes AC-1's byte-identity claim
439
+ // provable by construction rather than by inspection. `createBuiltinTools`
440
+ // above spreads `permission` for the identical reason (P1-2 / AC-28).
441
+ ...(this.compaction ? { contextManager: this.compaction.manager() } : {}),
392
442
  timeouts: {
393
443
  // Timeout invariant (R1): idle ≥ tool so long tool runs are not killed.
394
444
  toolTimeout: config.toolTimeoutMs,
@@ -408,6 +458,18 @@ export class AgentController {
408
458
  },
409
459
  },
410
460
  });
461
+ // --- Context compaction, part 2: AFTER the Agent exists -----------------
462
+ //
463
+ // `attach` is what lets the wiring see `compaction_end` — the ENGINE's
464
+ // `applied` verdict, which the compactor cannot know because validation
465
+ // happens after `compact()` returns — and `agent_start`, which resets the
466
+ // per-run cap. It is idempotent, and `dispose()` unsubscribes.
467
+ //
468
+ // A SEPARATE CALL RATHER THAN A `subscribe` DEP LIKE `FastWiring`'s, and that
469
+ // is the constraint that makes the whole split work: a constructor that
470
+ // subscribed could not run before `new Agent(...)`, and the port has to be
471
+ // inside its constructor argument (C-12 / P0-2).
472
+ this.compaction?.attach((listener) => this.agent.subscribe(listener));
411
473
  // --- Fast tier, part 2: after the Agent exists --------------------------
412
474
  //
413
475
  // `FastWiring` subscribes to the lead's event stream, so it cannot be built
@@ -670,6 +732,11 @@ export class AgentController {
670
732
  // nothing ever clears (RV-1).
671
733
  this.abortRequested = true;
672
734
  this.fast?.abort();
735
+ // The engine also races `ctx.signal` inside `runCompaction`, so this is the
736
+ // same DELIBERATE REDUNDANCY the `teamRuntime` line below records: it makes
737
+ // "Esc cancels an in-flight summarization" a property of this class rather
738
+ // than a consequence of the compactor honouring a signal three modules away.
739
+ this.compaction?.abort();
673
740
  this.agent.abort();
674
741
  // DELIBERATE REDUNDANCY (§3.8). Aborting the lead already aborts the
675
742
  // in-flight `task` call's `ctx.signal`, which the runtime listens to — that
@@ -870,6 +937,7 @@ export class AgentController {
870
937
  dispose() {
871
938
  this.teamRuntime?.dispose();
872
939
  this.fast?.dispose();
940
+ this.compaction?.dispose();
873
941
  }
874
942
  // -----------------------------------------------------------------------
875
943
  // Todo planning (todo-plan-execution §3.6a)
@@ -991,6 +1059,10 @@ export class AgentController {
991
1059
  // another vendor's gateway — and `<fast_tier>` would go on naming the model
992
1060
  // it used to resolve to.
993
1061
  this.fast?.onConfigChanged();
1062
+ // The summarizer resolves from LIVE config too — it may BE the fast tier —
1063
+ // so the snapshot the chip and `/compact status` read has to be refreshed
1064
+ // for the same reason and at the same moments (the RV-3 lesson).
1065
+ this.compaction?.onConfigChanged();
994
1066
  }
995
1067
  setThinkingLevel(level) {
996
1068
  this.config = { ...this.config, thinkingLevel: level };
@@ -1010,6 +1082,10 @@ export class AgentController {
1010
1082
  // Rule 5 of §3.2 reads `apiKeys`, so a key edit is a tier event: this is the
1011
1083
  // path that turns `no_key` back into a working tier without a relaunch.
1012
1084
  this.fast?.onConfigChanged();
1085
+ // The summarizer resolves from LIVE config too — it may BE the fast tier —
1086
+ // so the snapshot the chip and `/compact status` read has to be refreshed
1087
+ // for the same reason and at the same moments (the RV-3 lesson).
1088
+ this.compaction?.onConfigChanged();
1013
1089
  }
1014
1090
  setCwd(cwd) {
1015
1091
  this.cwd = cwd;
@@ -1082,6 +1158,10 @@ export class AgentController {
1082
1158
  }
1083
1159
  clearMessages() {
1084
1160
  this.agent.clearMessages();
1161
+ // INVALIDATION SITE 3 (context-auto-compaction-hardening §3.2.3). The history
1162
+ // was CLEARED, not appended to, so the prefix length the last measurement
1163
+ // covered indexes an array that no longer exists.
1164
+ this.compaction?.onHistoryReplaced();
1085
1165
  // A new conversation has no loaded skills, so the "already loaded earlier
1086
1166
  // in this conversation" hint would otherwise start lying (P2-9).
1087
1167
  this.skills.getRegistry().clearActive();
@@ -1098,6 +1178,10 @@ export class AgentController {
1098
1178
  }
1099
1179
  replaceMessages(messages) {
1100
1180
  this.agent.replaceMessages(messages);
1181
+ // INVALIDATION SITE 4 (§3.2.3) - `/resume`, and any future host-side rewrite.
1182
+ // `estimateAppendedTokens` bounds-checks as well, so a MISSED site costs
1183
+ // accuracy and never correctness; that belt does not make this call optional.
1184
+ this.compaction?.onHistoryReplaced();
1101
1185
  }
1102
1186
  getMessages() {
1103
1187
  return this.agent.state.messages;
@@ -1191,6 +1275,10 @@ export class AgentController {
1191
1275
  // was never registered) nothing has to be rebuilt, because no block is
1192
1276
  // spliced either way.
1193
1277
  this.fast?.onConfigChanged();
1278
+ // The summarizer resolves from LIVE config too — it may BE the fast tier —
1279
+ // so the snapshot the chip and `/compact status` read has to be refreshed
1280
+ // for the same reason and at the same moments (the RV-3 lesson).
1281
+ this.compaction?.onConfigChanged();
1194
1282
  return fast;
1195
1283
  }
1196
1284
  /** Everything `/fast status`, the settings line and the chip read. */
@@ -1209,6 +1297,117 @@ export class AgentController {
1209
1297
  return () => { };
1210
1298
  return this.fast.subscribe(listener);
1211
1299
  }
1300
+ // -----------------------------------------------------------------------
1301
+ // Context compaction (context-auto-compaction §3.2.1) — THIN FORWARDERS
1302
+ // -----------------------------------------------------------------------
1303
+ /** Whether a `ContextManager` exists at all. Fixed at construction. */
1304
+ isCompactionRegistered() {
1305
+ return this.compaction !== null;
1306
+ }
1307
+ /** Whether it will actually fire. Flipped live by `/compact on|off`. */
1308
+ isCompactionEnabled() {
1309
+ return this.compaction?.isEnabled() === true;
1310
+ }
1311
+ /**
1312
+ * Flip the live switch.
1313
+ *
1314
+ * A NO-OP WHEN NOTHING IS REGISTERED, and `/compact` reports that honestly by
1315
+ * checking `isCompactionRegistered()` first — the `/fast on` and `/team on`
1316
+ * shape. `enabled` decides whether the wiring EXISTS, which happens once at
1317
+ * construction and cannot be undone: a session that started with no manager
1318
+ * would have to be given one mid-flight, and the `Agent` has no setter for it
1319
+ * on purpose (§3.2).
1320
+ */
1321
+ setCompactionEnabled(enabled) {
1322
+ this.compaction?.setEnabled(enabled);
1323
+ }
1324
+ getCompactionConfig() {
1325
+ return this.config.compaction;
1326
+ }
1327
+ /**
1328
+ * Adopt a new compaction config for this session.
1329
+ *
1330
+ * THE `onConfigChanged` CALL IS NOT OPTIONAL, for the reason `setFastConfig`
1331
+ * records: a persist without it reports success and changes nothing visible
1332
+ * until relaunch, because the chip and `/compact status` read a snapshot.
1333
+ */
1334
+ setCompactionConfig(patch) {
1335
+ const compaction = clampCompactionConfig({ ...this.config.compaction, ...patch });
1336
+ this.config = { ...this.config, compaction };
1337
+ this.compaction?.onConfigChanged();
1338
+ return compaction;
1339
+ }
1340
+ /** Everything `/compact status`, the settings row and the chip read. */
1341
+ getCompactionSnapshot() {
1342
+ return this.compaction ? this.compaction.snapshot() : offCompactionSnapshot();
1343
+ }
1344
+ /**
1345
+ * The `ModelRef` the next compaction would summarize with, or `null`.
1346
+ *
1347
+ * IT IS CARRIED, NEVER RE-DERIVED, and that is the whole reason it exists. The
1348
+ * summarizer is the fast tier when `useFastTier` is on AND the tier resolves,
1349
+ * and the session's own model otherwise — a combination no caller can
1350
+ * reconstruct from `CliConfig` without re-implementing `resolveFastTier`, and
1351
+ * one that a plausible guess (`fast.enabled ? fast.provider : provider`) gets
1352
+ * WRONG whenever `fast.enabled` is true and `compaction.useFastTier` is false.
1353
+ * Pricing the spend against the wrong provider looks up a model the static
1354
+ * table has never seen, which prices it at `$0.00` — the C-11 / RV-4 lie, on
1355
+ * the one feature that spends money without being asked (AC-15).
1356
+ */
1357
+ getCompactionSummarizerRef() {
1358
+ return this.compaction?.summarizerRef() ?? null;
1359
+ }
1360
+ /**
1361
+ * This run's archive key, or `null` when compaction is unregistered
1362
+ * (context-auto-compaction-hardening §3.5 / W4).
1363
+ *
1364
+ * `/compact history` and `/compact show` scope themselves to it, so a second
1365
+ * `aragon` in another terminal is invisible here rather than interleaved with
1366
+ * this one's compactions.
1367
+ */
1368
+ getCompactionRunId() {
1369
+ return this.compaction?.archiveRunId() ?? null;
1370
+ }
1371
+ /** Ask for a compaction at the next turn boundary (§4.4 / D-17). */
1372
+ queueCompaction(instructions) {
1373
+ this.compaction?.queueManual(instructions);
1374
+ }
1375
+ /**
1376
+ * The IDLE `/compact` path (§4.4 / D-25).
1377
+ *
1378
+ * IT VALIDATES BEFORE IT SPLICES. `CompactionWiring.compactNow` runs
1379
+ * `validateHistory` itself and refuses on failure, because with no loop running
1380
+ * the engine's own gate is unreachable and D-4 declares that gate
1381
+ * unbypassable. `replaceMessages` validates NOTHING.
1382
+ */
1383
+ async compactNow(instructions) {
1384
+ if (!this.compaction)
1385
+ return { ok: false, reason: 'not_registered' };
1386
+ const controller = new AbortController();
1387
+ const outcome = await this.compaction.compactNow({
1388
+ messages: this.agent.state.messages,
1389
+ systemPrompt: this.agent.state.systemPrompt,
1390
+ model: this.agent.state.model,
1391
+ signal: controller.signal,
1392
+ ...(instructions ? { instructions } : {}),
1393
+ });
1394
+ if (!outcome.ok)
1395
+ return { ok: false, reason: outcome.reason };
1396
+ this.agent.replaceMessages(outcome.messages);
1397
+ return { ok: true };
1398
+ }
1399
+ /**
1400
+ * Subscribe to the CLI-local compaction event stream.
1401
+ *
1402
+ * Returns a no-op unsubscribe when compaction was off at construction, so
1403
+ * every caller can subscribe unconditionally — the contract `subscribeTeam`,
1404
+ * `subscribeTodos` and `subscribeFast` already set.
1405
+ */
1406
+ subscribeCompaction(listener) {
1407
+ if (!this.compaction)
1408
+ return () => { };
1409
+ return this.compaction.subscribe(listener);
1410
+ }
1212
1411
  /** List usable tools (name + description) for the /tools command. */
1213
1412
  listTools() {
1214
1413
  return this.tools;
@@ -9,6 +9,7 @@ import type { PreflightResult } from './controller.js';
9
9
  import type { TeamEvent } from '../team/types.js';
10
10
  import type { TodoEvent, TodoSnapshot } from '../todo/types.js';
11
11
  import type { FastEvent } from '../fast/types.js';
12
+ import type { CompactionEvent } from '../compaction/types.js';
12
13
  import { type FollowThroughMode } from '../todo/follow-through.js';
13
14
  /** The minimal controller surface headless mode needs (real or stubbed). */
14
15
  export interface HeadlessController {
@@ -43,6 +44,13 @@ export interface HeadlessController {
43
44
  * byte-identical to today's.
44
45
  */
45
46
  subscribeFast?(listener: (event: FastEvent) => void): () => void;
47
+ /**
48
+ * The CLI-local compaction stream — OPTIONAL, for the identical reason the
49
+ * three above are (context-auto-compaction §4.7). A controller with no
50
+ * `ContextManager` has no compaction events, and `runHeadless` calls it with
51
+ * `?.`, which keeps a `--no-compaction` headless run byte-identical to today's.
52
+ */
53
+ subscribeCompaction?(listener: (event: CompactionEvent) => void): () => void;
46
54
  /**
47
55
  * The current plan — OPTIONAL, for the identical reason the two subscriptions
48
56
  * above are (todo-plan-followthrough §3.6 rule 2). ABSENT MEANS THE
@@ -167,6 +167,52 @@ export async function runHeadless(controller, prompt, options = {}) {
167
167
  total.inputTokens += event.usage.inputTokens;
168
168
  total.outputTokens += event.usage.outputTokens;
169
169
  });
170
+ // Context compaction (context-auto-compaction §4.7).
171
+ //
172
+ // TWO STDERR LINES, AND UNLIKE THE FAST TIER THEY ARE WRITTEN (D-12's
173
+ // reasoning does not carry over). A review is an unrequested second opinion
174
+ // that changes nothing; a compaction PERMANENTLY DROPS MESSAGES from the
175
+ // conversation, and a `-p` run whose middle third vanished with no line
176
+ // anywhere saying so is not a compact run, it is an unexplained one — the same
177
+ // argument the retry lines above make for a three-minute pause.
178
+ //
179
+ // stderr, never stdout: stdout is the machine-readable answer and has to stay
180
+ // parseable by `aragon -p | jq`. Not `--quiet`-gated, for the reason the retry
181
+ // line is not.
182
+ //
183
+ // The summary text is NOT written (D-18): it can be tens of thousands of
184
+ // characters, and no consumer of `-p` asked for it.
185
+ const unsubscribeCompaction = controller.subscribeCompaction?.((event) => {
186
+ if (event.type === 'usage') {
187
+ total.inputTokens += event.usage.inputTokens;
188
+ total.outputTokens += event.usage.outputTokens;
189
+ return;
190
+ }
191
+ if (event.type !== 'compaction_end')
192
+ return;
193
+ const r = event.record;
194
+ if (!r.applied) {
195
+ err.write(`\n[compaction] skipped: ${r.reason ?? 'unknown'}\n`);
196
+ return;
197
+ }
198
+ const secs = (r.durationMs / 1000).toFixed(1);
199
+ const how = r.mode === 'truncated'
200
+ ? ' WITHOUT a summary (summarize failed)'
201
+ : r.mode === 'relieved'
202
+ ? ' by clipping the retained turns (nothing could be dropped)'
203
+ : '';
204
+ err.write(`\n[compaction] ${formatTokens(r.tokensBefore)} -> ${formatTokens(r.tokensAfter)} tokens, ` +
205
+ `${r.messagesBefore} -> ${r.messagesAfter} messages${how} (${secs}s)\n`);
206
+ // ONE MORE LINE, AND ONLY WHEN RELIEF FIRED (§4.5). A clip inside the turns
207
+ // the run is about to continue from is a real data loss, and a `-p` run whose
208
+ // recent tool output was silently shortened is an unexplained run - the same
209
+ // argument the compaction lines above make for a vanished middle third.
210
+ if (r.tailRelief) {
211
+ err.write(`[compaction] clipped ${r.tailRelief.messages} tool ` +
212
+ `${r.tailRelief.messages === 1 ? 'result' : 'results'} in the retained turns ` +
213
+ `(${formatTokens(r.tailRelief.charsRemoved)} chars)\n`);
214
+ }
215
+ });
170
216
  // --- The bounded continuation loop (todo-plan-followthrough §3.6). --------
171
217
  //
172
218
  // THE THREE `unsubscribe` CALLS STAY OUTSIDE IT (rule 4). The loop wraps the
@@ -227,6 +273,7 @@ export async function runHeadless(controller, prompt, options = {}) {
227
273
  unsubscribeTeam?.();
228
274
  unsubscribeTodos?.();
229
275
  unsubscribeFast?.();
276
+ unsubscribeCompaction?.();
230
277
  }
231
278
  // Ensure a trailing newline so piped output is well-formed.
232
279
  if (wroteText)
@@ -14,6 +14,7 @@ import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agen
14
14
  import type { AgentMode } from './agent-mode.js';
15
15
  import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
16
16
  import type { FastReview, FastSnapshot } from '../fast/types.js';
17
+ import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger } from '../compaction/types.js';
17
18
  import type { TodoItem, TodoSnapshot } from '../todo/types.js';
18
19
  import type { RetryPhase } from './retry-view.js';
19
20
  import type { FilePatch } from '../tools/patch.js';
@@ -222,6 +223,57 @@ export type Entry = {
222
223
  turn: number;
223
224
  durationMs?: number;
224
225
  live: boolean;
226
+ }
227
+ /**
228
+ * One context compaction (context-auto-compaction §5.5). ONE CARD PER
229
+ * COMPACTION, rewritten once when the call returns — like the fast card, and
230
+ * unlike the todo card's per-turn checklist.
231
+ *
232
+ * JSON-SERIALIZABLE by construction, so `/save` and `/resume` need no format
233
+ * change.
234
+ *
235
+ * `live: true` is normalized away ON LOAD, in `session/persist.ts`, next to
236
+ * `team.active`, `todo.live`, the retry card's phase and `fast.live`, and for
237
+ * the identical reason (C-8): `Transcript`'s settled boundary is MONOTONIC, so
238
+ * an entry that never settles never reaches `<Static>` and is re-rendered on
239
+ * every frame for the rest of the session.
240
+ *
241
+ * EVERY STRING ON THIS CARD DESCRIBES THE HISTORY, NEVER THE SCREEN (P2-9).
242
+ * `/clear` empties the transcript view but not `messages`, so a compaction can
243
+ * legitimately fire against a visually empty transcript and report
244
+ * "112 -> 9 messages" with nine visible rows.
245
+ */
246
+ | {
247
+ id: string;
248
+ kind: 'compaction';
249
+ /** 1-based within the session, so the card can name itself. */
250
+ index: number;
251
+ trigger: CompactionUiTrigger;
252
+ mode: CompactionMode;
253
+ applied: boolean;
254
+ reason?: string;
255
+ messagesBefore: number;
256
+ messagesAfter: number;
257
+ tokensBefore: number;
258
+ tokensAfter: number;
259
+ summary?: string;
260
+ model: string;
261
+ durationMs?: number;
262
+ /**
263
+ * When the live card opened, so the elapsed counter has an origin
264
+ * (context-auto-compaction-hardening §3.6 / W5).
265
+ *
266
+ * JSON-SERIALIZABLE like every other field here, and harmless on reload:
267
+ * `session/persist.ts` normalizes `live` to false, and a settled card
268
+ * renders `durationMs` rather than an elapsed figure.
269
+ */
270
+ startedAt?: number;
271
+ /** Set when tail relief clipped the retained turns (W2). */
272
+ tailRelief?: {
273
+ messages: number;
274
+ charsRemoved: number;
275
+ };
276
+ live: boolean;
225
277
  };
226
278
  export interface UsageTotal {
227
279
  inputTokens: number;
@@ -296,6 +348,25 @@ export interface ViewState {
296
348
  * what leaves an ordinary session's status bar byte-identical (AC-4).
297
349
  */
298
350
  fast: FastSnapshot | null;
351
+ /**
352
+ * The live compaction projection, or `null` when the feature is off for this
353
+ * session (context-auto-compaction §5.5).
354
+ *
355
+ * NEVER PERSISTED — `entries` is the history and the settled cards are the
356
+ * record. It drives the status chip, the gauge's marks and `/compact status`'s
357
+ * totals. `null` is what leaves an ordinary session's status bar unchanged.
358
+ */
359
+ compaction: CompactionSnapshot | null;
360
+ /**
361
+ * Whether `contextTokens` is currently a DERIVED figure rather than a measured
362
+ * one (§6.2 / P1-11).
363
+ *
364
+ * Set by `compactionEnd`, cleared by the next `turnEnd`. The status bar widens
365
+ * its leading `~` to cover it: today `~` means "the window is a guess", and
366
+ * this makes it also mean "the occupancy is a guess". Honesty about a guess is
367
+ * cheaper than a support thread about a wrong percentage.
368
+ */
369
+ contextTokensEstimated: boolean;
299
370
  seq: number;
300
371
  /** The `kind: 'team'` entry the live dispatch is writing into. */
301
372
  teamEntryId?: string;
@@ -305,6 +376,8 @@ export interface ViewState {
305
376
  retryEntryId?: string;
306
377
  /** The `kind: 'fast'` entry the in-flight review is writing into. */
307
378
  fastEntryId?: string;
379
+ /** The `kind: 'compaction'` entry the in-flight compaction is writing into. */
380
+ compactionEntryId?: string;
308
381
  streamingId?: string;
309
382
  turnProduced: boolean;
310
383
  errorNoticed: boolean;
@@ -497,6 +570,42 @@ export type ViewAction = {
497
570
  } | {
498
571
  type: 'fastTier';
499
572
  snapshot: FastSnapshot;
573
+ } | {
574
+ type: 'compactionStart';
575
+ index: number;
576
+ trigger: CompactionUiTrigger;
577
+ model: string;
578
+ } | {
579
+ type: 'compactionEnd';
580
+ record: CompactionRecord;
581
+ }
582
+ /**
583
+ * Compaction spend.
584
+ *
585
+ * `costDelta` IS PRECOMPUTED BY THE CALLER, exactly as `teamUsage`'s and
586
+ * `fastUsage`'s are: the reducer stays pure and cost-table-free. It matters
587
+ * MORE here than for either of those, because the summarizer may be a
588
+ * different model from the lead AND from the fast tier, so a reducer that
589
+ * computed its own would have to know about three price tables.
590
+ */
591
+ | {
592
+ type: 'compactionUsage';
593
+ usage: TokenUsage;
594
+ costDelta: number;
595
+ } | {
596
+ type: 'compactionSnapshot';
597
+ snapshot: CompactionSnapshot;
598
+ }
599
+ /**
600
+ * Overwrite the gauge with a DERIVED occupancy (§6.2).
601
+ *
602
+ * Separate from `compactionEnd` because the value is not always available at
603
+ * the same instant, and because the flag it sets — `contextTokensEstimated` —
604
+ * has to be clearable by the next `turnEnd` independently.
605
+ */
606
+ | {
607
+ type: 'contextTokensEstimated';
608
+ tokens: number;
500
609
  };
501
610
  export declare const STORED_PREVIEW_CHARS = 8000;
502
611
  export declare const STORED_PREVIEW_LINES = 200;
@@ -532,6 +641,17 @@ export interface PatchSource {
532
641
  * it — every existing test, and headless mode — produces byte-identical actions.
533
642
  */
534
643
  export declare function reduceEvent(event: AgentEvent, cost?: ModelCost, patches?: PatchSource): ViewAction[];
644
+ /**
645
+ * The opening words of the `context_overflow` banner.
646
+ *
647
+ * A SHARED CONSTANT RATHER THAN TWO LITERALS, because `compactionStart` matches
648
+ * on it to rewrite the notice a successful reactive recovery would otherwise
649
+ * leave behind (P1-4). Two spellings that must agree, with nothing checking that
650
+ * they do, is how that rewrite silently stops matching — at which point the red
651
+ * "start over" banner comes back and only on the recovery path, which is the one
652
+ * place nobody re-tests.
653
+ */
654
+ export declare const CONTEXT_OVERFLOW_NOTICE_PREFIX = "Context length exceeded";
535
655
  /** Turn a provider error into a friendly banner, mapping LLMError.errorType. */
536
656
  export declare function formatStreamError(error: Error): string;
537
657
  export declare function viewReducer(state: ViewState, action: ViewAction): ViewState;