@universal-agent/acp-extension-core 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lody
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 CHANGED
@@ -1,3 +1,211 @@
1
- # Temporary Holding Version
1
+ # ACP Extension Core
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
+ Provider-neutral contracts for the Lody extensions that sit on top of [Agent Client Protocol](https://agentclientprotocol.com/) v1.
4
+
5
+ Lody connects to many agent runtimes, and each one speaks its own dialect. ACP covers the common core, but features such as streaming subagent execution, durable goals, token accounting, and rate limits need a shared and stable description before clients can rely on them. This package is that description: a provider adapter translates its runtime into these contracts once, and any client that has negotiated the corresponding capability can read the result without knowing which runtime produced it.
6
+
7
+ Extensions are negotiated, not assumed. An agent advertises what it implements on `InitializeResponse.agentCapabilities._meta.lody`; a client advertises its own features on `InitializeRequest.clientCapabilities._meta.lody`. Each key carries its own `{ "version": 1 }`, and a missing key means the peer does not support that feature. Clients branch on the negotiated capability, never on which provider is behind the connection.
8
+
9
+ [中文](README.zh.md)
10
+
11
+ ## Reference implementations
12
+
13
+ The following public `acp-extension-*` repositories under [github.com/LodyAI](https://github.com/LodyAI) implement or consume this contract, as of 2026-10-01. This repository is the shared contract; the other eight are provider adapters that speak ACP v1 and map their provider onto the contracts above.
14
+
15
+ | Repository | What it covers |
16
+ | --- | --- |
17
+ | [acp-extension-core](https://github.com/LodyAI/acp-extension-core) | Shared v1 types, capability flags, and `_lody/` methods. This repository. |
18
+ | [acp-extension-claude](https://github.com/LodyAI/acp-extension-claude) | An ACP agent for the Claude Agent SDK, including Lody subagent events. |
19
+ | [acp-extension-codex](https://github.com/LodyAI/acp-extension-codex) | An ACP server for the Codex CLI / App Server, including fork, steer, and goals. |
20
+ | [acp-extension-devin](https://github.com/LodyAI/acp-extension-devin) | A proxy in front of Devin's native `devin acp`, translating Devin-only subagent traffic into Core events. |
21
+ | [acp-extension-dsh](https://github.com/LodyAI/acp-extension-dsh) | ACP session controls for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): models, permissions, subagents, compaction. |
22
+ | [acp-extension-grok](https://github.com/LodyAI/acp-extension-grok) | An ACP compatibility adapter for the official Grok runtime, including scheduled tasks and subagent events. |
23
+ | [acp-extension-kimi](https://github.com/LodyAI/acp-extension-kimi) | The Kimi Code CLI packaged for Lody. |
24
+ | [acp-extension-omp](https://github.com/LodyAI/acp-extension-omp) | The Oh My Pi adapter for Lody. |
25
+ | [acp-extension-pi](https://github.com/LodyAI/acp-extension-pi) | An ACP adapter for the pinned official Pi CLI (`--mode rpc`). |
26
+
27
+ ## Contents
28
+
29
+ - [Reference implementations](#reference-implementations)
30
+ - [Design principles](#design-principles)
31
+ - [Getting started](#getting-started)
32
+ - [How extensions are negotiated](#how-extensions-are-negotiated)
33
+ - [Capability catalog](#capability-catalog)
34
+ - [Custom `_lody/` methods](#custom-_lody-methods)
35
+ - [Metadata on standard ACP messages](#metadata-on-standard-acp-messages)
36
+ - [Shared rules](#shared-rules)
37
+ - [License](#license)
38
+
39
+ ## Design principles
40
+
41
+ Three placement rules, in order of preference, keep the contract small and interoperable:
42
+
43
+ 1. **Use standard ACP when it already fits.** `session/fork`, `elicitation/create`, `usage_update`, and the ordinary `tool_call` / `tool_call_update` lifecycle each already carry part of the behavior. Prefer them.
44
+ 2. **Attach Lody semantics to standard messages.** Where ACP has no field for something, put it under `_meta.lody.<feature>` on the message that expresses the behavior.
45
+ 3. **Add a `_lody/` method only when ACP has no equivalent.** The JSON-RPC namespace is reserved for requests and notifications that have no standard counterpart.
46
+
47
+ Every feature is versioned on its own. A `{ "version": 1 }` on one capability never implies support for another.
48
+
49
+ ## Getting started
50
+
51
+ ```bash
52
+ npm install acp-extension-core
53
+ ```
54
+
55
+ The package is mostly TypeScript types: capability shapes, the request and notification maps, and the metadata interfaces that define the contract. It depends on [`@agentclientprotocol/sdk`](https://www.npmjs.com/package/@agentclientprotocol/sdk) for the ACP wire types, and also ships a few runtime helpers:
56
+
57
+ - `SessionUsageAccumulator` — merges repeated usage snapshots into cumulative per-model totals plus a per-update delta.
58
+ - `LodySubagentEmitter` — maps native subagent executions onto opaque run IDs and emits ordered `_lody/subagents/event` messages.
59
+ - `createPlanModeConfigOption` — builds the standard `plan_mode` ACP config option.
60
+ - `isLodySubagentEvent`, `isLodySubagentSnapshot`, `isLodySubagentOutput` — guards for validating payloads at the wire boundary.
61
+ - `supportsLodySubagentEvents` — reads the negotiated `subagentEvents` flag from an ACP capabilities object.
62
+
63
+ A typical consumer checks the negotiated capability first, then routes by the exported method name and validates at the boundary:
64
+
65
+ ```ts
66
+ import {
67
+ LODY_EXTENSION_METHODS,
68
+ isLodySubagentEvent,
69
+ supportsLodySubagentEvents,
70
+ } from 'acp-extension-core';
71
+
72
+ if (supportsLodySubagentEvents(agentCapabilities)) {
73
+ onNotification(LODY_EXTENSION_METHODS.subagentEvent, (payload) => {
74
+ if (!isLodySubagentEvent(payload)) return;
75
+ // payload.runId is opaque; payload holds snapshot, progress, or output.
76
+ });
77
+ }
78
+ ```
79
+
80
+ To work on the package itself:
81
+
82
+ ```bash
83
+ npm install
84
+ npm run build # emit dist/
85
+ npm run typecheck # type-check without emitting
86
+ npm test # build, then run the contract tests
87
+ ```
88
+
89
+ ## How extensions are negotiated
90
+
91
+ Negotiation happens per feature, and some features require both directions.
92
+
93
+ - An **agent** advertises everything it implements under `InitializeResponse.agentCapabilities._meta.lody`.
94
+ - A **client** advertises its own features under `InitializeRequest.clientCapabilities._meta.lody`.
95
+ - Each key maps to an object containing at least `{ "version": 1 }`. A missing key means the peer does not support that feature, and support for one feature never implies support for another.
96
+ - A feature that needs both directions, such as `subagentEvents`, is active only after both peers advertise it. Agent-only features can be used as soon as the agent advertises them.
97
+
98
+ When ACP already provides a suitable message, the extension rides on it and no new method is added. Only when ACP has no equivalent request or notification does the contract define a `_lody/...` method.
99
+
100
+ ## Capability catalog
101
+
102
+ ### Agent capabilities (`LodyExtensionCapabilities`)
103
+
104
+ | Capability | What version 1 adds |
105
+ | --- | --- |
106
+ | `subagentEvents` | After **both** peers advertise it, the agent pushes child-run snapshots, progress, and output on [`_lody/subagents/event`](#custom-_lody-methods). Run IDs are opaque and never reuse native thread IDs; when observation is lost, the run state becomes `unknown` with `outputIncomplete: true`. `LodySubagentEmitter` is an optional adapter helper that covers one root session. |
107
+ | `sessionTitle` | The adapter owns automatic title generation and pushes a standard ACP `session_info_update` tagged with `_meta.lody.titleSource`. No request method is added. `generated` and `explicit` are authoritative title sources; `fallback` and `unset` are not. A title the user has set is always preserved. |
108
+ | `usage` | Context and token accounting. Prefer the standard ACP `usage_update`. Cumulative totals live in `modelUsage`; `delta` describes the newly accounted contribution and is already included in that total. The optional `_meta.lody.usageScopeId` makes `modelUsage` cumulative only within its scope, so a restarted adapter can begin a new scope. Missing cost means unknown, not zero. |
109
+ | `rateLimits` | Quota snapshots pushed on [`_lody/rate_limits/update`](#custom-_lody-methods). With `query: true`, the agent also accepts [`_lody/rate_limits/get`](#custom-_lody-methods). `windows` is the complete current list; a `label` is display-only, and windows with equal durations remain separate meters. |
110
+ | `forkAtTurn` | `session/fork` may carry `_meta.lody.forkAtTurn` (`{ version: 1, turnId? }`) to name the turn to fork from. |
111
+ | `steering` | Inject guidance into an in-flight turn. The agent advertises `transport` (`request` uses [`_lody/session/steer`](#custom-_lody-methods); `prompt` uses `_meta.lody.steer`), `upstreamTurn` (`same` or `handoff`), and `configPolicy` (`active` or `apply`). The result is `injected` or `failed`, and [`_lody/session/steer_applied`](#custom-_lody-methods) confirms the ID. |
112
+ | `tasks` | Background (`background: true`) and scheduled (`scheduled: true`) work. Lifecycle rides on ordinary `tool_call` / `tool_call_update` messages tagged with `_meta.lody.task` (`kind` is `background` or `scheduled`). |
113
+ | `subagents` | Subagent lifecycle on the same tool-call envelope (`kind: "subagent"`), independent of `subagentEvents`. `list`, `cancel`, and `output` each enable one query method. A client should show a control only when the individual run advertises it. |
114
+ | `goal` | A durable session goal rather than a prompt. `actions` lists everything the agent implements. `controlActions` are accepted on [`_lody/session/goal`](#custom-_lody-methods) while a prompt is in flight and never start a turn. `promptActions` travel on `session/prompt` as `_meta.lody.goalControl`; `set` and `resume` start work and therefore belong there, so the resulting turns stay attached to the client's own prompt. Read the transport from these two lists, not from `actions` alone. |
115
+ | `compaction` | Context compaction and retry, reported as a tool lifecycle tagged with `_meta.lody.activity` (`kind` is `context_compaction` or `retry`). |
116
+ | `sessionHistory` | Accepts [`_lody/session/history/read`](#custom-_lody-methods) for one session. The response body is empty. |
117
+ | `sessionConfig` | Client→agent startup metadata on new/load/resume/fork: `{ version: 1, modelId?, configOptionValues }`. Apply supported selections before native establishment; absence preserves native defaults. Explicit model selection takes precedence over a model option. |
118
+ | `sessionActivity` | The agent pushes [`_lody/session/activity_update`](#custom-_lody-methods) whenever its native state or its count of live background tasks changes, including between client prompts. A client uses it to tell an idle session from one that is waiting on a background shell, a monitor, or a subagent, or that is working on its own after one finished. |
119
+ | `worktreeProject` | On `session/new`, `session/load`, `session/resume`, and `session/fork`, `_meta.lody.worktreeProject` names the original project root (`originProjectPath`). ACP `cwd` remains the real execution directory. Omitting the field leaves the provider's ordinary project assignment unchanged. |
120
+
121
+ ### Client capabilities (`LodyClientExtensionCapabilities`)
122
+
123
+ | Capability | What version 1 adds |
124
+ | --- | --- |
125
+ | `subagentEvents` | The client can receive [`_lody/subagents/event`](#custom-_lody-methods). Required together with the agent flag. |
126
+ | `elicitation` | The client preserves Lody fields on the standard `elicitation/create`. `answerNotes: true` means a selected option and its separate note both survive editing, submission, and read-only display. `noteFor` adds a note; `customAnswerFor` replaces the referenced answer. The two do not combine. |
127
+
128
+ ### Standard ACP configuration
129
+
130
+ This option uses a fixed ID and needs no separate capability flag.
131
+
132
+ | Id | What version 1 adds |
133
+ | --- | --- |
134
+ | `plan_mode` | A boolean option created by `createPlanModeConfigOption` and set through `session/set_config_option`. Advertise it only for sessions that can plan independently. It leaves sandbox and approval policy unchanged. |
135
+
136
+ ### Canonical tool IDs
137
+
138
+ `LODY_TOOL_NAMES` defines the stable identities for tool flows that Lody handles specially. Adapters map provider-native names onto these values, and consumers never infer behavior from a human-facing title. The ID is sent as `_meta.lody.toolName`.
139
+
140
+ | ID | Flow |
141
+ | --- | --- |
142
+ | `ImageGeneration` | Image generation |
143
+ | `CronCreate` | Create a schedule |
144
+ | `CronDelete` | Delete a schedule |
145
+ | `CronList` | List schedules |
146
+ | `ScheduleWakeup` | Wake a scheduled run |
147
+
148
+ ## Custom `_lody/` methods
149
+
150
+ Method names and payload types are exported from `src/methods.ts`. `LodyExtensionRequestMap` and `LodyExtensionNotificationMap` bind each wire name to its DTO, so an adapter cannot implement a method against an unrelated type.
151
+
152
+ Requests:
153
+
154
+ | Method | Request | Response |
155
+ | --- | --- | --- |
156
+ | `_lody/rate_limits/get` | Optional filters `sessionId`, `accountId`, `modelId`. The call is not session-bound. | The current `RateLimitsSnapshot`. |
157
+ | `_lody/session/steer` | `sessionId`, `prompt` blocks, `steerId`. | `{ outcome: "injected" \| "failed" }`. |
158
+ | `_lody/session/goal` | `sessionId` plus `set` (with `objective`) or `pause` / `resume` / `clear`. | A `{ goal }` snapshot, or `null` when cleared. |
159
+ | `_lody/session/history/read` | `sessionId`. | An empty object. |
160
+ | `_lody/subagents/list` | `sessionId`, optional `activeOnly`. | `{ tasks }`. |
161
+ | `_lody/subagents/cancel` | `sessionId`, `taskId`, optional `reason`. | An empty object. |
162
+ | `_lody/subagents/output` | `sessionId`, `taskId`, optional `tail`. | `{ output }`. |
163
+
164
+ Notifications (agent to client):
165
+
166
+ | Method | Payload |
167
+ | --- | --- |
168
+ | `_lody/subagents/event` | `LodySubagentEvent`: `version: 1`, the root `sessionId`, an opaque `runId`, and then a `snapshot`, `progress`, or `output` (ACP text, thought, tool call, tool update, or plan). `isLodySubagentEvent` validates this boundary. Messages are delivered in connection order. |
169
+ | `_lody/session/usage_update` | `SessionUsageUpdate`: the latest operation in `usage`, cumulative per-model totals in `modelUsage`, and an optional `delta`. |
170
+ | `_lody/rate_limits/update` | The same snapshot shape as the query response. |
171
+ | `_lody/session/steer_applied` | `sessionId` and `steerId`. |
172
+ | `_lody/session/activity_update` | `LodySessionActivityUpdate`: `version: 1`, `sessionId`, `state` (`idle`, `running`, or `requires_action`), and `backgroundTaskCount`. Each update replaces the previous level; without one, assume `idle` with no tasks. |
173
+
174
+ ## Metadata on standard ACP messages
175
+
176
+ Each of these fields lives under `_meta.lody` on a message ACP already defines.
177
+
178
+ | Field | Envelope | Role |
179
+ | --- | --- | --- |
180
+ | `forkAtTurn` | `session/fork` | The optional source turn. |
181
+ | `elicitation` | `elicitation/create` | Questions, options, preview, secret, auto-resolve time, `customAnswerFor`, `noteFor`. |
182
+ | `task` | `tool_call`, `tool_call_update` | Subagent, background, or scheduled lifecycle (`LodyTaskMeta`). |
183
+ | `activity` | `tool_call`, `tool_call_update` | Compaction or retry (`LodyActivityMeta`). |
184
+ | `toolName` | `tool_call`, `tool_call_update` | A canonical ID from `LODY_TOOL_NAMES`. |
185
+ | `titleSource` | `session_info_update` | `explicit`, `generated`, `fallback`, or `unset`. |
186
+ | `goal` | session update | The agent-published `LodyGoalSnapshot`, or `null`. |
187
+ | `goalControl` | `session/prompt` | A client goal action. The prompt text is a fallback for when the action starts no native turn. |
188
+ | `steer` | `session/prompt` | `{ id }` for the prompt transport. |
189
+ | `notice` | session update | An `info`, `warning`, or `error` message. |
190
+ | `messagePhase` | session update | `commentary` or `final_answer`. |
191
+ | `worktreeProject` | `session/new`, `load`, `resume`, `fork` | `{ version: 1, originProjectPath }`. |
192
+ | `usageScopeId` | usage update | The accounting scope. Unique within the ACP session and never reused. |
193
+ | `turnId` | session meta | The turn ID, carried alongside the other session fields. |
194
+
195
+ ## Shared rules
196
+
197
+ - Each feature is advertised independently. Support for one `{ "version": 1 }` must not be treated as support for the others.
198
+ - Absolute timestamps are Unix epoch **seconds**, and the field name says so. Durations expressed in seconds also say so; `durationMs` is milliseconds.
199
+ - Provider adapters map native payloads into these contracts. Consumers do not branch on provider-specific bodies.
200
+ - Rate-limit `windows` is a complete replacement list. Never merge windows that share a duration, utilization, or reset time.
201
+ - An elicitation note is its own string property. It survives an option change, and editing it does not switch the question into custom-answer mode. Empty notes are omitted, and each note references exactly one question in the same schema.
202
+ - Usage token buckets are disjoint: input excludes cache reads and writes, and output excludes reasoning. `costUSD` is in US dollars and may be a documented estimate. Sum scopes independently, and never add `delta` on top of `modelUsage`.
203
+ - `SessionUsageAccumulator` is process-local. Keep one instance for the accounting lifetime of the session; replay and compaction must not reset it.
204
+ - Goal actions that only move state (`pause`, `clear`, and any other advertised `controlActions`) must be accepted mid-prompt and must not start a turn.
205
+ - `worktreeProject` grants no directory access, does not change `cwd`, and does not move worktree creation or cleanup onto the provider. An accepted but unresolvable root is an error.
206
+ - Plan mode does not promise a read-only sandbox. Claude's permission-mode plan switch is outside this contract.
207
+ - Adapters emit only the current contracts. Compatibility with older payloads belongs at the consumer boundary and should be time-bounded.
208
+
209
+ ## License
210
+
211
+ MIT — see [LICENSE](LICENSE).
package/README.zh.md ADDED
@@ -0,0 +1,211 @@
1
+ # ACP Extension Core
2
+
3
+ Lody 各项扩展的提供方无关契约,构建在 [Agent Client Protocol](https://agentclientprotocol.com/) v1 之上。
4
+
5
+ Lody 需要对接多种 agent 运行时,而它们各自使用不同的原生协议。ACP 覆盖了共同的核心部分,但子代理执行的流式事件、持久目标、token 记账、速率限制等能力,需要一份稳定、共享的描述,客户端才能真正依赖它们。本仓库提供的就是这份描述:适配器把某个提供方的数据翻译成这里的类型,任何协商到对应能力的客户端都能以同样的方式读取结果,无需关心背后是哪个运行时。
6
+
7
+ 扩展通过协商启用,而不是默认存在。Agent 在 `InitializeResponse.agentCapabilities._meta.lody` 上声明自己实现的能力,客户端在 `InitializeRequest.clientCapabilities._meta.lody` 上声明自己支持的能力。每个键都带各自的 `{ "version": 1 }`;缺少某个键就表示对方不支持该项能力。客户端根据协商结果分支,而不是根据连接背后的提供方分支。
8
+
9
+ [English](README.md)
10
+
11
+ ## 参考实现
12
+
13
+ 截至 2026-10-01,[github.com/LodyAI](https://github.com/LodyAI) 下公开的 `acp-extension-*` 仓库如下。本仓库是共享契约,其余八个是提供方适配器:它们使用 ACP v1,并把自己的提供方映射到上述契约。
14
+
15
+ | 仓库 | 覆盖范围 |
16
+ | --- | --- |
17
+ | [acp-extension-core](https://github.com/LodyAI/acp-extension-core) | 共享的 v1 类型、能力标志与 `_lody/` 方法。即本仓库。 |
18
+ | [acp-extension-claude](https://github.com/LodyAI/acp-extension-claude) | Claude Agent SDK 的 ACP agent,包含 Lody 子代理事件。 |
19
+ | [acp-extension-codex](https://github.com/LodyAI/acp-extension-codex) | Codex CLI / App Server 的 ACP 服务,包含 fork、steer 与 goal。 |
20
+ | [acp-extension-devin](https://github.com/LodyAI/acp-extension-devin) | Devin 原生 `devin acp` 之前的代理,把 Devin 私有的子代理流量翻译为 Core 事件。 |
21
+ | [acp-extension-dsh](https://github.com/LodyAI/acp-extension-dsh) | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的 ACP 会话控制:模型、权限、子代理与压缩。 |
22
+ | [acp-extension-grok](https://github.com/LodyAI/acp-extension-grok) | 官方 Grok 运行时的 ACP 兼容适配器,包含定时任务与子代理事件。 |
23
+ | [acp-extension-kimi](https://github.com/LodyAI/acp-extension-kimi) | 为 Lody 打包的 Kimi Code CLI。 |
24
+ | [acp-extension-omp](https://github.com/LodyAI/acp-extension-omp) | Lody 的 Oh My Pi 适配器。 |
25
+ | [acp-extension-pi](https://github.com/LodyAI/acp-extension-pi) | 针对固定版本官方 Pi CLI(`--mode rpc`)的 ACP 适配器。 |
26
+
27
+ ## 目录
28
+
29
+ - [参考实现](#参考实现)
30
+ - [设计原则](#设计原则)
31
+ - [开始使用](#开始使用)
32
+ - [扩展如何协商](#扩展如何协商)
33
+ - [能力一览](#能力一览)
34
+ - [自定义 `_lody/` 方法](#自定义-_lody-方法)
35
+ - [标准 ACP 消息上的元数据](#标准-acp-消息上的元数据)
36
+ - [共同约定](#共同约定)
37
+ - [许可证](#许可证)
38
+
39
+ ## 设计原则
40
+
41
+ 契约遵循三条放置规则,按优先级排列:
42
+
43
+ 1. **优先使用标准 ACP。** `session/fork`、`elicitation/create`、`usage_update` 以及普通的 `tool_call` / `tool_call_update` 生命周期本身已经承载了一部分行为,能复用就复用。
44
+ 2. **把 Lody 语义挂在标准消息上。** ACP 没有对应字段的内容,放到承载该行为的消息的 `_meta.lody.<feature>` 下。
45
+ 3. **只有 ACP 确实没有对应方法时,才新增 `_lody/` 方法。** 这个 JSON-RPC 命名空间只用于没有标准对应物的请求和通知。
46
+
47
+ 每项能力独立版本化。某一项能力上的 `{ "version": 1 }` 绝不代表其他能力也受支持。
48
+
49
+ ## 开始使用
50
+
51
+ ```bash
52
+ npm install acp-extension-core
53
+ ```
54
+
55
+ 这个包以 TypeScript 类型为主:能力结构、请求与通知映射,以及定义契约的元数据接口。它依赖 [`@agentclientprotocol/sdk`](https://www.npmjs.com/package/@agentclientprotocol/sdk) 提供 ACP 线路类型,同时也提供少量运行时辅助:
56
+
57
+ - `SessionUsageAccumulator`:把重复上报的用量快照合并为按模型累计的总量,并给出每次更新的增量。
58
+ - `LodySubagentEmitter`:把原生子代理执行映射为不透明的运行 id,并按顺序发送 `_lody/subagents/event` 消息。
59
+ - `createPlanModeConfigOption`:构造标准的 `plan_mode` ACP 配置项。
60
+ - `isLodySubagentEvent`、`isLodySubagentSnapshot`、`isLodySubagentOutput`:在消息边界做校验的运行时守卫。
61
+ - `supportsLodySubagentEvents`:从 ACP 能力对象中读取协商后的 `subagentEvents` 标志。
62
+
63
+ 典型用法是先确认协商结果,再按导出的方法名分发,并在边界处校验:
64
+
65
+ ```ts
66
+ import {
67
+ LODY_EXTENSION_METHODS,
68
+ isLodySubagentEvent,
69
+ supportsLodySubagentEvents,
70
+ } from 'acp-extension-core';
71
+
72
+ if (supportsLodySubagentEvents(agentCapabilities)) {
73
+ onNotification(LODY_EXTENSION_METHODS.subagentEvent, (payload) => {
74
+ if (!isLodySubagentEvent(payload)) return;
75
+ // payload.runId 不透明;payload 承载 snapshot、progress 或 output。
76
+ });
77
+ }
78
+ ```
79
+
80
+ 在本地开发这个包:
81
+
82
+ ```bash
83
+ npm install
84
+ npm run build # 生成 dist/
85
+ npm run typecheck # 只做类型检查
86
+ npm test # 先构建,再运行契约测试
87
+ ```
88
+
89
+ ## 扩展如何协商
90
+
91
+ 协商以能力为单位,部分能力需要双向确认。
92
+
93
+ - **Agent** 在 `InitializeResponse.agentCapabilities._meta.lody` 上声明自己实现的全部能力。
94
+ - **客户端** 在 `InitializeRequest.clientCapabilities._meta.lody` 上声明自己支持的能力。
95
+ - 每个键对应一个至少包含 `{ "version": 1 }` 的对象。缺少某个键表示对方不支持该能力;支持某一项也不代表支持其他项。
96
+ - 需要双向的能力(例如 `subagentEvents`)只有在双方都声明后才生效。仅属于 Agent 的能力在 Agent 声明后即可使用。
97
+
98
+ 如果 ACP 已经提供合适的消息,扩展就挂在标准消息上,不再新增方法。只有 ACP 没有等价的请求或通知时,契约才定义 `_lody/...` 方法。
99
+
100
+ ## 能力一览
101
+
102
+ ### Agent 能力(`LodyExtensionCapabilities`)
103
+
104
+ | 能力 | v1 额外提供的内容 |
105
+ | --- | --- |
106
+ | `subagentEvents` | **双方**都声明后,Agent 通过 [`_lody/subagents/event`](#自定义-_lody-方法) 推送子代理运行的快照、进度和输出。运行 id 不透明,绝不复用原生线程 id;观测丢失时,运行状态变为 `unknown`,并带上 `outputIncomplete: true`。`LodySubagentEmitter` 是可选的适配器辅助,覆盖一个根会话。 |
107
+ | `sessionTitle` | 自动标题由适配器负责,通过带 `_meta.lody.titleSource` 的标准 ACP `session_info_update` 推送,不新增请求方法。`generated` 和 `explicit` 是可信的标题来源,`fallback` 和 `unset` 不是。用户自己设置的标题始终保留。 |
108
+ | `usage` | 上下文与 token 记账。优先使用标准 ACP `usage_update`。累计总量在 `modelUsage`;`delta` 表示新增的记账部分,并且已经包含在该总量中。可选的 `_meta.lody.usageScopeId` 让 `modelUsage` 只在对应作用域内累计,适配器重启后可以开启新作用域。费用缺失表示未知,而不是零。 |
109
+ | `rateLimits` | 通过 [`_lody/rate_limits/update`](#自定义-_lody-方法) 推送额度快照。当 `query: true` 时,Agent 同时接受 [`_lody/rate_limits/get`](#自定义-_lody-方法)。`windows` 是当前完整列表;`label` 只用于展示,时长相同的窗口仍是相互独立的额度。 |
110
+ | `forkAtTurn` | `session/fork` 可以携带 `_meta.lody.forkAtTurn`(`{ version: 1, turnId? }`),指明从哪一轮分叉。 |
111
+ | `steering` | 向进行中的轮次注入指引。Agent 声明 `transport`(`request` 走 [`_lody/session/steer`](#自定义-_lody-方法),`prompt` 走 `_meta.lody.steer`)、`upstreamTurn`(`same` 或 `handoff`)和 `configPolicy`(`active` 或 `apply`)。结果是 `injected` 或 `failed`,由 [`_lody/session/steer_applied`](#自定义-_lody-方法) 确认对应 id。 |
112
+ | `tasks` | 后台任务(`background: true`)和定时任务(`scheduled: true`)。生命周期沿用普通的 `tool_call` / `tool_call_update`,并带 `_meta.lody.task`(`kind` 为 `background` 或 `scheduled`)。 |
113
+ | `subagents` | 子代理生命周期沿用同一套工具调用消息(`kind: "subagent"`),与 `subagentEvents` 相互独立。`list`、`cancel`、`output` 各自开启一个查询方法。只有当前运行声明了对应能力时,界面才应显示相应控件。 |
114
+ | `goal` | 持久的会话目标,而不是一条 prompt。`actions` 列出 Agent 实现的全部动作。`controlActions` 是 prompt 进行期间可经 [`_lody/session/goal`](#自定义-_lody-方法) 接受的动作,它们不会开启新轮次。`promptActions` 随 `session/prompt` 的 `_meta.lody.goalControl` 传递;`set` 和 `resume` 会开始工作,因此必须走这条通道,由此产生的轮次才归属于客户端自己的 prompt。传输方式请以这两份列表为准,不能只看 `actions`。 |
115
+ | `compaction` | 上下文压缩与重试,以工具调用生命周期上报,并带 `_meta.lody.activity`(`kind` 为 `context_compaction` 或 `retry`)。 |
116
+ | `sessionHistory` | 接受针对单个会话的 [`_lody/session/history/read`](#自定义-_lody-方法)。响应体为空。 |
117
+ | `sessionConfig` | 新建、加载、恢复、分叉时的客户端启动元数据:`{ version: 1, modelId?, configOptionValues }`。支持的选择在原生会话建立前应用;省略时保留原生默认行为。显式模型选择优先于模型选项。 |
118
+ | `sessionActivity` | 当原生状态或存活后台任务数变化时,Agent 推送 [`_lody/session/activity_update`](#自定义-_lody-方法),客户端 prompt 之间也会推送。客户端据此区分空闲会话,与在等后台 shell、monitor 或 subagent、或在它们结束后自行继续工作的会话。 |
119
+ | `worktreeProject` | 在 `session/new`、`session/load`、`session/resume`、`session/fork` 上,`_meta.lody.worktreeProject` 指明原始项目根(`originProjectPath`)。ACP `cwd` 仍然是实际执行目录。省略该字段则保持提供方原有的项目归属不变。 |
120
+
121
+ ### 客户端能力(`LodyClientExtensionCapabilities`)
122
+
123
+ | 能力 | v1 额外提供的内容 |
124
+ | --- | --- |
125
+ | `subagentEvents` | 客户端可以接收 [`_lody/subagents/event`](#自定义-_lody-方法)。必须与 Agent 侧的声明同时存在。 |
126
+ | `elicitation` | 客户端在标准 `elicitation/create` 上保留 Lody 字段。`answerNotes: true` 表示用户选中的选项和单独的附注在编辑、提交和只读展示中都会保留。`noteFor` 是附加说明;`customAnswerFor` 会替换所引用的答案。二者不同时使用。 |
127
+
128
+ ### 标准 ACP 配置
129
+
130
+ 这一项使用固定 id,不需要单独的能力标志。
131
+
132
+ | Id | v1 额外提供的内容 |
133
+ | --- | --- |
134
+ | `plan_mode` | 由 `createPlanModeConfigOption` 创建、通过 `session/set_config_option` 设置的布尔选项。只对支持独立规划的会话声明。沙箱和审批策略保持原样。 |
135
+
136
+ ### 稳定工具 id
137
+
138
+ `LODY_TOOL_NAMES` 定义了 Lody 特殊处理的一批工具流程的稳定身份。适配器把提供方的原生名称映射到这些值,消费方不应根据面向用户的标题推断行为。该 id 通过 `_meta.lody.toolName` 传递。
139
+
140
+ | Id | 流程 |
141
+ | --- | --- |
142
+ | `ImageGeneration` | 图像生成 |
143
+ | `CronCreate` | 创建定时任务 |
144
+ | `CronDelete` | 删除定时任务 |
145
+ | `CronList` | 列出定时任务 |
146
+ | `ScheduleWakeup` | 唤醒一次已排程的运行 |
147
+
148
+ ## 自定义 `_lody/` 方法
149
+
150
+ 方法名和载荷类型从 `src/methods.ts` 导出。`LodyExtensionRequestMap` 与 `LodyExtensionNotificationMap` 把每个协议方法名绑定到对应的 DTO,适配器无法把某个方法实现成不相关的类型。
151
+
152
+ 请求:
153
+
154
+ | 方法 | 请求 | 响应 |
155
+ | --- | --- | --- |
156
+ | `_lody/rate_limits/get` | 可选过滤 `sessionId`、`accountId`、`modelId`。该调用不绑定会话。 | 当前的 `RateLimitsSnapshot`。 |
157
+ | `_lody/session/steer` | `sessionId`、`prompt` 内容块、`steerId`。 | `{ outcome: "injected" \| "failed" }`。 |
158
+ | `_lody/session/goal` | `sessionId`,以及 `set`(带 `objective`)或 `pause` / `resume` / `clear`。 | `{ goal }` 快照;清除后为 `null`。 |
159
+ | `_lody/session/history/read` | `sessionId`。 | 空对象。 |
160
+ | `_lody/subagents/list` | `sessionId`,可选 `activeOnly`。 | `{ tasks }`。 |
161
+ | `_lody/subagents/cancel` | `sessionId`、`taskId`,可选 `reason`。 | 空对象。 |
162
+ | `_lody/subagents/output` | `sessionId`、`taskId`,可选 `tail`。 | `{ output }`。 |
163
+
164
+ 通知(Agent 到客户端):
165
+
166
+ | 方法 | 载荷 |
167
+ | --- | --- |
168
+ | `_lody/subagents/event` | `LodySubagentEvent`:`version: 1`、根 `sessionId`、不透明 `runId`,然后是 `snapshot`、`progress` 或 `output`(ACP 文本、思考、工具调用、工具更新或计划)。`isLodySubagentEvent` 负责校验这一边界。消息按连接顺序投递。 |
169
+ | `_lody/session/usage_update` | `SessionUsageUpdate`:`usage` 是最近一次操作,`modelUsage` 是按模型累计的总量,`delta` 可选。 |
170
+ | `_lody/rate_limits/update` | 与查询响应相同的快照结构。 |
171
+ | `_lody/session/steer_applied` | `sessionId` 和 `steerId`。 |
172
+ | `_lody/session/activity_update` | `LodySessionActivityUpdate`:`version: 1`、`sessionId`、`state`(`idle`、`running` 或 `requires_action`)和 `backgroundTaskCount`。每次更新替换上一次的状态;未收到时按 `idle` 且无任务处理。 |
173
+
174
+ ## 标准 ACP 消息上的元数据
175
+
176
+ 下面这些字段都位于 ACP 已有消息的 `_meta.lody` 下。
177
+
178
+ | 字段 | 所在消息 | 作用 |
179
+ | --- | --- | --- |
180
+ | `forkAtTurn` | `session/fork` | 可选的来源轮次。 |
181
+ | `elicitation` | `elicitation/create` | 问题、选项、预览、密文、自动解析时间、`customAnswerFor`、`noteFor`。 |
182
+ | `task` | `tool_call`、`tool_call_update` | 子代理、后台或定时任务的生命周期(`LodyTaskMeta`)。 |
183
+ | `activity` | `tool_call`、`tool_call_update` | 压缩或重试(`LodyActivityMeta`)。 |
184
+ | `toolName` | `tool_call`、`tool_call_update` | `LODY_TOOL_NAMES` 中的稳定 id。 |
185
+ | `titleSource` | `session_info_update` | `explicit`、`generated`、`fallback` 或 `unset`。 |
186
+ | `goal` | 会话更新 | Agent 发布的 `LodyGoalSnapshot`,或 `null`。 |
187
+ | `goalControl` | `session/prompt` | 客户端发起的目标动作。当该动作没有开启原生轮次时,prompt 正文只是后备文本。 |
188
+ | `steer` | `session/prompt` | `prompt` 传输方式下的 `{ id }`。 |
189
+ | `notice` | 会话更新 | `info`、`warning` 或 `error` 消息。 |
190
+ | `messagePhase` | 会话更新 | `commentary` 或 `final_answer`。 |
191
+ | `worktreeProject` | `session/new`、`load`、`resume`、`fork` | `{ version: 1, originProjectPath }`。 |
192
+ | `usageScopeId` | 用量更新 | 记账作用域。在同一个 ACP 会话内唯一,且不会被复用。 |
193
+ | `turnId` | 会话元数据 | 与其他会话字段一起携带的轮次 id。 |
194
+
195
+ ## 共同约定
196
+
197
+ - 每项能力独立声明,支持其中一项绝不意味着其他项也可用。
198
+ - 绝对时间戳是 Unix 纪元**秒**,字段名会写明单位;以秒计的时长同样写明单位,`durationMs` 则以毫秒计。
199
+ - 提供方适配器负责把原生载荷映射为这些契约,消费方不应根据提供方私有正文分支。
200
+ - 速率限制的 `windows` 是完整的替换列表。时长、使用率或重置时间相同的窗口也不得合并。
201
+ - 提问附注是独立的字符串字段:更换选项时它仍然保留,编辑它也不会把该问题切换成自定义答案模式。空附注应省略,每条附注只引用同一 schema 中的一个问题。
202
+ - 用量分桶互不重叠:input 不含缓存读和缓存写,output 不含推理 token。`costUSD` 以美元计,可以是有说明的估算值。各作用域独立求和,不要把 `delta` 叠加到 `modelUsage` 上。
203
+ - `SessionUsageAccumulator` 只在当前进程内有效。同一会话的记账生命周期内应保持同一个实例;重放和压缩都不得重置它。
204
+ - 只移动状态的目标动作(`pause`、`clear`,以及 `controlActions` 中声明的其他动作)必须能在 prompt 进行中接受,且不得开启新轮次。
205
+ - `worktreeProject` 不授予目录访问权限,不改变 `cwd`,也不会把工作树的创建或清理交给提供方。若接受该扩展却无法解析项目根,应返回错误。
206
+ - Plan mode 不承诺沙箱变为只读;Claude 基于权限模式的 Plan 切换不属于本契约。
207
+ - 适配器只发送当前版本的契约;对旧版载荷的兼容应放在消费方边界,并设定明确的退役时间。
208
+
209
+ ## 许可证
210
+
211
+ MIT,详见 [LICENSE](LICENSE)。
@@ -0,0 +1,85 @@
1
+ export type VersionOneCapability = {
2
+ version: 1;
3
+ };
4
+ /** Client support for Core semantics layered on standard ACP form elicitation. */
5
+ export type ElicitationCapability = VersionOneCapability & {
6
+ /** Preserve a selection and its optional, separately keyed note end to end. */
7
+ answerNotes?: true;
8
+ };
9
+ /** Advertised under InitializeRequest.clientCapabilities._meta.ua (or _meta.lody). */
10
+ export type ClientExtensionCapabilities = {
11
+ subagentEvents?: VersionOneCapability;
12
+ elicitation?: ElicitationCapability;
13
+ };
14
+ export type SteeringCapability = VersionOneCapability & {
15
+ transport: 'request' | 'prompt';
16
+ upstreamTurn: 'same' | 'handoff';
17
+ configPolicy: 'active' | 'apply';
18
+ };
19
+ export type GoalAction = 'set' | 'pause' | 'resume' | 'clear';
20
+ export type GoalCapability = VersionOneCapability & {
21
+ /** Every action the agent implements, on any transport. */
22
+ actions: readonly GoalAction[];
23
+ /**
24
+ * Actions the agent accepts on `_ua/session/goal` (`_lody/session/goal`) while a prompt is in
25
+ * flight. These only move durable goal state and never start a turn, so a
26
+ * client can send them without owning the session's prompt slot.
27
+ */
28
+ controlActions?: readonly GoalAction[];
29
+ /**
30
+ * Actions the agent accepts through `prompt._meta.ua.goalControl` (`prompt._meta.lody.goalControl`). Actions
31
+ * that start work appear only here: running them inside the client's own
32
+ * prompt is what keeps the resulting turns attributable to a conversation.
33
+ */
34
+ promptActions?: readonly GoalAction[];
35
+ };
36
+ export type SubagentCapability = VersionOneCapability & {
37
+ lifecycle: true;
38
+ list?: true;
39
+ cancel?: true;
40
+ output?: true;
41
+ };
42
+ export type TaskCapability = VersionOneCapability & {
43
+ background?: true;
44
+ scheduled?: true;
45
+ };
46
+ export type RateLimitsCapability = VersionOneCapability & {
47
+ query?: true;
48
+ };
49
+ export type ExtensionCapabilities = {
50
+ subagentEvents?: VersionOneCapability;
51
+ /** Owns title generation; pushes tagged session_info_update titles without a client request. */
52
+ sessionTitle?: VersionOneCapability;
53
+ usage?: VersionOneCapability;
54
+ rateLimits?: RateLimitsCapability;
55
+ forkAtTurn?: VersionOneCapability;
56
+ steering?: SteeringCapability;
57
+ tasks?: TaskCapability;
58
+ subagents?: SubagentCapability;
59
+ goal?: GoalCapability;
60
+ compaction?: VersionOneCapability;
61
+ sessionHistory?: VersionOneCapability;
62
+ worktreeProject?: VersionOneCapability;
63
+ /** Pushes `_ua/session/activity_update` (`_lody/session/activity_update`) levels, including outside client prompts. */
64
+ sessionActivity?: VersionOneCapability;
65
+ };
66
+ /** @deprecated Use `VersionOneCapability` instead. */
67
+ export type LodyVersionOneCapability = VersionOneCapability;
68
+ /** @deprecated Use `ElicitationCapability` instead. */
69
+ export type LodyElicitationCapability = ElicitationCapability;
70
+ /** @deprecated Use `ClientExtensionCapabilities` instead. */
71
+ export type LodyClientExtensionCapabilities = ClientExtensionCapabilities;
72
+ /** @deprecated Use `SteeringCapability` instead. */
73
+ export type LodySteeringCapability = SteeringCapability;
74
+ /** @deprecated Use `GoalAction` instead. */
75
+ export type LodyGoalAction = GoalAction;
76
+ /** @deprecated Use `GoalCapability` instead. */
77
+ export type LodyGoalCapability = GoalCapability;
78
+ /** @deprecated Use `SubagentCapability` instead. */
79
+ export type LodySubagentCapability = SubagentCapability;
80
+ /** @deprecated Use `TaskCapability` instead. */
81
+ export type LodyTaskCapability = TaskCapability;
82
+ /** @deprecated Use `RateLimitsCapability` instead. */
83
+ export type LodyRateLimitsCapability = RateLimitsCapability;
84
+ /** @deprecated Use `ExtensionCapabilities` instead. */
85
+ export type LodyExtensionCapabilities = ExtensionCapabilities;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Metadata for ACP form elicitation details that JSON Schema cannot express.
3
+ * The same shape is used at request, property, and enum-option scope; consumers
4
+ * read only the fields meaningful at that scope.
5
+ */
6
+ export type ElicitationOption = {
7
+ label: string;
8
+ description?: string;
9
+ preview?: string;
10
+ };
11
+ /** An optional additive note, stored under its own key in `answers`. */
12
+ export type ElicitationAnswerNote = {
13
+ /** The schema property key. Must not equal any question id or another note key. */
14
+ fieldId: string;
15
+ title?: string;
16
+ description?: string;
17
+ /** Applies to the note independently of the question's answer. */
18
+ isSecret?: boolean;
19
+ };
20
+ export type ElicitationQuestion = {
21
+ id?: string;
22
+ question: string;
23
+ header: string;
24
+ options: ElicitationOption[];
25
+ multiSelect: boolean;
26
+ allowCustomAnswer?: boolean;
27
+ isSecret?: boolean;
28
+ /** Does not enable a custom answer or replace the selected option(s). */
29
+ note?: ElicitationAnswerNote;
30
+ };
31
+ export type ElicitationAnswer = string | string[];
32
+ export type ElicitationMeta = {
33
+ version: 1;
34
+ autoResolveAfterSeconds?: number | null;
35
+ autoResolveAtEpochSeconds?: number;
36
+ /** Property scope: an alternative answer replacing the referenced selection. */
37
+ customAnswerFor?: string;
38
+ /**
39
+ * Property scope: an optional string note accompanying the referenced answer.
40
+ * Mutually exclusive with customAnswerFor. References a question property in
41
+ * the same schema; never another note or a custom-answer property.
42
+ */
43
+ noteFor?: string;
44
+ secret?: boolean;
45
+ preview?: string;
46
+ questions?: ElicitationQuestion[];
47
+ answers?: Record<string, ElicitationAnswer>;
48
+ };
49
+ /** @deprecated Use `ElicitationOption` instead. */
50
+ export type LodyElicitationOption = ElicitationOption;
51
+ /** @deprecated Use `ElicitationAnswerNote` instead. */
52
+ export type LodyElicitationAnswerNote = ElicitationAnswerNote;
53
+ /** @deprecated Use `ElicitationQuestion` instead. */
54
+ export type LodyElicitationQuestion = ElicitationQuestion;
55
+ /** @deprecated Use `ElicitationAnswer` instead. */
56
+ export type LodyElicitationAnswer = ElicitationAnswer;
57
+ /** @deprecated Use `ElicitationMeta` instead. */
58
+ export type LodyElicitationMeta = ElicitationMeta;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export * from './capabilities.js';
2
+ export * from './elicitation.js';
3
+ export * from './methods.js';
4
+ export * from './rate-limits.js';
5
+ export * from './session.js';
6
+ export * from './usage.js';
7
+ export * from './plan-mode.js';
8
+ export * from './subagent-events.js';
9
+ export * from './subagent-emitter.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from './capabilities.js';
2
+ export * from './elicitation.js';
3
+ export * from './methods.js';
4
+ export * from './rate-limits.js';
5
+ export * from './session.js';
6
+ export * from './usage.js';
7
+ export * from './plan-mode.js';
8
+ export * from './subagent-events.js';
9
+ export * from './subagent-emitter.js';