@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/agent/controller.js
CHANGED
|
@@ -9,19 +9,25 @@
|
|
|
9
9
|
* from the event stream by the reducer (TUI) and the headless writer.
|
|
10
10
|
*/
|
|
11
11
|
import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
|
|
12
|
-
import { clampTeamConfig, clampTodoConfig, isAdapterProvider } from '../config/schema.js';
|
|
12
|
+
import { clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
|
|
13
13
|
import { makeGetApiKey } from '../config/load.js';
|
|
14
14
|
import { updatePersistedConfig } from '../config/store.js';
|
|
15
15
|
import { createBuiltinTools } from '../tools/index.js';
|
|
16
|
+
import { createFileChangeStore } from '../tools/file-change-store.js';
|
|
17
|
+
import { createToolOutputStore, } from '../tools/tool-output-store.js';
|
|
16
18
|
import { TeamRuntime } from '../team/runtime.js';
|
|
17
19
|
import { TeamHumanQueue } from '../team/human-queue.js';
|
|
18
20
|
import { createTaskTool } from '../team/task-tool.js';
|
|
19
21
|
import { buildTeamBlock } from '../team/prompt.js';
|
|
22
|
+
import { buildFastBlock } from '../fast/prompt.js';
|
|
23
|
+
import { describeFastTierProblem, fastProviderOf, resolveFastTier } from '../fast/resolve.js';
|
|
24
|
+
import { FastWiring, offFastStatus } from '../fast/wiring.js';
|
|
20
25
|
import { TodoStore } from '../todo/store.js';
|
|
21
26
|
import { createTodoTool } from '../todo/todo-tool.js';
|
|
22
27
|
import { buildTodoBlock } from '../todo/prompt.js';
|
|
23
28
|
import { createPlanTools } from '../tools/plan-tools.js';
|
|
24
29
|
import { DENY_ALL_HUMAN_INPUT } from '../tools/human-input.js';
|
|
30
|
+
import { getLogger } from '../logging/logger.js';
|
|
25
31
|
import { createNodeSkillHost } from '../skills/node-host.js';
|
|
26
32
|
import { SkillService } from '../skills/service.js';
|
|
27
33
|
import { createSkillTools } from '../skills/tools.js';
|
|
@@ -94,13 +100,67 @@ export class AgentController {
|
|
|
94
100
|
todos;
|
|
95
101
|
/** Static: whether this session could ever render the rail. See the dep. */
|
|
96
102
|
todoPanelCapable;
|
|
103
|
+
/**
|
|
104
|
+
* Structured file diffs on their way to the UI (agent-activity-presentation
|
|
105
|
+
* §3.3.5). A bounded, take-once ring; see `file-change-store.ts` for why each
|
|
106
|
+
* of those three words is load-bearing.
|
|
107
|
+
*
|
|
108
|
+
* DECLARED WITH AN INITIALIZER, so it exists before `createBuiltinTools` runs
|
|
109
|
+
* in the constructor body and the recorder closure it hands over is valid from
|
|
110
|
+
* the first tool call.
|
|
111
|
+
*/
|
|
112
|
+
fileChanges = createFileChangeStore();
|
|
113
|
+
/**
|
|
114
|
+
* A running tool's output on its way to the UI
|
|
115
|
+
* (agent-activity-presentation-live §3.1.3).
|
|
116
|
+
*
|
|
117
|
+
* NULL WHEN THE FEATURE IS OFF (P1-6). `fileChanges` next door is declared
|
|
118
|
+
* with an initializer because it is unconditional; this one cannot be, since a
|
|
119
|
+
* field initializer cannot read a constructor parameter -- and an
|
|
120
|
+
* unconditional initializer would allocate on every construction and make
|
|
121
|
+
* AC-31's "no store is allocated" false by construction. `private readonly
|
|
122
|
+
* fast: FastWiring | null` below is the same declaration shape in the same
|
|
123
|
+
* class.
|
|
124
|
+
*
|
|
125
|
+
* ASSIGNED IN THE CONSTRUCTOR BODY, ABOVE THE `createBuiltinTools` CALL, not
|
|
126
|
+
* near `this.fast` which runs after it. The binding there is spread
|
|
127
|
+
* CONDITIONALLY on this field, so an assignment placed after the call would
|
|
128
|
+
* read `undefined`, add no property, and produce a build in which the feature
|
|
129
|
+
* is silently off with the key on.
|
|
130
|
+
*/
|
|
131
|
+
toolOutputs;
|
|
132
|
+
outputListeners = new Set();
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
// Fast model tier (fast-model-tier §3.1 / §3.3)
|
|
135
|
+
//
|
|
136
|
+
// ONE FIELD AND FOUR THIN FORWARDERS. The flag pair, the tier cache, the
|
|
137
|
+
// reviewer's lifetime and the `resolveTier` closure all live in
|
|
138
|
+
// `fast/wiring.ts`, which is a BUDGET decision rather than an aesthetic one
|
|
139
|
+
// (C-12 / RV-5): `CLAUDE.md` caps a source file at 1000 lines and this one had
|
|
140
|
+
// 40 to spare when the design was written.
|
|
141
|
+
//
|
|
142
|
+
// `null` when the tier could not be resolved at construction, which is the
|
|
143
|
+
// byte-identity branch: no reviewer is subscribed, `task` carries no `model`
|
|
144
|
+
// property, and `<fast_tier>` is never spliced (I-2).
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
fast;
|
|
147
|
+
/**
|
|
148
|
+
* Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
|
|
149
|
+
* Cleared at the next `agent_start`.
|
|
150
|
+
*/
|
|
151
|
+
abortRequested = false;
|
|
152
|
+
/** User messages queued since the last `turn_start` (§3.5.4a guard 2). */
|
|
153
|
+
userSteerCount = 0;
|
|
97
154
|
constructor(config, deps = {}) {
|
|
98
155
|
this.config = config;
|
|
99
156
|
this.cwd = config.cwd;
|
|
100
157
|
const humanInput = deps.humanInput ?? DENY_ALL_HUMAN_INPUT;
|
|
101
158
|
this.planInteractive = humanInput.neverPrompts !== true;
|
|
102
159
|
this.effectiveMode = config.startInPlanMode ? 'plan' : 'build';
|
|
103
|
-
|
|
160
|
+
// The retry policy is installed HERE and replaced live by `setRetryConfig`
|
|
161
|
+
// (llm-api-retry-backoff §6.3). Subagents inherit it for free: `TeamRuntime`
|
|
162
|
+
// reuses this very registry instance.
|
|
163
|
+
this.providerRegistry = initProviders({ retryPolicy: toRetryPolicy(config.retry) });
|
|
104
164
|
this.modelRegistry = new ModelRegistry(this.providerRegistry);
|
|
105
165
|
this.skillsEnabled = config.skills.enabled;
|
|
106
166
|
this.onSkillsChanged = deps.onSkillsChanged;
|
|
@@ -132,12 +192,33 @@ export class AgentController {
|
|
|
132
192
|
this.todoEnabled = config.todo.enabled;
|
|
133
193
|
this.todoPanelCapable = deps.todoPanelCapable === true;
|
|
134
194
|
this.todos = this.todoRegistered ? new TodoStore() : null;
|
|
195
|
+
// --- Fast model tier ---------------------------------------------------
|
|
196
|
+
//
|
|
197
|
+
// RESOLVED BEFORE THE TOOL ARRAY, because `fastRegistered` decides whether
|
|
198
|
+
// `task`'s schema carries a `model` property at all and the array is built
|
|
199
|
+
// ONCE (C-2). `FastWiring` re-runs the same pure function in its own
|
|
200
|
+
// constructor; the two agree by construction because they read the same
|
|
201
|
+
// config through the same resolver.
|
|
202
|
+
const fastTier = resolveFastTier(config, (id) => this.hasApiKey(id));
|
|
203
|
+
const fastRegistered = fastTier.ok;
|
|
135
204
|
// --- Team mode ---------------------------------------------------------
|
|
136
205
|
this.teamRegistered = config.team.enabled;
|
|
137
206
|
this.teamEnabled = config.team.enabled;
|
|
138
207
|
this.teamRuntime = this.teamRegistered
|
|
139
208
|
? new TeamRuntime({
|
|
140
209
|
getConfig: () => this.config,
|
|
210
|
+
// Read LIVE through the wiring, which does not exist yet — the closure
|
|
211
|
+
// is only ever called from inside a dispatch, long after construction.
|
|
212
|
+
resolveTier: (tier) => this.fast
|
|
213
|
+
? this.fast.resolveTier(tier)
|
|
214
|
+
: {
|
|
215
|
+
ref: {
|
|
216
|
+
providerId: this.config.provider,
|
|
217
|
+
modelId: this.config.model,
|
|
218
|
+
...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
|
|
219
|
+
},
|
|
220
|
+
thinkingLevel: this.config.thinkingLevel,
|
|
221
|
+
},
|
|
141
222
|
providerRegistry: this.providerRegistry,
|
|
142
223
|
getCwd: () => this.cwd,
|
|
143
224
|
getMode: () => this.effectiveMode,
|
|
@@ -166,8 +247,26 @@ export class AgentController {
|
|
|
166
247
|
...(confirm ? { humanQueue: new TeamHumanQueue(confirm) } : {}),
|
|
167
248
|
})
|
|
168
249
|
: null;
|
|
250
|
+
// ABOVE `createBuiltinTools`, and the ordering is load-bearing for the
|
|
251
|
+
// reason the field's own comment gives: the binding below is spread
|
|
252
|
+
// conditionally on it (§3.1.3 / P1-6).
|
|
253
|
+
this.toolOutputs = config.liveToolOutput ? createToolOutputStore() : null;
|
|
169
254
|
this.tools = createBuiltinTools({
|
|
170
255
|
getCwd: () => this.cwd,
|
|
256
|
+
// OWNED HERE, NOT IN `cli.tsx` (D-18): `createBuiltinTools` is called from
|
|
257
|
+
// this constructor and from `team/subagent.ts`, never from `cli.tsx`, so
|
|
258
|
+
// this is the one place a recorder can be bound to the toolset that will
|
|
259
|
+
// use it. Pre-bound to `'lead'`, so `fs-tools.ts` never learns what an
|
|
260
|
+
// owner is; subagents get no recorder this round (D-13).
|
|
261
|
+
recordChange: (id, patch) => this.fileChanges.record('lead', id, patch),
|
|
262
|
+
// The live-output recorder, at the identical site and for the identical
|
|
263
|
+
// reason, and spread ONLY WHEN THE STORE EXISTS: with `liveToolOutput` off
|
|
264
|
+
// the options bag has no `recordOutput` property at all, so
|
|
265
|
+
// `createBuiltinTools`'s conditional spread yields today's `deps` byte for
|
|
266
|
+
// byte and `bash` is the pre-round tool (AC-31).
|
|
267
|
+
...(this.toolOutputs
|
|
268
|
+
? { recordOutput: (id, chunk) => this.emitToolOutput('lead', id, chunk) }
|
|
269
|
+
: {}),
|
|
171
270
|
confirmTools: config.confirmTools,
|
|
172
271
|
// Routed through the same watchdog pause as the plan tools. This is a
|
|
173
272
|
// pre-existing bug being fixed in passing: a `--confirm` user who took
|
|
@@ -220,10 +319,23 @@ export class AgentController {
|
|
|
220
319
|
runtime: this.teamRuntime,
|
|
221
320
|
// Read LIVE: `/team off` flips the flag, it cannot unregister.
|
|
222
321
|
isTeamEnabled: () => this.teamEnabled,
|
|
322
|
+
// FROZEN AT CONSTRUCTION (C-2). `/fast on` in a session launched
|
|
323
|
+
// without the tier cannot add a schema property, and it says so
|
|
324
|
+
// rather than advertising one that is not there.
|
|
325
|
+
fastRegistered,
|
|
326
|
+
// Read LIVE at dispatch time (RV-3): the tier can stop resolving
|
|
327
|
+
// mid-session, and the normalizer and the subagent factory must
|
|
328
|
+
// agree about that at the same instant.
|
|
329
|
+
fastAvailable: () => this.fast?.delegationAvailable() === true,
|
|
223
330
|
maxSubagents: () => this.config.team.maxSubagents,
|
|
224
331
|
hasApiKey: () => this.hasApiKey(),
|
|
225
332
|
activeProvider: () => this.config.provider,
|
|
226
333
|
modelCost: () => this.getModelInfo().cost,
|
|
334
|
+
fastModelCost: () => {
|
|
335
|
+
const ref = this.fast?.fastRef();
|
|
336
|
+
return ref ? this.getModelInfoFor(ref).cost : undefined;
|
|
337
|
+
},
|
|
338
|
+
fastPricingUnknown: () => this.fast?.snapshot().pricingUnknown === true,
|
|
227
339
|
withPausedWatchdog: (fn) => this.withPausedWatchdog(fn),
|
|
228
340
|
}),
|
|
229
341
|
]
|
|
@@ -275,6 +387,71 @@ export class AgentController {
|
|
|
275
387
|
},
|
|
276
388
|
},
|
|
277
389
|
});
|
|
390
|
+
// --- Fast tier, part 2: after the Agent exists --------------------------
|
|
391
|
+
//
|
|
392
|
+
// `FastWiring` subscribes to the lead's event stream, so it cannot be built
|
|
393
|
+
// before the `Agent`. The prompt is therefore composed twice at startup:
|
|
394
|
+
// once above without `<fast_tier>` (the wiring is still null) and once here.
|
|
395
|
+
// That is one extra string build at construction and no live path.
|
|
396
|
+
this.fast = fastRegistered
|
|
397
|
+
? new FastWiring({
|
|
398
|
+
getConfig: () => this.config,
|
|
399
|
+
hasKey: (id) => this.hasApiKey(id),
|
|
400
|
+
getApiKey: (id) => this.resolveKey(id),
|
|
401
|
+
isPricedModel: (ref) => this.isPricedModel(ref),
|
|
402
|
+
subscribe: (listener) => this.agent.subscribe(listener),
|
|
403
|
+
complete: (providerId, request) => this.providerRegistry.complete(providerId, request),
|
|
404
|
+
// THE REVIEWER GOES THROUGH THIS METHOD, NOT `agent.steer()` DIRECTLY,
|
|
405
|
+
// so its own block is counted alongside the user's — which is what
|
|
406
|
+
// `userSteerCount` would otherwise mis-report and what would make
|
|
407
|
+
// guard 2 clear a queue it does not own (D-21).
|
|
408
|
+
//
|
|
409
|
+
// It is decremented back out below: the counter must answer "did the
|
|
410
|
+
// USER queue anything", and the reviewer's own message is not that.
|
|
411
|
+
steer: (text) => {
|
|
412
|
+
this.steer(text);
|
|
413
|
+
this.userSteerCount -= 1;
|
|
414
|
+
},
|
|
415
|
+
isRunning: () => this.isRunning(),
|
|
416
|
+
isAbortRequested: () => this.abortRequested,
|
|
417
|
+
userSteerCount: () => this.userSteerCount,
|
|
418
|
+
clearAllQueues: () => this.clearAllQueues(),
|
|
419
|
+
notify: (level, text) => deps.notify?.(level, text),
|
|
420
|
+
onPromptChanged: () => this.rebuildSystemPrompt(),
|
|
421
|
+
})
|
|
422
|
+
: null;
|
|
423
|
+
// The two guards' bookkeeping, and the only reason this class subscribes to
|
|
424
|
+
// its own agent. `turn_start` is where a user steer stops being pending;
|
|
425
|
+
// `agent_start` is where a requested abort stops being in force.
|
|
426
|
+
this.agent.subscribe((event) => {
|
|
427
|
+
if (event.type === 'agent_start')
|
|
428
|
+
this.abortRequested = false;
|
|
429
|
+
if (event.type === 'turn_start')
|
|
430
|
+
this.userSteerCount = 0;
|
|
431
|
+
// The authoritative result arrives with this event, so from this instant
|
|
432
|
+
// the tail is superseded and its slot must be released (§3.1.1). Clearing
|
|
433
|
+
// HERE rather than on read is what keeps the store's size a function of
|
|
434
|
+
// CONCURRENT calls instead of total calls.
|
|
435
|
+
if (event.type === 'tool_execution_end') {
|
|
436
|
+
this.toolOutputs?.clear('lead', event.toolCallId);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
if (this.fast) {
|
|
440
|
+
this.rebuildSystemPrompt();
|
|
441
|
+
// Fast-tier children are counted from the outcome rather than from the
|
|
442
|
+
// specs, so a downgraded child is not miscounted as a saving (§3.6).
|
|
443
|
+
this.teamRuntime?.subscribe((event) => {
|
|
444
|
+
if (event.type !== 'dispatch_end')
|
|
445
|
+
return;
|
|
446
|
+
this.fast?.noteDelegated(event.outcome.runs.filter((r) => r.tier === 'fast').length);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
// ONE LINE FOR THE TWO CONFIGURATION MISTAKES THAT ARE OTHERWISE INVISIBLE
|
|
450
|
+
// (§3.3): the tier is on but unnamed, or named against a provider with no
|
|
451
|
+
// key. Nothing else in the session would ever mention either.
|
|
452
|
+
const problem = describeFastTierProblem(fastTier, fastProviderOf(config));
|
|
453
|
+
if (problem)
|
|
454
|
+
deps.notify?.('warn', problem);
|
|
278
455
|
}
|
|
279
456
|
resolveKey(providerId) {
|
|
280
457
|
return makeGetApiKey(this.config)(providerId);
|
|
@@ -324,6 +501,11 @@ export class AgentController {
|
|
|
324
501
|
? buildTeamBlock({
|
|
325
502
|
maxSubagents: this.config.team.maxSubagents,
|
|
326
503
|
maxConcurrent: this.config.team.maxConcurrent,
|
|
504
|
+
// One cross-reference sentence, present only when the capability
|
|
505
|
+
// is (§3.8). `this.fast` is null during the FIRST compose (the
|
|
506
|
+
// wiring needs the Agent), which is why the constructor composes
|
|
507
|
+
// again once it exists.
|
|
508
|
+
fastDelegation: this.fast?.delegationAvailable() === true,
|
|
327
509
|
})
|
|
328
510
|
: '',
|
|
329
511
|
// BOTH FLAGS, exactly as team mode uses both, and for the identical
|
|
@@ -336,6 +518,17 @@ export class AgentController {
|
|
|
336
518
|
panelVisible: this.todoPanelCapable && this.config.todo.panel,
|
|
337
519
|
})
|
|
338
520
|
: '',
|
|
521
|
+
// The SAME two flags one more time, plus the live resolution — all three
|
|
522
|
+
// live inside `FastWiring.available()` (§3.3). `buildFastBlock` itself
|
|
523
|
+
// returns `''` when neither capability is on, so a user who turned both
|
|
524
|
+
// off pays nothing for the tier being technically live.
|
|
525
|
+
fastBlock: this.fast && this.fast.available()
|
|
526
|
+
? buildFastBlock({
|
|
527
|
+
model: this.fast.fastRef()?.modelId ?? '',
|
|
528
|
+
delegate: this.config.fast.delegate,
|
|
529
|
+
review: this.config.fast.review,
|
|
530
|
+
})
|
|
531
|
+
: '',
|
|
339
532
|
});
|
|
340
533
|
}
|
|
341
534
|
rebuildSystemPrompt() {
|
|
@@ -437,9 +630,21 @@ export class AgentController {
|
|
|
437
630
|
// an unfinished one survives "continue" / "now do the rest" but not four
|
|
438
631
|
// unrelated turns.
|
|
439
632
|
this.todos?.beginUserTurn();
|
|
633
|
+
// The fast reviewer is asked whether the run is on track FOR THIS (§3.5.2).
|
|
634
|
+
// Without it the reviewer judges progress without being told the goal, which
|
|
635
|
+
// is the one input it cannot reconstruct from the event stream.
|
|
636
|
+
this.fast?.setGoal(text);
|
|
440
637
|
return this.agent.prompt(text);
|
|
441
638
|
}
|
|
442
639
|
abort() {
|
|
640
|
+
// SET BEFORE `agent.abort()`, SYNCHRONOUSLY — guard 1 of §3.5.4a. The loop
|
|
641
|
+
// still emits the batch's final `tool_execution_end` (`agent-loop.ts:249`)
|
|
642
|
+
// after an Esc lands inside the last tool's `await`, and the reviewer's
|
|
643
|
+
// listener runs synchronously inside that emit; without this flag it would
|
|
644
|
+
// see `isRunning() === true`, steer, and strand a critique in a queue
|
|
645
|
+
// nothing ever clears (RV-1).
|
|
646
|
+
this.abortRequested = true;
|
|
647
|
+
this.fast?.abort();
|
|
443
648
|
this.agent.abort();
|
|
444
649
|
// DELIBERATE REDUNDANCY (§3.8). Aborting the lead already aborts the
|
|
445
650
|
// in-flight `task` call's `ctx.signal`, which the runtime listens to — that
|
|
@@ -456,6 +661,13 @@ export class AgentController {
|
|
|
456
661
|
// routes through `steer` whenever the agent happens to be running.
|
|
457
662
|
if (this.skillsEnabled)
|
|
458
663
|
this.skills.absorbPendingFrames();
|
|
664
|
+
// COUNTED so the reviewer can prove it owns the queue before clearing it
|
|
665
|
+
// (§3.5.4a guard 2 / D-21). This method is the only other writer of
|
|
666
|
+
// `steeringQueue` in the package, and `Agent` exposes no way to remove ONE
|
|
667
|
+
// message — so `userSteerCount === 0` is the only sound licence for
|
|
668
|
+
// `clearAllQueues()`, and with a user message in there the reviewer must
|
|
669
|
+
// leave its own residue rather than destroy something the user typed.
|
|
670
|
+
this.userSteerCount += 1;
|
|
459
671
|
this.agent.steer(text);
|
|
460
672
|
}
|
|
461
673
|
followUp(text) {
|
|
@@ -632,6 +844,7 @@ export class AgentController {
|
|
|
632
844
|
/** Abort every child and clear timers. Idempotent; safe to call twice. */
|
|
633
845
|
dispose() {
|
|
634
846
|
this.teamRuntime?.dispose();
|
|
847
|
+
this.fast?.dispose();
|
|
635
848
|
}
|
|
636
849
|
// -----------------------------------------------------------------------
|
|
637
850
|
// Todo planning (todo-plan-execution §3.6a)
|
|
@@ -708,11 +921,47 @@ export class AgentController {
|
|
|
708
921
|
this.todos?.clear('user');
|
|
709
922
|
}
|
|
710
923
|
// -----------------------------------------------------------------------
|
|
924
|
+
// API retry (llm-api-retry-backoff §6.3)
|
|
925
|
+
//
|
|
926
|
+
// NO `retryRegistered` TWIN of the team / todo flag pairs, and that asymmetry
|
|
927
|
+
// is the point: retry is not a tool, so nothing is decided at construction.
|
|
928
|
+
// The policy lives on the `ProviderRegistry`, which means it can be replaced
|
|
929
|
+
// for a running session — including for children, because `TeamRuntime` holds
|
|
930
|
+
// the same registry instance.
|
|
931
|
+
// -----------------------------------------------------------------------
|
|
932
|
+
getRetryConfig() {
|
|
933
|
+
return this.config.retry;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Adopt a new retry config for this session AND PUSH IT INTO THE REGISTRY.
|
|
937
|
+
*
|
|
938
|
+
* THE SECOND CALL IS REQUIRED, NOT A NICETY (§6.3). Without it `/retry off`
|
|
939
|
+
* would persist correctly and change nothing until the next launch — the
|
|
940
|
+
* precise failure `config/schema.ts` warns about twice already, and the one a
|
|
941
|
+
* user reports as "the setting does nothing".
|
|
942
|
+
*
|
|
943
|
+
* Clamped here as well as on the persist path, so a bad value can reach neither
|
|
944
|
+
* the runtime nor the file.
|
|
945
|
+
*/
|
|
946
|
+
setRetryConfig(patch) {
|
|
947
|
+
const retry = clampRetryConfig({ ...this.config.retry, ...patch });
|
|
948
|
+
this.config = { ...this.config, retry };
|
|
949
|
+
this.providerRegistry.setRetryPolicy(toRetryPolicy(retry));
|
|
950
|
+
return retry;
|
|
951
|
+
}
|
|
952
|
+
// -----------------------------------------------------------------------
|
|
711
953
|
// Mutators
|
|
712
954
|
// -----------------------------------------------------------------------
|
|
713
955
|
setModel(provider, model, baseUrl) {
|
|
714
956
|
this.config = { ...this.config, provider, model, baseUrl };
|
|
715
957
|
this.agent.setModel({ providerId: provider, modelId: model, ...(baseUrl ? { baseUrl } : {}) });
|
|
958
|
+
// NOT NEUTRAL TO THE FAST TIER even when the user never touched a `fast.*`
|
|
959
|
+
// key (§3.9 / RV-3): `fast.provider: ''` and `fast.baseUrl: ''` INHERIT from
|
|
960
|
+
// exactly these three fields. Switching the main provider can therefore turn
|
|
961
|
+
// a working tier into `no_key`, or silently re-point an inheriting tier at
|
|
962
|
+
// another vendor's gateway — and `<fast_tier>` would go on naming the model
|
|
963
|
+
// it used to resolve to.
|
|
964
|
+
this.fast?.onConfigChanged();
|
|
716
965
|
}
|
|
717
966
|
setThinkingLevel(level) {
|
|
718
967
|
this.config = { ...this.config, thinkingLevel: level };
|
|
@@ -729,6 +978,9 @@ export class AgentController {
|
|
|
729
978
|
...this.config,
|
|
730
979
|
apiKeys: { ...this.config.apiKeys, [provider]: key },
|
|
731
980
|
};
|
|
981
|
+
// Rule 5 of §3.2 reads `apiKeys`, so a key edit is a tier event: this is the
|
|
982
|
+
// path that turns `no_key` back into a working tier without a relaunch.
|
|
983
|
+
this.fast?.onConfigChanged();
|
|
732
984
|
}
|
|
733
985
|
setCwd(cwd) {
|
|
734
986
|
this.cwd = cwd;
|
|
@@ -744,6 +996,61 @@ export class AgentController {
|
|
|
744
996
|
getCwd() {
|
|
745
997
|
return this.cwd;
|
|
746
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* The structured diff a file tool left behind, CONSUMED ONCE.
|
|
1001
|
+
*
|
|
1002
|
+
* Called by `reduceEvent` on `tool_execution_end` and nowhere else, which is
|
|
1003
|
+
* what keeps the store at constant size however many files a session edits.
|
|
1004
|
+
* `undefined` is an ordinary answer — a tool that records nothing, a toolset
|
|
1005
|
+
* built without a recorder, or an entry the 64-write ring has already evicted
|
|
1006
|
+
* — and every one of those falls back to `ToolPreview`.
|
|
1007
|
+
*/
|
|
1008
|
+
takeFilePatch(toolCallId) {
|
|
1009
|
+
return this.fileChanges.take('lead', toolCallId);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Sanitise a chunk into the store and hand the resulting tail to the view.
|
|
1013
|
+
*
|
|
1014
|
+
* SYNCHRONOUS, INSIDE THE CHILD-PROCESS DATA HANDLER. The alternative (queue
|
|
1015
|
+
* plus timer) adds a second clock to a component that already has one, and
|
|
1016
|
+
* `App` coalesces at the other end anyway (D-34).
|
|
1017
|
+
*
|
|
1018
|
+
* EVERY LISTENER IS WRAPPED, and that is not defensive noise (R-5 / AC-35): a
|
|
1019
|
+
* throw here propagates into `child.stdout.on('data')`, which is the reason a
|
|
1020
|
+
* render-side bug could otherwise kill a ten-minute build. `debug` rather than
|
|
1021
|
+
* `warn` because a listener that throws once throws on every chunk.
|
|
1022
|
+
*/
|
|
1023
|
+
emitToolOutput(owner, toolCallId, chunk) {
|
|
1024
|
+
if (!this.toolOutputs)
|
|
1025
|
+
return;
|
|
1026
|
+
const rows = this.toolOutputs.append(owner, toolCallId, chunk);
|
|
1027
|
+
for (const listener of this.outputListeners) {
|
|
1028
|
+
try {
|
|
1029
|
+
listener({ toolCallId, rows });
|
|
1030
|
+
}
|
|
1031
|
+
catch (err) {
|
|
1032
|
+
getLogger().debug('tool', 'tool_output_listener_threw', {
|
|
1033
|
+
toolCallId,
|
|
1034
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Subscribe to the CLI-local live tool-output stream (§3.1.3).
|
|
1041
|
+
*
|
|
1042
|
+
* Returns a no-op unsubscribe when `liveToolOutput` was off at construction,
|
|
1043
|
+
* so every caller can subscribe unconditionally -- the contract
|
|
1044
|
+
* `subscribeTeam` / `subscribeTodos` / `subscribeFast` already set.
|
|
1045
|
+
*/
|
|
1046
|
+
subscribeToolOutput(listener) {
|
|
1047
|
+
if (!this.toolOutputs)
|
|
1048
|
+
return () => { };
|
|
1049
|
+
this.outputListeners.add(listener);
|
|
1050
|
+
return () => {
|
|
1051
|
+
this.outputListeners.delete(listener);
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
747
1054
|
clearMessages() {
|
|
748
1055
|
this.agent.clearMessages();
|
|
749
1056
|
// A new conversation has no loaded skills, so the "already loaded earlier
|
|
@@ -782,10 +1089,95 @@ export class AgentController {
|
|
|
782
1089
|
}
|
|
783
1090
|
/** Resolve `ModelInfo` for the active model, falling back to a runtime model. */
|
|
784
1091
|
getModelInfo() {
|
|
785
|
-
|
|
1092
|
+
return this.getModelInfoFor({
|
|
1093
|
+
providerId: this.config.provider,
|
|
1094
|
+
modelId: this.config.model,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Resolve `ModelInfo` for ANY model (fast-model-tier §3.6 / RV-4).
|
|
1099
|
+
*
|
|
1100
|
+
* ONE RESOLUTION PATH, which is the same reason §3.2 has exactly one
|
|
1101
|
+
* `resolveFastTier`: `getModelInfo()` above is now a call into this, so "the
|
|
1102
|
+
* cost table for the model that actually billed" cannot drift from "the cost
|
|
1103
|
+
* table for the session's model" as two separate bodies.
|
|
1104
|
+
*/
|
|
1105
|
+
getModelInfoFor(ref) {
|
|
1106
|
+
const found = this.modelRegistry.getModel(ref.providerId, ref.modelId);
|
|
786
1107
|
if (found)
|
|
787
1108
|
return found;
|
|
788
|
-
return this.modelRegistry.buildRuntimeModel(
|
|
1109
|
+
return this.modelRegistry.buildRuntimeModel(ref.providerId, ref.modelId);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Whether the STATIC table knows this model's price (C-11 / RV-4).
|
|
1113
|
+
*
|
|
1114
|
+
* `buildRuntimeModel` returns `cost: { input: 0, output: 0 }` for a model it
|
|
1115
|
+
* has never seen — a table of zeros, not a missing one — so every consumer
|
|
1116
|
+
* that renders spend needs to be able to tell "free" from "unknown". The fast
|
|
1117
|
+
* tier is precisely where an unrecognised id is likely, and `$0.00` on a
|
|
1118
|
+
* feature that is spending money is the same class of lie as pricing a Haiku
|
|
1119
|
+
* child at Sonnet rates.
|
|
1120
|
+
*/
|
|
1121
|
+
isPricedModel(ref) {
|
|
1122
|
+
return this.modelRegistry.getModel(ref.providerId, ref.modelId) !== undefined;
|
|
1123
|
+
}
|
|
1124
|
+
// -----------------------------------------------------------------------
|
|
1125
|
+
// Fast model tier (fast-model-tier §3.1) — FOUR THIN FORWARDERS
|
|
1126
|
+
// -----------------------------------------------------------------------
|
|
1127
|
+
/** Whether `task`'s schema carries `model`. Fixed at construction (C-2). */
|
|
1128
|
+
isFastRegistered() {
|
|
1129
|
+
return this.fast !== null;
|
|
1130
|
+
}
|
|
1131
|
+
/** Whether the tier will actually be used. Flipped live by `/fast on|off`. */
|
|
1132
|
+
isFastEnabled() {
|
|
1133
|
+
return this.fast?.isEnabled() === true;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Flip the live switch and re-render the prompt.
|
|
1137
|
+
*
|
|
1138
|
+
* Turning it ON in a session that started without the tier deliberately does
|
|
1139
|
+
* NOT register it: the tool array is immutable (C-2), and advertising a schema
|
|
1140
|
+
* field that is not there is the dead end `/team on` already documents.
|
|
1141
|
+
* `/fast` reports that honestly by checking `isFastRegistered()` first.
|
|
1142
|
+
*/
|
|
1143
|
+
setFastEnabled(enabled) {
|
|
1144
|
+
this.fast?.setEnabled(enabled);
|
|
1145
|
+
}
|
|
1146
|
+
getFastConfig() {
|
|
1147
|
+
return this.config.fast;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Adopt a new fast config for this session.
|
|
1151
|
+
*
|
|
1152
|
+
* THE RE-RESOLUTION IS NOT OPTIONAL (§3.2 rule 8). `App` reads
|
|
1153
|
+
* `controller.getConfig()` on every render and the block interpolates the
|
|
1154
|
+
* model id, so a persist without this call reports success and changes
|
|
1155
|
+
* nothing until relaunch — the failure `/todo panel` records (P1-2).
|
|
1156
|
+
*/
|
|
1157
|
+
setFastConfig(patch) {
|
|
1158
|
+
const fast = clampFastConfig({ ...this.config.fast, ...patch });
|
|
1159
|
+
this.config = { ...this.config, fast };
|
|
1160
|
+
// `onConfigChanged` rebuilds the prompt; when there is no wiring (the tier
|
|
1161
|
+
// was never registered) nothing has to be rebuilt, because no block is
|
|
1162
|
+
// spliced either way.
|
|
1163
|
+
this.fast?.onConfigChanged();
|
|
1164
|
+
return fast;
|
|
1165
|
+
}
|
|
1166
|
+
/** Everything `/fast status`, the settings line and the chip read. */
|
|
1167
|
+
getFastStatus() {
|
|
1168
|
+
return this.fast ? this.fast.status() : offFastStatus();
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Subscribe to the CLI-local fast event stream.
|
|
1172
|
+
*
|
|
1173
|
+
* Returns a no-op unsubscribe when the tier was off at construction, so every
|
|
1174
|
+
* caller can subscribe unconditionally — the contract `subscribeTeam` and
|
|
1175
|
+
* `subscribeTodos` already set.
|
|
1176
|
+
*/
|
|
1177
|
+
subscribeFast(listener) {
|
|
1178
|
+
if (!this.fast)
|
|
1179
|
+
return () => { };
|
|
1180
|
+
return this.fast.subscribe(listener);
|
|
789
1181
|
}
|
|
790
1182
|
/** List usable tools (name + description) for the /tools command. */
|
|
791
1183
|
listTools() {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded view state (tui-render-performance L1 / R5).
|
|
3
|
+
*
|
|
4
|
+
* `ViewState.entries` used to grow without any ceiling: `argsRaw`, assistant
|
|
5
|
+
* `text` and `thinking` all concatenated forever, and the entry array was never
|
|
6
|
+
* trimmed. That makes the per-frame cost of the renderer proportional to the
|
|
7
|
+
* total number of characters the session has ever produced, because Ink's
|
|
8
|
+
* `renderNodeToOutput` walks and measures every mounted text node whether or not
|
|
9
|
+
* `overflow: hidden` clips it.
|
|
10
|
+
*
|
|
11
|
+
* Two bounds live here, both pure and React-free:
|
|
12
|
+
* - `appendBounded` caps one field by eliding its MIDDLE, and
|
|
13
|
+
* - `trimEntries` ring-trims the entry array itself.
|
|
14
|
+
*
|
|
15
|
+
* ASCII ONLY. This module is produced outside `src/ui/**`, where no terminal
|
|
16
|
+
* capabilities are available, so the elision marker has to be safe on a legacy
|
|
17
|
+
* `cmd.exe` -- the same rule `PREVIEW_TRUNCATION_MARK` in `reducer.ts` records.
|
|
18
|
+
*/
|
|
19
|
+
import type { Entry } from './reducer.js';
|
|
20
|
+
export interface EntryLimits {
|
|
21
|
+
/** Assistant answer text ceiling, in characters. */
|
|
22
|
+
text: number;
|
|
23
|
+
/** Assistant thinking ceiling. */
|
|
24
|
+
thinking: number;
|
|
25
|
+
/** Streamed tool-argument JSON ceiling. */
|
|
26
|
+
argsRaw: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The per-field ceilings.
|
|
30
|
+
*
|
|
31
|
+
* `text` is the generous one on purpose: a 256 KiB answer is already far past
|
|
32
|
+
* anything a terminal can usefully show, and the middle is what gets elided, so
|
|
33
|
+
* both the opening and the live cursor survive. `argsRaw` is the tight one --
|
|
34
|
+
* it is a streamed JSON fragment rendered as a 60-character card summary, so
|
|
35
|
+
* anything past a few KiB is invisible by construction.
|
|
36
|
+
*/
|
|
37
|
+
export declare const ENTRY_LIMITS: EntryLimits;
|
|
38
|
+
/** Marker inserted at the elision point. ASCII -- produced outside `src/ui/**`. */
|
|
39
|
+
export declare const ELISION_MARK_PREFIX = "\n[... ";
|
|
40
|
+
export declare const ELISION_MARK_SUFFIX = " characters elided ...]\n";
|
|
41
|
+
/**
|
|
42
|
+
* Append `delta` to `prev`, keeping the result at or below `cap` by eliding the
|
|
43
|
+
* MIDDLE. Head and tail are both preserved: the head is the answer's opening,
|
|
44
|
+
* the tail is where the cursor is. Idempotent -- re-eliding an already elided
|
|
45
|
+
* string collapses the two markers into one.
|
|
46
|
+
*
|
|
47
|
+
* The returned string is NEVER longer than `cap` (AC-7). The marker's own digit
|
|
48
|
+
* count is bounded before the tail budget is computed, so the arithmetic cannot
|
|
49
|
+
* overshoot by a digit the way a naive `cap - marker.length` would.
|
|
50
|
+
*/
|
|
51
|
+
export declare function appendBounded(prev: string, delta: string, cap: number): string;
|
|
52
|
+
export interface TrimResult {
|
|
53
|
+
entries: Entry[];
|
|
54
|
+
dropped: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ring-trim to `retain` entries; returns the SAME array reference when no-op.
|
|
58
|
+
*
|
|
59
|
+
* The identity return is load-bearing: the reducer spreads this result into a
|
|
60
|
+
* new `ViewState` on every append, and a fresh array on every action would
|
|
61
|
+
* defeat the `React.memo` boundaries L2 installs on the transcript.
|
|
62
|
+
*/
|
|
63
|
+
export declare function trimEntries(entries: Entry[], retain: number): TrimResult;
|
|
64
|
+
export declare function setEntryRetain(value: number): void;
|
|
65
|
+
export declare function entryRetain(): number;
|