@osdlc/agent-call 0.0.1-stage → 0.4.0

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.
@@ -0,0 +1,21 @@
1
+ import type { AgentAdapter } from './adapters/types.js';
2
+ import type { AgentName } from './spec.js';
3
+ /** Instantiate a built-in adapter by name. */
4
+ export declare function adapterFor(name: AgentName, options?: {
5
+ binary?: string;
6
+ }): AgentAdapter;
7
+ /** The names `adapterFor` accepts, in a fixed order. Frozen at run time. */
8
+ export declare const BUILTIN_AGENTS: readonly AgentName[];
9
+ /** Whether one agent CLI is installed, as `detectAgents` reports it. */
10
+ export interface AgentAvailability {
11
+ /** The adapter's name. */
12
+ name: string;
13
+ /** `<binary> --version` ran, exited 0 and finished within 10 seconds. */
14
+ available: boolean;
15
+ /** First line of the `--version` output, when available. */
16
+ version?: string | undefined;
17
+ }
18
+ /** Probe which agent CLIs are installed (via `<binary> --version`). */
19
+ export declare function detectAgents(adapters?: AgentAdapter[]): Promise<AgentAvailability[]>;
20
+ /** @internal `timeoutMs` is a parameter so a test can shorten it. */
21
+ export declare function probeVersion(binary: string, timeoutMs?: number): Promise<string | undefined>;
@@ -0,0 +1,41 @@
1
+ import { claudeAdapter } from './adapters/claude.js';
2
+ import { codexAdapter } from './adapters/codex.js';
3
+ import { antigravityAdapter } from './adapters/antigravity.js';
4
+ import { opencodeAdapter } from './adapters/opencode.js';
5
+ import { spawnCollect } from './spawn.js';
6
+ /** Instantiate a built-in adapter by name. */
7
+ export function adapterFor(name, options = {}) {
8
+ switch (name) {
9
+ case 'claude':
10
+ return claudeAdapter(options);
11
+ case 'codex':
12
+ return codexAdapter(options);
13
+ case 'antigravity':
14
+ return antigravityAdapter(options);
15
+ case 'opencode':
16
+ return opencodeAdapter(options);
17
+ }
18
+ }
19
+ /** The names `adapterFor` accepts, in a fixed order. Frozen at run time. */
20
+ export const BUILTIN_AGENTS = Object.freeze([
21
+ 'claude',
22
+ 'codex',
23
+ 'antigravity',
24
+ 'opencode',
25
+ ]);
26
+ /** Probe which agent CLIs are installed (via `<binary> --version`). */
27
+ export async function detectAgents(adapters = BUILTIN_AGENTS.map((name) => adapterFor(name))) {
28
+ return Promise.all(adapters.map(async (adapter) => {
29
+ const version = await probeVersion(adapter.binary);
30
+ return {
31
+ name: adapter.name,
32
+ available: version !== undefined,
33
+ version,
34
+ };
35
+ }));
36
+ }
37
+ /** @internal `timeoutMs` is a parameter so a test can shorten it. */
38
+ export async function probeVersion(binary, timeoutMs = 10_000) {
39
+ const raw = await spawnCollect([binary, '--version'], { env: process.env, timeoutMs });
40
+ return raw.exitCode === 0 && !raw.timedOut ? raw.stdout.trim().split('\n')[0] : undefined;
41
+ }
package/dist/run.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { AgentAdapter, ParsedOutput, RawProcessOutput } from './adapters/types.js';
2
+ import type { Conversation } from './conversation.js';
3
+ import { type PlannedCall } from './plan.js';
4
+ import type { AgentCallSpec } from './spec.js';
5
+ /**
6
+ * Why a run failed, on `AgentResult.error`. `callAgent` returns it instead of
7
+ * throwing.
8
+ *
9
+ * `reason` is open: a later minor release may add reasons. Handle the ones you
10
+ * know and keep a default branch for the rest.
11
+ */
12
+ export interface CallError {
13
+ /**
14
+ * - `spawn-failed` — the process never started (binary missing, empty argv).
15
+ * - `timeout` — `timeoutMs` elapsed or the caller aborted.
16
+ * - `nonzero-exit` — the process exited with a nonzero code, or ended on a
17
+ * signal the runner did not send (such as an out-of-memory kill); `detail`
18
+ * names the signal.
19
+ * - `agent-error` — the process exited 0 but the agent reported failure, or
20
+ * the adapter's parser threw on the output.
21
+ */
22
+ reason: 'spawn-failed' | 'timeout' | 'nonzero-exit' | 'agent-error';
23
+ /** A human-readable explanation, taken from the agent's output where there is one. */
24
+ detail: string;
25
+ }
26
+ /** Everything a run produced: normalized fields plus the untouched raw output. */
27
+ export interface AgentResult extends ParsedOutput {
28
+ ok: boolean;
29
+ exitCode: number | null;
30
+ durationMs: number;
31
+ error?: CallError | undefined;
32
+ /** The exact invocation that ran — argv, env additions, notes, spec. */
33
+ planned: PlannedCall;
34
+ /** Raw process output, always preserved. */
35
+ raw: RawProcessOutput;
36
+ /** Normalized transcript, when `captureConversation` was requested. */
37
+ conversation?: Conversation | undefined;
38
+ }
39
+ /** Per-run options for `callAgent`, separate from the spec. */
40
+ export interface CallOptions {
41
+ /** Also lift the transcript into the normalized conversation model. */
42
+ captureConversation?: boolean | undefined;
43
+ /** Abort the run early: kills the child, or skips the spawn if already aborted. */
44
+ signal?: AbortSignal | undefined;
45
+ }
46
+ /**
47
+ * Plan a call without running it: the exact argv, stdin, env, temp files,
48
+ * and capability notes the adapter would use. Pure and side-effect free.
49
+ *
50
+ * @throws Whatever the adapter's `plan` throws: see `templateAdapter`'s
51
+ * `@throws`; and `customAdapter` lets its `args` function throw.
52
+ */
53
+ export declare function planCall(adapter: AgentAdapter, spec: AgentCallSpec): PlannedCall;
54
+ /**
55
+ * Translate the spec through the adapter, run the CLI, normalize the result.
56
+ * Never throws on agent failure: a failed run returns `ok: false` with an
57
+ * `error`.
58
+ *
59
+ * Runs the child in its own process group and kills the group on timeout or
60
+ * abort, so it needs POSIX process groups (Linux, macOS). The child inherits
61
+ * the parent's environment plus `spec.env`.
62
+ *
63
+ * Because the agent has its own process group, a terminal Ctrl+C or a kill of
64
+ * the host does not reach it, and it keeps running. A host that can be
65
+ * interrupted must pass a `signal` and abort it from its own SIGINT/SIGTERM
66
+ * handler. The runner installs no exit handlers.
67
+ *
68
+ * @throws The returned promise rejects, rather than resolving `ok: false`,
69
+ * when planning throws (see {@link planCall}), when a `TempFile.name` is not a
70
+ * plain file name (empty, or holding `/`, `\` or `..`), or when the per-call
71
+ * temp directory cannot be created or written. A directory that was made is
72
+ * removed. A parser that throws on the output does not reject: the result is
73
+ * `ok: false` with reason `agent-error`.
74
+ */
75
+ export declare function callAgent(adapter: AgentAdapter, spec: AgentCallSpec, options?: CallOptions): Promise<AgentResult>;
package/dist/run.js ADDED
@@ -0,0 +1,172 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { TMP_TOKEN } from './plan.js';
5
+ import { spawnCollect } from './spawn.js';
6
+ /**
7
+ * Plan a call without running it: the exact argv, stdin, env, temp files,
8
+ * and capability notes the adapter would use. Pure and side-effect free.
9
+ *
10
+ * @throws Whatever the adapter's `plan` throws: see `templateAdapter`'s
11
+ * `@throws`; and `customAdapter` lets its `args` function throw.
12
+ */
13
+ export function planCall(adapter, spec) {
14
+ return adapter.plan(spec);
15
+ }
16
+ /**
17
+ * Translate the spec through the adapter, run the CLI, normalize the result.
18
+ * Never throws on agent failure: a failed run returns `ok: false` with an
19
+ * `error`.
20
+ *
21
+ * Runs the child in its own process group and kills the group on timeout or
22
+ * abort, so it needs POSIX process groups (Linux, macOS). The child inherits
23
+ * the parent's environment plus `spec.env`.
24
+ *
25
+ * Because the agent has its own process group, a terminal Ctrl+C or a kill of
26
+ * the host does not reach it, and it keeps running. A host that can be
27
+ * interrupted must pass a `signal` and abort it from its own SIGINT/SIGTERM
28
+ * handler. The runner installs no exit handlers.
29
+ *
30
+ * @throws The returned promise rejects, rather than resolving `ok: false`,
31
+ * when planning throws (see {@link planCall}), when a `TempFile.name` is not a
32
+ * plain file name (empty, or holding `/`, `\` or `..`), or when the per-call
33
+ * temp directory cannot be created or written. A directory that was made is
34
+ * removed. A parser that throws on the output does not reject: the result is
35
+ * `ok: false` with reason `agent-error`.
36
+ */
37
+ export async function callAgent(adapter, spec, options = {}) {
38
+ const planned = adapter.plan(spec);
39
+ const raw = await execute(planned, spec, options.signal);
40
+ let error;
41
+ if (raw.exitCode === null && !raw.timedOut) {
42
+ error =
43
+ raw.signal === undefined
44
+ ? { reason: 'spawn-failed', detail: raw.stderr.trim() }
45
+ : {
46
+ reason: 'nonzero-exit',
47
+ detail: `${planned.argv[0]} was killed by signal ${raw.signal}: ${raw.stderr.trim().slice(0, 500)}`,
48
+ };
49
+ }
50
+ else if (raw.timedOut) {
51
+ error = {
52
+ reason: 'timeout',
53
+ detail: raw.cutShortBy === 'abort'
54
+ ? 'aborted by the caller'
55
+ : `killed after ${spec.timeoutMs ?? 0}ms`,
56
+ };
57
+ }
58
+ let parsed = { text: raw.stdout.trim() };
59
+ if (error === undefined) {
60
+ try {
61
+ parsed = adapter.parseResult(planned, raw);
62
+ }
63
+ catch (err) {
64
+ error = {
65
+ reason: 'agent-error',
66
+ detail: `the adapter's parser threw: ${errorMessage(err)}`,
67
+ };
68
+ }
69
+ }
70
+ if (error === undefined && raw.exitCode !== 0) {
71
+ const stderrTail = raw.stderr.trim().slice(0, 500);
72
+ error = {
73
+ reason: 'nonzero-exit',
74
+ detail: [
75
+ parsed.errorDetail,
76
+ stderrTail === ''
77
+ ? `${planned.argv[0]} exited ${raw.exitCode}`
78
+ : `${planned.argv[0]} exited ${raw.exitCode}: ${stderrTail}`,
79
+ ]
80
+ .filter((part) => part !== undefined && part !== '')
81
+ .join('; '),
82
+ };
83
+ }
84
+ else if (error === undefined && parsed.isError === true) {
85
+ error = {
86
+ reason: 'agent-error',
87
+ detail: parsed.errorDetail ?? 'the agent reported an error',
88
+ };
89
+ }
90
+ const result = {
91
+ ...parsed,
92
+ ok: error === undefined,
93
+ exitCode: raw.exitCode,
94
+ durationMs: raw.durationMs,
95
+ error,
96
+ planned,
97
+ raw,
98
+ };
99
+ if (options.captureConversation === true && adapter.toConversation) {
100
+ try {
101
+ result.conversation = adapter.toConversation(planned, raw);
102
+ }
103
+ catch (err) {
104
+ result.ok = false;
105
+ result.error ??= {
106
+ reason: 'agent-error',
107
+ detail: `the adapter's parser threw: ${errorMessage(err)}`,
108
+ };
109
+ }
110
+ }
111
+ return result;
112
+ }
113
+ function isPlainFileName(name) {
114
+ return name !== '' && !name.includes('/') && !name.includes('\\') && !name.includes('..');
115
+ }
116
+ function errorMessage(err) {
117
+ return err instanceof Error ? err.message : String(err);
118
+ }
119
+ async function execute(planned, spec, signal) {
120
+ for (const file of planned.tempFiles) {
121
+ if (!isPlainFileName(file.name)) {
122
+ throw new Error(`temp file name ${JSON.stringify(file.name)} is not a plain file name`);
123
+ }
124
+ }
125
+ let tempDir;
126
+ if (planned.tempFiles.length > 0) {
127
+ tempDir = await mkdtemp(join(tmpdir(), 'agent-call-'));
128
+ }
129
+ try {
130
+ if (tempDir !== undefined) {
131
+ for (const file of planned.tempFiles) {
132
+ await writeFile(join(tempDir, file.name), file.content, 'utf8');
133
+ }
134
+ }
135
+ const resolveTmp = (value) => tempDir === undefined ? value : value.replaceAll(TMP_TOKEN, tempDir);
136
+ const argv = planned.argv.map(resolveTmp);
137
+ const env = { ...process.env };
138
+ for (const [key, value] of Object.entries(planned.env)) {
139
+ env[key] = resolveTmp(value);
140
+ }
141
+ const startedAt = Date.now();
142
+ const raw = await spawnCollect(argv, {
143
+ stdin: planned.stdin,
144
+ cwd: planned.cwd,
145
+ env,
146
+ timeoutMs: spec.timeoutMs,
147
+ signal,
148
+ });
149
+ raw.durationMs = Date.now() - startedAt;
150
+ if (tempDir !== undefined) {
151
+ const files = {};
152
+ for (const file of planned.tempFiles) {
153
+ if (file.readBack !== true)
154
+ continue;
155
+ try {
156
+ files[file.name] = await readFile(join(tempDir, file.name), 'utf8');
157
+ }
158
+ catch {
159
+ // The CLI may not have written it (e.g. errored before finishing).
160
+ }
161
+ }
162
+ if (Object.keys(files).length > 0)
163
+ raw.files = files;
164
+ }
165
+ return raw;
166
+ }
167
+ finally {
168
+ if (tempDir !== undefined) {
169
+ await rm(tempDir, { recursive: true, force: true });
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,10 @@
1
+ import type { RawProcessOutput } from './adapters/types.js';
2
+ export interface SpawnOptions {
3
+ stdin?: string | undefined;
4
+ cwd?: string | undefined;
5
+ env: NodeJS.ProcessEnv;
6
+ timeoutMs?: number | undefined;
7
+ signal?: AbortSignal | undefined;
8
+ }
9
+ /** Spawn, collect output, honor timeout and abort. @internal */
10
+ export declare function spawnCollect(argv: string[], options: SpawnOptions): Promise<RawProcessOutput>;
package/dist/spawn.js ADDED
@@ -0,0 +1,143 @@
1
+ import { spawn } from 'node:child_process';
2
+ /** Grace period between SIGTERM and SIGKILL on timeout/abort. */
3
+ const KILL_GRACE_MS = 5_000;
4
+ /** Spawn, collect output, honor timeout and abort. @internal */
5
+ export function spawnCollect(argv, options) {
6
+ return new Promise((resolve) => {
7
+ const [binary, ...args] = argv;
8
+ if (binary === undefined) {
9
+ resolve({
10
+ stdout: '',
11
+ stderr: 'empty argv',
12
+ exitCode: null,
13
+ durationMs: 0,
14
+ timedOut: false,
15
+ });
16
+ return;
17
+ }
18
+ // An already-aborted signal never fires `abort` again, so it has to be
19
+ // honored before spawn — otherwise the agent runs the full turn. The
20
+ // result mirrors a mid-run abort: cut short, with no exit code.
21
+ if (options.signal?.aborted === true) {
22
+ resolve({
23
+ stdout: '',
24
+ stderr: '',
25
+ exitCode: null,
26
+ durationMs: 0,
27
+ timedOut: true,
28
+ cutShortBy: 'abort',
29
+ });
30
+ return;
31
+ }
32
+ // Raw `node:child_process`, not the command seam: the package stays
33
+ // dependency-free and this manages a long-lived process group (D-07).
34
+ const child = spawn(binary, args, {
35
+ cwd: options.cwd,
36
+ env: options.env,
37
+ stdio: ['pipe', 'pipe', 'pipe'],
38
+ // Own process group, so a timeout/abort can kill the whole tree. Without
39
+ // it, a shell child (e.g. `sh -c "sleep 30"`) leaves its grandchild
40
+ // orphaned holding the stdio pipe open, and `close` never fires — the
41
+ // call hangs until the real command ends instead of honoring timeoutMs.
42
+ detached: true,
43
+ });
44
+ let stdout = '';
45
+ let stderr = '';
46
+ let timedOut = false;
47
+ let cutShortBy;
48
+ let stdinError;
49
+ let settled = false;
50
+ let killTimer;
51
+ let graceTimer;
52
+ const killTree = (signal) => {
53
+ const pid = child.pid;
54
+ try {
55
+ // Negative pid targets the whole process group (child is a group
56
+ // leader via `detached`), so grandchildren die too and release the
57
+ // stdio pipe. Fall back to a direct kill if the group is already gone
58
+ // or the platform lacks process groups.
59
+ if (pid !== undefined)
60
+ process.kill(-pid, signal);
61
+ else
62
+ child.kill(signal);
63
+ }
64
+ catch {
65
+ try {
66
+ child.kill(signal);
67
+ }
68
+ catch {
69
+ /* already exited */
70
+ }
71
+ }
72
+ };
73
+ const terminate = () => {
74
+ if (graceTimer !== undefined)
75
+ return;
76
+ killTree('SIGTERM');
77
+ graceTimer = setTimeout(() => killTree('SIGKILL'), KILL_GRACE_MS);
78
+ };
79
+ if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
80
+ killTimer = setTimeout(() => {
81
+ timedOut = true;
82
+ cutShortBy ??= 'timeout';
83
+ terminate();
84
+ }, options.timeoutMs);
85
+ }
86
+ const onAbort = () => {
87
+ timedOut = true;
88
+ cutShortBy ??= 'abort';
89
+ terminate();
90
+ };
91
+ options.signal?.addEventListener('abort', onAbort, { once: true });
92
+ const settle = (exitCode, spawnError, signal) => {
93
+ if (settled)
94
+ return;
95
+ settled = true;
96
+ if (killTimer !== undefined)
97
+ clearTimeout(killTimer);
98
+ if (graceTimer !== undefined)
99
+ clearTimeout(graceTimer);
100
+ options.signal?.removeEventListener('abort', onAbort);
101
+ if (spawnError !== undefined && stderr === '') {
102
+ stderr = spawnError.message;
103
+ }
104
+ resolve({
105
+ stdout,
106
+ stderr,
107
+ exitCode,
108
+ durationMs: 0,
109
+ timedOut,
110
+ cutShortBy,
111
+ stdinError,
112
+ signal: signal ?? undefined,
113
+ });
114
+ };
115
+ // Decode through the stream, not per chunk: a UTF-8 sequence straddling
116
+ // two reads must be held over the boundary instead of becoming two U+FFFD.
117
+ child.stdout.setEncoding('utf8');
118
+ child.stderr.setEncoding('utf8');
119
+ child.stdout.on('data', (chunk) => {
120
+ stdout += chunk;
121
+ });
122
+ child.stderr.on('data', (chunk) => {
123
+ stderr += chunk;
124
+ });
125
+ child.on('error', (err) => settle(null, err));
126
+ child.on('close', (code, signal) => settle(code, undefined, signal));
127
+ // The child may exit before draining stdin (bad flag, auth failure, or a
128
+ // kill from the timeout/abort path), which fails the write with EPIPE.
129
+ // Without a listener that stream error is an uncaught exception that takes
130
+ // the host process down. It is recorded rather than dropped: the failure
131
+ // means the agent saw a truncated prompt, and nothing else reveals that
132
+ // when the child still exits 0. Recorded at `settle` so the diagnostic
133
+ // rides on the resolved value; node closes stdin before emitting `close`,
134
+ // so the error always lands first.
135
+ child.stdin.on('error', (err) => {
136
+ stdinError ??= err.message;
137
+ });
138
+ if (options.stdin !== undefined) {
139
+ child.stdin.write(options.stdin);
140
+ }
141
+ child.stdin.end();
142
+ });
143
+ }
package/dist/spec.d.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The canonical, agent-agnostic description of one headless agent call.
3
+ *
4
+ * A spec says WHAT you want — prompt, permission posture, context, output
5
+ * shape, session behavior — never HOW a particular CLI expresses it. Each
6
+ * built-in adapter translates the spec into a concrete invocation on a
7
+ * best-effort basis and records every approximation as a `CapabilityNote` (see
8
+ * plan.ts). Custom and template adapters note the fields they fold into the prompt and
9
+ * `permissions.mode`; any other field the `args` builder leaves out is not noted.
10
+ */
11
+ import type { EffortLevel } from './models.js';
12
+ /** Adapters shipped with this package. Custom adapters may use any name. */
13
+ export type AgentName = 'claude' | 'codex' | 'antigravity' | 'opencode';
14
+ /**
15
+ * Canonical permission posture, ordered from most to least restricted.
16
+ *
17
+ * - `read-only` — the agent may inspect but not mutate anything.
18
+ * - `default` — whatever the agent's own default posture is.
19
+ * - `edits` — file edits are pre-approved; shell/network still gated
20
+ * (gated means denied under headless runs — nothing prompts).
21
+ * - `full-auto` — edits and workspace-scoped commands pre-approved, inside
22
+ * whatever sandbox the agent offers.
23
+ * - `bypass` — skip all approvals and sandboxing. Dangerous; intended for
24
+ * externally sandboxed environments only.
25
+ */
26
+ export type PermissionMode = 'read-only' | 'default' | 'edits' | 'full-auto' | 'bypass';
27
+ /** Permission posture for a call: a canonical mode, plus per-tool rules where the CLI has them. */
28
+ export interface PermissionSpec {
29
+ mode?: PermissionMode | undefined;
30
+ /**
31
+ * Tool patterns to pre-approve. Passed through verbatim where the agent
32
+ * supports tool rules (claude's `--allowedTools "Bash(git *) Edit"`);
33
+ * recorded as dropped by the other built-in adapters.
34
+ */
35
+ allowTools?: string[] | undefined;
36
+ /** Tool patterns to deny. Same passthrough semantics as `allowTools`. */
37
+ denyTools?: string[] | undefined;
38
+ }
39
+ /** What the agent should know about beyond the prompt: files, directories, free text. */
40
+ export interface ContextSpec {
41
+ /**
42
+ * Files the agent should consider. Attached natively where the CLI supports
43
+ * it (opencode `-f`), otherwise emulated with a prompt preamble that lists
44
+ * the paths (and `@path` references for agents with @-inclusion syntax).
45
+ */
46
+ files?: string[] | undefined;
47
+ /** Extra directories the agent may access (`--add-dir` and friends). */
48
+ dirs?: string[] | undefined;
49
+ /** Freeform context prepended to the prompt on every agent. */
50
+ text?: string | undefined;
51
+ }
52
+ /** The shape of the CLI's output; see {@link OutputSpec.format}. */
53
+ export type OutputFormat = 'text' | 'json' | 'stream-json';
54
+ /** The output shape a call asks for: a format, and optionally a JSON Schema for the answer. */
55
+ export interface OutputSpec {
56
+ /**
57
+ * - `text` — final answer as plain text.
58
+ * - `json` — one machine-readable envelope with metadata.
59
+ * - `stream-json` — line-delimited JSON events as the run progresses.
60
+ *
61
+ * When omitted, adapters default to their richest aggregate format so
62
+ * usage/cost/session metadata can be captured; `AgentResult.text` always
63
+ * carries the final answer regardless.
64
+ */
65
+ format?: OutputFormat | undefined;
66
+ /**
67
+ * JSON Schema the final answer must satisfy. Native where supported
68
+ * (claude `--json-schema`, codex `--output-schema`), otherwise emulated by
69
+ * appending a strict respond-only-with-JSON instruction to the prompt.
70
+ * The parsed value lands in `AgentResult.structured`.
71
+ */
72
+ schema?: Record<string, unknown> | undefined;
73
+ }
74
+ /** Session behaviour: pin an id, resume, continue the last session, or keep the run off disk. */
75
+ export interface SessionSpec {
76
+ /** Pin the new session's id (claude only). */
77
+ id?: string | undefined;
78
+ /** Resume an existing session/thread by id. */
79
+ resume?: string | undefined;
80
+ /** Resume the most recent session instead of naming one. */
81
+ continueLast?: boolean | undefined;
82
+ /** Set false to keep the run off disk where supported (default: agent's own). */
83
+ persist?: boolean | undefined;
84
+ }
85
+ /** MCP servers to expose to the agent for this call. */
86
+ export interface McpSpec {
87
+ /**
88
+ * MCP servers to expose to the agent, in the common `mcpServers` config
89
+ * shape: name → `{command, args?, env?}` or `{url}`. Native on claude
90
+ * (`--mcp-config`), rendered to `-c mcp_servers.*` overrides on codex,
91
+ * config-file-only (dropped, with a note) on antigravity/opencode.
92
+ */
93
+ servers?: Record<string, Record<string, unknown>> | undefined;
94
+ /** Use ONLY these servers, ignoring user/project config (claude only). */
95
+ strict?: boolean | undefined;
96
+ }
97
+ /**
98
+ * Plugins for this session only (claude only). `dirs` load with
99
+ * `--plugin-dir`; `disable` lists installed plugin ids (`name@marketplace`)
100
+ * turned off through `--settings`.
101
+ */
102
+ export interface PluginSpec {
103
+ dirs: string[];
104
+ disable: string[];
105
+ }
106
+ /** The canonical call. Only `prompt` is required. */
107
+ export interface AgentCallSpec {
108
+ prompt: string;
109
+ /** Concrete model id or alias for the target agent, passed through as given. */
110
+ model?: string | undefined;
111
+ effort?: EffortLevel | undefined;
112
+ mcp?: McpSpec | undefined;
113
+ plugins?: PluginSpec | undefined;
114
+ /** Replace the agent's system prompt (native on claude; else emulated). */
115
+ systemPrompt?: string | undefined;
116
+ /** Append to the agent's system prompt (native on claude; else emulated). */
117
+ appendSystemPrompt?: string | undefined;
118
+ permissions?: PermissionSpec | undefined;
119
+ context?: ContextSpec | undefined;
120
+ output?: OutputSpec | undefined;
121
+ session?: SessionSpec | undefined;
122
+ /** Hard dollar cap where supported (claude `--max-budget-usd`). */
123
+ budgetUsd?: number | undefined;
124
+ /** Working directory for the agent process. */
125
+ cwd?: string | undefined;
126
+ /** Extra environment variables for the agent process. */
127
+ env?: Record<string, string> | undefined;
128
+ /** Kill the process after this long (SIGTERM, then SIGKILL after 5s). */
129
+ timeoutMs?: number | undefined;
130
+ /** Escape hatch: appended verbatim to the built argv, after everything. */
131
+ extraArgs?: string[] | undefined;
132
+ }
package/dist/spec.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The canonical, agent-agnostic description of one headless agent call.
3
+ *
4
+ * A spec says WHAT you want — prompt, permission posture, context, output
5
+ * shape, session behavior — never HOW a particular CLI expresses it. Each
6
+ * built-in adapter translates the spec into a concrete invocation on a
7
+ * best-effort basis and records every approximation as a `CapabilityNote` (see
8
+ * plan.ts). Custom and template adapters note the fields they fold into the prompt and
9
+ * `permissions.mode`; any other field the `args` builder leaves out is not noted.
10
+ */
11
+ export {};