@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.
- package/CHANGELOG.md +142 -0
- package/README.md +203 -13
- package/dist/agent/controller.d.ts +158 -2
- package/dist/agent/controller.js +400 -1
- package/dist/agent/reducer.d.ts +102 -19
- package/dist/agent/reducer.js +162 -71
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +132 -23
- package/dist/commands/builtins.js +101 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +94 -24
- package/dist/compaction/compactor.js +247 -98
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +77 -0
- package/dist/compaction/wiring.d.ts +56 -0
- package/dist/compaction/wiring.js +107 -25
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +63 -2
- package/dist/config/load.d.ts +28 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +129 -0
- package/dist/config/schema.js +69 -0
- package/dist/config/store.js +47 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -1
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +2 -0
- package/dist/logging/install.js +53 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +36 -1
- package/dist/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/App.d.ts +15 -2
- package/dist/ui/App.js +330 -69
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +56 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +52 -3
- package/dist/ui/PromptInput.js +201 -55
- package/dist/ui/StatusBar.d.ts +40 -15
- package/dist/ui/StatusBar.js +51 -9
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/Transcript.js +29 -1
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/exit-snapshot.d.ts +9 -0
- package/dist/ui/layout/ScrollViewport.d.ts +1 -1
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +24 -0
- package/dist/ui/layout/todo-layout.js +27 -0
- package/dist/ui/layout/virtual-window.js +51 -1
- package/dist/ui/overlays/HelpOverlay.js +3 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.js +14 -1
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +16 -0
- package/dist/ui/screen.js +45 -2
- package/dist/ui/selection/selection-controller.d.ts +1 -1
- package/dist/ui/transcript-text.js +55 -2
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -47
- package/dist/input/stdin-mouse-filter.js +0 -153
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ContextMeter` - the ONE owner of "how full is the context right now"
|
|
3
|
+
* (context-usage-gauge-accuracy §3.2 / W1).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts::inScope`). If this module is ever moved to a new tree, the
|
|
7
|
+
* scanner's hardcoded directory list must be widened IN THE SAME COMMIT - a
|
|
8
|
+
* scanner that silently stops scanning is worse than no scanner, and this
|
|
9
|
+
* package has paid for that edit eleven times (I-7).
|
|
10
|
+
*
|
|
11
|
+
* IT LIVES HERE BUT IT IS NOT PART OF COMPACTION, and that is deliberate. The
|
|
12
|
+
* whole point of this class is that occupancy is measurable when compaction is
|
|
13
|
+
* OFF: `AgentController` builds a `CompactionWiring` only when
|
|
14
|
+
* `compaction.enabled` is true, so a measurement owned by the compactor is
|
|
15
|
+
* unreachable for exactly the sessions that most need a working gauge. It sits
|
|
16
|
+
* in this directory because `pressure.ts` - the arithmetic it drives - already
|
|
17
|
+
* does, and because moving it would cost a scanner edit for no gain (R-10).
|
|
18
|
+
*
|
|
19
|
+
* THE STATE MACHINE IN ONE PARAGRAPH. Every mutation of the history marks the
|
|
20
|
+
* meter DIRTY; every read that finds it dirty re-measures SYNCHRONOUSLY. That
|
|
21
|
+
* ordering is what removes the subscription-order dependency in the APPEND
|
|
22
|
+
* direction: it does not matter whether this class or the compaction wiring
|
|
23
|
+
* hears `tool_execution_end` first, because whoever reads gets a number that
|
|
24
|
+
* includes the appended message either way (I-2). It does NOT remove the
|
|
25
|
+
* dependency in the REPLACE direction - a splice does not announce itself to a
|
|
26
|
+
* listener that has not run yet - which is why the splice marks this dirty from
|
|
27
|
+
* `CompactionWiring.settlePending`'s own synchronous code (I-9), and why
|
|
28
|
+
* `current()` carries a structural belt for a missed invalidation site.
|
|
29
|
+
*/
|
|
30
|
+
import { computeEstimateOffset, computePressure } from './pressure.js';
|
|
31
|
+
/**
|
|
32
|
+
* How long a scheduled publish waits.
|
|
33
|
+
*
|
|
34
|
+
* IT MUST BE A DELAY, NOT ZERO, AND NOT A SYNCHRONOUS MEASUREMENT (I-3). The
|
|
35
|
+
* loop emits `tool_execution_end` BEFORE it pushes the `tool_result` message, so
|
|
36
|
+
* anything that measures inside that event measures a history missing the single
|
|
37
|
+
* largest message of the turn - silently, and every turn. A `setTimeout` is
|
|
38
|
+
* necessarily later than that synchronous push.
|
|
39
|
+
*
|
|
40
|
+
* 400 ms also caps the publish rate at 2.5 Hz, which together with the reducer's
|
|
41
|
+
* identity short-circuit keeps a permanently visible row from becoming a
|
|
42
|
+
* re-render source (R-3).
|
|
43
|
+
*/
|
|
44
|
+
export const CONTEXT_METER_TICK_MS = 400;
|
|
45
|
+
/**
|
|
46
|
+
* The projection `ViewState` and `/context` read (§4.1).
|
|
47
|
+
*
|
|
48
|
+
* THE ONLY CONVERTER between `Pressure` and `ContextUsageSnapshot`. Two of them
|
|
49
|
+
* would eventually round the percentage differently, and "the status bar says 43
|
|
50
|
+
* and `/context` says 44" is the exact class of disagreement this whole feature
|
|
51
|
+
* exists to remove.
|
|
52
|
+
*/
|
|
53
|
+
export function toContextUsage(p) {
|
|
54
|
+
return {
|
|
55
|
+
occupied: p.occupied,
|
|
56
|
+
window: p.contextWindow,
|
|
57
|
+
// FROM `ratio`, NOT FROM A SECOND DIVISION. `computePressure` already
|
|
58
|
+
// clamped it to [0, 1] and already decided what a zero window means.
|
|
59
|
+
pct: Math.max(0, Math.min(100, Math.round(p.ratio * 100))),
|
|
60
|
+
source: p.source,
|
|
61
|
+
deltaTokens: p.deltaTokens,
|
|
62
|
+
windowKnown: p.windowKnown,
|
|
63
|
+
windowOverridden: p.windowOverridden,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The reading a meter with nothing measured yet reports. */
|
|
67
|
+
export function emptyContextUsage() {
|
|
68
|
+
return {
|
|
69
|
+
occupied: 0,
|
|
70
|
+
window: 0,
|
|
71
|
+
pct: 0,
|
|
72
|
+
source: 'estimate',
|
|
73
|
+
deltaTokens: 0,
|
|
74
|
+
windowKnown: false,
|
|
75
|
+
windowOverridden: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export class ContextMeter {
|
|
79
|
+
deps;
|
|
80
|
+
listeners = new Set();
|
|
81
|
+
unsubscribe = null;
|
|
82
|
+
timer = null;
|
|
83
|
+
disposed = false;
|
|
84
|
+
/**
|
|
85
|
+
* The authoritative usage of the last completed turn.
|
|
86
|
+
*
|
|
87
|
+
* Its presence is what selects `computePressure`'s MEASURED branch, so
|
|
88
|
+
* dropping it is how every reset degrades to an estimate rather than to a
|
|
89
|
+
* number describing a history that no longer exists.
|
|
90
|
+
*/
|
|
91
|
+
lastUsage;
|
|
92
|
+
/**
|
|
93
|
+
* The systematic difference between what `estimatePromptTokens` counts and
|
|
94
|
+
* what the provider bills (D-23) - chiefly the tool schemas.
|
|
95
|
+
*
|
|
96
|
+
* SURVIVES A SPLICE AND NOT A SESSION CHANGE (I-8). See `onHistorySpliced`.
|
|
97
|
+
*/
|
|
98
|
+
estimateOffset;
|
|
99
|
+
/** `messages.length` at the `turn_end` that produced `lastUsage`. */
|
|
100
|
+
measuredPrefixLength;
|
|
101
|
+
/** The last pressure PUBLISHED. Never re-derived on read - see `lastPublished`. */
|
|
102
|
+
last = null;
|
|
103
|
+
/** Whether the history has changed since the last measurement. */
|
|
104
|
+
dirty = true;
|
|
105
|
+
constructor(deps) {
|
|
106
|
+
this.deps = deps;
|
|
107
|
+
}
|
|
108
|
+
// =========================================================================
|
|
109
|
+
// Reading
|
|
110
|
+
// =========================================================================
|
|
111
|
+
/**
|
|
112
|
+
* Occupancy right now. FOR THE SCREEN.
|
|
113
|
+
*
|
|
114
|
+
* Re-measures synchronously when dirty (I-2), and publishes when it does - the
|
|
115
|
+
* gauge has to fall in the SAME FRAME as the compaction that caused it (AC-1),
|
|
116
|
+
* and the caller that triggers this re-measurement is `CompactionWiring`'s own
|
|
117
|
+
* synchronous `snapshot()` inside `settlePending`. Leaving the publish to the
|
|
118
|
+
* scheduled tick instead would be worse than slow: the tick fires only when
|
|
119
|
+
* `dirty`, and this call is what clears it.
|
|
120
|
+
*
|
|
121
|
+
* NEVER THROWS. A meter that can throw is a status bar that can take the
|
|
122
|
+
* process down.
|
|
123
|
+
*/
|
|
124
|
+
current() {
|
|
125
|
+
// THE BELT (I-9), AND IT RUNS EVEN WHEN NOT DIRTY. The case it exists for is
|
|
126
|
+
// a splice that NOBODY announced, and in that case a missing `dirty` flag is
|
|
127
|
+
// precisely the symptom. `estimateAppendedTokens` bounds-checks too, but it
|
|
128
|
+
// only zeroes the DELTA and keeps the wrong measured base, so that belt does
|
|
129
|
+
// not cover this.
|
|
130
|
+
if (this.discardStalePrefix())
|
|
131
|
+
this.dirty = true;
|
|
132
|
+
if (!this.dirty && this.last)
|
|
133
|
+
return this.last;
|
|
134
|
+
return this.measureAndPublish(this.lastUsage);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The last pressure this meter PUBLISHED. FOR THE BOOKS. Never re-measures.
|
|
138
|
+
*
|
|
139
|
+
* `Compactor.lastMeasured()` forwards here and not to `current()`, and the
|
|
140
|
+
* distinction is load-bearing (I-10): `compactor.ts`'s `tokensBefore` is the
|
|
141
|
+
* sole upstream of `tokensReclaimed`, the card's "reclaimed N tokens" line and
|
|
142
|
+
* `/compact status`'s session total. It wants the occupancy BEFORE the splice.
|
|
143
|
+
* Wired to `current()` it would re-measure the already-spliced history, report
|
|
144
|
+
* `reclaimed 0`, and render that number without a single log line about it.
|
|
145
|
+
*/
|
|
146
|
+
lastPublished() {
|
|
147
|
+
return this.last;
|
|
148
|
+
}
|
|
149
|
+
/** The published projection, or a zeroed one before the first measurement. */
|
|
150
|
+
currentUsage() {
|
|
151
|
+
return toContextUsage(this.current());
|
|
152
|
+
}
|
|
153
|
+
// =========================================================================
|
|
154
|
+
// Writing
|
|
155
|
+
// =========================================================================
|
|
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);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* `turn_end` - the one moment in the whole flow that is FULLY measured.
|
|
169
|
+
*
|
|
170
|
+
* The loop emits it BEFORE pushing the assistant message, so the history right
|
|
171
|
+
* now is exactly the history the reported `usage` was billed for: the offset
|
|
172
|
+
* and the prefix length describe the same array by construction, and
|
|
173
|
+
* `deltaTokens` is 0. Published immediately for that reason - there is no
|
|
174
|
+
* better number to wait for.
|
|
175
|
+
*/
|
|
176
|
+
onTurnEnd(usage) {
|
|
177
|
+
const messages = this.deps.getMessages();
|
|
178
|
+
const systemPrompt = this.readSystemPrompt();
|
|
179
|
+
this.lastUsage = usage;
|
|
180
|
+
this.estimateOffset = computeEstimateOffset(usage, messages, systemPrompt);
|
|
181
|
+
this.measuredPrefixLength = messages.length;
|
|
182
|
+
this.measureAndPublish(usage);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A compaction spliced the history (I-8, the SHALLOW reset).
|
|
186
|
+
*
|
|
187
|
+
* `estimateOffset` IS KEPT, and that is the whole reason this method is not
|
|
188
|
+
* `onHistoryReplaced`. The offset describes "what this toolset costs on this
|
|
189
|
+
* model above what the estimator counts"; a splice changes neither the toolset
|
|
190
|
+
* nor the model, so it is still valid - and it is the only calibration the
|
|
191
|
+
* estimate branch has until the next `turn_end`. Dropping it makes the gauge
|
|
192
|
+
* fall several thousand tokens too far and then visibly climb back on the next
|
|
193
|
+
* turn, for no reason the user can see (I-5).
|
|
194
|
+
*
|
|
195
|
+
* IDEMPOTENT. Both `settlePending` (the authority, I-9) and this meter's own
|
|
196
|
+
* `compaction_end` handler reach it, and the second one must cost nothing.
|
|
197
|
+
*/
|
|
198
|
+
onHistorySpliced() {
|
|
199
|
+
this.lastUsage = undefined;
|
|
200
|
+
this.measuredPrefixLength = undefined;
|
|
201
|
+
if (this.dirty)
|
|
202
|
+
return;
|
|
203
|
+
this.dirty = true;
|
|
204
|
+
this.scheduleTick();
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The history came from somewhere else entirely (I-8, the DEEP reset):
|
|
208
|
+
* `/clear`, `/reset`, `/resume`.
|
|
209
|
+
*
|
|
210
|
+
* `estimateOffset` GOES TOO, unlike the shallow reset above. `/resume` can
|
|
211
|
+
* change the model in the same breath (`builtins.ts`'s restore path does), and
|
|
212
|
+
* an offset carried across that is a calibration constant applied to something
|
|
213
|
+
* it never calibrated. The first reading afterwards is therefore a bare
|
|
214
|
+
* estimate - biased LOW, and corrected by the very next `turn_end`. Biased low
|
|
215
|
+
* and self-correcting beats biased high and permanent.
|
|
216
|
+
*/
|
|
217
|
+
onHistoryReplaced() {
|
|
218
|
+
this.lastUsage = undefined;
|
|
219
|
+
this.measuredPrefixLength = undefined;
|
|
220
|
+
this.estimateOffset = undefined;
|
|
221
|
+
this.dirty = true;
|
|
222
|
+
this.scheduleTick();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Only the DENOMINATOR moved (`/model`, a `contextWindow` edit).
|
|
226
|
+
*
|
|
227
|
+
* DELIBERATELY NOT A RESET. The measured base is still a true statement about
|
|
228
|
+
* the history, so throwing it away would trade a correct numerator for a whole
|
|
229
|
+
* 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).
|
|
232
|
+
*/
|
|
233
|
+
onWindowChanged() {
|
|
234
|
+
this.dirty = true;
|
|
235
|
+
this.measureAndPublish(this.lastUsage);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Something was appended. Mark dirty, arrange a throttled publish, MEASURE
|
|
239
|
+
* NOTHING (I-3).
|
|
240
|
+
*/
|
|
241
|
+
scheduleTick() {
|
|
242
|
+
this.dirty = true;
|
|
243
|
+
// NO SUBSCRIBERS, NO TIMER (I-11). `AgentController` is shared with
|
|
244
|
+
// `aragon exec`, where nothing reads this number; a headless run must not
|
|
245
|
+
// pay a timer per tool call, and a pending tick must never be able to hold
|
|
246
|
+
// the process open for an extra 400 ms. `current()` is unaffected - it
|
|
247
|
+
// measures on demand, so `wiring.snapshot()` and `/context` still work in a
|
|
248
|
+
// host with no UI.
|
|
249
|
+
if (this.listeners.size === 0)
|
|
250
|
+
return;
|
|
251
|
+
if (this.timer !== null || this.disposed)
|
|
252
|
+
return;
|
|
253
|
+
const handle = setTimeout(() => {
|
|
254
|
+
this.timer = null;
|
|
255
|
+
if (!this.dirty || this.disposed)
|
|
256
|
+
return;
|
|
257
|
+
this.measureAndPublish(this.lastUsage);
|
|
258
|
+
}, CONTEXT_METER_TICK_MS);
|
|
259
|
+
// The other half of I-11: an armed tick must not keep the event loop alive.
|
|
260
|
+
const unrefable = handle;
|
|
261
|
+
if (typeof unrefable.unref === 'function')
|
|
262
|
+
unrefable.unref();
|
|
263
|
+
this.timer = handle;
|
|
264
|
+
}
|
|
265
|
+
// =========================================================================
|
|
266
|
+
// Plumbing
|
|
267
|
+
// =========================================================================
|
|
268
|
+
/** Publish to this meter's own subscribers. Returns an unsubscribe function. */
|
|
269
|
+
subscribe(listener) {
|
|
270
|
+
this.listeners.add(listener);
|
|
271
|
+
return () => {
|
|
272
|
+
this.listeners.delete(listener);
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Subscribe to the lead agent's stream. IDEMPOTENT.
|
|
277
|
+
*
|
|
278
|
+
* THIS TABLE IS NOT THE SOURCE OF COMPACTION CORRECTNESS (I-9). The splice is
|
|
279
|
+
* marked dirty by `CompactionWiring.settlePending` inside its own synchronous
|
|
280
|
+
* code, BEFORE it emits either event, so a reader is correct regardless of
|
|
281
|
+
* which listener the emitter reaches first. The `compaction_end` row below is
|
|
282
|
+
* a second, idempotent path that serves the manual `/compact` route and any
|
|
283
|
+
* future splicer.
|
|
284
|
+
*/
|
|
285
|
+
attach(subscribe) {
|
|
286
|
+
if (this.unsubscribe)
|
|
287
|
+
return;
|
|
288
|
+
this.unsubscribe = subscribe((event) => this.onAgentEvent(event));
|
|
289
|
+
}
|
|
290
|
+
dispose() {
|
|
291
|
+
this.disposed = true;
|
|
292
|
+
if (this.timer !== null) {
|
|
293
|
+
clearTimeout(this.timer);
|
|
294
|
+
this.timer = null;
|
|
295
|
+
}
|
|
296
|
+
this.unsubscribe?.();
|
|
297
|
+
this.unsubscribe = null;
|
|
298
|
+
this.listeners.clear();
|
|
299
|
+
}
|
|
300
|
+
// =========================================================================
|
|
301
|
+
// Internals
|
|
302
|
+
// =========================================================================
|
|
303
|
+
onAgentEvent(event) {
|
|
304
|
+
switch (event.type) {
|
|
305
|
+
case 'turn_end':
|
|
306
|
+
this.onTurnEnd(event.usage);
|
|
307
|
+
return;
|
|
308
|
+
case 'tool_execution_end':
|
|
309
|
+
// DELAYED, NEVER SYNCHRONOUS. See `CONTEXT_METER_TICK_MS` (I-3).
|
|
310
|
+
this.scheduleTick();
|
|
311
|
+
return;
|
|
312
|
+
case 'compaction_end':
|
|
313
|
+
// The engine's verdict. `applied` means the history really was replaced.
|
|
314
|
+
if (event.applied)
|
|
315
|
+
this.onHistorySpliced();
|
|
316
|
+
else
|
|
317
|
+
this.scheduleTick();
|
|
318
|
+
return;
|
|
319
|
+
case 'agent_end':
|
|
320
|
+
this.scheduleTick();
|
|
321
|
+
return;
|
|
322
|
+
default:
|
|
323
|
+
// `message_update` is a STREAM event and is deliberately ignored (N-2):
|
|
324
|
+
// the output tokens of an in-flight reply are not knowable until `done`,
|
|
325
|
+
// and guessing them would add a third approximation to a number that
|
|
326
|
+
// already carries two.
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The structural belt for a missed invalidation site (I-9).
|
|
332
|
+
*
|
|
333
|
+
* A recorded prefix that is LONGER than the history it indexes describes an
|
|
334
|
+
* array that no longer exists. Rather than merely zeroing the delta - which is
|
|
335
|
+
* what `estimateAppendedTokens` does, keeping the wrong measured base - this
|
|
336
|
+
* drops the base too and forces the estimate branch.
|
|
337
|
+
*
|
|
338
|
+
* THE COMPARISON IS STRICT, AND THAT BOUNDARY IS LOAD-BEARING (IF-1). The
|
|
339
|
+
* design document wrote `length <= measuredPrefixLength`, which is off by one:
|
|
340
|
+
* `turn_end` is emitted BEFORE the assistant message is pushed, so the instant
|
|
341
|
+
* `onTurnEnd` records the prefix the history is EXACTLY that long. An
|
|
342
|
+
* inclusive test therefore discards a measurement one statement after taking
|
|
343
|
+
* it - but only when this meter's listener runs before the compaction wiring's,
|
|
344
|
+
* because the wiring's `snapshot()` is what reads in that window. That is a
|
|
345
|
+
* silent, subscription-order-dependent fallback to the estimate branch on
|
|
346
|
+
* every single turn, which is the exact class of bug this feature exists to
|
|
347
|
+
* remove. A measurement taken at length N describes every history of length
|
|
348
|
+
* >= N; only a SHORTER one is impossible.
|
|
349
|
+
*
|
|
350
|
+
* Returns whether anything was discarded.
|
|
351
|
+
*/
|
|
352
|
+
discardStalePrefix() {
|
|
353
|
+
if (this.measuredPrefixLength === undefined)
|
|
354
|
+
return false;
|
|
355
|
+
const length = this.deps.getMessages().length;
|
|
356
|
+
if (length >= this.measuredPrefixLength)
|
|
357
|
+
return false;
|
|
358
|
+
this.lastUsage = undefined;
|
|
359
|
+
this.measuredPrefixLength = undefined;
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Measure, cache, clear the dirty flag, publish. The ONE write path.
|
|
364
|
+
*
|
|
365
|
+
* Every public mutator funnels through here so "what was published" and "what
|
|
366
|
+
* is cached" cannot drift, which is what makes `lastPublished()` a meaningful
|
|
367
|
+
* answer rather than a stale one.
|
|
368
|
+
*/
|
|
369
|
+
measureAndPublish(lastUsage) {
|
|
370
|
+
const pressure = this.compute(lastUsage);
|
|
371
|
+
this.last = pressure;
|
|
372
|
+
this.dirty = false;
|
|
373
|
+
this.publish(pressure);
|
|
374
|
+
return pressure;
|
|
375
|
+
}
|
|
376
|
+
compute(lastUsage) {
|
|
377
|
+
const window = this.resolveWindow();
|
|
378
|
+
return computePressure({
|
|
379
|
+
...(lastUsage ? { lastUsage } : {}),
|
|
380
|
+
messages: this.deps.getMessages(),
|
|
381
|
+
systemPrompt: this.readSystemPrompt(),
|
|
382
|
+
contextWindow: window.contextWindow,
|
|
383
|
+
windowKnown: window.windowKnown,
|
|
384
|
+
windowOverridden: window.windowOverridden,
|
|
385
|
+
...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
|
|
386
|
+
...(this.measuredPrefixLength !== undefined
|
|
387
|
+
? { measuredPrefixLength: this.measuredPrefixLength }
|
|
388
|
+
: {}),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Where the denominator comes from, and whether it can be trusted.
|
|
393
|
+
*
|
|
394
|
+
* AN OVERRIDE MAKES THE WINDOW KNOWN (I-6): the user asserted it, so the `~`
|
|
395
|
+
* that means "this denominator is a guess" has no business staying on the
|
|
396
|
+
* percentage. `windowOverridden` travels separately so `/context` can still
|
|
397
|
+
* name the source - a hand-typed window that is wrong is exactly the case that
|
|
398
|
+
* most needs to be visible.
|
|
399
|
+
*/
|
|
400
|
+
resolveWindow() {
|
|
401
|
+
const override = this.deps.getWindowOverride();
|
|
402
|
+
if (override !== null && Number.isFinite(override) && override > 0) {
|
|
403
|
+
return { contextWindow: override, windowKnown: true, windowOverridden: true };
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
contextWindow: this.deps.getModelInfo().contextWindow,
|
|
407
|
+
windowKnown: this.deps.isWindowKnown(),
|
|
408
|
+
windowOverridden: false,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
readSystemPrompt() {
|
|
412
|
+
return this.deps.getSystemPrompt();
|
|
413
|
+
}
|
|
414
|
+
publish(pressure) {
|
|
415
|
+
if (this.listeners.size === 0)
|
|
416
|
+
return;
|
|
417
|
+
const usage = toContextUsage(pressure);
|
|
418
|
+
for (const listener of [...this.listeners]) {
|
|
419
|
+
try {
|
|
420
|
+
listener(usage);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// One bad subscriber must not take the run down with it - the rule
|
|
424
|
+
// `CompactionWiring.emit` and `TeamRuntime.emit` both already state.
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -57,6 +57,12 @@ export interface PressureInput {
|
|
|
57
57
|
contextWindow: number;
|
|
58
58
|
/** False when the window is `buildRuntimeModel`'s 128k placeholder. */
|
|
59
59
|
windowKnown: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Whether `contextWindow` came from the user's `contextWindow` override
|
|
62
|
+
* (context-usage-gauge-accuracy I-6). Defaults to `false`, so every existing
|
|
63
|
+
* construction site is unchanged and the arithmetic below is untouched.
|
|
64
|
+
*/
|
|
65
|
+
windowOverridden?: boolean;
|
|
60
66
|
/** Carried from the last `turn_end`; applied to every ESTIMATED figure. */
|
|
61
67
|
estimateOffset?: number;
|
|
62
68
|
/**
|
|
@@ -138,6 +138,10 @@ export function computePressure(input) {
|
|
|
138
138
|
headroom: Math.max(0, window - occupied),
|
|
139
139
|
source,
|
|
140
140
|
windowKnown: input.windowKnown,
|
|
141
|
+
// PURE PASS-THROUGH. This function's arithmetic is unchanged by
|
|
142
|
+
// context-usage-gauge-accuracy; the flag only travels with the number so
|
|
143
|
+
// `/context` can name the denominator's source (I-6).
|
|
144
|
+
windowOverridden: input.windowOverridden ?? false,
|
|
141
145
|
...(input.estimateOffset !== undefined ? { estimateOffset: input.estimateOffset } : {}),
|
|
142
146
|
deltaTokens,
|
|
143
147
|
};
|
|
@@ -60,6 +60,18 @@ export interface Pressure {
|
|
|
60
60
|
source: PressureSource;
|
|
61
61
|
/** False when the window came from `buildRuntimeModel`'s 128k placeholder. */
|
|
62
62
|
windowKnown: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the denominator came from the user's `contextWindow` override
|
|
65
|
+
* (context-usage-gauge-accuracy I-6).
|
|
66
|
+
*
|
|
67
|
+
* SEPARATE FROM `windowKnown` RATHER THAN FOLDED INTO IT. An override makes
|
|
68
|
+
* the window KNOWN - the user asserted it, so the `~` has no business staying
|
|
69
|
+
* on the percentage - but `/context` still has to be able to say WHO supplied
|
|
70
|
+
* the number. Collapsing the two into one boolean makes "found in the model
|
|
71
|
+
* table" and "typed by hand" indistinguishable in the report, and the second
|
|
72
|
+
* one being wrong is exactly the case that most needs to be visible.
|
|
73
|
+
*/
|
|
74
|
+
windowOverridden: boolean;
|
|
63
75
|
/**
|
|
64
76
|
* `occupiedTokens(lastUsage) - estimateOf(the history that produced it)`,
|
|
65
77
|
* clamped to `>= 0`, or `undefined` before the first `turn_end`.
|
|
@@ -89,6 +101,35 @@ export interface Pressure {
|
|
|
89
101
|
*/
|
|
90
102
|
deltaTokens: number;
|
|
91
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The ONE occupancy projection the status bar and `/context` read
|
|
106
|
+
* (context-usage-gauge-accuracy §4.1).
|
|
107
|
+
*
|
|
108
|
+
* DELIBERATELY CARRIES NO TIMESTAMP. Its only consumer is the reducer's identity
|
|
109
|
+
* short-circuit (§5.2), and a field that changes on every tick would make that
|
|
110
|
+
* short-circuit never hold - turning a permanently visible row into a 2.5 Hz
|
|
111
|
+
* re-render source and pushing the render governor up its ladder for nothing.
|
|
112
|
+
*
|
|
113
|
+
* `Pressure` is the compaction subsystem's shape and carries a headroom and a
|
|
114
|
+
* ratio the UI does not want; this is the projection of it that crosses into
|
|
115
|
+
* view state, and `toContextUsage` in `meter.ts` is the ONLY converter.
|
|
116
|
+
*/
|
|
117
|
+
export interface ContextUsageSnapshot {
|
|
118
|
+
/** Occupied tokens (measured, plus an estimate of anything appended since). */
|
|
119
|
+
occupied: number;
|
|
120
|
+
/** The denominator actually used. */
|
|
121
|
+
window: number;
|
|
122
|
+
/** `occupied / window` as a percentage, clamped to [0, 100] and rounded. */
|
|
123
|
+
pct: number;
|
|
124
|
+
/** Where the BASE came from. */
|
|
125
|
+
source: PressureSource;
|
|
126
|
+
/** How much of `occupied` is an estimate of appended messages; `> 0` means approximate. */
|
|
127
|
+
deltaTokens: number;
|
|
128
|
+
/** Whether the denominator is trustworthy (in the model table, or overridden). */
|
|
129
|
+
windowKnown: boolean;
|
|
130
|
+
/** Whether the denominator came from the user's `contextWindow` (I-6). */
|
|
131
|
+
windowOverridden: boolean;
|
|
132
|
+
}
|
|
92
133
|
/** One compaction, in whatever state it reached. Rendered by `CompactionCard`. */
|
|
93
134
|
export interface CompactionRecord {
|
|
94
135
|
/** 1-based within the session, so the card can name itself. */
|
|
@@ -143,7 +184,26 @@ export interface CompactionSnapshot {
|
|
|
143
184
|
live: boolean;
|
|
144
185
|
/** The summarizer's model id, or `''`. */
|
|
145
186
|
model: string;
|
|
187
|
+
/**
|
|
188
|
+
* How many compactions ANNOUNCED themselves (quiet-noop §6.2 / D-5).
|
|
189
|
+
*
|
|
190
|
+
* COMPACTIONS THAT RAN, NOT CHECKPOINTS THAT FIRED. A `pressure` compaction
|
|
191
|
+
* that declines before calling a model or changing the history never advances
|
|
192
|
+
* this, which is what keeps the card's `#N` contiguous. `declined` below is
|
|
193
|
+
* the other half of the same total.
|
|
194
|
+
*/
|
|
146
195
|
compactions: number;
|
|
196
|
+
/**
|
|
197
|
+
* How many `pressure` checkpoints declined BEFORE committing to any work
|
|
198
|
+
* (quiet-noop D-9 / P1-2).
|
|
199
|
+
*
|
|
200
|
+
* OPTIONAL SO EVERY EXISTING CONSTRUCTION SITE STAYS UNTOUCHED, and rendered
|
|
201
|
+
* by `/compact status` only when non-zero — a healthy session must not read
|
|
202
|
+
* `(0 checkpoints declined)`. This is the one human surface that still speaks
|
|
203
|
+
* about a decline after the transcript went quiet, which is what keeps
|
|
204
|
+
* `context-auto-compaction` §6.5's "guaranteed reporting surface" promise true.
|
|
205
|
+
*/
|
|
206
|
+
declined?: number;
|
|
147
207
|
/** Sum of `(tokensBefore - tokensAfter)` over applied compactions. */
|
|
148
208
|
tokensReclaimed: number;
|
|
149
209
|
/** What compaction itself has spent this session. */
|
|
@@ -161,6 +221,23 @@ export interface CompactionSnapshot {
|
|
|
161
221
|
generation: number;
|
|
162
222
|
pressure: Pressure;
|
|
163
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* THE COMPACTION EVENT STREAM.
|
|
226
|
+
*
|
|
227
|
+
* `compaction_start` and `compaction_end` are emitted ONLY for compactions that
|
|
228
|
+
* COMMITTED TO WORK (quiet-noop §3.1): a `pressure` compaction that declines
|
|
229
|
+
* before calling a model or changing the history emits NEITHER, and is
|
|
230
|
+
* deliberately indistinguishable from a turn at which `shouldCompact` returned
|
|
231
|
+
* false. `manual` and `overflow` always emit both.
|
|
232
|
+
*
|
|
233
|
+
* The pair is still BALANCED: every `compaction_start` is followed by exactly
|
|
234
|
+
* one `compaction_end` (a `finally` in the engine guarantees the verdict, and
|
|
235
|
+
* `settlePending` synthesizes a skeleton record when the compactor's own has not
|
|
236
|
+
* arrived). Consumers that count PAIRS are unaffected; consumers that counted
|
|
237
|
+
* ATTEMPTS must read `/compact status` or the JSONL diagnostic log instead -
|
|
238
|
+
* CORE's own `compaction_start` / `compaction_end` pair still fires for every
|
|
239
|
+
* attempt, declined or not, and that is the stream the log subscribes to.
|
|
240
|
+
*/
|
|
164
241
|
export type CompactionEvent = {
|
|
165
242
|
type: 'compaction_start';
|
|
166
243
|
index: number;
|
|
@@ -25,11 +25,21 @@
|
|
|
25
25
|
* methods delegate to a compactor that reads config LIVE - which is what lets
|
|
26
26
|
* `/compact off`, `/compact threshold 0.8` and a settings-screen save take effect
|
|
27
27
|
* mid-session without touching the `Agent`.
|
|
28
|
+
*
|
|
29
|
+
* DECLINED COMPACTIONS PRODUCE NO CLI EVENTS (quiet-noop §3.6). A `pressure`
|
|
30
|
+
* compaction that changed nothing, called nothing and spent nothing never emits
|
|
31
|
+
* `compaction_start`, so `onCompactorEvent` never opens a card and `settlePending`
|
|
32
|
+
* finds neither a held record nor a skeleton to synthesize. CORE still emits its
|
|
33
|
+
* own pair for every attempt, which is why `settlePending` still runs - it just
|
|
34
|
+
* refreshes the snapshot and returns. `onCompactorEvent`'s `if (!this.open)`
|
|
35
|
+
* guard is now unreachable for declines and stays for the abort race it was
|
|
36
|
+
* written for.
|
|
28
37
|
*/
|
|
29
38
|
import { type AgentEvent, type ContextManager, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ProviderRegistryOptions, type TokenUsage } from '@aragon-agent/core';
|
|
30
39
|
import type { CliConfig } from '../config/schema.js';
|
|
31
40
|
import type { NoticeLevel } from '../agent/reducer.js';
|
|
32
41
|
import { type ChildContextManagerFactory } from './child.js';
|
|
42
|
+
import { ContextMeter } from './meter.js';
|
|
33
43
|
import type { CompactionEventListener, CompactionSnapshot } from './types.js';
|
|
34
44
|
export interface CompactionWiringDeps {
|
|
35
45
|
/** Read LIVE on every call - the controller replaces the object on mutation. */
|
|
@@ -45,6 +55,17 @@ export interface CompactionWiringDeps {
|
|
|
45
55
|
getMessages: () => readonly Message[];
|
|
46
56
|
getSystemPrompt: () => string;
|
|
47
57
|
notify: (level: NoticeLevel, text: string) => void;
|
|
58
|
+
/**
|
|
59
|
+
* The controller's `ContextMeter`, forwarded to the compactor
|
|
60
|
+
* (context-usage-gauge-accuracy W1).
|
|
61
|
+
*
|
|
62
|
+
* OPTIONAL SO EVERY EXISTING CONSTRUCTION SITE COMPILES (R-1); production
|
|
63
|
+
* always injects, and `AgentController` owns the instance because it builds
|
|
64
|
+
* one UNCONDITIONALLY while this class only exists when `compaction.enabled`
|
|
65
|
+
* is true. That asymmetry is the whole point: occupancy has to be measurable
|
|
66
|
+
* in a session that turned compaction off.
|
|
67
|
+
*/
|
|
68
|
+
meter?: ContextMeter;
|
|
48
69
|
/**
|
|
49
70
|
* How the summarizer's own registry is built. Defaults to `initProviders`.
|
|
50
71
|
*
|
|
@@ -71,6 +92,8 @@ export declare class CompactionWiring {
|
|
|
71
92
|
private readonly log;
|
|
72
93
|
private readonly listeners;
|
|
73
94
|
private readonly compactor;
|
|
95
|
+
/** The occupancy meter, injected or private (R-1). Forwarded to the compactor. */
|
|
96
|
+
private readonly meter;
|
|
74
97
|
/** The compactor's OWN transport (D-10). Lazy - see `getRegistry()`. */
|
|
75
98
|
private registry;
|
|
76
99
|
private unsubscribe;
|
|
@@ -143,6 +166,20 @@ export declare class CompactionWiring {
|
|
|
143
166
|
* `/clear`, `/resume`, and any future host-side rewrite. A recorded prefix
|
|
144
167
|
* length indexes an array that no longer exists, so the delta must be
|
|
145
168
|
* abandoned rather than applied to a different history.
|
|
169
|
+
*
|
|
170
|
+
* THE **DEEP** RESET (I-8). `/resume` can change the model in the same breath,
|
|
171
|
+
* so `estimateOffset` - a calibration for one toolset on one model - goes too.
|
|
172
|
+
* The SHALLOW reset is `settlePending`'s splice branch, and the two are not
|
|
173
|
+
* interchangeable: swapping them produces silent errors in OPPOSITE
|
|
174
|
+
* directions (a splice that drops the offset reads too low and climbs back; a
|
|
175
|
+
* resume that keeps it reads too high and never self-corrects).
|
|
176
|
+
*
|
|
177
|
+
* DUPLICATED BY `AgentController` ON PURPOSE. The controller calls
|
|
178
|
+
* `contextMeter.onHistoryReplaced()` directly at the same two sites, because
|
|
179
|
+
* this class does not exist at all when `compaction.enabled` is false - and
|
|
180
|
+
* that unconditional call is the only thing that keeps `/resume` re-measuring
|
|
181
|
+
* for such a session. A deep reset is idempotent, so both firing costs
|
|
182
|
+
* nothing; deleting the controller's copy "to de-duplicate" reintroduces P0-2.
|
|
146
183
|
*/
|
|
147
184
|
onHistoryReplaced(): void;
|
|
148
185
|
/**
|
|
@@ -192,6 +229,17 @@ export declare class CompactionWiring {
|
|
|
192
229
|
ok: false;
|
|
193
230
|
reason: string;
|
|
194
231
|
}>;
|
|
232
|
+
/**
|
|
233
|
+
* The status a reader gets right now.
|
|
234
|
+
*
|
|
235
|
+
* IT READS `meter.current()`, WHICH RE-MEASURES WHEN DIRTY - and that is the
|
|
236
|
+
* fix for P0-1 (§3.3). This method is called one statement after
|
|
237
|
+
* `settlePending` marks the meter spliced, so the `snapshot` event emitted
|
|
238
|
+
* immediately after `compaction_end` carries the POST-compaction occupancy.
|
|
239
|
+
* The old body preferred `compactor.lastMeasured()`, i.e. the measurement that
|
|
240
|
+
* TRIGGERED this compaction, so the gauge fell and was bounced straight back
|
|
241
|
+
* to the pre-compaction figure inside the same synchronous emit loop.
|
|
242
|
+
*/
|
|
195
243
|
snapshot(): CompactionSnapshot;
|
|
196
244
|
/**
|
|
197
245
|
* `snapshot()` with the pressure argument substituted for the re-measurement.
|
|
@@ -205,6 +253,14 @@ export declare class CompactionWiring {
|
|
|
205
253
|
/** The summarizer that would be used right now, for `/compact status`. */
|
|
206
254
|
summarizerRef(): ModelRef | null;
|
|
207
255
|
subscribe(listener: CompactionEventListener): () => void;
|
|
256
|
+
/**
|
|
257
|
+
* The main model, for the FALLBACK meter's deps only (R-1).
|
|
258
|
+
*
|
|
259
|
+
* Production injects a meter built by `AgentController`, which resolves the
|
|
260
|
+
* same ref from the same live config; this exists so a test that constructs a
|
|
261
|
+
* bare wiring still gets a working denominator.
|
|
262
|
+
*/
|
|
263
|
+
private mainRef;
|
|
208
264
|
/**
|
|
209
265
|
* The transport one summarization call goes through - separate from the lead's,
|
|
210
266
|
* and fail-fast (D-10).
|