@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/CHANGELOG.md +181 -0
- package/CONTRIBUTING.md +65 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +299 -2
- package/SECURITY.md +23 -0
- package/dist/adapters/antigravity.d.ts +15 -0
- package/dist/adapters/antigravity.js +262 -0
- package/dist/adapters/claude.d.ts +29 -0
- package/dist/adapters/claude.js +314 -0
- package/dist/adapters/codex.d.ts +18 -0
- package/dist/adapters/codex.js +374 -0
- package/dist/adapters/custom.d.ts +31 -0
- package/dist/adapters/custom.js +42 -0
- package/dist/adapters/opencode.d.ts +12 -0
- package/dist/adapters/opencode.js +207 -0
- package/dist/adapters/shared.d.ts +40 -0
- package/dist/adapters/shared.js +101 -0
- package/dist/adapters/template.d.ts +43 -0
- package/dist/adapters/template.js +62 -0
- package/dist/adapters/types.d.ts +55 -0
- package/dist/adapters/types.js +1 -0
- package/dist/conversation.d.ts +73 -0
- package/dist/conversation.js +37 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +12 -0
- package/dist/model-args.d.ts +37 -0
- package/dist/model-args.js +58 -0
- package/dist/models.d.ts +9 -0
- package/dist/models.js +12 -0
- package/dist/plan.d.ts +56 -0
- package/dist/plan.js +9 -0
- package/dist/registry.d.ts +21 -0
- package/dist/registry.js +41 -0
- package/dist/run.d.ts +75 -0
- package/dist/run.js +172 -0
- package/dist/spawn.d.ts +10 -0
- package/dist/spawn.js +143 -0
- package/dist/spec.d.ts +132 -0
- package/dist/spec.js +11 -0
- package/package.json +70 -3
package/README.md
CHANGED
|
@@ -1,3 +1,300 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @osdlc/agent-call
|
|
2
2
|
|
|
3
|
-
|
|
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[];
|