@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
package/dist/config/store.js
CHANGED
|
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
|
|
|
14
14
|
import process from 'node:process';
|
|
15
15
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
16
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
-
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampMaxTokens, clampSkillsConfig, clampTeamConfig, clampTodoConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
17
|
+
import { CONFIG_VERSION, DEFAULT_CONFIG, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
18
18
|
/**
|
|
19
19
|
* Normalize `maxTokens` for both directions of the config file.
|
|
20
20
|
*
|
|
@@ -126,6 +126,14 @@ export function loadPersistedConfig() {
|
|
|
126
126
|
// the READ half of the pair that keeps `/todo panel off` from silently
|
|
127
127
|
// unregistering the tool on the next launch (P0-1 / AC-35).
|
|
128
128
|
todo: clampTodoConfig({ ...DEFAULT_CONFIG.todo, ...(partial.todo ?? {}) }),
|
|
129
|
+
// `retry` is the sixth. The READ half: without it a config file with no
|
|
130
|
+
// `retry` key yields `undefined` and `toRetryPolicy` throws on the value it
|
|
131
|
+
// is handed (R-9 / AC-20).
|
|
132
|
+
retry: clampRetryConfig({ ...DEFAULT_CONFIG.retry, ...(partial.retry ?? {}) }),
|
|
133
|
+
// `fast` is the seventh. Same one-level rule, same merge — and this is the
|
|
134
|
+
// READ half of the pair that keeps `/fast review 8` from silently switching
|
|
135
|
+
// the whole tier off on the next launch (C-3 / R-10).
|
|
136
|
+
fast: clampFastConfig({ ...DEFAULT_CONFIG.fast, ...(partial.fast ?? {}) }),
|
|
129
137
|
};
|
|
130
138
|
}
|
|
131
139
|
/**
|
|
@@ -177,6 +185,17 @@ export function updatePersistedConfig(patch) {
|
|
|
177
185
|
// spread turns into "turning the panel off silently unregisters the tool",
|
|
178
186
|
// permanently, from the next launch onward, with nothing anywhere saying so.
|
|
179
187
|
todo: clampTodoConfig({ ...current.todo, ...(patch.todo ?? {}) }),
|
|
188
|
+
// The sixth section. `/retry max 5` sends `{ retry: { maxRetries: 5 } }`,
|
|
189
|
+
// which a shallow spread turns into "changing the retry count silently
|
|
190
|
+
// reverted `respectRetryAfter` and `jitter`" (R-9 / AC-19).
|
|
191
|
+
retry: clampRetryConfig({ ...current.retry, ...(patch.retry ?? {}) }),
|
|
192
|
+
// The seventh section, and THE WRITE HALF IS THE ONE THAT BITES (R-10).
|
|
193
|
+
// `/fast review 8` sends `{ fast: { reviewEveryTurns: 8 } }`; without this
|
|
194
|
+
// line the shallow top-level spread REPLACES the section, `enabled` is
|
|
195
|
+
// written absent, and the tier the user configured yesterday is gone
|
|
196
|
+
// tomorrow with no error anywhere. This package has paid for that once
|
|
197
|
+
// already, in `todo` (P0-1).
|
|
198
|
+
fast: clampFastConfig({ ...current.fast, ...(patch.fast ?? {}) }),
|
|
180
199
|
};
|
|
181
200
|
writeConfigFile(merged);
|
|
182
201
|
return merged;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `normalizeCritique` — turn whatever the fast model wrote into one of three
|
|
3
|
+
* outcomes (fast-model-tier §3.5.3). PURE: no I/O, no state.
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* What a review amounted to.
|
|
9
|
+
*
|
|
10
|
+
* `ok` and `empty` are NOT INJECTED (D-16). They still produce a transcript
|
|
11
|
+
* entry and a log record, so the user can see the harness is alive, but the lead
|
|
12
|
+
* pays no context for the message "nothing to say".
|
|
13
|
+
*/
|
|
14
|
+
export type Critique = {
|
|
15
|
+
kind: 'ok';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'advice';
|
|
18
|
+
text: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'empty';
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* THE ON-TRACK TEST IS A NORMALIZED REGEX, NEVER STRING EQUALITY (D-17).
|
|
24
|
+
*
|
|
25
|
+
* The system prompt asks for the exact string `OK - on track.`, and models
|
|
26
|
+
* mostly comply - but one that writes `Ok, on track` would, under an equality
|
|
27
|
+
* test, be classified as ADVICE and injected into the main context on every
|
|
28
|
+
* cycle, forever, saying nothing. That is worse than having no reviewer: it is a
|
|
29
|
+
* permanent context tax that looks like the feature working.
|
|
30
|
+
*
|
|
31
|
+
* So: trim, lowercase, drop trailing punctuation, then `/^ok\b.*on track/`.
|
|
32
|
+
* `'OK, but the tests are failing'` still classifies as advice, because it does
|
|
33
|
+
* not say "on track".
|
|
34
|
+
*/
|
|
35
|
+
export declare function isOnTrack(text: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Normalize and classify one raw review answer.
|
|
38
|
+
*
|
|
39
|
+
* @param raw Whatever the model returned.
|
|
40
|
+
* @param maxChars `fast.reviewMaxChars` — the ceiling the user chose on how much
|
|
41
|
+
* of their own context a critique may spend.
|
|
42
|
+
*/
|
|
43
|
+
export declare function normalizeCritique(raw: string, maxChars: number): Critique;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `normalizeCritique` — turn whatever the fast model wrote into one of three
|
|
3
|
+
* outcomes (fast-model-tier §3.5.3). PURE: no I/O, no state.
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*/
|
|
7
|
+
/** Strip a fenced code block wrapper the model may have put around its prose. */
|
|
8
|
+
function stripFences(raw) {
|
|
9
|
+
const trimmed = raw.trim();
|
|
10
|
+
if (!trimmed.startsWith('```'))
|
|
11
|
+
return trimmed;
|
|
12
|
+
const withoutOpen = trimmed.replace(/^```[a-zA-Z0-9_-]*\s*\r?\n?/, '');
|
|
13
|
+
return withoutOpen.replace(/\r?\n?```\s*$/, '').trim();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* THE ON-TRACK TEST IS A NORMALIZED REGEX, NEVER STRING EQUALITY (D-17).
|
|
17
|
+
*
|
|
18
|
+
* The system prompt asks for the exact string `OK - on track.`, and models
|
|
19
|
+
* mostly comply - but one that writes `Ok, on track` would, under an equality
|
|
20
|
+
* test, be classified as ADVICE and injected into the main context on every
|
|
21
|
+
* cycle, forever, saying nothing. That is worse than having no reviewer: it is a
|
|
22
|
+
* permanent context tax that looks like the feature working.
|
|
23
|
+
*
|
|
24
|
+
* So: trim, lowercase, drop trailing punctuation, then `/^ok\b.*on track/`.
|
|
25
|
+
* `'OK, but the tests are failing'` still classifies as advice, because it does
|
|
26
|
+
* not say "on track".
|
|
27
|
+
*/
|
|
28
|
+
export function isOnTrack(text) {
|
|
29
|
+
const normalized = text
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[.!\s]+$/, '')
|
|
33
|
+
.replace(/\s+/g, ' ');
|
|
34
|
+
return /^ok\b.*on track/.test(normalized);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalize and classify one raw review answer.
|
|
38
|
+
*
|
|
39
|
+
* @param raw Whatever the model returned.
|
|
40
|
+
* @param maxChars `fast.reviewMaxChars` — the ceiling the user chose on how much
|
|
41
|
+
* of their own context a critique may spend.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeCritique(raw, maxChars) {
|
|
44
|
+
const stripped = stripFences(raw ?? '');
|
|
45
|
+
// Whitespace is collapsed BEFORE the length test: a model that answers with
|
|
46
|
+
// four blank lines and a sentence must not spend its budget on the blanks.
|
|
47
|
+
const collapsed = stripped.replace(/\s+/g, ' ').trim();
|
|
48
|
+
if (collapsed.length === 0)
|
|
49
|
+
return { kind: 'empty' };
|
|
50
|
+
if (isOnTrack(collapsed))
|
|
51
|
+
return { kind: 'ok' };
|
|
52
|
+
const limit = Math.max(1, Math.floor(maxChars) || 1);
|
|
53
|
+
if (collapsed.length <= limit)
|
|
54
|
+
return { kind: 'advice', text: collapsed };
|
|
55
|
+
// Clamp on a word boundary when one is close enough that the sentence still
|
|
56
|
+
// reads; otherwise a hard cut, because a critique is prose and a mid-word
|
|
57
|
+
// truncation is what makes advice look like corruption.
|
|
58
|
+
const cut = collapsed.slice(0, limit);
|
|
59
|
+
const space = cut.lastIndexOf(' ');
|
|
60
|
+
const text = space > limit * 0.6 ? cut.slice(0, space) : cut;
|
|
61
|
+
return { kind: 'advice', text };
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildReviewDigest` — the bounded text one review is asked about
|
|
3
|
+
* (fast-model-tier §3.5.2). PURE: no I/O, no clock, no state.
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THE REVIEWER NEVER READS `agent.state.messages`, AND THAT IS THE SINGLE MOST
|
|
8
|
+
* IMPORTANT ECONOMIC DECISION IN THE FEATURE (D-6 / R-2). That history holds
|
|
9
|
+
* whole file bodies: a session that has read four 60 KB files carries 240 KB of
|
|
10
|
+
* them. Shipping it to the "cheap" model to ask whether the run is on track
|
|
11
|
+
* would make a review cost MORE than the main-model turn it is reviewing - the
|
|
12
|
+
* exact inversion this feature exists to prevent. The digest is bounded at
|
|
13
|
+
* `FAST_LIMITS.digestMaxChars` BY CONSTRUCTION, so a review's input cost is flat
|
|
14
|
+
* regardless of session length.
|
|
15
|
+
*
|
|
16
|
+
* Anyone tempted to "just pass the messages, the model has the context anyway"
|
|
17
|
+
* is about to turn a 400-token call into a 60 000-token one, and nothing in the
|
|
18
|
+
* UI would show it except the bill.
|
|
19
|
+
*/
|
|
20
|
+
import type { TurnFrame } from './types.js';
|
|
21
|
+
export interface ReviewDigestParams {
|
|
22
|
+
/** The reviewer's ring buffer, oldest first. */
|
|
23
|
+
frames: TurnFrame[];
|
|
24
|
+
/** The user's request for this run, already clamped by the caller. */
|
|
25
|
+
goal: string;
|
|
26
|
+
/** `fast.reviewContextTurns` — how many SEALED frames to include. */
|
|
27
|
+
contextTurns: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the digest.
|
|
31
|
+
*
|
|
32
|
+
* SEALED FRAMES ONLY (RV-2 / D-22). The turn being reviewed is sealed by
|
|
33
|
+
* definition - sealing is what triggered the review - so the window is the
|
|
34
|
+
* newest `contextTurns` sealed frames, and a frame that is still accumulating is
|
|
35
|
+
* never half-rendered into a prompt.
|
|
36
|
+
*
|
|
37
|
+
* NEWEST FRAMES SURVIVE THE BOUND. When the assembled text exceeds
|
|
38
|
+
* `digestMaxChars` the OLDEST frame is dropped and the document is rebuilt,
|
|
39
|
+
* because the reviewer's question is about what just happened. Trimming from the
|
|
40
|
+
* tail would hand it the beginning of a run and ask about the end of one.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildReviewDigest(params: ReviewDigestParams): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildReviewDigest` — the bounded text one review is asked about
|
|
3
|
+
* (fast-model-tier §3.5.2). PURE: no I/O, no clock, no state.
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THE REVIEWER NEVER READS `agent.state.messages`, AND THAT IS THE SINGLE MOST
|
|
8
|
+
* IMPORTANT ECONOMIC DECISION IN THE FEATURE (D-6 / R-2). That history holds
|
|
9
|
+
* whole file bodies: a session that has read four 60 KB files carries 240 KB of
|
|
10
|
+
* them. Shipping it to the "cheap" model to ask whether the run is on track
|
|
11
|
+
* would make a review cost MORE than the main-model turn it is reviewing - the
|
|
12
|
+
* exact inversion this feature exists to prevent. The digest is bounded at
|
|
13
|
+
* `FAST_LIMITS.digestMaxChars` BY CONSTRUCTION, so a review's input cost is flat
|
|
14
|
+
* regardless of session length.
|
|
15
|
+
*
|
|
16
|
+
* Anyone tempted to "just pass the messages, the model has the context anyway"
|
|
17
|
+
* is about to turn a 400-token call into a 60 000-token one, and nothing in the
|
|
18
|
+
* UI would show it except the bill.
|
|
19
|
+
*/
|
|
20
|
+
import { FAST_LIMITS } from './limits.js';
|
|
21
|
+
/** Clip to `maxChars` on a word boundary when one is close enough to matter. */
|
|
22
|
+
function clip(text, maxChars) {
|
|
23
|
+
if (text.length <= maxChars)
|
|
24
|
+
return text;
|
|
25
|
+
const cut = text.slice(0, maxChars);
|
|
26
|
+
const space = cut.lastIndexOf(' ');
|
|
27
|
+
return space > maxChars * 0.6 ? cut.slice(0, space) : cut;
|
|
28
|
+
}
|
|
29
|
+
/** One tool as the digest names it: `read_file(src/app.ts) 12ms`. */
|
|
30
|
+
function renderTool(tool) {
|
|
31
|
+
const arg = tool.arg ? `(${clip(tool.arg, FAST_LIMITS.toolArgChars)})` : '';
|
|
32
|
+
const failed = tool.isError ? ' FAILED' : '';
|
|
33
|
+
const ms = tool.ms === undefined ? '' : ` ${Math.round(tool.ms)}ms`;
|
|
34
|
+
return `${tool.name}${arg}${failed}${ms}`;
|
|
35
|
+
}
|
|
36
|
+
function renderFrame(frame) {
|
|
37
|
+
const lines = [`Turn ${frame.index}:`];
|
|
38
|
+
if (frame.tools.length > 0) {
|
|
39
|
+
lines.push(` tools: ${frame.tools.map(renderTool).join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
lines.push(' tools: (none)');
|
|
43
|
+
}
|
|
44
|
+
if (frame.error)
|
|
45
|
+
lines.push(` error: ${clip(frame.error, 200)}`);
|
|
46
|
+
const tail = frame.textTail.trim();
|
|
47
|
+
if (tail.length > 0)
|
|
48
|
+
lines.push(` said: ${clip(tail, FAST_LIMITS.turnTailChars)}`);
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the digest.
|
|
53
|
+
*
|
|
54
|
+
* SEALED FRAMES ONLY (RV-2 / D-22). The turn being reviewed is sealed by
|
|
55
|
+
* definition - sealing is what triggered the review - so the window is the
|
|
56
|
+
* newest `contextTurns` sealed frames, and a frame that is still accumulating is
|
|
57
|
+
* never half-rendered into a prompt.
|
|
58
|
+
*
|
|
59
|
+
* NEWEST FRAMES SURVIVE THE BOUND. When the assembled text exceeds
|
|
60
|
+
* `digestMaxChars` the OLDEST frame is dropped and the document is rebuilt,
|
|
61
|
+
* because the reviewer's question is about what just happened. Trimming from the
|
|
62
|
+
* tail would hand it the beginning of a run and ask about the end of one.
|
|
63
|
+
*/
|
|
64
|
+
export function buildReviewDigest(params) {
|
|
65
|
+
const sealed = params.frames.filter((f) => f.sealed);
|
|
66
|
+
const want = Math.max(1, Math.floor(params.contextTurns) || 1);
|
|
67
|
+
let window = sealed.slice(-want);
|
|
68
|
+
const goal = clip(params.goal.trim(), FAST_LIMITS.goalChars);
|
|
69
|
+
const assemble = (frames) => [
|
|
70
|
+
`Goal: ${goal.length > 0 ? goal : '(not captured)'}`,
|
|
71
|
+
'',
|
|
72
|
+
...frames.flatMap(renderFrame),
|
|
73
|
+
].join('\n');
|
|
74
|
+
let out = assemble(window);
|
|
75
|
+
while (out.length > FAST_LIMITS.digestMaxChars && window.length > 1) {
|
|
76
|
+
window = window.slice(1);
|
|
77
|
+
out = assemble(window);
|
|
78
|
+
}
|
|
79
|
+
// A single frame can still be over budget on its own (a 600-char tail plus
|
|
80
|
+
// twelve tools), so the hard clamp is unconditional. It is a CEILING, not the
|
|
81
|
+
// trimming strategy: the loop above is what decides WHAT is lost.
|
|
82
|
+
return out.length > FAST_LIMITS.digestMaxChars
|
|
83
|
+
? out.slice(0, FAST_LIMITS.digestMaxChars)
|
|
84
|
+
: out;
|
|
85
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TurnFrameRing — the reviewer's bounded record of what the lead just did
|
|
3
|
+
* (fast-model-tier §3.5.1, extracted here by fast-model-tier-hardening W3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THIS IS THE `data` HALF OF THE SPLIT, AND IT IS A LEAF ON PURPOSE. `reviewer.ts`
|
|
8
|
+
* was at 871 of the repo's 1 000-line cap before round 2 added a budget to it, so
|
|
9
|
+
* the ring and the transport moved out. The split line is drawn along data and
|
|
10
|
+
* transport BECAUSE BOTH ARE LEAVES: the protocol — the two drain guards and the
|
|
11
|
+
* call-cancellation stamping — stays whole in `reviewer.ts`, where a module
|
|
12
|
+
* boundary through it would reopen a P0 (§3.3 / AC-H11 / AC-H16).
|
|
13
|
+
*
|
|
14
|
+
* WHAT THIS CLASS OWNS: the open frame, the retired ring, the outstanding-call
|
|
15
|
+
* counter and the tool-argument map. WHAT IT DECIDES: nothing. It reports that a
|
|
16
|
+
* batch has emptied; the caller decides what that means.
|
|
17
|
+
*/
|
|
18
|
+
import type { AssistantMessage, StreamEvent } from '@aragon-agent/core';
|
|
19
|
+
import type { TurnFrame } from './types.js';
|
|
20
|
+
export interface TurnFrameRingDeps {
|
|
21
|
+
/**
|
|
22
|
+
* Whether a frame can still be consumed by a review this run.
|
|
23
|
+
*
|
|
24
|
+
* A PREDICATE, NOT A BOOLEAN, and read inside `recordDelta` rather than at its
|
|
25
|
+
* call site: this runs on every `text_delta` of every turn, and evaluating the
|
|
26
|
+
* caller's gate eagerly would move a config read onto the hottest path in the
|
|
27
|
+
* package for the deltas that are discarded by type anyway.
|
|
28
|
+
*/
|
|
29
|
+
canRecord(): boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare class TurnFrameRing {
|
|
32
|
+
private readonly deps;
|
|
33
|
+
private retained;
|
|
34
|
+
private open;
|
|
35
|
+
/**
|
|
36
|
+
* Tool calls still to run in the current batch.
|
|
37
|
+
*
|
|
38
|
+
* ASSIGNED at `turn_end`, NEVER CARRIED ACROSS TURNS (RV-7), and the
|
|
39
|
+
* difference is not cosmetic. Two paths end a batch without emitting a
|
|
40
|
+
* `tool_execution_end` for every call: checkpoint 2 skips the whole batch
|
|
41
|
+
* (`agent-loop.ts:199-207`) and an abort breaks out mid-batch (`:211`). A
|
|
42
|
+
* counter that only ever decremented would sit permanently above zero after
|
|
43
|
+
* either, and the injection window would never open again for the rest of the
|
|
44
|
+
* run. Re-assigning at each `turn_end` makes both cases self-heal on the next
|
|
45
|
+
* turn, which is correct: those batches genuinely had no window.
|
|
46
|
+
*/
|
|
47
|
+
private outstanding;
|
|
48
|
+
private completedTurns;
|
|
49
|
+
/** Tool args captured at `tool_execution_start`, keyed by call id. */
|
|
50
|
+
private readonly toolArgs;
|
|
51
|
+
constructor(deps: TurnFrameRingDeps);
|
|
52
|
+
/** Completed turns of the CURRENT run — the cadence and staleness clock. */
|
|
53
|
+
get turns(): number;
|
|
54
|
+
/** Always 0 at the injection window; logged there as the assertion, recorded. */
|
|
55
|
+
get outstandingCalls(): number;
|
|
56
|
+
/** The ring, oldest first. `buildReviewDigest` filters it for sealed frames. */
|
|
57
|
+
frames(): TurnFrame[];
|
|
58
|
+
/** `agent_start`. */
|
|
59
|
+
reset(): void;
|
|
60
|
+
/** `agent_end` — the ring survives for a late answer; the open frame does not. */
|
|
61
|
+
endRun(): void;
|
|
62
|
+
/** `turn_start`. */
|
|
63
|
+
beginTurn(): void;
|
|
64
|
+
/**
|
|
65
|
+
* The frame recorder.
|
|
66
|
+
*
|
|
67
|
+
* GATED ON `text_delta` AS AN ALLOW-TEST, NEVER A DENY-LIST (RV-10 / R-16).
|
|
68
|
+
* `agent-loop.ts:170` forwards EVERY stream event through `message_update` -
|
|
69
|
+
* `text_delta`, `thinking_delta`, `tool_call_delta`, `done`, and (as of
|
|
70
|
+
* `llm-api-retry-backoff`) `retry_scheduled` / `retry_attempt` - so this is a
|
|
71
|
+
* synchronous listener on the hottest path in the package. A deny-list would
|
|
72
|
+
* have silently started recording the two new members the moment they shipped.
|
|
73
|
+
*
|
|
74
|
+
* The remaining conditions are economic rather than structural: after the last
|
|
75
|
+
* permitted review of a run there is nothing left that can consume a frame.
|
|
76
|
+
*/
|
|
77
|
+
recordDelta(streamEvent: StreamEvent): void;
|
|
78
|
+
/** `tool_execution_start`. */
|
|
79
|
+
noteToolArgs(toolCallId: string, args: unknown): void;
|
|
80
|
+
/** `turn_end`. A turn with NO tool calls ends the run, so its frame seals here. */
|
|
81
|
+
endTurn(message: AssistantMessage): void;
|
|
82
|
+
/**
|
|
83
|
+
* `tool_execution_end`. Returns whether this call EMPTIED the batch — which is
|
|
84
|
+
* both the sealing point (§3.5.1) and the one injection window (§3.5.4). The
|
|
85
|
+
* ring reports the fact; `reviewer.ts` owns what follows from it.
|
|
86
|
+
*/
|
|
87
|
+
endTool(toolCallId: string, toolName: string, isError: boolean, duration: number): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Seal the open frame and retire it into the ring.
|
|
90
|
+
*
|
|
91
|
+
* A REVIEW IS TRIGGERED ON SEALING, NOT AT `turn_end` (RV-2 / D-22).
|
|
92
|
+
* `turn_end` fires at `agent-loop.ts:188`, BEFORE any tool runs, while
|
|
93
|
+
* `TurnFrame.tools` carries `isError` and `ms` - fields that exist only once
|
|
94
|
+
* `tool_execution_end` has been emitted (`:249-256`). Triggering there meant
|
|
95
|
+
* the newest frame of every digest was structurally missing the single fact a
|
|
96
|
+
* reviewer most needs: whether the batch it is reviewing just failed.
|
|
97
|
+
*/
|
|
98
|
+
seal(): void;
|
|
99
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TurnFrameRing — the reviewer's bounded record of what the lead just did
|
|
3
|
+
* (fast-model-tier §3.5.1, extracted here by fast-model-tier-hardening W3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THIS IS THE `data` HALF OF THE SPLIT, AND IT IS A LEAF ON PURPOSE. `reviewer.ts`
|
|
8
|
+
* was at 871 of the repo's 1 000-line cap before round 2 added a budget to it, so
|
|
9
|
+
* the ring and the transport moved out. The split line is drawn along data and
|
|
10
|
+
* transport BECAUSE BOTH ARE LEAVES: the protocol — the two drain guards and the
|
|
11
|
+
* call-cancellation stamping — stays whole in `reviewer.ts`, where a module
|
|
12
|
+
* boundary through it would reopen a P0 (§3.3 / AC-H11 / AC-H16).
|
|
13
|
+
*
|
|
14
|
+
* WHAT THIS CLASS OWNS: the open frame, the retired ring, the outstanding-call
|
|
15
|
+
* counter and the tool-argument map. WHAT IT DECIDES: nothing. It reports that a
|
|
16
|
+
* batch has emptied; the caller decides what that means.
|
|
17
|
+
*/
|
|
18
|
+
import { FAST_LIMITS } from './limits.js';
|
|
19
|
+
// The one `unknown -> text` helper of this tree lives with the module that
|
|
20
|
+
// classifies errors. A second copy here would be a second answer to one question.
|
|
21
|
+
import { errorText } from './review-call.js';
|
|
22
|
+
/** The one argument a tool call is worth naming in a digest. */
|
|
23
|
+
function pickToolArg(args) {
|
|
24
|
+
const a = (args ?? {});
|
|
25
|
+
for (const key of ['path', 'pattern', 'command', 'query', 'label']) {
|
|
26
|
+
const value = a[key];
|
|
27
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
28
|
+
return value.slice(0, FAST_LIMITS.toolArgChars);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export class TurnFrameRing {
|
|
34
|
+
deps;
|
|
35
|
+
retained = [];
|
|
36
|
+
open = null;
|
|
37
|
+
/**
|
|
38
|
+
* Tool calls still to run in the current batch.
|
|
39
|
+
*
|
|
40
|
+
* ASSIGNED at `turn_end`, NEVER CARRIED ACROSS TURNS (RV-7), and the
|
|
41
|
+
* difference is not cosmetic. Two paths end a batch without emitting a
|
|
42
|
+
* `tool_execution_end` for every call: checkpoint 2 skips the whole batch
|
|
43
|
+
* (`agent-loop.ts:199-207`) and an abort breaks out mid-batch (`:211`). A
|
|
44
|
+
* counter that only ever decremented would sit permanently above zero after
|
|
45
|
+
* either, and the injection window would never open again for the rest of the
|
|
46
|
+
* run. Re-assigning at each `turn_end` makes both cases self-heal on the next
|
|
47
|
+
* turn, which is correct: those batches genuinely had no window.
|
|
48
|
+
*/
|
|
49
|
+
outstanding = 0;
|
|
50
|
+
completedTurns = 0;
|
|
51
|
+
/** Tool args captured at `tool_execution_start`, keyed by call id. */
|
|
52
|
+
toolArgs = new Map();
|
|
53
|
+
constructor(deps) {
|
|
54
|
+
this.deps = deps;
|
|
55
|
+
}
|
|
56
|
+
/** Completed turns of the CURRENT run — the cadence and staleness clock. */
|
|
57
|
+
get turns() {
|
|
58
|
+
return this.completedTurns;
|
|
59
|
+
}
|
|
60
|
+
/** Always 0 at the injection window; logged there as the assertion, recorded. */
|
|
61
|
+
get outstandingCalls() {
|
|
62
|
+
return this.outstanding;
|
|
63
|
+
}
|
|
64
|
+
/** The ring, oldest first. `buildReviewDigest` filters it for sealed frames. */
|
|
65
|
+
frames() {
|
|
66
|
+
return this.retained;
|
|
67
|
+
}
|
|
68
|
+
/** `agent_start`. */
|
|
69
|
+
reset() {
|
|
70
|
+
this.retained = [];
|
|
71
|
+
this.open = null;
|
|
72
|
+
this.outstanding = 0;
|
|
73
|
+
this.completedTurns = 0;
|
|
74
|
+
this.toolArgs.clear();
|
|
75
|
+
}
|
|
76
|
+
/** `agent_end` — the ring survives for a late answer; the open frame does not. */
|
|
77
|
+
endRun() {
|
|
78
|
+
this.open = null;
|
|
79
|
+
this.toolArgs.clear();
|
|
80
|
+
}
|
|
81
|
+
/** `turn_start`. */
|
|
82
|
+
beginTurn() {
|
|
83
|
+
this.open = {
|
|
84
|
+
index: this.completedTurns + 1,
|
|
85
|
+
textTail: '',
|
|
86
|
+
tools: [],
|
|
87
|
+
sealed: false,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The frame recorder.
|
|
92
|
+
*
|
|
93
|
+
* GATED ON `text_delta` AS AN ALLOW-TEST, NEVER A DENY-LIST (RV-10 / R-16).
|
|
94
|
+
* `agent-loop.ts:170` forwards EVERY stream event through `message_update` -
|
|
95
|
+
* `text_delta`, `thinking_delta`, `tool_call_delta`, `done`, and (as of
|
|
96
|
+
* `llm-api-retry-backoff`) `retry_scheduled` / `retry_attempt` - so this is a
|
|
97
|
+
* synchronous listener on the hottest path in the package. A deny-list would
|
|
98
|
+
* have silently started recording the two new members the moment they shipped.
|
|
99
|
+
*
|
|
100
|
+
* The remaining conditions are economic rather than structural: after the last
|
|
101
|
+
* permitted review of a run there is nothing left that can consume a frame.
|
|
102
|
+
*/
|
|
103
|
+
recordDelta(streamEvent) {
|
|
104
|
+
if (streamEvent.type === 'error') {
|
|
105
|
+
if (this.open)
|
|
106
|
+
this.open.error = errorText(streamEvent.error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (streamEvent.type !== 'text_delta')
|
|
110
|
+
return;
|
|
111
|
+
if (!this.open)
|
|
112
|
+
return;
|
|
113
|
+
if (!this.deps.canRecord())
|
|
114
|
+
return;
|
|
115
|
+
const delta = streamEvent.delta;
|
|
116
|
+
if (delta.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
// O(1) per delta: one bounded concat and one bounded slice, the same rolling
|
|
119
|
+
// tail `subagent.ts:392` already uses for the activity line.
|
|
120
|
+
this.open.textTail = (this.open.textTail + delta).slice(-FAST_LIMITS.turnTailChars);
|
|
121
|
+
}
|
|
122
|
+
/** `tool_execution_start`. */
|
|
123
|
+
noteToolArgs(toolCallId, args) {
|
|
124
|
+
const arg = pickToolArg(args);
|
|
125
|
+
if (arg !== undefined)
|
|
126
|
+
this.toolArgs.set(toolCallId, arg);
|
|
127
|
+
}
|
|
128
|
+
/** `turn_end`. A turn with NO tool calls ends the run, so its frame seals here. */
|
|
129
|
+
endTurn(message) {
|
|
130
|
+
this.completedTurns += 1;
|
|
131
|
+
const calls = Array.isArray(message?.content)
|
|
132
|
+
? message.content.filter((b) => b.type === 'tool_call').length
|
|
133
|
+
: 0;
|
|
134
|
+
this.outstanding = calls;
|
|
135
|
+
if (this.open)
|
|
136
|
+
this.open.index = this.completedTurns;
|
|
137
|
+
if (calls === 0)
|
|
138
|
+
this.seal();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* `tool_execution_end`. Returns whether this call EMPTIED the batch — which is
|
|
142
|
+
* both the sealing point (§3.5.1) and the one injection window (§3.5.4). The
|
|
143
|
+
* ring reports the fact; `reviewer.ts` owns what follows from it.
|
|
144
|
+
*/
|
|
145
|
+
endTool(toolCallId, toolName, isError, duration) {
|
|
146
|
+
if (this.open) {
|
|
147
|
+
const arg = this.toolArgs.get(toolCallId);
|
|
148
|
+
this.open.tools.push({
|
|
149
|
+
name: toolName,
|
|
150
|
+
...(arg !== undefined ? { arg } : {}),
|
|
151
|
+
...(isError ? { isError: true } : {}),
|
|
152
|
+
ms: duration,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
this.toolArgs.delete(toolCallId);
|
|
156
|
+
if (this.outstanding > 0)
|
|
157
|
+
this.outstanding -= 1;
|
|
158
|
+
return this.outstanding === 0;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Seal the open frame and retire it into the ring.
|
|
162
|
+
*
|
|
163
|
+
* A REVIEW IS TRIGGERED ON SEALING, NOT AT `turn_end` (RV-2 / D-22).
|
|
164
|
+
* `turn_end` fires at `agent-loop.ts:188`, BEFORE any tool runs, while
|
|
165
|
+
* `TurnFrame.tools` carries `isError` and `ms` - fields that exist only once
|
|
166
|
+
* `tool_execution_end` has been emitted (`:249-256`). Triggering there meant
|
|
167
|
+
* the newest frame of every digest was structurally missing the single fact a
|
|
168
|
+
* reviewer most needs: whether the batch it is reviewing just failed.
|
|
169
|
+
*/
|
|
170
|
+
seal() {
|
|
171
|
+
if (!this.open || this.open.sealed)
|
|
172
|
+
return;
|
|
173
|
+
this.open.sealed = true;
|
|
174
|
+
this.retained.push(this.open);
|
|
175
|
+
if (this.retained.length > FAST_LIMITS.maxFramesRetained) {
|
|
176
|
+
this.retained.splice(0, this.retained.length - FAST_LIMITS.maxFramesRetained);
|
|
177
|
+
}
|
|
178
|
+
this.open = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -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 declare 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
|
+
readonly digestMaxChars: 4000;
|
|
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
|
+
readonly goalChars: 500;
|
|
38
|
+
/** Per-frame assistant-text tail. Rolling, clipped on every append. */
|
|
39
|
+
readonly turnTailChars: 600;
|
|
40
|
+
/** Per picked tool argument. Mirrors `TEAM_LIMITS.activityArgChars`. */
|
|
41
|
+
readonly toolArgChars: 120;
|
|
42
|
+
/** Frames kept per run. `>=` the max of `reviewContextTurns`, with slack. */
|
|
43
|
+
readonly maxFramesRetained: 12;
|
|
44
|
+
/** Hard cap on the review's own output. */
|
|
45
|
+
readonly 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
|
+
readonly reviewTimeoutMs: 20000;
|
|
70
|
+
/** Reviews STARTED per run. A 40-turn run at cadence 5 would otherwise buy 8
|
|
71
|
+
* critiques the lead increasingly ignores. */
|
|
72
|
+
readonly maxReviewsPerRun: 6;
|
|
73
|
+
/** Consecutive NON-TRANSIENT failures before the reviewer self-disables. */
|
|
74
|
+
readonly 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
|
+
readonly pendingMaxAgeMs: 120000;
|
|
85
|
+
/** ... or is overtaken by the run itself, in turns. */
|
|
86
|
+
readonly pendingMaxTurnsBehind: 2;
|
|
87
|
+
/** Rows the transcript card renders before eliding. */
|
|
88
|
+
readonly 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
|
+
readonly 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 declare const FAST_BLOCK_VERSION = "v1-2026-07";
|