@aragon-agent/cli 0.6.11 → 0.6.13
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 +10 -1
- package/README.md +97 -20
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +151 -60
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +199 -0
- package/dist/agent/queued-messages.d.ts +20 -0
- package/dist/agent/queued-messages.js +46 -0
- package/dist/agent/reducer.d.ts +21 -2
- package/dist/agent/reducer.js +65 -37
- package/dist/cli.js +15 -1
- package/dist/commands/builtins.js +83 -42
- package/dist/commands/queue-command.d.ts +3 -0
- package/dist/commands/queue-command.js +5 -0
- package/dist/commands/registry.d.ts +3 -0
- package/dist/compaction/child.d.ts +4 -2
- package/dist/compaction/child.js +4 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +13 -11
- package/dist/compaction/context-command.js +5 -0
- package/dist/compaction/wiring.d.ts +4 -3
- package/dist/compaction/wiring.js +6 -1
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +2 -1
- package/dist/config/load.js +15 -8
- package/dist/config/model-profile-resolution.d.ts +20 -0
- package/dist/config/model-profile-resolution.js +81 -0
- package/dist/config/model-profile-store.d.ts +67 -0
- package/dist/config/model-profile-store.js +153 -0
- package/dist/config/model-profiles.d.ts +44 -0
- package/dist/config/model-profiles.js +121 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/store.d.ts +11 -1
- package/dist/config/store.js +43 -27
- package/dist/fast/resolve.d.ts +2 -1
- package/dist/fast/resolve.js +6 -4
- package/dist/fast/wiring.d.ts +5 -3
- package/dist/fast/wiring.js +8 -4
- package/dist/input/enter-sequences.d.ts +11 -10
- package/dist/input/enter-sequences.js +29 -27
- package/dist/input/stdin-filter.js +93 -28
- package/dist/logging/install.d.ts +3 -0
- package/dist/logging/install.js +4 -2
- package/dist/logging/secret-registry.d.ts +2 -0
- package/dist/logging/secret-registry.js +18 -0
- package/dist/session/persist.d.ts +6 -7
- package/dist/session/persist.js +6 -9
- package/dist/session/store.d.ts +4 -1
- package/dist/session/validate-session.d.ts +3 -0
- package/dist/session/validate-session.js +207 -0
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/subagent.d.ts +3 -1
- package/dist/team/subagent.js +3 -1
- package/dist/ui/ActivityLine.d.ts +6 -70
- package/dist/ui/ActivityLine.js +12 -34
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +161 -226
- package/dist/ui/BottomStatusRow.d.ts +6 -86
- package/dist/ui/BottomStatusRow.js +11 -26
- package/dist/ui/Composer.d.ts +18 -84
- package/dist/ui/Composer.js +9 -100
- package/dist/ui/PromptInput.d.ts +6 -2
- package/dist/ui/PromptInput.js +91 -105
- package/dist/ui/QueuePanel.d.ts +8 -0
- package/dist/ui/QueuePanel.js +9 -0
- package/dist/ui/QueueStatusRow.d.ts +24 -0
- package/dist/ui/QueueStatusRow.js +51 -0
- package/dist/ui/StatusBar.d.ts +15 -149
- package/dist/ui/StatusBar.js +27 -112
- package/dist/ui/clipboard-task.d.ts +29 -0
- package/dist/ui/clipboard-task.js +66 -0
- package/dist/ui/clipboard.d.ts +29 -50
- package/dist/ui/clipboard.js +141 -75
- package/dist/ui/composer-input.d.ts +28 -0
- package/dist/ui/composer-input.js +46 -0
- package/dist/ui/composer-limits.d.ts +3 -4
- package/dist/ui/composer-limits.js +4 -9
- package/dist/ui/composer-rows.d.ts +1 -1
- package/dist/ui/composer-rows.js +24 -137
- package/dist/ui/editor-navigation.d.ts +45 -0
- package/dist/ui/editor-navigation.js +85 -0
- package/dist/ui/editor-reducer.d.ts +9 -0
- package/dist/ui/editor-reducer.js +31 -15
- package/dist/ui/enter-frames.d.ts +15 -31
- package/dist/ui/enter-frames.js +14 -33
- package/dist/ui/interaction-copy.d.ts +77 -0
- package/dist/ui/interaction-copy.js +40 -0
- package/dist/ui/interaction-hints.d.ts +18 -0
- package/dist/ui/interaction-hints.js +92 -0
- package/dist/ui/layout/budget.js +1 -1
- package/dist/ui/layout/queue-layout.d.ts +19 -0
- package/dist/ui/layout/queue-layout.js +26 -0
- package/dist/ui/layout/status-layout.d.ts +55 -0
- package/dist/ui/layout/status-layout.js +109 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +11 -3
- package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
- package/dist/ui/overlays/ModelProfileEditor.js +103 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
- package/dist/ui/overlays/ModelProfilePicker.js +167 -0
- package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
- package/dist/ui/overlays/QueueOverlay.js +79 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
- package/dist/ui/overlays/SettingsScreen.js +347 -3
- package/dist/ui/overlays/model-profile-state.d.ts +74 -0
- package/dist/ui/overlays/model-profile-state.js +119 -0
- package/dist/ui/run-status-row.d.ts +7 -19
- package/dist/ui/run-status-row.js +2 -18
- package/dist/ui/selection/selection-controller.d.ts +2 -2
- package/dist/ui/selection/selection-controller.js +58 -20
- package/dist/ui/transcript-text.js +20 -12
- package/dist/ui/use-wheel-routing.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { loadConfig } from '../config/load.js';
|
|
2
|
+
import { DEFAULT_CONFIG } from '../config/schema.js';
|
|
3
|
+
import { mergePersistedConfig } from '../config/store.js';
|
|
4
|
+
import { adaptLegacyConnectionPatch, assertLegacyFastTarget, commitModelSettings, readModelSettingsDisk, } from '../config/model-profile-store.js';
|
|
5
|
+
import { projectModelProfiles, resolveModelProfileState } from '../config/model-profile-resolution.js';
|
|
6
|
+
import { validateModelProfiles } from '../config/model-profiles.js';
|
|
7
|
+
import { registerProfileSecrets } from '../logging/secret-registry.js';
|
|
8
|
+
/** Capture both baselines when the editor opens, without migrating or writing anything. */
|
|
9
|
+
export function createModelSettingsDraft(live, liveRevision) {
|
|
10
|
+
const disk = readModelSettingsDisk();
|
|
11
|
+
return {
|
|
12
|
+
diskRevision: disk.revision, liveRevision,
|
|
13
|
+
baseline: structuredClone(disk.config ?? DEFAULT_CONFIG),
|
|
14
|
+
liveBaseline: structuredClone(live),
|
|
15
|
+
profiles: disk.config?.modelProfiles
|
|
16
|
+
? structuredClone(disk.config.modelProfiles) : undefined,
|
|
17
|
+
activateRoles: [], patch: {},
|
|
18
|
+
...(!disk.ok ? { readError: 'Cannot read config. Repair it with aragon config edit.' } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Map stable failure codes to safe UI text; never include parser or SDK error messages. */
|
|
22
|
+
export function modelSettingsResultMessage(result) {
|
|
23
|
+
if (result.status === 'saved_apply_failed') {
|
|
24
|
+
return result.persisted
|
|
25
|
+
? 'Saved, but this session could not apply all settings. Restart required.'
|
|
26
|
+
: 'Could not apply the reloaded settings completely. Restart required.';
|
|
27
|
+
}
|
|
28
|
+
if (result.status === 'restart_required') {
|
|
29
|
+
return 'Saved. Fast tier requires a restart; check its model and credentials.';
|
|
30
|
+
}
|
|
31
|
+
const messages = {
|
|
32
|
+
busy: 'Wait for the run, team, review, or compaction to finish before saving.',
|
|
33
|
+
conflict: 'Settings changed elsewhere. Reload before saving.',
|
|
34
|
+
read_failed: 'Cannot read config. Repair it with aragon config edit.',
|
|
35
|
+
write_failed: 'Could not write config. Your draft is preserved.',
|
|
36
|
+
invalid: 'Invalid model settings. Check the highlighted field or aragon config edit.',
|
|
37
|
+
profile_key_required: 'This profile has a dedicated key. Edit or copy it in /settings.',
|
|
38
|
+
legacy_conversion_unsupported: 'This endpoint needs a profile. Edit it in /settings.',
|
|
39
|
+
};
|
|
40
|
+
return result.ok ? 'Settings saved.' : messages[result.code ?? 'invalid'];
|
|
41
|
+
}
|
|
42
|
+
function rejected(code) {
|
|
43
|
+
return { ok: false, persisted: false, status: 'rejected', code };
|
|
44
|
+
}
|
|
45
|
+
function connectionChanged(draft, role) {
|
|
46
|
+
if (draft.profiles === undefined)
|
|
47
|
+
return false;
|
|
48
|
+
const before = draft.baseline.modelProfiles;
|
|
49
|
+
const after = draft.profiles;
|
|
50
|
+
if ((before?.[`${role}Id`] ?? null) !== (after?.[`${role}Id`] ?? null))
|
|
51
|
+
return true;
|
|
52
|
+
const id = after?.[`${role}Id`];
|
|
53
|
+
if (!id)
|
|
54
|
+
return false;
|
|
55
|
+
const a = before?.entries.find((entry) => entry.id === id);
|
|
56
|
+
const b = after?.entries.find((entry) => entry.id === id);
|
|
57
|
+
return ['provider', 'model', 'baseUrl', 'apiKey']
|
|
58
|
+
.some((key) => a?.[key] !== b?.[key]);
|
|
59
|
+
}
|
|
60
|
+
function hasConnectionPatch(patch, role) {
|
|
61
|
+
const section = role === 'main' ? patch : patch.fast;
|
|
62
|
+
return !!section && ['provider', 'model', 'baseUrl'].some((key) => key in section);
|
|
63
|
+
}
|
|
64
|
+
function liveCandidate(live, candidate, draft) {
|
|
65
|
+
const patch = draft.patch;
|
|
66
|
+
const normalizedPatch = {};
|
|
67
|
+
for (const key of Object.keys(patch)) {
|
|
68
|
+
normalizedPatch[key] = candidate[key];
|
|
69
|
+
}
|
|
70
|
+
const next = { ...live, ...normalizedPatch };
|
|
71
|
+
for (const section of ['apiKeys', 'fast', 'compaction', 'log']) {
|
|
72
|
+
const normalized = { ...live[section] };
|
|
73
|
+
for (const key of Object.keys(patch[section] ?? {})) {
|
|
74
|
+
normalized[key] = candidate[section][key];
|
|
75
|
+
}
|
|
76
|
+
Object.assign(next, { [section]: normalized });
|
|
77
|
+
}
|
|
78
|
+
if (patch.apiKeys && Object.hasOwn(patch.apiKeys, live.provider)) {
|
|
79
|
+
next.apiKeyOverride = undefined;
|
|
80
|
+
next.apiKeyOverrideTarget = undefined;
|
|
81
|
+
}
|
|
82
|
+
// These fields have intentionally different disk and runtime representations.
|
|
83
|
+
if ('maxTokens' in patch)
|
|
84
|
+
next.maxTokens = candidate.maxTokens ?? undefined;
|
|
85
|
+
if ('baseUrl' in patch)
|
|
86
|
+
next.baseUrl = candidate.baseUrl ?? undefined;
|
|
87
|
+
const projected = projectModelProfiles(candidate).file;
|
|
88
|
+
if (draft.profiles !== undefined || candidate.modelProfiles !== undefined) {
|
|
89
|
+
next.modelProfiles = structuredClone(projected.modelProfiles);
|
|
90
|
+
}
|
|
91
|
+
next.liveToolOutput = live.liveToolOutput;
|
|
92
|
+
for (const role of ['main', 'fast']) {
|
|
93
|
+
if (!draft.activateRoles.includes(role) && !connectionChanged(draft, role)
|
|
94
|
+
&& !hasConnectionPatch(patch, role))
|
|
95
|
+
continue;
|
|
96
|
+
if (role === 'main') {
|
|
97
|
+
next.provider = projected.provider;
|
|
98
|
+
next.model = projected.model;
|
|
99
|
+
next.baseUrl = projected.baseUrl ?? undefined;
|
|
100
|
+
next.apiKeyOverride = undefined;
|
|
101
|
+
next.apiKeyOverrideTarget = undefined;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
next.fast = { ...next.fast, provider: projected.fast.provider,
|
|
105
|
+
model: projected.fast.model, baseUrl: projected.fast.baseUrl };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (next.apiKeyOverride && !next.apiKeyOverrideTarget
|
|
109
|
+
&& (next.modelProfiles?.mainId || next.modelProfiles?.fastId)) {
|
|
110
|
+
next.apiKeyOverrideTarget = { provider: live.provider, baseUrl: live.baseUrl ?? null };
|
|
111
|
+
}
|
|
112
|
+
next.modelProfileState = resolveModelProfileState(next);
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
/** Validate and prepare everything before the one synchronous, atomic disk commit. */
|
|
116
|
+
export function saveModelSettings(input) {
|
|
117
|
+
const { draft, controller } = input;
|
|
118
|
+
if (controller.isModelSettingsBusy())
|
|
119
|
+
return rejected('busy');
|
|
120
|
+
if (draft.liveRevision !== controller.getSettingsRevision())
|
|
121
|
+
return rejected('conflict');
|
|
122
|
+
const disk = readModelSettingsDisk();
|
|
123
|
+
if (!disk.ok || draft.readError)
|
|
124
|
+
return rejected('read_failed');
|
|
125
|
+
if (disk.revision !== draft.diskRevision)
|
|
126
|
+
return rejected('conflict');
|
|
127
|
+
const issues = validateModelProfiles(draft.profiles ?? disk.config?.modelProfiles);
|
|
128
|
+
if (issues.length)
|
|
129
|
+
return { ...rejected('invalid'), issues };
|
|
130
|
+
let snapshot;
|
|
131
|
+
let candidate;
|
|
132
|
+
try {
|
|
133
|
+
const current = disk.config ?? structuredClone(DEFAULT_CONFIG);
|
|
134
|
+
const profilesChanged = draft.activateRoles.length > 0
|
|
135
|
+
|| JSON.stringify(draft.profiles) !== JSON.stringify(draft.baseline.modelProfiles);
|
|
136
|
+
const implicitLegacy = draft.profiles === undefined || !profilesChanged;
|
|
137
|
+
const patch = adaptLegacyConnectionPatch({ current, patch: implicitLegacy
|
|
138
|
+
? draft.patch : { ...draft.patch, modelProfiles: draft.profiles } });
|
|
139
|
+
candidate = mergePersistedConfig(current, patch);
|
|
140
|
+
registerProfileSecrets(candidate);
|
|
141
|
+
const next = liveCandidate(controller.getConfig(), candidate, draft);
|
|
142
|
+
// Disk preferences cannot reveal startup/session gateways retained by the live main role.
|
|
143
|
+
if (implicitLegacy && current.modelProfiles?.fastId && hasConnectionPatch(draft.patch, 'fast')) {
|
|
144
|
+
assertLegacyFastTarget(next.fast, next);
|
|
145
|
+
}
|
|
146
|
+
snapshot = controller.prepareModelSettingsSnapshot(next);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const code = error.code ?? 'invalid';
|
|
150
|
+
return rejected(code);
|
|
151
|
+
}
|
|
152
|
+
const commit = commitModelSettings({ candidate, diskRevision: draft.diskRevision });
|
|
153
|
+
if (!commit.ok)
|
|
154
|
+
return rejected(commit.code ?? 'write_failed');
|
|
155
|
+
return applyPreparedSnapshot(controller, snapshot, true);
|
|
156
|
+
}
|
|
157
|
+
function applyPreparedSnapshot(controller, snapshot, persisted) {
|
|
158
|
+
try {
|
|
159
|
+
controller.applyModelSettingsSnapshot(snapshot);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
controller.blockModelSettingsRequests();
|
|
163
|
+
return { ok: false, persisted, status: 'saved_apply_failed', code: 'apply_failed', restartRequired: true };
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
controller.applyModelSettingsEffects();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { ok: false, persisted, status: 'saved_apply_failed', code: 'apply_failed', restartRequired: true };
|
|
170
|
+
}
|
|
171
|
+
const restartRequired = snapshot.config.fast.enabled && !controller.isFastRegistered();
|
|
172
|
+
return { ok: true, persisted, status: restartRequired ? 'restart_required' : 'applied',
|
|
173
|
+
restartRequired };
|
|
174
|
+
}
|
|
175
|
+
/** Reload the complete model snapshot; invalid disk content never replaces a live session. */
|
|
176
|
+
export function reloadModelSettings(controller) {
|
|
177
|
+
if (controller.isModelSettingsBusy())
|
|
178
|
+
return rejected('busy');
|
|
179
|
+
const disk = readModelSettingsDisk();
|
|
180
|
+
if (!disk.ok)
|
|
181
|
+
return rejected('read_failed');
|
|
182
|
+
if (validateModelProfiles(disk.config?.modelProfiles).length)
|
|
183
|
+
return rejected('invalid');
|
|
184
|
+
try {
|
|
185
|
+
const resolved = loadConfig({ cwd: controller.getConfig().cwd });
|
|
186
|
+
resolved.liveToolOutput = controller.getConfig().liveToolOutput;
|
|
187
|
+
const after = readModelSettingsDisk();
|
|
188
|
+
if (!after.ok)
|
|
189
|
+
return rejected('read_failed');
|
|
190
|
+
if (after.revision !== disk.revision)
|
|
191
|
+
return rejected('conflict');
|
|
192
|
+
if (resolved.modelProfileState?.main.invalid)
|
|
193
|
+
return rejected('invalid');
|
|
194
|
+
return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(resolved), false);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return rejected('invalid');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Entry } from './reducer.js';
|
|
2
|
+
/** Full user text retained until Core acknowledges its opaque ID. */
|
|
3
|
+
export interface PendingSteering {
|
|
4
|
+
readonly queueId: string;
|
|
5
|
+
readonly text: string;
|
|
6
|
+
}
|
|
7
|
+
/** Hide only active pending entries, using the same projection for measurement and paint. */
|
|
8
|
+
export declare function selectTranscriptEntries(entries: Entry[], pending: readonly PendingSteering[]): Entry[];
|
|
9
|
+
/** Restore trimmed messages in FIFO order before atomically accepting known receipts. */
|
|
10
|
+
export declare function reconcileSteeringReceipt(options: {
|
|
11
|
+
entries: Entry[];
|
|
12
|
+
pending: readonly PendingSteering[];
|
|
13
|
+
ids: readonly string[];
|
|
14
|
+
}): {
|
|
15
|
+
entries: Entry[];
|
|
16
|
+
pending: PendingSteering[];
|
|
17
|
+
changed: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** Supplement trimmed/cleared history for save and exit without deduplicating text. */
|
|
20
|
+
export declare function mergePendingEntries(entries: Entry[], pending: readonly PendingSteering[]): Entry[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Hide only active pending entries, using the same projection for measurement and paint. */
|
|
2
|
+
export function selectTranscriptEntries(entries, pending) {
|
|
3
|
+
if (pending.length === 0)
|
|
4
|
+
return entries;
|
|
5
|
+
const ids = new Set(pending.map((item) => item.queueId));
|
|
6
|
+
return entries.filter((entry) => entry.kind !== 'queued' || !ids.has(entry.queueId ?? ''));
|
|
7
|
+
}
|
|
8
|
+
/** Restore trimmed messages in FIFO order before atomically accepting known receipts. */
|
|
9
|
+
export function reconcileSteeringReceipt(options) {
|
|
10
|
+
const receipts = new Set(options.ids);
|
|
11
|
+
const ids = new Set(options.pending.filter((item) => receipts.has(item.queueId))
|
|
12
|
+
.map((item) => item.queueId));
|
|
13
|
+
if (!ids.size)
|
|
14
|
+
return { entries: options.entries, pending: [...options.pending], changed: false };
|
|
15
|
+
const entries = mergePendingEntries(options.entries, options.pending).map((entry) => entry.kind === 'queued' && ids.has(entry.queueId ?? '')
|
|
16
|
+
? { id: entry.id, kind: 'user', text: entry.text } : entry);
|
|
17
|
+
return { entries, pending: options.pending.filter((item) => !ids.has(item.queueId)), changed: true };
|
|
18
|
+
}
|
|
19
|
+
/** Supplement trimmed/cleared history for save and exit without deduplicating text. */
|
|
20
|
+
export function mergePendingEntries(entries, pending) {
|
|
21
|
+
if (pending.length === 0)
|
|
22
|
+
return entries;
|
|
23
|
+
const positions = new Map(pending.map((item, index) => [item.queueId, index]));
|
|
24
|
+
const visible = new Map(entries.flatMap((entry) => entry.kind === 'queued' && entry.queueId !== undefined
|
|
25
|
+
? [[entry.queueId, entry]] : []));
|
|
26
|
+
const merged = [];
|
|
27
|
+
let cursor = 0;
|
|
28
|
+
const appendThrough = (last) => {
|
|
29
|
+
while (cursor <= last) {
|
|
30
|
+
const item = pending[cursor++];
|
|
31
|
+
const existing = visible.get(item.queueId);
|
|
32
|
+
merged.push(existing ? { ...existing, text: item.text } :
|
|
33
|
+
{ id: `pending:${item.queueId}`, kind: 'queued', ...item });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const position = entry.kind === 'queued' && entry.queueId !== undefined
|
|
38
|
+
? positions.get(entry.queueId) : undefined;
|
|
39
|
+
if (position === undefined)
|
|
40
|
+
merged.push(entry);
|
|
41
|
+
else
|
|
42
|
+
appendThrough(position);
|
|
43
|
+
}
|
|
44
|
+
appendThrough(pending.length - 1);
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - `agent_end` after a turn that produced no `turn_end` and no error notice
|
|
11
11
|
* synthesizes a generic error notice (a swallowed throw is never a no-op).
|
|
12
12
|
*/
|
|
13
|
+
import type { PendingSteering } from './queued-messages.js';
|
|
13
14
|
import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agent/core';
|
|
14
15
|
import type { AgentMode } from './agent-mode.js';
|
|
15
16
|
import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
|
|
@@ -21,7 +22,7 @@ import type { RetryPhase } from './retry-view.js';
|
|
|
21
22
|
import type { FilePatch } from '../tools/patch.js';
|
|
22
23
|
export type NoticeLevel = 'info' | 'warn' | 'error';
|
|
23
24
|
export type ToastLevel = NoticeLevel | 'success';
|
|
24
|
-
export type Overlay = null | 'settings' | 'model' | 'help' | 'confirm' | 'question' | 'plan';
|
|
25
|
+
export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan';
|
|
25
26
|
export type ToolStatus = 'pending' | 'running' | 'done' | 'error';
|
|
26
27
|
/** Ephemeral, auto-dismissed acknowledgement (spec §3.9). */
|
|
27
28
|
export interface Toast {
|
|
@@ -40,7 +41,7 @@ export type Entry = {
|
|
|
40
41
|
/**
|
|
41
42
|
* A message submitted while a run was in flight (tui-shift-enter-copy-queue
|
|
42
43
|
* 5.2). It renders as `Queue: <first line>` and is rewritten IN PLACE, same
|
|
43
|
-
* id, into the `user` entry above by the
|
|
44
|
+
* id, into the `user` entry above by the matching receipt that proves Core
|
|
44
45
|
* accepted it -- so the id-based height cache re-measures rather than
|
|
45
46
|
* freezes, and the settled boundary (positional, monotonic) never moves.
|
|
46
47
|
*
|
|
@@ -51,6 +52,7 @@ export type Entry = {
|
|
|
51
52
|
id: string;
|
|
52
53
|
kind: 'queued';
|
|
53
54
|
text: string;
|
|
55
|
+
queueId?: string;
|
|
54
56
|
} | {
|
|
55
57
|
id: string;
|
|
56
58
|
kind: 'assistant';
|
|
@@ -275,6 +277,14 @@ export interface RetrySnapshot {
|
|
|
275
277
|
}
|
|
276
278
|
export interface ViewState {
|
|
277
279
|
entries: Entry[];
|
|
280
|
+
pendingSteering: PendingSteering[];
|
|
281
|
+
seenSteeringIds: ReadonlySet<string>;
|
|
282
|
+
runPhase: 'idle' | 'starting' | 'waiting' | 'thinking' | 'generating' | 'preparing-tool' | 'tool';
|
|
283
|
+
activeTool?: {
|
|
284
|
+
toolCallId: string;
|
|
285
|
+
name: string;
|
|
286
|
+
};
|
|
287
|
+
runOutcome: 'none' | 'ended' | 'interrupted' | 'failed';
|
|
278
288
|
status: 'idle' | 'running';
|
|
279
289
|
usageTotal: UsageTotal;
|
|
280
290
|
/**
|
|
@@ -402,9 +412,17 @@ export type ViewAction = {
|
|
|
402
412
|
*/
|
|
403
413
|
| {
|
|
404
414
|
type: 'steerQueued';
|
|
415
|
+
queueId: string;
|
|
405
416
|
text: string;
|
|
417
|
+
} | {
|
|
418
|
+
type: 'steeringAccepted';
|
|
419
|
+
ids: readonly string[];
|
|
406
420
|
} | {
|
|
407
421
|
type: 'runStart';
|
|
422
|
+
} | {
|
|
423
|
+
type: 'runStarting';
|
|
424
|
+
} | {
|
|
425
|
+
type: 'runError';
|
|
408
426
|
} | {
|
|
409
427
|
type: 'turnStart';
|
|
410
428
|
} | {
|
|
@@ -431,6 +449,7 @@ export type ViewAction = {
|
|
|
431
449
|
} | {
|
|
432
450
|
type: 'toolExecStart';
|
|
433
451
|
toolCallId: string;
|
|
452
|
+
toolName?: string;
|
|
434
453
|
} | {
|
|
435
454
|
type: 'toolExecEnd';
|
|
436
455
|
toolCallId: string;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* synthesizes a generic error notice (a swallowed throw is never a no-op).
|
|
12
12
|
*/
|
|
13
13
|
import process from 'node:process';
|
|
14
|
+
import { reconcileSteeringReceipt } from './queued-messages.js';
|
|
14
15
|
import { emptyContextUsage } from '../compaction/meter.js';
|
|
15
16
|
import { addUsage, computeCost } from './usage.js';
|
|
16
17
|
import { ENTRY_LIMITS, appendBounded, entryRetain, trimEntries } from './entry-limits.js';
|
|
@@ -46,6 +47,10 @@ const EMPTY_USAGE_TOTAL = {
|
|
|
46
47
|
export function initialViewState(seed = {}) {
|
|
47
48
|
return {
|
|
48
49
|
entries: [],
|
|
50
|
+
pendingSteering: [],
|
|
51
|
+
seenSteeringIds: new Set(),
|
|
52
|
+
runPhase: 'idle',
|
|
53
|
+
runOutcome: 'none',
|
|
49
54
|
status: 'idle',
|
|
50
55
|
usageTotal: EMPTY_USAGE_TOTAL,
|
|
51
56
|
context: emptyContextUsage(),
|
|
@@ -127,6 +132,8 @@ function firstLine(text) {
|
|
|
127
132
|
*/
|
|
128
133
|
export function reduceEvent(event, cost, patches) {
|
|
129
134
|
switch (event.type) {
|
|
135
|
+
case 'steering_accepted':
|
|
136
|
+
return [{ type: 'steeringAccepted', ids: event.ids }];
|
|
130
137
|
case 'agent_start':
|
|
131
138
|
return [{ type: 'runStart' }];
|
|
132
139
|
case 'turn_start':
|
|
@@ -142,7 +149,7 @@ export function reduceEvent(event, cost, patches) {
|
|
|
142
149
|
case 'agent_end':
|
|
143
150
|
return [{ type: 'runEnd' }];
|
|
144
151
|
case 'tool_execution_start':
|
|
145
|
-
return [{ type: 'toolExecStart', toolCallId: event.toolCallId }];
|
|
152
|
+
return [{ type: 'toolExecStart', toolCallId: event.toolCallId, toolName: event.toolName }];
|
|
146
153
|
case 'tool_execution_end': {
|
|
147
154
|
const patch = patches?.take(event.toolCallId);
|
|
148
155
|
const preview = buildToolPreview(event.result);
|
|
@@ -197,8 +204,6 @@ function reduceStreamEvent(se) {
|
|
|
197
204
|
case 'text_delta':
|
|
198
205
|
return [{ type: 'textDelta', delta: se.delta }];
|
|
199
206
|
case 'tool_call_start':
|
|
200
|
-
if (SELF_RENDERING_TOOLS.has(se.toolName))
|
|
201
|
-
return [];
|
|
202
207
|
return [
|
|
203
208
|
{ type: 'toolCallStart', toolCallId: se.toolCallId, toolName: se.toolName },
|
|
204
209
|
];
|
|
@@ -210,6 +215,7 @@ function reduceStreamEvent(se) {
|
|
|
210
215
|
return [{ type: 'toolCallEnd', toolCallId: se.toolCallId, args: se.args }];
|
|
211
216
|
case 'error':
|
|
212
217
|
return [
|
|
218
|
+
{ type: 'runError' },
|
|
213
219
|
{
|
|
214
220
|
type: 'notice',
|
|
215
221
|
level: 'error',
|
|
@@ -289,29 +295,6 @@ function nextId(state) {
|
|
|
289
295
|
function mapEntry(entries, id, fn) {
|
|
290
296
|
return entries.map((e) => (e.id === id ? fn(e) : e));
|
|
291
297
|
}
|
|
292
|
-
/**
|
|
293
|
-
* Rewrite every `queued` entry in place as the user message it became
|
|
294
|
-
* (tui-shift-enter-copy-queue 5.2.3).
|
|
295
|
-
*
|
|
296
|
-
* SAME ID, NEW OBJECT -- the `mapEntry` discipline for a non-append change:
|
|
297
|
-
* the position (and therefore the settled boundary, which advances by
|
|
298
|
-
* POSITION and never by identity) is untouched, while the new object
|
|
299
|
-
* reference is what lets `React.memo` and the height cache see that this
|
|
300
|
-
* one row changed. ALL of them at once, because `drainSteering` is a full
|
|
301
|
-
* drain: converting only some would show a partial acceptance that never
|
|
302
|
-
* happened. Identity-preserving when nothing is queued -- `turnStart` fires
|
|
303
|
-
* every turn, and this must stay free for the common case.
|
|
304
|
-
*/
|
|
305
|
-
function convertQueuedEntries(entries) {
|
|
306
|
-
let touched = false;
|
|
307
|
-
const next = entries.map((e) => {
|
|
308
|
-
if (e.kind !== 'queued')
|
|
309
|
-
return e;
|
|
310
|
-
touched = true;
|
|
311
|
-
return { id: e.id, kind: 'user', text: e.text };
|
|
312
|
-
});
|
|
313
|
-
return touched ? next : entries;
|
|
314
|
-
}
|
|
315
298
|
/**
|
|
316
299
|
* Seal `thinkingMs`, once, as a spreadable patch (§3.1.2).
|
|
317
300
|
*
|
|
@@ -425,7 +408,40 @@ function findToolEntryId(state, toolCallId) {
|
|
|
425
408
|
}
|
|
426
409
|
return undefined;
|
|
427
410
|
}
|
|
411
|
+
/** Runtime facts outlive transcript cards and are independent of command/clipboard notices. */
|
|
412
|
+
function reduceRunFacts(state, action) {
|
|
413
|
+
switch (action.type) {
|
|
414
|
+
case 'runStarting': return { ...state, runPhase: 'starting', runOutcome: 'none' };
|
|
415
|
+
case 'runStart': return { ...state, runPhase: 'waiting', runOutcome: 'none', activeTool: undefined };
|
|
416
|
+
case 'turnStart': return { ...state, runPhase: 'waiting' };
|
|
417
|
+
case 'thinkingStart':
|
|
418
|
+
case 'thinkingDelta': return { ...state, runPhase: 'thinking' };
|
|
419
|
+
case 'textDelta': return { ...state, runPhase: 'generating' };
|
|
420
|
+
case 'toolCallStart':
|
|
421
|
+
case 'toolCallDelta':
|
|
422
|
+
case 'toolCallEnd': return { ...state, runPhase: 'preparing-tool' };
|
|
423
|
+
case 'toolExecStart': {
|
|
424
|
+
const card = state.entries.find((entry) => entry.kind === 'tool' && entry.toolCallId === action.toolCallId);
|
|
425
|
+
return { ...state, runPhase: 'tool', activeTool: {
|
|
426
|
+
toolCallId: action.toolCallId,
|
|
427
|
+
name: action.toolName ?? (card?.kind === 'tool' ? card.name : 'tool'),
|
|
428
|
+
} };
|
|
429
|
+
}
|
|
430
|
+
case 'toolExecEnd': return state.activeTool?.toolCallId === action.toolCallId
|
|
431
|
+
? { ...state, runPhase: 'waiting', activeTool: undefined } : state;
|
|
432
|
+
case 'runError': return { ...state, runOutcome: 'failed' };
|
|
433
|
+
case 'abortMark': return { ...state, runOutcome: 'interrupted' };
|
|
434
|
+
case 'runEnd': return { ...state, runPhase: 'idle', activeTool: undefined,
|
|
435
|
+
runOutcome: state.runOutcome !== 'none' ? state.runOutcome
|
|
436
|
+
: !state.turnProduced && !state.errorNoticed && !state.aborted ? 'failed' : 'ended' };
|
|
437
|
+
case 'resetConversation':
|
|
438
|
+
case 'restoreEntries': return { ...state, runPhase: 'idle', activeTool: undefined,
|
|
439
|
+
runOutcome: 'none', seenSteeringIds: new Set() };
|
|
440
|
+
default: return state;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
428
443
|
export function viewReducer(state, action) {
|
|
444
|
+
state = reduceRunFacts(state, action);
|
|
429
445
|
switch (action.type) {
|
|
430
446
|
case 'submit': {
|
|
431
447
|
const { id, seq } = nextId(state);
|
|
@@ -449,13 +465,29 @@ export function viewReducer(state, action) {
|
|
|
449
465
|
};
|
|
450
466
|
}
|
|
451
467
|
case 'steerQueued': {
|
|
468
|
+
if (state.seenSteeringIds.has(action.queueId))
|
|
469
|
+
return state;
|
|
452
470
|
const { id, seq } = nextId(state);
|
|
453
|
-
const entry = { id, kind: 'queued', text: action.text };
|
|
471
|
+
const entry = { id, kind: 'queued', queueId: action.queueId, text: action.text };
|
|
454
472
|
// NOTHING ELSE RESETS. This is not a new turn -- `turnProduced`,
|
|
455
473
|
// `errorNoticed` and `aborted` describe the run that is still in
|
|
456
474
|
// flight, and clearing them here would let the next `runEnd` failure
|
|
457
475
|
// guard misfire on a turn that produced plenty.
|
|
458
|
-
return {
|
|
476
|
+
return {
|
|
477
|
+
...state, seq, ...appendEntry(state, state.entries, entry),
|
|
478
|
+
seenSteeringIds: new Set([...state.seenSteeringIds, action.queueId]),
|
|
479
|
+
pendingSteering: [...state.pendingSteering, { queueId: action.queueId, text: action.text }],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
case 'steeringAccepted': {
|
|
483
|
+
const receipt = reconcileSteeringReceipt({
|
|
484
|
+
entries: state.entries, pending: state.pendingSteering, ids: action.ids,
|
|
485
|
+
});
|
|
486
|
+
if (!receipt.changed)
|
|
487
|
+
return state;
|
|
488
|
+
const trimmed = trimEntries(receipt.entries, entryRetain());
|
|
489
|
+
return { ...state, pendingSteering: receipt.pending, entries: trimmed.entries,
|
|
490
|
+
droppedEntries: state.droppedEntries + trimmed.dropped };
|
|
459
491
|
}
|
|
460
492
|
case 'runStart':
|
|
461
493
|
return {
|
|
@@ -467,14 +499,6 @@ export function viewReducer(state, action) {
|
|
|
467
499
|
};
|
|
468
500
|
case 'turnStart': {
|
|
469
501
|
const { id, seq } = nextId(state);
|
|
470
|
-
// CONVERT, THEN APPEND, IN THAT ORDER AND IN THIS ONE ACTION
|
|
471
|
-
// (5.2.3): `turn_start` is the authoritative "the steering queue was
|
|
472
|
-
// drained into the conversation" signal (the loop drains at its top,
|
|
473
|
-
// immediately before emitting it), so the user message and the answer
|
|
474
|
-
// that follows it must land in the same commit, in render order, or a
|
|
475
|
-
// frame can exist where the message has vanished and the answer has
|
|
476
|
-
// not appeared.
|
|
477
|
-
const entries = convertQueuedEntries(state.entries);
|
|
478
502
|
const entry = {
|
|
479
503
|
id,
|
|
480
504
|
kind: 'assistant',
|
|
@@ -483,7 +507,7 @@ export function viewReducer(state, action) {
|
|
|
483
507
|
thinkingOpen: false,
|
|
484
508
|
streaming: true,
|
|
485
509
|
};
|
|
486
|
-
return { ...state, seq, ...appendEntry(state, entries, entry), streamingId: id };
|
|
510
|
+
return { ...state, seq, ...appendEntry(state, state.entries, entry), streamingId: id };
|
|
487
511
|
}
|
|
488
512
|
case 'thinkingStart': {
|
|
489
513
|
if (!state.streamingId)
|
|
@@ -530,6 +554,8 @@ export function viewReducer(state, action) {
|
|
|
530
554
|
};
|
|
531
555
|
}
|
|
532
556
|
case 'toolCallStart': {
|
|
557
|
+
if (SELF_RENDERING_TOOLS.has(action.toolName))
|
|
558
|
+
return state;
|
|
533
559
|
const { id, seq } = nextId(state);
|
|
534
560
|
const entry = {
|
|
535
561
|
id,
|
|
@@ -777,6 +803,7 @@ export function viewReducer(state, action) {
|
|
|
777
803
|
case 'resetConversation':
|
|
778
804
|
return {
|
|
779
805
|
...state,
|
|
806
|
+
pendingSteering: [],
|
|
780
807
|
entries: [],
|
|
781
808
|
droppedEntries: 0,
|
|
782
809
|
streamingId: undefined,
|
|
@@ -1295,6 +1322,7 @@ export function viewReducer(state, action) {
|
|
|
1295
1322
|
return {
|
|
1296
1323
|
...state,
|
|
1297
1324
|
entries: restored.entries,
|
|
1325
|
+
pendingSteering: [],
|
|
1298
1326
|
droppedEntries: restored.dropped,
|
|
1299
1327
|
seq: maxSeq,
|
|
1300
1328
|
teamEntryId: undefined,
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import '../runtime/insecure-tls-warning.cjs';
|
|
|
18
18
|
// Must precede every other dependency that imports Ink: CI detection is cached.
|
|
19
19
|
import { render } from './ui/ink-runtime.js';
|
|
20
20
|
import { Command } from 'commander';
|
|
21
|
+
import { ModelProfileConfigError } from './config/model-profile-store.js';
|
|
21
22
|
import { loadConfig } from './config/load.js';
|
|
22
23
|
import { getConfigPath, readConfigFile, updatePersistedConfig } from './config/store.js';
|
|
23
24
|
import { formatMigrationNotice, migrateLegacyState } from './config/migrate-legacy-state.js';
|
|
@@ -458,7 +459,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
458
459
|
// advising the user about a mode they just disabled.
|
|
459
460
|
const wantPaste = config.paste && !!process.stdin.isTTY;
|
|
460
461
|
const pasteBridge = { notify: null };
|
|
461
|
-
const filter =
|
|
462
|
+
const filter = process.stdin.isTTY
|
|
462
463
|
? tryCreateStdinFilter(process.stdin, { mouse: wantMouse, paste: wantPaste }, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
|
|
463
464
|
: null;
|
|
464
465
|
/**
|
|
@@ -648,6 +649,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
648
649
|
};
|
|
649
650
|
const terminalBridge = {
|
|
650
651
|
mouseSelect: wantSelect,
|
|
652
|
+
deleteDisambiguated: filter !== null,
|
|
651
653
|
scrollbar,
|
|
652
654
|
...(mouseOn
|
|
653
655
|
? {
|
|
@@ -1009,6 +1011,18 @@ function runConfigSetContextWindow(value) {
|
|
|
1009
1011
|
process.stdout.write(`Set contextWindow = ${merged.contextWindow ?? 'auto'}\n`);
|
|
1010
1012
|
}
|
|
1011
1013
|
function runConfigSet(key, value) {
|
|
1014
|
+
try {
|
|
1015
|
+
runConfigSetValue(key, value);
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
const invalid = error instanceof ModelProfileConfigError
|
|
1019
|
+
&& !['read_failed', 'write_failed'].includes(error.code);
|
|
1020
|
+
process.stderr.write(error instanceof ModelProfileConfigError
|
|
1021
|
+
? `${error.message}\n` : 'Could not write config. Check file permissions.\n');
|
|
1022
|
+
process.exitCode = invalid ? 2 : 1;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function runConfigSetValue(key, value) {
|
|
1012
1026
|
if (!CONFIG_SET_KEYS.has(key)) {
|
|
1013
1027
|
process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
|
|
1014
1028
|
process.exitCode = 2;
|