@hanphone/dsh-a2a 0.1.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 +196 -0
- package/README.zh.md +195 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +166 -0
- package/lib/index.js +2073 -0
- package/lib/tsconfig.client.tsbuildinfo +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/api.d.ts +41 -0
- package/lib/types/api.d.ts.map +1 -0
- package/lib/types/api.js +86 -0
- package/lib/types/api.js.map +1 -0
- package/lib/types/client/index.d.ts +35 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +155 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/commands.d.ts +10 -0
- package/lib/types/commands.d.ts.map +1 -0
- package/lib/types/commands.js +93 -0
- package/lib/types/commands.js.map +1 -0
- package/lib/types/events.d.ts +74 -0
- package/lib/types/events.d.ts.map +1 -0
- package/lib/types/events.js +10 -0
- package/lib/types/events.js.map +1 -0
- package/lib/types/index.d.ts +54 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +296 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/jsonrpc.d.ts +20 -0
- package/lib/types/jsonrpc.d.ts.map +1 -0
- package/lib/types/jsonrpc.js +48 -0
- package/lib/types/jsonrpc.js.map +1 -0
- package/lib/types/outbound/calls.d.ts +37 -0
- package/lib/types/outbound/calls.d.ts.map +1 -0
- package/lib/types/outbound/calls.js +120 -0
- package/lib/types/outbound/calls.js.map +1 -0
- package/lib/types/outbound/registry.d.ts +102 -0
- package/lib/types/outbound/registry.d.ts.map +1 -0
- package/lib/types/outbound/registry.js +249 -0
- package/lib/types/outbound/registry.js.map +1 -0
- package/lib/types/outbound/tools.d.ts +30 -0
- package/lib/types/outbound/tools.d.ts.map +1 -0
- package/lib/types/outbound/tools.js +86 -0
- package/lib/types/outbound/tools.js.map +1 -0
- package/lib/types/protocol.d.ts +194 -0
- package/lib/types/protocol.d.ts.map +1 -0
- package/lib/types/protocol.js +78 -0
- package/lib/types/protocol.js.map +1 -0
- package/lib/types/server/a2a-server.d.ts +98 -0
- package/lib/types/server/a2a-server.d.ts.map +1 -0
- package/lib/types/server/a2a-server.js +397 -0
- package/lib/types/server/a2a-server.js.map +1 -0
- package/lib/types/server/card.d.ts +48 -0
- package/lib/types/server/card.d.ts.map +1 -0
- package/lib/types/server/card.js +75 -0
- package/lib/types/server/card.js.map +1 -0
- package/lib/types/server/exec/agent-runtime.d.ts +126 -0
- package/lib/types/server/exec/agent-runtime.d.ts.map +1 -0
- package/lib/types/server/exec/agent-runtime.js +138 -0
- package/lib/types/server/exec/agent-runtime.js.map +1 -0
- package/lib/types/server/exec/session.d.ts +11 -0
- package/lib/types/server/exec/session.d.ts.map +1 -0
- package/lib/types/server/exec/session.js +22 -0
- package/lib/types/server/exec/session.js.map +1 -0
- package/lib/types/server/exec/subagent.d.ts +39 -0
- package/lib/types/server/exec/subagent.d.ts.map +1 -0
- package/lib/types/server/exec/subagent.js +44 -0
- package/lib/types/server/exec/subagent.js.map +1 -0
- package/lib/types/server/executor.d.ts +63 -0
- package/lib/types/server/executor.d.ts.map +1 -0
- package/lib/types/server/executor.js +38 -0
- package/lib/types/server/executor.js.map +1 -0
- package/lib/types/server/routes.d.ts +32 -0
- package/lib/types/server/routes.d.ts.map +1 -0
- package/lib/types/server/routes.js +131 -0
- package/lib/types/server/routes.js.map +1 -0
- package/lib/types/server/store.d.ts +157 -0
- package/lib/types/server/store.d.ts.map +1 -0
- package/lib/types/server/store.js +239 -0
- package/lib/types/server/store.js.map +1 -0
- package/lib/types/service.d.ts +49 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/service.js +46 -0
- package/lib/types/service.js.map +1 -0
- package/package.json +82 -0
- package/src/api.ts +115 -0
- package/src/client/index.ts +293 -0
- package/src/commands.ts +92 -0
- package/src/events.ts +58 -0
- package/src/index.ts +353 -0
- package/src/jsonrpc.ts +61 -0
- package/src/outbound/calls.ts +145 -0
- package/src/outbound/registry.ts +290 -0
- package/src/outbound/tools.ts +118 -0
- package/src/protocol.ts +212 -0
- package/src/server/a2a-server.ts +455 -0
- package/src/server/card.ts +106 -0
- package/src/server/exec/agent-runtime.ts +197 -0
- package/src/server/exec/session.ts +25 -0
- package/src/server/exec/subagent.ts +67 -0
- package/src/server/executor.ts +73 -0
- package/src/server/routes.ts +156 -0
- package/src/server/store.ts +320 -0
- package/src/service.ts +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 寒枫
|
|
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,196 @@
|
|
|
1
|
+
# dsh-a2a
|
|
2
|
+
|
|
3
|
+
Agent2Agent (A2A) Protocol v1.0 dual-end plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
|
|
4
|
+
|
|
5
|
+
> **English** | [中文](README.zh.md)
|
|
6
|
+
|
|
7
|
+
`@hanphone/dsh-a2a` turns a DeepSeek Harness profile into a first-class A2A agent:
|
|
8
|
+
|
|
9
|
+
- **Inbound server** — AgentCard derived from the live tool registry, JSON-RPC + SSE, durable task store, pluggable session/subagent executors, and a policy gate (`a2a/inbound-task`) with audit.
|
|
10
|
+
- **Outbound client** — a persisted multi-agent AgentCard registry, remote skills mapped to model tools (`a2a__<name>__<skill>`), sync calls with per-agent timeout.
|
|
11
|
+
- **GUI dashboard** — an **A2A 连接** settings page in the Harness Web UI: toggle the inbound server, manage outbound agents, view and cancel tasks — no config files required.
|
|
12
|
+
|
|
13
|
+
Design decisions are recorded in [docs/architecture.md](docs/architecture.md). Current scope is P0 of that document.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **A2A v1.0 protocol surface** — `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `GetExtendedAgentCard`, `SubscribeToTask` over JSON-RPC; SSE streaming with catch-up frames.
|
|
18
|
+
- **Dynamic AgentCard** — skills derived from the live `ctx.tools` registry (explicit id list, loud failure on missing referents) plus a built-in `chat` skill so a fresh install is immediately exercisable.
|
|
19
|
+
- **Durable task store** — tasks live in the `a2a` storage domain (JSON backend by default, SQLite per deployment choice); task ids are server-generated and survive restarts.
|
|
20
|
+
- **Executors** — `session` (one DSH session per `contextId`) and `subagent` (delegates to `ctx.subagents`, streams tool-call artifacts back) as built-in implementations.
|
|
21
|
+
- **Governed inbound** — every inbound task passes through the `a2a/inbound-task` waterfall, so policy plugins can veto or audit before execution.
|
|
22
|
+
- **Auth by environment variable** — inbound bearer token is referenced by env-var name (`authTokenEnv`), never stored in config as plaintext.
|
|
23
|
+
- **Install-and-use** — both halves are enabled by default after `dsh plugin add`; no manual patch required to start.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### From npm (published)
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
dsh plugin --profile web add @hanphone/dsh-a2a
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This works for any profile name (`web`, custom profiles, etc.):
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
dsh plugin --profile <name> add @hanphone/dsh-a2a
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### From a local build
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
cd dsh-a2a
|
|
43
|
+
pnpm build
|
|
44
|
+
npm pack
|
|
45
|
+
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.1.0.tgz
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
1. **Install** — `dsh plugin --profile web add @hanphone/dsh-a2a`.
|
|
51
|
+
2. **Restart the GUI** — the browser half is scanned at host startup, so restart `pnpm dsh web` (or your profile launcher) once after installing.
|
|
52
|
+
3. **Open Settings → A2A 连接** — you will see the inbound server status, the outbound agent list, and the task list.
|
|
53
|
+
|
|
54
|
+
The inbound server listens on the profile's webServer (default `http://127.0.0.1:3080`):
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
curl http://127.0.0.1:3080/.well-known/agent-card.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Send a task (the built-in `chat` skill):
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
curl -X POST http://127.0.0.1:3080/a2a \
|
|
64
|
+
-H "content-type: application/json" \
|
|
65
|
+
-d '{"jsonrpc":"2.0","id":"1","method":"SendMessage","params":{"message":{"role":"user","parts":[{"text":"hello"}],"metadata":{"skill":"chat"}}}}'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## GUI dashboard
|
|
69
|
+
|
|
70
|
+
The browser half registers an **A2A 连接** page under Settings. From it you can, without touching any file:
|
|
71
|
+
|
|
72
|
+
- toggle the inbound server (`server.enable` / `server.disable`),
|
|
73
|
+
- list, add, enable/disable, refresh, and remove outbound agents,
|
|
74
|
+
- view and cancel inbound tasks.
|
|
75
|
+
|
|
76
|
+
All dashboard traffic goes through the **loopback-only** `/a2a/api` route on the profile's webServer — remote peers can never drive it.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
The dashboard covers the day-to-day operations. Values the dashboard does not edit (name/description, baseUrl, `authTokenEnv`, skills, executors, toolPrefix) are configured through the profile's user patch layer (`$DSH_HOME/profiles/<name>/cordis.patch.yml`) — the reserve path:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
- id: a2a
|
|
84
|
+
config:
|
|
85
|
+
server:
|
|
86
|
+
enabled: true
|
|
87
|
+
name: My DSH Agent
|
|
88
|
+
description: A DeepSeek Harness agent exposed over A2A v1.0
|
|
89
|
+
version: 0.1.0
|
|
90
|
+
baseUrl: http://127.0.0.1:<port> # omit to derive from the webServer address
|
|
91
|
+
endpointPath: /a2a
|
|
92
|
+
authTokenEnv: A2A_INBOUND_TOKEN # optional; an env var NAME, never the token
|
|
93
|
+
skills:
|
|
94
|
+
ids: [] # explicit tool ids to expose; chat is built-in
|
|
95
|
+
exclude: []
|
|
96
|
+
executors:
|
|
97
|
+
chat: session # or subagent (needs the subagent seam)
|
|
98
|
+
subagentProvider: in-process
|
|
99
|
+
client:
|
|
100
|
+
toolPrefix: a2a
|
|
101
|
+
agents: [] # or declare agents declaratively
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Required host services
|
|
105
|
+
|
|
106
|
+
Base-backed profiles mount them all: `webServer` (`@deepseek-ai/dsh-host-webserver`), the storage stack (`@deepseek-ai/dsh-storage` + `@deepseek-ai/dsh-storage-domain`), the tools registry (`@deepseek-ai/dsh-tools`), and an agent loop (`@deepseek-ai/dsh-agent` + `@deepseek-ai/dsh-agent-loop`; the subagent executor additionally needs `@deepseek-ai/dsh-subagent`).
|
|
107
|
+
|
|
108
|
+
### Storage backend
|
|
109
|
+
|
|
110
|
+
The task store lives in the `a2a` storage domain. The base composition routes storage through the `json` backend; to use SQLite, route the domain and add the backend in the same patch layer:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
- id: storage-domain
|
|
114
|
+
config:
|
|
115
|
+
backend: json
|
|
116
|
+
routes:
|
|
117
|
+
a2a: sqlite
|
|
118
|
+
- insert:
|
|
119
|
+
- id: storage-sqlite
|
|
120
|
+
name: '@deepseek-ai/dsh-storage-sqlite'
|
|
121
|
+
config:
|
|
122
|
+
path: /absolute/path/to/a2a.sqlite
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Outbound agents (file-declared, optional — the GUI manages the same list)
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
- id: a2a
|
|
129
|
+
config:
|
|
130
|
+
client:
|
|
131
|
+
toolPrefix: a2a
|
|
132
|
+
agents:
|
|
133
|
+
- name: my-remote-agent
|
|
134
|
+
agentCardUrl: https://remote.example/.well-known/agent-card.json
|
|
135
|
+
bearerTokenEnv: A2A_REMOTE_TOKEN # optional; env var NAME
|
|
136
|
+
enabled: true
|
|
137
|
+
timeoutMs: 60000
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Each enabled remote agent's skills become model tools named `a2a__<name>__<skill>` (normalized, collision-hashed). The registry persists across restarts.
|
|
141
|
+
|
|
142
|
+
## CLI
|
|
143
|
+
|
|
144
|
+
A `/a2a` chat command mirrors the dashboard:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
a2a status | enable | disable | card | agents |
|
|
148
|
+
agent add|remove|enable|disable|refresh |
|
|
149
|
+
tasks | task get|cancel <id> | help
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## How it works
|
|
153
|
+
|
|
154
|
+
- **Inbound** — `POST /a2a` (JSON-RPC) and `GET /.well-known/agent-card.json`; the AgentCard is derived from the live tool registry. Tasks flow through `a2a/inbound-task` → executor → task store, with SSE frames streamed to subscribers.
|
|
155
|
+
- **Outbound** — a persisted `agents` table in the `a2a` domain; `A2AClient` discovers an AgentCard, and each skill registers as a tool.
|
|
156
|
+
- **Dashboard** — the browser half (React, `settings.section`) reads/writes the loopback-only `/a2a/api` route.
|
|
157
|
+
|
|
158
|
+
See [docs/architecture.md](docs/architecture.md) for the full design.
|
|
159
|
+
|
|
160
|
+
## Directory structure
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
src/
|
|
164
|
+
api.ts # loopback dashboard API (/a2a/api)
|
|
165
|
+
index.ts # Cordis plugin entry (apply)
|
|
166
|
+
protocol.ts # A2A v1.0 protocol constants + types
|
|
167
|
+
jsonrpc.ts # JSON-RPC framing
|
|
168
|
+
server/ # inbound half: store, card, a2a-server, routes, executors
|
|
169
|
+
outbound/ # outbound half: A2AClient, registry, tools
|
|
170
|
+
client/ # browser half: settings dashboard (React)
|
|
171
|
+
service.ts # ctx.a2a service facade
|
|
172
|
+
commands.ts # /a2a chat command
|
|
173
|
+
tests/
|
|
174
|
+
unit/ # protocol, framing, card, store, registry, server, client, api
|
|
175
|
+
composition/ # apply() on a real Cordis Context with stub host services
|
|
176
|
+
cordis.patch.yml # bundle patch (mounts the plugin, enabled by default)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
> The plugin typechecks against the harness source graph through project
|
|
182
|
+
> references; a harness checkout with its built host aggregate is required.
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
pnpm typecheck # host (tsc -b) + client (tsc -p tsconfig.client.json)
|
|
186
|
+
pnpm test # vitest run (unit + composition suites)
|
|
187
|
+
pnpm build # tsc + tsdown → lib/index.js (host) + lib/client.js (browser)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Known limitations
|
|
191
|
+
|
|
192
|
+
OAuth 2.0 / per-client credentials, gRPC binding, push notifications, `INPUT_REQUIRED` ↔ approval, and passive outbound result injection are listed as P1 or explicitly-not-doing in [docs/architecture.md](docs/architecture.md). They are documented gaps, not a roadmap.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# dsh-a2a
|
|
2
|
+
|
|
3
|
+
Agent2Agent(A2A)v1.0 双端插件,用于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)。
|
|
4
|
+
|
|
5
|
+
> [English](README.md) | **中文**
|
|
6
|
+
|
|
7
|
+
`@hanphone/dsh-a2a` 把 DeepSeek Harness profile 变成 A2A 一等公民:
|
|
8
|
+
|
|
9
|
+
- **入站服务端** —— 从实时工具注册表派生的 AgentCard、JSON-RPC + SSE、持久化任务存储、可插拔的会话/子代理执行器,以及带审计的策略门禁(`a2a/inbound-task`)。
|
|
10
|
+
- **出站客户端** —— 持久化的多 Agent 注册表,远程技能映射为模型工具(`a2a__<name>__<skill>`),带每 agent 超时的同步调用。
|
|
11
|
+
- **GUI 面板** —— Harness Web UI 设置中的 **A2A 连接** 页:开关入站服务端、管理出站 agent、查看与取消任务——无需改任何配置文件。
|
|
12
|
+
|
|
13
|
+
设计决策见 [docs/architecture.md](docs/architecture.md)。当前范围为该文档中的 P0。
|
|
14
|
+
|
|
15
|
+
## 功能
|
|
16
|
+
|
|
17
|
+
- **A2A v1.0 协议面** —— JSON-RPC 上的 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`GetExtendedAgentCard`、`SubscribeToTask`;SSE 流式带补发帧。
|
|
18
|
+
- **动态 AgentCard** —— 技能从实时 `ctx.tools` 注册表派生(显式 id 清单,缺失引用大声失败),外加内置 `chat` 技能,装完即可直接验证。
|
|
19
|
+
- **持久化任务存储** —— 任务存于 `a2a` 存储域(默认 json 后端,可按部署切 SQLite);任务 id 服务端生成且跨重启存活。
|
|
20
|
+
- **执行器** —— `session`(每个 `contextId` 一个 DSH 会话)与 `subagent`(委托 `ctx.subagents`,把工具调用过程流式回传)两种内置实现。
|
|
21
|
+
- **受治理的入站** —— 每个入站任务都经过 `a2a/inbound-task` waterfall,策略插件可在执行前否决或审计。
|
|
22
|
+
- **环境变量鉴权** —— 入站 Bearer token 只以环境变量名(`authTokenEnv`)引用,不以明文落配置。
|
|
23
|
+
- **装完即用** —— `dsh plugin add` 后两端默认启用,无需手动 patch。
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
### 从 npm 发布版安装
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
dsh plugin --profile web add @hanphone/dsh-a2a
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
任意 profile 名均可:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
dsh plugin --profile <name> add @hanphone/dsh-a2a
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 本地构建安装
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
cd dsh-a2a
|
|
43
|
+
pnpm build
|
|
44
|
+
npm pack
|
|
45
|
+
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.1.0.tgz
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 快速开始
|
|
49
|
+
|
|
50
|
+
1. **安装** —— `dsh plugin --profile web add @hanphone/dsh-a2a`。
|
|
51
|
+
2. **重启 GUI** —— 浏览器端插件表在 host 启动时扫描,装完请重启一次 `pnpm dsh web`(或对应 profile 启动命令)。
|
|
52
|
+
3. **打开 设置 → A2A 连接** —— 会看到入站服务端状态、出站 agent 列表与任务列表。
|
|
53
|
+
|
|
54
|
+
入站服务端监听在 profile 的 webServer 上(默认 `http://127.0.0.1:3080`):
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
curl http://127.0.0.1:3080/.well-known/agent-card.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
发一个任务(内置 `chat` 技能):
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
curl -X POST http://127.0.0.1:3080/a2a \
|
|
64
|
+
-H "content-type: application/json" \
|
|
65
|
+
-d '{"jsonrpc":"2.0","id":"1","method":"SendMessage","params":{"message":{"role":"user","parts":[{"text":"hello"}],"metadata":{"skill":"chat"}}}}'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## GUI 面板
|
|
69
|
+
|
|
70
|
+
浏览器端在设置中注册 **A2A 连接** 页。无需改文件即可:
|
|
71
|
+
|
|
72
|
+
- 开关入站服务端(`server.enable` / `server.disable`),
|
|
73
|
+
- 列出、添加、启用/停用、刷新、删除出站 agent,
|
|
74
|
+
- 查看与取消入站任务。
|
|
75
|
+
|
|
76
|
+
所有面板流量都走 profile webServer 上的**仅回环** `/a2a/api` 路由——远程对端永远无法驱动它。
|
|
77
|
+
|
|
78
|
+
## 配置
|
|
79
|
+
|
|
80
|
+
面板覆盖日常操作。面板不编辑的项(name/description、baseUrl、`authTokenEnv`、skills、executors、toolPrefix)通过 profile 用户 patch 层(`$DSH_HOME/profiles/<name>/cordis.patch.yml`)配置——保留方式:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
- id: a2a
|
|
84
|
+
config:
|
|
85
|
+
server:
|
|
86
|
+
enabled: true
|
|
87
|
+
name: My DSH Agent
|
|
88
|
+
description: A DeepSeek Harness agent exposed over A2A v1.0
|
|
89
|
+
version: 0.1.0
|
|
90
|
+
baseUrl: http://127.0.0.1:<port> # 省略则从 webServer 地址派生
|
|
91
|
+
endpointPath: /a2a
|
|
92
|
+
authTokenEnv: A2A_INBOUND_TOKEN # 可选;环境变量名,绝不写 token 明文
|
|
93
|
+
skills:
|
|
94
|
+
ids: [] # 暴露的显式工具 id;chat 为内置
|
|
95
|
+
exclude: []
|
|
96
|
+
executors:
|
|
97
|
+
chat: session # 或 subagent(需要 subagent 接缝)
|
|
98
|
+
subagentProvider: in-process
|
|
99
|
+
client:
|
|
100
|
+
toolPrefix: a2a
|
|
101
|
+
agents: [] # 或声明式列出 agent
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 所需宿主服务
|
|
105
|
+
|
|
106
|
+
base 类 profile 全部挂载:`webServer`(`@deepseek-ai/dsh-host-webserver`)、存储栈(`@deepseek-ai/dsh-storage` + `@deepseek-ai/dsh-storage-domain`)、工具注册表(`@deepseek-ai/dsh-tools`)、agent 循环(`@deepseek-ai/dsh-agent` + `@deepseek-ai/dsh-agent-loop`;subagent 执行器还需要 `@deepseek-ai/dsh-subagent`)。
|
|
107
|
+
|
|
108
|
+
### 存储后端
|
|
109
|
+
|
|
110
|
+
任务存储位于 `a2a` 存储域。base 组合默认走 `json` 后端;要切 SQLite,在同一 patch 层路由域并加后端:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
- id: storage-domain
|
|
114
|
+
config:
|
|
115
|
+
backend: json
|
|
116
|
+
routes:
|
|
117
|
+
a2a: sqlite
|
|
118
|
+
- insert:
|
|
119
|
+
- id: storage-sqlite
|
|
120
|
+
name: '@deepseek-ai/dsh-storage-sqlite'
|
|
121
|
+
config:
|
|
122
|
+
path: /absolute/path/to/a2a.sqlite
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 出站 agent(文件声明,可选——GUI 管理同一列表)
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
- id: a2a
|
|
129
|
+
config:
|
|
130
|
+
client:
|
|
131
|
+
toolPrefix: a2a
|
|
132
|
+
agents:
|
|
133
|
+
- name: my-remote-agent
|
|
134
|
+
agentCardUrl: https://remote.example/.well-known/agent-card.json
|
|
135
|
+
bearerTokenEnv: A2A_REMOTE_TOKEN # 可选;环境变量名
|
|
136
|
+
enabled: true
|
|
137
|
+
timeoutMs: 60000
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
每个启用远程 agent 的技能会注册为 `a2a__<name>__<skill>` 模型工具(规范化、冲突哈希)。注册表跨重启持久化。
|
|
141
|
+
|
|
142
|
+
## CLI
|
|
143
|
+
|
|
144
|
+
`/a2a` 聊天命令与面板对应:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
a2a status | enable | disable | card | agents |
|
|
148
|
+
agent add|remove|enable|disable|refresh |
|
|
149
|
+
tasks | task get|cancel <id> | help
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 工作原理
|
|
153
|
+
|
|
154
|
+
- **入站** —— `POST /a2a`(JSON-RPC)与 `GET /.well-known/agent-card.json`;AgentCard 从实时工具注册表派生。任务流经 `a2a/inbound-task` → 执行器 → 任务存储,SSE 帧推送给订阅者。
|
|
155
|
+
- **出站** —— `a2a` 域中的持久化 `agents` 表;`A2AClient` 发现 AgentCard,每个技能注册为一个工具。
|
|
156
|
+
- **面板** —— 浏览器端(React,`settings.section`)经仅回环 `/a2a/api` 路由读写。
|
|
157
|
+
|
|
158
|
+
完整设计见 [docs/architecture.md](docs/architecture.md)。
|
|
159
|
+
|
|
160
|
+
## 目录结构
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
src/
|
|
164
|
+
api.ts # 回环面板 API (/a2a/api)
|
|
165
|
+
index.ts # Cordis 插件入口 (apply)
|
|
166
|
+
protocol.ts # A2A v1.0 协议常量与类型
|
|
167
|
+
jsonrpc.ts # JSON-RPC 帧
|
|
168
|
+
server/ # 入站半区:store、card、a2a-server、routes、executors
|
|
169
|
+
outbound/ # 出站半区:A2AClient、registry、tools
|
|
170
|
+
client/ # 浏览器半区:设置面板 (React)
|
|
171
|
+
service.ts # ctx.a2a 服务 facade
|
|
172
|
+
commands.ts # /a2a 聊天命令
|
|
173
|
+
tests/
|
|
174
|
+
unit/ # protocol、framing、card、store、registry、server、client、api
|
|
175
|
+
composition/ # 在真实 Cordis Context 上以 stub 宿主服务跑 apply()
|
|
176
|
+
cordis.patch.yml # bundle patch(挂载插件,默认启用)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 开发
|
|
180
|
+
|
|
181
|
+
> 插件通过项目引用对 harness 源码图做类型检查;需要已构建宿主聚合的 harness checkout。
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
pnpm typecheck # host (tsc -b) + client (tsc -p tsconfig.client.json)
|
|
185
|
+
pnpm test # vitest run(单元 + 组合套件)
|
|
186
|
+
pnpm build # tsc + tsdown → lib/index.js(host)+ lib/client.js(浏览器)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 已知限制
|
|
190
|
+
|
|
191
|
+
OAuth 2.0 / 每客户端凭据、gRPC 绑定、push notifications、`INPUT_REQUIRED` ↔ 审批、被动出站结果注入,在 [docs/architecture.md](docs/architecture.md) 中列为 P1 或明确不做。它们是已记录的空缺,不是路线图。
|
|
192
|
+
|
|
193
|
+
## 许可证
|
|
194
|
+
|
|
195
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @hanphone/dsh-a2a bundle patch: mount the plugin as one row, enabled by
|
|
2
|
+
# default (install-and-use; turn either half off through the GUI dashboard or
|
|
3
|
+
# an overlay patch on this row).
|
|
4
|
+
- insert:
|
|
5
|
+
- id: a2a
|
|
6
|
+
name: '@hanphone/dsh-a2a'
|
|
7
|
+
config:
|
|
8
|
+
server:
|
|
9
|
+
enabled: true
|
|
10
|
+
client:
|
|
11
|
+
agents: []
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@hanphone/dsh-a2a",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
//#region src/client/index.ts
|
|
9
|
+
/** Services this plugin needs from the client runtime. */
|
|
10
|
+
const inject = ["slots"];
|
|
11
|
+
const NS = "a2a";
|
|
12
|
+
function injectStyles() {
|
|
13
|
+
const tag = document.createElement("style");
|
|
14
|
+
tag.dataset.plugin = NS;
|
|
15
|
+
tag.dataset.pluginCss = `${NS}/dashboard`;
|
|
16
|
+
tag.textContent = `
|
|
17
|
+
.dsh-a2a-panel { display: flex; flex-direction: column; gap: 16px; padding: 12px 0; }
|
|
18
|
+
.dsh-a2a-card { border: 1px solid var(--dsw-alias-border, #d0d7de); border-radius: 8px; padding: 12px 14px; }
|
|
19
|
+
.dsh-a2a-card h3 { margin: 0 0 8px; font-size: 14px; }
|
|
20
|
+
.dsh-a2a-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
|
|
21
|
+
.dsh-a2a-row .muted { color: var(--dsw-alias-text-secondary, #59636e); font-size: 12px; }
|
|
22
|
+
.dsh-a2a-row button { padding: 2px 10px; border-radius: 6px; border: 1px solid var(--dsw-alias-border, #d0d7de); background: transparent; cursor: pointer; }
|
|
23
|
+
.dsh-a2a-row button.primary { background: var(--dsw-alias-accent, #0f6); color: var(--dsw-alias-bg, #fff); border-color: transparent; }
|
|
24
|
+
.dsh-a2a-state { font-weight: 600; font-size: 12px; padding: 1px 8px; border-radius: 10px; }
|
|
25
|
+
.dsh-a2a-state.ok { background: #1a7f3722; color: #1a7f37; }
|
|
26
|
+
.dsh-a2a-state.bad { background: #d1242f22; color: #d1242f; }
|
|
27
|
+
.dsh-a2a-form { display: flex; gap: 6px; margin: 8px 0; flex-wrap: wrap; }
|
|
28
|
+
.dsh-a2a-form input { padding: 4px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border, #d0d7de); background: transparent; color: inherit; }
|
|
29
|
+
.dsh-a2a-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
30
|
+
.dsh-a2a-table th, .dsh-a2a-table td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--dsw-alias-border, #d0d7de); }
|
|
31
|
+
.dsh-a2a-error { color: #d1242f; font-size: 12px; }
|
|
32
|
+
`;
|
|
33
|
+
document.head.appendChild(tag);
|
|
34
|
+
return () => tag.remove();
|
|
35
|
+
}
|
|
36
|
+
function apply(ctx) {
|
|
37
|
+
ctx.effect(() => injectStyles(), `${NS}: dashboard styles`);
|
|
38
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
39
|
+
name: "settings.section",
|
|
40
|
+
id: "a2a",
|
|
41
|
+
order: 90,
|
|
42
|
+
label: () => "A2A 连接",
|
|
43
|
+
inject: () => ({})
|
|
44
|
+
}, A2aSection));
|
|
45
|
+
}
|
|
46
|
+
async function fetchSnapshot() {
|
|
47
|
+
const res = await fetch("/a2a/api", { headers: { accept: "application/json" } });
|
|
48
|
+
if (!res.ok) throw new Error(`snapshot HTTP ${res.status}`);
|
|
49
|
+
return await res.json();
|
|
50
|
+
}
|
|
51
|
+
async function postControl(payload) {
|
|
52
|
+
const res = await fetch("/a2a/api", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "content-type": "application/json" },
|
|
55
|
+
body: JSON.stringify(payload)
|
|
56
|
+
});
|
|
57
|
+
return await res.json().catch(() => ({
|
|
58
|
+
ok: false,
|
|
59
|
+
message: `HTTP ${res.status}`
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
function A2aSection(_props) {
|
|
63
|
+
const [snap, setSnap] = (0, react.useState)(void 0);
|
|
64
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
65
|
+
const [agentName, setAgentName] = (0, react.useState)("");
|
|
66
|
+
const [agentUrl, setAgentUrl] = (0, react.useState)("");
|
|
67
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
68
|
+
(0, react.useEffect)(() => {
|
|
69
|
+
let alive = true;
|
|
70
|
+
const tick = async () => {
|
|
71
|
+
try {
|
|
72
|
+
const next = await fetchSnapshot();
|
|
73
|
+
if (alive) {
|
|
74
|
+
setSnap(next);
|
|
75
|
+
setError(void 0);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (alive) setError(err.message);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
tick();
|
|
82
|
+
const timer = setInterval(() => void tick(), 3e3);
|
|
83
|
+
return () => {
|
|
84
|
+
alive = false;
|
|
85
|
+
clearInterval(timer);
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
88
|
+
const control = async (payload) => {
|
|
89
|
+
setBusy(true);
|
|
90
|
+
try {
|
|
91
|
+
const result = await postControl(payload);
|
|
92
|
+
if (!result.ok) setError(result.message);
|
|
93
|
+
setSnap(await fetchSnapshot());
|
|
94
|
+
} catch (err) {
|
|
95
|
+
setError(err.message);
|
|
96
|
+
} finally {
|
|
97
|
+
setBusy(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const server = snap?.server;
|
|
101
|
+
const agents = snap?.agents ?? [];
|
|
102
|
+
const tasks = snap?.tasks ?? [];
|
|
103
|
+
return (0, react.createElement)("div", { className: "dsh-a2a-panel" }, error !== void 0 ? (0, react.createElement)("div", { className: "dsh-a2a-error" }, error) : null, (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, "入站 Server"), server === void 0 ? (0, react.createElement)("div", { className: "muted" }, "加载中…") : (0, react.createElement)("div", { className: "dsh-a2a-row" }, (0, react.createElement)("span", { className: `dsh-a2a-state ${server.enabled ? "ok" : "bad"}` }, server.enabled ? "已启用" : "已停用"), (0, react.createElement)("button", {
|
|
104
|
+
className: server.enabled ? "" : "primary",
|
|
105
|
+
disabled: busy,
|
|
106
|
+
onClick: () => void control({ action: server.enabled ? "server.disable" : "server.enable" })
|
|
107
|
+
}, server.enabled ? "停用" : "启用")), server !== void 0 && server.enabled ? (0, react.createElement)("div", { className: "dsh-a2a-row muted" }, `AgentCard: ${server.cardUrl ?? "(n/a)"} · 技能: ${server.skills.join(", ") || "(chat)"}`) : null), (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, `出站 Agents(${agents.length})`), agents.length === 0 ? (0, react.createElement)("div", { className: "muted" }, "未配置远程 agent。") : (0, react.createElement)("table", { className: "dsh-a2a-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, "名称"), (0, react.createElement)("th", null, "状态"), (0, react.createElement)("th", null, "工具"), (0, react.createElement)("th", null, "操作"))), (0, react.createElement)("tbody", null, ...agents.map((a) => (0, react.createElement)("tr", { key: a.id }, (0, react.createElement)("td", null, (0, react.createElement)("div", null, a.name), (0, react.createElement)("div", { className: "muted" }, a.agentCardUrl), a.lastError !== void 0 ? (0, react.createElement)("div", { className: "dsh-a2a-error" }, a.lastError) : null), (0, react.createElement)("td", null, (0, react.createElement)("span", { className: `dsh-a2a-state ${a.state === "connected" ? "ok" : "bad"}` }, a.state)), (0, react.createElement)("td", null, `${a.toolCount} / ${a.skillCount}`), (0, react.createElement)("td", null, (0, react.createElement)("button", {
|
|
108
|
+
disabled: busy,
|
|
109
|
+
onClick: () => void control({
|
|
110
|
+
action: a.enabled ? "agent.disable" : "agent.enable",
|
|
111
|
+
id: a.id
|
|
112
|
+
})
|
|
113
|
+
}, a.enabled ? "停用" : "启用"), " ", (0, react.createElement)("button", {
|
|
114
|
+
disabled: busy,
|
|
115
|
+
onClick: () => void control({
|
|
116
|
+
action: "agent.refresh",
|
|
117
|
+
id: a.id
|
|
118
|
+
})
|
|
119
|
+
}, "刷新"), " ", (0, react.createElement)("button", {
|
|
120
|
+
disabled: busy,
|
|
121
|
+
onClick: () => void control({
|
|
122
|
+
action: "agent.remove",
|
|
123
|
+
id: a.id
|
|
124
|
+
})
|
|
125
|
+
}, "删除")))))), (0, react.createElement)("form", {
|
|
126
|
+
className: "dsh-a2a-form",
|
|
127
|
+
onSubmit: (ev) => {
|
|
128
|
+
ev.preventDefault();
|
|
129
|
+
if (!agentName.trim() || !agentUrl.trim()) return;
|
|
130
|
+
control({
|
|
131
|
+
action: "agent.add",
|
|
132
|
+
name: agentName.trim(),
|
|
133
|
+
agentCardUrl: agentUrl.trim()
|
|
134
|
+
}).then(() => {
|
|
135
|
+
setAgentName("");
|
|
136
|
+
setAgentUrl("");
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}, (0, react.createElement)("input", {
|
|
140
|
+
value: agentName,
|
|
141
|
+
placeholder: "名称",
|
|
142
|
+
onChange: (e) => setAgentName(e.target.value)
|
|
143
|
+
}), (0, react.createElement)("input", {
|
|
144
|
+
value: agentUrl,
|
|
145
|
+
placeholder: "AgentCard URL",
|
|
146
|
+
onChange: (e) => setAgentUrl(e.target.value)
|
|
147
|
+
}), (0, react.createElement)("button", {
|
|
148
|
+
type: "submit",
|
|
149
|
+
className: "primary",
|
|
150
|
+
disabled: busy
|
|
151
|
+
}, "添加 Agent"))), (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, `任务(${tasks.length})`), tasks.length === 0 ? (0, react.createElement)("div", { className: "muted" }, "暂无任务。") : (0, react.createElement)("table", { className: "dsh-a2a-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, "ID"), (0, react.createElement)("th", null, "技能"), (0, react.createElement)("th", null, "状态"), (0, react.createElement)("th", null, "操作"))), (0, react.createElement)("tbody", null, ...tasks.slice(-20).reverse().map((t) => (0, react.createElement)("tr", { key: t.id }, (0, react.createElement)("td", null, (0, react.createElement)("span", { className: "muted" }, t.id)), (0, react.createElement)("td", null, t.metadata?.skill ?? "-"), (0, react.createElement)("td", null, t.status?.state ?? "-"), (0, react.createElement)("td", null, (0, react.createElement)("button", {
|
|
152
|
+
disabled: busy,
|
|
153
|
+
onClick: () => void control({
|
|
154
|
+
action: "task.cancel",
|
|
155
|
+
id: t.id
|
|
156
|
+
})
|
|
157
|
+
}, "取消"))))))));
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
exports.A2aSection = A2aSection;
|
|
161
|
+
exports.apply = apply;
|
|
162
|
+
exports.inject = inject;
|
|
163
|
+
|
|
164
|
+
return module.exports;
|
|
165
|
+
}
|
|
166
|
+
});
|