@osdlc/agent-call 0.0.0-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.
package/README.md CHANGED
@@ -1,3 +1,300 @@
1
- # Temporary Holding Version
1
+ # @osdlc/agent-call
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One call spec for headless coding-agent CLIs.
4
+
5
+ You describe a call once: the prompt, the permission posture, the context, the
6
+ output shape and the session. Each adapter turns that into a `claude -p`,
7
+ `codex exec`, `agy -p` or `opencode run` command line on a best-effort basis
8
+ and records every approximation. The result comes back normalized (final text,
9
+ structured output, session id, token usage, cost) with the raw output kept. An
10
+ optional layer lifts the whole transcript into one conversation model for
11
+ analysis afterwards. No dependencies.
12
+
13
+ ## Why
14
+
15
+ Every program that runs an agent CLI grows its own argument builder, output
16
+ parser, permission flags and timeout code. The CLIs disagree on how a prompt
17
+ travels, what "auto-approve edits" is called, and whether structured output is
18
+ a flag at all. This package is that code, written once, with the differences
19
+ listed in the capability map below.
20
+
21
+ It is for tools that run a coding agent once and read the answer: scripts,
22
+ CI steps, evaluation harnesses, review and triage bots, and orchestrators that
23
+ dispatch work to whichever agent is installed.
24
+
25
+ When not to use it:
26
+
27
+ - For a conversation. A call is one prompt and one answer. It can resume a
28
+ session by id, but it holds no session open.
29
+ - For a run you want to steer or watch live. Output is collected, not
30
+ streamed to your code as it arrives (`stream-json` returns the events after
31
+ the run).
32
+ - For an agent that has no one-shot CLI mode.
33
+ - For an exact, identical result on every agent. Where a CLI cannot express a
34
+ field, the adapter approximates it, folds it into the prompt, or drops it,
35
+ and says so in `notes`.
36
+ - To call a model API. This package starts agent CLIs as processes.
37
+
38
+ ## How it compares
39
+
40
+ - **acpx** is a session client. It drives agents through Agent Client Protocol
41
+ adapters such as `@agentclientprotocol/claude-agent-acp`, not through the
42
+ CLIs, keeps its sessions under `~/.acpx`, and has runtime dependencies.
43
+ agent-call is a one-shot call over the CLIs themselves, with no
44
+ dependencies and no state on disk. Pick acpx for multi-turn sessions,
45
+ queues, flows, or an agent that speaks only ACP.
46
+ - **The Agent Client Protocol** (ACP) is a protocol between a client, such as
47
+ an editor, and an agent. `@agentclientprotocol/sdk` is its library.
48
+ agent-call does not speak ACP. Its conversation model follows the common
49
+ core of ACP session updates, so a stored conversation maps onto them.
50
+ - **The vendor SDKs**, `@anthropic-ai/claude-agent-sdk` and
51
+ `@openai/codex-sdk`, each drive one vendor's agent from code. Pick one when
52
+ you target that agent only and need what it offers beyond a one-shot call.
53
+ Pick agent-call when the same call has to run on more than one agent.
54
+ - **coder/agentapi** runs agents behind an HTTP service with terminal
55
+ emulation. agent-call is a library with no server.
56
+ - **The Vercel AI SDK** abstracts model APIs. agent-call drives agent CLIs as
57
+ processes.
58
+
59
+ ## Install
60
+
61
+ ```sh
62
+ npm install @osdlc/agent-call
63
+ ```
64
+
65
+ ESM only. Node.js 22 or later.
66
+
67
+ The agent CLIs are not installed by this package. Install the ones you call
68
+ (`claude`, `codex`, `agy`, `opencode`) and sign in to each.
69
+
70
+ ## Quick start
71
+
72
+ Plan a call without running it. `planned.argv` is the exact command line, and
73
+ `planned.notes` says what was approximated:
74
+
75
+ ```ts
76
+ import { adapterFor, callAgent, customAdapter, planCall, type AgentCallSpec } from '@osdlc/agent-call'
77
+
78
+ const spec: AgentCallSpec = {
79
+ prompt: 'Summarize the failing tests and propose a fix.',
80
+ permissions: { mode: 'read-only' },
81
+ context: { files: ['src/runner.ts'] },
82
+ timeoutMs: 120_000,
83
+ }
84
+
85
+ // planned.argv is the exact command line; planned.notes says what was approximated.
86
+ const planned = planCall(adapterFor('codex'), spec)
87
+ ```
88
+
89
+ To run a built-in agent, pass its adapter to `callAgent`:
90
+ `callAgent(adapterFor('claude'), spec)`. Swap `'claude'` for `'codex'`,
91
+ `'antigravity'` or `'opencode'` and the same spec runs there. `callAgent`
92
+ resolves to an `AgentResult`: `ok`, `text`, `structured`, `sessionId`, `usage`,
93
+ `costUsd`, `exitCode`, `durationMs`, the `planned` call that ran, and the `raw`
94
+ output.
95
+
96
+ Any one-shot CLI runs through `customAdapter`. This one needs only `node`:
97
+
98
+ ```ts
99
+ const shout = customAdapter({
100
+ name: 'shout',
101
+ binary: 'node',
102
+ args: (_spec, prompt) => ['-e', 'console.log(process.argv[1].toUpperCase())', prompt],
103
+ })
104
+
105
+ const result = await callAgent(shout, { prompt: 'hello' })
106
+ // result.ok is true and result.text is 'HELLO'
107
+ ```
108
+
109
+ Every example in this README is in `examples/`, which runs as part of the test
110
+ suite.
111
+
112
+ ### Failures
113
+
114
+ `callAgent` does not throw when the agent fails. It resolves with `ok: false`
115
+ and an `error`: `{ reason, detail }`. The reasons today are `spawn-failed`,
116
+ `timeout`, `nonzero-exit` and `agent-error`. **The set of reasons is open.** A
117
+ minor release may add one, so handle the reasons you know and keep a default
118
+ branch.
119
+
120
+ It does reject, with an `Error`, when planning throws: `templateAdapter`
121
+ throws for a model or effort that does not fit its template, and
122
+ `customAdapter` passes on whatever your `args` function throws. It also rejects
123
+ when a plan's `TempFile.name` is not a plain file name (empty, or holding `/`,
124
+ `\` or `..`), or when the temp directory for the plan's temp files cannot be
125
+ created or written.
126
+ A parser that throws on the output does not reject: the result is `ok: false`
127
+ with reason `agent-error`. `planCall` throws the same way as planning. The
128
+ `callAgent` TSDoc is the full list.
129
+
130
+ ## API
131
+
132
+ Everything is exported from `@osdlc/agent-call`. `@osdlc/agent-call/models`
133
+ exports `EFFORT_LEVELS` and `EffortLevel` with no Node imports, so a browser
134
+ bundle can use them.
135
+
136
+ The package is 0.x. The stable exports follow semantic versioning for 0.x: a
137
+ breaking change goes in a minor release and is marked `Breaking` in the
138
+ changelog. The experimental exports may change in any minor release.
139
+
140
+ The stable exports:
141
+
142
+ - The call spec: `AgentCallSpec`, `AgentName`, `PermissionSpec`,
143
+ `PermissionMode`, `ContextSpec`, `OutputSpec`, `OutputFormat`, `SessionSpec`,
144
+ `McpSpec`, `PluginSpec`, and `EFFORT_LEVELS` with `EffortLevel`.
145
+ - Planning and running: `planCall`, `callAgent`, `PlannedCall`,
146
+ `CapabilityNote`, `Support`, `TempFile`, `TMP_TOKEN`, `AgentResult`,
147
+ `CallError` and `CallOptions`.
148
+ - Built-in adapters: `adapterFor`, `BUILTIN_AGENTS`, `detectAgents`,
149
+ `AgentAvailability`, `claudeAdapter`, `codexAdapter`, `antigravityAdapter`,
150
+ `opencodeAdapter`, and their options types `ClaudeAdapterOptions`,
151
+ `CodexAdapterOptions`, `AntigravityAdapterOptions` and
152
+ `OpencodeAdapterOptions`.
153
+ - Your own adapters: `customAdapter`, `CustomAdapterOptions`, `AgentAdapter`,
154
+ `ParsedOutput` and `RawProcessOutput`.
155
+ - The conversation model: `Conversation`, `ConversationMessage`, `Part`,
156
+ `Role`, `Usage`, `CONVERSATION_SCHEMA`, `assistantText` and `toolCalls`.
157
+
158
+ The experimental exports:
159
+
160
+ `modelArgs`, `modelArgsRefusal`, `ModelChoice`, `templateAdapter`,
161
+ `templateRefusal`, `TemplateAdapterOptions`, `claudePluginArgv` and
162
+ `claudeResumeFlag`.
163
+
164
+ These build launch arguments for callers that assemble a command line
165
+ themselves: model and effort flags for claude, codex and pi, an argument
166
+ template for any CLI, claude's plugin flags and its `--resume=<id>` flag. They
167
+ work and are tested, but their shape is not settled.
168
+
169
+ `BUILTIN_AGENTS` and `EFFORT_LEVELS` are frozen at run time and typed
170
+ `readonly`.
171
+
172
+ Each export has TSDoc, including `@throws` where it can throw. Your editor
173
+ shows it.
174
+
175
+ ## Design
176
+
177
+ - **Spec** (`AgentCallSpec`) is one call in agent-agnostic terms. Only
178
+ `prompt` is required.
179
+ - **Plan** (`planCall(adapter, spec)`) is a pure translation: spec in,
180
+ `PlannedCall` out (argv, stdin, env, temp files, capability notes). It has no
181
+ side effects, so you can inspect it or golden-test it before running
182
+ anything.
183
+ - **Run** (`callAgent(adapter, spec)`) spawns the planned call, applies the
184
+ library-level timeout (no agent CLI has one), and returns an `AgentResult`.
185
+ - **Conversation** (optional): `callAgent(adapter, spec, { captureConversation:
186
+ true })` lifts the transcript into `agent-conversation/1`: role-tagged
187
+ messages of typed parts (`text`, `reasoning`, `tool-call`, `tool-result`)
188
+ with usage, cost, duration and stop reason. The untouched transcript stays in
189
+ `raw`. The shape follows the common core of ACP session updates, OpenTelemetry
190
+ GenAI attributes and the Vercel AI SDK message model.
191
+
192
+ ```ts
193
+ const result = await callAgent(adapter, spec, { captureConversation: true })
194
+ const conversation = result.conversation!
195
+ const answer = assistantText(conversation)
196
+ const calls = toolCalls(conversation)
197
+ ```
198
+
199
+ `assistantText` joins the assistant's text. `toolCalls` lists every tool call
200
+ in order. `JSON.stringify(conversation)` is stable: store it.
201
+
202
+ ### Best-effort translation, reported
203
+
204
+ Every planned call carries `notes: CapabilityNote[]`, one entry per spec field
205
+ that did not map one to one:
206
+
207
+ - `native`: the CLI has a first-class flag.
208
+ - `approximated`: mapped to the closest available behaviour.
209
+ - `emulated`: reproduced in the prompt or through the environment.
210
+ - `dropped`: the CLI cannot express it. The field is ignored and the note says
211
+ why.
212
+
213
+ ### Capability map
214
+
215
+ | Spec field | claude | codex | antigravity | opencode |
216
+ |---|---|---|---|---|
217
+ | `prompt` | stdin | stdin (`-`) | `-p <prompt>` | positional |
218
+ | `model` | `--model` | `--model` | `--model` | `--model` (provider/model) |
219
+ | `effort` (`low` to `max`, `xhigh` included) | `--effort` | `model_reasoning_effort` | `--effort` (`xhigh`, `max` → `high`) | `--variant` |
220
+ | `systemPrompt` | `--system-prompt` | prompt-emulated | prompt-emulated | prompt-emulated |
221
+ | `appendSystemPrompt` | `--append-system-prompt` | prompt-emulated | prompt-emulated | prompt-emulated |
222
+ | `permissions.mode: read-only` | `--permission-mode plan` | `--sandbox read-only` | `--sandbox` (approximated) | `OPENCODE_PERMISSION` deny map |
223
+ | `permissions.mode: edits` | `--permission-mode acceptEdits` | `--sandbox workspace-write` | dropped | `OPENCODE_PERMISSION` |
224
+ | `permissions.mode: full-auto` | acceptEdits + allowed Bash/Web | `--sandbox workspace-write` | `--dangerously-skip-permissions` | `OPENCODE_PERMISSION` allow map |
225
+ | `permissions.mode: bypass` | `--dangerously-skip-permissions` | `--dangerously-bypass-approvals-and-sandbox` | `--dangerously-skip-permissions` | `OPENCODE_PERMISSION` allow map |
226
+ | `permissions.allowTools` | `--allowedTools` | dropped | dropped | dropped |
227
+ | `permissions.denyTools` | `--disallowedTools` | dropped | dropped | dropped |
228
+ | `context.files` | prompt preamble | prompt preamble | `@path` in prompt | `--file` |
229
+ | `context.dirs` | `--add-dir` | `--add-dir` | dropped | dropped |
230
+ | `context.text` | prompt preamble | prompt preamble | prompt preamble | prompt preamble |
231
+ | `output.format` | `text\|json\|stream-json` | `--json` events | `text\|json\|stream-json` | `--format json` events |
232
+ | `output.schema` | `--json-schema` | `--output-schema` file | `--json-schema` | prompt-emulated |
233
+ | `session.resume` | `--resume=<id>` | `exec resume <id>` | `--conversation <id>` | `--session` |
234
+ | `session.continueLast` | `--continue` | `exec resume --last` | `--continue` | `--continue` |
235
+ | `session.id` | `--session-id` | dropped | dropped | dropped |
236
+ | `session.persist: false` | `--no-session-persistence` | `--ephemeral` | dropped | dropped |
237
+ | `mcp.servers` | `--mcp-config` | `-c mcp_servers.*` | dropped | dropped |
238
+ | `budgetUsd` | `--max-budget-usd` | dropped | dropped | dropped |
239
+ | `timeoutMs` | library-level (SIGTERM → SIGKILL) on all agents | | | |
240
+ | `extraArgs` | appended verbatim on all agents | | | |
241
+
242
+ `tests/contract.test.ts` plans one spec per row on each adapter and checks the
243
+ support level of each cell against the notes.
244
+
245
+ When no `output.format` is given, adapters default to their richest aggregate
246
+ format (a JSON envelope or an event stream), so metadata is captured.
247
+ `result.text` carries the final answer either way.
248
+
249
+ ## Runtimes and platforms
250
+
251
+ - **Runtimes:** Node.js 22 or later. ESM only. Another runtime is not claimed
252
+ until a test runs on it.
253
+ - **Platforms:** Linux and macOS are supported. Windows is untested. A
254
+ timeout or abort kills the agent's whole process group, which needs POSIX
255
+ process groups. On Windows the group kill fails and the call falls back to
256
+ killing the direct child, so a grandchild process can outlive a timeout.
257
+ - **Interrupts:** the agent runs in its own process group, so Ctrl+C or a kill
258
+ of the host does not reach it. The package installs no exit handlers. A host
259
+ that can be interrupted must pass a `signal` to `callAgent` and abort it from
260
+ its own SIGINT/SIGTERM handler, or the agent keeps running.
261
+
262
+ ## Security
263
+
264
+ - The child process inherits the parent's whole environment, plus the spec's
265
+ `env`. Credentials in your environment reach the agent CLI. Pass a trimmed
266
+ environment to the parent if that matters.
267
+ - Any local user can read argv in the process list. The prompt travels in
268
+ argv for antigravity (`-p <prompt>`) and opencode (positional), so do not
269
+ put secrets in a prompt for those two agents. claude and codex read the
270
+ prompt from stdin. claude reads MCP server config, including `env` values,
271
+ from a temp file. codex has no config-file option, so its MCP servers,
272
+ including `env` values, go on the command line as `-c mcp_servers.*`
273
+ overrides. claude's `systemPrompt` and `appendSystemPrompt` are also argv.
274
+ - `permissions.mode: 'bypass'` skips all approvals and sandboxing
275
+ (`--dangerously-skip-permissions` and its equivalents). Use it only in an
276
+ environment that is sandboxed from outside.
277
+ - The runner starts the binary with an argument array and no shell.
278
+
279
+ ## Development
280
+
281
+ The tests run on [Vitest](https://vitest.dev) under Node. From the package
282
+ directory, `pnpm test` runs the unit tests. Parsers are tested against
283
+ fixtures and the runner against real processes. `pnpm run typecheck` and
284
+ `pnpm run build` (tsc to `dist/`) check the types and the emit. See
285
+ [CONTRIBUTING.md](./CONTRIBUTING.md).
286
+
287
+ The live smoke tests make real, cheap agent calls against whichever CLIs are
288
+ installed: `pnpm run test:live`. They are skipped unless `AGENT_CALL_LIVE=1`
289
+ is set, which that script does.
290
+
291
+ Verified against: claude 2.x, codex and opencode 1.x (live). The
292
+ antigravity mappings are built from the CLI's documentation and tested against
293
+ fixtures only. The live suite has no antigravity case.
294
+
295
+ ## Non-goals
296
+
297
+ - Orchestration (worktrees, queues, parallelism). This is the invocation
298
+ primitive such tools build on.
299
+ - A trace backend. `agent-conversation/1` is meant to be stored or exported;
300
+ bring your own sink.
package/SECURITY.md ADDED
@@ -0,0 +1,23 @@
1
+ # Security policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please report vulnerabilities privately. Do not open a public issue.
6
+
7
+ Use GitHub private vulnerability reporting on the repository: open the
8
+ [Security tab](https://github.com/osdlc/osdlc/security) and choose
9
+ "Report a vulnerability".
10
+
11
+ Include the version, a short description, and steps to reproduce. A minimal
12
+ code sample helps.
13
+
14
+ ## Supported versions
15
+
16
+ Only the latest release of the newest major version gets security fixes.
17
+ Upgrade to it.
18
+
19
+ ## What to expect
20
+
21
+ This is a small project. We aim to reply within a week. We will tell you
22
+ whether we accept the report and keep you updated until a fix is released.
23
+ We will credit you in the release notes if you want that.
@@ -0,0 +1,15 @@
1
+ import type { AgentAdapter } from './types.js';
2
+ /** Options for {@link antigravityAdapter}. */
3
+ export interface AntigravityAdapterOptions {
4
+ /** Binary to spawn. Default: the CLI's own name. */
5
+ binary?: string;
6
+ }
7
+ /**
8
+ * Google Antigravity CLI (`agy`). Headless via `-p <prompt>` with
9
+ * `--output-format text|json|stream-json`. Effort, the output schema,
10
+ * session continuation and the full-auto posture are native flags; the CLI
11
+ * documents no system-prompt flag, so system prompts are emulated; `cwd` and
12
+ * `env` apply to the process. Model slugs are not documented (`agy models`
13
+ * lists them), so `model` passes through as given.
14
+ */
15
+ export declare function antigravityAdapter(options?: AntigravityAdapterOptions): AgentAdapter;
@@ -0,0 +1,262 @@
1
+ import { CONVERSATION_SCHEMA, } from '../conversation.js';
2
+ import { note } from '../plan.js';
3
+ import { applyPermissionMode, asNumber, asString, buildEffectivePrompt, get, modelFlag, parseJsonLines, tryParseJson, } from './shared.js';
4
+ /**
5
+ * Google Antigravity CLI (`agy`). Headless via `-p <prompt>` with
6
+ * `--output-format text|json|stream-json`. Effort, the output schema,
7
+ * session continuation and the full-auto posture are native flags; the CLI
8
+ * documents no system-prompt flag, so system prompts are emulated; `cwd` and
9
+ * `env` apply to the process. Model slugs are not documented (`agy models`
10
+ * lists them), so `model` passes through as given.
11
+ */
12
+ export function antigravityAdapter(options = {}) {
13
+ const binary = options.binary ?? 'agy';
14
+ return {
15
+ name: 'antigravity',
16
+ binary,
17
+ plan(spec) {
18
+ return planAntigravity(binary, spec);
19
+ },
20
+ parseResult(planned, raw) {
21
+ return parseAntigravity(planned, raw);
22
+ },
23
+ toConversation(planned, raw) {
24
+ return antigravityConversation(planned, raw);
25
+ },
26
+ };
27
+ }
28
+ function planAntigravity(binary, spec) {
29
+ const notes = [];
30
+ const env = { ...spec.env };
31
+ const argv = [binary];
32
+ const format = spec.output?.format ?? 'json';
33
+ if (format !== 'text')
34
+ argv.push('--output-format', format);
35
+ if (spec.model !== undefined)
36
+ argv.push(...modelFlag(spec.model));
37
+ if (spec.effort !== undefined) {
38
+ // agy documents low|medium|high; `xhigh` and `max` clamp to the top rung.
39
+ const clamped = spec.effort === 'xhigh' || spec.effort === 'max';
40
+ argv.push('--effort', clamped ? 'high' : spec.effort);
41
+ note(notes, 'effort', clamped ? 'approximated' : 'native', clamped ? '--effort high (agy tops out at high)' : '--effort');
42
+ }
43
+ if (spec.mcp?.servers !== undefined) {
44
+ note(notes, 'mcp.servers', 'dropped', 'antigravity takes MCP servers from mcp_config.json, not a flag');
45
+ }
46
+ const permissions = spec.permissions;
47
+ applyPermissionMode(argv, notes, permissions?.mode, {
48
+ 'read-only': {
49
+ argv: ['--sandbox'],
50
+ support: 'approximated',
51
+ detail: 'read-only mapped to --sandbox (agy documents no read-only approval mode)',
52
+ },
53
+ 'full-auto': {
54
+ argv: ['--dangerously-skip-permissions'],
55
+ support: 'approximated',
56
+ detail: 'full-auto mapped to --dangerously-skip-permissions (agy approves everything; no workspace scoping)',
57
+ },
58
+ bypass: {
59
+ argv: ['--dangerously-skip-permissions'],
60
+ support: 'native',
61
+ detail: '--dangerously-skip-permissions',
62
+ },
63
+ });
64
+ if (permissions?.mode === 'edits') {
65
+ note(notes, 'permissions.mode', 'dropped', 'antigravity documents no edits-only approval mode; unapproved tool calls are denied headless');
66
+ }
67
+ if ((permissions?.allowTools ?? []).length > 0) {
68
+ note(notes, 'permissions.allowTools', 'dropped', 'no CLI flag; configure permissions.allow in antigravity-cli/settings.json');
69
+ }
70
+ if ((permissions?.denyTools ?? []).length > 0) {
71
+ note(notes, 'permissions.denyTools', 'dropped', 'no CLI flag; configure permission rules in antigravity-cli/settings.json');
72
+ }
73
+ if ((spec.context?.dirs ?? []).length > 0) {
74
+ note(notes, 'context.dirs', 'dropped', 'antigravity documents no extra-directory flag');
75
+ }
76
+ if (spec.output?.schema !== undefined) {
77
+ argv.push('--json-schema', JSON.stringify(spec.output.schema));
78
+ note(notes, 'output.schema', 'native', '--json-schema');
79
+ }
80
+ const session = spec.session;
81
+ if (session?.resume !== undefined) {
82
+ argv.push('--conversation', session.resume);
83
+ note(notes, 'session.resume', 'native', '--conversation');
84
+ }
85
+ else if (session?.continueLast === true) {
86
+ argv.push('--continue');
87
+ note(notes, 'session.continueLast', 'native', '--continue');
88
+ }
89
+ if (session?.id !== undefined) {
90
+ note(notes, 'session.id', 'dropped', 'antigravity assigns conversation ids itself');
91
+ }
92
+ if (session?.persist === false) {
93
+ note(notes, 'session.persist', 'dropped', 'antigravity always persists conversations');
94
+ }
95
+ if (spec.plugins !== undefined) {
96
+ note(notes, 'plugins', 'dropped', 'session-only plugins are claude only');
97
+ }
98
+ if (spec.budgetUsd !== undefined) {
99
+ note(notes, 'budgetUsd', 'dropped', 'antigravity has no budget cap flag');
100
+ }
101
+ argv.push(...(spec.extraArgs ?? []));
102
+ // The prompt is the value of -p, so it travels in argv, not stdin. The schema
103
+ // is native (--json-schema); both system prompts have no flag and are emulated.
104
+ const prompt = buildEffectivePrompt(spec, { filesAtSyntax: true, schemaNative: true }, notes);
105
+ argv.push('-p', prompt);
106
+ return {
107
+ agent: 'antigravity',
108
+ argv,
109
+ prompt,
110
+ env,
111
+ cwd: spec.cwd,
112
+ outputFormat: format,
113
+ tempFiles: [],
114
+ notes,
115
+ spec,
116
+ };
117
+ }
118
+ function parseAntigravity(planned, raw) {
119
+ if (planned.outputFormat === 'text') {
120
+ const parsed = { text: raw.stdout.trim() };
121
+ if (planned.spec.output?.schema !== undefined) {
122
+ parsed.structured = tryParseJson(parsed.text);
123
+ }
124
+ return parsed;
125
+ }
126
+ if (planned.outputFormat === 'stream-json') {
127
+ return parseAntigravityStream(planned, raw);
128
+ }
129
+ const envelope = tryParseJson(raw.stdout);
130
+ if (envelope === undefined) {
131
+ return {
132
+ text: raw.stdout.trim(),
133
+ isError: true,
134
+ errorDetail: 'agy --output-format json produced no parseable envelope',
135
+ };
136
+ }
137
+ return fromResultEnvelope(planned, envelope);
138
+ }
139
+ /** Shared by the json envelope and the stream's closing `result` event, which carries the same shape. */
140
+ function fromResultEnvelope(planned, envelope) {
141
+ const text = asString(get(envelope, 'response')) ?? '';
142
+ const status = asString(get(envelope, 'status'));
143
+ const error = get(envelope, 'error');
144
+ const hasError = error !== undefined && error !== null && error !== '';
145
+ const parsed = {
146
+ text,
147
+ sessionId: asString(get(envelope, 'conversation_id')),
148
+ usage: antigravityUsage(get(envelope, 'usage')),
149
+ isError: hasError || (status !== undefined && status !== 'SUCCESS'),
150
+ };
151
+ if (parsed.isError === true) {
152
+ parsed.errorDetail =
153
+ asString(error) ?? asString(get(error, 'message')) ?? `agy finished with status ${status}`;
154
+ }
155
+ if (planned.spec.output?.schema !== undefined) {
156
+ parsed.structured = get(envelope, 'structured_output') ?? tryParseJson(text);
157
+ }
158
+ return parsed;
159
+ }
160
+ function parseAntigravityStream(planned, raw) {
161
+ const events = parseJsonLines(raw.stdout);
162
+ const textChunks = [];
163
+ let model;
164
+ let result;
165
+ for (const event of events) {
166
+ const kind = get(event, 'event');
167
+ if (kind === 'init') {
168
+ model = asString(get(event, 'model'));
169
+ }
170
+ else if (kind === 'step_update') {
171
+ const delta = asString(get(event, 'text_delta'));
172
+ if (get(event, 'step_type') === 'agent_response' && delta !== undefined) {
173
+ textChunks.push(delta);
174
+ }
175
+ }
176
+ else if (kind === 'result') {
177
+ result = fromResultEnvelope(planned, event);
178
+ }
179
+ }
180
+ const streamed = textChunks.join('').trim();
181
+ const parsed = {
182
+ ...result,
183
+ text: result !== undefined && result.text !== '' ? result.text : streamed,
184
+ model,
185
+ events,
186
+ };
187
+ if (result === undefined) {
188
+ parsed.isError = true;
189
+ parsed.errorDetail = 'agy stream-json ended without a result event';
190
+ }
191
+ if (planned.spec.output?.schema !== undefined && parsed.structured === undefined) {
192
+ parsed.structured = tryParseJson(parsed.text);
193
+ }
194
+ return parsed;
195
+ }
196
+ /**
197
+ * agy reports `thinking_tokens` beside `output_tokens` (its `total_tokens` is
198
+ * input + output + thinking + cache reads). claude's and codex's output counts
199
+ * already include reasoning, so thinking folds into `outputTokens` here and a
200
+ * sum of the parts matches `totalTokens`. `input_tokens` excludes cache reads,
201
+ * as claude's does.
202
+ */
203
+ function antigravityUsage(usage) {
204
+ if (usage === null || typeof usage !== 'object')
205
+ return undefined;
206
+ const output = asNumber(get(usage, 'output_tokens'));
207
+ const thinking = asNumber(get(usage, 'thinking_tokens'));
208
+ return {
209
+ inputTokens: asNumber(get(usage, 'input_tokens')),
210
+ outputTokens: output === undefined && thinking === undefined ? undefined : (output ?? 0) + (thinking ?? 0),
211
+ cacheReadTokens: asNumber(get(usage, 'cache_read_tokens')),
212
+ totalTokens: asNumber(get(usage, 'total_tokens')),
213
+ };
214
+ }
215
+ function antigravityConversation(planned, raw) {
216
+ const parsed = parseAntigravity(planned, raw);
217
+ const messages = [
218
+ { role: 'user', parts: [{ type: 'text', text: planned.prompt ?? '' }] },
219
+ ];
220
+ if (planned.outputFormat === 'stream-json') {
221
+ // Tool steps are reported as step_update events; the CLI documents no
222
+ // tool-result payload, so only the call is surfaced (once, when DONE).
223
+ for (const event of parsed.events ?? []) {
224
+ if (get(event, 'event') === 'step_update' &&
225
+ get(event, 'step_type') === 'tool' &&
226
+ get(event, 'state') === 'DONE') {
227
+ messages.push({
228
+ role: 'assistant',
229
+ parts: [
230
+ {
231
+ type: 'tool-call',
232
+ callId: String(get(event, 'step_index') ?? ''),
233
+ name: asString(get(event, 'tool_name')) ?? 'unknown',
234
+ input: get(event, 'tool_info'),
235
+ },
236
+ ],
237
+ });
238
+ }
239
+ }
240
+ }
241
+ if (parsed.text !== '') {
242
+ messages.push({
243
+ role: 'assistant',
244
+ parts: [{ type: 'text', text: parsed.text }],
245
+ });
246
+ }
247
+ return {
248
+ schema: CONVERSATION_SCHEMA,
249
+ agent: 'antigravity',
250
+ model: parsed.model,
251
+ sessionId: parsed.sessionId,
252
+ messages,
253
+ usage: parsed.usage,
254
+ stopReason: parsed.isError === true ? 'error' : undefined,
255
+ raw: {
256
+ format: `antigravity/${planned.outputFormat}`,
257
+ data: planned.outputFormat === 'stream-json'
258
+ ? parsed.events
259
+ : (tryParseJson(raw.stdout) ?? raw.stdout),
260
+ },
261
+ };
262
+ }
@@ -0,0 +1,29 @@
1
+ import type { PluginSpec } from '../spec.js';
2
+ import type { AgentAdapter } from './types.js';
3
+ /** Options for {@link claudeAdapter}. */
4
+ export interface ClaudeAdapterOptions {
5
+ /** Binary to spawn. Default: the CLI's own name. */
6
+ binary?: string;
7
+ }
8
+ /**
9
+ * Claude Code (`claude -p`). The richest surface: permissions, tool rules,
10
+ * system prompts, structured output, sessions, and budget are all native.
11
+ */
12
+ export declare function claudeAdapter(options?: ClaudeAdapterOptions): AgentAdapter;
13
+ /**
14
+ * `--resume=<id>`: `--resume` takes an optional value, so the `=` form keeps an
15
+ * id that starts with `-` bound to it.
16
+ *
17
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
18
+ */
19
+ export declare function claudeResumeFlag(id: string): string;
20
+ /** `--effort <level>`: passed through as given, so a level the CLI adds later needs no change here. */
21
+ export declare function claudeEffortArgv(effort: string): string[];
22
+ /**
23
+ * Claude argv for session-only plugins: `--plugin-dir` per dir, then a
24
+ * `--settings` disable map. For callers that build claude's command line
25
+ * themselves.
26
+ *
27
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
28
+ */
29
+ export declare function claudePluginArgv(plugins: PluginSpec): string[];