@aragon-agent/cli 0.5.8 → 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 +279 -0
- package/README.md +1429 -1163
- 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/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +245 -3
- package/dist/agent/reducer.js +490 -17
- 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 +86 -2
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +308 -2
- package/dist/config/cli-commands.d.ts +53 -2
- package/dist/config/cli-commands.js +136 -3
- package/dist/config/env.d.ts +6 -3
- package/dist/config/env.js +103 -3
- package/dist/config/load.d.ts +49 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +212 -2
- package/dist/config/schema.js +207 -1
- 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 +273 -37
- 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/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +37 -0
- package/dist/ui/StatusBar.js +5 -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 +26 -3
- package/dist/ui/Transcript.js +76 -10
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +20 -0
- package/dist/ui/entries/AssistantEntry.js +3 -2
- 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.js +6 -1
- package/dist/ui/entries/ToolCard.d.ts +38 -1
- package/dist/ui/entries/ToolCard.js +68 -5
- 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/virtual-window.d.ts +1 -1
- package/dist/ui/layout/virtual-window.js +109 -5
- 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/transcript-text.js +27 -0
- 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,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
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastReviewer — the periodic asynchronous review (fast-model-tier §3.5).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/fast/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* THE ENTIRE CLASS IS SHAPED AROUND *WHEN* IT IS ALLOWED TO SPEAK.
|
|
7
|
+
* `Agent.steer()` pushes onto a queue that `runAgentLoop` drains at three
|
|
8
|
+
* places, and only one of them is safe:
|
|
9
|
+
*
|
|
10
|
+
* 1 top of the loop, before the LLM call (`agent-loop.ts:126`) - pushed as a
|
|
11
|
+
* `user` message, conversation shape stays valid. SAFE.
|
|
12
|
+
* 2 after the LLM call, before tool execution (`:199`) - `continue`, leaving
|
|
13
|
+
* the assistant's `tool_use` blocks with no `tool_result`. Anthropic then
|
|
14
|
+
* rejects the NEXT request with HTTP 400. HARD FAILURE.
|
|
15
|
+
* 3 between two tool executions (`:214`) - every remaining call in the batch
|
|
16
|
+
* is replaced with "Tool execution skipped due to steering interrupt."
|
|
17
|
+
* SILENTLY DESTRUCTIVE.
|
|
18
|
+
*
|
|
19
|
+
* For a human pressing Enter mid-run, 2 and 3 are the DESIRED semantics. For an
|
|
20
|
+
* unrequested background critique they are a defect generator whose symptoms
|
|
21
|
+
* (an HTTP 400 about tool_result ids; work silently deleted) point nowhere near
|
|
22
|
+
* the reviewer. So this class calls `steer()` from EXACTLY ONE PLACE: inside the
|
|
23
|
+
* synchronous listener for the `tool_execution_end` that brings the batch to
|
|
24
|
+
* zero. `Agent.emit()` runs listeners synchronously inside the loop
|
|
25
|
+
* (`agent.ts:340`), the batch is finished, and the next `hasSteering()` the loop
|
|
26
|
+
* evaluates is therefore checkpoint 1 (AC-20).
|
|
27
|
+
*
|
|
28
|
+
* AND STEERING IS NOT DELIVERY (§3.5.4a / RV-1 / D-19). `steer()` only enqueues;
|
|
29
|
+
* `runLoopWithLifecycle`'s `finally` never clears the queues (`agent.ts:410`);
|
|
30
|
+
* and the loop emits the batch's last `tool_execution_end` (`:249`) BEFORE it
|
|
31
|
+
* re-tests the abort signal (`:271`). So an Esc landing inside the final tool's
|
|
32
|
+
* `await` yields exactly the interleaving where the reviewer steers, the loop
|
|
33
|
+
* breaks, and the block resurfaces in the user's NEXT conversation wearing the
|
|
34
|
+
* user's role. Two guards close it:
|
|
35
|
+
*
|
|
36
|
+
* guard 1 do not steer into a REQUESTED abort (`isAbortRequested`).
|
|
37
|
+
* guard 2 prove delivery. `awaitingDrain` is set on steer and cleared at the
|
|
38
|
+
* next `turn_start` - the loop cannot reach `turn_start` (`:136`)
|
|
39
|
+
* without having passed checkpoint 1 (`:126`), so `turn_start` IS the
|
|
40
|
+
* receipt. Still true at `agent_end` means the block was stranded.
|
|
41
|
+
*
|
|
42
|
+
* Guard 2 exists because the idle watchdog calls `Agent.abort()` DIRECTLY
|
|
43
|
+
* (`watchdog.ts:15-17`), where no controller-level flag can see it (D-20) - and
|
|
44
|
+
* the watchdog fires precisely on the long quiet runs where a review is most
|
|
45
|
+
* likely to be pending.
|
|
46
|
+
*
|
|
47
|
+
* A future refactor that moves `steer()` into the `complete()` callback will
|
|
48
|
+
* look tidier and will reintroduce all of the above.
|
|
49
|
+
*
|
|
50
|
+
* WHAT WAS SPLIT OUT, AND WHAT DELIBERATELY WAS NOT (hardening W3 / §3.3).
|
|
51
|
+
* The turn-frame ring moved to `frames.ts` and the bounded call to
|
|
52
|
+
* `review-call.ts`, both leaves. THE PROTOCOL STAYED HERE, WHOLE: the two drain
|
|
53
|
+
* guards above (AC-H11) and the call-cancellation stamping - `callAbort`,
|
|
54
|
+
* `callAbortReason`, `cancelCall()` and the one `new AbortController()`
|
|
55
|
+
* (AC-H16). A split that puts half of either behind a module boundary is a
|
|
56
|
+
* "simplification" that reopens a P0, and in the cancellation case it is round
|
|
57
|
+
* 1's IF-7 verbatim; `review-call.ts`'s header states why.
|
|
58
|
+
*/
|
|
59
|
+
import type { AgentEvent, AssistantMessage, LLMRequest, TokenUsage } from '@aragon-agent/core';
|
|
60
|
+
import type { FastConfig } from '../config/schema.js';
|
|
61
|
+
import type { NoticeLevel } from '../agent/reducer.js';
|
|
62
|
+
import type { FastEvent, FastTier } from './types.js';
|
|
63
|
+
/**
|
|
64
|
+
* Everything the reviewer needs from the rest of the world.
|
|
65
|
+
*
|
|
66
|
+
* ALL INJECTED, so `fast-reviewer.test.ts` can replay a scripted `AgentEvent`
|
|
67
|
+
* sequence against a stub `complete` and assert the injection window without a
|
|
68
|
+
* network, a terminal or an `Agent` (§8.1).
|
|
69
|
+
*/
|
|
70
|
+
export interface FastReviewerDeps {
|
|
71
|
+
/** The LEAD agent's event stream, through `AgentController` (C-7). */
|
|
72
|
+
subscribe(listener: (event: AgentEvent) => void): () => void;
|
|
73
|
+
/**
|
|
74
|
+
* `ProviderRegistry.complete` — bound since round 2 to `FastWiring`'s OWN
|
|
75
|
+
* fail-fast registry, not to the lead's (W2 / §3.2). Nothing in this file
|
|
76
|
+
* knows that, which is the whole reason the dep is injected.
|
|
77
|
+
*/
|
|
78
|
+
complete(providerId: string, request: LLMRequest): Promise<AssistantMessage>;
|
|
79
|
+
/** `AgentController.steer` - the ONLY writer of the queue besides the user. */
|
|
80
|
+
steer(text: string): void;
|
|
81
|
+
isRunning(): boolean;
|
|
82
|
+
/** Guard 1: `AgentController.abort()` sets this synchronously BEFORE
|
|
83
|
+
* `agent.abort()`, so the reviewer can see an Esc that has not landed yet. */
|
|
84
|
+
isAbortRequested(): boolean;
|
|
85
|
+
/** Guard 2: user messages queued since the last `turn_start`. `0` is what
|
|
86
|
+
* makes `clearAllQueues()` provably safe (D-21). */
|
|
87
|
+
userSteerCount(): number;
|
|
88
|
+
clearAllQueues(): void;
|
|
89
|
+
/** Re-read at EVERY use, never snapshot (RV-3). */
|
|
90
|
+
getTier(): FastTier;
|
|
91
|
+
getConfig(): FastConfig;
|
|
92
|
+
/** `FastWiring.available()` - the live predicate of §3.3. */
|
|
93
|
+
available(): boolean;
|
|
94
|
+
getApiKey(providerId: string): string | undefined;
|
|
95
|
+
emit(event: FastEvent): void;
|
|
96
|
+
notify(level: NoticeLevel, text: string): void;
|
|
97
|
+
/** Injected clock. A test that reads the wall clock fails on a slow machine
|
|
98
|
+
* and nowhere else - the reason `TodoStore` and `TeamPanel` take one. */
|
|
99
|
+
now?: () => number;
|
|
100
|
+
}
|
|
101
|
+
export declare class FastReviewer {
|
|
102
|
+
private readonly deps;
|
|
103
|
+
/** Scope `fast` since round 2 (W4): an unrequested background call is not one
|
|
104
|
+
* of the LEAD's records, and a support reader filtering `agent` wants those. */
|
|
105
|
+
private readonly log;
|
|
106
|
+
private readonly now;
|
|
107
|
+
private unsubscribe;
|
|
108
|
+
private disposed;
|
|
109
|
+
private runId;
|
|
110
|
+
/** The turn-frame ring (`frames.ts`). Owns `turns`, the open frame and the
|
|
111
|
+
* outstanding-call counter; decides nothing. */
|
|
112
|
+
private readonly ring;
|
|
113
|
+
private inFlight;
|
|
114
|
+
private pending;
|
|
115
|
+
private reviewsThisRun;
|
|
116
|
+
/** Set on steer, cleared at the next `turn_start`. Guard 2's receipt. */
|
|
117
|
+
private awaitingDrain;
|
|
118
|
+
private awaitingReviewIndex;
|
|
119
|
+
/** The run's own abort channel for an in-flight `complete()`. */
|
|
120
|
+
private callAbort;
|
|
121
|
+
/**
|
|
122
|
+
* WHY the in-flight call was aborted - RECORDED here because it cannot be
|
|
123
|
+
* recovered from the error: a timeout and a cancellation go through the same
|
|
124
|
+
* `controller.abort()` and arrive as byte-identical bare `Error`s. The full
|
|
125
|
+
* argument, and the prohibition it implies for `review-call.ts`, is in that
|
|
126
|
+
* file's header; this field and `cancelCall()` are the halves it protects.
|
|
127
|
+
*/
|
|
128
|
+
private callAbortReason;
|
|
129
|
+
private goal;
|
|
130
|
+
/**
|
|
131
|
+
* Reviews STARTED this session. NEVER RESET (§3.1.1).
|
|
132
|
+
*
|
|
133
|
+
* This is BOTH the status line's numerator and the session budget's left-hand
|
|
134
|
+
* side, on purpose: a parallel `reviewsThisSession` would be a second answer
|
|
135
|
+
* to one question, and the first time the two drifted the product would print
|
|
136
|
+
* `12/40` while suppressing at a number nobody can see.
|
|
137
|
+
*/
|
|
138
|
+
private reviewIndex;
|
|
139
|
+
/** The limit at which exhaustion was last announced; `-1` = not announced.
|
|
140
|
+
* Keyed on the LIMIT, not on a boolean, so raising the budget re-arms the
|
|
141
|
+
* notice without a second latch to keep in sync (§3.1.3). */
|
|
142
|
+
private budgetAnnouncedAt;
|
|
143
|
+
private consecutiveFailures;
|
|
144
|
+
private selfDisabled;
|
|
145
|
+
private usage;
|
|
146
|
+
constructor(deps: FastReviewerDeps);
|
|
147
|
+
/** The user's request for this run, captured in `AgentController.prompt()`. */
|
|
148
|
+
setGoal(text: string): void;
|
|
149
|
+
/** Reviews STARTED this session, for `/fast status` and `FastSnapshot`. */
|
|
150
|
+
reviewCount(): number;
|
|
151
|
+
/** The live session budget, read from config so `/fast budget <n>` takes
|
|
152
|
+
* effect on the next sealed frame with no restart. */
|
|
153
|
+
budgetLimit(): number;
|
|
154
|
+
/** Carried on the snapshot rather than re-derived by callers (RV-H10). */
|
|
155
|
+
isBudgetReached(): boolean;
|
|
156
|
+
sessionUsage(): TokenUsage;
|
|
157
|
+
isReviewInFlight(): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* `AgentController.abort()`.
|
|
160
|
+
*
|
|
161
|
+
* Aborts the in-flight call and drops `pending`. It MUST NOT clear
|
|
162
|
+
* `awaitingDrain`: that flag is guard 2's receipt and is resolved only at
|
|
163
|
+
* `turn_start` (delivered) or `agent_end` (stranded).
|
|
164
|
+
*/
|
|
165
|
+
abort(): void;
|
|
166
|
+
/**
|
|
167
|
+
* Cancel an in-flight review because nobody wants the answer any more.
|
|
168
|
+
*
|
|
169
|
+
* A CANCELLATION IS NOT A FAILURE (§3.5.5). The three callers - `endRun()`
|
|
170
|
+
* (the run finished first), `abort()` (Esc) and `dispose()` - all mean "never
|
|
171
|
+
* mind", and none of them is evidence of a MISCONFIGURED tier, which is the
|
|
172
|
+
* only thing self-disable exists for (D-18 / D-24). Scoring them as strikes
|
|
173
|
+
* silently killed the reviewer for the rest of a session over three ordinary
|
|
174
|
+
* runs - the common shape, since a turn that answers without tools ends the
|
|
175
|
+
* run - behind a warn quoting an internal stream message (IF-7).
|
|
176
|
+
*
|
|
177
|
+
* THE STAMP MUST HAPPEN HERE, BEFORE THE ABORT, AND THE CONTROLLER MUST STAY
|
|
178
|
+
* REACHABLE FROM THIS METHOD (AC-H16).
|
|
179
|
+
*/
|
|
180
|
+
private cancelCall;
|
|
181
|
+
/** Idempotent. Unsubscribes as well as aborting. */
|
|
182
|
+
dispose(): void;
|
|
183
|
+
private onAgentEvent;
|
|
184
|
+
private beginRun;
|
|
185
|
+
private onTurnStart;
|
|
186
|
+
private onToolEnd;
|
|
187
|
+
private endRun;
|
|
188
|
+
/**
|
|
189
|
+
* Remove a block that was steered and never delivered.
|
|
190
|
+
*
|
|
191
|
+
* THE ASYMMETRY IS DELIBERATE (D-21): the reviewer may always destroy its own
|
|
192
|
+
* message and may NEVER destroy the user's. `Agent` exposes no way to remove
|
|
193
|
+
* one entry - only `clearAllQueues()` - so removal is permitted only while the
|
|
194
|
+
* reviewer can PROVE it owns the whole queue, which `userSteerCount() === 0`
|
|
195
|
+
* establishes: `AgentController.steer()` is the only other writer in the
|
|
196
|
+
* package and the counter resets at every `turn_start`.
|
|
197
|
+
*
|
|
198
|
+
* With a user steer queued it clears NOTHING and warns. The residue is
|
|
199
|
+
* bounded, rare, labelled by its own `<fast_review turn model>` wrapper, and -
|
|
200
|
+
* unlike v1 - reported.
|
|
201
|
+
*/
|
|
202
|
+
private recoverStranded;
|
|
203
|
+
/** Whether another review could still be STARTED this run. */
|
|
204
|
+
private canReviewAgain;
|
|
205
|
+
private maybeStartReview;
|
|
206
|
+
/**
|
|
207
|
+
* Say it once per limit value, and say what is NOT affected.
|
|
208
|
+
*
|
|
209
|
+
* `info`, NOT `warn` (D-H6): D-24 reserves the alarming register for a
|
|
210
|
+
* MISCONFIGURED tier, because that is the case that never fixes itself. A
|
|
211
|
+
* budget reached is the system working as configured, and warning here would
|
|
212
|
+
* train the user to ignore the register that matters.
|
|
213
|
+
*
|
|
214
|
+
* NEITHER `consecutiveFailures` NOR `selfDisabled` IS TOUCHED. A budget stop
|
|
215
|
+
* is neither a success that clears the strike counter nor a failure that
|
|
216
|
+
* raises it - the exact distinction IF-7 was filed for, applied to a second
|
|
217
|
+
* cause. And the reviewer is not broken: it must revive the moment the budget
|
|
218
|
+
* rises, which `budgetLimit()` reading live config is what delivers.
|
|
219
|
+
*
|
|
220
|
+
* NO TRANSCRIPT CARD, and that is a correctness requirement rather than a
|
|
221
|
+
* styling choice (§3.1.3). R-8 ("it did nothing" is never the observable
|
|
222
|
+
* outcome) governs a review that STARTED; every such path emits a `FastReview`
|
|
223
|
+
* carrying an `index`, and indices come from `this.reviewIndex += 1`. Minting
|
|
224
|
+
* one here would advance the very counter `/fast status` renders as the
|
|
225
|
+
* numerator, so the first suppression would display `41/40`.
|
|
226
|
+
*/
|
|
227
|
+
private announceBudgetReached;
|
|
228
|
+
private currentModelId;
|
|
229
|
+
private runReview;
|
|
230
|
+
/**
|
|
231
|
+
* A review nobody is waiting for any more.
|
|
232
|
+
*
|
|
233
|
+
* It still has to produce a TERMINAL card. `review_start` already put a
|
|
234
|
+
* `running` entry in the transcript, and `Transcript`'s settled boundary is
|
|
235
|
+
* monotonic (C-5) - an entry that never settles is re-rendered on every frame
|
|
236
|
+
* for the rest of the session. "It did nothing" is also never allowed to be
|
|
237
|
+
* the observable outcome (R-8).
|
|
238
|
+
*
|
|
239
|
+
* `consecutiveFailures` is left EXACTLY as it was: a cancellation is neither a
|
|
240
|
+
* success that clears the count nor a failure that raises it.
|
|
241
|
+
*/
|
|
242
|
+
private onReviewCancelled;
|
|
243
|
+
private onReviewAnswer;
|
|
244
|
+
private onReviewFailure;
|
|
245
|
+
private maybeInject;
|
|
246
|
+
private dropPending;
|
|
247
|
+
private emitReview;
|
|
248
|
+
}
|