@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
package/dist/team/task-tool.d.ts
CHANGED
|
@@ -27,12 +27,38 @@ export interface TaskToolDeps {
|
|
|
27
27
|
runtime: TeamRuntime;
|
|
28
28
|
/** Read LIVE, never cached: `/team off` flips this, it does not unregister. */
|
|
29
29
|
isTeamEnabled: () => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the fast tier was resolvable AT CONSTRUCTION (fast-model-tier §3.3 /
|
|
32
|
+
* C-2).
|
|
33
|
+
*
|
|
34
|
+
* DECIDED ONCE, because the tool array is built once and must never be
|
|
35
|
+
* rebuilt: `Agent.setTools()` mutates the live `ToolRegistry`, and
|
|
36
|
+
* `submit_plan` flips the session mode from INSIDE a tool execution. So the
|
|
37
|
+
* `model` property either exists in this schema for the whole session or never
|
|
38
|
+
* does — and with it absent, `TASK_DESCRIPTION` and the schema are
|
|
39
|
+
* BYTE-IDENTICAL to the pre-feature build (AC-3).
|
|
40
|
+
*
|
|
41
|
+
* Optional so every existing construction site and test compiles unchanged.
|
|
42
|
+
*/
|
|
43
|
+
fastRegistered?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether `model:"fast"` is honoured RIGHT NOW. Read LIVE, at dispatch time
|
|
46
|
+
* (RV-3) — the tier can stop resolving mid-session, and a downgrade is the
|
|
47
|
+
* honest observable either way.
|
|
48
|
+
*/
|
|
49
|
+
fastAvailable?: () => boolean;
|
|
30
50
|
maxSubagents: () => number;
|
|
31
51
|
/** Whether a usable key exists for the active provider right now (P2-3). */
|
|
32
52
|
hasApiKey: () => boolean;
|
|
33
53
|
activeProvider: () => string;
|
|
34
54
|
/** The lead's cost table, so the report header states real spend (R-5). */
|
|
35
55
|
modelCost: () => ModelCost | undefined;
|
|
56
|
+
/** The FAST model's cost table (fast-model-tier §3.4). Absent when the tier is
|
|
57
|
+
* off, which is what leaves the report byte-identical. */
|
|
58
|
+
fastModelCost?: () => ModelCost | undefined;
|
|
59
|
+
/** Whether the static price table has never heard of the fast model (C-11 /
|
|
60
|
+
* RV-4). `unknown` is rendered as unknown, NEVER as `$0.00`. */
|
|
61
|
+
fastPricingUnknown?: () => boolean;
|
|
36
62
|
/**
|
|
37
63
|
* Run the dispatch with the LEAD's idle watchdog paused, `try/finally` inside.
|
|
38
64
|
*
|
package/dist/team/task-tool.js
CHANGED
|
@@ -41,11 +41,37 @@ const TASK_DESCRIPTION = 'Run several independent subagents in parallel and get
|
|
|
41
41
|
'cannot ask the user anything. Give each a description of at most ' +
|
|
42
42
|
`${TEAM_LIMITS.descriptionChars} characters and a complete prompt. Extra entries beyond the ` +
|
|
43
43
|
'configured maximum are dropped, so keep within it.';
|
|
44
|
+
/**
|
|
45
|
+
* The one sentence appended when the fast tier exists (fast-model-tier §4.1).
|
|
46
|
+
*
|
|
47
|
+
* WHEN to use a tier is POLICY and lives here and in `<fast_tier>`, where the
|
|
48
|
+
* model actually reads it; the `enum` below is SHAPE and lives in the schema.
|
|
49
|
+
* That split is what keeps behaviour the same whether or not `ajv` — an OPTIONAL
|
|
50
|
+
* dependency of core — happened to install (C-8).
|
|
51
|
+
*/
|
|
52
|
+
const TASK_FAST_SENTENCE = ' Set model:"fast" on a subagent whose job is bulk reading, searching or summarizing; ' +
|
|
53
|
+
'leave it out for work that needs judgement.';
|
|
54
|
+
/**
|
|
55
|
+
* NOT `as const`: `AgentTool.parameters` is a `JSONSchema7`, whose `enum` is a
|
|
56
|
+
* MUTABLE `unknown[]`, and a readonly tuple is not assignable to it.
|
|
57
|
+
*/
|
|
58
|
+
const SUBAGENT_MODEL_PROPERTY = {
|
|
59
|
+
type: 'string',
|
|
60
|
+
enum: ['main', 'fast'],
|
|
61
|
+
default: 'main',
|
|
62
|
+
description: 'Which tier runs this subagent. "fast" is a cheaper, quicker model - use it for ' +
|
|
63
|
+
'mechanical, high-volume work (reading or searching many files, summarizing long ' +
|
|
64
|
+
'output, mechanical edits). Keep "main" for design, tricky debugging, and anything ' +
|
|
65
|
+
'that must be right the first time.',
|
|
66
|
+
};
|
|
44
67
|
export function createTaskTool(deps) {
|
|
68
|
+
const fastRegistered = deps.fastRegistered === true;
|
|
45
69
|
return {
|
|
46
70
|
name: 'task',
|
|
47
71
|
label: 'Dispatch subagents',
|
|
48
|
-
|
|
72
|
+
// BYTE-IDENTICAL WITHOUT THE TIER (AC-3): concatenation is conditional, so a
|
|
73
|
+
// default session's description is the exact string it always was.
|
|
74
|
+
description: fastRegistered ? `${TASK_DESCRIPTION}${TASK_FAST_SENTENCE}` : TASK_DESCRIPTION,
|
|
49
75
|
parameters: {
|
|
50
76
|
type: 'object',
|
|
51
77
|
properties: {
|
|
@@ -73,6 +99,11 @@ export function createTaskTool(deps) {
|
|
|
73
99
|
default: false,
|
|
74
100
|
description: 'Refuse all writes and shell for this subagent.',
|
|
75
101
|
},
|
|
102
|
+
// Spread rather than a `?:` property, so with the tier off the
|
|
103
|
+
// object has exactly the four keys it always had — `undefined`
|
|
104
|
+
// survives `JSON.stringify` as an absent key but not as an
|
|
105
|
+
// identical object, and AC-3 is asserted on the schema itself.
|
|
106
|
+
...(fastRegistered ? { model: SUBAGENT_MODEL_PROPERTY } : {}),
|
|
76
107
|
},
|
|
77
108
|
required: ['description', 'prompt'],
|
|
78
109
|
},
|
|
@@ -100,19 +131,32 @@ export function createTaskTool(deps) {
|
|
|
100
131
|
'yourself, or ask the user to set a key.');
|
|
101
132
|
}
|
|
102
133
|
// 4. Repair, never reject. Zero survivors is the single hard failure.
|
|
103
|
-
|
|
134
|
+
// `fastAvailable` is read HERE, at dispatch time, not captured at
|
|
135
|
+
// construction (RV-3): the tier can stop resolving mid-session, and the
|
|
136
|
+
// normalizer and the subagent factory must agree about that at the same
|
|
137
|
+
// instant or a child gets built against a provider with no key.
|
|
138
|
+
const { specs, requested, downgraded } = normalizeSubagentSpecs(params.subagents, deps.maxSubagents(), { fastAvailable: deps.fastAvailable?.() === true });
|
|
104
139
|
if (specs.length === 0) {
|
|
105
140
|
return errorResult('No usable subagent specs: each needs a description and a prompt.');
|
|
106
141
|
}
|
|
107
142
|
// 5. Pause the lead's watchdog for the whole dispatch (I-3).
|
|
108
|
-
const outcome = await deps.withPausedWatchdog(() => deps.runtime.dispatch(specs, requested, ctx.signal));
|
|
143
|
+
const outcome = await deps.withPausedWatchdog(() => deps.runtime.dispatch(specs, requested, ctx.signal, { downgraded }));
|
|
109
144
|
// 6. ALWAYS A NON-ERROR RESULT, abort and all-children-failed included.
|
|
110
145
|
// A cancellation or a partial result rendered as a tool FAILURE invites
|
|
111
146
|
// the model to retry the whole fan-out, which is the most expensive
|
|
112
147
|
// possible reaction (the same reasoning as `ask_user`'s cancelled
|
|
113
148
|
// shape, and D-14 one level up).
|
|
149
|
+
// TWO COST TABLES (§3.4 / R-7). Summing fast-tier tokens at the lead's
|
|
150
|
+
// price is not a rounding error: for a Haiku child under a Sonnet lead
|
|
151
|
+
// it over-reports by roughly an order of magnitude, and the whole
|
|
152
|
+
// justification for the feature is a number in this report.
|
|
114
153
|
const cost = deps.modelCost();
|
|
115
|
-
|
|
154
|
+
const fastCost = deps.fastModelCost?.();
|
|
155
|
+
return textResult(buildDispatchReport(outcome, {
|
|
156
|
+
...(cost ? { cost } : {}),
|
|
157
|
+
...(fastCost ? { fastCost } : {}),
|
|
158
|
+
...(deps.fastPricingUnknown?.() ? { fastPricingUnknown: true } : {}),
|
|
159
|
+
}));
|
|
116
160
|
},
|
|
117
161
|
};
|
|
118
162
|
}
|
package/dist/team/types.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* changes ZERO files under `packages/core/` (AC-12).
|
|
12
12
|
*/
|
|
13
13
|
import type { TokenUsage } from '@aragon-agent/core';
|
|
14
|
+
import type { FastTierName } from '../fast/types.js';
|
|
14
15
|
import type { ActivityArgs } from './activity.js';
|
|
15
16
|
/** One normalized delegation request. Produced only by `normalizeSubagentSpecs`. */
|
|
16
17
|
export interface SubagentSpec {
|
|
@@ -26,12 +27,34 @@ export interface SubagentSpec {
|
|
|
26
27
|
* permission than the session it was spawned from.
|
|
27
28
|
*/
|
|
28
29
|
readOnly: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Which model runs this child (fast-model-tier §3.4).
|
|
32
|
+
*
|
|
33
|
+
* THE WIRE NAME IS `model` AND THE INTERNAL NAME IS `tier`, on purpose: a
|
|
34
|
+
* model expects to see `model:"fast"`, while `SubagentSpec.model` would read
|
|
35
|
+
* as a model ID at every call site in this package.
|
|
36
|
+
*
|
|
37
|
+
* DELEGATION IS A MODEL CHOICE, NEVER A PERMISSION BOUNDARY (D-15 / I-4). A
|
|
38
|
+
* fast child gets the same tools, the same `--confirm` gate, the same
|
|
39
|
+
* plan-mode gate and the same skills ceiling as any other; only the model
|
|
40
|
+
* differs. Restricting a fast child's tools would look like security, would
|
|
41
|
+
* not be, and would make the report's `[fast]` rows mean two different things.
|
|
42
|
+
*/
|
|
43
|
+
tier: FastTierName;
|
|
29
44
|
}
|
|
30
45
|
export type SubagentPhase = 'queued' | 'starting' | 'thinking' | 'tool' | 'waiting' | 'done' | 'failed' | 'aborted';
|
|
31
46
|
/** Live, mutable record of one child. Projected into the UI and the report. */
|
|
32
47
|
export interface SubagentRun {
|
|
33
48
|
label: string;
|
|
34
49
|
description: string;
|
|
50
|
+
/**
|
|
51
|
+
* The tier this child actually ran on (fast-model-tier §3.4).
|
|
52
|
+
*
|
|
53
|
+
* WHAT RAN, NOT WHAT WAS ASKED FOR. A spec the normalizer downgraded arrives
|
|
54
|
+
* here as `'main'`, which is what makes the report's `[fast]` annotation and
|
|
55
|
+
* its second cost table agree with the bill.
|
|
56
|
+
*/
|
|
57
|
+
tier: FastTierName;
|
|
35
58
|
phase: SubagentPhase;
|
|
36
59
|
startedAt?: number;
|
|
37
60
|
endedAt?: number;
|
|
@@ -104,6 +127,42 @@ export interface SubagentRun {
|
|
|
104
127
|
* `formatStreamError` and must never become a machine-readable contract.
|
|
105
128
|
*/
|
|
106
129
|
retryable?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The child's LIVE API-retry state (llm-api-retry-backoff §6.10), or absent when
|
|
132
|
+
* it is not in a backoff.
|
|
133
|
+
*
|
|
134
|
+
* THIS EXISTS BECAUSE A CHILD'S RETRY IS OTHERWISE INVISIBLE (R-14). Subagents
|
|
135
|
+
* inherit the lead's retry policy for free — `TeamRuntime` reuses the lead's
|
|
136
|
+
* registry instance — but a child's `retry_scheduled` goes to that CHILD's own
|
|
137
|
+
* `Agent` listeners and never reaches the lead's `ViewState`. Without this field
|
|
138
|
+
* a subagent spending three minutes in backoff shows no card, no chip and no
|
|
139
|
+
* countdown: the dispatch simply appears hung, which is the single worst reading
|
|
140
|
+
* of a mechanism whose whole claim is that waiting is now legible.
|
|
141
|
+
*
|
|
142
|
+
* The fix uses the channel that already exists rather than inventing a second
|
|
143
|
+
* card: `TeamCard` and `transcript-text` already render one row per child, and
|
|
144
|
+
* this rides on it. `ViewState.retry` stays LEAD-ONLY, and the lead's status-bar
|
|
145
|
+
* chip is deliberately NOT driven by children — `agents 3/5` already reports the
|
|
146
|
+
* dispatch, and a second aggregate would put two numbers on the bar that
|
|
147
|
+
* disagree about what "3" means.
|
|
148
|
+
*
|
|
149
|
+
* EPHEMERAL, exactly like `activity` and `activityArgs`: cleared on the child's
|
|
150
|
+
* next `turn_end` or terminal error, never persisted, and a settled team entry
|
|
151
|
+
* keeps only what the run finally did.
|
|
152
|
+
*
|
|
153
|
+
* NO `resumeAt`, DELIBERATELY, and this is the one place §6.10's sketch is not
|
|
154
|
+
* followed. A countdown needs something to re-render it, and nothing does: the
|
|
155
|
+
* team row is drawn from `agent_update`, a child in backoff emits exactly ONE
|
|
156
|
+
* event for the whole wait, and the lead's 1 Hz `retryTick` is gated on the
|
|
157
|
+
* LEAD's own `state.retry`. A `7s` that never changed for thirty seconds is a
|
|
158
|
+
* worse readout than no number at all — it reads as a hung countdown, which is
|
|
159
|
+
* precisely the impression this row exists to remove. The counter alone is
|
|
160
|
+
* honest and is what AC-34 asserts.
|
|
161
|
+
*/
|
|
162
|
+
retry?: {
|
|
163
|
+
attempt: number;
|
|
164
|
+
maxRetries: number;
|
|
165
|
+
};
|
|
107
166
|
}
|
|
108
167
|
/** One message on the team bus. */
|
|
109
168
|
export interface TeamMessage {
|
|
@@ -124,8 +183,27 @@ export interface DispatchOutcome {
|
|
|
124
183
|
aborted: boolean;
|
|
125
184
|
/** Messages addressed to `lead`; surfaced in their own report section. */
|
|
126
185
|
leadMail: TeamMessage[];
|
|
127
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Summed across MAIN-TIER children. Real spend; it must reach the status bar
|
|
188
|
+
* (§3.9).
|
|
189
|
+
*
|
|
190
|
+
* THE MEANING NARROWED WHEN THE FAST TIER LANDED, and the split is not a
|
|
191
|
+
* rounding concern: a Haiku child under a Sonnet lead priced at the lead's
|
|
192
|
+
* table over-reports by roughly an order of magnitude, and the entire
|
|
193
|
+
* justification for delegating is a number in this report (R-7).
|
|
194
|
+
*/
|
|
128
195
|
usage: TokenUsage;
|
|
196
|
+
/** Summed across FAST-TIER children. Absent when none ran, which is what
|
|
197
|
+
* keeps a pre-feature dispatch's report byte-identical. */
|
|
198
|
+
fastUsage?: TokenUsage;
|
|
199
|
+
/**
|
|
200
|
+
* Specs that asked for `model:"fast"` and ran on `main` anyway (§3.4 / R-6).
|
|
201
|
+
*
|
|
202
|
+
* REPORTED, NEVER SILENT. A silent downgrade means the model asked for a cheap
|
|
203
|
+
* child, got an expensive one, and has no way to learn that its cost model is
|
|
204
|
+
* wrong.
|
|
205
|
+
*/
|
|
206
|
+
downgraded?: number;
|
|
129
207
|
}
|
|
130
208
|
/**
|
|
131
209
|
* What the UI renders: a cheap immutable projection, rebuilt per event.
|
|
@@ -160,6 +238,7 @@ export type TeamEvent = {
|
|
|
160
238
|
dispatchId: string;
|
|
161
239
|
label: string;
|
|
162
240
|
usage: TokenUsage;
|
|
241
|
+
tier: FastTierName;
|
|
163
242
|
} | {
|
|
164
243
|
type: 'message';
|
|
165
244
|
dispatchId: string;
|
package/dist/tools/bash-tool.js
CHANGED
|
@@ -57,7 +57,9 @@ export function makeBash(deps) {
|
|
|
57
57
|
},
|
|
58
58
|
required: ['command'],
|
|
59
59
|
},
|
|
60
|
-
|
|
60
|
+
// `id` rather than `_id`: the tool-call id is what keys the live-output side
|
|
61
|
+
// channel (agent-activity-presentation-live §3.1.2).
|
|
62
|
+
async execute(id, rawParams, ctx) {
|
|
61
63
|
const params = rawParams;
|
|
62
64
|
const command = params.command;
|
|
63
65
|
const cwd = params.cwd ? params.cwd : deps.getCwd();
|
|
@@ -106,11 +108,25 @@ export function makeBash(deps) {
|
|
|
106
108
|
clearTimeout(timer);
|
|
107
109
|
ctx.signal?.removeEventListener('abort', onAbort);
|
|
108
110
|
}
|
|
111
|
+
// `output` IS UNTOUCHED BY THE RECORDER, and that is what makes AC-30
|
|
112
|
+
// checkable by comparison rather than by inspection: the string the
|
|
113
|
+
// model receives, its truncation and the `[exit code N]` footer are all
|
|
114
|
+
// byte-identical to the pre-round build. The recorder is a SECOND
|
|
115
|
+
// consumer of a chunk this tool already had (R-7).
|
|
116
|
+
//
|
|
117
|
+
// No throttle here, deliberately: `append` is O(chunk), and a per-chunk
|
|
118
|
+
// throttle in the tool would drop the LAST chunk of a burst -- the one
|
|
119
|
+
// that matters. What must be throttled is the view update, and `App`
|
|
120
|
+
// does that where the machinery for it already lives (§3.1.2 / D-34).
|
|
109
121
|
child.stdout?.on('data', (d) => {
|
|
110
|
-
|
|
122
|
+
const s = d.toString();
|
|
123
|
+
output += s;
|
|
124
|
+
deps.recordOutput?.(id, s);
|
|
111
125
|
});
|
|
112
126
|
child.stderr?.on('data', (d) => {
|
|
113
|
-
|
|
127
|
+
const s = d.toString();
|
|
128
|
+
output += s;
|
|
129
|
+
deps.recordOutput?.(id, s);
|
|
114
130
|
});
|
|
115
131
|
child.on('error', (err) => {
|
|
116
132
|
finish(() => errorResult(`Failed to run command: ${err.message}`));
|
package/dist/tools/diff.d.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The MODEL-FACING unified diff for `edit_file` (spec §4), re-expressed on top
|
|
3
|
+
* of `buildPatch` (agent-activity-presentation §3.3.4).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The signature, the call site (`fs-tools.ts`), the `---` / `+++` headers and the
|
|
6
|
+
* ` ` / `- ` / `+ ` row prefixes are all unchanged; what changed is that the
|
|
7
|
+
* differing middle is now a real hunk list instead of one removed block followed
|
|
8
|
+
* by one added block. For a `replace_all` touching line 40 and line 900 that used
|
|
9
|
+
* to be 860 removed rows and 860 near-identical added rows.
|
|
10
|
+
*
|
|
11
|
+
* TWO WIDTHS, ONE ALGORITHM (D-20). This renderer keeps `CONTEXT_LINES = 2`; the
|
|
12
|
+
* UI passes `PATCH_LIMITS.context` (3). Collapsing them to one constant is the
|
|
13
|
+
* tempting simplification and it is wrong: `fs-tools.ts` passes no override, so a
|
|
14
|
+
* shared 3 would add two context rows to EVERY model-facing diff — the direction
|
|
15
|
+
* AC-17 forbids, in the most common case, with nothing watching.
|
|
16
|
+
*
|
|
17
|
+
* The shape of the change to the model's input is therefore exact: a
|
|
18
|
+
* single-region edit is today's rows plus ONE `@@` header, a multi-region edit is
|
|
19
|
+
* strictly smaller, and a trailing `\r` no longer reaches the prompt.
|
|
8
20
|
*/
|
|
21
|
+
import { type FilePatch } from './patch.js';
|
|
9
22
|
export interface DiffOptions {
|
|
10
23
|
/** File path shown in the `---`/`+++` header. */
|
|
11
24
|
path?: string;
|
|
@@ -13,3 +26,10 @@ export interface DiffOptions {
|
|
|
13
26
|
context?: number;
|
|
14
27
|
}
|
|
15
28
|
export declare function renderUnifiedDiff(oldStr: string, newStr: string, options?: DiffOptions): string;
|
|
29
|
+
/**
|
|
30
|
+
* A `FilePatch` as unified-diff text.
|
|
31
|
+
*
|
|
32
|
+
* Exported so the patch the user reads and the diff the model reads are provably
|
|
33
|
+
* the same change (D-10), and so `patch.test.ts` can assert the round trip.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderPatchText(patch: FilePatch, path?: string): string;
|
package/dist/tools/diff.js
CHANGED
|
@@ -1,50 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The MODEL-FACING unified diff for `edit_file` (spec §4), re-expressed on top
|
|
3
|
+
* of `buildPatch` (agent-activity-presentation §3.3.4).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The signature, the call site (`fs-tools.ts`), the `---` / `+++` headers and the
|
|
6
|
+
* ` ` / `- ` / `+ ` row prefixes are all unchanged; what changed is that the
|
|
7
|
+
* differing middle is now a real hunk list instead of one removed block followed
|
|
8
|
+
* by one added block. For a `replace_all` touching line 40 and line 900 that used
|
|
9
|
+
* to be 860 removed rows and 860 near-identical added rows.
|
|
10
|
+
*
|
|
11
|
+
* TWO WIDTHS, ONE ALGORITHM (D-20). This renderer keeps `CONTEXT_LINES = 2`; the
|
|
12
|
+
* UI passes `PATCH_LIMITS.context` (3). Collapsing them to one constant is the
|
|
13
|
+
* tempting simplification and it is wrong: `fs-tools.ts` passes no override, so a
|
|
14
|
+
* shared 3 would add two context rows to EVERY model-facing diff — the direction
|
|
15
|
+
* AC-17 forbids, in the most common case, with nothing watching.
|
|
16
|
+
*
|
|
17
|
+
* The shape of the change to the model's input is therefore exact: a
|
|
18
|
+
* single-region edit is today's rows plus ONE `@@` header, a multi-region edit is
|
|
19
|
+
* strictly smaller, and a trailing `\r` no longer reaches the prompt.
|
|
8
20
|
*/
|
|
21
|
+
import { PATCH_LIMITS, buildPatch } from './patch.js';
|
|
9
22
|
const CONTEXT_LINES = 2;
|
|
23
|
+
/** Appended when the rendered text hits `PATCH_LIMITS.modelDiffMaxChars`. */
|
|
24
|
+
const DIFF_TRUNCATED_NOTE = '... (diff truncated)';
|
|
10
25
|
export function renderUnifiedDiff(oldStr, newStr, options = {}) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
const header = options.path
|
|
29
|
-
? [`--- ${options.path}`, `+++ ${options.path}`]
|
|
30
|
-
: [];
|
|
31
|
-
const lines = [];
|
|
32
|
-
const ctxStart = Math.max(0, start - context);
|
|
33
|
-
for (let i = ctxStart; i < start; i += 1) {
|
|
34
|
-
lines.push(` ${oldLines[i]}`);
|
|
35
|
-
}
|
|
36
|
-
for (let i = start; i <= endOld; i += 1) {
|
|
37
|
-
lines.push(`- ${oldLines[i]}`);
|
|
38
|
-
}
|
|
39
|
-
for (let i = start; i <= endNew; i += 1) {
|
|
40
|
-
lines.push(`+ ${newLines[i]}`);
|
|
26
|
+
const patch = buildPatch(oldStr, newStr, {
|
|
27
|
+
path: options.path ?? '',
|
|
28
|
+
context: options.context ?? CONTEXT_LINES,
|
|
29
|
+
});
|
|
30
|
+
return renderPatchText(patch, options.path);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A `FilePatch` as unified-diff text.
|
|
34
|
+
*
|
|
35
|
+
* Exported so the patch the user reads and the diff the model reads are provably
|
|
36
|
+
* the same change (D-10), and so `patch.test.ts` can assert the round trip.
|
|
37
|
+
*/
|
|
38
|
+
export function renderPatchText(patch, path) {
|
|
39
|
+
const header = path ? [`--- ${path}`, `+++ ${path}`] : [];
|
|
40
|
+
if (patch.hunks.length === 0) {
|
|
41
|
+
return `${header.join('\n')}${header.length ? '\n' : ''}(no textual change)`;
|
|
41
42
|
}
|
|
42
|
-
const
|
|
43
|
-
for (
|
|
44
|
-
lines.push(
|
|
43
|
+
const lines = [...header];
|
|
44
|
+
for (const hunk of patch.hunks) {
|
|
45
|
+
lines.push(`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`);
|
|
46
|
+
for (const line of hunk.lines)
|
|
47
|
+
lines.push(`${signOf(line)}${line.text}`);
|
|
45
48
|
}
|
|
46
|
-
if (
|
|
47
|
-
|
|
49
|
+
if (patch.truncated)
|
|
50
|
+
lines.push(DIFF_TRUNCATED_NOTE);
|
|
51
|
+
return clampToBudget(lines);
|
|
52
|
+
}
|
|
53
|
+
function signOf(line) {
|
|
54
|
+
if (line.kind === 'add')
|
|
55
|
+
return '+ ';
|
|
56
|
+
if (line.kind === 'del')
|
|
57
|
+
return '- ';
|
|
58
|
+
return ' ';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Cap the rendered text at `modelDiffMaxChars`.
|
|
62
|
+
*
|
|
63
|
+
* Whole rows, not a mid-row slice: half a diff line reaching the model reads as
|
|
64
|
+
* a real line that happens to be wrong, which is worse than an honestly short
|
|
65
|
+
* diff. `edit_file`'s result had no bound at all before this round beyond the
|
|
66
|
+
* executor's 100 KB backstop.
|
|
67
|
+
*/
|
|
68
|
+
function clampToBudget(lines) {
|
|
69
|
+
const kept = [];
|
|
70
|
+
let size = 0;
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
const next = size + line.length + 1;
|
|
73
|
+
if (kept.length > 0 && next > PATCH_LIMITS.modelDiffMaxChars) {
|
|
74
|
+
kept.push(DIFF_TRUNCATED_NOTE);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
kept.push(line);
|
|
78
|
+
size = next;
|
|
48
79
|
}
|
|
49
|
-
return
|
|
80
|
+
return kept.join('\n');
|
|
50
81
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI-local side channel a `FilePatch` travels on (agent-activity-presentation
|
|
3
|
+
* §3.3.5 / D-8).
|
|
4
|
+
*
|
|
5
|
+
* The patch does NOT ride in the tool result: `ToolResult` has no metadata
|
|
6
|
+
* channel and `packages/core`'s surface is frozen by `public-api.test.ts`, and
|
|
7
|
+
* more importantly a richer display must not cost the model more tokens. So the
|
|
8
|
+
* tool records the patch here, keyed by its own tool-call id, and `reduceEvent`
|
|
9
|
+
* takes it out again when `tool_execution_end` arrives.
|
|
10
|
+
*
|
|
11
|
+
* Three properties, each load-bearing:
|
|
12
|
+
*
|
|
13
|
+
* - **Owner-scoped.** The key is `${owner}:${toolCallId}`. Only `'lead'` is ever
|
|
14
|
+
* written this round (`team/**` is out of scope, so `buildSubagentTools`
|
|
15
|
+
* passes no recorder), but subagents build their tools from the same factory
|
|
16
|
+
* — so the day one gets a recorder, a child's tool-call id must not be able
|
|
17
|
+
* to land on a lead's card. One string concatenation, spent now (D-13).
|
|
18
|
+
* - **Take-once.** `tool_execution_end` fires exactly once per call, and a patch
|
|
19
|
+
* not consumed there never will be. Removal on read is what keeps a long
|
|
20
|
+
* session's store at constant size however many files are edited.
|
|
21
|
+
* - **Bounded FIFO.** At `cap` entries the oldest key is evicted. The cap is a
|
|
22
|
+
* RING, not a leak detector: nothing is retained longer than `cap` writes, so
|
|
23
|
+
* a patch nobody consumes cannot accumulate.
|
|
24
|
+
*/
|
|
25
|
+
import type { FilePatch } from './patch.js';
|
|
26
|
+
export interface FileChangeStore {
|
|
27
|
+
record(owner: string, toolCallId: string, patch: FilePatch): void;
|
|
28
|
+
/** Returns and REMOVES the patch. A patch is consumed exactly once. */
|
|
29
|
+
take(owner: string, toolCallId: string): FilePatch | undefined;
|
|
30
|
+
size(): number;
|
|
31
|
+
}
|
|
32
|
+
/** Entries retained before the oldest is evicted. */
|
|
33
|
+
export declare const FILE_CHANGE_STORE_CAP = 64;
|
|
34
|
+
export declare function createFileChangeStore(cap?: number): FileChangeStore;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI-local side channel a `FilePatch` travels on (agent-activity-presentation
|
|
3
|
+
* §3.3.5 / D-8).
|
|
4
|
+
*
|
|
5
|
+
* The patch does NOT ride in the tool result: `ToolResult` has no metadata
|
|
6
|
+
* channel and `packages/core`'s surface is frozen by `public-api.test.ts`, and
|
|
7
|
+
* more importantly a richer display must not cost the model more tokens. So the
|
|
8
|
+
* tool records the patch here, keyed by its own tool-call id, and `reduceEvent`
|
|
9
|
+
* takes it out again when `tool_execution_end` arrives.
|
|
10
|
+
*
|
|
11
|
+
* Three properties, each load-bearing:
|
|
12
|
+
*
|
|
13
|
+
* - **Owner-scoped.** The key is `${owner}:${toolCallId}`. Only `'lead'` is ever
|
|
14
|
+
* written this round (`team/**` is out of scope, so `buildSubagentTools`
|
|
15
|
+
* passes no recorder), but subagents build their tools from the same factory
|
|
16
|
+
* — so the day one gets a recorder, a child's tool-call id must not be able
|
|
17
|
+
* to land on a lead's card. One string concatenation, spent now (D-13).
|
|
18
|
+
* - **Take-once.** `tool_execution_end` fires exactly once per call, and a patch
|
|
19
|
+
* not consumed there never will be. Removal on read is what keeps a long
|
|
20
|
+
* session's store at constant size however many files are edited.
|
|
21
|
+
* - **Bounded FIFO.** At `cap` entries the oldest key is evicted. The cap is a
|
|
22
|
+
* RING, not a leak detector: nothing is retained longer than `cap` writes, so
|
|
23
|
+
* a patch nobody consumes cannot accumulate.
|
|
24
|
+
*/
|
|
25
|
+
/** Entries retained before the oldest is evicted. */
|
|
26
|
+
export const FILE_CHANGE_STORE_CAP = 64;
|
|
27
|
+
export function createFileChangeStore(cap = FILE_CHANGE_STORE_CAP) {
|
|
28
|
+
// A `Map` iterates in insertion order, so the first key IS the oldest.
|
|
29
|
+
const entries = new Map();
|
|
30
|
+
const limit = Math.max(1, Math.floor(cap));
|
|
31
|
+
const keyOf = (owner, toolCallId) => `${owner}:${toolCallId}`;
|
|
32
|
+
return {
|
|
33
|
+
record(owner, toolCallId, patch) {
|
|
34
|
+
const key = keyOf(owner, toolCallId);
|
|
35
|
+
// Delete first so a re-record moves the key to the END of the ring rather
|
|
36
|
+
// than leaving it at its original age.
|
|
37
|
+
entries.delete(key);
|
|
38
|
+
entries.set(key, patch);
|
|
39
|
+
while (entries.size > limit) {
|
|
40
|
+
const oldest = entries.keys().next();
|
|
41
|
+
if (oldest.done)
|
|
42
|
+
break;
|
|
43
|
+
entries.delete(oldest.value);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
take(owner, toolCallId) {
|
|
47
|
+
const key = keyOf(owner, toolCallId);
|
|
48
|
+
const patch = entries.get(key);
|
|
49
|
+
if (patch !== undefined)
|
|
50
|
+
entries.delete(key);
|
|
51
|
+
return patch;
|
|
52
|
+
},
|
|
53
|
+
size() {
|
|
54
|
+
return entries.size;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/tools/fs-tools.d.ts
CHANGED
|
@@ -7,8 +7,34 @@
|
|
|
7
7
|
* tools do not re-implement them.
|
|
8
8
|
*/
|
|
9
9
|
import { type AgentTool } from '@aragon-agent/core';
|
|
10
|
+
import { type FilePatch } from './patch.js';
|
|
10
11
|
export interface ToolDeps {
|
|
11
12
|
getCwd: () => string;
|
|
13
|
+
/**
|
|
14
|
+
* Hand a structured patch to the UI on the CLI-local side channel
|
|
15
|
+
* (agent-activity-presentation §3.3.5).
|
|
16
|
+
*
|
|
17
|
+
* PRE-BOUND TO ITS OWNER by whoever supplies it, so this module never learns
|
|
18
|
+
* what an owner is. OPTIONAL, and omitting it leaves both tools byte-identical
|
|
19
|
+
* to the pre-feature build — including the extra `write_file` stat/read, which
|
|
20
|
+
* is skipped entirely when there is nobody to record for.
|
|
21
|
+
*/
|
|
22
|
+
recordChange?: (toolCallId: string, patch: FilePatch) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Hand a raw output chunk to the UI on the CLI-local side channel
|
|
25
|
+
* (agent-activity-presentation-live §3.1).
|
|
26
|
+
*
|
|
27
|
+
* PRE-BOUND TO ITS OWNER, exactly like `recordChange` above, so this module
|
|
28
|
+
* never learns what an owner is. OPTIONAL: omitting it leaves `bash`
|
|
29
|
+
* byte-identical to the pre-feature build -- not one extra allocation, not one
|
|
30
|
+
* extra call (AC-31).
|
|
31
|
+
*
|
|
32
|
+
* ONLY `bash` CALLS IT (D-32). Every other builtin is bounded local I/O that
|
|
33
|
+
* settles in milliseconds, so a progress channel for them would be machinery
|
|
34
|
+
* with no observer. The channel is keyed by tool-call id rather than by tool
|
|
35
|
+
* name, so a future long-running tool costs one call site.
|
|
36
|
+
*/
|
|
37
|
+
recordOutput?: (toolCallId: string, chunk: string) => void;
|
|
12
38
|
}
|
|
13
39
|
export declare function makeReadFile(deps: ToolDeps): AgentTool;
|
|
14
40
|
export declare function makeWriteFile(deps: ToolDeps): AgentTool;
|