@aragon-agent/cli 0.5.11 → 0.6.1
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 +124 -0
- package/README.md +306 -9
- package/dist/agent/controller.d.ts +37 -1
- package/dist/agent/controller.js +34 -4
- package/dist/agent/reducer.js +14 -4
- package/dist/agent/system-prompt.d.ts +16 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/cli.js +71 -2
- package/dist/commands/builtins.js +21 -2
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +10 -0
- package/dist/diagnostics/cli-commands.d.ts +10 -0
- package/dist/diagnostics/cli-commands.js +61 -0
- package/dist/diagnostics/doctor.d.ts +29 -0
- package/dist/diagnostics/doctor.js +167 -0
- package/dist/diagnostics/info.d.ts +30 -0
- package/dist/diagnostics/info.js +99 -0
- package/dist/exec/cli-commands.d.ts +31 -0
- package/dist/exec/cli-commands.js +63 -0
- package/dist/exec/emitter.d.ts +80 -0
- package/dist/exec/emitter.js +127 -0
- package/dist/exec/events.d.ts +226 -0
- package/dist/exec/events.js +43 -0
- package/dist/exec/index.d.ts +56 -0
- package/dist/exec/index.js +435 -0
- package/dist/exec/options.d.ts +76 -0
- package/dist/exec/options.js +221 -0
- package/dist/exec/permission.d.ts +66 -0
- package/dist/exec/permission.js +96 -0
- package/dist/exec/runner.d.ts +204 -0
- package/dist/exec/runner.js +559 -0
- package/dist/exec/stdin-stream.d.ts +46 -0
- package/dist/exec/stdin-stream.js +144 -0
- package/dist/logging/install.d.ts +22 -0
- package/dist/logging/install.js +22 -0
- package/dist/session/cli-commands.d.ts +30 -0
- package/dist/session/cli-commands.js +162 -0
- package/dist/session/persist.d.ts +46 -0
- package/dist/session/persist.js +8 -0
- package/dist/session/store.d.ts +129 -0
- package/dist/session/store.js +385 -0
- package/dist/team/runtime.d.ts +12 -0
- package/dist/team/runtime.js +6 -0
- package/dist/team/subagent.d.ts +12 -0
- package/dist/team/subagent.js +4 -0
- package/dist/todo/store.d.ts +12 -6
- package/dist/todo/store.js +12 -6
- package/dist/todo/types.d.ts +2 -1
- package/dist/tools/index.d.ts +20 -0
- package/dist/tools/index.js +26 -0
- package/dist/ui/ActivityLine.d.ts +32 -0
- package/dist/ui/ActivityLine.js +23 -1
- package/dist/ui/App.d.ts +14 -1
- package/dist/ui/App.js +70 -6
- package/dist/ui/BottomStatusRow.d.ts +33 -1
- package/dist/ui/BottomStatusRow.js +20 -6
- package/dist/ui/Transcript.js +15 -0
- package/dist/ui/entries/FastCard.js +20 -2
- package/dist/ui/overlays/HelpOverlay.js +1 -1
- package/dist/ui/use-startup-notices.d.ts +67 -4
- package/dist/ui/use-startup-notices.js +76 -2
- package/dist/ui/win-vt-input.d.ts +57 -0
- package/dist/ui/win-vt-input.js +62 -0
- package/package.json +7 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon info` - capability discovery (cli-integration-surface section 5.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/diagnostics/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* "INSTALL AND CALL" ACROSS VERSIONS IS ONLY ROBUST IF THE CALLER CAN ASK WHAT
|
|
7
|
+
* IT JUST INSTALLED (G6). Everything a wrapper might branch on is here, and
|
|
8
|
+
* `features` is a FLAT STRING LIST on purpose: a wrapper testing
|
|
9
|
+
* `info.features.includes('sessions')` keeps working across versions that add
|
|
10
|
+
* capabilities, which a nested shape with renamed keys would not.
|
|
11
|
+
*/
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
import { getConfigPath, getSessionsDir, getUserDataDir } from '../config/app-paths.js';
|
|
17
|
+
import { loadConfig, makeGetApiKey } from '../config/load.js';
|
|
18
|
+
import { HOST_TOOL_NAMES } from '../tools/index.js';
|
|
19
|
+
import { EXEC_SCHEMA_VERSION } from '../exec/events.js';
|
|
20
|
+
/**
|
|
21
|
+
* Everything this build can do, as names a wrapper can test for.
|
|
22
|
+
*
|
|
23
|
+
* ADD, NEVER RENAME. A removed name is a breaking change for every consumer that
|
|
24
|
+
* guarded on it, and the whole value of the list is that guarding is cheap.
|
|
25
|
+
*/
|
|
26
|
+
const FEATURES = [
|
|
27
|
+
'exec',
|
|
28
|
+
'sessions',
|
|
29
|
+
'permissions',
|
|
30
|
+
'budgets',
|
|
31
|
+
'team',
|
|
32
|
+
'todo',
|
|
33
|
+
'skills',
|
|
34
|
+
'fast',
|
|
35
|
+
];
|
|
36
|
+
export function buildInfo(flags, version) {
|
|
37
|
+
const config = loadConfig(flags);
|
|
38
|
+
const getKey = makeGetApiKey(config);
|
|
39
|
+
return {
|
|
40
|
+
cli: version,
|
|
41
|
+
core: readCoreVersion(),
|
|
42
|
+
node: process.version,
|
|
43
|
+
schemaVersion: EXEC_SCHEMA_VERSION,
|
|
44
|
+
home: getUserDataDir(),
|
|
45
|
+
configPath: getConfigPath(),
|
|
46
|
+
sessionsDir: getSessionsDir(),
|
|
47
|
+
provider: config.provider,
|
|
48
|
+
model: config.model,
|
|
49
|
+
hasApiKey: Boolean(getKey(config.provider)),
|
|
50
|
+
// THE SUPERSET, not "what a run would register right now". This answers
|
|
51
|
+
// "which names can `--allow-tool` take?", which is exactly what
|
|
52
|
+
// `HOST_TOOL_NAMES` means and exactly what `--allow-tool` validates against.
|
|
53
|
+
tools: [...HOST_TOOL_NAMES],
|
|
54
|
+
outputFormats: ['text', 'json', 'stream-json'],
|
|
55
|
+
inputFormats: ['text', 'stream-json'],
|
|
56
|
+
permissionModes: ['auto', 'plan', 'strict'],
|
|
57
|
+
features: [...FEATURES],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The installed `@aragon-agent/core` version, or `'unknown'`.
|
|
62
|
+
*
|
|
63
|
+
* RESOLVED, NOT READ OFF THE DEPENDENCY RANGE. `"^0.2.12"` in this package's
|
|
64
|
+
* own `package.json` is what was ASKED FOR; a consumer asking `aragon info`
|
|
65
|
+
* wants what is actually loaded, and the two differ on every machine that
|
|
66
|
+
* installed a patch release.
|
|
67
|
+
*
|
|
68
|
+
* A DIRECTORY WALK, AND NEITHER `createRequire().resolve()` NOR
|
|
69
|
+
* `import.meta.resolve()` (implementation finding IF-2). Core's `exports` map
|
|
70
|
+
* declares only an `import` condition, so `createRequire(...).resolve()` fails
|
|
71
|
+
* with `ERR_PACKAGE_PATH_NOT_EXPORTED` - and it fails on the DEVELOPER's machine
|
|
72
|
+
* as readily as on a user's, which is how this was caught. `import.meta.resolve`
|
|
73
|
+
* is not synchronously available on Node 18, and `engines` says `>=18`.
|
|
74
|
+
*
|
|
75
|
+
* The walk covers all three real layouts: the monorepo (a symlink in the root
|
|
76
|
+
* `node_modules`), a flat npm install (core beside cli under the project's
|
|
77
|
+
* `node_modules`), and a nested one (core under the cli package's own
|
|
78
|
+
* `node_modules`).
|
|
79
|
+
*/
|
|
80
|
+
function readCoreVersion() {
|
|
81
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
83
|
+
try {
|
|
84
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'node_modules', '@aragon-agent', 'core', 'package.json'), 'utf-8'));
|
|
85
|
+
if (pkg.version)
|
|
86
|
+
return pkg.version;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Not this level. Keep walking.
|
|
90
|
+
}
|
|
91
|
+
const parent = dirname(dir);
|
|
92
|
+
if (parent === dir)
|
|
93
|
+
break;
|
|
94
|
+
dir = parent;
|
|
95
|
+
}
|
|
96
|
+
// A bundled or unusual layout. A missing version is worth reporting honestly;
|
|
97
|
+
// it is not worth failing a discovery command over.
|
|
98
|
+
return 'unknown';
|
|
99
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon exec` commander wiring (cli-integration-surface section 3.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* THE DECLARATIONS LIVE HERE, NOT IN `cli.tsx`, AND THAT IS A DELIBERATE
|
|
7
|
+
* DEPARTURE FROM THE EXISTING CONVENTION. `logs` / `history` / `skills` /
|
|
8
|
+
* `update` declare their options in `cli.tsx` and delegate to a `run*Command`.
|
|
9
|
+
* `cli.tsx` is already 1600 lines against this repository's 1000-line guideline,
|
|
10
|
+
* and four new subcommands carrying about twenty declarations between them is
|
|
11
|
+
* another 120 lines on a file that is 60% over budget. So `cli.tsx` gains four
|
|
12
|
+
* imports and four `register*Command(...)` call sites instead. This is a rule
|
|
13
|
+
* for the NEW code, not a refactor of the old (which is out of scope).
|
|
14
|
+
*
|
|
15
|
+
* NOT `src/commands/` EITHER: that directory is the SLASH-command tree
|
|
16
|
+
* (`/help`, `/model`, `/save`, `/resume`), and it already owns the two names
|
|
17
|
+
* exec's session work sits closest to.
|
|
18
|
+
*/
|
|
19
|
+
import { type Command } from 'commander';
|
|
20
|
+
import type { CliFlags } from '../config/load.js';
|
|
21
|
+
import { type ExecControllerFactory } from './index.js';
|
|
22
|
+
export interface ExecCommandDeps {
|
|
23
|
+
version: string;
|
|
24
|
+
/**
|
|
25
|
+
* Supplied by `cli.tsx`. It cannot be imported: `cli.tsx` imports this module,
|
|
26
|
+
* so reaching back for `makeController` would be a cycle, and re-deriving it
|
|
27
|
+
* here would give the headless approval gates a second definition.
|
|
28
|
+
*/
|
|
29
|
+
makeController: ExecControllerFactory;
|
|
30
|
+
}
|
|
31
|
+
export declare function registerExecCommand(program: Command, toFlags: (opts: Record<string, unknown>) => CliFlags, deps: ExecCommandDeps): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon exec` commander wiring (cli-integration-surface section 3.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* THE DECLARATIONS LIVE HERE, NOT IN `cli.tsx`, AND THAT IS A DELIBERATE
|
|
7
|
+
* DEPARTURE FROM THE EXISTING CONVENTION. `logs` / `history` / `skills` /
|
|
8
|
+
* `update` declare their options in `cli.tsx` and delegate to a `run*Command`.
|
|
9
|
+
* `cli.tsx` is already 1600 lines against this repository's 1000-line guideline,
|
|
10
|
+
* and four new subcommands carrying about twenty declarations between them is
|
|
11
|
+
* another 120 lines on a file that is 60% over budget. So `cli.tsx` gains four
|
|
12
|
+
* imports and four `register*Command(...)` call sites instead. This is a rule
|
|
13
|
+
* for the NEW code, not a refactor of the old (which is out of scope).
|
|
14
|
+
*
|
|
15
|
+
* NOT `src/commands/` EITHER: that directory is the SLASH-command tree
|
|
16
|
+
* (`/help`, `/model`, `/save`, `/resume`), and it already owns the two names
|
|
17
|
+
* exec's session work sits closest to.
|
|
18
|
+
*/
|
|
19
|
+
import { Option } from 'commander';
|
|
20
|
+
import { runExec } from './index.js';
|
|
21
|
+
export function registerExecCommand(program, toFlags, deps) {
|
|
22
|
+
const exec = program
|
|
23
|
+
.command('exec')
|
|
24
|
+
.description('Headless, machine-facing run: JSON events, sessions, tool permissions, budgets')
|
|
25
|
+
.argument('[prompt]', 'The prompt (or use --prompt-file, or pipe it on stdin)')
|
|
26
|
+
.option('--output-format <fmt>', 'text | json | stream-json (default: text)')
|
|
27
|
+
.option('--input-format <fmt>', 'text | stream-json (needs --output-format stream-json)')
|
|
28
|
+
.option('--partial-messages', 'Emit text_delta events (stream-json only)')
|
|
29
|
+
.option('--include-thinking', 'Emit thinking events (stream-json only)')
|
|
30
|
+
.option('--prompt-file <path>', 'Read the prompt from a file')
|
|
31
|
+
.option('--session-id <id>', 'Create or resume a session by id')
|
|
32
|
+
.option('--resume <id|path>', 'Resume an existing session; fail if it is absent')
|
|
33
|
+
.option('-c, --continue', 'Resume the newest session for this working directory')
|
|
34
|
+
.option('--no-save-session', 'Run statelessly: write nothing to the sessions directory')
|
|
35
|
+
.option('--max-turns <n>', 'Stop after n assistant turns (exit 3)')
|
|
36
|
+
.option('--max-duration <ms>', 'Stop after ms of wall clock (exit 3). --idle-timeout still applies to a silent ' +
|
|
37
|
+
'stretch inside the run, and --tool-timeout to a single tool call')
|
|
38
|
+
.option('--permission-mode <m>', 'auto | plan | strict (default: auto)')
|
|
39
|
+
.option('--allow-tool <names>', 'Add tools to the baseline (repeatable; comma-separated accepted)', collect)
|
|
40
|
+
.option('--deny-tool <names>', 'Remove tools; wins over --allow-tool (repeatable; comma-separated accepted)', collect)
|
|
41
|
+
.option('--append-system-prompt <text>', 'Append text to the builtin system prompt')
|
|
42
|
+
.option('--append-system-prompt-file <path>', 'Append the contents of a file')
|
|
43
|
+
// HIDDEN, AND KEPT ONLY SO A GUESS IS NOT PUNISHED (P2-1 / D-16). The root
|
|
44
|
+
// command already carries `--tool-timeout` (a single tool call) and
|
|
45
|
+
// `--idle-timeout` (the engine watchdog); a third option called plainly
|
|
46
|
+
// `--timeout` would be the most generic of the three names attached to the
|
|
47
|
+
// newest and least familiar of them.
|
|
48
|
+
.addOption(new Option('--timeout <ms>', 'Alias of --max-duration').hideHelp());
|
|
49
|
+
exec.action(async (prompt, opts) => {
|
|
50
|
+
const rootOpts = program.opts();
|
|
51
|
+
process.exitCode = await runExec(toFlags(program.opts()), {
|
|
52
|
+
...opts,
|
|
53
|
+
// Hoisted from the root by commander, and read here because `exec` does
|
|
54
|
+
// not redeclare them. `--print` is REFUSED rather than ignored (P2-5).
|
|
55
|
+
...(rootOpts.quiet !== undefined ? { quiet: rootOpts.quiet } : {}),
|
|
56
|
+
...(rootOpts.print !== undefined ? { print: rootOpts.print } : {}),
|
|
57
|
+
}, prompt, { version: deps.version, makeController: deps.makeController });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Repeatable option accumulator. Comma splitting happens in `options.ts`. */
|
|
61
|
+
function collect(value, previous = []) {
|
|
62
|
+
return [...previous, value];
|
|
63
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three output faces of `aragon exec` (cli-integration-surface section 5.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* THE EMITTER IS THE ONLY OBJECT HANDED A STDOUT STREAM (R-2). Everything else
|
|
7
|
+
* on the exec path writes to stderr: migration notices, config-parse warnings,
|
|
8
|
+
* untrusted-skill notices, retry lines, `[usage]`, `[team]`, `[todo]` and every
|
|
9
|
+
* log record. One stray `process.stdout.write` corrupts every consumer, silently
|
|
10
|
+
* and only in the field, so the containment is structural rather than a rule
|
|
11
|
+
* anyone has to remember.
|
|
12
|
+
*
|
|
13
|
+
* `result()` IS IDEMPOTENT ON ALL THREE (AC-6). A `finally` and an error handler
|
|
14
|
+
* racing must not produce two `result` lines, and the second call being a no-op
|
|
15
|
+
* is what makes "exactly one, always last" true on every exit path including
|
|
16
|
+
* error, budget and signal - rather than true for the paths someone remembered.
|
|
17
|
+
*/
|
|
18
|
+
import type { ExecEvent, ExecInitParams, ExecResultEvent, ExecResultParams } from './events.js';
|
|
19
|
+
export interface ExecEmitter {
|
|
20
|
+
init(params: ExecInitParams): void;
|
|
21
|
+
/** Every non-`result` event. Ignored wholesale by `text` and `json`. */
|
|
22
|
+
emit(event: ExecEvent): void;
|
|
23
|
+
/** Exactly once per run; the second call is a no-op. */
|
|
24
|
+
result(params: ExecResultParams): void;
|
|
25
|
+
/** What `result()` actually wrote, or `null`. For tests and for `runExec`. */
|
|
26
|
+
emitted(): ExecResultEvent | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `--output-format text`: WRITES NOTHING (D-1).
|
|
30
|
+
*
|
|
31
|
+
* Text mode delegates to `runHeadless` VERBATIM - same controller, same
|
|
32
|
+
* `quiet`, same `followThrough` - and this object exists only so `runExec` has
|
|
33
|
+
* one shape to talk to. Reimplementing text rendering here would make "the human
|
|
34
|
+
* CLI is unchanged" an argument someone has to trust instead of a fact anyone
|
|
35
|
+
* can check, and the two implementations would drift the first time a retry line
|
|
36
|
+
* changed. AC-1 asserts byte equality on BOTH streams; this class is what makes
|
|
37
|
+
* it true by construction.
|
|
38
|
+
*/
|
|
39
|
+
export declare class TextEmitter implements ExecEmitter {
|
|
40
|
+
private done;
|
|
41
|
+
init(_params: ExecInitParams): void;
|
|
42
|
+
emit(_event: ExecEvent): void;
|
|
43
|
+
result(params: ExecResultParams): void;
|
|
44
|
+
emitted(): ExecResultEvent | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `--output-format json`: the `result` object alone, pretty-printed, on stdout.
|
|
48
|
+
*
|
|
49
|
+
* `aragon exec --output-format json "..." | jq -r .result` is the intended
|
|
50
|
+
* one-liner, which is why the intermediate events are dropped rather than
|
|
51
|
+
* buffered into an array: a wrapper that wants the stream asks for the stream.
|
|
52
|
+
*/
|
|
53
|
+
export declare class JsonEmitter implements ExecEmitter {
|
|
54
|
+
private readonly out;
|
|
55
|
+
private done;
|
|
56
|
+
constructor(out: NodeJS.WritableStream);
|
|
57
|
+
init(_params: ExecInitParams): void;
|
|
58
|
+
emit(_event: ExecEvent): void;
|
|
59
|
+
result(params: ExecResultParams): void;
|
|
60
|
+
emitted(): ExecResultEvent | null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* `--output-format stream-json`: one JSON object per line, `\n`-terminated, no
|
|
64
|
+
* pretty-printing, UTF-8.
|
|
65
|
+
*
|
|
66
|
+
* EVENTS AFTER `result` ARE DROPPED, not appended. "Exactly one `result`, always
|
|
67
|
+
* last" is the property consumers build their read loop around, and a late
|
|
68
|
+
* `tool_result` arriving from a torn-down subscription would break it in the
|
|
69
|
+
* rarest and least reproducible circumstances there are.
|
|
70
|
+
*/
|
|
71
|
+
export declare class StreamJsonEmitter implements ExecEmitter {
|
|
72
|
+
private readonly out;
|
|
73
|
+
private done;
|
|
74
|
+
constructor(out: NodeJS.WritableStream);
|
|
75
|
+
init(params: ExecInitParams): void;
|
|
76
|
+
emit(event: ExecEvent): void;
|
|
77
|
+
result(params: ExecResultParams): void;
|
|
78
|
+
emitted(): ExecResultEvent | null;
|
|
79
|
+
}
|
|
80
|
+
export declare function createEmitter(format: 'text' | 'json' | 'stream-json', out: NodeJS.WritableStream): ExecEmitter;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three output faces of `aragon exec` (cli-integration-surface section 5.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* THE EMITTER IS THE ONLY OBJECT HANDED A STDOUT STREAM (R-2). Everything else
|
|
7
|
+
* on the exec path writes to stderr: migration notices, config-parse warnings,
|
|
8
|
+
* untrusted-skill notices, retry lines, `[usage]`, `[team]`, `[todo]` and every
|
|
9
|
+
* log record. One stray `process.stdout.write` corrupts every consumer, silently
|
|
10
|
+
* and only in the field, so the containment is structural rather than a rule
|
|
11
|
+
* anyone has to remember.
|
|
12
|
+
*
|
|
13
|
+
* `result()` IS IDEMPOTENT ON ALL THREE (AC-6). A `finally` and an error handler
|
|
14
|
+
* racing must not produce two `result` lines, and the second call being a no-op
|
|
15
|
+
* is what makes "exactly one, always last" true on every exit path including
|
|
16
|
+
* error, budget and signal - rather than true for the paths someone remembered.
|
|
17
|
+
*/
|
|
18
|
+
import { buildInitEvent, buildResultEvent, toNdjsonLine } from './events.js';
|
|
19
|
+
/**
|
|
20
|
+
* `--output-format text`: WRITES NOTHING (D-1).
|
|
21
|
+
*
|
|
22
|
+
* Text mode delegates to `runHeadless` VERBATIM - same controller, same
|
|
23
|
+
* `quiet`, same `followThrough` - and this object exists only so `runExec` has
|
|
24
|
+
* one shape to talk to. Reimplementing text rendering here would make "the human
|
|
25
|
+
* CLI is unchanged" an argument someone has to trust instead of a fact anyone
|
|
26
|
+
* can check, and the two implementations would drift the first time a retry line
|
|
27
|
+
* changed. AC-1 asserts byte equality on BOTH streams; this class is what makes
|
|
28
|
+
* it true by construction.
|
|
29
|
+
*/
|
|
30
|
+
export class TextEmitter {
|
|
31
|
+
done = null;
|
|
32
|
+
// The parameters are DECLARED even though they are unused, so a caller
|
|
33
|
+
// holding a concrete `TextEmitter` can call it exactly as it calls the other
|
|
34
|
+
// two. A zero-arity method satisfies the interface and then makes the class
|
|
35
|
+
// unusable through its own type, which is the sort of asymmetry that turns
|
|
36
|
+
// into a cast at the call site.
|
|
37
|
+
init(_params) {
|
|
38
|
+
// Deliberately empty. `runHeadless` owns every byte of the text stream.
|
|
39
|
+
}
|
|
40
|
+
emit(_event) {
|
|
41
|
+
// Deliberately empty, same reason.
|
|
42
|
+
}
|
|
43
|
+
result(params) {
|
|
44
|
+
if (this.done)
|
|
45
|
+
return;
|
|
46
|
+
// RECORDED BUT NOT WRITTEN. `runExec` reads it for the exit code and the
|
|
47
|
+
// session bookkeeping; writing it would append a JSON line to prose that
|
|
48
|
+
// `aragon -p` does not produce.
|
|
49
|
+
this.done = buildResultEvent(params);
|
|
50
|
+
}
|
|
51
|
+
emitted() {
|
|
52
|
+
return this.done;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `--output-format json`: the `result` object alone, pretty-printed, on stdout.
|
|
57
|
+
*
|
|
58
|
+
* `aragon exec --output-format json "..." | jq -r .result` is the intended
|
|
59
|
+
* one-liner, which is why the intermediate events are dropped rather than
|
|
60
|
+
* buffered into an array: a wrapper that wants the stream asks for the stream.
|
|
61
|
+
*/
|
|
62
|
+
export class JsonEmitter {
|
|
63
|
+
out;
|
|
64
|
+
done = null;
|
|
65
|
+
constructor(out) {
|
|
66
|
+
this.out = out;
|
|
67
|
+
}
|
|
68
|
+
init(_params) {
|
|
69
|
+
// The init event is carried by `result.sessionId` / `result.model` for this
|
|
70
|
+
// format. Writing it here would put two objects on a stream documented to
|
|
71
|
+
// carry one.
|
|
72
|
+
}
|
|
73
|
+
emit(_event) {
|
|
74
|
+
// Dropped by design; see the class comment.
|
|
75
|
+
}
|
|
76
|
+
result(params) {
|
|
77
|
+
if (this.done)
|
|
78
|
+
return;
|
|
79
|
+
this.done = buildResultEvent(params);
|
|
80
|
+
this.out.write(`${JSON.stringify(this.done, null, 2)}\n`);
|
|
81
|
+
}
|
|
82
|
+
emitted() {
|
|
83
|
+
return this.done;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `--output-format stream-json`: one JSON object per line, `\n`-terminated, no
|
|
88
|
+
* pretty-printing, UTF-8.
|
|
89
|
+
*
|
|
90
|
+
* EVENTS AFTER `result` ARE DROPPED, not appended. "Exactly one `result`, always
|
|
91
|
+
* last" is the property consumers build their read loop around, and a late
|
|
92
|
+
* `tool_result` arriving from a torn-down subscription would break it in the
|
|
93
|
+
* rarest and least reproducible circumstances there are.
|
|
94
|
+
*/
|
|
95
|
+
export class StreamJsonEmitter {
|
|
96
|
+
out;
|
|
97
|
+
done = null;
|
|
98
|
+
constructor(out) {
|
|
99
|
+
this.out = out;
|
|
100
|
+
}
|
|
101
|
+
init(params) {
|
|
102
|
+
if (this.done)
|
|
103
|
+
return;
|
|
104
|
+
this.out.write(toNdjsonLine(buildInitEvent(params)));
|
|
105
|
+
}
|
|
106
|
+
emit(event) {
|
|
107
|
+
if (this.done)
|
|
108
|
+
return;
|
|
109
|
+
this.out.write(toNdjsonLine(event));
|
|
110
|
+
}
|
|
111
|
+
result(params) {
|
|
112
|
+
if (this.done)
|
|
113
|
+
return;
|
|
114
|
+
this.done = buildResultEvent(params);
|
|
115
|
+
this.out.write(toNdjsonLine(this.done));
|
|
116
|
+
}
|
|
117
|
+
emitted() {
|
|
118
|
+
return this.done;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function createEmitter(format, out) {
|
|
122
|
+
if (format === 'json')
|
|
123
|
+
return new JsonEmitter(out);
|
|
124
|
+
if (format === 'stream-json')
|
|
125
|
+
return new StreamJsonEmitter(out);
|
|
126
|
+
return new TextEmitter();
|
|
127
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CONTRACT (cli-integration-surface section 5.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope
|
|
5
|
+
* (`glyphs.test.ts::inScope`, R-3 / AC-25). Every string here can reach a
|
|
6
|
+
* consumer's terminal through `--output-format text` fallbacks or a stderr
|
|
7
|
+
* warning, and the whole point of the scanner is that a legacy `cmd.exe` shows
|
|
8
|
+
* mojibake for anything else.
|
|
9
|
+
*
|
|
10
|
+
* PURE TYPES AND BUILDERS. No I/O, no process access, no imports outside
|
|
11
|
+
* `node:`-free type space, so a consumer of this module cannot accidentally
|
|
12
|
+
* evaluate the runner.
|
|
13
|
+
*
|
|
14
|
+
* `EXEC_SCHEMA_VERSION` appears on `system` and on `result` and NOWHERE ELSE.
|
|
15
|
+
* ADDING A FIELD OR AN EVENT TYPE NEVER BUMPS IT; renaming or removing one does.
|
|
16
|
+
* That rule only holds if consumers keep their side of it, so the README states
|
|
17
|
+
* it in the same words: ignore event types you do not know, ignore fields you do
|
|
18
|
+
* not know. Without that contract the version would have to move for every
|
|
19
|
+
* additive change and every wrapper would pin an exact number forever.
|
|
20
|
+
*/
|
|
21
|
+
export declare const EXEC_SCHEMA_VERSION = 1;
|
|
22
|
+
/** The three baselines of section 4.2. `plan` is a MODE, never a filter. */
|
|
23
|
+
export type ExecPermissionMode = 'auto' | 'plan' | 'strict';
|
|
24
|
+
export type ExecOutputFormat = 'text' | 'json' | 'stream-json';
|
|
25
|
+
export type ExecInputFormat = 'text' | 'stream-json';
|
|
26
|
+
/**
|
|
27
|
+
* Why the run ended.
|
|
28
|
+
*
|
|
29
|
+
* `interrupted` covers ALL THREE signals (section 3.6): which one arrived is an
|
|
30
|
+
* operating-system fact, not an agent outcome, and a wrapper that wants to know
|
|
31
|
+
* reads `exitCode` - which follows the CLI's existing `128 + signo` convention.
|
|
32
|
+
*/
|
|
33
|
+
export type ExecStopReason = 'end_turn' | 'max_turns' | 'timeout' | 'interrupted' | 'error';
|
|
34
|
+
export interface ExecUsage {
|
|
35
|
+
inputTokens: number;
|
|
36
|
+
outputTokens: number;
|
|
37
|
+
totalTokens: number;
|
|
38
|
+
}
|
|
39
|
+
export interface ExecCost {
|
|
40
|
+
amount: number;
|
|
41
|
+
currency: 'USD';
|
|
42
|
+
/**
|
|
43
|
+
* `controller.isPricedModel(...)` (P2-3) - the method the CLI already uses for
|
|
44
|
+
* this question, not a second price table. When it is `false`, `amount` is `0`:
|
|
45
|
+
* a feature that looks free while it is spending money is worse than one that
|
|
46
|
+
* admits it does not know.
|
|
47
|
+
*/
|
|
48
|
+
known: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface ExecTodoCounts {
|
|
51
|
+
total: number;
|
|
52
|
+
done: number;
|
|
53
|
+
}
|
|
54
|
+
export interface ExecErrorInfo {
|
|
55
|
+
code: string;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
export interface ExecSystemInitEvent {
|
|
59
|
+
type: 'system';
|
|
60
|
+
subtype: 'init';
|
|
61
|
+
schemaVersion: number;
|
|
62
|
+
sessionId: string;
|
|
63
|
+
cli: string;
|
|
64
|
+
cwd: string;
|
|
65
|
+
startedAt: number;
|
|
66
|
+
model: {
|
|
67
|
+
provider: string;
|
|
68
|
+
id: string;
|
|
69
|
+
baseUrl: string | null;
|
|
70
|
+
};
|
|
71
|
+
permissionMode: ExecPermissionMode;
|
|
72
|
+
tools: string[];
|
|
73
|
+
resumed: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface ExecUserEvent {
|
|
76
|
+
type: 'user';
|
|
77
|
+
sessionId: string;
|
|
78
|
+
turn: number;
|
|
79
|
+
text: string;
|
|
80
|
+
/**
|
|
81
|
+
* `todo_continue` marks a message the CLI generated on the caller's behalf
|
|
82
|
+
* through the follow-through loop. A wrapper replaying a transcript has to be
|
|
83
|
+
* able to tell those apart from what it actually sent.
|
|
84
|
+
*/
|
|
85
|
+
source: 'caller' | 'todo_continue';
|
|
86
|
+
}
|
|
87
|
+
export interface ExecTextDeltaEvent {
|
|
88
|
+
type: 'text_delta';
|
|
89
|
+
sessionId: string;
|
|
90
|
+
turn: number;
|
|
91
|
+
delta: string;
|
|
92
|
+
}
|
|
93
|
+
export interface ExecThinkingEvent {
|
|
94
|
+
type: 'thinking';
|
|
95
|
+
sessionId: string;
|
|
96
|
+
turn: number;
|
|
97
|
+
text: string;
|
|
98
|
+
}
|
|
99
|
+
export interface ExecAssistantEvent {
|
|
100
|
+
type: 'assistant';
|
|
101
|
+
sessionId: string;
|
|
102
|
+
turn: number;
|
|
103
|
+
text: string;
|
|
104
|
+
}
|
|
105
|
+
export interface ExecToolCallEvent {
|
|
106
|
+
type: 'tool_call';
|
|
107
|
+
sessionId: string;
|
|
108
|
+
turn: number;
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
input: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
export interface ExecToolResultEvent {
|
|
114
|
+
type: 'tool_result';
|
|
115
|
+
sessionId: string;
|
|
116
|
+
turn: number;
|
|
117
|
+
id: string;
|
|
118
|
+
name: string;
|
|
119
|
+
isError: boolean;
|
|
120
|
+
durationMs: number;
|
|
121
|
+
/**
|
|
122
|
+
* NOT TRUNCATED A SECOND TIME (AC-26). The core executor already clips at
|
|
123
|
+
* roughly 100 KB, and this event carries what the MODEL saw - the only
|
|
124
|
+
* faithful choice. A consumer therefore needs a line reader without a small
|
|
125
|
+
* buffer cap, and the README says so.
|
|
126
|
+
*/
|
|
127
|
+
output: string;
|
|
128
|
+
}
|
|
129
|
+
export interface ExecTodoEvent {
|
|
130
|
+
type: 'todo';
|
|
131
|
+
sessionId: string;
|
|
132
|
+
total: number;
|
|
133
|
+
done: number;
|
|
134
|
+
activeIndex: number;
|
|
135
|
+
items: {
|
|
136
|
+
content: string;
|
|
137
|
+
status: 'pending' | 'in_progress' | 'completed';
|
|
138
|
+
}[];
|
|
139
|
+
}
|
|
140
|
+
export interface ExecTeamEvent {
|
|
141
|
+
type: 'team';
|
|
142
|
+
sessionId: string;
|
|
143
|
+
subtype: 'dispatch_start' | 'agent_update' | 'dispatch_end';
|
|
144
|
+
label?: string;
|
|
145
|
+
phase?: string;
|
|
146
|
+
description?: string;
|
|
147
|
+
durationMs?: number;
|
|
148
|
+
toolCalls?: number;
|
|
149
|
+
ok?: number;
|
|
150
|
+
total?: number;
|
|
151
|
+
aborted?: boolean;
|
|
152
|
+
}
|
|
153
|
+
export interface ExecFastReviewEvent {
|
|
154
|
+
type: 'fast_review';
|
|
155
|
+
sessionId: string;
|
|
156
|
+
turn: number;
|
|
157
|
+
model: string;
|
|
158
|
+
text: string;
|
|
159
|
+
}
|
|
160
|
+
export interface ExecRetryEvent {
|
|
161
|
+
type: 'retry';
|
|
162
|
+
sessionId: string;
|
|
163
|
+
attempt: number;
|
|
164
|
+
maxRetries: number;
|
|
165
|
+
errorType: string;
|
|
166
|
+
delayMs: number;
|
|
167
|
+
}
|
|
168
|
+
export interface ExecErrorEvent {
|
|
169
|
+
type: 'error';
|
|
170
|
+
sessionId: string;
|
|
171
|
+
/** `false` for a line the run recovered from (a malformed stdin line, say). */
|
|
172
|
+
fatal: boolean;
|
|
173
|
+
code: string;
|
|
174
|
+
message: string;
|
|
175
|
+
}
|
|
176
|
+
export interface ExecResultEvent {
|
|
177
|
+
type: 'result';
|
|
178
|
+
schemaVersion: number;
|
|
179
|
+
sessionId: string;
|
|
180
|
+
isError: boolean;
|
|
181
|
+
stopReason: ExecStopReason;
|
|
182
|
+
/**
|
|
183
|
+
* `0` success | `1` agent error | `2` usage/config | `3` budget |
|
|
184
|
+
* `128 + signo` for a signal (130 SIGINT, 143 SIGTERM, 129 SIGHUP).
|
|
185
|
+
*
|
|
186
|
+
* DELIBERATELY `number`, NOT A UNION (P1-11): `logging/install.ts` holds
|
|
187
|
+
* `SIGNAL_NUMBERS = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1 }` and exits
|
|
188
|
+
* `128 + signo`, so the signal values are open-ended and a union that omitted
|
|
189
|
+
* 143 would make the honest value unrepresentable.
|
|
190
|
+
*/
|
|
191
|
+
exitCode: number;
|
|
192
|
+
/** The final assistant text; `''` when there was none. */
|
|
193
|
+
result: string;
|
|
194
|
+
turns: number;
|
|
195
|
+
durationMs: number;
|
|
196
|
+
usage: ExecUsage;
|
|
197
|
+
cost: ExecCost;
|
|
198
|
+
model: {
|
|
199
|
+
provider: string;
|
|
200
|
+
id: string;
|
|
201
|
+
};
|
|
202
|
+
todos: ExecTodoCounts | null;
|
|
203
|
+
error: ExecErrorInfo | null;
|
|
204
|
+
}
|
|
205
|
+
export type ExecEvent = ExecSystemInitEvent | ExecUserEvent | ExecTextDeltaEvent | ExecThinkingEvent | ExecAssistantEvent | ExecToolCallEvent | ExecToolResultEvent | ExecTodoEvent | ExecTeamEvent | ExecFastReviewEvent | ExecRetryEvent | ExecErrorEvent | ExecResultEvent;
|
|
206
|
+
/** Everything `system/init` needs that the runner cannot derive for itself. */
|
|
207
|
+
export interface ExecInitParams {
|
|
208
|
+
sessionId: string;
|
|
209
|
+
cli: string;
|
|
210
|
+
cwd: string;
|
|
211
|
+
startedAt: number;
|
|
212
|
+
model: {
|
|
213
|
+
provider: string;
|
|
214
|
+
id: string;
|
|
215
|
+
baseUrl: string | null;
|
|
216
|
+
};
|
|
217
|
+
permissionMode: ExecPermissionMode;
|
|
218
|
+
tools: string[];
|
|
219
|
+
resumed: boolean;
|
|
220
|
+
}
|
|
221
|
+
export declare function buildInitEvent(params: ExecInitParams): ExecSystemInitEvent;
|
|
222
|
+
/** Everything `result` needs. `schemaVersion` is stamped here, never by a caller. */
|
|
223
|
+
export type ExecResultParams = Omit<ExecResultEvent, 'type' | 'schemaVersion'>;
|
|
224
|
+
export declare function buildResultEvent(params: ExecResultParams): ExecResultEvent;
|
|
225
|
+
/** Serialize one event as an NDJSON line, terminator included. */
|
|
226
|
+
export declare function toNdjsonLine(event: ExecEvent): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CONTRACT (cli-integration-surface section 5.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope
|
|
5
|
+
* (`glyphs.test.ts::inScope`, R-3 / AC-25). Every string here can reach a
|
|
6
|
+
* consumer's terminal through `--output-format text` fallbacks or a stderr
|
|
7
|
+
* warning, and the whole point of the scanner is that a legacy `cmd.exe` shows
|
|
8
|
+
* mojibake for anything else.
|
|
9
|
+
*
|
|
10
|
+
* PURE TYPES AND BUILDERS. No I/O, no process access, no imports outside
|
|
11
|
+
* `node:`-free type space, so a consumer of this module cannot accidentally
|
|
12
|
+
* evaluate the runner.
|
|
13
|
+
*
|
|
14
|
+
* `EXEC_SCHEMA_VERSION` appears on `system` and on `result` and NOWHERE ELSE.
|
|
15
|
+
* ADDING A FIELD OR AN EVENT TYPE NEVER BUMPS IT; renaming or removing one does.
|
|
16
|
+
* That rule only holds if consumers keep their side of it, so the README states
|
|
17
|
+
* it in the same words: ignore event types you do not know, ignore fields you do
|
|
18
|
+
* not know. Without that contract the version would have to move for every
|
|
19
|
+
* additive change and every wrapper would pin an exact number forever.
|
|
20
|
+
*/
|
|
21
|
+
export const EXEC_SCHEMA_VERSION = 1;
|
|
22
|
+
export function buildInitEvent(params) {
|
|
23
|
+
return {
|
|
24
|
+
type: 'system',
|
|
25
|
+
subtype: 'init',
|
|
26
|
+
schemaVersion: EXEC_SCHEMA_VERSION,
|
|
27
|
+
sessionId: params.sessionId,
|
|
28
|
+
cli: params.cli,
|
|
29
|
+
cwd: params.cwd,
|
|
30
|
+
startedAt: params.startedAt,
|
|
31
|
+
model: params.model,
|
|
32
|
+
permissionMode: params.permissionMode,
|
|
33
|
+
tools: params.tools,
|
|
34
|
+
resumed: params.resumed,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function buildResultEvent(params) {
|
|
38
|
+
return { type: 'result', schemaVersion: EXEC_SCHEMA_VERSION, ...params };
|
|
39
|
+
}
|
|
40
|
+
/** Serialize one event as an NDJSON line, terminator included. */
|
|
41
|
+
export function toNdjsonLine(event) {
|
|
42
|
+
return `${JSON.stringify(event)}\n`;
|
|
43
|
+
}
|