@aragon-agent/cli 0.5.6 → 0.5.9
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 +356 -0
- package/README.md +1429 -1105
- package/dist/agent/coalesce.d.ts +13 -0
- package/dist/agent/coalesce.js +50 -11
- package/dist/agent/controller.d.ts +141 -2
- package/dist/agent/controller.js +396 -4
- package/dist/agent/entry-limits.d.ts +65 -0
- package/dist/agent/entry-limits.js +133 -0
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +253 -3
- package/dist/agent/reducer.js +553 -28
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +122 -4
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +313 -2
- package/dist/commands/perf.d.ts +45 -0
- package/dist/commands/perf.js +78 -0
- package/dist/config/cli-commands.d.ts +53 -2
- package/dist/config/cli-commands.js +136 -3
- package/dist/config/env.d.ts +6 -1
- package/dist/config/env.js +131 -1
- package/dist/config/load.d.ts +63 -0
- package/dist/config/load.js +125 -2
- package/dist/config/schema.d.ts +273 -10
- package/dist/config/schema.js +250 -7
- package/dist/config/store.js +20 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +1 -4
- package/dist/ui/App.js +357 -44
- package/dist/ui/BottomStatusRow.d.ts +43 -0
- package/dist/ui/BottomStatusRow.js +14 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Markdown.d.ts +7 -1
- package/dist/ui/Markdown.js +15 -48
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +50 -0
- package/dist/ui/StatusBar.js +7 -3
- package/dist/ui/TeamPanel.d.ts +5 -4
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/TodoPanel.d.ts +5 -4
- package/dist/ui/Transcript.d.ts +80 -5
- package/dist/ui/Transcript.js +179 -11
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +38 -1
- package/dist/ui/entries/AssistantEntry.js +34 -3
- package/dist/ui/entries/DiffView.d.ts +67 -0
- package/dist/ui/entries/DiffView.js +103 -0
- package/dist/ui/entries/FastCard.d.ts +53 -0
- package/dist/ui/entries/FastCard.js +87 -0
- package/dist/ui/entries/RetryCard.d.ts +56 -0
- package/dist/ui/entries/RetryCard.js +60 -0
- package/dist/ui/entries/TeamCard.d.ts +9 -1
- package/dist/ui/entries/TeamCard.js +28 -2
- package/dist/ui/entries/TodoCard.d.ts +9 -1
- package/dist/ui/entries/TodoCard.js +23 -1
- package/dist/ui/entries/ToolCard.d.ts +50 -1
- package/dist/ui/entries/ToolCard.js +104 -7
- package/dist/ui/entries/ToolPreview.d.ts +2 -2
- package/dist/ui/entries/UserEntry.d.ts +9 -1
- package/dist/ui/entries/UserEntry.js +17 -1
- package/dist/ui/exit-snapshot.d.ts +7 -0
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/AppShell.d.ts +13 -22
- package/dist/ui/layout/AppShell.js +3 -38
- package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
- package/dist/ui/layout/MeasuredEntry.js +30 -0
- package/dist/ui/layout/ScrollViewport.js +8 -2
- package/dist/ui/layout/viewport-geometry.d.ts +28 -0
- package/dist/ui/layout/viewport-geometry.js +27 -0
- package/dist/ui/layout/virtual-window.d.ts +100 -0
- package/dist/ui/layout/virtual-window.js +308 -0
- package/dist/ui/live-clamp.d.ts +42 -0
- package/dist/ui/live-clamp.js +74 -0
- package/dist/ui/markdown-blocks.d.ts +25 -1
- package/dist/ui/markdown-blocks.js +43 -0
- package/dist/ui/overlays/HelpOverlay.js +16 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
- package/dist/ui/overlays/SettingsScreen.js +147 -1
- package/dist/ui/render-cache.d.ts +81 -0
- package/dist/ui/render-cache.js +195 -0
- package/dist/ui/render-governor.d.ts +54 -0
- package/dist/ui/render-governor.js +78 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +32 -0
- package/dist/ui/use-height-store.d.ts +31 -0
- package/dist/ui/use-height-store.js +76 -0
- package/dist/ui/use-render-governor.d.ts +39 -0
- package/dist/ui/use-render-governor.js +57 -0
- package/dist/ui/use-startup-notices.d.ts +9 -1
- package/dist/ui/use-startup-notices.js +35 -1
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- package/package.json +2 -2
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAST_LIMITS — the single authority on every bound the fast tier enforces
|
|
3
|
+
* (fast-model-tier §5.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte. The
|
|
7
|
+
* regex there is a hardcoded directory list, which is why adding this tree and
|
|
8
|
+
* adding `fast` to that list are the SAME change (C-4 / AC-27) - a scanner that
|
|
9
|
+
* silently stops scanning is worse than no scanner.
|
|
10
|
+
*
|
|
11
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
|
|
12
|
+
* `TEAM_LIMITS` and `TODO_LIMITS` both state:
|
|
13
|
+
*
|
|
14
|
+
* - The entries below are STRUCTURAL. They describe what a review call, a
|
|
15
|
+
* digest and a card can physically carry. A user has no business tuning them.
|
|
16
|
+
* - The ten `fast.*` keys in `config/schema.ts` are POLICY: which model, how
|
|
17
|
+
* often, how long a critique may be. Those are clamped, persisted and
|
|
18
|
+
* user-facing.
|
|
19
|
+
*
|
|
20
|
+
* `reviewMaxChars` therefore lives in the CONFIG and not here: it is the one
|
|
21
|
+
* number whose right value depends on how much of their own context the user is
|
|
22
|
+
* willing to spend on advice. `reviewOutputTokens` stays here because 512 tokens
|
|
23
|
+
* is a bound on a protocol, not a preference.
|
|
24
|
+
*/
|
|
25
|
+
export const FAST_LIMITS = {
|
|
26
|
+
/**
|
|
27
|
+
* The flat input cost of one review (D-6 / R-2).
|
|
28
|
+
*
|
|
29
|
+
* THIS NUMBER IS THE WHOLE ECONOMIC ARGUMENT. The reviewer builds its own
|
|
30
|
+
* bounded frame ring and never reads `agent.state.messages`, which after four
|
|
31
|
+
* 60 KB file reads carries 240 KB of file bodies; shipping that to the "cheap"
|
|
32
|
+
* model would make a review cost MORE than the turn it is reviewing.
|
|
33
|
+
*/
|
|
34
|
+
digestMaxChars: 4_000,
|
|
35
|
+
/** The user's request for the run, clamped. Without it the reviewer is asked
|
|
36
|
+
* whether work is on track without being told what the track is. */
|
|
37
|
+
goalChars: 500,
|
|
38
|
+
/** Per-frame assistant-text tail. Rolling, clipped on every append. */
|
|
39
|
+
turnTailChars: 600,
|
|
40
|
+
/** Per picked tool argument. Mirrors `TEAM_LIMITS.activityArgChars`. */
|
|
41
|
+
toolArgChars: 120,
|
|
42
|
+
/** Frames kept per run. `>=` the max of `reviewContextTurns`, with slack. */
|
|
43
|
+
maxFramesRetained: 12,
|
|
44
|
+
/** Hard cap on the review's own output. */
|
|
45
|
+
reviewOutputTokens: 512,
|
|
46
|
+
/**
|
|
47
|
+
* Wall-clock budget for ONE review call INCLUDING RETRIES (D-H11).
|
|
48
|
+
*
|
|
49
|
+
* THE BUDGET HAS TWO TERMS AND ONLY ONE OF THEM SHRANK. Round 1 set 30 s as
|
|
50
|
+
* IF-3's fallback: `ProviderRegistry.complete()` routed through a retrying
|
|
51
|
+
* `stream()` with `maxRetries: 10`, so the budget covered
|
|
52
|
+
* `attempts x (call + backoff)`. Round 2 gives the reviewer its own
|
|
53
|
+
* `maxRetries: 1` registry (`wiring.ts`), which collapses that term to about
|
|
54
|
+
* one call plus a one-second backoff - but it does nothing at all to the
|
|
55
|
+
* other term, THE FAST MODEL'S OWN GENERATION LATENCY. The review asks for up
|
|
56
|
+
* to `reviewOutputTokens` from a `digestMaxChars` prompt, and the tier is by
|
|
57
|
+
* construction pointed at the cheapest endpoint the user has: the population
|
|
58
|
+
* where a 15-25 s completion is ordinary rather than pathological.
|
|
59
|
+
*
|
|
60
|
+
* SO THIS IS 20 000 AND NOT 12 000. A timeout IS a strike (see `runReview`),
|
|
61
|
+
* and `maxConsecutiveFailures` is 3, so a 12 s bound would convert "slow but
|
|
62
|
+
* healthy provider" into a tier that self-disables for the session and
|
|
63
|
+
* reports itself as MISCONFIGURED - the one register D-24 reserves for faults
|
|
64
|
+
* that never self-heal, spent on one that does. 20 s keeps a margin over a
|
|
65
|
+
* slow completion and still cuts a stuck review's occupancy of the
|
|
66
|
+
* single-flight slot by a third. Lower it only with a measured p99, which
|
|
67
|
+
* `manual-test.md` row 6 exists to produce.
|
|
68
|
+
*/
|
|
69
|
+
reviewTimeoutMs: 20_000,
|
|
70
|
+
/** Reviews STARTED per run. A 40-turn run at cadence 5 would otherwise buy 8
|
|
71
|
+
* critiques the lead increasingly ignores. */
|
|
72
|
+
maxReviewsPerRun: 6,
|
|
73
|
+
/** Consecutive NON-TRANSIENT failures before the reviewer self-disables. */
|
|
74
|
+
maxConsecutiveFailures: 3,
|
|
75
|
+
/**
|
|
76
|
+
* A critique waiting for its injection window goes stale (RV-12).
|
|
77
|
+
*
|
|
78
|
+
* The window is not guaranteed to be prompt: one `task` dispatch is a single
|
|
79
|
+
* tool call that can run for `team.dispatchTimeoutMs`, so a critique triggered
|
|
80
|
+
* just before one arrives at the far side describing a state five children
|
|
81
|
+
* have since rewritten. Advice that is merely late is worse than no advice,
|
|
82
|
+
* because the lead cannot tell that it is late.
|
|
83
|
+
*/
|
|
84
|
+
pendingMaxAgeMs: 120_000,
|
|
85
|
+
/** ... or is overtaken by the run itself, in turns. */
|
|
86
|
+
pendingMaxTurnsBehind: 2,
|
|
87
|
+
/** Rows the transcript card renders before eliding. */
|
|
88
|
+
cardTextRows: 4,
|
|
89
|
+
/** Below this many columns the status chip drops (D-20's treatment, not
|
|
90
|
+
* `agentMode`'s - the guaranteed reporting surface is `/fast status`). */
|
|
91
|
+
statusCompactCols: 100,
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Bumped whenever the wording of `<fast_tier>` or the review system prompt
|
|
95
|
+
* changes, so a behaviour report can be tied to a block revision with one grep -
|
|
96
|
+
* exactly what `TEAM_BLOCK_VERSION` and `TODO_BLOCK_VERSION` are for.
|
|
97
|
+
*/
|
|
98
|
+
export const FAST_BLOCK_VERSION = 'v1-2026-07';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt text the fast tier adds (fast-model-tier §4.6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope, and these
|
|
5
|
+
* strings also reach a model that may be running in a `cmd.exe` terminal.
|
|
6
|
+
*
|
|
7
|
+
* `<fast_tier>` IS SPLICED CONDITIONALLY by `buildSystemPrompt`, which is what
|
|
8
|
+
* preserves invariant I-2: with the tier off the prompt is BYTE-IDENTICAL to the
|
|
9
|
+
* pre-feature output for a fixed tool array. An unconditional guidance line here
|
|
10
|
+
* would quietly break `--no-fast`, `--no-team`, `--no-skills` and `--no-todo`
|
|
11
|
+
* all at once.
|
|
12
|
+
*
|
|
13
|
+
* `FAST_BLOCK_VERSION` exists so a change to this wording is greppable from a
|
|
14
|
+
* behaviour report.
|
|
15
|
+
*/
|
|
16
|
+
import { FAST_BLOCK_VERSION } from './limits.js';
|
|
17
|
+
export { FAST_BLOCK_VERSION };
|
|
18
|
+
export interface FastBlockParams {
|
|
19
|
+
/** The resolved fast model id, interpolated so the prompt never names a model
|
|
20
|
+
* the tier will not actually use (§3.9 / RV-3). */
|
|
21
|
+
model: string;
|
|
22
|
+
/** `fast.delegate` — whether `model: "fast"` is honoured on `task`. */
|
|
23
|
+
delegate: boolean;
|
|
24
|
+
/** `fast.review` — whether `<fast_review>` blocks can appear at all. */
|
|
25
|
+
review: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The lead's `<fast_tier>` block. Under 1100 characters on purpose: it is paid
|
|
29
|
+
* for on EVERY turn of every fast-enabled session, so anything that does not
|
|
30
|
+
* change a delegation decision does not belong in it (R-11).
|
|
31
|
+
*
|
|
32
|
+
* EACH PARAGRAPH IS PRESENT ONLY WHEN ITS CAPABILITY IS. `fast.delegate: false,
|
|
33
|
+
* fast.review: true` advertises only the review, because telling a model about a
|
|
34
|
+
* `model: "fast"` field that the normalizer will downgrade every time is how you
|
|
35
|
+
* get a model that keeps asking for something it can never have.
|
|
36
|
+
*
|
|
37
|
+
* Returns `''` when neither capability is live, which the splice treats as "no
|
|
38
|
+
* block" and which keeps the byte-identity branch intact.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildFastBlock(params: FastBlockParams): string;
|
|
41
|
+
export interface ReviewSystemPromptParams {
|
|
42
|
+
/** `fast.reviewMaxChars` — quoted so the model and the clamp agree. */
|
|
43
|
+
maxChars: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The reviewer's own system prompt.
|
|
47
|
+
*
|
|
48
|
+
* IT ASKS FOR AN EXACT STRING when there is nothing to say (`OK - on track.`)
|
|
49
|
+
* and `normalizeCritique` still matches it with a NORMALIZED REGEX rather than
|
|
50
|
+
* equality (D-17). The two are not redundant: the prompt makes compliance the
|
|
51
|
+
* likely outcome, the regex makes non-compliance harmless.
|
|
52
|
+
*
|
|
53
|
+
* NO TOOLS ARE OFFERED AND NONE ARE MENTIONED (D-11). A tool-using reviewer
|
|
54
|
+
* would be a second agent editing the same working tree with none of the gates
|
|
55
|
+
* the lead's tools carry.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildReviewSystemPrompt(params: ReviewSystemPromptParams): string;
|
|
58
|
+
export interface ReviewInjectionParams {
|
|
59
|
+
turn: number;
|
|
60
|
+
model: string;
|
|
61
|
+
text: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The block that is actually steered into the run.
|
|
65
|
+
*
|
|
66
|
+
* THE ATTRIBUTES ARE NOT DECORATION. Steering delivers this in the `user` role
|
|
67
|
+
* (`agent-loop.ts:129`), so the wrapper is the only thing distinguishing an
|
|
68
|
+
* automated critique from something the human typed - and it is also what makes
|
|
69
|
+
* the residual case in §3.5.4a (a stranded block the reviewer may not clear)
|
|
70
|
+
* self-identifying if it ever does surface.
|
|
71
|
+
*/
|
|
72
|
+
export declare function renderReviewInjection(params: ReviewInjectionParams): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt text the fast tier adds (fast-model-tier §4.6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope, and these
|
|
5
|
+
* strings also reach a model that may be running in a `cmd.exe` terminal.
|
|
6
|
+
*
|
|
7
|
+
* `<fast_tier>` IS SPLICED CONDITIONALLY by `buildSystemPrompt`, which is what
|
|
8
|
+
* preserves invariant I-2: with the tier off the prompt is BYTE-IDENTICAL to the
|
|
9
|
+
* pre-feature output for a fixed tool array. An unconditional guidance line here
|
|
10
|
+
* would quietly break `--no-fast`, `--no-team`, `--no-skills` and `--no-todo`
|
|
11
|
+
* all at once.
|
|
12
|
+
*
|
|
13
|
+
* `FAST_BLOCK_VERSION` exists so a change to this wording is greppable from a
|
|
14
|
+
* behaviour report.
|
|
15
|
+
*/
|
|
16
|
+
import { FAST_BLOCK_VERSION } from './limits.js';
|
|
17
|
+
export { FAST_BLOCK_VERSION };
|
|
18
|
+
/**
|
|
19
|
+
* The lead's `<fast_tier>` block. Under 1100 characters on purpose: it is paid
|
|
20
|
+
* for on EVERY turn of every fast-enabled session, so anything that does not
|
|
21
|
+
* change a delegation decision does not belong in it (R-11).
|
|
22
|
+
*
|
|
23
|
+
* EACH PARAGRAPH IS PRESENT ONLY WHEN ITS CAPABILITY IS. `fast.delegate: false,
|
|
24
|
+
* fast.review: true` advertises only the review, because telling a model about a
|
|
25
|
+
* `model: "fast"` field that the normalizer will downgrade every time is how you
|
|
26
|
+
* get a model that keeps asking for something it can never have.
|
|
27
|
+
*
|
|
28
|
+
* Returns `''` when neither capability is live, which the splice treats as "no
|
|
29
|
+
* block" and which keeps the byte-identity branch intact.
|
|
30
|
+
*/
|
|
31
|
+
export function buildFastBlock(params) {
|
|
32
|
+
if (!params.delegate && !params.review)
|
|
33
|
+
return '';
|
|
34
|
+
const lines = [
|
|
35
|
+
'<fast_tier>',
|
|
36
|
+
`A second, cheaper model is available in this session (${params.model}).`,
|
|
37
|
+
];
|
|
38
|
+
if (params.delegate) {
|
|
39
|
+
lines.push('', 'Delegation: pass model:"fast" to a task subagent whose work is mechanical and', 'high-volume - reading or searching many files, summarizing long command output,', 'applying the same small edit in several places. Keep model:"main" (the default)', 'for design decisions, tricky debugging, and anything that has to be right the', 'first time. A fast subagent has the same tools and the same permissions as any', 'other; only the model differs.');
|
|
40
|
+
}
|
|
41
|
+
if (params.review) {
|
|
42
|
+
lines.push('', 'Reviews: every few turns a <fast_review> block may appear in the conversation.', 'It is an automated second opinion from the fast model - NOT a message from the', 'user. Treat it as advice: act on it when it is right, say so briefly and carry', 'on when it is not, and never ask the user to confirm it.');
|
|
43
|
+
}
|
|
44
|
+
lines.push('</fast_tier>');
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The reviewer's own system prompt.
|
|
49
|
+
*
|
|
50
|
+
* IT ASKS FOR AN EXACT STRING when there is nothing to say (`OK - on track.`)
|
|
51
|
+
* and `normalizeCritique` still matches it with a NORMALIZED REGEX rather than
|
|
52
|
+
* equality (D-17). The two are not redundant: the prompt makes compliance the
|
|
53
|
+
* likely outcome, the regex makes non-compliance harmless.
|
|
54
|
+
*
|
|
55
|
+
* NO TOOLS ARE OFFERED AND NONE ARE MENTIONED (D-11). A tool-using reviewer
|
|
56
|
+
* would be a second agent editing the same working tree with none of the gates
|
|
57
|
+
* the lead's tools carry.
|
|
58
|
+
*/
|
|
59
|
+
export function buildReviewSystemPrompt(params) {
|
|
60
|
+
const max = Math.max(1, Math.floor(params.maxChars) || 1);
|
|
61
|
+
return [
|
|
62
|
+
'You are a fast reviewer watching another agent work. You see a short digest of',
|
|
63
|
+
'its recent turns: the goal, the tools it called, and the tail of what it said.',
|
|
64
|
+
'',
|
|
65
|
+
'Answer ONE question: is this run still on track for the goal?',
|
|
66
|
+
'',
|
|
67
|
+
`If it is, reply with EXACTLY: OK - on track.`,
|
|
68
|
+
'If it is not, reply with plain prose naming the single most useful correction -',
|
|
69
|
+
`at most ${max} characters, no preamble, no code fences, no lists, no headings.`,
|
|
70
|
+
'',
|
|
71
|
+
'Things worth saying: repeated edits with no verification, a failing tool call',
|
|
72
|
+
'that was ignored, work that has drifted from the goal, an obvious cheaper route.',
|
|
73
|
+
'Things not worth saying: style opinions, praise, restating what the agent did.',
|
|
74
|
+
'',
|
|
75
|
+
'You cannot run tools and you are not talking to the user. You are advising the',
|
|
76
|
+
'agent, and it will decide.',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The block that is actually steered into the run.
|
|
81
|
+
*
|
|
82
|
+
* THE ATTRIBUTES ARE NOT DECORATION. Steering delivers this in the `user` role
|
|
83
|
+
* (`agent-loop.ts:129`), so the wrapper is the only thing distinguishing an
|
|
84
|
+
* automated critique from something the human typed - and it is also what makes
|
|
85
|
+
* the residual case in §3.5.4a (a stranded block the reviewer may not clear)
|
|
86
|
+
* self-identifying if it ever does surface.
|
|
87
|
+
*/
|
|
88
|
+
export function renderReviewInjection(params) {
|
|
89
|
+
return [
|
|
90
|
+
`<fast_review turn="${params.turn}" model="${params.model}">`,
|
|
91
|
+
params.text,
|
|
92
|
+
'</fast_review>',
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `resolveFastTier` — THE single authority on whether a fast tier exists and
|
|
3
|
+
* what it points at (fast-model-tier §3.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* EVERY CONSUMER READS THE TIER THROUGH THIS FUNCTION AND NOWHERE ELSE - the
|
|
8
|
+
* controller, the subagent factory, the reviewer, `/fast`, the settings screen
|
|
9
|
+
* and the status bar. Four call sites recomputing "is fast usable?" from raw
|
|
10
|
+
* config keys is exactly how a UI ends up showing a chip for a tier that refuses
|
|
11
|
+
* every call.
|
|
12
|
+
*
|
|
13
|
+
* IT IS ALSO RE-RUN ON EVERY CONFIG MUTATION, NOT ONCE (rule 8 / RV-3). Rules 2
|
|
14
|
+
* and 6 make `fast.provider: ''` and `fast.baseUrl: ''` INHERIT from
|
|
15
|
+
* `config.provider` / `config.baseUrl`, and rule 5 reads `apiKeys`. All of those
|
|
16
|
+
* are live: `/model`, `/provider`, the settings screen's save, `/reload` and a
|
|
17
|
+
* one-shot key edit each rewrite one of them mid-session. A tier resolved once
|
|
18
|
+
* at construction is a tier that can be describing a provider the session no
|
|
19
|
+
* longer uses.
|
|
20
|
+
*/
|
|
21
|
+
import { type CliConfig } from '../config/schema.js';
|
|
22
|
+
import type { FastTier } from './types.js';
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the fast tier from the CURRENT config.
|
|
25
|
+
*
|
|
26
|
+
* @param config The live effective config. Read fresh; never snapshot it.
|
|
27
|
+
* @param hasKey `AgentController.hasApiKey`, which already resolves the
|
|
28
|
+
* config-file key, the provider environment variable and the one-shot
|
|
29
|
+
* `--api-key` override. Injected rather than reimplemented, because a second
|
|
30
|
+
* key resolver is a second answer to "why does it say no key?".
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveFastTier(config: CliConfig, hasKey: (providerId: string) => boolean): FastTier;
|
|
33
|
+
/**
|
|
34
|
+
* The one-line notice for a tier the user switched on that cannot resolve, or
|
|
35
|
+
* `null` when there is nothing worth saying.
|
|
36
|
+
*
|
|
37
|
+
* `disabled` returns `null` - the tier being off because the user left it off is
|
|
38
|
+
* not news. The other three are CONFIGURATION MISTAKES that are otherwise
|
|
39
|
+
* completely invisible: nothing else in the session would ever mention them.
|
|
40
|
+
*/
|
|
41
|
+
export declare function describeFastTierProblem(tier: FastTier, provider: string): string | null;
|
|
42
|
+
/** The provider a tier resolution was about, for the notice above. */
|
|
43
|
+
export declare function fastProviderOf(config: CliConfig): string;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `resolveFastTier` — THE single authority on whether a fast tier exists and
|
|
3
|
+
* what it points at (fast-model-tier §3.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* EVERY CONSUMER READS THE TIER THROUGH THIS FUNCTION AND NOWHERE ELSE - the
|
|
8
|
+
* controller, the subagent factory, the reviewer, `/fast`, the settings screen
|
|
9
|
+
* and the status bar. Four call sites recomputing "is fast usable?" from raw
|
|
10
|
+
* config keys is exactly how a UI ends up showing a chip for a tier that refuses
|
|
11
|
+
* every call.
|
|
12
|
+
*
|
|
13
|
+
* IT IS ALSO RE-RUN ON EVERY CONFIG MUTATION, NOT ONCE (rule 8 / RV-3). Rules 2
|
|
14
|
+
* and 6 make `fast.provider: ''` and `fast.baseUrl: ''` INHERIT from
|
|
15
|
+
* `config.provider` / `config.baseUrl`, and rule 5 reads `apiKeys`. All of those
|
|
16
|
+
* are live: `/model`, `/provider`, the settings screen's save, `/reload` and a
|
|
17
|
+
* one-shot key edit each rewrite one of them mid-session. A tier resolved once
|
|
18
|
+
* at construction is a tier that can be describing a provider the session no
|
|
19
|
+
* longer uses.
|
|
20
|
+
*/
|
|
21
|
+
import { isAdapterProvider } from '../config/schema.js';
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the fast tier from the CURRENT config.
|
|
24
|
+
*
|
|
25
|
+
* @param config The live effective config. Read fresh; never snapshot it.
|
|
26
|
+
* @param hasKey `AgentController.hasApiKey`, which already resolves the
|
|
27
|
+
* config-file key, the provider environment variable and the one-shot
|
|
28
|
+
* `--api-key` override. Injected rather than reimplemented, because a second
|
|
29
|
+
* key resolver is a second answer to "why does it say no key?".
|
|
30
|
+
*/
|
|
31
|
+
export function resolveFastTier(config, hasKey) {
|
|
32
|
+
const fast = config.fast;
|
|
33
|
+
// 1. The switch.
|
|
34
|
+
if (!fast || fast.enabled !== true)
|
|
35
|
+
return { ok: false, reason: 'disabled' };
|
|
36
|
+
// 2. An empty provider INHERITS the main one. This is what makes `fast.model`
|
|
37
|
+
// alone a complete configuration for the overwhelmingly common case ("same
|
|
38
|
+
// vendor, cheaper model").
|
|
39
|
+
const provider = fast.provider.trim().length > 0 ? fast.provider.trim() : config.provider;
|
|
40
|
+
// 3. An empty model is NOT CONFIGURED, and deliberately does not inherit
|
|
41
|
+
// (D-4). Silently falling back to the main model would make R-e - opting in
|
|
42
|
+
// to fast == main, which is legal - indistinguishable from a typo.
|
|
43
|
+
const modelId = fast.model.trim();
|
|
44
|
+
if (modelId.length === 0)
|
|
45
|
+
return { ok: false, reason: 'no_model' };
|
|
46
|
+
// 4 / 5. Both are reported separately because they send the user to two
|
|
47
|
+
// different settings.
|
|
48
|
+
if (!isAdapterProvider(provider))
|
|
49
|
+
return { ok: false, reason: 'no_adapter' };
|
|
50
|
+
if (!hasKey(provider))
|
|
51
|
+
return { ok: false, reason: 'no_key' };
|
|
52
|
+
// 6. A fast tier on the SAME provider inherits the session's base URL: a user
|
|
53
|
+
// pointing the CLI at a gateway means both models, not one. A fast tier on
|
|
54
|
+
// a DIFFERENT provider does not, because that base URL belongs to another
|
|
55
|
+
// API and would be an immediate 404.
|
|
56
|
+
const explicitBaseUrl = fast.baseUrl.trim();
|
|
57
|
+
const baseUrl = explicitBaseUrl.length > 0
|
|
58
|
+
? explicitBaseUrl
|
|
59
|
+
: provider === config.provider
|
|
60
|
+
? config.baseUrl
|
|
61
|
+
: undefined;
|
|
62
|
+
const ref = {
|
|
63
|
+
providerId: provider,
|
|
64
|
+
modelId,
|
|
65
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
ref,
|
|
70
|
+
thinkingLevel: fast.thinkingLevel,
|
|
71
|
+
// 7. DISPLAY ONLY, and no branch anywhere may read it (R-13). "The two tiers
|
|
72
|
+
// resolve to the same model, so skip the work" is the tempting
|
|
73
|
+
// optimization that silently turns R-e into a no-op: the user is allowed
|
|
74
|
+
// to choose it, and the cost of the review is real either way.
|
|
75
|
+
sameAsMain: provider === config.provider && modelId === config.model,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The one-line notice for a tier the user switched on that cannot resolve, or
|
|
80
|
+
* `null` when there is nothing worth saying.
|
|
81
|
+
*
|
|
82
|
+
* `disabled` returns `null` - the tier being off because the user left it off is
|
|
83
|
+
* not news. The other three are CONFIGURATION MISTAKES that are otherwise
|
|
84
|
+
* completely invisible: nothing else in the session would ever mention them.
|
|
85
|
+
*/
|
|
86
|
+
export function describeFastTierProblem(tier, provider) {
|
|
87
|
+
if (tier.ok)
|
|
88
|
+
return null;
|
|
89
|
+
switch (tier.reason) {
|
|
90
|
+
case 'no_model':
|
|
91
|
+
return 'fast.enabled is on but fast.model is empty - the fast tier is off. ' +
|
|
92
|
+
'Set it with /fast model <id>.';
|
|
93
|
+
case 'no_adapter':
|
|
94
|
+
return `Provider "${provider}" has no adapter - the fast tier is off.`;
|
|
95
|
+
case 'no_key':
|
|
96
|
+
return `No API key for ${provider} - the fast tier is off.`;
|
|
97
|
+
default:
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The provider a tier resolution was about, for the notice above. */
|
|
102
|
+
export function fastProviderOf(config) {
|
|
103
|
+
const explicit = config.fast?.provider.trim() ?? '';
|
|
104
|
+
return explicit.length > 0 ? explicit : config.provider;
|
|
105
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounded review call (fast-model-tier §3.5.3, extracted here by
|
|
3
|
+
* fast-model-tier-hardening W3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THIS MODULE DOES NOT OWN AN `AbortController`, AND THAT IS A P0 PROHIBITION
|
|
8
|
+
* RATHER THAN AN OVERSIGHT (RV-H1 / AC-H16). Encapsulating a controller with the
|
|
9
|
+
* call it aborts is correct everywhere else in this codebase, which is exactly
|
|
10
|
+
* why it has to be forbidden in writing here:
|
|
11
|
+
*
|
|
12
|
+
* - `callAbort` and `callAbortReason` are SHARED MUTABLE STATE between the
|
|
13
|
+
* call and the lifecycle. The call arms them and reads the reason in its
|
|
14
|
+
* catch; `cancelCall()` — reached from `abort()`, `endRun()` and
|
|
15
|
+
* `dispose()` — stamps `'cancelled'` BEFORE aborting.
|
|
16
|
+
* - Once the controller is private to this module, that stamp becomes
|
|
17
|
+
* impossible, every cancelled review falls into the failure branch, and each
|
|
18
|
+
* one scores a strike. Three ordinary runs then self-disable the tier for the
|
|
19
|
+
* rest of the session behind a warn quoting an internal stream message.
|
|
20
|
+
* - It cannot be recovered at the catch: the retry layer returns SILENTLY on
|
|
21
|
+
* abort (`retry.ts` G3, deliberately — a user Esc must not be blamed on the
|
|
22
|
+
* network), so a cancellation and a timeout arrive as byte-identical bare
|
|
23
|
+
* `Error`s carrying no `errorType`.
|
|
24
|
+
*
|
|
25
|
+
* ROUND 1 SHIPPED THAT DEFECT AND FIXED IT AS IF-7. So this module RECEIVES the
|
|
26
|
+
* signal and a timeout callback, and returns an outcome. It does not create,
|
|
27
|
+
* own, or classify the abort. `fast-reviewer.test.ts`'s cancellation cases are
|
|
28
|
+
* the behavioural guard; they were written before this file existed.
|
|
29
|
+
*/
|
|
30
|
+
import type { AssistantMessage, LLMRequest, ModelRef, TokenUsage } from '@aragon-agent/core';
|
|
31
|
+
/** The assistant text of a completion, or `''` for a tool-only answer. */
|
|
32
|
+
export declare function assistantText(message: AssistantMessage | undefined): string;
|
|
33
|
+
export declare function errorText(err: unknown): string;
|
|
34
|
+
/** Whether a failure is the provider being busy rather than the tier being wrong. */
|
|
35
|
+
export declare function classifyReviewFailure(err: unknown): {
|
|
36
|
+
errorType: string;
|
|
37
|
+
transient: boolean;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The review's spend.
|
|
41
|
+
*
|
|
42
|
+
* OPTIONAL BY DECLARATION (RV-11). All three adapters populate `usage` before
|
|
43
|
+
* yielding `done`, but the field is `usage?` on `AssistantMessage`, and
|
|
44
|
+
* `computeCost(undefined, cost)` is a NaN that reaches the status bar and stays
|
|
45
|
+
* there for the session.
|
|
46
|
+
*/
|
|
47
|
+
export declare function usageOf(message: AssistantMessage | undefined): {
|
|
48
|
+
usage: TokenUsage;
|
|
49
|
+
missing: boolean;
|
|
50
|
+
};
|
|
51
|
+
export declare function accumulateUsage(total: TokenUsage, next: TokenUsage): TokenUsage;
|
|
52
|
+
export interface ReviewRequestParams {
|
|
53
|
+
ref: ModelRef;
|
|
54
|
+
apiKey: string;
|
|
55
|
+
digest: string;
|
|
56
|
+
/** `fast.reviewMaxChars` — the ceiling the system prompt states. */
|
|
57
|
+
reviewMaxChars: number;
|
|
58
|
+
/** Created and owned by `reviewer.ts`; forwarded into `withRetry` from there. */
|
|
59
|
+
signal: AbortSignal;
|
|
60
|
+
now: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The request one review sends. NO TOOLS, no thinking, one bounded user message.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildReviewRequest(params: ReviewRequestParams): LLMRequest;
|
|
66
|
+
export interface ReviewCallParams extends ReviewRequestParams {
|
|
67
|
+
complete(providerId: string, request: LLMRequest): Promise<AssistantMessage>;
|
|
68
|
+
/**
|
|
69
|
+
* Fired when `reviewTimeoutMs` elapses.
|
|
70
|
+
*
|
|
71
|
+
* THE CALLER ABORTS, NOT THIS MODULE. `reviewer.ts` implements this as "stamp
|
|
72
|
+
* `callAbortReason = 'timeout'`, then abort", which is what keeps a timeout
|
|
73
|
+
* (a strike) distinguishable from a cancellation (not a strike) once the retry
|
|
74
|
+
* layer has flattened both into the same bare `Error` — see the file header.
|
|
75
|
+
*/
|
|
76
|
+
onTimeout(): void;
|
|
77
|
+
}
|
|
78
|
+
export type ReviewCallOutcome = {
|
|
79
|
+
ok: true;
|
|
80
|
+
message: AssistantMessage;
|
|
81
|
+
} | {
|
|
82
|
+
ok: false;
|
|
83
|
+
error: unknown;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Send one review and bound it in time.
|
|
87
|
+
*
|
|
88
|
+
* THE TIMEOUT IS AUTHORITATIVE OVER THE RETRY LAYER'S SLEEP, and that is not
|
|
89
|
+
* automatic (RV-H12). The fast registry spreads `DEFAULT_RETRY_POLICY`, so it
|
|
90
|
+
* inherits `respectRetryAfter: true`, and a `Retry-After` is applied with
|
|
91
|
+
* `Math.max` AFTER the `maxDelayMs` clamp (`retry.ts:316-324`) — one honoured
|
|
92
|
+
* retry may therefore ask to sleep up to the 60 s `retryAfterCeilingMs`. Two
|
|
93
|
+
* inherited properties stop that from pinning the single-flight slot for a
|
|
94
|
+
* minute: `defaultSleep` resolves on either the timer or the signal's `abort`
|
|
95
|
+
* (`retry.ts:417-427`, guard G2), and the `signal` this request carries is
|
|
96
|
+
* forwarded into `withRetry` by `ProviderRegistry.stream()`. A change to either
|
|
97
|
+
* is a change to this design.
|
|
98
|
+
*/
|
|
99
|
+
export declare function runReviewCall(params: ReviewCallParams): Promise<ReviewCallOutcome>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounded review call (fast-model-tier §3.5.3, extracted here by
|
|
3
|
+
* fast-model-tier-hardening W3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THIS MODULE DOES NOT OWN AN `AbortController`, AND THAT IS A P0 PROHIBITION
|
|
8
|
+
* RATHER THAN AN OVERSIGHT (RV-H1 / AC-H16). Encapsulating a controller with the
|
|
9
|
+
* call it aborts is correct everywhere else in this codebase, which is exactly
|
|
10
|
+
* why it has to be forbidden in writing here:
|
|
11
|
+
*
|
|
12
|
+
* - `callAbort` and `callAbortReason` are SHARED MUTABLE STATE between the
|
|
13
|
+
* call and the lifecycle. The call arms them and reads the reason in its
|
|
14
|
+
* catch; `cancelCall()` — reached from `abort()`, `endRun()` and
|
|
15
|
+
* `dispose()` — stamps `'cancelled'` BEFORE aborting.
|
|
16
|
+
* - Once the controller is private to this module, that stamp becomes
|
|
17
|
+
* impossible, every cancelled review falls into the failure branch, and each
|
|
18
|
+
* one scores a strike. Three ordinary runs then self-disable the tier for the
|
|
19
|
+
* rest of the session behind a warn quoting an internal stream message.
|
|
20
|
+
* - It cannot be recovered at the catch: the retry layer returns SILENTLY on
|
|
21
|
+
* abort (`retry.ts` G3, deliberately — a user Esc must not be blamed on the
|
|
22
|
+
* network), so a cancellation and a timeout arrive as byte-identical bare
|
|
23
|
+
* `Error`s carrying no `errorType`.
|
|
24
|
+
*
|
|
25
|
+
* ROUND 1 SHIPPED THAT DEFECT AND FIXED IT AS IF-7. So this module RECEIVES the
|
|
26
|
+
* signal and a timeout callback, and returns an outcome. It does not create,
|
|
27
|
+
* own, or classify the abort. `fast-reviewer.test.ts`'s cancellation cases are
|
|
28
|
+
* the behavioural guard; they were written before this file existed.
|
|
29
|
+
*/
|
|
30
|
+
import { FAST_LIMITS } from './limits.js';
|
|
31
|
+
import { buildReviewSystemPrompt } from './prompt.js';
|
|
32
|
+
/**
|
|
33
|
+
* The two `LLMErrorType`s the retry layer classifies as retryable.
|
|
34
|
+
*
|
|
35
|
+
* THESE ARE TRANSIENT AND DO NOT COUNT AS STRIKES (D-24 / R-17). Self-disabling
|
|
36
|
+
* exists for a MISCONFIGURED tier - a wrong model id, a dead gateway, a key with
|
|
37
|
+
* no access - because that is the case a user must be told about, since it will
|
|
38
|
+
* never fix itself. A fast provider that is merely busy for ninety seconds is
|
|
39
|
+
* the opposite. This matters MORE since round 2 gave the reviewer a fail-fast
|
|
40
|
+
* registry (W2): fewer retries means transients surface more often, so the
|
|
41
|
+
* strike counter must keep ignoring them or the fail-fast policy would cause the
|
|
42
|
+
* self-disable round 1 worked to prevent.
|
|
43
|
+
*/
|
|
44
|
+
const TRANSIENT_ERROR_TYPES = new Set(['rate_limit', 'overloaded']);
|
|
45
|
+
/** The assistant text of a completion, or `''` for a tool-only answer. */
|
|
46
|
+
export function assistantText(message) {
|
|
47
|
+
if (!message || !Array.isArray(message.content))
|
|
48
|
+
return '';
|
|
49
|
+
return message.content
|
|
50
|
+
.filter((b) => b.type === 'text')
|
|
51
|
+
.map((b) => b.text)
|
|
52
|
+
.join('')
|
|
53
|
+
.trim();
|
|
54
|
+
}
|
|
55
|
+
export function errorText(err) {
|
|
56
|
+
return err instanceof Error ? err.message : String(err);
|
|
57
|
+
}
|
|
58
|
+
/** Whether a failure is the provider being busy rather than the tier being wrong. */
|
|
59
|
+
export function classifyReviewFailure(err) {
|
|
60
|
+
const raw = err?.errorType;
|
|
61
|
+
const errorType = typeof raw === 'string' ? raw : 'unknown';
|
|
62
|
+
return { errorType, transient: TRANSIENT_ERROR_TYPES.has(errorType) };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The review's spend.
|
|
66
|
+
*
|
|
67
|
+
* OPTIONAL BY DECLARATION (RV-11). All three adapters populate `usage` before
|
|
68
|
+
* yielding `done`, but the field is `usage?` on `AssistantMessage`, and
|
|
69
|
+
* `computeCost(undefined, cost)` is a NaN that reaches the status bar and stays
|
|
70
|
+
* there for the session.
|
|
71
|
+
*/
|
|
72
|
+
export function usageOf(message) {
|
|
73
|
+
const usage = message?.usage;
|
|
74
|
+
return {
|
|
75
|
+
usage: usage ?? { inputTokens: 0, outputTokens: 0 },
|
|
76
|
+
missing: usage === undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function accumulateUsage(total, next) {
|
|
80
|
+
return {
|
|
81
|
+
inputTokens: total.inputTokens + next.inputTokens,
|
|
82
|
+
outputTokens: total.outputTokens + next.outputTokens,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The request one review sends. NO TOOLS, no thinking, one bounded user message.
|
|
87
|
+
*/
|
|
88
|
+
export function buildReviewRequest(params) {
|
|
89
|
+
return {
|
|
90
|
+
model: params.ref.modelId,
|
|
91
|
+
...(params.ref.baseUrl ? { baseUrl: params.ref.baseUrl } : {}),
|
|
92
|
+
apiKey: params.apiKey,
|
|
93
|
+
systemPrompt: buildReviewSystemPrompt({ maxChars: params.reviewMaxChars }),
|
|
94
|
+
messages: [{ role: 'user', content: params.digest, timestamp: params.now }],
|
|
95
|
+
maxTokens: FAST_LIMITS.reviewOutputTokens,
|
|
96
|
+
// PAIRED WITH `temperature: 0` ON PURPOSE: the Anthropic adapter deletes
|
|
97
|
+
// `temperature` whenever a thinking budget is set (`anthropic.ts:395-402`),
|
|
98
|
+
// so these two are one choice and not two.
|
|
99
|
+
thinkingLevel: 'off',
|
|
100
|
+
temperature: 0,
|
|
101
|
+
signal: params.signal,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Send one review and bound it in time.
|
|
106
|
+
*
|
|
107
|
+
* THE TIMEOUT IS AUTHORITATIVE OVER THE RETRY LAYER'S SLEEP, and that is not
|
|
108
|
+
* automatic (RV-H12). The fast registry spreads `DEFAULT_RETRY_POLICY`, so it
|
|
109
|
+
* inherits `respectRetryAfter: true`, and a `Retry-After` is applied with
|
|
110
|
+
* `Math.max` AFTER the `maxDelayMs` clamp (`retry.ts:316-324`) — one honoured
|
|
111
|
+
* retry may therefore ask to sleep up to the 60 s `retryAfterCeilingMs`. Two
|
|
112
|
+
* inherited properties stop that from pinning the single-flight slot for a
|
|
113
|
+
* minute: `defaultSleep` resolves on either the timer or the signal's `abort`
|
|
114
|
+
* (`retry.ts:417-427`, guard G2), and the `signal` this request carries is
|
|
115
|
+
* forwarded into `withRetry` by `ProviderRegistry.stream()`. A change to either
|
|
116
|
+
* is a change to this design.
|
|
117
|
+
*/
|
|
118
|
+
export async function runReviewCall(params) {
|
|
119
|
+
const timer = setTimeout(params.onTimeout, FAST_LIMITS.reviewTimeoutMs);
|
|
120
|
+
try {
|
|
121
|
+
const message = await params.complete(params.ref.providerId, buildReviewRequest(params));
|
|
122
|
+
return { ok: true, message };
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return { ok: false, error };
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
}
|