@aragon-agent/cli 0.6.13 → 0.6.15

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 (131) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +250 -222
  3. package/dist/agent/controller.d.ts +36 -2
  4. package/dist/agent/controller.js +244 -57
  5. package/dist/agent/model-profile-settings.js +11 -2
  6. package/dist/agent/plan-prompt.d.ts +1 -1
  7. package/dist/agent/plan-prompt.js +4 -3
  8. package/dist/agent/project-guidance-prompt.d.ts +4 -0
  9. package/dist/agent/project-guidance-prompt.js +104 -0
  10. package/dist/agent/reducer.d.ts +2 -0
  11. package/dist/agent/reducer.js +5 -0
  12. package/dist/agent/system-prompt.js +4 -0
  13. package/dist/boot/guard.d.ts +3 -3
  14. package/dist/boot/guard.js +3 -3
  15. package/dist/cli.js +33 -15
  16. package/dist/commands/builtins.js +2 -2
  17. package/dist/compaction/child.d.ts +9 -1
  18. package/dist/compaction/child.js +43 -13
  19. package/dist/compaction/command.js +20 -10
  20. package/dist/compaction/compactor.d.ts +33 -204
  21. package/dist/compaction/compactor.js +311 -723
  22. package/dist/compaction/context-command.js +5 -3
  23. package/dist/compaction/digest.d.ts +15 -51
  24. package/dist/compaction/digest.js +113 -148
  25. package/dist/compaction/limits.d.ts +19 -142
  26. package/dist/compaction/limits.js +20 -147
  27. package/dist/compaction/memory-identity.d.ts +16 -0
  28. package/dist/compaction/memory-identity.js +63 -0
  29. package/dist/compaction/memory-input.d.ts +25 -0
  30. package/dist/compaction/memory-input.js +106 -0
  31. package/dist/compaction/memory-render.d.ts +4 -0
  32. package/dist/compaction/memory-render.js +47 -0
  33. package/dist/compaction/memory-validation.d.ts +21 -0
  34. package/dist/compaction/memory-validation.js +268 -0
  35. package/dist/compaction/memory.d.ts +79 -0
  36. package/dist/compaction/memory.js +118 -0
  37. package/dist/compaction/meter.d.ts +14 -10
  38. package/dist/compaction/meter.js +55 -19
  39. package/dist/compaction/operation.d.ts +18 -0
  40. package/dist/compaction/operation.js +68 -0
  41. package/dist/compaction/pressure.d.ts +6 -20
  42. package/dist/compaction/pressure.js +14 -23
  43. package/dist/compaction/summarize-call.d.ts +3 -16
  44. package/dist/compaction/summarize-call.js +20 -20
  45. package/dist/compaction/summary-prompt.d.ts +29 -66
  46. package/dist/compaction/summary-prompt.js +132 -178
  47. package/dist/compaction/types.d.ts +23 -3
  48. package/dist/compaction/wiring.d.ts +10 -25
  49. package/dist/compaction/wiring.js +60 -67
  50. package/dist/config/load.d.ts +3 -1
  51. package/dist/config/load.js +8 -4
  52. package/dist/config/schema.d.ts +3 -3
  53. package/dist/config/schema.js +1 -1
  54. package/dist/exec/events.d.ts +8 -0
  55. package/dist/exec/index.js +13 -6
  56. package/dist/exec/runner.d.ts +8 -0
  57. package/dist/exec/runner.js +26 -2
  58. package/dist/session/persist.d.ts +3 -0
  59. package/dist/session/persist.js +9 -0
  60. package/dist/session/store.js +2 -0
  61. package/dist/session/validate-session.js +34 -0
  62. package/dist/team/subagent.js +8 -0
  63. package/dist/ui/App.js +221 -126
  64. package/dist/ui/BottomStatusRow.d.ts +5 -10
  65. package/dist/ui/BottomStatusRow.js +7 -11
  66. package/dist/ui/Composer.d.ts +4 -2
  67. package/dist/ui/Composer.js +2 -2
  68. package/dist/ui/Header.d.ts +6 -2
  69. package/dist/ui/Header.js +7 -6
  70. package/dist/ui/Logo.d.ts +3 -5
  71. package/dist/ui/Logo.js +4 -8
  72. package/dist/ui/PromptInput.d.ts +3 -1
  73. package/dist/ui/PromptInput.js +35 -42
  74. package/dist/ui/StatusBar.d.ts +6 -0
  75. package/dist/ui/StatusBar.js +8 -7
  76. package/dist/ui/Transcript.js +1 -1
  77. package/dist/ui/capabilities.d.ts +2 -0
  78. package/dist/ui/capabilities.js +6 -0
  79. package/dist/ui/color-runtime.d.ts +5 -0
  80. package/dist/ui/color-runtime.js +29 -0
  81. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  82. package/dist/ui/entries/CompactionCard.js +7 -5
  83. package/dist/ui/frame-differ.js +27 -0
  84. package/dist/ui/frame-observer.js +18 -5
  85. package/dist/ui/glyphs.js +10 -8
  86. package/dist/ui/ink-runtime.d.ts +3 -0
  87. package/dist/ui/ink-runtime.js +30 -0
  88. package/dist/ui/interaction-copy.d.ts +73 -73
  89. package/dist/ui/interaction-copy.js +30 -38
  90. package/dist/ui/interaction-hints.d.ts +8 -1
  91. package/dist/ui/interaction-hints.js +56 -69
  92. package/dist/ui/layout/AppShell.d.ts +6 -2
  93. package/dist/ui/layout/AppShell.js +2 -2
  94. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  95. package/dist/ui/layout/OverlayFrame.js +23 -14
  96. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  97. package/dist/ui/layout/ScrollViewport.js +10 -2
  98. package/dist/ui/layout/budget.d.ts +25 -26
  99. package/dist/ui/layout/budget.js +29 -22
  100. package/dist/ui/layout/document-layout.d.ts +2 -2
  101. package/dist/ui/layout/document-layout.js +2 -3
  102. package/dist/ui/layout/queue-layout.js +4 -4
  103. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  104. package/dist/ui/layout/status-detail-layout.js +56 -0
  105. package/dist/ui/layout/status-layout.d.ts +19 -2
  106. package/dist/ui/layout/status-layout.js +95 -83
  107. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  108. package/dist/ui/layout/wrap-rows.js +21 -7
  109. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  110. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  111. package/dist/ui/overlays/HelpOverlay.js +9 -2
  112. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  113. package/dist/ui/overlays/ModelPicker.js +44 -10
  114. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  115. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  116. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  117. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  118. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  119. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  120. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  121. package/dist/ui/overlays/QueueOverlay.js +2 -2
  122. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  123. package/dist/ui/overlays/SettingsScreen.js +10 -3
  124. package/dist/ui/render-cache.d.ts +2 -2
  125. package/dist/ui/render-cache.js +4 -3
  126. package/dist/ui/run-metrics.d.ts +23 -0
  127. package/dist/ui/run-metrics.js +27 -0
  128. package/dist/ui/status-feedback.d.ts +33 -0
  129. package/dist/ui/status-feedback.js +36 -0
  130. package/dist/ui/stdout-frame-writer.js +28 -0
  131. package/package.json +2 -2
@@ -53,6 +53,7 @@ export const CONTEXT_METER_TICK_MS = 400;
53
53
  export function toContextUsage(p) {
54
54
  return {
55
55
  occupied: p.occupied,
56
+ ...(p.windowSource ? { windowSource: p.windowSource } : {}),
56
57
  window: p.contextWindow,
57
58
  // FROM `ratio`, NOT FROM A SECOND DIVISION. `computePressure` already
58
59
  // clamped it to [0, 1] and already decided what a zero window means.
@@ -102,6 +103,13 @@ export class ContextMeter {
102
103
  last = null;
103
104
  /** Whether the history has changed since the last measurement. */
104
105
  dirty = true;
106
+ prefixLast;
107
+ sampledPrompt;
108
+ sampledVersion;
109
+ publishedLength = -1;
110
+ publishedLast;
111
+ publishedPrompt;
112
+ publishedVersion;
105
113
  constructor(deps) {
106
114
  this.deps = deps;
107
115
  }
@@ -127,8 +135,7 @@ export class ContextMeter {
127
135
  // precisely the symptom. `estimateAppendedTokens` bounds-checks too, but it
128
136
  // only zeroes the DELTA and keeps the wrong measured base, so that belt does
129
137
  // not cover this.
130
- if (this.discardStalePrefix())
131
- this.dirty = true;
138
+ this.refreshValidity();
132
139
  if (!this.dirty && this.last)
133
140
  return this.last;
134
141
  return this.measureAndPublish(this.lastUsage);
@@ -153,16 +160,9 @@ export class ContextMeter {
153
160
  // =========================================================================
154
161
  // Writing
155
162
  // =========================================================================
156
- /**
157
- * Measure with the caller's own authoritative usage. FOR A DECISION POINT.
158
- *
159
- * `Compactor.shouldCompact` has a `CompactionProbe.lastUsage` the meter may
160
- * not have seen yet, so it supplies it rather than trusting the field here.
161
- */
162
- measureWith(lastUsage) {
163
- if (this.discardStalePrefix())
164
- this.dirty = true;
165
- return this.measureAndPublish(lastUsage);
163
+ /** Compatibility reads cannot establish or resurrect a provider sample. */
164
+ measureWith(_lastUsage) {
165
+ return this.current();
166
166
  }
167
167
  /**
168
168
  * `turn_end` - the one moment in the whole flow that is FULLY measured.
@@ -179,6 +179,9 @@ export class ContextMeter {
179
179
  this.lastUsage = usage;
180
180
  this.estimateOffset = computeEstimateOffset(usage, messages, systemPrompt);
181
181
  this.measuredPrefixLength = messages.length;
182
+ this.prefixLast = messages[messages.length - 1];
183
+ this.sampledPrompt = systemPrompt;
184
+ this.sampledVersion = this.deps.getRequestVersion?.() ?? 0;
182
185
  this.measureAndPublish(usage);
183
186
  }
184
187
  /**
@@ -198,6 +201,7 @@ export class ContextMeter {
198
201
  onHistorySpliced() {
199
202
  this.lastUsage = undefined;
200
203
  this.measuredPrefixLength = undefined;
204
+ this.prefixLast = undefined;
201
205
  if (this.dirty)
202
206
  return;
203
207
  this.dirty = true;
@@ -217,22 +221,25 @@ export class ContextMeter {
217
221
  onHistoryReplaced() {
218
222
  this.lastUsage = undefined;
219
223
  this.measuredPrefixLength = undefined;
224
+ this.prefixLast = undefined;
220
225
  this.estimateOffset = undefined;
226
+ this.sampledPrompt = undefined;
227
+ this.sampledVersion = undefined;
221
228
  this.dirty = true;
222
229
  this.scheduleTick();
223
230
  }
224
231
  /**
225
- * Only the DENOMINATOR moved (`/model`, a `contextWindow` edit).
232
+ * Only the denominator moved (window override or model metadata refresh).
226
233
  *
227
234
  * DELIBERATELY NOT A RESET. The measured base is still a true statement about
228
235
  * the history, so throwing it away would trade a correct numerator for a whole
229
236
  * history estimate and produce a visible jump on a common operation. The
230
- * numerator's calibration is one turn stale until the next `turn_end`
231
- * refreshes it, which is the trade this method names (RV-13).
237
+ * Changing the model connection or request environment requires the deep
238
+ * invalidation path instead; its measurement is no longer transferable.
232
239
  */
233
240
  onWindowChanged() {
234
241
  this.dirty = true;
235
- this.measureAndPublish(this.lastUsage);
242
+ this.current();
236
243
  }
237
244
  /**
238
245
  * Something was appended. Mark dirty, arrange a throttled publish, MEASURE
@@ -254,7 +261,7 @@ export class ContextMeter {
254
261
  this.timer = null;
255
262
  if (!this.dirty || this.disposed)
256
263
  return;
257
- this.measureAndPublish(this.lastUsage);
264
+ this.current();
258
265
  }, CONTEXT_METER_TICK_MS);
259
266
  // The other half of I-11: an armed tick must not keep the event loop alive.
260
267
  const unrefable = handle;
@@ -352,11 +359,13 @@ export class ContextMeter {
352
359
  discardStalePrefix() {
353
360
  if (this.measuredPrefixLength === undefined)
354
361
  return false;
355
- const length = this.deps.getMessages().length;
356
- if (length >= this.measuredPrefixLength)
362
+ const messages = this.deps.getMessages();
363
+ if (messages.length >= this.measuredPrefixLength &&
364
+ messages[this.measuredPrefixLength - 1] === this.prefixLast)
357
365
  return false;
358
366
  this.lastUsage = undefined;
359
367
  this.measuredPrefixLength = undefined;
368
+ this.prefixLast = undefined;
360
369
  return true;
361
370
  }
362
371
  /**
@@ -366,8 +375,34 @@ export class ContextMeter {
366
375
  * is cached" cannot drift, which is what makes `lastPublished()` a meaningful
367
376
  * answer rather than a stale one.
368
377
  */
378
+ refreshValidity() {
379
+ const messages = this.deps.getMessages();
380
+ const prompt = this.readSystemPrompt();
381
+ const version = this.deps.getRequestVersion?.() ?? 0;
382
+ if (this.sampledPrompt !== undefined &&
383
+ (prompt !== this.sampledPrompt || version !== this.sampledVersion)) {
384
+ this.onHistoryReplaced();
385
+ }
386
+ if (this.discardStalePrefix())
387
+ this.dirty = true;
388
+ const window = this.resolveWindow();
389
+ const last = this.last;
390
+ if (messages.length !== this.publishedLength ||
391
+ messages[messages.length - 1] !== this.publishedLast ||
392
+ prompt !== this.publishedPrompt || version !== this.publishedVersion ||
393
+ window.contextWindow !== last?.contextWindow ||
394
+ window.windowKnown !== last?.windowKnown ||
395
+ window.windowOverridden !== last?.windowOverridden ||
396
+ this.deps.getModelInfo().contextWindowSource !== last?.windowSource)
397
+ this.dirty = true;
398
+ }
369
399
  measureAndPublish(lastUsage) {
370
400
  const pressure = this.compute(lastUsage);
401
+ const messages = this.deps.getMessages();
402
+ this.publishedLength = messages.length;
403
+ this.publishedLast = messages[messages.length - 1];
404
+ this.publishedPrompt = this.readSystemPrompt();
405
+ this.publishedVersion = this.deps.getRequestVersion?.() ?? 0;
371
406
  this.last = pressure;
372
407
  this.dirty = false;
373
408
  this.publish(pressure);
@@ -380,6 +415,7 @@ export class ContextMeter {
380
415
  messages: this.deps.getMessages(),
381
416
  systemPrompt: this.readSystemPrompt(),
382
417
  contextWindow: window.contextWindow,
418
+ windowSource: this.deps.getModelInfo().contextWindowSource,
383
419
  windowKnown: window.windowKnown,
384
420
  windowOverridden: window.windowOverridden,
385
421
  ...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
@@ -0,0 +1,18 @@
1
+ /** Local cancellation and deadline ownership, independent of transport cooperation. */
2
+ export declare class CompactionOperation {
3
+ readonly id: number;
4
+ private readonly parent;
5
+ readonly controller: AbortController;
6
+ readonly deadline: number;
7
+ private settled;
8
+ private reason;
9
+ private readonly onParentAbort;
10
+ constructor(id: number, parent: AbortSignal, timeoutMs: number);
11
+ get signal(): AbortSignal;
12
+ /** Abort is local; a late remote reply never restores ownership. */
13
+ abort(reason?: string): void;
14
+ /** One settlement per operation, including rejected or abandoned candidates. */
15
+ settle(): boolean;
16
+ /** Race every call locally and attach rejection handling even to late promises. */
17
+ run<T>(call: () => Promise<T>, timeoutMs: number): Promise<T>;
18
+ }
@@ -0,0 +1,68 @@
1
+ /** Local cancellation and deadline ownership, independent of transport cooperation. */
2
+ export class CompactionOperation {
3
+ id;
4
+ parent;
5
+ controller = new AbortController();
6
+ deadline;
7
+ settled = false;
8
+ reason = 'aborted';
9
+ onParentAbort = () => this.abort();
10
+ constructor(id, parent, timeoutMs) {
11
+ this.id = id;
12
+ this.parent = parent;
13
+ this.deadline = Date.now() + timeoutMs;
14
+ parent.addEventListener('abort', this.onParentAbort, { once: true });
15
+ if (parent.aborted)
16
+ this.abort();
17
+ }
18
+ get signal() { return this.controller.signal; }
19
+ /** Abort is local; a late remote reply never restores ownership. */
20
+ abort(reason = 'aborted') {
21
+ if (this.signal.aborted)
22
+ return;
23
+ this.reason = reason;
24
+ this.controller.abort();
25
+ }
26
+ /** One settlement per operation, including rejected or abandoned candidates. */
27
+ settle() {
28
+ if (this.settled)
29
+ return false;
30
+ this.settled = true;
31
+ this.parent.removeEventListener('abort', this.onParentAbort);
32
+ this.abort();
33
+ return true;
34
+ }
35
+ /** Race every call locally and attach rejection handling even to late promises. */
36
+ async run(call, timeoutMs) {
37
+ if (this.signal.aborted || this.settled)
38
+ throw new Error(this.reason);
39
+ const remaining = Math.min(timeoutMs, this.deadline - Date.now());
40
+ if (remaining <= 0)
41
+ throw new Error('timeout');
42
+ let timer;
43
+ let onAbort = () => { };
44
+ const cancelled = new Promise((_, reject) => {
45
+ onAbort = () => reject(new Error(this.reason));
46
+ this.signal.addEventListener('abort', onAbort, { once: true });
47
+ if (this.signal.aborted)
48
+ onAbort();
49
+ timer = setTimeout(() => this.abort('timeout'), remaining);
50
+ });
51
+ try {
52
+ if (this.signal.aborted)
53
+ throw new Error(this.reason);
54
+ const remote = Promise.resolve().then(() => {
55
+ if (this.signal.aborted || this.settled)
56
+ throw new Error(this.reason);
57
+ if (Date.now() >= this.deadline)
58
+ throw new Error('timeout');
59
+ return call();
60
+ });
61
+ return await Promise.race([remote, cancelled]);
62
+ }
63
+ finally {
64
+ clearTimeout(timer);
65
+ this.signal.removeEventListener('abort', onAbort);
66
+ }
67
+ }
68
+ }
@@ -50,6 +50,7 @@ export declare function occupiedTokens(usage: TokenUsage): number;
50
50
  */
51
51
  export declare function computeEstimateOffset(usage: TokenUsage, messages: readonly Message[], systemPrompt: string): number;
52
52
  export interface PressureInput {
53
+ windowSource?: Pressure['windowSource'];
53
54
  /** Authoritative usage of the last completed turn, or `undefined` (§3.4.2). */
54
55
  lastUsage?: TokenUsage;
55
56
  messages: readonly Message[];
@@ -118,24 +119,9 @@ export declare function isApproximate(p: Pressure): boolean;
118
119
  * support thread about a wrong percentage.
119
120
  */
120
121
  export declare function computePressure(input: PressureInput): Pressure;
121
- export interface HeadroomRequirement {
122
- /** The session's effective output cap for one call. */
122
+ /** Only actual occupancy authorizes automatic compaction. */
123
+ export declare function shouldCompactAt(p: Pressure, threshold: number): boolean;
124
+ /** Capacity helper retained for the standalone tail-budget API, never a trigger. */
125
+ export declare function requiredHeadroom(req: {
123
126
  maxOutputTokens: number;
124
- }
125
- /**
126
- * The tokens the NEXT request needs on top of the history.
127
- *
128
- * `THINKING_HEADROOM_TOKENS` (4096) and `CONTEXT_SAFETY_MARGIN_TOKENS` (1024) are
129
- * existing core exports and are reused rather than re-spelled.
130
- */
131
- export declare function requiredHeadroom(req: HeadroomRequirement): number;
132
- /**
133
- * The trigger, and the second term is not belt-and-braces (D-12).
134
- *
135
- * 90 % OF A 200 k WINDOW LEAVES 20 k, WHICH IS PLENTY. 90 % OF A 32 k WINDOW
136
- * LEAVES 3.2 k, WHICH IS LESS THAN A SINGLE `max_tokens` OF 8192 - the request is
137
- * already impossible. A pure ratio trigger is therefore correct for the models
138
- * most users run and quietly wrong for the small ones, and "quietly wrong on the
139
- * cheap models" is not a property this product ships.
140
- */
141
- export declare function shouldCompactAt(p: Pressure, threshold: number, req: HeadroomRequirement): boolean;
127
+ }): number;
@@ -123,9 +123,8 @@ export function computePressure(input) {
123
123
  source = 'usage';
124
124
  }
125
125
  else {
126
- // Before the first `turn_end` there is nothing to calibrate against and the
127
- // raw estimate is used - still under-reporting, but `requiredHeadroom` below
128
- // is the belt for that case and it does not depend on the ratio being right.
126
+ // Before the first turn_end there is no calibration. Keep the estimate
127
+ // explicit: output reservations never authorize automatic compaction.
129
128
  const raw = estimatePromptTokens(input.messages, input.systemPrompt);
130
129
  occupied = raw + (input.estimateOffset ?? 0);
131
130
  source = 'estimate';
@@ -133,6 +132,7 @@ export function computePressure(input) {
133
132
  const ratio = window > 0 ? Math.max(0, Math.min(1, occupied / window)) : 0;
134
133
  return {
135
134
  occupied,
135
+ ...(input.windowSource ? { windowSource: input.windowSource } : {}),
136
136
  contextWindow: window,
137
137
  ratio,
138
138
  headroom: Math.max(0, window - occupied),
@@ -146,26 +146,17 @@ export function computePressure(input) {
146
146
  deltaTokens,
147
147
  };
148
148
  }
149
- /**
150
- * The tokens the NEXT request needs on top of the history.
151
- *
152
- * `THINKING_HEADROOM_TOKENS` (4096) and `CONTEXT_SAFETY_MARGIN_TOKENS` (1024) are
153
- * existing core exports and are reused rather than re-spelled.
154
- */
149
+ /** Only actual occupancy authorizes automatic compaction. */
150
+ export function shouldCompactAt(p, threshold) {
151
+ if (!Number.isFinite(p.occupied) || p.occupied < 0)
152
+ return false;
153
+ if (!Number.isFinite(p.contextWindow) || p.contextWindow <= 0)
154
+ return false;
155
+ if (!Number.isFinite(threshold) || threshold < 0.5 || threshold > 0.95)
156
+ return false;
157
+ return p.occupied / p.contextWindow >= threshold;
158
+ }
159
+ /** Capacity helper retained for the standalone tail-budget API, never a trigger. */
155
160
  export function requiredHeadroom(req) {
156
161
  return req.maxOutputTokens + THINKING_HEADROOM_TOKENS + CONTEXT_SAFETY_MARGIN_TOKENS;
157
162
  }
158
- /**
159
- * The trigger, and the second term is not belt-and-braces (D-12).
160
- *
161
- * 90 % OF A 200 k WINDOW LEAVES 20 k, WHICH IS PLENTY. 90 % OF A 32 k WINDOW
162
- * LEAVES 3.2 k, WHICH IS LESS THAN A SINGLE `max_tokens` OF 8192 - the request is
163
- * already impossible. A pure ratio trigger is therefore correct for the models
164
- * most users run and quietly wrong for the small ones, and "quietly wrong on the
165
- * cheap models" is not a property this product ships.
166
- */
167
- export function shouldCompactAt(p, threshold, req) {
168
- if (p.contextWindow <= 0)
169
- return false;
170
- return p.ratio >= threshold || p.headroom < requiredHeadroom(req);
171
- }
@@ -1,19 +1,4 @@
1
- /**
2
- * The bounded summarization call (context-auto-compaction §3.6.4).
3
- *
4
- * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
5
- *
6
- * THIS MODULE DOES NOT OWN AN `AbortController`, AND THAT IS A PROHIBITION
7
- * RATHER THAN AN OVERSIGHT - the identical rule `fast/review-call.ts` records at
8
- * length, and for the identical reason: the retry layer returns SILENTLY on
9
- * abort, so a cancellation and a timeout arrive as byte-identical bare `Error`s
10
- * carrying no `errorType`. Once the controller is private to this module, the
11
- * caller can no longer stamp WHICH of the two happened, and every cancelled
12
- * summarization is misreported as a failure.
13
- *
14
- * So this module RECEIVES the signal and a timeout callback, and returns an
15
- * outcome. It does not create, own, or classify the abort.
16
- */
1
+ /** Bounded summary transport; local cancellation also settles uncooperative providers. */
17
2
  import type { AssistantMessage, LLMRequest, ModelRef, TokenUsage } from '@aragon-agent/core';
18
3
  /** The assistant text of a completion, or `''` for an answer with no text block. */
19
4
  export declare function assistantText(message: AssistantMessage | undefined): string;
@@ -34,6 +19,7 @@ export interface SummarizeRequestParams {
34
19
  digest: string;
35
20
  instructions?: string;
36
21
  hasPriorSummary: boolean;
22
+ validationError?: string;
37
23
  /** Created and owned by the compactor; forwarded into the provider from there. */
38
24
  signal: AbortSignal;
39
25
  now: number;
@@ -57,6 +43,7 @@ export interface SummarizeCallParams extends SummarizeRequestParams {
57
43
  * THE CALLER ABORTS, NOT THIS MODULE. See the file header.
58
44
  */
59
45
  onTimeout(): void;
46
+ onUsage?(usage: TokenUsage): void;
60
47
  }
61
48
  export type SummarizeOutcome = {
62
49
  ok: true;
@@ -1,21 +1,6 @@
1
- /**
2
- * The bounded summarization call (context-auto-compaction §3.6.4).
3
- *
4
- * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
5
- *
6
- * THIS MODULE DOES NOT OWN AN `AbortController`, AND THAT IS A PROHIBITION
7
- * RATHER THAN AN OVERSIGHT - the identical rule `fast/review-call.ts` records at
8
- * length, and for the identical reason: the retry layer returns SILENTLY on
9
- * abort, so a cancellation and a timeout arrive as byte-identical bare `Error`s
10
- * carrying no `errorType`. Once the controller is private to this module, the
11
- * caller can no longer stamp WHICH of the two happened, and every cancelled
12
- * summarization is misreported as a failure.
13
- *
14
- * So this module RECEIVES the signal and a timeout callback, and returns an
15
- * outcome. It does not create, own, or classify the abort.
16
- */
17
1
  import { COMPACTION_LIMITS } from './limits.js';
18
2
  import { buildSummarySystemPrompt, buildSummaryUserMessage } from './summary-prompt.js';
3
+ import { CompactionOperation } from './operation.js';
19
4
  /** The assistant text of a completion, or `''` for an answer with no text block. */
20
5
  export function assistantText(message) {
21
6
  if (!message || !Array.isArray(message.content))
@@ -44,6 +29,10 @@ export function accumulateUsage(total, next) {
44
29
  return {
45
30
  inputTokens: total.inputTokens + next.inputTokens,
46
31
  outputTokens: total.outputTokens + next.outputTokens,
32
+ ...((total.cacheReadTokens !== undefined || next.cacheReadTokens !== undefined)
33
+ ? { cacheReadTokens: (total.cacheReadTokens ?? 0) + (next.cacheReadTokens ?? 0) } : {}),
34
+ ...((total.cacheWriteTokens !== undefined || next.cacheWriteTokens !== undefined)
35
+ ? { cacheWriteTokens: (total.cacheWriteTokens ?? 0) + (next.cacheWriteTokens ?? 0) } : {}),
47
36
  };
48
37
  }
49
38
  /**
@@ -59,7 +48,8 @@ export function buildSummarizeRequest(params) {
59
48
  maxChars: COMPACTION_LIMITS.summaryMaxChars,
60
49
  ...(params.instructions ? { instructions: params.instructions } : {}),
61
50
  hasPriorSummary: params.hasPriorSummary,
62
- }),
51
+ }) + (params.validationError
52
+ ? `\nPrevious delta rejected: ${params.validationError.slice(0, 200)}. Correct that error.` : ''),
63
53
  messages: [buildSummaryUserMessage(params.digest, params.now)],
64
54
  maxTokens: COMPACTION_LIMITS.summaryOutputTokens,
65
55
  // PAIRED WITH `temperature: 0` ON PURPOSE: the Anthropic adapter deletes
@@ -72,15 +62,25 @@ export function buildSummarizeRequest(params) {
72
62
  }
73
63
  /** Send one summarization and bound it in wall clock. */
74
64
  export async function runSummarizeCall(params) {
75
- const timer = setTimeout(params.onTimeout, COMPACTION_LIMITS.callTimeoutMs);
65
+ const operation = new CompactionOperation(0, params.signal, COMPACTION_LIMITS.callTimeoutMs);
66
+ let charged = false;
76
67
  try {
77
- const message = await params.complete(params.ref.providerId, buildSummarizeRequest(params));
68
+ const message = await operation.run(async () => {
69
+ const result = await params.complete(params.ref.providerId, buildSummarizeRequest({ ...params, signal: operation.signal }));
70
+ if (result.usage && !charged) {
71
+ charged = true;
72
+ params.onUsage?.(result.usage);
73
+ }
74
+ return result;
75
+ }, COMPACTION_LIMITS.callTimeoutMs);
78
76
  return { ok: true, text: assistantText(message), usage: usageOf(message) };
79
77
  }
80
78
  catch (error) {
79
+ if (errorText(error) === 'timeout')
80
+ params.onTimeout();
81
81
  return { ok: false, error };
82
82
  }
83
83
  finally {
84
- clearTimeout(timer);
84
+ operation.settle();
85
85
  }
86
86
  }
@@ -1,85 +1,48 @@
1
- /**
2
- * The summarizer's system prompt and the `<compacted_context>` splice
3
- * (context-auto-compaction §3.6.4 / §3.6.5).
4
- *
5
- * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
6
- *
7
- * PURE. Every input arrives as an argument, so the block a session produced can
8
- * be reconstructed from a bug report with one call.
9
- */
1
+ /** Strict v2 framing and summarizer instructions. Text shape never grants authority. */
10
2
  import type { Message, UserMessage } from '@aragon-agent/core';
11
- /**
12
- * The section skeleton the summary must fill.
13
- *
14
- * THE HEADINGS ARE PART OF THE CONTRACT, because the next model to read them is
15
- * an LLM: a summary with predictable sections can be scanned for "what is still
16
- * open" without re-reading the whole thing.
17
- */
3
+ import { type CompactionMemory } from './memory.js';
4
+ import { type CompactionIdentity } from './memory-identity.js';
5
+ /** Build a strict delta-only prompt; throws on an oversized manual focus. */
18
6
  export declare function buildSummarySystemPrompt(opts: {
19
7
  maxChars: number;
20
- /** Free text from `/compact <instructions>`, appended as `## Focus`. */
21
8
  instructions?: string;
22
- /** True when the digest opens with a `PRIOR SUMMARY` section. */
23
9
  hasPriorSummary: boolean;
24
10
  }): string;
25
- /** The user message the summarization call sends: the digest, and nothing else. */
11
+ /** Wrap a digest for the summary request without changing its body. */
26
12
  export declare function buildSummaryUserMessage(digest: string, now: number): UserMessage;
27
13
  export type AnchorShape = 'tagged' | 'verbatim';
28
14
  export interface AnchorInfo {
29
- /** The message to place first, verbatim in the history. */
30
- message: Message;
15
+ message: UserMessage;
31
16
  shape: AnchorShape;
32
17
  }
33
- /**
34
- * Recognize an anchor this feature produced on a previous pass.
35
- *
36
- * STRUCTURAL, NOT LENGTH-DEPENDENT (D-21 / P1-9). v1 of the design identified
37
- * the anchor as "the first `user` message of the conversation" and chose between
38
- * two shapes by length. Neither rule composes across repeated compactions,
39
- * because after the first pass the first user message IS the compacted block:
40
- * the merged shape promotes a SUMMARY to "the user's original task", and the
41
- * standalone shape keeps the block as an anchor AND re-digests it. Neither
42
- * failure raises anything, and both get worse every pass.
43
- */
44
- export declare function isTaggedAnchor(message: Message): boolean;
45
- /**
46
- * Build (or carry through) the leading anchor.
47
- *
48
- * ALWAYS ITS OWN LEADING MESSAGE, AND ALWAYS STRUCTURALLY IDENTIFIABLE. A
49
- * summarizer that is 90 % faithful is fine for the middle of a conversation and
50
- * catastrophic for the sentence that says what the user wanted (D-6).
51
- */
18
+ /** Preserve the first original user Message, including all parts and timestamp. */
52
19
  export declare function buildAnchor(messages: readonly Message[]): AnchorInfo | null;
53
- /**
54
- * How many leading messages are ALREADY structurally preserved, and must be
55
- * carried through untouched rather than digested.
56
- *
57
- * This is `planCompaction`'s `protectedPrefix` (§3.6.2). The HOST computes it
58
- * because it is the host that knows what an `<original_task>` message is; core
59
- * stays free of any knowledge of the block format.
60
- */
61
- export declare function countProtectedPrefix(messages: readonly Message[]): number;
62
- /** Read `generation="N"` off a prior block, so the next one can say `N + 1`. */
63
- export declare function readGeneration(messages: readonly Message[]): number;
20
+ /** Legacy syntax helper only; callers must independently establish provenance. */
21
+ export declare function isTaggedAnchor(message: Message): boolean;
22
+ /** Count only an adopted or restored verified prefix; tags alone protect nothing. */
23
+ export declare function countProtectedPrefix(messages: readonly Message[], identity?: CompactionIdentity): number;
24
+ /** Read the adopted generation; never continue a user-supplied block's authority. */
25
+ export declare function readGeneration(messages: readonly Message[], identity?: CompactionIdentity): number;
64
26
  export interface BlockInput {
65
- summary: string;
27
+ memory?: CompactionMemory;
28
+ /** Compatibility argument accepts only a complete validated v2 JSON body. */
29
+ summary?: string;
66
30
  replaced: number;
67
31
  turns: number;
68
32
  anchor: AnchorShape;
69
33
  generation: number;
70
- /** True for a `mode: 'truncated'` splice — the model is entitled to know. */
71
- truncated: boolean;
34
+ truncated?: boolean;
35
+ timestamp?: number;
72
36
  }
73
- /** The body used when the ladder ran out and there is no summary (§3.7 rung 3). */
74
- export declare function truncationBody(replaced: number): string;
75
- /**
76
- * The spliced block.
77
- *
78
- * A `user` MESSAGE, NEVER A FABRICATED ASSISTANT ONE (D-5). An assistant message
79
- * claiming the model said something it did not is a lie the model will then act
80
- * on, and it corrupts every subsequent turn. The XML tag is the same disclosure
81
- * device `<fast_review>` and `<skill>` already use in this codebase, and the
82
- * instruction line is what stops the model reading the block as the user ASKING
83
- * for a summary.
84
- */
37
+ /** Build a complete v2 block; rejects free text, truncation and mismatched metadata. */
85
38
  export declare function buildCompactedBlock(input: BlockInput): UserMessage;
39
+ export interface ParsedCompactedBlock {
40
+ memory: CompactionMemory;
41
+ anchor: AnchorShape;
42
+ replaced: number;
43
+ turns: number;
44
+ }
45
+ /** Parse exact v2 syntax and schema. This does not authenticate a history prefix. */
46
+ export declare function parseCompactedBlock(message: Message): ParsedCompactedBlock;
47
+ /** Retained legacy display helper; v2 never uses it to delete history. */
48
+ export declare function truncationBody(replaced: number): string;