@aragon-agent/cli 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +216 -0
- package/README.md +241 -18
- package/dist/agent/agent-mode.d.ts +39 -0
- package/dist/agent/agent-mode.js +36 -0
- package/dist/agent/controller.d.ts +74 -1
- package/dist/agent/controller.js +200 -1
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +120 -0
- package/dist/agent/reducer.js +201 -2
- package/dist/cli.js +297 -6
- package/dist/commands/builtins.js +73 -23
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +113 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +342 -0
- package/dist/compaction/compactor.d.ts +268 -0
- package/dist/compaction/compactor.js +689 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +179 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +255 -0
- package/dist/compaction/wiring.js +556 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +92 -2
- package/dist/config/load.d.ts +31 -0
- package/dist/config/load.js +63 -1
- package/dist/config/schema.d.ts +159 -0
- package/dist/config/schema.js +137 -0
- package/dist/config/store.js +17 -1
- package/dist/config/ui-state.d.ts +44 -6
- package/dist/config/ui-state.js +21 -12
- package/dist/diagnostics/doctor.js +41 -1
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/index.js +8 -4
- package/dist/exec/runner.js +44 -2
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/stdin-mouse-filter.d.ts +8 -3
- package/dist/logging/install.d.ts +51 -1
- package/dist/logging/install.js +28 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/cli-commands.js +23 -1
- package/dist/session/persist.js +19 -0
- package/dist/session/store.d.ts +18 -0
- package/dist/session/store.js +59 -2
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +48 -1
- package/dist/ui/App.js +317 -17
- package/dist/ui/Composer.d.ts +26 -1
- package/dist/ui/Composer.js +6 -6
- package/dist/ui/PromptInput.d.ts +34 -1
- package/dist/ui/PromptInput.js +54 -3
- package/dist/ui/StatusBar.d.ts +40 -0
- package/dist/ui/StatusBar.js +13 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +32 -2
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +10 -3
- package/dist/ui/layout/budget.js +10 -3
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +34 -0
- package/dist/ui/overlays/HelpOverlay.js +18 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +60 -1
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/screen.d.ts +22 -0
- package/dist/ui/screen.js +70 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +23 -0
- package/dist/ui/use-startup-notices.d.ts +62 -5
- package/dist/ui/use-startup-notices.js +81 -17
- package/dist/ui/use-wheel-routing.js +8 -0
- package/dist/ui/win-vt-force.d.ts +136 -0
- package/dist/ui/win-vt-force.js +237 -0
- package/package.json +4 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context occupancy and the trigger (context-auto-compaction §3.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* PURE. No clock, no config read, no I/O - every input arrives as an argument,
|
|
7
|
+
* which is what lets the trigger and the status-bar gauge share one function and
|
|
8
|
+
* be tested without a live session.
|
|
9
|
+
*/
|
|
10
|
+
import { type Message, type TokenUsage } from '@aragon-agent/core';
|
|
11
|
+
import type { Pressure } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* How much of the window the conversation occupies, from ONE reported usage.
|
|
14
|
+
*
|
|
15
|
+
* THE CACHE TERMS ARE NOT DECORATION. Anthropic's `message_start` reports
|
|
16
|
+
* `input_tokens` EXCLUDING cached tokens and puts them in
|
|
17
|
+
* `cache_read_input_tokens` / `cache_creation_input_tokens`. This CLI never sends
|
|
18
|
+
* `cache_control`, so on a direct connection they are absent and the sum is
|
|
19
|
+
* unchanged - but any gateway or proxy that caches on the user's behalf makes
|
|
20
|
+
* them non-zero, and the status bar's old formula
|
|
21
|
+
* (`usage.inputTokens + usage.outputTokens`) silently UNDER-reports occupancy for
|
|
22
|
+
* exactly those users. That is a live, pre-existing defect in the number this
|
|
23
|
+
* feature triggers on.
|
|
24
|
+
*
|
|
25
|
+
* THIS SUM IS ONLY VALID FOR *ADDITIVE* CACHE FIELDS, AND THAT IS A PROVIDER-SIDE
|
|
26
|
+
* INVARIANT RATHER THAN A PROPERTY OF `TokenUsage` (C-14 / P1-10). It holds today
|
|
27
|
+
* because exactly one adapter writes those fields: `openai.ts` sets
|
|
28
|
+
* `inputTokens = prompt_tokens` and `google.ts` sets it from `promptTokenCount`,
|
|
29
|
+
* and NEITHER ever mentions `cacheReadTokens` / `cacheWriteTokens`. OpenAI's
|
|
30
|
+
* `prompt_tokens` ALREADY INCLUDES cached tokens, so mapping
|
|
31
|
+
* `prompt_tokens_details.cached_tokens` onto the existing field - a change that
|
|
32
|
+
* reads as a pure improvement and would sail through review - turns this into a
|
|
33
|
+
* double-count and fires compaction early on every OpenAI session, with the gauge
|
|
34
|
+
* agreeing with it and nothing reporting a fault. A provider that reports an
|
|
35
|
+
* INCLUSIVE total must add a DIFFERENTLY NAMED field. `compaction-pressure.test.ts`
|
|
36
|
+
* scans the adapters so the trap fails loudly at the moment someone opens it.
|
|
37
|
+
*
|
|
38
|
+
* THE GAUGE AND THE TRIGGER MUST READ THIS SAME FUNCTION (R-11 / AC-3). If they
|
|
39
|
+
* diverge, a user watching 78 % on the status bar sees compaction fire, concludes
|
|
40
|
+
* the feature is broken, and turns it off.
|
|
41
|
+
*/
|
|
42
|
+
export declare function occupiedTokens(usage: TokenUsage): number;
|
|
43
|
+
/**
|
|
44
|
+
* The calibration offset for this turn (D-23 / P1-11).
|
|
45
|
+
*
|
|
46
|
+
* Clamped at 0: a NEGATIVE offset would mean the estimator over-counted, which
|
|
47
|
+
* is possible for a tool-free session on a verbose model, and applying it would
|
|
48
|
+
* make the fallback read LOWER than the raw estimate - moving the one number
|
|
49
|
+
* that is already biased low further in the wrong direction.
|
|
50
|
+
*/
|
|
51
|
+
export declare function computeEstimateOffset(usage: TokenUsage, messages: readonly Message[], systemPrompt: string): number;
|
|
52
|
+
export interface PressureInput {
|
|
53
|
+
/** Authoritative usage of the last completed turn, or `undefined` (§3.4.2). */
|
|
54
|
+
lastUsage?: TokenUsage;
|
|
55
|
+
messages: readonly Message[];
|
|
56
|
+
systemPrompt: string;
|
|
57
|
+
contextWindow: number;
|
|
58
|
+
/** False when the window is `buildRuntimeModel`'s 128k placeholder. */
|
|
59
|
+
windowKnown: boolean;
|
|
60
|
+
/** Carried from the last `turn_end`; applied to every ESTIMATED figure. */
|
|
61
|
+
estimateOffset?: number;
|
|
62
|
+
/**
|
|
63
|
+
* The history LENGTH the measurement in `lastUsage` covered - i.e.
|
|
64
|
+
* `messages.length` at the `turn_end` that produced it, BEFORE the assistant
|
|
65
|
+
* message was pushed.
|
|
66
|
+
*
|
|
67
|
+
* `undefined` DISABLES THE DELTA ENTIRELY and reproduces round 1's behaviour
|
|
68
|
+
* exactly, which is what makes a missed invalidation cost accuracy rather than
|
|
69
|
+
* correctness.
|
|
70
|
+
*/
|
|
71
|
+
measuredPrefixLength?: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Tokens appended to the history since the measurement that produced `lastUsage`
|
|
75
|
+
* (context-auto-compaction-hardening §3.2.2 / W1).
|
|
76
|
+
*
|
|
77
|
+
* `+ 1` SKIPS THE ASSISTANT MESSAGE, and that is not an off-by-one to tidy away:
|
|
78
|
+
* `turn_end` is emitted BEFORE the push (`agent-loop.ts:478` / `:497`, and
|
|
79
|
+
* `Agent.emit` is synchronous), so the message at index `measuredPrefixLength` is
|
|
80
|
+
* the assistant turn whose cost is ALREADY inside `usage.outputTokens`. Counting
|
|
81
|
+
* it again double-charges every turn.
|
|
82
|
+
*
|
|
83
|
+
* NO `estimateOffset` IS APPLIED HERE. That offset is the systematic difference
|
|
84
|
+
* between what `estimatePromptTokens` counts and what the provider bills for a
|
|
85
|
+
* WHOLE REQUEST - chiefly the tool schemas, which are sent once per request and
|
|
86
|
+
* are already inside the measured base. It is a per-request constant, not a
|
|
87
|
+
* per-message rate; adding it to a slice would inflate occupancy by several
|
|
88
|
+
* thousand tokens on every turn and fire compaction early on short
|
|
89
|
+
* conversations.
|
|
90
|
+
*
|
|
91
|
+
* BOUNDS-CHECKED RATHER THAN TRUSTED. If the recorded prefix no longer indexes
|
|
92
|
+
* this array - a `/clear`, a `/resume`, a splice whose invalidation call was
|
|
93
|
+
* missed - the delta is 0 and the function degrades to round 1's number.
|
|
94
|
+
*/
|
|
95
|
+
export declare function estimateAppendedTokens(messages: readonly Message[], measuredPrefixLength: number | undefined): number;
|
|
96
|
+
/**
|
|
97
|
+
* THE ONE RULE FOR "PART OF THIS NUMBER IS A GUESS" (§6.3).
|
|
98
|
+
*
|
|
99
|
+
* `Pressure.source` deliberately keeps two members: it answers "where did the
|
|
100
|
+
* BASE come from", and a third member would force every existing consumer of the
|
|
101
|
+
* union to be revisited for a distinction none of them acts on. The `~` question
|
|
102
|
+
* is this function, and it has one test.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isApproximate(p: Pressure): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Occupancy right now, measured when there is a measurement and calibrated when
|
|
107
|
+
* there is not.
|
|
108
|
+
*
|
|
109
|
+
* `source` is CARRIED, never re-derived, and it reaches the UI: the status bar
|
|
110
|
+
* already renders a leading `~` when the window is unknown, and this widens that
|
|
111
|
+
* to "the NUMBER is approximate too". Honesty about a guess is cheaper than a
|
|
112
|
+
* support thread about a wrong percentage.
|
|
113
|
+
*/
|
|
114
|
+
export declare function computePressure(input: PressureInput): Pressure;
|
|
115
|
+
export interface HeadroomRequirement {
|
|
116
|
+
/** The session's effective output cap for one call. */
|
|
117
|
+
maxOutputTokens: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The tokens the NEXT request needs on top of the history.
|
|
121
|
+
*
|
|
122
|
+
* `THINKING_HEADROOM_TOKENS` (4096) and `CONTEXT_SAFETY_MARGIN_TOKENS` (1024) are
|
|
123
|
+
* existing core exports and are reused rather than re-spelled.
|
|
124
|
+
*/
|
|
125
|
+
export declare function requiredHeadroom(req: HeadroomRequirement): number;
|
|
126
|
+
/**
|
|
127
|
+
* The trigger, and the second term is not belt-and-braces (D-12).
|
|
128
|
+
*
|
|
129
|
+
* 90 % OF A 200 k WINDOW LEAVES 20 k, WHICH IS PLENTY. 90 % OF A 32 k WINDOW
|
|
130
|
+
* LEAVES 3.2 k, WHICH IS LESS THAN A SINGLE `max_tokens` OF 8192 - the request is
|
|
131
|
+
* already impossible. A pure ratio trigger is therefore correct for the models
|
|
132
|
+
* most users run and quietly wrong for the small ones, and "quietly wrong on the
|
|
133
|
+
* cheap models" is not a property this product ships.
|
|
134
|
+
*/
|
|
135
|
+
export declare function shouldCompactAt(p: Pressure, threshold: number, req: HeadroomRequirement): boolean;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context occupancy and the trigger (context-auto-compaction §3.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* PURE. No clock, no config read, no I/O - every input arrives as an argument,
|
|
7
|
+
* which is what lets the trigger and the status-bar gauge share one function and
|
|
8
|
+
* be tested without a live session.
|
|
9
|
+
*/
|
|
10
|
+
import { CONTEXT_SAFETY_MARGIN_TOKENS, THINKING_HEADROOM_TOKENS, estimatePromptTokens, } from '@aragon-agent/core';
|
|
11
|
+
/**
|
|
12
|
+
* How much of the window the conversation occupies, from ONE reported usage.
|
|
13
|
+
*
|
|
14
|
+
* THE CACHE TERMS ARE NOT DECORATION. Anthropic's `message_start` reports
|
|
15
|
+
* `input_tokens` EXCLUDING cached tokens and puts them in
|
|
16
|
+
* `cache_read_input_tokens` / `cache_creation_input_tokens`. This CLI never sends
|
|
17
|
+
* `cache_control`, so on a direct connection they are absent and the sum is
|
|
18
|
+
* unchanged - but any gateway or proxy that caches on the user's behalf makes
|
|
19
|
+
* them non-zero, and the status bar's old formula
|
|
20
|
+
* (`usage.inputTokens + usage.outputTokens`) silently UNDER-reports occupancy for
|
|
21
|
+
* exactly those users. That is a live, pre-existing defect in the number this
|
|
22
|
+
* feature triggers on.
|
|
23
|
+
*
|
|
24
|
+
* THIS SUM IS ONLY VALID FOR *ADDITIVE* CACHE FIELDS, AND THAT IS A PROVIDER-SIDE
|
|
25
|
+
* INVARIANT RATHER THAN A PROPERTY OF `TokenUsage` (C-14 / P1-10). It holds today
|
|
26
|
+
* because exactly one adapter writes those fields: `openai.ts` sets
|
|
27
|
+
* `inputTokens = prompt_tokens` and `google.ts` sets it from `promptTokenCount`,
|
|
28
|
+
* and NEITHER ever mentions `cacheReadTokens` / `cacheWriteTokens`. OpenAI's
|
|
29
|
+
* `prompt_tokens` ALREADY INCLUDES cached tokens, so mapping
|
|
30
|
+
* `prompt_tokens_details.cached_tokens` onto the existing field - a change that
|
|
31
|
+
* reads as a pure improvement and would sail through review - turns this into a
|
|
32
|
+
* double-count and fires compaction early on every OpenAI session, with the gauge
|
|
33
|
+
* agreeing with it and nothing reporting a fault. A provider that reports an
|
|
34
|
+
* INCLUSIVE total must add a DIFFERENTLY NAMED field. `compaction-pressure.test.ts`
|
|
35
|
+
* scans the adapters so the trap fails loudly at the moment someone opens it.
|
|
36
|
+
*
|
|
37
|
+
* THE GAUGE AND THE TRIGGER MUST READ THIS SAME FUNCTION (R-11 / AC-3). If they
|
|
38
|
+
* diverge, a user watching 78 % on the status bar sees compaction fire, concludes
|
|
39
|
+
* the feature is broken, and turns it off.
|
|
40
|
+
*/
|
|
41
|
+
export function occupiedTokens(usage) {
|
|
42
|
+
return (usage.inputTokens +
|
|
43
|
+
(usage.cacheReadTokens ?? 0) +
|
|
44
|
+
(usage.cacheWriteTokens ?? 0) +
|
|
45
|
+
usage.outputTokens);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The calibration offset for this turn (D-23 / P1-11).
|
|
49
|
+
*
|
|
50
|
+
* Clamped at 0: a NEGATIVE offset would mean the estimator over-counted, which
|
|
51
|
+
* is possible for a tool-free session on a verbose model, and applying it would
|
|
52
|
+
* make the fallback read LOWER than the raw estimate - moving the one number
|
|
53
|
+
* that is already biased low further in the wrong direction.
|
|
54
|
+
*/
|
|
55
|
+
export function computeEstimateOffset(usage, messages, systemPrompt) {
|
|
56
|
+
const measured = occupiedTokens(usage);
|
|
57
|
+
const estimated = estimatePromptTokens(messages, systemPrompt);
|
|
58
|
+
return Math.max(0, measured - estimated);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Tokens appended to the history since the measurement that produced `lastUsage`
|
|
62
|
+
* (context-auto-compaction-hardening §3.2.2 / W1).
|
|
63
|
+
*
|
|
64
|
+
* `+ 1` SKIPS THE ASSISTANT MESSAGE, and that is not an off-by-one to tidy away:
|
|
65
|
+
* `turn_end` is emitted BEFORE the push (`agent-loop.ts:478` / `:497`, and
|
|
66
|
+
* `Agent.emit` is synchronous), so the message at index `measuredPrefixLength` is
|
|
67
|
+
* the assistant turn whose cost is ALREADY inside `usage.outputTokens`. Counting
|
|
68
|
+
* it again double-charges every turn.
|
|
69
|
+
*
|
|
70
|
+
* NO `estimateOffset` IS APPLIED HERE. That offset is the systematic difference
|
|
71
|
+
* between what `estimatePromptTokens` counts and what the provider bills for a
|
|
72
|
+
* WHOLE REQUEST - chiefly the tool schemas, which are sent once per request and
|
|
73
|
+
* are already inside the measured base. It is a per-request constant, not a
|
|
74
|
+
* per-message rate; adding it to a slice would inflate occupancy by several
|
|
75
|
+
* thousand tokens on every turn and fire compaction early on short
|
|
76
|
+
* conversations.
|
|
77
|
+
*
|
|
78
|
+
* BOUNDS-CHECKED RATHER THAN TRUSTED. If the recorded prefix no longer indexes
|
|
79
|
+
* this array - a `/clear`, a `/resume`, a splice whose invalidation call was
|
|
80
|
+
* missed - the delta is 0 and the function degrades to round 1's number.
|
|
81
|
+
*/
|
|
82
|
+
export function estimateAppendedTokens(messages, measuredPrefixLength) {
|
|
83
|
+
if (measuredPrefixLength === undefined)
|
|
84
|
+
return 0;
|
|
85
|
+
const start = measuredPrefixLength + 1;
|
|
86
|
+
if (start < 0 || start >= messages.length)
|
|
87
|
+
return 0;
|
|
88
|
+
return estimatePromptTokens(messages.slice(start));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* THE ONE RULE FOR "PART OF THIS NUMBER IS A GUESS" (§6.3).
|
|
92
|
+
*
|
|
93
|
+
* `Pressure.source` deliberately keeps two members: it answers "where did the
|
|
94
|
+
* BASE come from", and a third member would force every existing consumer of the
|
|
95
|
+
* union to be revisited for a distinction none of them acts on. The `~` question
|
|
96
|
+
* is this function, and it has one test.
|
|
97
|
+
*/
|
|
98
|
+
export function isApproximate(p) {
|
|
99
|
+
return p.source === 'estimate' || p.deltaTokens > 0;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Occupancy right now, measured when there is a measurement and calibrated when
|
|
103
|
+
* there is not.
|
|
104
|
+
*
|
|
105
|
+
* `source` is CARRIED, never re-derived, and it reaches the UI: the status bar
|
|
106
|
+
* already renders a leading `~` when the window is unknown, and this widens that
|
|
107
|
+
* to "the NUMBER is approximate too". Honesty about a guess is cheaper than a
|
|
108
|
+
* support thread about a wrong percentage.
|
|
109
|
+
*/
|
|
110
|
+
export function computePressure(input) {
|
|
111
|
+
const window = input.contextWindow > 0 ? input.contextWindow : 0;
|
|
112
|
+
let occupied;
|
|
113
|
+
let source;
|
|
114
|
+
let deltaTokens = 0;
|
|
115
|
+
if (input.lastUsage) {
|
|
116
|
+
// THE MEASURED FIGURE STAYS AUTHORITATIVE FOR WHAT IT COVERS, and only the
|
|
117
|
+
// messages appended since are estimated (W1 / DH-1). Mixing two units is the
|
|
118
|
+
// trap D-23 warned about; this does not mix them, it adds a second term for
|
|
119
|
+
// a range the first one demonstrably does not describe.
|
|
120
|
+
const base = occupiedTokens(input.lastUsage);
|
|
121
|
+
deltaTokens = estimateAppendedTokens(input.messages, input.measuredPrefixLength);
|
|
122
|
+
occupied = base + deltaTokens;
|
|
123
|
+
source = 'usage';
|
|
124
|
+
}
|
|
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.
|
|
129
|
+
const raw = estimatePromptTokens(input.messages, input.systemPrompt);
|
|
130
|
+
occupied = raw + (input.estimateOffset ?? 0);
|
|
131
|
+
source = 'estimate';
|
|
132
|
+
}
|
|
133
|
+
const ratio = window > 0 ? Math.max(0, Math.min(1, occupied / window)) : 0;
|
|
134
|
+
return {
|
|
135
|
+
occupied,
|
|
136
|
+
contextWindow: window,
|
|
137
|
+
ratio,
|
|
138
|
+
headroom: Math.max(0, window - occupied),
|
|
139
|
+
source,
|
|
140
|
+
windowKnown: input.windowKnown,
|
|
141
|
+
...(input.estimateOffset !== undefined ? { estimateOffset: input.estimateOffset } : {}),
|
|
142
|
+
deltaTokens,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The tokens the NEXT request needs on top of the history.
|
|
147
|
+
*
|
|
148
|
+
* `THINKING_HEADROOM_TOKENS` (4096) and `CONTEXT_SAFETY_MARGIN_TOKENS` (1024) are
|
|
149
|
+
* existing core exports and are reused rather than re-spelled.
|
|
150
|
+
*/
|
|
151
|
+
export function requiredHeadroom(req) {
|
|
152
|
+
return req.maxOutputTokens + THINKING_HEADROOM_TOKENS + CONTEXT_SAFETY_MARGIN_TOKENS;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The trigger, and the second term is not belt-and-braces (D-12).
|
|
156
|
+
*
|
|
157
|
+
* 90 % OF A 200 k WINDOW LEAVES 20 k, WHICH IS PLENTY. 90 % OF A 32 k WINDOW
|
|
158
|
+
* LEAVES 3.2 k, WHICH IS LESS THAN A SINGLE `max_tokens` OF 8192 - the request is
|
|
159
|
+
* already impossible. A pure ratio trigger is therefore correct for the models
|
|
160
|
+
* most users run and quietly wrong for the small ones, and "quietly wrong on the
|
|
161
|
+
* cheap models" is not a property this product ships.
|
|
162
|
+
*/
|
|
163
|
+
export function shouldCompactAt(p, threshold, req) {
|
|
164
|
+
if (p.contextWindow <= 0)
|
|
165
|
+
return false;
|
|
166
|
+
return p.ratio >= threshold || p.headroom < requiredHeadroom(req);
|
|
167
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
import type { AssistantMessage, LLMRequest, ModelRef, TokenUsage } from '@aragon-agent/core';
|
|
18
|
+
/** The assistant text of a completion, or `''` for an answer with no text block. */
|
|
19
|
+
export declare function assistantText(message: AssistantMessage | undefined): string;
|
|
20
|
+
export declare function errorText(err: unknown): string;
|
|
21
|
+
/**
|
|
22
|
+
* The call's spend.
|
|
23
|
+
*
|
|
24
|
+
* OPTIONAL BY DECLARATION. All three adapters populate `usage` before yielding
|
|
25
|
+
* `done`, but the field is `usage?` on `AssistantMessage`, and a `computeCost`
|
|
26
|
+
* over `undefined` is a NaN that reaches the status bar and stays there for the
|
|
27
|
+
* session (the RV-11 lesson from the fast tier).
|
|
28
|
+
*/
|
|
29
|
+
export declare function usageOf(message: AssistantMessage | undefined): TokenUsage;
|
|
30
|
+
export declare function accumulateUsage(total: TokenUsage, next: TokenUsage): TokenUsage;
|
|
31
|
+
export interface SummarizeRequestParams {
|
|
32
|
+
ref: ModelRef;
|
|
33
|
+
apiKey: string;
|
|
34
|
+
digest: string;
|
|
35
|
+
instructions?: string;
|
|
36
|
+
hasPriorSummary: boolean;
|
|
37
|
+
/** Created and owned by the compactor; forwarded into the provider from there. */
|
|
38
|
+
signal: AbortSignal;
|
|
39
|
+
now: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The request one summarization sends. NO TOOLS, no thinking, one bounded user
|
|
43
|
+
* message - the exact shape `fast/review-call.ts` uses.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildSummarizeRequest(params: SummarizeRequestParams): LLMRequest;
|
|
46
|
+
export interface SummarizeCallParams extends SummarizeRequestParams {
|
|
47
|
+
/**
|
|
48
|
+
* `ProviderRegistry.complete()` - NOT `stream()` (P2-6).
|
|
49
|
+
*
|
|
50
|
+
* Nothing consumes a summary incrementally, and `complete` is itself
|
|
51
|
+
* `consumeStream(this.stream(...))`, which is the fast reviewer's precedent.
|
|
52
|
+
*/
|
|
53
|
+
complete(providerId: string, request: LLMRequest): Promise<AssistantMessage>;
|
|
54
|
+
/**
|
|
55
|
+
* Fired when `callTimeoutMs` elapses.
|
|
56
|
+
*
|
|
57
|
+
* THE CALLER ABORTS, NOT THIS MODULE. See the file header.
|
|
58
|
+
*/
|
|
59
|
+
onTimeout(): void;
|
|
60
|
+
}
|
|
61
|
+
export type SummarizeOutcome = {
|
|
62
|
+
ok: true;
|
|
63
|
+
text: string;
|
|
64
|
+
usage: TokenUsage;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
error: unknown;
|
|
68
|
+
};
|
|
69
|
+
/** Send one summarization and bound it in wall clock. */
|
|
70
|
+
export declare function runSummarizeCall(params: SummarizeCallParams): Promise<SummarizeOutcome>;
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
import { COMPACTION_LIMITS } from './limits.js';
|
|
18
|
+
import { buildSummarySystemPrompt, buildSummaryUserMessage } from './summary-prompt.js';
|
|
19
|
+
/** The assistant text of a completion, or `''` for an answer with no text block. */
|
|
20
|
+
export function assistantText(message) {
|
|
21
|
+
if (!message || !Array.isArray(message.content))
|
|
22
|
+
return '';
|
|
23
|
+
return message.content
|
|
24
|
+
.filter((b) => b.type === 'text')
|
|
25
|
+
.map((b) => b.text)
|
|
26
|
+
.join('')
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
export function errorText(err) {
|
|
30
|
+
return err instanceof Error ? err.message : String(err);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The call's spend.
|
|
34
|
+
*
|
|
35
|
+
* OPTIONAL BY DECLARATION. All three adapters populate `usage` before yielding
|
|
36
|
+
* `done`, but the field is `usage?` on `AssistantMessage`, and a `computeCost`
|
|
37
|
+
* over `undefined` is a NaN that reaches the status bar and stays there for the
|
|
38
|
+
* session (the RV-11 lesson from the fast tier).
|
|
39
|
+
*/
|
|
40
|
+
export function usageOf(message) {
|
|
41
|
+
return message?.usage ?? { inputTokens: 0, outputTokens: 0 };
|
|
42
|
+
}
|
|
43
|
+
export function accumulateUsage(total, next) {
|
|
44
|
+
return {
|
|
45
|
+
inputTokens: total.inputTokens + next.inputTokens,
|
|
46
|
+
outputTokens: total.outputTokens + next.outputTokens,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The request one summarization sends. NO TOOLS, no thinking, one bounded user
|
|
51
|
+
* message - the exact shape `fast/review-call.ts` uses.
|
|
52
|
+
*/
|
|
53
|
+
export function buildSummarizeRequest(params) {
|
|
54
|
+
return {
|
|
55
|
+
model: params.ref.modelId,
|
|
56
|
+
...(params.ref.baseUrl ? { baseUrl: params.ref.baseUrl } : {}),
|
|
57
|
+
apiKey: params.apiKey,
|
|
58
|
+
systemPrompt: buildSummarySystemPrompt({
|
|
59
|
+
maxChars: COMPACTION_LIMITS.summaryMaxChars,
|
|
60
|
+
...(params.instructions ? { instructions: params.instructions } : {}),
|
|
61
|
+
hasPriorSummary: params.hasPriorSummary,
|
|
62
|
+
}),
|
|
63
|
+
messages: [buildSummaryUserMessage(params.digest, params.now)],
|
|
64
|
+
maxTokens: COMPACTION_LIMITS.summaryOutputTokens,
|
|
65
|
+
// PAIRED WITH `temperature: 0` ON PURPOSE: the Anthropic adapter deletes
|
|
66
|
+
// `temperature` whenever a thinking budget is set, so these two are one
|
|
67
|
+
// choice and not two.
|
|
68
|
+
thinkingLevel: 'off',
|
|
69
|
+
temperature: 0,
|
|
70
|
+
signal: params.signal,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Send one summarization and bound it in wall clock. */
|
|
74
|
+
export async function runSummarizeCall(params) {
|
|
75
|
+
const timer = setTimeout(params.onTimeout, COMPACTION_LIMITS.callTimeoutMs);
|
|
76
|
+
try {
|
|
77
|
+
const message = await params.complete(params.ref.providerId, buildSummarizeRequest(params));
|
|
78
|
+
return { ok: true, text: assistantText(message), usage: usageOf(message) };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return { ok: false, error };
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
*/
|
|
10
|
+
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
|
+
*/
|
|
18
|
+
export declare function buildSummarySystemPrompt(opts: {
|
|
19
|
+
maxChars: number;
|
|
20
|
+
/** Free text from `/compact <instructions>`, appended as `## Focus`. */
|
|
21
|
+
instructions?: string;
|
|
22
|
+
/** True when the digest opens with a `PRIOR SUMMARY` section. */
|
|
23
|
+
hasPriorSummary: boolean;
|
|
24
|
+
}): string;
|
|
25
|
+
/** The user message the summarization call sends: the digest, and nothing else. */
|
|
26
|
+
export declare function buildSummaryUserMessage(digest: string, now: number): UserMessage;
|
|
27
|
+
export type AnchorShape = 'tagged' | 'verbatim';
|
|
28
|
+
export interface AnchorInfo {
|
|
29
|
+
/** The message to place first, verbatim in the history. */
|
|
30
|
+
message: Message;
|
|
31
|
+
shape: AnchorShape;
|
|
32
|
+
}
|
|
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
|
+
*/
|
|
52
|
+
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;
|
|
64
|
+
export interface BlockInput {
|
|
65
|
+
summary: string;
|
|
66
|
+
replaced: number;
|
|
67
|
+
turns: number;
|
|
68
|
+
anchor: AnchorShape;
|
|
69
|
+
generation: number;
|
|
70
|
+
/** True for a `mode: 'truncated'` splice — the model is entitled to know. */
|
|
71
|
+
truncated: boolean;
|
|
72
|
+
}
|
|
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
|
+
*/
|
|
85
|
+
export declare function buildCompactedBlock(input: BlockInput): UserMessage;
|