@aragon-agent/cli 0.4.3
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 +292 -0
- package/README.md +558 -0
- package/dist/agent/coalesce.d.ts +14 -0
- package/dist/agent/coalesce.js +41 -0
- package/dist/agent/controller.d.ts +132 -0
- package/dist/agent/controller.js +332 -0
- package/dist/agent/headless.d.ts +22 -0
- package/dist/agent/headless.js +74 -0
- package/dist/agent/reducer.d.ts +181 -0
- package/dist/agent/reducer.js +420 -0
- package/dist/agent/system-prompt.d.ts +30 -0
- package/dist/agent/system-prompt.js +58 -0
- package/dist/agent/usage.d.ts +13 -0
- package/dist/agent/usage.js +41 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +482 -0
- package/dist/commands/builtins.d.ts +16 -0
- package/dist/commands/builtins.js +223 -0
- package/dist/commands/registry.d.ts +48 -0
- package/dist/commands/registry.js +45 -0
- package/dist/commands/skills.d.ts +22 -0
- package/dist/commands/skills.js +625 -0
- package/dist/config/app-paths.d.ts +11 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/env.d.ts +28 -0
- package/dist/config/env.js +92 -0
- package/dist/config/load.d.ts +48 -0
- package/dist/config/load.js +158 -0
- package/dist/config/migrate-legacy-state.d.ts +78 -0
- package/dist/config/migrate-legacy-state.js +181 -0
- package/dist/config/schema.d.ts +249 -0
- package/dist/config/schema.js +247 -0
- package/dist/config/store.d.ts +44 -0
- package/dist/config/store.js +126 -0
- package/dist/input/keymap.d.ts +20 -0
- package/dist/input/keymap.js +85 -0
- package/dist/session/persist.d.ts +24 -0
- package/dist/session/persist.js +51 -0
- package/dist/skills/archive.d.ts +41 -0
- package/dist/skills/archive.js +152 -0
- package/dist/skills/cli-commands.d.ts +52 -0
- package/dist/skills/cli-commands.js +502 -0
- package/dist/skills/fetch-source.d.ts +104 -0
- package/dist/skills/fetch-source.js +448 -0
- package/dist/skills/installer.d.ts +117 -0
- package/dist/skills/installer.js +414 -0
- package/dist/skills/lock.d.ts +75 -0
- package/dist/skills/lock.js +223 -0
- package/dist/skills/manifest.d.ts +47 -0
- package/dist/skills/manifest.js +95 -0
- package/dist/skills/node-host.d.ts +23 -0
- package/dist/skills/node-host.js +59 -0
- package/dist/skills/paths.d.ts +61 -0
- package/dist/skills/paths.js +107 -0
- package/dist/skills/service.d.ts +256 -0
- package/dist/skills/service.js +730 -0
- package/dist/skills/tools.d.ts +35 -0
- package/dist/skills/tools.js +164 -0
- package/dist/skills/updater.d.ts +101 -0
- package/dist/skills/updater.js +330 -0
- package/dist/skills/usage.d.ts +72 -0
- package/dist/skills/usage.js +216 -0
- package/dist/tools/bash-tool.d.ts +12 -0
- package/dist/tools/bash-tool.js +131 -0
- package/dist/tools/diff.d.ts +15 -0
- package/dist/tools/diff.js +50 -0
- package/dist/tools/fs-tools.d.ts +16 -0
- package/dist/tools/fs-tools.js +215 -0
- package/dist/tools/index.d.ts +93 -0
- package/dist/tools/index.js +157 -0
- package/dist/tools/search-tools.d.ts +11 -0
- package/dist/tools/search-tools.js +225 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.js +522 -0
- package/dist/ui/AutocompletePopup.d.ts +21 -0
- package/dist/ui/AutocompletePopup.js +19 -0
- package/dist/ui/Composer.d.ts +40 -0
- package/dist/ui/Composer.js +61 -0
- package/dist/ui/Header.d.ts +30 -0
- package/dist/ui/Header.js +25 -0
- package/dist/ui/Logo.d.ts +50 -0
- package/dist/ui/Logo.js +85 -0
- package/dist/ui/Markdown.d.ts +23 -0
- package/dist/ui/Markdown.js +122 -0
- package/dist/ui/PromptInput.d.ts +81 -0
- package/dist/ui/PromptInput.js +383 -0
- package/dist/ui/SessionOpener.d.ts +30 -0
- package/dist/ui/SessionOpener.js +12 -0
- package/dist/ui/StatusBar.d.ts +39 -0
- package/dist/ui/StatusBar.js +43 -0
- package/dist/ui/ToastStack.d.ts +24 -0
- package/dist/ui/ToastStack.js +38 -0
- package/dist/ui/Transcript.d.ts +62 -0
- package/dist/ui/Transcript.js +128 -0
- package/dist/ui/capabilities.d.ts +25 -0
- package/dist/ui/capabilities.js +59 -0
- package/dist/ui/console-bridge.d.ts +19 -0
- package/dist/ui/console-bridge.js +52 -0
- package/dist/ui/density.d.ts +26 -0
- package/dist/ui/density.js +34 -0
- package/dist/ui/entries/AssistantEntry.d.ts +23 -0
- package/dist/ui/entries/AssistantEntry.js +13 -0
- package/dist/ui/entries/EntryFrame.d.ts +23 -0
- package/dist/ui/entries/EntryFrame.js +6 -0
- package/dist/ui/entries/ToolCard.d.ts +38 -0
- package/dist/ui/entries/ToolCard.js +72 -0
- package/dist/ui/entries/ToolPreview.d.ts +57 -0
- package/dist/ui/entries/ToolPreview.js +144 -0
- package/dist/ui/entries/UserEntry.d.ts +14 -0
- package/dist/ui/entries/UserEntry.js +5 -0
- package/dist/ui/exit-snapshot.d.ts +27 -0
- package/dist/ui/exit-snapshot.js +26 -0
- package/dist/ui/gauge.d.ts +24 -0
- package/dist/ui/gauge.js +36 -0
- package/dist/ui/glyphs.d.ts +87 -0
- package/dist/ui/glyphs.js +127 -0
- package/dist/ui/gradient.d.ts +11 -0
- package/dist/ui/gradient.js +50 -0
- package/dist/ui/layout/AppShell.d.ts +24 -0
- package/dist/ui/layout/AppShell.js +9 -0
- package/dist/ui/layout/Gutter.d.ts +67 -0
- package/dist/ui/layout/Gutter.js +42 -0
- package/dist/ui/layout/OverlayFrame.d.ts +62 -0
- package/dist/ui/layout/OverlayFrame.js +92 -0
- package/dist/ui/layout/ScrollViewport.d.ts +33 -0
- package/dist/ui/layout/ScrollViewport.js +71 -0
- package/dist/ui/layout/budget.d.ts +42 -0
- package/dist/ui/layout/budget.js +47 -0
- package/dist/ui/layout/frame.d.ts +58 -0
- package/dist/ui/layout/frame.js +75 -0
- package/dist/ui/layout/overlay-window.d.ts +43 -0
- package/dist/ui/layout/overlay-window.js +51 -0
- package/dist/ui/layout/scroll.d.ts +28 -0
- package/dist/ui/layout/scroll.js +76 -0
- package/dist/ui/layout/useTerminalSize.d.ts +13 -0
- package/dist/ui/layout/useTerminalSize.js +50 -0
- package/dist/ui/markdown-blocks.d.ts +62 -0
- package/dist/ui/markdown-blocks.js +120 -0
- package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
- package/dist/ui/overlays/ConfirmDialog.js +25 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
- package/dist/ui/overlays/HelpOverlay.js +60 -0
- package/dist/ui/overlays/ModelPicker.d.ts +29 -0
- package/dist/ui/overlays/ModelPicker.js +29 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
- package/dist/ui/overlays/SettingsScreen.js +106 -0
- package/dist/ui/palettes.d.ts +79 -0
- package/dist/ui/palettes.js +115 -0
- package/dist/ui/screen.d.ts +27 -0
- package/dist/ui/screen.js +57 -0
- package/dist/ui/theme.d.ts +78 -0
- package/dist/ui/theme.js +127 -0
- package/dist/ui/transcript-text.d.ts +27 -0
- package/dist/ui/transcript-text.js +110 -0
- package/package.json +73 -0
- package/skills/skill-creator/SKILL.md +96 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentController — builds the core `Agent`, wires the built-in toolset, and
|
|
3
|
+
* exposes a small run/abort/steer surface plus the pre-flight validation that
|
|
4
|
+
* turns the most common failure (no API key) into a guided action instead of a
|
|
5
|
+
* blank screen (spec §3.3.1 / R2).
|
|
6
|
+
*
|
|
7
|
+
* The controller never inspects the resolved promise of `agent.prompt()` for
|
|
8
|
+
* success — that promise resolves even on failure. Success/failure is derived
|
|
9
|
+
* from the event stream by the reducer (TUI) and the headless writer.
|
|
10
|
+
*/
|
|
11
|
+
import { ModelRegistry, type AgentEvent, type AgentTool, type Message, type ModelInfo, type ProviderRegistry, type ThinkingLevel } from '@aragon-agent/core';
|
|
12
|
+
import type { CliConfig, ThemeName } from '../config/schema.js';
|
|
13
|
+
import { type ConfirmRequest } from '../tools/index.js';
|
|
14
|
+
import { SkillService, type ApprovalGate } from '../skills/service.js';
|
|
15
|
+
import type { NoticeLevel } from './reducer.js';
|
|
16
|
+
/** Approval gate used whenever there is provably no human to ask (headless). */
|
|
17
|
+
export declare const DENY_ALL_APPROVAL: ApprovalGate;
|
|
18
|
+
export interface ControllerDeps {
|
|
19
|
+
/** Awaited before a mutating tool runs when `confirmTools` is enabled. */
|
|
20
|
+
confirm?: (req: ConfirmRequest) => Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Skill-install approval (§8.3.1 / D17). Distinct from `confirm` on purpose:
|
|
23
|
+
* that callback auto-approves when unattached, which is a sane default for
|
|
24
|
+
* `--confirm`-gated mutating tools and a silent security hole for skills.
|
|
25
|
+
* Defaults to deny-all, so forgetting to pass one fails CLOSED.
|
|
26
|
+
*/
|
|
27
|
+
approval?: ApprovalGate;
|
|
28
|
+
/**
|
|
29
|
+
* CLI version stamped into `.aragon-skill.json` and the install User-Agent.
|
|
30
|
+
* Without it an agent-initiated install records `installer: "0.0.0"` while the
|
|
31
|
+
* same install through `/skills` or `aragon skills` records the real version —
|
|
32
|
+
* so provenance would depend on who started it, which is exactly the question
|
|
33
|
+
* `/skills info` exists to answer.
|
|
34
|
+
*/
|
|
35
|
+
version?: string;
|
|
36
|
+
/** Called after the skill set changes so the UI can rebuild its commands. */
|
|
37
|
+
onSkillsChanged?: () => void;
|
|
38
|
+
/** Transcript notices from the skill subsystem. */
|
|
39
|
+
notify?: (level: NoticeLevel, text: string) => void;
|
|
40
|
+
}
|
|
41
|
+
export interface PreflightResult {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
message?: string;
|
|
44
|
+
/** 'config' for a missing/invalid key or unusable provider. */
|
|
45
|
+
kind?: 'config';
|
|
46
|
+
}
|
|
47
|
+
export declare class AgentController {
|
|
48
|
+
private config;
|
|
49
|
+
private readonly agent;
|
|
50
|
+
private readonly providerRegistry;
|
|
51
|
+
private readonly modelRegistry;
|
|
52
|
+
private readonly tools;
|
|
53
|
+
private readonly skills;
|
|
54
|
+
private readonly skillsEnabled;
|
|
55
|
+
private onSkillsChanged?;
|
|
56
|
+
/** Mutable session cwd — tools resolve relative paths against this. */
|
|
57
|
+
private cwd;
|
|
58
|
+
constructor(config: CliConfig, deps?: ControllerDeps);
|
|
59
|
+
private resolveKey;
|
|
60
|
+
/**
|
|
61
|
+
* The tool names registered right now — LAZY, and the laziness is required.
|
|
62
|
+
*
|
|
63
|
+
* `this.skills.discover()` runs during construction, before `this.tools` is
|
|
64
|
+
* assigned, and the closure handed to `createBuiltinTools` is invoked later
|
|
65
|
+
* still. Reading `this.tools` eagerly would capture `undefined`; reading it
|
|
66
|
+
* through this getter yields `[]` in that window, which `computeToolPolicy`
|
|
67
|
+
* handles by design (I-G1: no registered tools means no ceiling, never an
|
|
68
|
+
* empty permitted set).
|
|
69
|
+
*
|
|
70
|
+
* The same ordering is why tool-name resolution must NOT move into
|
|
71
|
+
* `validateSkillFrontmatter()`: that runs inside `discover()`, when the tool
|
|
72
|
+
* list does not exist yet, and would report every skill as unresolvable.
|
|
73
|
+
* `doctor` carries that check instead (§5.7).
|
|
74
|
+
*/
|
|
75
|
+
private toolNames;
|
|
76
|
+
private composeSystemPrompt;
|
|
77
|
+
private rebuildSystemPrompt;
|
|
78
|
+
/** Re-render the prompt from the current skill set and tell the UI. */
|
|
79
|
+
refreshSkills(): void;
|
|
80
|
+
getSkillService(): SkillService;
|
|
81
|
+
/** Let the UI attach its handler after construction (App mounts later). */
|
|
82
|
+
setOnSkillsChanged(handler: () => void): void;
|
|
83
|
+
/**
|
|
84
|
+
* Whether a usable API key exists for the given provider (default: active).
|
|
85
|
+
* Read-only CLI-layer helper for the Header/Welcome key-status dot (P1-1);
|
|
86
|
+
* does not touch `@aragon-agent/core`.
|
|
87
|
+
*/
|
|
88
|
+
hasApiKey(provider?: string): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Set the color theme live. Mirrors the `setModel` / `setThinkingLevel`
|
|
91
|
+
* mutator pattern — a subsequent re-render re-reads `getConfig().theme` and
|
|
92
|
+
* re-memoizes the palette (P1-1). Does not touch `@aragon-agent/core`.
|
|
93
|
+
*/
|
|
94
|
+
setTheme(name: ThemeName): void;
|
|
95
|
+
/**
|
|
96
|
+
* Advance the lifetime submit counter that drives the composer's progressive
|
|
97
|
+
* disclosure (§4.6). Lives here rather than in React state so incrementing it
|
|
98
|
+
* on every submit does not re-render the transcript.
|
|
99
|
+
*/
|
|
100
|
+
setSubmitCount(n: number): void;
|
|
101
|
+
subscribe(listener: (event: AgentEvent) => void): () => void;
|
|
102
|
+
preflight(): PreflightResult;
|
|
103
|
+
/**
|
|
104
|
+
* Start a run. Fire-and-forget for the TUI (events drive the UI); returns the
|
|
105
|
+
* underlying promise so headless mode can await completion. `agent.prompt()`
|
|
106
|
+
* never rejects, so callers must not treat resolution as success.
|
|
107
|
+
*/
|
|
108
|
+
prompt(text: string): Promise<void>;
|
|
109
|
+
abort(): void;
|
|
110
|
+
steer(text: string): void;
|
|
111
|
+
followUp(text: string): void;
|
|
112
|
+
isRunning(): boolean;
|
|
113
|
+
setModel(provider: string, model: string, baseUrl?: string): void;
|
|
114
|
+
setThinkingLevel(level: ThinkingLevel): void;
|
|
115
|
+
setMaxTokens(value: number | undefined): void;
|
|
116
|
+
setApiKey(provider: string, key: string): void;
|
|
117
|
+
setCwd(cwd: string): void;
|
|
118
|
+
getCwd(): string;
|
|
119
|
+
clearMessages(): void;
|
|
120
|
+
/** Read-only view of the live system prompt (regression tests, /debug). */
|
|
121
|
+
getSystemPrompt(): string;
|
|
122
|
+
replaceMessages(messages: Message[]): void;
|
|
123
|
+
getMessages(): Message[];
|
|
124
|
+
clearAllQueues(): void;
|
|
125
|
+
getConfig(): CliConfig;
|
|
126
|
+
getProviderRegistry(): ProviderRegistry;
|
|
127
|
+
getModelRegistry(): ModelRegistry;
|
|
128
|
+
/** Resolve `ModelInfo` for the active model, falling back to a runtime model. */
|
|
129
|
+
getModelInfo(): ModelInfo;
|
|
130
|
+
/** List usable tools (name + description) for the /tools command. */
|
|
131
|
+
listTools(): AgentTool[];
|
|
132
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentController — builds the core `Agent`, wires the built-in toolset, and
|
|
3
|
+
* exposes a small run/abort/steer surface plus the pre-flight validation that
|
|
4
|
+
* turns the most common failure (no API key) into a guided action instead of a
|
|
5
|
+
* blank screen (spec §3.3.1 / R2).
|
|
6
|
+
*
|
|
7
|
+
* The controller never inspects the resolved promise of `agent.prompt()` for
|
|
8
|
+
* success — that promise resolves even on failure. Success/failure is derived
|
|
9
|
+
* from the event stream by the reducer (TUI) and the headless writer.
|
|
10
|
+
*/
|
|
11
|
+
import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
|
|
12
|
+
import { isAdapterProvider } from '../config/schema.js';
|
|
13
|
+
import { makeGetApiKey } from '../config/load.js';
|
|
14
|
+
import { updatePersistedConfig } from '../config/store.js';
|
|
15
|
+
import { createBuiltinTools } from '../tools/index.js';
|
|
16
|
+
import { createNodeSkillHost } from '../skills/node-host.js';
|
|
17
|
+
import { SkillService } from '../skills/service.js';
|
|
18
|
+
import { createSkillTools } from '../skills/tools.js';
|
|
19
|
+
import { buildSystemPrompt } from './system-prompt.js';
|
|
20
|
+
/** Approval gate used whenever there is provably no human to ask (headless). */
|
|
21
|
+
export const DENY_ALL_APPROVAL = {
|
|
22
|
+
canPrompt: () => false,
|
|
23
|
+
request: async () => false,
|
|
24
|
+
};
|
|
25
|
+
export class AgentController {
|
|
26
|
+
config;
|
|
27
|
+
agent;
|
|
28
|
+
providerRegistry;
|
|
29
|
+
modelRegistry;
|
|
30
|
+
tools;
|
|
31
|
+
skills;
|
|
32
|
+
skillsEnabled;
|
|
33
|
+
onSkillsChanged;
|
|
34
|
+
/** Mutable session cwd — tools resolve relative paths against this. */
|
|
35
|
+
cwd;
|
|
36
|
+
constructor(config, deps = {}) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.cwd = config.cwd;
|
|
39
|
+
this.providerRegistry = initProviders();
|
|
40
|
+
this.modelRegistry = new ModelRegistry(this.providerRegistry);
|
|
41
|
+
this.skillsEnabled = config.skills.enabled;
|
|
42
|
+
this.onSkillsChanged = deps.onSkillsChanged;
|
|
43
|
+
this.skills = new SkillService({
|
|
44
|
+
host: createNodeSkillHost(),
|
|
45
|
+
getCwd: () => this.cwd,
|
|
46
|
+
config: config.skills,
|
|
47
|
+
runtime: config.skillsRuntime,
|
|
48
|
+
// Fail-closed by default (D17): a missing gate must never mean "approved".
|
|
49
|
+
approval: deps.approval ?? DENY_ALL_APPROVAL,
|
|
50
|
+
onChange: () => this.refreshSkills(),
|
|
51
|
+
...(deps.notify ? { notify: deps.notify } : {}),
|
|
52
|
+
persist: (patch) => {
|
|
53
|
+
try {
|
|
54
|
+
updatePersistedConfig({ skills: patch });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Best-effort: a failed persist must not break the live session.
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (this.skillsEnabled) {
|
|
62
|
+
this.skills.discover();
|
|
63
|
+
this.skills.reportUnknownForcedSkills();
|
|
64
|
+
}
|
|
65
|
+
this.tools = createBuiltinTools({
|
|
66
|
+
getCwd: () => this.cwd,
|
|
67
|
+
confirmTools: config.confirmTools,
|
|
68
|
+
confirm: deps.confirm,
|
|
69
|
+
// `--no-skills` yields an empty array, so the tool list is byte-identical
|
|
70
|
+
// to the pre-Skills seven (invariant I-S1 / AC-10).
|
|
71
|
+
skillTools: this.skillsEnabled
|
|
72
|
+
? createSkillTools(this.skills, deps.version, () => this.toolNames())
|
|
73
|
+
: [],
|
|
74
|
+
// Omitted entirely when skills are off, which is what leaves the seven
|
|
75
|
+
// built-ins unwrapped and AC-G17 provable by object identity.
|
|
76
|
+
...(this.skillsEnabled
|
|
77
|
+
? {
|
|
78
|
+
toolPolicy: () => this.skills.toolPolicyDecision(() => this.toolNames()),
|
|
79
|
+
onToolPolicyEvent: (e) => this.skills.reportToolPolicyEvent(e),
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
82
|
+
});
|
|
83
|
+
const model = {
|
|
84
|
+
providerId: config.provider,
|
|
85
|
+
modelId: config.model,
|
|
86
|
+
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
87
|
+
};
|
|
88
|
+
this.agent = new Agent({
|
|
89
|
+
systemPrompt: this.composeSystemPrompt(),
|
|
90
|
+
model,
|
|
91
|
+
tools: this.tools,
|
|
92
|
+
thinkingLevel: config.thinkingLevel,
|
|
93
|
+
providerRegistry: this.providerRegistry,
|
|
94
|
+
// Resolve the key dynamically so live edits (settings screen) take effect
|
|
95
|
+
// without rebuilding the Agent — the closure reads the current config.
|
|
96
|
+
getApiKey: (id) => this.resolveKey(id),
|
|
97
|
+
maxTokens: config.maxTokens,
|
|
98
|
+
timeouts: {
|
|
99
|
+
// Timeout invariant (R1): idle ≥ tool so long tool runs are not killed.
|
|
100
|
+
toolTimeout: config.toolTimeoutMs,
|
|
101
|
+
idleTimeout: config.idleTimeoutMs,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
resolveKey(providerId) {
|
|
106
|
+
return makeGetApiKey(this.config)(providerId);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The tool names registered right now — LAZY, and the laziness is required.
|
|
110
|
+
*
|
|
111
|
+
* `this.skills.discover()` runs during construction, before `this.tools` is
|
|
112
|
+
* assigned, and the closure handed to `createBuiltinTools` is invoked later
|
|
113
|
+
* still. Reading `this.tools` eagerly would capture `undefined`; reading it
|
|
114
|
+
* through this getter yields `[]` in that window, which `computeToolPolicy`
|
|
115
|
+
* handles by design (I-G1: no registered tools means no ceiling, never an
|
|
116
|
+
* empty permitted set).
|
|
117
|
+
*
|
|
118
|
+
* The same ordering is why tool-name resolution must NOT move into
|
|
119
|
+
* `validateSkillFrontmatter()`: that runs inside `discover()`, when the tool
|
|
120
|
+
* list does not exist yet, and would report every skill as unresolvable.
|
|
121
|
+
* `doctor` carries that check instead (§5.7).
|
|
122
|
+
*/
|
|
123
|
+
toolNames() {
|
|
124
|
+
return this.tools?.map((t) => t.name) ?? [];
|
|
125
|
+
}
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
// System prompt — invariant I-S2 (§6.3.1 / P1-1 / C3)
|
|
128
|
+
//
|
|
129
|
+
// `agent.setSystemPrompt()` is called from EXACTLY ONE place in this class:
|
|
130
|
+
// `rebuildSystemPrompt()`. Before Skills existed, `setCwd()` built its own
|
|
131
|
+
// prompt inline; leaving it that way meant any rescan-then-setCwd ordering
|
|
132
|
+
// would drop `<available_skills>` with no error, no log, and no plausible
|
|
133
|
+
// connection between the user's action (`/cwd ..`) and the symptom (the model
|
|
134
|
+
// suddenly denies having any skills). Structure, not call ordering, is what
|
|
135
|
+
// rules that out — so if you add a third caller, route it through here.
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
composeSystemPrompt() {
|
|
138
|
+
return buildSystemPrompt({
|
|
139
|
+
cwd: this.cwd,
|
|
140
|
+
tools: this.tools,
|
|
141
|
+
skillsBlock: this.skillsEnabled ? this.skills.catalogBlock() + this.skills.alwaysBlock() : '',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
rebuildSystemPrompt() {
|
|
145
|
+
this.agent.setSystemPrompt(this.composeSystemPrompt());
|
|
146
|
+
}
|
|
147
|
+
/** Re-render the prompt from the current skill set and tell the UI. */
|
|
148
|
+
refreshSkills() {
|
|
149
|
+
this.rebuildSystemPrompt();
|
|
150
|
+
this.onSkillsChanged?.();
|
|
151
|
+
}
|
|
152
|
+
getSkillService() {
|
|
153
|
+
return this.skills;
|
|
154
|
+
}
|
|
155
|
+
/** Let the UI attach its handler after construction (App mounts later). */
|
|
156
|
+
setOnSkillsChanged(handler) {
|
|
157
|
+
this.onSkillsChanged = handler;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Whether a usable API key exists for the given provider (default: active).
|
|
161
|
+
* Read-only CLI-layer helper for the Header/Welcome key-status dot (P1-1);
|
|
162
|
+
* does not touch `@aragon-agent/core`.
|
|
163
|
+
*/
|
|
164
|
+
hasApiKey(provider) {
|
|
165
|
+
const key = this.resolveKey(provider ?? this.config.provider);
|
|
166
|
+
return !!key && key.trim().length > 0;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Set the color theme live. Mirrors the `setModel` / `setThinkingLevel`
|
|
170
|
+
* mutator pattern — a subsequent re-render re-reads `getConfig().theme` and
|
|
171
|
+
* re-memoizes the palette (P1-1). Does not touch `@aragon-agent/core`.
|
|
172
|
+
*/
|
|
173
|
+
setTheme(name) {
|
|
174
|
+
this.config = { ...this.config, theme: name };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Advance the lifetime submit counter that drives the composer's progressive
|
|
178
|
+
* disclosure (§4.6). Lives here rather than in React state so incrementing it
|
|
179
|
+
* on every submit does not re-render the transcript.
|
|
180
|
+
*/
|
|
181
|
+
setSubmitCount(n) {
|
|
182
|
+
this.config = { ...this.config, submitCount: n };
|
|
183
|
+
}
|
|
184
|
+
// -----------------------------------------------------------------------
|
|
185
|
+
// Event subscription (delegates to the core Agent)
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
subscribe(listener) {
|
|
188
|
+
return this.agent.subscribe(listener);
|
|
189
|
+
}
|
|
190
|
+
// -----------------------------------------------------------------------
|
|
191
|
+
// Pre-flight (R2): validate BEFORE entering the loop.
|
|
192
|
+
// -----------------------------------------------------------------------
|
|
193
|
+
preflight() {
|
|
194
|
+
const provider = this.config.provider;
|
|
195
|
+
if (!isAdapterProvider(provider)) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
kind: 'config',
|
|
199
|
+
message: `Provider "${provider}" has no adapter. Choose one of anthropic, openai, or google ` +
|
|
200
|
+
'(open /settings or set --provider).',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const key = makeGetApiKey(this.config)(provider);
|
|
204
|
+
if (!key || key.trim().length === 0) {
|
|
205
|
+
const envVar = provider === 'anthropic'
|
|
206
|
+
? 'ANTHROPIC_API_KEY'
|
|
207
|
+
: provider === 'openai'
|
|
208
|
+
? 'OPENAI_API_KEY'
|
|
209
|
+
: 'GOOGLE_API_KEY';
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
kind: 'config',
|
|
213
|
+
message: `No API key for "${provider}" - open /settings or set ${envVar}.`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return { ok: true };
|
|
217
|
+
}
|
|
218
|
+
// -----------------------------------------------------------------------
|
|
219
|
+
// Run lifecycle
|
|
220
|
+
// -----------------------------------------------------------------------
|
|
221
|
+
/**
|
|
222
|
+
* Start a run. Fire-and-forget for the TUI (events drive the UI); returns the
|
|
223
|
+
* underlying promise so headless mode can await completion. `agent.prompt()`
|
|
224
|
+
* never rejects, so callers must not treat resolution as success.
|
|
225
|
+
*/
|
|
226
|
+
prompt(text) {
|
|
227
|
+
// A new user message is a new intent, so the previous turn's tool ceiling
|
|
228
|
+
// expires here and anything a slash command queued is promoted (D-G1 / D-G2).
|
|
229
|
+
// This and `steer()` below are the ONLY two places a user message enters the
|
|
230
|
+
// engine, which is what makes a per-turn scope implementable at all (FG9).
|
|
231
|
+
if (this.skillsEnabled)
|
|
232
|
+
this.skills.beginUserTurn();
|
|
233
|
+
return this.agent.prompt(text);
|
|
234
|
+
}
|
|
235
|
+
abort() {
|
|
236
|
+
this.agent.abort();
|
|
237
|
+
}
|
|
238
|
+
steer(text) {
|
|
239
|
+
// Mid-run interjection: promote the queue, keep what is already in force
|
|
240
|
+
// (D-G3). Clearing here would make "ask a follow-up" the way around the
|
|
241
|
+
// ceiling; skipping the promotion would miss slash commands, which the TUI
|
|
242
|
+
// routes through `steer` whenever the agent happens to be running.
|
|
243
|
+
if (this.skillsEnabled)
|
|
244
|
+
this.skills.absorbPendingFrames();
|
|
245
|
+
this.agent.steer(text);
|
|
246
|
+
}
|
|
247
|
+
followUp(text) {
|
|
248
|
+
this.agent.followUp(text);
|
|
249
|
+
}
|
|
250
|
+
isRunning() {
|
|
251
|
+
return this.agent.state.isRunning;
|
|
252
|
+
}
|
|
253
|
+
// -----------------------------------------------------------------------
|
|
254
|
+
// Mutators
|
|
255
|
+
// -----------------------------------------------------------------------
|
|
256
|
+
setModel(provider, model, baseUrl) {
|
|
257
|
+
this.config = { ...this.config, provider, model, baseUrl };
|
|
258
|
+
this.agent.setModel({ providerId: provider, modelId: model, ...(baseUrl ? { baseUrl } : {}) });
|
|
259
|
+
}
|
|
260
|
+
setThinkingLevel(level) {
|
|
261
|
+
this.config = { ...this.config, thinkingLevel: level };
|
|
262
|
+
this.agent.setThinkingLevel(level);
|
|
263
|
+
}
|
|
264
|
+
setMaxTokens(value) {
|
|
265
|
+
this.config = { ...this.config, maxTokens: value };
|
|
266
|
+
this.agent.setMaxTokens(value);
|
|
267
|
+
}
|
|
268
|
+
setApiKey(provider, key) {
|
|
269
|
+
// The Agent resolves keys via `resolveKey(this.config)` at call time, so
|
|
270
|
+
// updating the config here is sufficient — no Agent rebuild needed.
|
|
271
|
+
this.config = {
|
|
272
|
+
...this.config,
|
|
273
|
+
apiKeys: { ...this.config.apiKeys, [provider]: key },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
setCwd(cwd) {
|
|
277
|
+
this.cwd = cwd;
|
|
278
|
+
this.config = { ...this.config, cwd };
|
|
279
|
+
// The project scope moved with the cwd, so rescan BEFORE rebuilding — then
|
|
280
|
+
// let the single prompt entry point emit a prompt that reflects both the new
|
|
281
|
+
// directory and the new skill set (I-S2 / AC-17).
|
|
282
|
+
if (this.skillsEnabled)
|
|
283
|
+
this.skills.discover();
|
|
284
|
+
this.rebuildSystemPrompt();
|
|
285
|
+
this.onSkillsChanged?.();
|
|
286
|
+
}
|
|
287
|
+
getCwd() {
|
|
288
|
+
return this.cwd;
|
|
289
|
+
}
|
|
290
|
+
clearMessages() {
|
|
291
|
+
this.agent.clearMessages();
|
|
292
|
+
// A new conversation has no loaded skills, so the "already loaded earlier
|
|
293
|
+
// in this conversation" hint would otherwise start lying (P2-9).
|
|
294
|
+
this.skills.getRegistry().clearActive();
|
|
295
|
+
}
|
|
296
|
+
/** Read-only view of the live system prompt (regression tests, /debug). */
|
|
297
|
+
getSystemPrompt() {
|
|
298
|
+
return this.agent.state.systemPrompt;
|
|
299
|
+
}
|
|
300
|
+
replaceMessages(messages) {
|
|
301
|
+
this.agent.replaceMessages(messages);
|
|
302
|
+
}
|
|
303
|
+
getMessages() {
|
|
304
|
+
return this.agent.state.messages;
|
|
305
|
+
}
|
|
306
|
+
clearAllQueues() {
|
|
307
|
+
this.agent.clearAllQueues();
|
|
308
|
+
}
|
|
309
|
+
// -----------------------------------------------------------------------
|
|
310
|
+
// Model / provider info
|
|
311
|
+
// -----------------------------------------------------------------------
|
|
312
|
+
getConfig() {
|
|
313
|
+
return this.config;
|
|
314
|
+
}
|
|
315
|
+
getProviderRegistry() {
|
|
316
|
+
return this.providerRegistry;
|
|
317
|
+
}
|
|
318
|
+
getModelRegistry() {
|
|
319
|
+
return this.modelRegistry;
|
|
320
|
+
}
|
|
321
|
+
/** Resolve `ModelInfo` for the active model, falling back to a runtime model. */
|
|
322
|
+
getModelInfo() {
|
|
323
|
+
const found = this.modelRegistry.getModel(this.config.provider, this.config.model);
|
|
324
|
+
if (found)
|
|
325
|
+
return found;
|
|
326
|
+
return this.modelRegistry.buildRuntimeModel(this.config.provider, this.config.model);
|
|
327
|
+
}
|
|
328
|
+
/** List usable tools (name + description) for the /tools command. */
|
|
329
|
+
listTools() {
|
|
330
|
+
return this.tools;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot / print mode (spec §3.7). No Ink: stream `text_delta` to stdout,
|
|
3
|
+
* compact tool lines to stderr, then a usage footer. Exit-code correctness is
|
|
4
|
+
* derived from the event stream (R2), not from the resolved promise:
|
|
5
|
+
* 0 = success, 1 = agent error (incl. a silently-swallowed throw), 2 = config.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentEvent, ModelInfo } from '@aragon-agent/core';
|
|
8
|
+
import type { PreflightResult } from './controller.js';
|
|
9
|
+
/** The minimal controller surface headless mode needs (real or stubbed). */
|
|
10
|
+
export interface HeadlessController {
|
|
11
|
+
preflight(): PreflightResult;
|
|
12
|
+
subscribe(listener: (event: AgentEvent) => void): () => void;
|
|
13
|
+
prompt(text: string): Promise<void>;
|
|
14
|
+
getModelInfo(): ModelInfo;
|
|
15
|
+
}
|
|
16
|
+
export interface HeadlessOptions {
|
|
17
|
+
quiet?: boolean;
|
|
18
|
+
stdout?: NodeJS.WritableStream;
|
|
19
|
+
stderr?: NodeJS.WritableStream;
|
|
20
|
+
}
|
|
21
|
+
/** Run a single prompt headlessly and resolve with the process exit code. */
|
|
22
|
+
export declare function runHeadless(controller: HeadlessController, prompt: string, options?: HeadlessOptions): Promise<number>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot / print mode (spec §3.7). No Ink: stream `text_delta` to stdout,
|
|
3
|
+
* compact tool lines to stderr, then a usage footer. Exit-code correctness is
|
|
4
|
+
* derived from the event stream (R2), not from the resolved promise:
|
|
5
|
+
* 0 = success, 1 = agent error (incl. a silently-swallowed throw), 2 = config.
|
|
6
|
+
*/
|
|
7
|
+
import { formatCost, formatTokens, computeCost } from './usage.js';
|
|
8
|
+
import { formatStreamError } from './reducer.js';
|
|
9
|
+
/** Run a single prompt headlessly and resolve with the process exit code. */
|
|
10
|
+
export async function runHeadless(controller, prompt, options = {}) {
|
|
11
|
+
const out = options.stdout ?? process.stdout;
|
|
12
|
+
const err = options.stderr ?? process.stderr;
|
|
13
|
+
const quiet = options.quiet ?? false;
|
|
14
|
+
const pre = controller.preflight();
|
|
15
|
+
if (!pre.ok) {
|
|
16
|
+
err.write(`${pre.message ?? 'Configuration error.'}\n`);
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
let errored = false;
|
|
20
|
+
let sawTurnEnd = false;
|
|
21
|
+
let wroteText = false;
|
|
22
|
+
const total = { inputTokens: 0, outputTokens: 0 };
|
|
23
|
+
const unsubscribe = controller.subscribe((event) => {
|
|
24
|
+
switch (event.type) {
|
|
25
|
+
case 'message_update': {
|
|
26
|
+
const se = event.streamEvent;
|
|
27
|
+
if (se.type === 'text_delta') {
|
|
28
|
+
out.write(se.delta);
|
|
29
|
+
wroteText = true;
|
|
30
|
+
}
|
|
31
|
+
else if (se.type === 'error') {
|
|
32
|
+
errored = true;
|
|
33
|
+
err.write(`\n${formatStreamError(se.error)}\n`);
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case 'tool_execution_start':
|
|
38
|
+
if (!quiet)
|
|
39
|
+
err.write(`\n▸ ${event.toolName}`);
|
|
40
|
+
break;
|
|
41
|
+
case 'tool_execution_end':
|
|
42
|
+
if (!quiet) {
|
|
43
|
+
err.write(` (${event.duration}ms)${event.isError ? ' [error]' : ''}\n`);
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case 'turn_end':
|
|
47
|
+
sawTurnEnd = true;
|
|
48
|
+
total.inputTokens += event.usage.inputTokens;
|
|
49
|
+
total.outputTokens += event.usage.outputTokens;
|
|
50
|
+
break;
|
|
51
|
+
case 'agent_end':
|
|
52
|
+
// A swallowed throw (missing key / empty stream) produces no turn_end.
|
|
53
|
+
if (!sawTurnEnd && !errored)
|
|
54
|
+
errored = true;
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
await controller.prompt(prompt);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
unsubscribe();
|
|
65
|
+
}
|
|
66
|
+
// Ensure a trailing newline so piped output is well-formed.
|
|
67
|
+
if (wroteText)
|
|
68
|
+
out.write('\n');
|
|
69
|
+
if (!quiet) {
|
|
70
|
+
const cost = computeCost(total, controller.getModelInfo().cost);
|
|
71
|
+
err.write(`\n[usage] in ${formatTokens(total.inputTokens)} · out ${formatTokens(total.outputTokens)} · ${formatCost(cost)}\n`);
|
|
72
|
+
}
|
|
73
|
+
return errored ? 1 : 0;
|
|
74
|
+
}
|