agy-acp-map 0.5.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/LICENSE +21 -0
- package/README.md +302 -0
- package/dist/agent-sdk.d.ts +156 -0
- package/dist/bin.js +16165 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +16051 -0
- package/dist/lib/agy-args.d.ts +152 -0
- package/dist/lib/agy-discovery.d.ts +28 -0
- package/dist/lib/agy-process.d.ts +71 -0
- package/dist/lib/map-agy-to-acp.d.ts +51 -0
- package/dist/lib/path-allowlist.d.ts +21 -0
- package/dist/lib/prompt-normalize.d.ts +51 -0
- package/dist/lib/rich-content.d.ts +45 -0
- package/dist/lib/session-store.d.ts +82 -0
- package/dist/lib/soft-deny.d.ts +36 -0
- package/dist/sdk-server.d.ts +2 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yitom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# agy-acp-map — ACP v2 ↔ agy stream-json bridge (v0.1.2 · Bun + TypeScript)
|
|
2
|
+
|
|
3
|
+
**English** | **中文**
|
|
4
|
+
|
|
5
|
+
## What is NDJSON / stream-json?
|
|
6
|
+
|
|
7
|
+
NDJSON (Newline-Delimited JSON), also called JSONL, is a framing where each line is one complete JSON value. Both ACP stdio JSON-RPC and `agy --output-format stream-json` use this: the agent writes one JSON-RPC object per line on stdout; agy emits one event object per line (`init`, `step_update`, `result`). No length prefixes, no SQLite — just lines.
|
|
8
|
+
|
|
9
|
+
NDJSON(换行分隔 JSON / JSONL)是一种按行分帧的格式:每一行是一个完整的 JSON 值。ACP 的 stdio JSON-RPC 与 `agy --output-format stream-json` 都采用这种方式。不依赖 SQLite,也不使用长度前缀。
|
|
10
|
+
|
|
11
|
+
> **Note:** There is **no** `agy acp` subcommand. This bridge (`agy-acp` / `src/server.ts`) is a third-party ACP agent that spawns the normal `agy` CLI with stream-json.
|
|
12
|
+
> **说明:** 不存在 `agy acp` 子命令。本仓库是第三方 ACP agent,通过官方 `agy` CLI 的 stream-json 桥接。
|
|
13
|
+
|
|
14
|
+
## Architecture / 架构
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
ACP Client ←stdio JSON-RPC NDJSON→ src/server.ts ←stdin/stdout stream-json→ agy CLI
|
|
18
|
+
│
|
|
19
|
+
├─ src/lib/map-agy-to-acp.ts
|
|
20
|
+
├─ src/lib/agy-process.ts (spawn/kill/generation)
|
|
21
|
+
├─ src/lib/agy-args.ts (buildAgyArgs / safety / printTimeout)
|
|
22
|
+
├─ src/lib/agy-discovery.ts (agy models / agents)
|
|
23
|
+
├─ src/lib/prompt-normalize.ts (image → files + size/cleanup)
|
|
24
|
+
├─ src/lib/rich-content.ts (paths → ACP image + allowlist)
|
|
25
|
+
├─ src/lib/path-allowlist.ts (session root checks)
|
|
26
|
+
├─ src/lib/soft-deny.ts (stderr soft-deny)
|
|
27
|
+
└─ src/lib/session-store.ts (disk id/config index)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **No SQLite / no `~/.agy` DB reads**: never opens agy conversation stores.
|
|
31
|
+
- **Lightweight SessionStore** (`~/.agy-acp-map/sessions.json`): ACP `sessionId` ↔ agy `conversationId` + launch snapshot only. **Not** a transcript DB.
|
|
32
|
+
- **Client owns transcript** (e.g. zustand / gateway UI). Bridge advertises `historyReplay: false`.
|
|
33
|
+
- **No Zed / Antigravity plugin code**.
|
|
34
|
+
- **Persistent stdin stream-json** (same child across turns until cancel/config change).
|
|
35
|
+
- **Resume:** `session/resume` rehydrates from memory or disk; next prompt respawns with `--conversation <id>` + saved flags. No history replay via `session/update`.
|
|
36
|
+
|
|
37
|
+
## Requirements / 环境
|
|
38
|
+
|
|
39
|
+
- Bun ≥ 1.1 (tested 1.4.2) + TypeScript sources under `src/`
|
|
40
|
+
- `agy` on PATH (`export PATH="/home/box/.local/bin:$PATH"`) and logged in
|
|
41
|
+
|
|
42
|
+
## Run / 运行
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export PATH="/home/box/.local/bin:$PATH"
|
|
46
|
+
cd /workspace/agy-acp-map
|
|
47
|
+
|
|
48
|
+
bun src/server.ts # Minimal ACP stdio agent (zero-runtime-dep)
|
|
49
|
+
bun src/sdk-server.ts # Official @agentclientprotocol/sdk stdio agent
|
|
50
|
+
bun test # unit tests (src/lib/*.test.ts)
|
|
51
|
+
bun tests/test-agy-args.ts # CLI args unit test
|
|
52
|
+
|
|
53
|
+
# Live smokes (need logged-in agy)
|
|
54
|
+
bun tests/smoke/smoke-basic.ts
|
|
55
|
+
bun tests/smoke/smoke-permissions.ts
|
|
56
|
+
bun tests/smoke/smoke-image-in.ts
|
|
57
|
+
bun tests/smoke/smoke-image-out.ts # may take longer
|
|
58
|
+
bun tests/smoke/smoke-flags.ts # model / --conversation / sandbox / json-schema
|
|
59
|
+
bun tests/smoke/smoke-robustness.ts # cancel / empty / bad model / set_config / list
|
|
60
|
+
|
|
61
|
+
bun run smoke:all # units + full live matrix
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Windows:** install [Bun](https://bun.sh), put `agy` on `PATH`, then the same commands.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Launch flags / 启动参数 (v0.1.2)
|
|
68
|
+
|
|
69
|
+
Configure on **`session/new`** (preferred) and/or env fallbacks. Stored on the Session; every `spawnAgy` builds argv via `buildAgyArgs(session)`.
|
|
70
|
+
|
|
71
|
+
在 **`session/new`** 上配置(优先),或用环境变量兜底。写入 Session;每次 spawn 由 `buildAgyArgs` 组装参数。
|
|
72
|
+
|
|
73
|
+
| agy flag | `session/new` field | env fallback |
|
|
74
|
+
|----------|---------------------|--------------|
|
|
75
|
+
| `--model` | `model` or `_meta.model` or `config.model` | `AGY_ACP_MODEL` |
|
|
76
|
+
| `--effort` | `effort` / `_meta` / `config` | `AGY_ACP_EFFORT` |
|
|
77
|
+
| `--mode` | `mode` (`accept-edits` \| `plan`) | `AGY_ACP_MODE` |
|
|
78
|
+
| `--agent` | `agent` | `AGY_ACP_AGENT` |
|
|
79
|
+
| `--sandbox` | `sandbox: true` | `AGY_ACP_SANDBOX=1` |
|
|
80
|
+
| `--json-schema` | `jsonSchema` (string or object→stringify) | `AGY_ACP_JSON_SCHEMA` (string or path) |
|
|
81
|
+
| `--conversation` | `conversationId` (resume / switch-model flow) | _(from prior turn)_ |
|
|
82
|
+
| `--dangerously-skip-permissions` | `safety: 'autonomous' \| 'autonomous-unsandboxed'` or `skipPermissions: true` | `AGY_ACP_SAFETY=…` / `AGY_ACP_SKIP_PERMISSIONS=1` if safety unset (**default off / safe**) |
|
|
83
|
+
| `--disable-slash-commands` | `disableSlashCommands` (default **true**) | `AGY_ACP_DISABLE_SLASH_COMMANDS=0` to omit |
|
|
84
|
+
| `--print-timeout` | `printTimeout` (e.g. `30m`, `120s`, `0`) | `AGY_ACP_PRINT_TIMEOUT` (default `0`) |
|
|
85
|
+
|
|
86
|
+
Also accepts a simple ACP-ish `configOptions: [{ id|configId, value }, ...]` including `printTimeout`, `safety`, `disableSlashCommands`.
|
|
87
|
+
|
|
88
|
+
Invalid model / effort values fail **loud** at agy spawn (stderr + turn ends); the bridge does not validate catalog ids.
|
|
89
|
+
|
|
90
|
+
无效的 model/effort 会在 agy 启动时失败(stderr 可见);桥接层不做模型目录校验。
|
|
91
|
+
|
|
92
|
+
### Session store / 会话索引 (v0.1.2)
|
|
93
|
+
|
|
94
|
+
**What is stored / 存什么**
|
|
95
|
+
|
|
96
|
+
| Field | Meaning |
|
|
97
|
+
|-------|---------|
|
|
98
|
+
| `sessionId` | ACP id minted by this bridge |
|
|
99
|
+
| `conversationId` | agy conversation id when known |
|
|
100
|
+
| `title`, `cwd`, `additionalDirectories` | display / workspace |
|
|
101
|
+
| `model`, `effort`, `mode`, `agent`, `safety`, `sandbox`, `jsonSchema`, `printTimeout`, `disableSlashCommands` | launch snapshot for respawn |
|
|
102
|
+
| `createdAt`, `updatedAt` | ISO timestamps |
|
|
103
|
+
|
|
104
|
+
**What is NOT stored / 不存什么**
|
|
105
|
+
|
|
106
|
+
- Messages, tool traces, NDJSON event logs, images, or any full transcript
|
|
107
|
+
- Client UI state — **zustand / ACP Client owns the transcript**
|
|
108
|
+
|
|
109
|
+
Default path: `~/.agy-acp-map/sessions.json`. Override with `AGY_ACP_STORE` or `AGY_ACP_SESSION_STORE`.
|
|
110
|
+
|
|
111
|
+
默认路径:`~/.agy-acp-map/sessions.json`;可用 `AGY_ACP_STORE` / `AGY_ACP_SESSION_STORE` 覆盖。
|
|
112
|
+
|
|
113
|
+
### Resume vs load / 恢复 vs 加载
|
|
114
|
+
|
|
115
|
+
| | `session/resume` (this bridge) | History load / replay |
|
|
116
|
+
|--|-------------------------------|------------------------|
|
|
117
|
+
| Purpose | Re-attach ACP `sessionId` → memory + spawn with `--conversation` | Stream past turns into UI |
|
|
118
|
+
| Bridge behavior | Rehydrate id/config from memory or **disk store**; **no** `session/update` history | **Not implemented** (`historyReplay: false`) |
|
|
119
|
+
| Who has messages | Client already has them (or reloads from its own store) | Would be Agent → Client replay |
|
|
120
|
+
|
|
121
|
+
中文:`session/resume` 只恢复 id/配置映射并在下次 prompt 带 `--conversation` 拉起 agy;**不会**通过 `session/update` 重放历史。完整对话记录由 Client(如 zustand)持有。不实现 `session/load` 式 history replay。
|
|
122
|
+
|
|
123
|
+
### Resume & dynamic config / 恢复与动态配置
|
|
124
|
+
|
|
125
|
+
1. **First spawn** of a brand-new session: omit `--conversation`; `session/new` upserts disk store immediately.
|
|
126
|
+
2. Mapper learns `conversation_id` from agy `init`/`result` → `session.conversationId` + store upsert.
|
|
127
|
+
3. Exported in `session/new` / `session/resume` `_meta`, and `session/list` (`_meta.conversationId` when present). `session/list` merges memory + disk (**prefer memory**).
|
|
128
|
+
4. After cancel/kill/crash **respawn**: `--conversation <id>` so Agent-side context resumes.
|
|
129
|
+
5. After **close** or process restart: `session/resume` with stored `sessionId` rehydrates into memory (no child yet); next prompt spawns with saved flags + `--conversation`. Disk row kept on close unless `AGY_ACP_DELETE_ON_CLOSE=1`.
|
|
130
|
+
6. **Change model mid-life (recommended):** wait idle → `session/close` → `session/new` with `{ cwd, conversationId, model, ... }`.
|
|
131
|
+
7. **Or** idle `session/set_config_option`:
|
|
132
|
+
```json
|
|
133
|
+
{ "sessionId": "...", "configId": "model|effort|mode|agent|sandbox|jsonSchema|printTimeout|safety|disableSlashCommands", "value": "..." }
|
|
134
|
+
```
|
|
135
|
+
Updates session fields, kills lingering child; **next** `session/prompt` respawns with new flags + `--conversation`. When busy → error `-32002`.
|
|
136
|
+
|
|
137
|
+
`bridgeCapabilities.dynamicConfig: "restart"` · `resume: true` · `historyReplay: false`.
|
|
138
|
+
|
|
139
|
+
## Env / 环境变量
|
|
140
|
+
|
|
141
|
+
| Env | Default | Meaning |
|
|
142
|
+
|-----|---------|---------|
|
|
143
|
+
| `AGY_ACP_SAFETY` | `safe` | `safe` \| `autonomous` \| `autonomous-unsandboxed` (aliases: `unsandboxed`, `autonomous_unsandboxed`). See Safety modes table. |
|
|
144
|
+
| `AGY_ACP_SKIP_PERMISSIONS` | `0` | If **safety unset**: `1` ≈ treat as `autonomous`; `0` ≈ `safe`. Explicit `AGY_ACP_SAFETY` / `session.safety` wins. |
|
|
145
|
+
| `AGY_ACP_DISABLE_SLASH_COMMANDS` | `1` | When `1` (default), pass `--disable-slash-commands`. Set `0` to omit. |
|
|
146
|
+
| `AGY_ACP_PRINT_TIMEOUT` | `0` | Passed as `--print-timeout` (e.g. `30m`, `120s`, `0` = wait until turn completes). |
|
|
147
|
+
| `AGY_BIN` | `agy` | Override binary |
|
|
148
|
+
| `AGY_ACP_MODEL` | — | Default `--model` |
|
|
149
|
+
| `AGY_ACP_EFFORT` | — | Default `--effort` |
|
|
150
|
+
| `AGY_ACP_MODE` | — | Default `--mode` |
|
|
151
|
+
| `AGY_ACP_AGENT` | — | Default `--agent` |
|
|
152
|
+
| `AGY_ACP_SANDBOX` | — | `1`/`0` → force sandbox on/off for `safe`/`autonomous`. Ignored for `autonomous-unsandboxed` (never sandboxed). |
|
|
153
|
+
| `AGY_ACP_JSON_SCHEMA` | — | Schema string or file path for `--json-schema` |
|
|
154
|
+
| `AGY_ACP_KEEP_STAGING` | — | `1` → keep `.agy-acp-staging` files after turn/close (debug) |
|
|
155
|
+
| `AGY_ACP_STORE` / `AGY_ACP_SESSION_STORE` | `~/.agy-acp-map/sessions.json` | Disk session index path (`SESSION_STORE` wins if both set) |
|
|
156
|
+
| `AGY_ACP_DELETE_ON_CLOSE` | unset | `1`/`true` → also delete store row on `session/close` (default: **keep** disk for resume) |
|
|
157
|
+
|
|
158
|
+
## `initialize` → bridgeCapabilities
|
|
159
|
+
|
|
160
|
+
Returned alongside standard ACP fields (also under `_meta.bridgeCapabilities`):
|
|
161
|
+
|
|
162
|
+
| Field | Value | Notes |
|
|
163
|
+
|-------|-------|-------|
|
|
164
|
+
| `prompt` | `true` | |
|
|
165
|
+
| `streaming` | `true` | |
|
|
166
|
+
| `tools` | `true` | Mapped from agy tool steps |
|
|
167
|
+
| `resume` | `true` | Disk/memory SessionStore; respawn passes `--conversation` |
|
|
168
|
+
| `permissionRoundTrip` | `false` | No ACP permission UI |
|
|
169
|
+
| `permissionMode` | `safety_tiers` | Launch strategies only; no ACP permission UI |
|
|
170
|
+
| `safetyTiers` | `[safe, autonomous, autonomous-unsandboxed]` | See Safety modes table |
|
|
171
|
+
| `nativeCancel` | `false` | |
|
|
172
|
+
| `cancelMode` | `SIGINT_then_KILL` | |
|
|
173
|
+
| `historyReplay` | `false` | No Agent→Client history replay; Client owns transcript |
|
|
174
|
+
| `dynamicConfig` | `restart` | `session/new` + idle `set_config_option` / close+new |
|
|
175
|
+
| `richContentInput` | `degrade_to_files` | Images → `.agy-acp-staging/` + text path |
|
|
176
|
+
| `richContentOutput` | `best_effort` | Detect paths; inline base64 ≤2MB |
|
|
177
|
+
| `clientFilesystem` | `false` | |
|
|
178
|
+
| `clientTerminal` | `false` | |
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
On `initialize`, the bridge runs `agy models` and `agy agents` (≈10s timeout, cached for process lifetime). Results appear as `availableModels` / `availableAgents` on `bridgeCapabilities` and `_meta`, plus ACP-ish `configOptions`. Discovery failure → empty arrays (initialize still succeeds).
|
|
182
|
+
|
|
183
|
+
`initialize` 时会跑 `agy models` / `agy agents`(约 10s 超时,进程内缓存),结果挂在 `bridgeCapabilities.availableModels|availableAgents` 与 `configOptions`。失败则空数组,不阻断 initialize。
|
|
184
|
+
|
|
185
|
+
### Safety modes / 安全模式 (v0.1.1)
|
|
186
|
+
|
|
187
|
+
No interactive ACP permission UI — **launch strategies only** (`resolveSafety` → agy flags).
|
|
188
|
+
|
|
189
|
+
无交互式 ACP 权限 UI,仅启动策略(`resolveSafety` → agy 参数)。
|
|
190
|
+
|
|
191
|
+
| Mode / 模式 | Skip permissions / 跳过权限 | Sandbox / 沙箱 |
|
|
192
|
+
|-------------|------------------------------|----------------|
|
|
193
|
+
| **safe** (default) | no — rely on agy `settings.json` allow/deny + soft-deny messages | only if user sets `sandbox: true` / `AGY_ACP_SANDBOX=1` |
|
|
194
|
+
| **autonomous** | yes (`--dangerously-skip-permissions`) | **default `--sandbox`** unless user sets `sandbox: false` / `AGY_ACP_SANDBOX=0` |
|
|
195
|
+
| **autonomous-unsandboxed** (aliases: `autonomous_unsandboxed`, `unsandboxed`) | yes (`--dangerously-skip-permissions`) | **never** pass `--sandbox` (explicit dangerous tier; ignores sandbox overrides) |
|
|
196
|
+
|
|
197
|
+
Accept via:
|
|
198
|
+
- `session/new` field `safety`: `'safe' | 'autonomous' | 'autonomous-unsandboxed'`
|
|
199
|
+
- env `AGY_ACP_SAFETY` (same values)
|
|
200
|
+
- idle `session/set_config_option` with `configId: "safety"`
|
|
201
|
+
- Backward compat: `AGY_ACP_SKIP_PERMISSIONS=1` ≈ `autonomous` **if safety unset**; `=0` ≈ `safe`
|
|
202
|
+
|
|
203
|
+
输入:`session/new.safety` / `AGY_ACP_SAFETY` / `set_config_option`;兼容 `AGY_ACP_SKIP_PERMISSIONS`(仅在未设 safety 时生效)。
|
|
204
|
+
|
|
205
|
+
## Image / rich content strategy / 富内容策略
|
|
206
|
+
|
|
207
|
+
**Input:** agy stream-json stdin **rejects** non-text content blocks. The bridge stages `image` / binary `resource` / `audio` under `<cwd>/.agy-acp-staging/<uuid>.<ext>` and injects text like:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
User attached an image file at: /abs/path.png
|
|
211
|
+
Please open/view that file and answer based on what you see.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`--add-dir <cwd>` covers the staging directory.
|
|
215
|
+
|
|
216
|
+
**Output:** When tools (especially `generate_image`) or agent text mention `png|jpg|webp|gif` paths, the mapper adds ACP `{ type: 'image', mimeType, data }` (base64) if the file exists, is ≤2MB, **and** resolves under session `cwd` / `.agy-acp-staging` / `additionalDirectories`; otherwise path text only.
|
|
217
|
+
|
|
218
|
+
## Soft-deny / 权限
|
|
219
|
+
|
|
220
|
+
Without skip-permissions, agy may stderr e.g.:
|
|
221
|
+
|
|
222
|
+
> a tool required the "command" permission … auto-denied. Add an allow-rule … (e.g. command(\<target\>))
|
|
223
|
+
|
|
224
|
+
`src/lib/soft-deny.ts` parses these; at turn end the server emits an `agent_message_chunk` listing suggested allow rules.
|
|
225
|
+
|
|
226
|
+
## Files / 文件
|
|
227
|
+
|
|
228
|
+
| File | Role |
|
|
229
|
+
|------|------|
|
|
230
|
+
| `src/server.ts` | Minimal ACP v2 stdio server (zero-runtime-dep) |
|
|
231
|
+
| `src/agent-sdk.ts` | Official `@agentclientprotocol/sdk` Agent App |
|
|
232
|
+
| `src/sdk-server.ts` | Official ACP SDK stdio server entry |
|
|
233
|
+
| `tests/test-agy-args.ts` / `bun test` | Unit tests |
|
|
234
|
+
| `tests/smoke/*.ts` | Live smoke tests (basic, flags, perms, images, robust) |
|
|
235
|
+
| `tests/fixtures/tiny.png` | Blue “HI” PNG for image-in |
|
|
236
|
+
| `src/lib/session-store.ts` | Disk session id/config index (atomic JSON) |
|
|
237
|
+
| `docs/AGY_ACP_MAP_ANALYSIS.zh-CN.md` | Architecture analysis (kept) |
|
|
238
|
+
| `src/test-agy-args.ts / `bun test`` | Unit tests (no live agy) |
|
|
239
|
+
| `src/client-smoke*.ts` | Smokes |
|
|
240
|
+
| `fixtures/tiny.png` | Blue “HI” PNG for image-in |
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## Opinion / 看法(v0.1.2)
|
|
244
|
+
|
|
245
|
+
Agree with the analysis: **stream-json > PTY/SQLite** for coupling. 0.1.0+ added process supervision, allowlists, staging cleanup; **0.1.2** adds the lightweight disk SessionStore (id mapping only — Client still owns transcript).
|
|
246
|
+
|
|
247
|
+
认同分析结论:stream-json 在耦合上优于 PTY/SQLite。0.1.0+ 已有进程监督、白名单、staging 清理;**0.1.2** 增加轻量磁盘 SessionStore(仅 id/配置映射,对话正文仍由 Client 持有)。
|
|
248
|
+
|
|
249
|
+
## Engineering baseline (0.1.0) / 工程基线
|
|
250
|
+
|
|
251
|
+
| Area | Change |
|
|
252
|
+
|------|--------|
|
|
253
|
+
| Child `error` | `child.on('error')` → ACP agent_message + idle `error`/`cancelled`; no unhandled EventEmitter errors |
|
|
254
|
+
| Process supervision | `src/lib/agy-process.ts`: generation tokens, SIGINT→SIGTERM→force kill (Windows `taskkill`), await old exit before respawn, ignore stale NDJSON |
|
|
255
|
+
| Image allowlist | `fileToAcpImageBlock` only reads under session `cwd` / staging / `additionalDirectories` (realpath; relative → session cwd) |
|
|
256
|
+
| Staging | Max 8MB/blob, 32MB/turn; cleanup after idle / session close / shutdown; `AGY_ACP_KEEP_STAGING=1` to keep |
|
|
257
|
+
| Mapper | `result.response` fallback when no `text_delta`; `CANCELLED`/`INTERRUPTED` → `stopReason: cancelled` |
|
|
258
|
+
| Soft-deny | Generic tool ERROR no longer treated as permission deny |
|
|
259
|
+
| session/new | Reject missing / non-directory `cwd` |
|
|
260
|
+
|
|
261
|
+
See also `docs/AGY_ACP_MAP_ANALYSIS.zh-CN.md` (analysis kept; P0 items addressed in this release).
|
|
262
|
+
|
|
263
|
+
## Session store engineering (0.1.2) / 会话存储工程
|
|
264
|
+
|
|
265
|
+
| Area | Behavior |
|
|
266
|
+
|------|----------|
|
|
267
|
+
| Atomic write | Write `.<name>.<pid>.<ts>.tmp` then `rename` into place |
|
|
268
|
+
| `session/list` | Union of memory + disk; **memory wins** on same `sessionId` |
|
|
269
|
+
| `session/resume` | Memory hit → existing behavior; disk-only → rehydrate Session (no child); unknown → JSON-RPC `-32001` |
|
|
270
|
+
| `session/close` | Kill child, drop memory; **keep** disk row (unless `AGY_ACP_DELETE_ON_CLOSE=1`) |
|
|
271
|
+
| History | Never emits past turns on resume |
|
|
272
|
+
|
|
273
|
+
## Non-goals / 明确不做
|
|
274
|
+
|
|
275
|
+
- **OneShot `-p` backend**: not planned. Continuous ACP sessions use persistent `stream-json` only. Use the `agy` CLI directly for one-off CI/`-p` scripts.
|
|
276
|
+
- **OneShot `-p` 后端**:不做。ACP 连续会话只走常驻 `stream-json`;一次性脚本请直接用 `agy -p`。
|
|
277
|
+
- **Interactive ACP permission round-trip**:CLI stream-json 不支持;用 safety 三档 + settings allow。
|
|
278
|
+
- **History replay / `session/load` 重放**:不把全文存 bridge;Client(zustand/Zed)自管 transcript,bridge 只做 id 级 `resume`。
|
|
279
|
+
|
|
280
|
+
## Limitations / 限制
|
|
281
|
+
|
|
282
|
+
- MCP servers from `session/new` ignored (agy has its own).
|
|
283
|
+
- No ACP permission round-trip UI (`permissionRoundTrip: false`).
|
|
284
|
+
- Cancel = SIGINT then SIGKILL (no mid-turn stream cancel API).
|
|
285
|
+
- No history replay (`historyReplay: false`). Client / zustand owns transcript; store is id+config only.
|
|
286
|
+
- Image input depends on agy `view_file` / vision actually reading the staged path.
|
|
287
|
+
- Image output inlining is best-effort (path detection + 2MB cap); `generate_image` may be slow or gated.
|
|
288
|
+
- `usage_update.size` is a soft floor (200k).
|
|
289
|
+
- Structured `--json-schema` output: mapper surfaces `result.structured_output` as a fenced JSON `agent_message_chunk` plus `_meta.structuredOutput`.
|
|
290
|
+
- Third-party bridge — review Google ToS yourself. 第三方工程桥接,请自行评估 ToS。
|
|
291
|
+
|
|
292
|
+
## ToS note / 条款说明
|
|
293
|
+
|
|
294
|
+
Engineering feasibility ≠ legal permission. 即便只用官方 CLI I/O,仍可能受 ToS 约束。
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
## Bun + TypeScript / Windows
|
|
298
|
+
|
|
299
|
+
- Runtime is **Bun** (not Node). Entry: `bun src/server.ts` (or `bun run start`).
|
|
300
|
+
- Sources are TypeScript under `src/`; Bun runs `.ts` directly (no emit step).
|
|
301
|
+
- **Windows:** install [Bun](https://bun.sh), ensure `agy` is on `PATH`, then same commands (`bun src/server.ts`, `bun test`, `bun run smoke:all`).
|
|
302
|
+
- Unit tests: `bun test` (files `src/lib/*.test.ts`). Full live matrix: `bun run smoke:all`.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as v1 from '@agentclientprotocol/sdk';
|
|
2
|
+
import * as v2 from '@agentclientprotocol/sdk/experimental/v2';
|
|
3
|
+
import { type MapperState } from './lib/map-agy-to-acp.ts';
|
|
4
|
+
import { type SoftDenyInfo } from './lib/soft-deny.ts';
|
|
5
|
+
import { AgyProcessManager } from './lib/agy-process.ts';
|
|
6
|
+
export declare const AGENT_INFO: {
|
|
7
|
+
name: string;
|
|
8
|
+
title: string;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const BRIDGE_CAPABILITIES: {
|
|
12
|
+
prompt: boolean;
|
|
13
|
+
streaming: boolean;
|
|
14
|
+
tools: boolean;
|
|
15
|
+
resume: boolean;
|
|
16
|
+
permissionRoundTrip: boolean;
|
|
17
|
+
permissionMode: string;
|
|
18
|
+
safetyTiers: ("autonomous" | "autonomous-unsandboxed" | "safe")[];
|
|
19
|
+
nativeCancel: boolean;
|
|
20
|
+
cancelMode: string;
|
|
21
|
+
historyReplay: boolean;
|
|
22
|
+
dynamicConfig: string;
|
|
23
|
+
richContentInput: string;
|
|
24
|
+
richContentOutput: string;
|
|
25
|
+
clientFilesystem: boolean;
|
|
26
|
+
clientTerminal: boolean;
|
|
27
|
+
};
|
|
28
|
+
export interface SdkSession {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
cwd: string;
|
|
31
|
+
additionalDirectories?: string[];
|
|
32
|
+
createdAt: string;
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
title?: string;
|
|
35
|
+
proc: AgyProcessManager;
|
|
36
|
+
mapper: MapperState;
|
|
37
|
+
busy: boolean;
|
|
38
|
+
cancelled: boolean;
|
|
39
|
+
protocolVersion: number;
|
|
40
|
+
stderrBuf: string;
|
|
41
|
+
softDenies: SoftDenyInfo[];
|
|
42
|
+
softDenyEmitted: boolean;
|
|
43
|
+
stagedFiles: string[];
|
|
44
|
+
conversationId?: string;
|
|
45
|
+
model?: string;
|
|
46
|
+
effort?: string;
|
|
47
|
+
mode?: string;
|
|
48
|
+
agent?: string;
|
|
49
|
+
sandbox?: boolean;
|
|
50
|
+
jsonSchema?: string;
|
|
51
|
+
safety?: 'safe' | 'autonomous' | 'autonomous-unsandboxed';
|
|
52
|
+
skipPermissions?: boolean;
|
|
53
|
+
disableSlashCommands?: boolean;
|
|
54
|
+
printTimeout?: string;
|
|
55
|
+
}
|
|
56
|
+
export declare class AgyAcpService {
|
|
57
|
+
private sessions;
|
|
58
|
+
private sessionStore;
|
|
59
|
+
private catalogPromise;
|
|
60
|
+
constructor();
|
|
61
|
+
private persistSession;
|
|
62
|
+
private sessionMeta;
|
|
63
|
+
initialize(): Promise<{
|
|
64
|
+
agentInfo: {
|
|
65
|
+
name: string;
|
|
66
|
+
title: string;
|
|
67
|
+
version: string;
|
|
68
|
+
};
|
|
69
|
+
info: {
|
|
70
|
+
name: string;
|
|
71
|
+
title: string;
|
|
72
|
+
version: string;
|
|
73
|
+
};
|
|
74
|
+
capabilities: {
|
|
75
|
+
session: {};
|
|
76
|
+
};
|
|
77
|
+
bridgeCapabilities: {
|
|
78
|
+
prompt: boolean;
|
|
79
|
+
streaming: boolean;
|
|
80
|
+
tools: boolean;
|
|
81
|
+
resume: boolean;
|
|
82
|
+
permissionRoundTrip: boolean;
|
|
83
|
+
permissionMode: string;
|
|
84
|
+
safetyTiers: ("autonomous" | "autonomous-unsandboxed" | "safe")[];
|
|
85
|
+
nativeCancel: boolean;
|
|
86
|
+
cancelMode: string;
|
|
87
|
+
historyReplay: boolean;
|
|
88
|
+
dynamicConfig: string;
|
|
89
|
+
richContentInput: string;
|
|
90
|
+
richContentOutput: string;
|
|
91
|
+
clientFilesystem: boolean;
|
|
92
|
+
clientTerminal: boolean;
|
|
93
|
+
availableModels: any;
|
|
94
|
+
availableAgents: any;
|
|
95
|
+
configOptions: {
|
|
96
|
+
id: string;
|
|
97
|
+
name: string;
|
|
98
|
+
description: string;
|
|
99
|
+
category: string;
|
|
100
|
+
options: any;
|
|
101
|
+
}[];
|
|
102
|
+
};
|
|
103
|
+
_meta: {
|
|
104
|
+
bridgeCapabilities: {
|
|
105
|
+
prompt: boolean;
|
|
106
|
+
streaming: boolean;
|
|
107
|
+
tools: boolean;
|
|
108
|
+
resume: boolean;
|
|
109
|
+
permissionRoundTrip: boolean;
|
|
110
|
+
permissionMode: string;
|
|
111
|
+
safetyTiers: ("autonomous" | "autonomous-unsandboxed" | "safe")[];
|
|
112
|
+
nativeCancel: boolean;
|
|
113
|
+
cancelMode: string;
|
|
114
|
+
historyReplay: boolean;
|
|
115
|
+
dynamicConfig: string;
|
|
116
|
+
richContentInput: string;
|
|
117
|
+
richContentOutput: string;
|
|
118
|
+
clientFilesystem: boolean;
|
|
119
|
+
clientTerminal: boolean;
|
|
120
|
+
};
|
|
121
|
+
availableModels: any;
|
|
122
|
+
availableAgents: any;
|
|
123
|
+
configOptions: {
|
|
124
|
+
id: string;
|
|
125
|
+
name: string;
|
|
126
|
+
description: string;
|
|
127
|
+
category: string;
|
|
128
|
+
options: any;
|
|
129
|
+
}[];
|
|
130
|
+
};
|
|
131
|
+
}>;
|
|
132
|
+
newSession(params: any): Promise<{
|
|
133
|
+
sessionId: `${string}-${string}-${string}-${string}-${string}`;
|
|
134
|
+
_meta?: Record<string, unknown> | undefined;
|
|
135
|
+
}>;
|
|
136
|
+
listSessions(params?: any): Promise<{
|
|
137
|
+
sessions: any[];
|
|
138
|
+
}>;
|
|
139
|
+
closeSession(params: any): Promise<{}>;
|
|
140
|
+
cancelSession(params: any): void;
|
|
141
|
+
promptSession(params: any, notifyClient: (update: any) => Promise<void> | void): Promise<{
|
|
142
|
+
stopReason: string;
|
|
143
|
+
}>;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Creates an ACP v1 agent app wrapped with official @agentclientprotocol/sdk.
|
|
147
|
+
*/
|
|
148
|
+
export declare function createAcpV1App(service?: AgyAcpService): v1.AgentApp;
|
|
149
|
+
/**
|
|
150
|
+
* Creates an ACP v2 agent app wrapped with official @agentclientprotocol/sdk.
|
|
151
|
+
*/
|
|
152
|
+
export declare function createAcpV2App(service?: AgyAcpService): v2.AgentApp;
|
|
153
|
+
/**
|
|
154
|
+
* Dual router supporting both ACP v1 and v2 clients automatically.
|
|
155
|
+
*/
|
|
156
|
+
export declare function createDualAcpApp(service?: AgyAcpService): v2.AgentProtocolRouter;
|