@aragon-agent/cli 0.6.3 → 0.6.7

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 (138) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +203 -13
  3. package/dist/agent/controller.d.ts +158 -2
  4. package/dist/agent/controller.js +400 -1
  5. package/dist/agent/reducer.d.ts +102 -19
  6. package/dist/agent/reducer.js +162 -71
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +2 -0
  9. package/dist/agent/usage.d.ts +34 -0
  10. package/dist/agent/usage.js +43 -0
  11. package/dist/cli.js +132 -23
  12. package/dist/commands/builtins.js +101 -1
  13. package/dist/compaction/child.js +5 -0
  14. package/dist/compaction/command.js +14 -1
  15. package/dist/compaction/compactor.d.ts +94 -24
  16. package/dist/compaction/compactor.js +247 -98
  17. package/dist/compaction/context-command.d.ts +33 -0
  18. package/dist/compaction/context-command.js +166 -0
  19. package/dist/compaction/meter.d.ts +251 -0
  20. package/dist/compaction/meter.js +428 -0
  21. package/dist/compaction/pressure.d.ts +6 -0
  22. package/dist/compaction/pressure.js +4 -0
  23. package/dist/compaction/types.d.ts +77 -0
  24. package/dist/compaction/wiring.d.ts +56 -0
  25. package/dist/compaction/wiring.js +107 -25
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.d.ts +1 -1
  28. package/dist/config/env.js +63 -2
  29. package/dist/config/load.d.ts +28 -0
  30. package/dist/config/load.js +83 -1
  31. package/dist/config/schema.d.ts +129 -0
  32. package/dist/config/schema.js +69 -0
  33. package/dist/config/store.js +47 -1
  34. package/dist/exec/events.d.ts +138 -1
  35. package/dist/exec/events.js +42 -0
  36. package/dist/exec/index.js +43 -6
  37. package/dist/exec/progress-reporter.d.ts +23 -0
  38. package/dist/exec/progress-reporter.js +88 -0
  39. package/dist/exec/runner.d.ts +34 -0
  40. package/dist/exec/runner.js +137 -25
  41. package/dist/fast/reviewer.d.ts +19 -1
  42. package/dist/fast/reviewer.js +22 -0
  43. package/dist/fast/types.d.ts +15 -0
  44. package/dist/fast/wiring.d.ts +10 -1
  45. package/dist/fast/wiring.js +18 -1
  46. package/dist/input/limits.d.ts +72 -0
  47. package/dist/input/limits.js +63 -0
  48. package/dist/input/paste-parse.d.ts +71 -0
  49. package/dist/input/paste-parse.js +95 -0
  50. package/dist/input/stdin-filter.d.ts +81 -0
  51. package/dist/input/stdin-filter.js +379 -0
  52. package/dist/logging/install.d.ts +2 -0
  53. package/dist/logging/install.js +53 -1
  54. package/dist/proc/classify.d.ts +52 -0
  55. package/dist/proc/classify.js +143 -0
  56. package/dist/proc/kill-tree.d.ts +42 -0
  57. package/dist/proc/kill-tree.js +119 -0
  58. package/dist/proc/limits.d.ts +84 -0
  59. package/dist/proc/limits.js +84 -0
  60. package/dist/proc/log-ring.d.ts +68 -0
  61. package/dist/proc/log-ring.js +102 -0
  62. package/dist/proc/prompt.d.ts +24 -0
  63. package/dist/proc/prompt.js +49 -0
  64. package/dist/proc/readiness.d.ts +102 -0
  65. package/dist/proc/readiness.js +219 -0
  66. package/dist/proc/supervisor.d.ts +103 -0
  67. package/dist/proc/supervisor.js +465 -0
  68. package/dist/proc/types.d.ts +175 -0
  69. package/dist/proc/types.js +15 -0
  70. package/dist/session/persist.js +36 -1
  71. package/dist/todo/limits.d.ts +4 -17
  72. package/dist/todo/limits.js +4 -17
  73. package/dist/todo/panel-rows.d.ts +8 -6
  74. package/dist/todo/panel-rows.js +19 -6
  75. package/dist/tools/bash-tool.d.ts +51 -2
  76. package/dist/tools/bash-tool.js +321 -51
  77. package/dist/tools/fs-tools.d.ts +17 -0
  78. package/dist/tools/index.d.ts +55 -2
  79. package/dist/tools/index.js +39 -1
  80. package/dist/tools/proc-tools.d.ts +25 -0
  81. package/dist/tools/proc-tools.js +102 -0
  82. package/dist/ui/App.d.ts +15 -2
  83. package/dist/ui/App.js +330 -69
  84. package/dist/ui/AutocompletePopup.d.ts +3 -1
  85. package/dist/ui/AutocompletePopup.js +9 -11
  86. package/dist/ui/BottomStatusRow.d.ts +1 -1
  87. package/dist/ui/Composer.d.ts +56 -1
  88. package/dist/ui/Composer.js +50 -6
  89. package/dist/ui/PromptInput.d.ts +52 -3
  90. package/dist/ui/PromptInput.js +201 -55
  91. package/dist/ui/StatusBar.d.ts +40 -15
  92. package/dist/ui/StatusBar.js +51 -9
  93. package/dist/ui/TeamPanel.d.ts +3 -1
  94. package/dist/ui/TeamPanel.js +13 -9
  95. package/dist/ui/TodoPanel.d.ts +5 -5
  96. package/dist/ui/TodoPanel.js +56 -24
  97. package/dist/ui/Transcript.js +29 -1
  98. package/dist/ui/UpdateLine.d.ts +1 -1
  99. package/dist/ui/capabilities.js +5 -5
  100. package/dist/ui/composer-limits.d.ts +31 -0
  101. package/dist/ui/composer-limits.js +50 -0
  102. package/dist/ui/composer-rows.d.ts +73 -0
  103. package/dist/ui/composer-rows.js +201 -0
  104. package/dist/ui/editor-reducer.d.ts +37 -0
  105. package/dist/ui/editor-reducer.js +88 -12
  106. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  107. package/dist/ui/entries/ServiceCard.js +116 -0
  108. package/dist/ui/entries/UserEntry.d.ts +17 -3
  109. package/dist/ui/entries/UserEntry.js +34 -5
  110. package/dist/ui/exit-snapshot.d.ts +9 -0
  111. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  112. package/dist/ui/layout/autocomplete.d.ts +18 -0
  113. package/dist/ui/layout/autocomplete.js +26 -0
  114. package/dist/ui/layout/budget.d.ts +34 -6
  115. package/dist/ui/layout/budget.js +32 -7
  116. package/dist/ui/layout/rail.d.ts +4 -53
  117. package/dist/ui/layout/rail.js +10 -62
  118. package/dist/ui/layout/team-panel.d.ts +18 -0
  119. package/dist/ui/layout/team-panel.js +22 -0
  120. package/dist/ui/layout/todo-layout.d.ts +24 -0
  121. package/dist/ui/layout/todo-layout.js +27 -0
  122. package/dist/ui/layout/virtual-window.js +51 -1
  123. package/dist/ui/overlays/HelpOverlay.js +3 -0
  124. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  125. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  126. package/dist/ui/overlays/SettingsScreen.js +14 -1
  127. package/dist/ui/paste-frames.d.ts +87 -0
  128. package/dist/ui/paste-frames.js +112 -0
  129. package/dist/ui/paste-tokens.d.ts +90 -0
  130. package/dist/ui/paste-tokens.js +147 -0
  131. package/dist/ui/screen.d.ts +16 -0
  132. package/dist/ui/screen.js +45 -2
  133. package/dist/ui/selection/selection-controller.d.ts +1 -1
  134. package/dist/ui/transcript-text.js +55 -2
  135. package/dist/ui/use-wheel-routing.d.ts +1 -1
  136. package/package.json +2 -2
  137. package/dist/input/stdin-mouse-filter.d.ts +0 -47
  138. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -14,8 +14,9 @@ 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
+ import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
18
18
  import type { TodoItem, TodoSnapshot } from '../todo/types.js';
19
+ import type { ServiceSnapshot, ServiceStatus } from '../proc/types.js';
19
20
  import type { RetryPhase } from './retry-view.js';
20
21
  import type { FilePatch } from '../tools/patch.js';
21
22
  export type NoticeLevel = 'info' | 'warn' | 'error';
@@ -274,10 +275,70 @@ export type Entry = {
274
275
  charsRemoved: number;
275
276
  };
276
277
  live: boolean;
278
+ }
279
+ /**
280
+ * One supervised background service (background-service-supervision §3.10).
281
+ *
282
+ * JSON-SERIALIZABLE by construction, so `/save` needs no format change.
283
+ *
284
+ * IT IS THE ONE LIVE CARD THAT DOES **NOT** BLOCK THE SETTLED BOUNDARY ONCE IT
285
+ * IS UP (D-11 / P0-4). Every other live clause in
286
+ * `Transcript.tsx::computeSettledCount` is bounded by an OPERATION — a
287
+ * dispatch, a turn, one LLM call, one countdown — and each carries a comment
288
+ * saying that an entry which never settles is re-rendered on every frame for
289
+ * the rest of the session. A service is bounded by the USER'S INTENT: a dev
290
+ * server left up for an hour is the normal case. So only `starting` blocks
291
+ * (bounded by `readyTimeoutMs`), a `ready`/`running` card commits its body to
292
+ * `<Static>`, and a later terminal transition appends a NEW one-row entry
293
+ * rather than trying to rewrite a card `<Static>` has already printed.
294
+ *
295
+ * `terminal: true` marks that second entry. `session/persist.ts` rewrites any
296
+ * restored non-terminal card to `stopped`, next to `team.active`, `todo.live`,
297
+ * the retry phase, `fast.live` and `compaction.live`, and for the identical
298
+ * reason.
299
+ */
300
+ | {
301
+ id: string;
302
+ kind: 'service';
303
+ /** `s1`, `s2`, ... — the id the model quotes back. */
304
+ serviceId: string;
305
+ command: string;
306
+ status: ServiceStatus;
307
+ url?: string;
308
+ port?: number;
309
+ exitCode: number | null;
310
+ startedAt: number;
311
+ readyAt?: number;
312
+ endedAt?: number;
313
+ rows: readonly string[];
314
+ /**
315
+ * MONOTONIC row cursor — the revision term, NEVER `rows.length` (P0-3).
316
+ *
317
+ * The tail is a FIXED-SIZE RING: it evicts its oldest row while appending
318
+ * a new one, so the joined length is unchanged while the content is not.
319
+ * That is verbatim the non-append mutation the `tool` branch's `liveSeq`
320
+ * comment was written for, and a length term would go on matching while
321
+ * the card changed underneath it.
322
+ */
323
+ rowsSeen: number;
324
+ /** The one-row entry a terminal transition appends (D-11). */
325
+ terminal?: boolean;
326
+ /** The stop reported the pid still alive afterwards (R-2 / P2-6). */
327
+ killIncomplete?: boolean;
277
328
  };
278
329
  export interface UsageTotal {
279
330
  inputTokens: number;
280
331
  outputTokens: number;
332
+ /**
333
+ * Cache read hits (context-usage-gauge-accuracy §3.6 / P1-3).
334
+ *
335
+ * `occupiedTokens` counts them and `computeCost` PRICES them, so a session
336
+ * total that omits them makes the status bar's `^` read lower than the `$`
337
+ * next to it - on the same row, from the same turns. That was three units on
338
+ * one line, and this field plus its sibling are what collapse them to one.
339
+ */
340
+ cacheReadTokens: number;
341
+ cacheWriteTokens: number;
281
342
  costUsd: number;
282
343
  }
283
344
  /**
@@ -298,8 +359,20 @@ export interface ViewState {
298
359
  entries: Entry[];
299
360
  status: 'idle' | 'running';
300
361
  usageTotal: UsageTotal;
301
- /** Running input size of the most recent turn vs. the model context window. */
302
- contextTokens: number;
362
+ /**
363
+ * Context occupancy, as published by `AgentController`'s `ContextMeter`.
364
+ *
365
+ * ITS ONLY WRITER IS `case 'contextUsage'` (I-1), and that is the structural
366
+ * half of the P0-1 fix. It replaced `contextTokens` + `contextTokensEstimated`,
367
+ * which between them had THREE writing branches (`turnEnd`,
368
+ * `contextTokensEstimated`, `resetConversation`) fed by two competing
369
+ * upstreams. Two writers in one synchronous fan-out means the later one wins,
370
+ * and the later one was the stale `snapshot` carrying the PRE-compaction
371
+ * figure - so the bar fell and bounced straight back, every time, with nothing
372
+ * logging a fault. `context-one-writer.test.ts` scans this file to keep it
373
+ * that way.
374
+ */
375
+ context: ContextUsageSnapshot;
303
376
  overlay: Overlay;
304
377
  thinkingVisible: boolean;
305
378
  /** Ephemeral acks; auto-dismissed by App (spec §3.9). */
@@ -357,16 +430,6 @@ export interface ViewState {
357
430
  * totals. `null` is what leaves an ordinary session's status bar unchanged.
358
431
  */
359
432
  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;
370
433
  seq: number;
371
434
  /** The `kind: 'team'` entry the live dispatch is writing into. */
372
435
  teamEntryId?: string;
@@ -597,15 +660,35 @@ export type ViewAction = {
597
660
  snapshot: CompactionSnapshot;
598
661
  }
599
662
  /**
600
- * Overwrite the gauge with a DERIVED occupancy (§6.2).
663
+ * The occupancy reading, from `AgentController.subscribeContextUsage`.
664
+ *
665
+ * THE ONLY ACTION THAT WRITES `state.context`, AND IT IS DISPATCHED FROM
666
+ * EXACTLY ONE PLACE (I-1 / T11). It replaced `contextTokensEstimated`, which
667
+ * `App` fired from two branches of the compaction stream while `turnEnd` wrote
668
+ * the same field from the agent stream - three writers, two upstreams, one
669
+ * number, and the P0-1 bounce as the result.
670
+ */
671
+ | {
672
+ type: 'contextUsage';
673
+ snapshot: ContextUsageSnapshot;
674
+ } | {
675
+ type: 'serviceStart';
676
+ service: ServiceSnapshot;
677
+ } | {
678
+ type: 'serviceUpdate';
679
+ service: ServiceSnapshot;
680
+ }
681
+ /**
682
+ * A service reached a terminal state.
601
683
  *
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.
684
+ * IT APPENDS A NEW ONE-ROW ENTRY rather than rewriting the card (D-11). The
685
+ * card may already have been printed into `<Static>`, and `<Static>` cannot
686
+ * un-print — so any design that rewrites a printed card is wrong whatever it
687
+ * claims. A second event at a second time is also simply more honest.
605
688
  */
606
689
  | {
607
- type: 'contextTokensEstimated';
608
- tokens: number;
690
+ type: 'serviceEnd';
691
+ service: ServiceSnapshot;
609
692
  };
610
693
  export declare const STORED_PREVIEW_CHARS = 8000;
611
694
  export declare const STORED_PREVIEW_LINES = 200;
@@ -11,11 +11,25 @@
11
11
  * synthesizes a generic error notice (a swallowed throw is never a no-op).
12
12
  */
13
13
  import process from 'node:process';
14
- import { occupiedTokens } from '../compaction/pressure.js';
15
- import { computeCost } from './usage.js';
14
+ import { emptyContextUsage } from '../compaction/meter.js';
15
+ import { addUsage, computeCost } from './usage.js';
16
16
  import { ENTRY_LIMITS, appendBounded, entryRetain, trimEntries } from './entry-limits.js';
17
17
  /** Default auto-dismiss window for a toast. */
18
18
  export const DEFAULT_TOAST_TTL_MS = 2500;
19
+ /**
20
+ * The zero session total.
21
+ *
22
+ * ONE LITERAL FOR TWO SITES (`initialViewState` and `resetConversation`). They
23
+ * used to spell the object out twice, which is how `cacheReadTokens` would have
24
+ * been added to one of them and not the other.
25
+ */
26
+ const EMPTY_USAGE_TOTAL = {
27
+ inputTokens: 0,
28
+ outputTokens: 0,
29
+ cacheReadTokens: 0,
30
+ cacheWriteTokens: 0,
31
+ costUsd: 0,
32
+ };
19
33
  /**
20
34
  * The initial view state, optionally seeded from config.
21
35
  *
@@ -33,8 +47,8 @@ export function initialViewState(seed = {}) {
33
47
  return {
34
48
  entries: [],
35
49
  status: 'idle',
36
- usageTotal: { inputTokens: 0, outputTokens: 0, costUsd: 0 },
37
- contextTokens: 0,
50
+ usageTotal: EMPTY_USAGE_TOTAL,
51
+ context: emptyContextUsage(),
38
52
  overlay: null,
39
53
  thinkingVisible: seed.thinkingVisible ?? false,
40
54
  toasts: [],
@@ -48,7 +62,6 @@ export function initialViewState(seed = {}) {
48
62
  retry: null,
49
63
  fast: null,
50
64
  compaction: null,
51
- contextTokensEstimated: false,
52
65
  seq: 0,
53
66
  teamEntryId: undefined,
54
67
  todoEntryId: undefined,
@@ -610,22 +623,12 @@ export function viewReducer(state, action) {
610
623
  ...state,
611
624
  streamingId: undefined,
612
625
  turnProduced: true,
613
- // ONE FUNCTION, SHARED WITH THE COMPACTION TRIGGER (§3.4.1 / R-11 /
614
- // AC-3). The old expression — `inputTokens + outputTokens` — silently
615
- // UNDER-reports for anyone behind a caching gateway, because Anthropic
616
- // reports `input_tokens` EXCLUDING cached tokens and puts them in two
617
- // separate fields. That was a live defect in the number this gauge shows;
618
- // it becomes a much worse one the moment a trigger fires on a different
619
- // number from the one the user is watching.
620
- contextTokens: occupiedTokens(action.usage),
621
- // A MEASUREMENT SUPERSEDES A DERIVATION (§6.2). `compactionEnd` writes an
622
- // estimate so the bar falls immediately; this is where it stops being one.
623
- contextTokensEstimated: false,
624
- usageTotal: {
625
- inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
626
- outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
627
- costUsd: state.usageTotal.costUsd + action.costDelta,
628
- },
626
+ // `contextTokens` IS NOT WRITTEN HERE ANY MORE (I-1). The occupancy this
627
+ // turn produced reaches the view through `ContextMeter`, which subscribes
628
+ // to the same `turn_end` and publishes the measured pressure - window,
629
+ // percentage and approximation markers included, none of which this
630
+ // branch ever had. Two branches writing one number is what P0-1 was.
631
+ usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta),
629
632
  // The turn produced an answer, so whatever it had to retry, it recovered.
630
633
  ...settleRetryCard(state, entries, 'recovered'),
631
634
  };
@@ -763,8 +766,13 @@ export function viewReducer(state, action) {
763
766
  droppedEntries: 0,
764
767
  streamingId: undefined,
765
768
  expandedToolIds: {},
766
- contextTokens: 0,
767
- usageTotal: { inputTokens: 0, outputTokens: 0, costUsd: 0 },
769
+ // `context` IS DELIBERATELY UNTOUCHED (I-1). `builtins.ts` calls
770
+ // `controller.clearMessages()` BEFORE dispatching this, and that marks
771
+ // the meter dirty and schedules a publication; zeroing here would make
772
+ // the final state `0` rather than the meter's real answer - which for an
773
+ // emptied conversation is the system prompt's own couple of percent, not
774
+ // nothing. `usageTotal` DOES reset: that is session spend, not occupancy.
775
+ usageTotal: EMPTY_USAGE_TOTAL,
768
776
  teamEntryId: undefined,
769
777
  // `/reset` clears `messages`, so the belief is gone and the list goes
770
778
  // with it — the other half of I-2. `controller.clearMessages()` clears
@@ -774,10 +782,6 @@ export function viewReducer(state, action) {
774
782
  retryEntryId: undefined,
775
783
  retry: null,
776
784
  compactionEntryId: undefined,
777
- // `contextTokens` is zeroed above, so the flag that says it is a
778
- // DERIVATION has to go with it — otherwise a fresh conversation renders
779
- // `~0%` and the tilde is a lie about a number that is simply zero.
780
- contextTokensEstimated: false,
781
785
  };
782
786
  case 'setOverlay':
783
787
  return { ...state, overlay: action.overlay };
@@ -860,18 +864,11 @@ export function viewReducer(state, action) {
860
864
  }
861
865
  case 'teamUsage':
862
866
  // `usageTotal` AND NOTHING ELSE (§3.9 / AC-15). In particular NOT
863
- // `contextTokens`: that gauge shows the LEAD's context occupancy against
867
+ // `context`: that gauge shows the LEAD's context occupancy against
864
868
  // the model's window, and folding five children into it would read 180%
865
869
  // on a perfectly healthy session. Child spend is real money and has to
866
870
  // appear in the cost readout; it is not the lead's context.
867
- return {
868
- ...state,
869
- usageTotal: {
870
- inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
871
- outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
872
- costUsd: state.usageTotal.costUsd + action.costDelta,
873
- },
874
- };
871
+ return { ...state, usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta) };
875
872
  case 'teamEnd': {
876
873
  const { outcome } = action;
877
874
  const entries = state.teamEntryId
@@ -991,20 +988,19 @@ export function viewReducer(state, action) {
991
988
  }
992
989
  case 'fastUsage':
993
990
  // `usageTotal` AND NOTHING ELSE, exactly as `teamUsage` (§3.6 / AC-18). In
994
- // particular NOT `contextTokens`: that gauge shows the LEAD's context
991
+ // particular NOT `context`: that gauge shows the LEAD's context
995
992
  // occupancy against the lead model's window, and a review is a separate
996
993
  // conversation with a different model entirely.
997
- return {
998
- ...state,
999
- usageTotal: {
1000
- inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
1001
- outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
1002
- costUsd: state.usageTotal.costUsd + action.costDelta,
1003
- },
1004
- };
994
+ return { ...state, usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta) };
1005
995
  case 'fastTier':
1006
996
  return { ...state, fast: action.snapshot };
1007
997
  // --- Context compaction (context-auto-compaction §5.5) ----------------
998
+ // NOT A TOTAL FUNCTION OVER COMPACTION ATTEMPTS (quiet-noop §3.1). A
999
+ // `pressure` compaction that declines before calling a model or changing the
1000
+ // history dispatches NOTHING - no `compactionStart`, no `compactionEnd`, no
1001
+ // entry - exactly as a checkpoint below the threshold dispatches nothing.
1002
+ // Counting cards here counts compactions that RAN, never checkpoints that
1003
+ // fired; `/compact status` and the diagnostic log are where the attempts are.
1008
1004
  case 'compactionStart': {
1009
1005
  const { id, seq } = nextId(state);
1010
1006
  let entries = state.entries;
@@ -1122,40 +1118,41 @@ export function viewReducer(state, action) {
1122
1118
  }
1123
1119
  case 'compactionUsage':
1124
1120
  // `usageTotal` AND NOTHING ELSE, exactly as `teamUsage` and `fastUsage`. In
1125
- // particular NOT `contextTokens`: that gauge shows the LEAD's context
1121
+ // particular NOT `context`: that gauge shows the LEAD's context
1126
1122
  // occupancy against the lead model's window, and a summarization is a
1127
1123
  // separate conversation with a possibly different model entirely.
1128
- return {
1129
- ...state,
1130
- usageTotal: {
1131
- inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
1132
- outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
1133
- costUsd: state.usageTotal.costUsd + action.costDelta,
1134
- },
1135
- };
1124
+ return { ...state, usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta) };
1136
1125
  case 'compactionSnapshot':
1137
1126
  return { ...state, compaction: action.snapshot };
1138
1127
  /**
1139
- * THE GAUGE MUST FALL IMMEDIATELY (§6.2 / AC-5).
1128
+ * THE ONE WRITER OF `state.context` (I-1).
1140
1129
  *
1141
- * `contextTokens` is otherwise only written at `turnEnd`, so a compaction
1142
- * that took the session from 92 % to 24 % would leave the bar at 92 % until
1143
- * the next turn completes — and the user would reasonably conclude that
1144
- * nothing happened. This is a small change and it is the difference between
1145
- * the feature feeling real and feeling broken.
1130
+ * THE IDENTITY SHORT-CIRCUIT BELOW IS LOAD-BEARING, NOT A MICRO-OPTIMISATION
1131
+ * (§5.2 / R-3). The meter publishes on a 400 ms tick throughout a turn and
1132
+ * most of those ticks measure the same number - the history did not change
1133
+ * between two tool calls of the same size, or the percentage rounded to the
1134
+ * same integer. Returning the SAME state object lets React skip the whole
1135
+ * subtree; returning a fresh one would make a permanently visible row into a
1136
+ * 2.5 Hz re-render source and walk the render governor up its ladder for no
1137
+ * visible change at all.
1146
1138
  *
1147
- * The value arrives ALREADY CALIBRATED from `App` (§6.2 / P1-11): it replaces
1148
- * a MEASUREMENT that included the tool schemas, and core's raw estimator
1149
- * never counts them, so writing the raw figure would make the bar fall too
1150
- * far and then visibly climb again on the next `turn_end` for no reason the
1151
- * user can see.
1139
+ * `ContextUsageSnapshot` carries no timestamp for exactly this reason: a
1140
+ * field that moves every tick would make this comparison never hold.
1152
1141
  */
1153
- case 'contextTokensEstimated':
1154
- return {
1155
- ...state,
1156
- contextTokens: Math.max(0, Math.round(action.tokens)),
1157
- contextTokensEstimated: true,
1158
- };
1142
+ case 'contextUsage': {
1143
+ const prev = state.context;
1144
+ const next = action.snapshot;
1145
+ if (prev.occupied === next.occupied &&
1146
+ prev.window === next.window &&
1147
+ prev.pct === next.pct &&
1148
+ prev.source === next.source &&
1149
+ prev.deltaTokens === next.deltaTokens &&
1150
+ prev.windowKnown === next.windowKnown &&
1151
+ prev.windowOverridden === next.windowOverridden) {
1152
+ return state;
1153
+ }
1154
+ return { ...state, context: next };
1155
+ }
1159
1156
  // --- API retry (llm-api-retry-backoff §6.4) ---------------------------
1160
1157
  case 'retryScheduled': {
1161
1158
  const snapshot = {
@@ -1317,10 +1314,104 @@ export function viewReducer(state, action) {
1317
1314
  fastEntryId: undefined,
1318
1315
  };
1319
1316
  }
1317
+ // --- Background services (§3.10 / D-11) -----------------------------
1318
+ case 'serviceStart': {
1319
+ const { id, seq } = nextId(state);
1320
+ const entry = { id, ...serviceFields(action.service) };
1321
+ const appended = appendEntry(state, state.entries, entry);
1322
+ return {
1323
+ ...state,
1324
+ seq,
1325
+ entries: appended.entries,
1326
+ droppedEntries: appended.droppedEntries,
1327
+ };
1328
+ }
1329
+ /**
1330
+ * Rewrite the LIVE card in place.
1331
+ *
1332
+ * SAFE ONLY BECAUSE OF D-11'S OTHER HALF: a `ready` / `running` card has
1333
+ * reached `<Static>` and cannot be re-printed, so `serviceEnd` appends a new
1334
+ * entry instead of routing through here. What this handles is the window in
1335
+ * which the card is still in the live region — `starting`, and the one
1336
+ * transition out of it — plus the output tail while it is there.
1337
+ *
1338
+ * The lookup is by `serviceId` and SKIPS terminal entries, so a stray update
1339
+ * cannot rewrite the one-row record `serviceEnd` appended.
1340
+ */
1341
+ case 'serviceUpdate': {
1342
+ const target = findServiceEntryId(state, action.service.id);
1343
+ if (!target)
1344
+ return state;
1345
+ return {
1346
+ ...state,
1347
+ entries: mapEntry(state.entries, target, (e) => e.kind === 'service' ? { ...e, ...serviceFields(action.service) } : e),
1348
+ };
1349
+ }
1350
+ /**
1351
+ * A service reached a terminal state.
1352
+ *
1353
+ * TWO WRITES, AND BOTH ARE REQUIRED. The live card is settled in place so
1354
+ * `computeSettledCount` can move past it and `estimateEntryRows` stops
1355
+ * charging for a tail nothing will add to; the NEW one-row entry is what the
1356
+ * user actually reads, because the card may already be printed into
1357
+ * `<Static>` and `<Static>` cannot un-print (D-11 / R-13).
1358
+ */
1359
+ case 'serviceEnd': {
1360
+ const target = findServiceEntryId(state, action.service.id);
1361
+ const settled = target
1362
+ ? mapEntry(state.entries, target, (e) => e.kind === 'service' ? { ...e, ...serviceFields(action.service) } : e)
1363
+ : state.entries;
1364
+ const { id, seq } = nextId(state);
1365
+ const appended = appendEntry(state, settled, {
1366
+ id,
1367
+ ...serviceFields(action.service),
1368
+ terminal: true,
1369
+ });
1370
+ return {
1371
+ ...state,
1372
+ seq,
1373
+ entries: appended.entries,
1374
+ droppedEntries: appended.droppedEntries,
1375
+ };
1376
+ }
1320
1377
  default:
1321
1378
  return state;
1322
1379
  }
1323
1380
  }
1381
+ /**
1382
+ * Project a supervisor snapshot onto the entry's own fields.
1383
+ *
1384
+ * ONE PROJECTION FOR ALL THREE ACTIONS, so a field that reaches the card through
1385
+ * `serviceStart` can never fail to reach it through `serviceUpdate` — the kind of
1386
+ * asymmetry that produces a card frozen at its first values with nothing
1387
+ * reporting it.
1388
+ */
1389
+ function serviceFields(service) {
1390
+ return {
1391
+ kind: 'service',
1392
+ serviceId: service.id,
1393
+ command: service.command,
1394
+ status: service.status,
1395
+ ...(service.url !== undefined ? { url: service.url } : {}),
1396
+ ...(service.port !== undefined ? { port: service.port } : {}),
1397
+ exitCode: service.exitCode,
1398
+ startedAt: service.startedAt,
1399
+ ...(service.readyAt !== undefined ? { readyAt: service.readyAt } : {}),
1400
+ ...(service.endedAt !== undefined ? { endedAt: service.endedAt } : {}),
1401
+ rows: service.rows,
1402
+ rowsSeen: service.rowsSeen,
1403
+ ...(service.killIncomplete ? { killIncomplete: true } : {}),
1404
+ };
1405
+ }
1406
+ /** The LIVE card for a service id, newest first; terminal records are skipped. */
1407
+ function findServiceEntryId(state, serviceId) {
1408
+ for (let i = state.entries.length - 1; i >= 0; i -= 1) {
1409
+ const e = state.entries[i];
1410
+ if (e && e.kind === 'service' && e.serviceId === serviceId && !e.terminal)
1411
+ return e.id;
1412
+ }
1413
+ return undefined;
1414
+ }
1324
1415
  function safeStringify(value) {
1325
1416
  try {
1326
1417
  return JSON.stringify(value);
@@ -88,6 +88,20 @@ export interface SystemPromptParams {
88
88
  * for a fixed `tools` array. Every existing caller passes nothing.
89
89
  */
90
90
  appendSystemPrompt?: string;
91
+ /**
92
+ * The rendered `<background_services>` block, or `''`
93
+ * (background-service-supervision §5.2).
94
+ *
95
+ * Spliced conditionally exactly as `skillsBlock`, the plan block, `teamBlock`,
96
+ * `todoBlock` and `fastBlock` are, which is what keeps I-2 true:
97
+ * `bash.background: false` produces the pre-feature prompt BYTE FOR BYTE for a
98
+ * fixed tool array.
99
+ *
100
+ * A CHILD'S PROMPT NEVER CARRIES IT: subagents build tools from the same
101
+ * factory but are given no supervisor, so telling one about `bash_output` /
102
+ * `bash_kill` would describe tools it does not have.
103
+ */
104
+ backgroundBlock?: string;
91
105
  }
92
106
  /** Describe the active shell so the model does not emit incompatible syntax. */
93
107
  export declare function describeShell(): {
@@ -41,6 +41,7 @@ export function buildSystemPrompt(params) {
41
41
  const subagentBlock = params.subagentBlock ?? '';
42
42
  const todoBlock = params.todoBlock ?? '';
43
43
  const fastBlock = params.fastBlock ?? '';
44
+ const backgroundBlock = params.backgroundBlock ?? '';
44
45
  const appended = (params.appendSystemPrompt ?? '').trim();
45
46
  return [
46
47
  'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
@@ -59,6 +60,7 @@ export function buildSystemPrompt(params) {
59
60
  ...(subagentBlock ? ['', subagentBlock] : []),
60
61
  ...(todoBlock ? ['', todoBlock] : []),
61
62
  ...(fastBlock ? ['', fastBlock] : []),
63
+ ...(backgroundBlock ? ['', backgroundBlock] : []),
62
64
  '',
63
65
  'Operating guidance:',
64
66
  '- Prefer reading files and inspecting the workspace before making changes.',
@@ -3,8 +3,42 @@
3
3
  * (which is expressed in USD per 1M tokens). See spec §6.2 status bar.
4
4
  */
5
5
  import type { ModelCost, TokenUsage } from '@aragon-agent/core';
6
+ import type { UsageTotal } from './reducer.js';
6
7
  /** Compute the USD cost of a single `TokenUsage` given a model's cost table. */
7
8
  export declare function computeCost(usage: TokenUsage, cost?: ModelCost): number;
9
+ /**
10
+ * Fold one `TokenUsage` into a running session total
11
+ * (context-usage-gauge-accuracy §3.6 / W5).
12
+ *
13
+ * THE ONE ADDITION SITE, and collapsing four hand-written copies into it is the
14
+ * point rather than a tidy-up. The reducer had the same three lines in four
15
+ * branches - `turnEnd`, `teamUsage`, `fastUsage`, `compactionUsage` - and all
16
+ * four silently omitted the cache terms, so the status bar's `^` read LOWER than
17
+ * the `$` beside it on any session behind a caching gateway. A fifth source
18
+ * added later would have made the identical omission; now it cannot.
19
+ *
20
+ * `costDelta` IS PRECOMPUTED BY THE CALLER, exactly as the four branches already
21
+ * required: the summarizer and the fast tier may be different models with
22
+ * different price tables, and a reducer that priced its own would have to know
23
+ * about three of them.
24
+ */
25
+ export declare function addUsage(total: UsageTotal, usage: TokenUsage, costDelta: number): UsageTotal;
26
+ /**
27
+ * The number the status bar's `^` shows: the PROMPT side of the session total.
28
+ *
29
+ * DELIBERATELY THE SAME TERMS AS `occupiedTokens` MINUS `outputTokens`
30
+ * (`compaction/pressure.ts`). Anthropic reports `input_tokens` EXCLUDING cached
31
+ * tokens and puts them in two separate fields, so `inputTokens` alone
32
+ * under-reports for anyone behind a caching gateway - while `computeCost` above
33
+ * has always priced all three. That is the "three units on one row" defect
34
+ * (P1-3): the percentage counted cache, the dollars counted cache, and the arrow
35
+ * did not.
36
+ *
37
+ * `$` IS STILL NOT THE SAME TOTAL, and the distinction is worth keeping straight
38
+ * (RV-14): `computeCost` also prices OUTPUT tokens. What this function asserts is
39
+ * narrower and exact - both cache terms are counted, on both sides.
40
+ */
41
+ export declare function promptTokensOf(total: UsageTotal): number;
8
42
  /** Format a USD amount for the status bar (`$0.0000`, or `$1.23` when large). */
9
43
  export declare function formatCost(usd: number): string;
10
44
  /** Format a token count compactly (`1.2k`, `3.4M`). */
@@ -12,6 +12,49 @@ export function computeCost(usage, cost) {
12
12
  perM(usage.cacheReadTokens ?? 0, cost.cacheRead) +
13
13
  perM(usage.cacheWriteTokens ?? 0, cost.cacheWrite));
14
14
  }
15
+ /**
16
+ * Fold one `TokenUsage` into a running session total
17
+ * (context-usage-gauge-accuracy §3.6 / W5).
18
+ *
19
+ * THE ONE ADDITION SITE, and collapsing four hand-written copies into it is the
20
+ * point rather than a tidy-up. The reducer had the same three lines in four
21
+ * branches - `turnEnd`, `teamUsage`, `fastUsage`, `compactionUsage` - and all
22
+ * four silently omitted the cache terms, so the status bar's `^` read LOWER than
23
+ * the `$` beside it on any session behind a caching gateway. A fifth source
24
+ * added later would have made the identical omission; now it cannot.
25
+ *
26
+ * `costDelta` IS PRECOMPUTED BY THE CALLER, exactly as the four branches already
27
+ * required: the summarizer and the fast tier may be different models with
28
+ * different price tables, and a reducer that priced its own would have to know
29
+ * about three of them.
30
+ */
31
+ export function addUsage(total, usage, costDelta) {
32
+ return {
33
+ inputTokens: total.inputTokens + usage.inputTokens,
34
+ outputTokens: total.outputTokens + usage.outputTokens,
35
+ cacheReadTokens: total.cacheReadTokens + (usage.cacheReadTokens ?? 0),
36
+ cacheWriteTokens: total.cacheWriteTokens + (usage.cacheWriteTokens ?? 0),
37
+ costUsd: total.costUsd + costDelta,
38
+ };
39
+ }
40
+ /**
41
+ * The number the status bar's `^` shows: the PROMPT side of the session total.
42
+ *
43
+ * DELIBERATELY THE SAME TERMS AS `occupiedTokens` MINUS `outputTokens`
44
+ * (`compaction/pressure.ts`). Anthropic reports `input_tokens` EXCLUDING cached
45
+ * tokens and puts them in two separate fields, so `inputTokens` alone
46
+ * under-reports for anyone behind a caching gateway - while `computeCost` above
47
+ * has always priced all three. That is the "three units on one row" defect
48
+ * (P1-3): the percentage counted cache, the dollars counted cache, and the arrow
49
+ * did not.
50
+ *
51
+ * `$` IS STILL NOT THE SAME TOTAL, and the distinction is worth keeping straight
52
+ * (RV-14): `computeCost` also prices OUTPUT tokens. What this function asserts is
53
+ * narrower and exact - both cache terms are counted, on both sides.
54
+ */
55
+ export function promptTokensOf(total) {
56
+ return total.inputTokens + total.cacheReadTokens + total.cacheWriteTokens;
57
+ }
15
58
  /** Format a USD amount for the status bar (`$0.0000`, or `$1.23` when large). */
16
59
  export function formatCost(usd) {
17
60
  if (usd <= 0)