@ryubyte/dsh-a2a 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md ADDED
@@ -0,0 +1,267 @@
1
+ # dsh-a2a
2
+
3
+ [中文文档](./README.md) | **English**
4
+
5
+ A **DeepSeek Harness (DSH)** plugin that implements the **Agent2Agent (A2A) Protocol v1.0** (Linux Foundation, `a2aproject/A2A`, Apache-2.0).
6
+
7
+ It turns a DSH instance into a full A2A peer in both directions:
8
+
9
+ - **Client mode (outbound)** — discover a remote agent via its **AgentCard**, map each **skill** to a DSH model tool (`a2a__<name>__<skill>`), and execute remote tasks through A2A **JSON-RPC**.
10
+ - **Server mode (inbound)** — expose this DSH to other agents: `/.well-known/agent-card.json` plus a JSON-RPC endpoint (`SendMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SendStreamingMessage` over SSE) on the DSH webserver.
11
+
12
+ ## Features
13
+
14
+ ### Protocol surface (A2A v1.0)
15
+
16
+ - **Discovery**: `GET /.well-known/agent-card.json` (well-known first, configured URL fallback)
17
+ - **Unary**: `SendMessage`, `GetTask`, `ListTasks`, `CancelTask`, `GetExtendedAgentCard`
18
+ - **Streaming**: `SendStreamingMessage` (SSE) / `SubscribeToTask`, with `TaskStatusUpdateEvent` + `TaskArtifactUpdateEvent`
19
+ - **Task lifecycle**: `SUBMITTED → WORKING → COMPLETED / FAILED / CANCELED / REJECTED / INPUT_REQUIRED / AUTH_REQUIRED`
20
+ - **Auth**: outbound sends a per-agent Bearer token (`Authorization: Bearer …`); inbound can protect the endpoint with a shared Bearer token — when set, the AgentCard declares a `bearerAuth` scheme and requests without/with a wrong token get 401 (both unary calls and the SSE stream)
21
+ - **Types**: `Task`, `Message`, `Part` (text/raw/url/data), `Artifact`, `AgentCard`, security schemes, extensions
22
+
23
+ ### Connection dashboard (设置 → A2A 连接)
24
+
25
+ - **Outbound agents** — remote agents this DSH connects to (name, connection state, AgentCard URL, skill/tool counts, last activity), each with **Reconnect / Enable / Disable / Delete** controls. When adding an agent you can supply an optional **Bearer token** — it is sent as `Authorization: Bearer …` on both the AgentCard fetch and every JSON-RPC call, and persisted alongside the agent in `a2a.json`.
26
+ - **Inbound peers** — who is calling this DSH (label, source address, first/last seen, task count, streaming flag), with a close control.
27
+ - **A2A server** — inbound server status with an online/offline toggle, endpoint and skill overview; it also shows **who executes inbound tasks** (custom executor / this DSH's agent session / none) and an **inbound auth** control to set or clear the shared Bearer token that gates the endpoint (persisted to `a2a.json`, effective immediately).
28
+ - The panel refreshes every 3 s; `/a2a/api` serves a JSON snapshot and control endpoints, fenced to loopback/same-origin (the token value is never returned in the snapshot — only whether one is configured).
29
+
30
+ ### Runtime configuration (a2a.json)
31
+
32
+ Configuration is driven by **`a2a.json`** (per profile, UI-editable) — manage
33
+ agents without touching the composition. The file is resolved from the active
34
+ profile, not the launch directory: the plugin parses `--profile <name>` from
35
+ the process command line and reads/writes `~/.dsh/profiles/<name>/a2a.json`.
36
+
37
+ ## Screenshots
38
+
39
+ | Outbound agents (who this DSH connects to) | Inbound peers (who calls this DSH) |
40
+ |---|---|
41
+ | ![Outbound agents dashboard](docs/screenshots/dashboard-outbound.png) | ![Inbound peers dashboard](docs/screenshots/dashboard-inbound.png) |
42
+
43
+ | Inbound with a live record | A2A server status panel |
44
+ |---|---|
45
+ | ![Inbound peer record](docs/screenshots/server-inbound.png) | ![A2A server status](docs/screenshots/server-serve.png) |
46
+
47
+ ## Installation
48
+
49
+ ### Option 1: from npm (recommended)
50
+
51
+ Once published to npm, install into the target profile with one command and
52
+ restart:
53
+
54
+ dsh plugin --profile <profile> add @ryubyte/dsh-a2a
55
+ dsh --profile <profile>
56
+
57
+ `dsh plugin` installs the package into the profile and auto-registers the
58
+ plugin layer (this package declares `dsh.bundle.patch` in `package.json`) — no
59
+ manual configuration is needed.
60
+
61
+ ### Option 2: from the repository (development)
62
+
63
+ For hacking on the code, requires Node.js >= 22:
64
+
65
+ # 1. Clone the repo
66
+ git clone https://github.com/ryubyte/dsh-a2a.git
67
+ cd dsh-a2a
68
+
69
+ # 2. Install deps and build
70
+ npm install
71
+ npm run build
72
+
73
+ # 3. Link into the target profile
74
+ dsh plugin --profile <profile> add link:$(pwd)
75
+
76
+ # 4. Restart
77
+ dsh --profile <profile>
78
+
79
+ After changing source, re-run `npm run build` and restart.
80
+
81
+ ## Usage
82
+
83
+ ### 1. Configure a remote agent (outbound)
84
+
85
+ Declare agents to connect to in the profile's `a2a.json`:
86
+
87
+ ```jsonc
88
+ {
89
+ "agents": [
90
+ {
91
+ "name": "research",
92
+ "agentCardUrl": "https://research.example.com/.well-known/agent-card.json",
93
+ // "bearerToken": "...",
94
+ // "timeoutMs": 8000,
95
+ // "mapSkills": true,
96
+ // "enabled": false // keep the entry, but don't auto-connect
97
+ }
98
+ ]
99
+ }
100
+ ```
101
+
102
+ Alternatively, in the Web GUI go to 设置 → A2A 连接 → 添加 Agent, paste an
103
+ AgentCard URL to import and connect — the config is written back to
104
+ `a2a.json`.
105
+
106
+ Once connected, the remote agent's skills appear as model tools:
107
+ `a2a__<name>__<skill>`.
108
+
109
+ A local agent session's repeated tool calls to a given remote agent reuse one
110
+ A2A `contextId`, so they land in the same remote session with cross-turn memory;
111
+ different local sessions are isolated into separate conversations.
112
+
113
+ ### 2. Expose this DSH as an A2A agent (inbound)
114
+
115
+ Enable server mode in `a2a.json`:
116
+
117
+ ```jsonc
118
+ {
119
+ "mode": "server",
120
+ "server": {
121
+ "enabled": true, // explicitly turn the inbound server on
122
+ // "baseUrl": "http://127.0.0.1:3080", // optional; default: derive from webServer
123
+ "agentName": "My DSH Agent",
124
+ "agentDescription": "A DSH agent over A2A v1.0",
125
+ "agentVersion": "0.1.0",
126
+ "skills": [
127
+ { "id": "coding", "name": "Coding", "description": "Execute coding and shell tasks.", "tags": ["coding", "shell"] }
128
+ ]
129
+ }
130
+ }
131
+ ```
132
+
133
+ Other A2A clients discover this DSH via
134
+ `http://<host>:<port>/.well-known/agent-card.json` and call the `/a2a` endpoint.
135
+
136
+ #### Who executes an inbound task (executor precedence)
137
+
138
+ On each inbound task the server picks an executor in this order:
139
+
140
+ 1. **Explicit `execute`** (injected programmatically) — yours wins.
141
+ 2. **This DSH's own agent session** (the default, when an agent loop is
142
+ present) — **one session per A2A `contextId`**: the first task for a context
143
+ `ctx.agents.create()`s a session (later tasks for the same context reuse it,
144
+ so history accumulates; a restarted process `resume`s it on demand), sends
145
+ the message as a `next-turn` waking item, awaits `whenIdle()`, and takes the
146
+ latest assistant reply as the A2A artifact. Concurrent tasks for one context
147
+ are serialized; distinct contexts run in parallel. Sessions are not disposed
148
+ per task (that would defeat context accumulation) — all are disposed on
149
+ plugin unload/disable. Turning on server mode is enough — no code.
150
+ 3. **`notConfiguredExecutor`** (fallback) — when nothing is injected *and* no
151
+ usable agent loop is composed (e.g. a bare dsh-base profile), it returns
152
+ "no executor configured — inject one" and **runs nothing**.
153
+
154
+ > On "present": `ctx.agents` (the registry) existing ≠ being able to create an
155
+ > agent. `create()` needs a factory registered by an agent-loop plugin; without
156
+ > it `create()` rejects and the DSH agent executor surfaces that as a readable
157
+ > error rather than throwing. So a minimal profile still needs an explicit
158
+ > executor.
159
+ >
160
+ > Session ↔ cwd: a DSH session validates `cwd` as an absolute path and freezes
161
+ > it into the session header at creation. Inbound sessions use a **dedicated**
162
+ > cwd `<profile>/.a2a-sessions` (kept separate from the profile root so they
163
+ > don't crowd your interactive sessions). They appear in the session list's
164
+ > "Ungrouped" bucket, each named `A2A: <first-prompt summary>` (inbound messages
165
+ > carry a plugin source, which DSH's automatic titling ignores, so we name it
166
+ > explicitly). Naming is cosmetic — failures are logged, never failing the task.
167
+ >
168
+ > Cancellation: `CancelTask` aborts the running task's executor signal, actually
169
+ > stopping the agent turn (not just flipping task status).
170
+ >
171
+ > For local testing you may still pass `shellExecutor`, which runs the prompt as
172
+ > a `/bin/sh -c` command — trusted clients only. For production put a TLS
173
+ > reverse proxy in front and set `authToken` as needed.
174
+
175
+ ### 3. Programmatic use
176
+
177
+ ```ts
178
+ import { A2AClient, A2AServer, TaskStore } from '@ryubyte/dsh-a2a';
179
+
180
+ // outbound: connect to a remote agent
181
+ const client = await A2AClient.connect('https://agent.example.com');
182
+ const { task } = await client.sendMessage({
183
+ messageId: crypto.randomUUID(),
184
+ role: 'ROLE_USER',
185
+ parts: [{ text: 'Draft a report' }],
186
+ });
187
+ for await (const ev of client.streamMessage({ messageId: crypto.randomUUID(), role: 'ROLE_USER', parts: [{ text: 'hi' }] })) {
188
+ // ev.statusUpdate | ev.artifactUpdate | ev.task | ev.message
189
+ }
190
+
191
+ // inbound: wire a custom executor to your own agent loop
192
+ const store = new TaskStore();
193
+ const server = new A2AServer({
194
+ baseUrl: 'http://127.0.0.1:3080',
195
+ agentName: 'My DSH',
196
+ agentDescription: '...',
197
+ agentVersion: '1.0.0',
198
+ execute: async ({ message }) => ({
199
+ messageId: crypto.randomUUID(),
200
+ role: 'ROLE_AGENT',
201
+ parts: [{ text: await myAgentRun(message.parts) }],
202
+ }),
203
+ }, store);
204
+ ```
205
+
206
+ ## Directory structure
207
+
208
+ src/
209
+ protocol.ts # A2A v1.0 types & constants
210
+ card.ts # AgentCard discovery / validation
211
+ jsonrpc.ts # JSON-RPC 2.0 + SSE transport
212
+ client.ts # outbound client (A2AClient)
213
+ server.ts # inbound server + TaskStore (A2AServer)
214
+ outbound.ts # skill → model-tool bridge (registerAgentTools)
215
+ dashboard.ts # connection registry + control
216
+ a2a-config.ts # runtime a2a.json parsing / persistence
217
+ index.ts # Cordis plugin entry (client / server modes)
218
+ client/
219
+ index.ts # browser half: settings "A2A 连接" panel
220
+ lib/ # build output (index.js + client.js + types/)
221
+ scripts/
222
+ build-client.mjs # wrap client bundle (__ModuleLoader__)
223
+ capture-shots.mjs # README screenshot capture (headless Chrome + CDP)
224
+ cordis.patch.yml # plugin registration row (self-mounted bundle)
225
+ package.json # dsh.bundle.patch + dsh.client manifest
226
+
227
+ ## How it works
228
+
229
+ - The plugin reads `ctx.webServer` and `ctx.tools` as context properties inside
230
+ `ctx.inject(...)` / `ctx.effect(...)` — the native Cordis pattern used by DSH
231
+ bundles (`dsh-client-hmr`, …). A composition missing either half simply idles
232
+ that half; nothing crashes.
233
+ - **`baseUrl` is optional in server mode**: when omitted the AgentCard
234
+ advertises the webserver's real listen address, so ephemeral ports never
235
+ publish a stale URL. Set it explicitly only when a reverse proxy fronts the
236
+ instance.
237
+ - Client-mode connection lifecycle hooks (`onReady` / `onDispose`) feed a
238
+ **process-wide shared registry**; multiple instances (client + server) share
239
+ it, and `/a2a/api` is registered once per process.
240
+ - All registrations are fiber-scoped: Cordis disposes them on plugin
241
+ stop/update.
242
+ - Disabled agents (`enabled: false`) show as grayed-out placeholder cards and
243
+ are **not** reconnected on restart until re-enabled.
244
+
245
+ ## Known limitations
246
+
247
+ - The dashboard API (`/a2a/api`) listens on loopback / same-origin only; it
248
+ carries no secrets and exists for operator visibility.
249
+ - Inbound execution follows the [executor precedence](#who-executes-an-inbound-task-executor-precedence):
250
+ with an agent loop present it defaults to this DSH's own cwd-bound
251
+ session; otherwise (and with nothing injected) it **refuses** and returns
252
+ "no executor configured". `shellExecutor` is local-testing only. Whatever the
253
+ executor, never expose server mode to untrusted networks (no auth by default
254
+ unless you set `authToken`).
255
+ - The A2A spec requires HTTPS for production `AgentInterface.url`; DSH's
256
+ webserver binds loopback by default, so put a TLS reverse proxy in front
257
+ before exposing beyond localhost.
258
+ - Bearer tokens — both the outbound per-agent token and the inbound endpoint
259
+ `authToken` — are stored in plaintext in `a2a.json`; secure the file yourself.
260
+
261
+ ## Development
262
+
263
+ ```bash
264
+ npm install
265
+ npm run build # tsc(types) + tsdown(lib) + wrap client → lib/client.js
266
+ npm test # node:test + tsx (unit/integration) + client-bundle smoke
267
+ ```
package/README.md CHANGED
@@ -1,87 +1,82 @@
1
1
  # dsh-a2a
2
2
 
3
- [中文文档](./README.zh-CN.md) | **English**
3
+ [English](./README.en.md) | **中文文档**
4
4
 
5
- A **DeepSeek Harness (DSH)** plugin that implements the **Agent2Agent (A2A) Protocol v1.0** (Linux Foundation, `a2aproject/A2A`, Apache-2.0).
5
+ 一个 **DeepSeek Harness (DSH)** 插件,实现 **Agent2Agent (A2A) 协议 v1.0**(Linux 基金会,`a2aproject/A2A`,Apache-2.0)。
6
6
 
7
- It turns a DSH instance into a full A2A peer in both directions:
7
+ 让一个 DSH 实例成为完整的 A2A 双端节点:
8
8
 
9
- - **Client mode (outbound)** — discover a remote agent via its **AgentCard**, map each **skill** to a DSH model tool (`a2a__<name>__<skill>`), and execute remote tasks through A2A **JSON-RPC**.
10
- - **Server mode (inbound)** — expose this DSH to other agents: `/.well-known/agent-card.json` plus a JSON-RPC endpoint (`SendMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SendStreamingMessage` over SSE) on the DSH webserver.
9
+ - **Client 模式(出站)**——通过远程 Agent 的 **AgentCard** 发现它,把每个 **skill** 映射为 DSH 模型工具(`a2a__<name>__<skill>`),通过 A2A **JSON-RPC** 执行远程任务。
10
+ - **Server 模式(入站)**——把本 DSH 暴露给其他 Agent:在 DSH webserver 上提供 `/.well-known/agent-card.json` 与 JSON-RPC 端点(`SendMessage`、`GetTask`、`ListTasks`、`CancelTask`、基于 SSE 的 `SendStreamingMessage`)。
11
11
 
12
- ## Features
12
+ ## 功能
13
13
 
14
- ### Protocol surface (A2A v1.0)
14
+ ### 协议能力(A2A v1.0)
15
15
 
16
- - **Discovery**: `GET /.well-known/agent-card.json` (well-known first, configured URL fallback)
17
- - **Unary**: `SendMessage`, `GetTask`, `ListTasks`, `CancelTask`, `GetExtendedAgentCard`
18
- - **Streaming**: `SendStreamingMessage` (SSE) / `SubscribeToTask`, with `TaskStatusUpdateEvent` + `TaskArtifactUpdateEvent`
19
- - **Task lifecycle**: `SUBMITTED → WORKING → COMPLETED / FAILED / CANCELED / REJECTED / INPUT_REQUIRED / AUTH_REQUIRED`
20
- - **Types**: `Task`, `Message`, `Part` (text/raw/url/data), `Artifact`, `AgentCard`, security schemes, extensions
16
+ - **发现**:`GET /.well-known/agent-card.json`(优先 well-known,配置的 URL 兜底)
17
+ - **单次调用**:`SendMessage`、`GetTask`、`ListTasks`、`CancelTask`、`GetExtendedAgentCard`
18
+ - **流式**:`SendStreamingMessage`(SSE)/ `SubscribeToTask`,含 `TaskStatusUpdateEvent` + `TaskArtifactUpdateEvent`
19
+ - **任务生命周期**:`SUBMITTED → WORKING → COMPLETED / FAILED / CANCELED / REJECTED / INPUT_REQUIRED / AUTH_REQUIRED`
20
+ - **鉴权**:出站按 Agent 配置 Bearer Token(`Authorization: Bearer …`);入站可选共享 Bearer Token 保护端点——配置后 AgentCard 声明 `bearerAuth` 安全方案,未带/错误 token 的请求返回 401(单次调用与 SSE 流均生效)
21
+ - **类型**:`Task`、`Message`、`Part`(text/raw/url/data)、`Artifact`、`AgentCard`、安全方案、扩展
21
22
 
22
- ### Connection dashboard (设置 → A2A 连接)
23
+ ### 连接面板(设置 → A2A 连接)
23
24
 
24
- - **Outbound agents** — remote agents this DSH connects to (name, connection state, AgentCard URL, skill/tool counts, last activity), each with **Reconnect / Enable / Disable / Delete** controls.
25
- - **Inbound peers** — who is calling this DSH (label, source address, first/last seen, task count, streaming flag), with a close control.
26
- - **A2A server** — inbound server status with an online/offline toggle, endpoint and skill overview.
27
- - The panel refreshes every 3 s; `/a2a/api` serves a JSON snapshot and control endpoints, fenced to loopback/same-origin.
25
+ - **出站 Agent**:列出本 DSH 连接的远程 Agent(名称、连接状态、AgentCard URL、技能/工具数、最近活动),每个 Agent 支持 **重连 / 启用 / 禁用 / 删除**。添加 Agent 时可填 **Bearer Token**(可选),导入与后续 JSON-RPC 调用都会带上 `Authorization: Bearer …`,并随 Agent 一起持久化到 `a2a.json`。
26
+ - **入站连接**:谁在调用本 DSH(来源、地址、首/末次连接、任务数、流式标记),支持关闭。
27
+ - **A2A 服务**:入站服务的运行状态,上线 / 下线开关,端点与技能一览;并显示 **入站任务由谁执行**(自定义 executor / 本 DSH agent 会话 / 未配置),以及 **入站鉴权** 开关——可设置或清除保护入站端点的共享 Bearer Token(持久化到 `a2a.json`,实时生效)。
28
+ - 面板每 3 秒自动刷新;`/a2a/api` 提供 JSON 快照与控制接口,受 loopback/same-origin 保护(token 值不会通过快照回传,只暴露「是否已配置」)。
28
29
 
29
- ### Runtime configuration (a2a.json)
30
+ ### 运行时配置(a2a.json)
30
31
 
31
- Configuration is driven by **`a2a.json`** (per profile, UI-editable) — manage
32
- agents without touching the composition. The file is resolved from the active
33
- profile, not the launch directory: the plugin parses `--profile <name>` from
34
- the process command line and reads/writes `~/.dsh/profiles/<name>/a2a.json`.
32
+ 配置以 **`a2a.json`(按 profile 存放,UI 可编辑)为准**,无需改动组合配置即可管理 Agent。文件定位不依赖启动目录:插件解析进程命令行里的 `--profile <name>`,读写 `~/.dsh/profiles/<name>/a2a.json`。
35
33
 
36
- ## Screenshots
34
+ ## 截图
37
35
 
38
- | Outbound agents (who this DSH connects to) | Inbound peers (who calls this DSH) |
36
+ | 出站 Agent(本 DSH 连接了谁) | 入站连接(谁在调用本 DSH) |
39
37
  |---|---|
40
- | ![Outbound agents dashboard](docs/screenshots/dashboard-outbound.png) | ![Inbound peers dashboard](docs/screenshots/dashboard-inbound.png) |
38
+ | ![出站 Agent 面板](docs/screenshots/dashboard-outbound.png) | ![入站连接面板](docs/screenshots/dashboard-inbound.png) |
41
39
 
42
- | Inbound with a live record | A2A server status panel |
40
+ | 含真实记录的入站 | A2A 服务状态面板 |
43
41
  |---|---|
44
- | ![Inbound peer record](docs/screenshots/server-inbound.png) | ![A2A server status](docs/screenshots/server-serve.png) |
42
+ | ![入站连接记录](docs/screenshots/server-inbound.png) | ![A2A 服务状态](docs/screenshots/server-serve.png) |
45
43
 
46
- ## Installation
44
+ ## 安装
47
45
 
48
- ### Option 1: from npm (recommended)
46
+ ### 方式一:从 npm 安装(推荐)
49
47
 
50
- Once published to npm, install into the target profile with one command and
51
- restart:
48
+ 插件发布到 npm 后,一行命令装进目标 profile,重启即生效:
52
49
 
53
50
  dsh plugin --profile <profile> add @ryubyte/dsh-a2a
54
51
  dsh --profile <profile>
55
52
 
56
- `dsh plugin` installs the package into the profile and auto-registers the
57
- plugin layer (this package declares `dsh.bundle.patch` in `package.json`) — no
58
- manual configuration is needed.
53
+ `dsh plugin` 会把包装进 profile 并自动加入插件层(本包在 package.json 里声明了 `dsh.bundle.patch`),无需手动改任何配置。
59
54
 
60
- ### Option 2: from the repository (development)
55
+ ### 方式二:从仓库安装(开发调试)
61
56
 
62
- For hacking on the code, requires Node.js >= 22:
57
+ 用于改代码调试,需要 Node.js >= 22 与 pnpm:
63
58
 
64
- # 1. Clone the repo
59
+ # 1. 克隆仓库
65
60
  git clone https://github.com/ryubyte/dsh-a2a.git
66
61
  cd dsh-a2a
67
62
 
68
- # 2. Install deps and build
63
+ # 2. 安装依赖并构建
69
64
  npm install
70
65
  npm run build
71
66
 
72
- # 3. Link into the target profile
67
+ # 3. 链接进目标 profile
73
68
  dsh plugin --profile <profile> add link:$(pwd)
74
69
 
75
- # 4. Restart
70
+ # 4. 重启
76
71
  dsh --profile <profile>
77
72
 
78
- After changing source, re-run `npm run build` and restart.
73
+ 改完源码后重跑 `npm run build` 再重启即可。
79
74
 
80
- ## Usage
75
+ ## 使用
81
76
 
82
- ### 1. Configure a remote agent (outbound)
77
+ ### 1. 配置远程 Agent(出站)
83
78
 
84
- Declare agents to connect to in the profile's `a2a.json`:
79
+ 在 profile 目录下的 `a2a.json` 里声明要连接的 Agent:
85
80
 
86
81
  ```jsonc
87
82
  {
@@ -92,29 +87,28 @@ Declare agents to connect to in the profile's `a2a.json`:
92
87
  // "bearerToken": "...",
93
88
  // "timeoutMs": 8000,
94
89
  // "mapSkills": true,
95
- // "enabled": false // keep the entry, but don't auto-connect
90
+ // "enabled": false // 保留条目,但不自动连接
96
91
  }
97
92
  ]
98
93
  }
99
94
  ```
100
95
 
101
- Alternatively, in the Web GUI go to 设置 → A2A 连接 → 添加 Agent, paste an
102
- AgentCard URL to import and connect — the config is written back to
103
- `a2a.json`.
96
+ 也可以在 Web GUI「设置 → A2A 连接 → 添加 Agent」里输入 AgentCard URL 导入并连接,配置会写回 `a2a.json`。
104
97
 
105
- Once connected, the remote agent's skills appear as model tools:
106
- `a2a__<name>__<skill>`.
98
+ 连接成功后,对方 Agent 的 skills 会作为模型工具出现:`a2a__<name>__<skill>`。
107
99
 
108
- ### 2. Expose this DSH as an A2A agent (inbound)
100
+ 同一个本地 agent 会话对某远程 Agent 的多次工具调用会复用同一个 A2A `contextId`,因此在远端落到同一会话、具备跨轮记忆;不同本地会话相互隔离,各自独立对话。
109
101
 
110
- Enable server mode in `a2a.json`:
102
+ ### 2. 对外提供 A2A 服务(入站)
103
+
104
+ 同样在 `a2a.json` 里开启 server 模式:
111
105
 
112
106
  ```jsonc
113
107
  {
114
108
  "mode": "server",
115
109
  "server": {
116
- "enabled": true, // explicitly turn the inbound server on
117
- // "baseUrl": "http://127.0.0.1:3080", // optional; default: derive from webServer
110
+ "enabled": true, // 显式开启入站服务
111
+ // "baseUrl": "http://127.0.0.1:3080", // 可选;默认从 webServer 监听地址推导
118
112
  "agentName": "My DSH Agent",
119
113
  "agentDescription": "A DSH agent over A2A v1.0",
120
114
  "agentVersion": "0.1.0",
@@ -125,19 +119,30 @@ Enable server mode in `a2a.json`:
125
119
  }
126
120
  ```
127
121
 
128
- Other A2A clients discover this DSH via
129
- `http://<host>:<port>/.well-known/agent-card.json` and call the `/a2a` endpoint.
122
+ 之后其他 A2A 客户端通过 `http://<host>:<port>/.well-known/agent-card.json` 发现本 DSH,通过 `/a2a` 端点调用。
123
+
124
+ #### 入站任务由谁执行(executor 优先级)
125
+
126
+ server 模式收到任务后,按以下优先级选择 executor:
130
127
 
131
- > Note: the default inbound executor runs shell commands — fine for local
132
- > testing only; for production, inject a constrained executor
133
- > programmatically (see below) and put a TLS reverse proxy in front.
128
+ 1. **显式 `execute`**(编程接入时注入)—— 用你的。
129
+ 2. **本 DSH 的 agent 会话**(默认,当 agent 循环就位时)—— 按 A2A `contextId` **一个对话一个会话**:某 `contextId` 首个任务 `ctx.agents.create()` 建会话(后续同 `contextId` 复用同一会话,历史累积;进程重启后按需 `resume`),把消息作为 `next-turn` 唤醒项发给它,等 `whenIdle()`,取最新 assistant 回复作为 A2A artifact。同一 `contextId` 的并发任务按序串行,不同 `contextId` 并行。会话不逐任务释放(否则无法累积上下文),插件卸载/下线时统一释放。开 server 即可「接收并处理任务」,无需写代码。
130
+ 3. **`notConfiguredExecutor`**(兜底)—— 既没注入 `execute`、composition 里又没有可用 agent 循环(如仅 dsh-base 的最小 profile)时,返回「no executor configured — inject one」,**不执行任何命令**。
134
131
 
135
- ### 3. Programmatic use
132
+ > 关于「就位」:`ctx.agents`(registry)存在 ≠ 能创建 agent。`create()` 需要 agent-loop 插件注册的 factory;缺 factory 时 `create()` 会 reject,此时 DSH agent executor 会把它作为可读错误返回(而非抛出)。因此最小 profile 仍需显式注入 executor。
133
+ >
134
+ > 会话与工作区:DSH 会话在创建时把 `cwd` 校验为绝对路径并冻结进会话头。入站会话用**专用目录** `<profile>/.a2a-sessions` 作为 cwd(与 profile 根分开,避免入站会话挤占你的交互会话)。它们出现在会话列表的「未分组」里,并按首条消息摘要命名为 `A2A: <摘要>`(入站消息带 plugin 来源,DSH 的自动标题只认 human 消息,故显式命名)。命名是装饰性的,失败只记日志、绝不影响任务。
135
+ >
136
+ > 取消:`CancelTask` 会 abort 正在运行任务的 executor 信号,真正中止 agent 回合(不只是改任务状态)。
137
+ >
138
+ > 本机自测也可显式传入 `shellExecutor`(把 prompt 当作 shell 命令执行,仅限可信客户端);生产请置于 TLS 反向代理之后并按需配 `authToken`。
139
+
140
+ ### 3. 编程接入
136
141
 
137
142
  ```ts
138
143
  import { A2AClient, A2AServer, TaskStore } from '@ryubyte/dsh-a2a';
139
144
 
140
- // outbound: connect to a remote agent
145
+ // 出站:连接远程 Agent
141
146
  const client = await A2AClient.connect('https://agent.example.com');
142
147
  const { task } = await client.sendMessage({
143
148
  messageId: crypto.randomUUID(),
@@ -148,7 +153,7 @@ for await (const ev of client.streamMessage({ messageId: crypto.randomUUID(), ro
148
153
  // ev.statusUpdate | ev.artifactUpdate | ev.task | ev.message
149
154
  }
150
155
 
151
- // inbound: wire a custom executor to your own agent loop
156
+ // 入站:自定义 executor 接入 DSH agent 循环
152
157
  const store = new TaskStore();
153
158
  const server = new A2AServer({
154
159
  baseUrl: 'http://127.0.0.1:3080',
@@ -163,62 +168,46 @@ const server = new A2AServer({
163
168
  }, store);
164
169
  ```
165
170
 
166
- ## Directory structure
171
+ ## 目录结构
167
172
 
168
173
  src/
169
- protocol.ts # A2A v1.0 types & constants
170
- card.ts # AgentCard discovery / validation
171
- jsonrpc.ts # JSON-RPC 2.0 + SSE transport
172
- client.ts # outbound client (A2AClient)
173
- server.ts # inbound server + TaskStore (A2AServer)
174
- outbound.ts # skill → model-tool bridge (registerAgentTools)
175
- dashboard.ts # connection registry + control
176
- a2a-config.ts # runtime a2a.json parsing / persistence
177
- index.ts # Cordis plugin entry (client / server modes)
174
+ protocol.ts # A2A v1.0 类型与常量
175
+ card.ts # AgentCard 发现 / 校验
176
+ jsonrpc.ts # JSON-RPC 2.0 + SSE 传输
177
+ client.ts # 出站客户端(A2AClient)
178
+ server.ts # 入站服务 + TaskStore(A2AServer)
179
+ outbound.ts # skill → 模型工具桥(registerAgentTools)
180
+ dashboard.ts # 连接注册表 + 控制
181
+ a2a-config.ts # 运行时 a2a.json 解析 / 持久化
182
+ index.ts # Cordis 插件入口(client / server 两种模式)
178
183
  client/
179
- index.ts # browser half: settings "A2A 连接" panel
180
- lib/ # build output (index.js + client.js + types/)
184
+ index.ts # 浏览器半区:设置页「A2A 连接」面板
185
+ lib/ # 构建产物(index.js + client.js + types/)
181
186
  scripts/
182
- build-client.mjs # wrap client bundle (__ModuleLoader__)
183
- capture-shots.mjs # README screenshot capture (headless Chrome + CDP)
184
- cordis.patch.yml # plugin registration row (self-mounted bundle)
185
- package.json # dsh.bundle.patch + dsh.client manifest
186
-
187
- ## How it works
188
-
189
- - The plugin reads `ctx.webServer` and `ctx.tools` as context properties inside
190
- `ctx.inject(...)` / `ctx.effect(...)` — the native Cordis pattern used by DSH
191
- bundles (`dsh-client-hmr`, …). A composition missing either half simply idles
192
- that half; nothing crashes.
193
- - **`baseUrl` is optional in server mode**: when omitted the AgentCard
194
- advertises the webserver's real listen address, so ephemeral ports never
195
- publish a stale URL. Set it explicitly only when a reverse proxy fronts the
196
- instance.
197
- - Client-mode connection lifecycle hooks (`onReady` / `onDispose`) feed a
198
- **process-wide shared registry**; multiple instances (client + server) share
199
- it, and `/a2a/api` is registered once per process.
200
- - All registrations are fiber-scoped: Cordis disposes them on plugin
201
- stop/update.
202
- - Disabled agents (`enabled: false`) show as grayed-out placeholder cards and
203
- are **not** reconnected on restart until re-enabled.
204
-
205
- ## Known limitations
206
-
207
- - The dashboard API (`/a2a/api`) listens on loopback / same-origin only; it
208
- carries no secrets and exists for operator visibility.
209
- - The default inbound executor runs shell commands directly (`/bin/sh -c`), not
210
- an LLM agent; replace it with a constrained executor (e.g. DSH's
211
- sandbox/approval pipeline) in production.
212
- - The A2A spec requires HTTPS for production `AgentInterface.url`; DSH's
213
- webserver binds loopback by default, so put a TLS reverse proxy in front
214
- before exposing beyond localhost.
215
- - Outbound bearer tokens are stored in plaintext in `a2a.json`; secure the file
216
- yourself.
217
-
218
- ## Development
187
+ build-client.mjs # 客户端 bundle 包装(__ModuleLoader__)
188
+ capture-shots.mjs # README 截图抓取(headless Chrome + CDP)
189
+ cordis.patch.yml # 插件注册行(bundle 自挂载,无需手动配置)
190
+ package.json # dsh.bundle.patch + dsh.client 清单
191
+
192
+ ## 工作原理
193
+
194
+ - 插件在 `ctx.inject(...)` / `ctx.effect(...)` 内读取 `ctx.webServer` 与 `ctx.tools`(原生 Cordis 模式,与 `dsh-client-hmr` 等 bundle 一致);缺少任一半的组合只会闲置该半区,不会崩溃。
195
+ - **server 模式 `baseUrl` 可选**:省略时 AgentCard 公布 webServer 的真实监听地址,临时端口不会发布过期 URL;仅在反向代理前置时才需显式设置。
196
+ - 客户端模式的连接生命周期钩子(`onReady` / `onDispose`)上报到**进程级共享注册表**;多个实例(client + server)共用,`/a2a/api` 每个进程注册一次。
197
+ - 所有注册 fiber 作用域化:插件停止 / 更新时由 Cordis 自动清理。
198
+ - 禁用(`enabled:false`)的 Agent 显示为灰化占位卡,**不自动连接**,直到重新启用。
199
+
200
+ ## 已知限制
201
+
202
+ - 面板 API(`/a2a/api`)只监听 loopback / same-origin,供运维可见性使用,不承载机密。
203
+ - 入站服务的执行策略见[入站任务由谁执行](#入站任务由谁执行executor-优先级):有 agent 循环时默认由本 DSH 会话执行(绑定工作目录 cwd),否则(且未注入 executor)**拒绝执行**并返回「no executor configured」。`shellExecutor` 仅限本地自测。无论哪种 executor,都切勿向不受信网络暴露 server 模式(默认无鉴权,除非配置 `authToken`)。
204
+ - A2A 规范要求生产 `AgentInterface.url` 使用 HTTPS;DSH webServer 默认仅绑 loopback,对外暴露前需自行加 TLS。
205
+ - Bearer token(出站按 Agent 配置的、入站保护端点的 `authToken`)均明文存于 `a2a.json`,请自行保证该文件权限。
206
+
207
+ ## 开发
219
208
 
220
209
  ```bash
221
210
  npm install
222
211
  npm run build # tsc(types) + tsdown(lib) + wrap client → lib/client.js
223
- npm test # node:test + tsx (unit/integration) + client-bundle smoke
212
+ npm test # node:test + tsx(单元 / 集成)+ client-bundle smoke
224
213
  ```