@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/README.zh.md +195 -0
  4. package/cordis.patch.yml +11 -0
  5. package/lib/client.js +166 -0
  6. package/lib/index.js +2073 -0
  7. package/lib/tsconfig.client.tsbuildinfo +1 -0
  8. package/lib/tsconfig.tsbuildinfo +1 -0
  9. package/lib/types/api.d.ts +41 -0
  10. package/lib/types/api.d.ts.map +1 -0
  11. package/lib/types/api.js +86 -0
  12. package/lib/types/api.js.map +1 -0
  13. package/lib/types/client/index.d.ts +35 -0
  14. package/lib/types/client/index.d.ts.map +1 -0
  15. package/lib/types/client/index.js +155 -0
  16. package/lib/types/client/index.js.map +1 -0
  17. package/lib/types/commands.d.ts +10 -0
  18. package/lib/types/commands.d.ts.map +1 -0
  19. package/lib/types/commands.js +93 -0
  20. package/lib/types/commands.js.map +1 -0
  21. package/lib/types/events.d.ts +74 -0
  22. package/lib/types/events.d.ts.map +1 -0
  23. package/lib/types/events.js +10 -0
  24. package/lib/types/events.js.map +1 -0
  25. package/lib/types/index.d.ts +54 -0
  26. package/lib/types/index.d.ts.map +1 -0
  27. package/lib/types/index.js +296 -0
  28. package/lib/types/index.js.map +1 -0
  29. package/lib/types/jsonrpc.d.ts +20 -0
  30. package/lib/types/jsonrpc.d.ts.map +1 -0
  31. package/lib/types/jsonrpc.js +48 -0
  32. package/lib/types/jsonrpc.js.map +1 -0
  33. package/lib/types/outbound/calls.d.ts +37 -0
  34. package/lib/types/outbound/calls.d.ts.map +1 -0
  35. package/lib/types/outbound/calls.js +120 -0
  36. package/lib/types/outbound/calls.js.map +1 -0
  37. package/lib/types/outbound/registry.d.ts +102 -0
  38. package/lib/types/outbound/registry.d.ts.map +1 -0
  39. package/lib/types/outbound/registry.js +249 -0
  40. package/lib/types/outbound/registry.js.map +1 -0
  41. package/lib/types/outbound/tools.d.ts +30 -0
  42. package/lib/types/outbound/tools.d.ts.map +1 -0
  43. package/lib/types/outbound/tools.js +86 -0
  44. package/lib/types/outbound/tools.js.map +1 -0
  45. package/lib/types/protocol.d.ts +194 -0
  46. package/lib/types/protocol.d.ts.map +1 -0
  47. package/lib/types/protocol.js +78 -0
  48. package/lib/types/protocol.js.map +1 -0
  49. package/lib/types/server/a2a-server.d.ts +98 -0
  50. package/lib/types/server/a2a-server.d.ts.map +1 -0
  51. package/lib/types/server/a2a-server.js +397 -0
  52. package/lib/types/server/a2a-server.js.map +1 -0
  53. package/lib/types/server/card.d.ts +48 -0
  54. package/lib/types/server/card.d.ts.map +1 -0
  55. package/lib/types/server/card.js +75 -0
  56. package/lib/types/server/card.js.map +1 -0
  57. package/lib/types/server/exec/agent-runtime.d.ts +126 -0
  58. package/lib/types/server/exec/agent-runtime.d.ts.map +1 -0
  59. package/lib/types/server/exec/agent-runtime.js +138 -0
  60. package/lib/types/server/exec/agent-runtime.js.map +1 -0
  61. package/lib/types/server/exec/session.d.ts +11 -0
  62. package/lib/types/server/exec/session.d.ts.map +1 -0
  63. package/lib/types/server/exec/session.js +22 -0
  64. package/lib/types/server/exec/session.js.map +1 -0
  65. package/lib/types/server/exec/subagent.d.ts +39 -0
  66. package/lib/types/server/exec/subagent.d.ts.map +1 -0
  67. package/lib/types/server/exec/subagent.js +44 -0
  68. package/lib/types/server/exec/subagent.js.map +1 -0
  69. package/lib/types/server/executor.d.ts +63 -0
  70. package/lib/types/server/executor.d.ts.map +1 -0
  71. package/lib/types/server/executor.js +38 -0
  72. package/lib/types/server/executor.js.map +1 -0
  73. package/lib/types/server/routes.d.ts +32 -0
  74. package/lib/types/server/routes.d.ts.map +1 -0
  75. package/lib/types/server/routes.js +131 -0
  76. package/lib/types/server/routes.js.map +1 -0
  77. package/lib/types/server/store.d.ts +157 -0
  78. package/lib/types/server/store.d.ts.map +1 -0
  79. package/lib/types/server/store.js +239 -0
  80. package/lib/types/server/store.js.map +1 -0
  81. package/lib/types/service.d.ts +49 -0
  82. package/lib/types/service.d.ts.map +1 -0
  83. package/lib/types/service.js +46 -0
  84. package/lib/types/service.js.map +1 -0
  85. package/package.json +82 -0
  86. package/src/api.ts +115 -0
  87. package/src/client/index.ts +293 -0
  88. package/src/commands.ts +92 -0
  89. package/src/events.ts +58 -0
  90. package/src/index.ts +353 -0
  91. package/src/jsonrpc.ts +61 -0
  92. package/src/outbound/calls.ts +145 -0
  93. package/src/outbound/registry.ts +290 -0
  94. package/src/outbound/tools.ts +118 -0
  95. package/src/protocol.ts +212 -0
  96. package/src/server/a2a-server.ts +455 -0
  97. package/src/server/card.ts +106 -0
  98. package/src/server/exec/agent-runtime.ts +197 -0
  99. package/src/server/exec/session.ts +25 -0
  100. package/src/server/exec/subagent.ts +67 -0
  101. package/src/server/executor.ts +73 -0
  102. package/src/server/routes.ts +156 -0
  103. package/src/server/store.ts +320 -0
  104. package/src/service.ts +78 -0
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@hanphone/dsh-a2a",
3
+ "version": "0.1.0",
4
+ "description": "Agent2Agent (A2A) v1.0 dual-end plugin for DeepSeek Harness: governed inbound tasks, durable task store, pluggable executors, multi-agent outbound consumption, GUI management dashboard.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "deepseek-harness",
9
+ "dsh",
10
+ "a2a",
11
+ "agent2agent",
12
+ "cordis",
13
+ "ai-agent",
14
+ "plugin"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/HanphoneJan/dsh-a2a.git"
22
+ },
23
+ "homepage": "https://github.com/HanphoneJan/dsh-a2a#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/HanphoneJan/dsh-a2a/issues"
26
+ },
27
+ "files": [
28
+ "lib",
29
+ "src",
30
+ "cordis.patch.yml",
31
+ "README.md",
32
+ "README.zh.md"
33
+ ],
34
+ "main": "./lib/index.js",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./lib/types/index.d.ts",
38
+ "default": "./lib/index.js"
39
+ },
40
+ "./client": {
41
+ "types": "./lib/types/client/index.d.ts",
42
+ "default": "./lib/client.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "dsh": {
47
+ "bundle": {
48
+ "patch": "./cordis.patch.yml"
49
+ },
50
+ "client": {
51
+ "inject": [
52
+ "@deepseek-ai/dsh-client-ui-renderer",
53
+ "@deepseek-ai/dsh-client-ui-settings",
54
+ "@deepseek-ai/dsh-client-ui-slots"
55
+ ],
56
+ "platform": "web"
57
+ }
58
+ },
59
+ "//peers": "A2A runs inside a DSH harness composition: the host provides these. Local typecheck resolves them through tsconfig paths to the harness checkout, not from node_modules.",
60
+ "peerDependencies": {
61
+ "@deepseek-ai/cordis": "4.0.2",
62
+ "@deepseek-ai/dsh-commands": "*",
63
+ "@deepseek-ai/dsh-storage-domain": "*",
64
+ "@deepseek-ai/schemastery": "3.*"
65
+ },
66
+ "dependencies": {
67
+ "zod": "^4.4.3"
68
+ },
69
+ "devDependencies": {
70
+ "@types/node": "^22.20.0",
71
+ "@types/react": "^18.3.31",
72
+ "react": "^18.3.1",
73
+ "tsdown": "^0.22.2",
74
+ "typescript": "^6.0.3",
75
+ "vitest": "^4.1.8"
76
+ },
77
+ "scripts": {
78
+ "build": "tsc -b tsconfig.json && tsc -p tsconfig.client.json && tsdown && node scripts/build-client.mjs",
79
+ "typecheck": "tsc -b tsconfig.json && tsc -p tsconfig.client.json",
80
+ "test": "vitest run"
81
+ }
82
+ }
package/src/api.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * GUI control API for the A2A settings dashboard: one `GET /a2a/api`
3
+ * snapshot plus `POST /a2a/api` control actions (server toggle, outbound
4
+ * agent management, task view). The browser half talks only to this route;
5
+ * it carries no protocol knowledge. Loopback-only by default so the
6
+ * dashboard cannot be driven from the wire.
7
+ *
8
+ * The route body is bound to the same `A2AServiceImpl` facade the `/a2a`
9
+ * command surface uses, so GUI actions and commands cannot disagree.
10
+ * @module dsh-a2a/api
11
+ */
12
+
13
+ import type { IncomingMessage, ServerResponse } from 'node:http'
14
+ import type { A2AServiceImpl } from './service.ts'
15
+
16
+ /** Control actions the dashboard can issue. */
17
+ export type ApiAction =
18
+ | { readonly action: 'server.enable' | 'server.disable' }
19
+ | { readonly action: 'agent.add'; readonly name: string; readonly agentCardUrl: string; readonly bearerTokenEnv?: string }
20
+ | { readonly action: 'agent.remove' | 'agent.enable' | 'agent.disable' | 'agent.refresh'; readonly id: string }
21
+ | { readonly action: 'task.cancel'; readonly id: string }
22
+
23
+ /** One snapshot of the whole plugin for the dashboard. */
24
+ export interface ApiSnapshot {
25
+ readonly server: { readonly enabled: boolean; readonly cardUrl?: string; readonly skills: readonly string[] }
26
+ readonly tasks: readonly unknown[]
27
+ readonly agents: readonly unknown[]
28
+ }
29
+
30
+ function readBody(req: IncomingMessage): Promise<string> {
31
+ return new Promise<string>((resolve, reject) => {
32
+ const chunks: Buffer[] = []
33
+ req.on('data', (chunk: Buffer) => chunks.push(chunk))
34
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
35
+ req.once('error', reject)
36
+ })
37
+ }
38
+
39
+ /** The caller is the local machine (never a remote peer). */
40
+ function isLoopback(req: IncomingMessage): boolean {
41
+ const address = req.socket?.remoteAddress
42
+ return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
43
+ }
44
+
45
+ function json(res: ServerResponse, status: number, value: unknown): void {
46
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-cache' })
47
+ res.end(JSON.stringify(value))
48
+ }
49
+
50
+ /** Handle one dashboard API request against the service facade. */
51
+ export async function handleApiRequest(
52
+ req: IncomingMessage,
53
+ res: ServerResponse,
54
+ impl: A2AServiceImpl,
55
+ ): Promise<void> {
56
+ if (!isLoopback(req)) {
57
+ res.writeHead(403, { 'content-type': 'text/plain' })
58
+ res.end('forbidden')
59
+ return
60
+ }
61
+ if (req.method === 'GET') {
62
+ json(res, 200, snapshotOf(impl))
63
+ return
64
+ }
65
+ if (req.method === 'POST') {
66
+ let payload: ApiAction
67
+ try {
68
+ payload = JSON.parse(await readBody(req)) as ApiAction
69
+ } catch {
70
+ json(res, 400, { ok: false, message: 'invalid JSON body' })
71
+ return
72
+ }
73
+ const result = await dispatch(payload, impl)
74
+ json(res, result.ok ? 200 : 409, result)
75
+ return
76
+ }
77
+ res.writeHead(405, { 'content-type': 'text/plain' })
78
+ res.end('Method Not Allowed')
79
+ }
80
+
81
+ function snapshotOf(impl: A2AServiceImpl): ApiSnapshot {
82
+ const status = impl.status() as {
83
+ server: { enabled: boolean; cardUrl?: string; skills: readonly string[] }
84
+ tasks: number
85
+ agents: readonly unknown[]
86
+ }
87
+ return {
88
+ server: status.server,
89
+ tasks: impl.listTasks() as readonly unknown[],
90
+ agents: impl.agents() as readonly unknown[],
91
+ }
92
+ }
93
+
94
+ async function dispatch(payload: ApiAction, impl: A2AServiceImpl): Promise<{ readonly ok: boolean; readonly message: string }> {
95
+ switch (payload.action) {
96
+ case 'server.enable':
97
+ return impl.enableServer(true)
98
+ case 'server.disable':
99
+ return impl.enableServer(false)
100
+ case 'agent.add':
101
+ return impl.addAgent({ name: payload.name, agentCardUrl: payload.agentCardUrl, ...(payload.bearerTokenEnv ? { bearerTokenEnv: payload.bearerTokenEnv } : {}) })
102
+ case 'agent.remove':
103
+ return impl.removeAgent(payload.id)
104
+ case 'agent.enable':
105
+ return impl.setAgentEnabled(payload.id, true)
106
+ case 'agent.disable':
107
+ return impl.setAgentEnabled(payload.id, false)
108
+ case 'agent.refresh':
109
+ return impl.refreshAgentCard(payload.id)
110
+ case 'task.cancel':
111
+ return impl.cancelTask(payload.id)
112
+ default:
113
+ return { ok: false, message: `unknown action ${String((payload as { action?: unknown }).action)}` }
114
+ }
115
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * A2A settings dashboard — browser half.
3
+ *
4
+ * Registers one `settings.section` ("A2A 连接") listing who is connected TO
5
+ * this DSH (inbound server state + tasks) and who this DSH is connected TO
6
+ * (outbound agents), with per-agent controls (enable/disable/remove/refresh)
7
+ * and the inbound-server toggle. All data and controls live behind the host's
8
+ * loopback-only `/a2a/api` route (src/api.ts); this half is a thin read/write
9
+ * client with no protocol knowledge and no direct storage access.
10
+ *
11
+ * Styling: one idempotent `<style>` injector built on the product's
12
+ * `--dsw-alias-*` design tokens so light/dark themes follow the DSH shell.
13
+ * No inline styles.
14
+ * @module dsh-a2a/client
15
+ */
16
+
17
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
18
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
19
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
20
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
21
+ import { createElement, useEffect, useState, type ReactElement } from 'react'
22
+
23
+ /** Services this plugin needs from the client runtime. */
24
+ export const inject = ['slots']
25
+
26
+ const NS = 'a2a'
27
+
28
+ function injectStyles(): () => void {
29
+ const tag = document.createElement('style')
30
+ tag.dataset.plugin = NS
31
+ tag.dataset.pluginCss = `${NS}/dashboard`
32
+ tag.textContent = `
33
+ .dsh-a2a-panel { display: flex; flex-direction: column; gap: 16px; padding: 12px 0; }
34
+ .dsh-a2a-card { border: 1px solid var(--dsw-alias-border, #d0d7de); border-radius: 8px; padding: 12px 14px; }
35
+ .dsh-a2a-card h3 { margin: 0 0 8px; font-size: 14px; }
36
+ .dsh-a2a-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
37
+ .dsh-a2a-row .muted { color: var(--dsw-alias-text-secondary, #59636e); font-size: 12px; }
38
+ .dsh-a2a-row button { padding: 2px 10px; border-radius: 6px; border: 1px solid var(--dsw-alias-border, #d0d7de); background: transparent; cursor: pointer; }
39
+ .dsh-a2a-row button.primary { background: var(--dsw-alias-accent, #0f6); color: var(--dsw-alias-bg, #fff); border-color: transparent; }
40
+ .dsh-a2a-state { font-weight: 600; font-size: 12px; padding: 1px 8px; border-radius: 10px; }
41
+ .dsh-a2a-state.ok { background: #1a7f3722; color: #1a7f37; }
42
+ .dsh-a2a-state.bad { background: #d1242f22; color: #d1242f; }
43
+ .dsh-a2a-form { display: flex; gap: 6px; margin: 8px 0; flex-wrap: wrap; }
44
+ .dsh-a2a-form input { padding: 4px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border, #d0d7de); background: transparent; color: inherit; }
45
+ .dsh-a2a-table { width: 100%; border-collapse: collapse; font-size: 13px; }
46
+ .dsh-a2a-table th, .dsh-a2a-table td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--dsw-alias-border, #d0d7de); }
47
+ .dsh-a2a-error { color: #d1242f; font-size: 12px; }
48
+ `
49
+ document.head.appendChild(tag)
50
+ return () => tag.remove()
51
+ }
52
+
53
+ export function apply(ctx: ClientContext): void {
54
+ ctx.effect(() => injectStyles(), `${NS}: dashboard styles`)
55
+ // settings.section is declared at runtime by ui-settings-general; slots.inject
56
+ // defers registration until that declaration exists and follows its lifetime
57
+ // (a bare register before the declaration can fail or land invisible).
58
+ ctx.slots.inject('settings.section', () => ctx.slots.register(
59
+ {
60
+ name: 'settings.section',
61
+ id: 'a2a',
62
+ order: 90,
63
+ label: () => 'A2A 连接',
64
+ inject: () => ({}),
65
+ },
66
+ A2aSection,
67
+ ))
68
+ }
69
+
70
+ /** Wire types — mirror the host's src/api.ts snapshot. */
71
+ export interface ApiSnapshot {
72
+ readonly server: { readonly enabled: boolean; readonly cardUrl?: string; readonly skills: readonly string[] }
73
+ readonly tasks: readonly unknown[]
74
+ readonly agents: readonly unknown[]
75
+ }
76
+
77
+ interface AgentView {
78
+ readonly id: string
79
+ readonly name: string
80
+ readonly agentCardUrl: string
81
+ readonly enabled: boolean
82
+ readonly state: string
83
+ readonly skillCount: number
84
+ readonly toolCount: number
85
+ readonly lastError?: string
86
+ }
87
+
88
+ interface TaskView {
89
+ readonly id: string
90
+ readonly contextId?: string
91
+ readonly status?: { readonly state: string }
92
+ readonly metadata?: { readonly skill?: string }
93
+ }
94
+
95
+ async function fetchSnapshot(): Promise<ApiSnapshot> {
96
+ const res = await fetch('/a2a/api', { headers: { accept: 'application/json' } })
97
+ if (!res.ok) throw new Error(`snapshot HTTP ${res.status}`)
98
+ return (await res.json()) as ApiSnapshot
99
+ }
100
+
101
+ async function postControl(payload: Record<string, unknown>): Promise<{ readonly ok: boolean; readonly message: string }> {
102
+ const res = await fetch('/a2a/api', {
103
+ method: 'POST',
104
+ headers: { 'content-type': 'application/json' },
105
+ body: JSON.stringify(payload),
106
+ })
107
+ const body = (await res.json().catch(() => ({ ok: false, message: `HTTP ${res.status}` }))) as { ok: boolean; message: string }
108
+ return body
109
+ }
110
+
111
+ type SectionProps = PropsRuntime<'settings.section'>
112
+
113
+ export function A2aSection(_props: SectionProps): ReactElement {
114
+ const [snap, setSnap] = useState<ApiSnapshot | undefined>(undefined)
115
+ const [error, setError] = useState<string | undefined>(undefined)
116
+ const [agentName, setAgentName] = useState('')
117
+ const [agentUrl, setAgentUrl] = useState('')
118
+ const [busy, setBusy] = useState(false)
119
+
120
+ useEffect(() => {
121
+ let alive = true
122
+ const tick = async (): Promise<void> => {
123
+ try {
124
+ const next = await fetchSnapshot()
125
+ if (alive) { setSnap(next); setError(undefined) }
126
+ } catch (err) {
127
+ if (alive) setError((err as Error).message)
128
+ }
129
+ }
130
+ void tick()
131
+ const timer = setInterval(() => void tick(), 3000)
132
+ return () => {
133
+ alive = false
134
+ clearInterval(timer)
135
+ }
136
+ }, [])
137
+
138
+ const control = async (payload: Record<string, unknown>): Promise<void> => {
139
+ setBusy(true)
140
+ try {
141
+ const result = await postControl(payload)
142
+ if (!result.ok) setError(result.message)
143
+ setSnap(await fetchSnapshot())
144
+ } catch (err) {
145
+ setError((err as Error).message)
146
+ } finally {
147
+ setBusy(false)
148
+ }
149
+ }
150
+
151
+ const server = snap?.server
152
+ const agents = (snap?.agents ?? []) as readonly AgentView[]
153
+ const tasks = (snap?.tasks ?? []) as readonly TaskView[]
154
+
155
+ return createElement(
156
+ 'div',
157
+ { className: 'dsh-a2a-panel' },
158
+ error !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, error) : null,
159
+
160
+ // ── inbound server ───────────────────────────────────────────────────
161
+ createElement(
162
+ 'div',
163
+ { className: 'dsh-a2a-card' },
164
+ createElement('h3', null, '入站 Server'),
165
+ server === undefined
166
+ ? createElement('div', { className: 'muted' }, '加载中…')
167
+ : createElement(
168
+ 'div',
169
+ { className: 'dsh-a2a-row' },
170
+ createElement(
171
+ 'span',
172
+ { className: `dsh-a2a-state ${server.enabled ? 'ok' : 'bad'}` },
173
+ server.enabled ? '已启用' : '已停用',
174
+ ),
175
+ createElement(
176
+ 'button',
177
+ {
178
+ className: server.enabled ? '' : 'primary',
179
+ disabled: busy,
180
+ onClick: () => void control({ action: server.enabled ? 'server.disable' : 'server.enable' }),
181
+ },
182
+ server.enabled ? '停用' : '启用',
183
+ ),
184
+ ),
185
+ server !== undefined && server.enabled
186
+ ? createElement(
187
+ 'div',
188
+ { className: 'dsh-a2a-row muted' },
189
+ `AgentCard: ${server.cardUrl ?? '(n/a)'} · 技能: ${server.skills.join(', ') || '(chat)'}`,
190
+ )
191
+ : null,
192
+ ),
193
+
194
+ // ── outbound agents ──────────────────────────────────────────────────
195
+ createElement(
196
+ 'div',
197
+ { className: 'dsh-a2a-card' },
198
+ createElement('h3', null, `出站 Agents(${agents.length})`),
199
+ agents.length === 0
200
+ ? createElement('div', { className: 'muted' }, '未配置远程 agent。')
201
+ : createElement(
202
+ 'table',
203
+ { className: 'dsh-a2a-table' },
204
+ createElement(
205
+ 'thead',
206
+ null,
207
+ createElement('tr', null,
208
+ createElement('th', null, '名称'),
209
+ createElement('th', null, '状态'),
210
+ createElement('th', null, '工具'),
211
+ createElement('th', null, '操作'),
212
+ ),
213
+ ),
214
+ createElement(
215
+ 'tbody',
216
+ null,
217
+ ...agents.map((a) =>
218
+ createElement(
219
+ 'tr',
220
+ { key: a.id },
221
+ createElement('td', null,
222
+ createElement('div', null, a.name),
223
+ createElement('div', { className: 'muted' }, a.agentCardUrl),
224
+ a.lastError !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, a.lastError) : null,
225
+ ),
226
+ createElement('td', null,
227
+ createElement('span', { className: `dsh-a2a-state ${a.state === 'connected' ? 'ok' : 'bad'}` }, a.state),
228
+ ),
229
+ createElement('td', null, `${a.toolCount} / ${a.skillCount}`),
230
+ createElement('td', null,
231
+ createElement('button', { disabled: busy, onClick: () => void control({ action: a.enabled ? 'agent.disable' : 'agent.enable', id: a.id }) }, a.enabled ? '停用' : '启用'),
232
+ ' ',
233
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'agent.refresh', id: a.id }) }, '刷新'),
234
+ ' ',
235
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'agent.remove', id: a.id }) }, '删除'),
236
+ ),
237
+ ),
238
+ ),
239
+ ),
240
+ ),
241
+ createElement(
242
+ 'form',
243
+ {
244
+ className: 'dsh-a2a-form',
245
+ onSubmit: (ev: { preventDefault(): void }) => {
246
+ ev.preventDefault()
247
+ if (!agentName.trim() || !agentUrl.trim()) return
248
+ void control({ action: 'agent.add', name: agentName.trim(), agentCardUrl: agentUrl.trim() }).then(() => {
249
+ setAgentName('')
250
+ setAgentUrl('')
251
+ })
252
+ },
253
+ },
254
+ createElement('input', { value: agentName, placeholder: '名称', onChange: (e: { target: { value: string } }) => setAgentName(e.target.value) }),
255
+ createElement('input', { value: agentUrl, placeholder: 'AgentCard URL', onChange: (e: { target: { value: string } }) => setAgentUrl(e.target.value) }),
256
+ createElement('button', { type: 'submit', className: 'primary', disabled: busy }, '添加 Agent'),
257
+ ),
258
+ ),
259
+
260
+ // ── tasks ────────────────────────────────────────────────────────────
261
+ createElement(
262
+ 'div',
263
+ { className: 'dsh-a2a-card' },
264
+ createElement('h3', null, `任务(${tasks.length})`),
265
+ tasks.length === 0
266
+ ? createElement('div', { className: 'muted' }, '暂无任务。')
267
+ : createElement(
268
+ 'table',
269
+ { className: 'dsh-a2a-table' },
270
+ createElement('thead', null,
271
+ createElement('tr', null,
272
+ createElement('th', null, 'ID'),
273
+ createElement('th', null, '技能'),
274
+ createElement('th', null, '状态'),
275
+ createElement('th', null, '操作'),
276
+ ),
277
+ ),
278
+ createElement('tbody', null,
279
+ ...tasks.slice(-20).reverse().map((t) =>
280
+ createElement('tr', { key: t.id },
281
+ createElement('td', null, createElement('span', { className: 'muted' }, t.id)),
282
+ createElement('td', null, t.metadata?.skill ?? '-'),
283
+ createElement('td', null, t.status?.state ?? '-'),
284
+ createElement('td', null,
285
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'task.cancel', id: t.id }) }, '取消'),
286
+ ),
287
+ ),
288
+ ),
289
+ ),
290
+ ),
291
+ ),
292
+ )
293
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The `/a2a` command surface: status, server enable/disable, card, agents,
3
+ * and task control — the P0 operations surface (the settings panel is P1).
4
+ * @module dsh-a2a/commands
5
+ */
6
+
7
+ import type { CommandDefinition, CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands'
8
+ import type { A2AServiceImpl } from './service.ts'
9
+
10
+ /** Build the `/a2a` command definition over the service facade. */
11
+ export function buildA2aCommand(impl: A2AServiceImpl): CommandDefinition {
12
+ return {
13
+ name: 'a2a',
14
+ description: 'Manage the A2A plugin: status, server, agents, tasks, card.',
15
+ handler: async (invocation: CommandInvocation): Promise<CommandResult> => {
16
+ const [verb, ...rest] = invocation.rawInput.trim().split(/\s+/)
17
+ switch (verb ?? '') {
18
+ case '': {
19
+ const status = impl.status() as { server?: unknown; client?: unknown }
20
+ return success(`A2A status\n${JSON.stringify(status, null, 2)}`)
21
+ }
22
+ case 'status':
23
+ return success(JSON.stringify(impl.status(), null, 2))
24
+ case 'enable':
25
+ return outcome(await impl.enableServer(true))
26
+ case 'disable':
27
+ return outcome(await impl.enableServer(false))
28
+ case 'card':
29
+ return success(JSON.stringify(impl.listTasks(), null, 2).slice(0, 4000))
30
+ case 'agents':
31
+ return success(JSON.stringify(impl.agents(), null, 2))
32
+ case 'agent': {
33
+ const [action, ...args] = rest
34
+ switch (action ?? '') {
35
+ case 'add': {
36
+ const url = args[0]
37
+ if (!url) return error('usage: /a2a agent add <agentCardUrl> [name]')
38
+ const name = args[1] ?? url
39
+ return outcome(await impl.addAgent({ name, agentCardUrl: url }))
40
+ }
41
+ case 'remove': {
42
+ const id = args[0]
43
+ if (!id) return error('usage: /a2a agent remove <id>')
44
+ return outcome(await impl.removeAgent(id))
45
+ }
46
+ case 'enable': {
47
+ const id = args[0]
48
+ if (!id) return error('usage: /a2a agent enable <id>')
49
+ return outcome(await impl.setAgentEnabled(id, true))
50
+ }
51
+ case 'disable': {
52
+ const id = args[0]
53
+ if (!id) return error('usage: /a2a agent disable <id>')
54
+ return outcome(await impl.setAgentEnabled(id, false))
55
+ }
56
+ case 'refresh': {
57
+ const id = args[0]
58
+ if (!id) return error('usage: /a2a agent refresh <id>')
59
+ return outcome(await impl.refreshAgentCard(id))
60
+ }
61
+ default:
62
+ return error('usage: /a2a agent add|remove|enable|disable|refresh ...')
63
+ }
64
+ }
65
+ case 'tasks':
66
+ return success(JSON.stringify(impl.listTasks(), null, 2))
67
+ case 'task': {
68
+ const [action, id] = rest
69
+ if (action === 'get' && id) return success(JSON.stringify(impl.getTask(id), null, 2))
70
+ if (action === 'cancel' && id) return outcome(await impl.cancelTask(id))
71
+ return error('usage: /a2a task get|cancel <taskId>')
72
+ }
73
+ case 'help':
74
+ return success('a2a commands: status | enable | disable | card | agents | agent add/remove/enable/disable/refresh | tasks | task get/cancel <id> | help')
75
+ default:
76
+ return error(`unknown a2a verb "${verb}" (try /a2a help)`)
77
+ }
78
+ },
79
+ }
80
+ }
81
+
82
+ function success(text: string): CommandResult {
83
+ return { kind: 'success', text }
84
+ }
85
+
86
+ function error(text: string): CommandResult {
87
+ return { kind: 'error', text }
88
+ }
89
+
90
+ function outcome(result: { readonly ok: boolean; readonly message: string }): CommandResult {
91
+ return result.ok ? success(result.message) : error(result.message)
92
+ }
package/src/events.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The `a2a/*` event domain and the `ctx.a2a` service key. P0 ships exactly one
3
+ * event — `a2a/inbound-task`, the policy seam — so the governance story has a
4
+ * real built-in consumer (skill gate + audit) and third-party plugins can hang
5
+ * approval-style policy on inbound tasks. Further events are deferred until a
6
+ * consumer exists (see design §4).
7
+ * @module dsh-a2a/events
8
+ */
9
+
10
+ import type { Part } from './protocol.ts'
11
+
12
+ /** The mutable policy decision for one inbound task, before execution. */
13
+ export interface InboundTaskDecision {
14
+ readonly contextId: string
15
+ readonly skill: string
16
+ readonly parts: readonly Part[]
17
+ /** Request source identity (auth hash or null) for audit. */
18
+ readonly remotePeerId: string | null
19
+ /** Set by a policy listener to defer the task for human approval (P1). */
20
+ requiresApproval?: boolean
21
+ /** Set by a policy listener to veto the task outright. */
22
+ rejected?: { readonly reason: string }
23
+ }
24
+
25
+ /** The facade other plugins read as `ctx.a2a`. */
26
+ export interface A2AServiceLike {
27
+ status(): unknown
28
+ enableServer(enable: boolean): Promise<{ readonly ok: boolean; readonly message: string }>
29
+ listTasks(): unknown
30
+ getTask(taskId: string): unknown
31
+ cancelTask(taskId: string): Promise<{ readonly ok: boolean; readonly message: string }>
32
+ agents(): unknown
33
+ addAgent(spec: unknown): Promise<{ readonly ok: boolean; readonly message: string }>
34
+ removeAgent(id: string): Promise<{ readonly ok: boolean; readonly message: string }>
35
+ setAgentEnabled(id: string, enabled: boolean): Promise<{ readonly ok: boolean; readonly message: string }>
36
+ refreshAgentCard(id: string): Promise<{ readonly ok: boolean; readonly message: string }>
37
+ }
38
+
39
+ declare module '@deepseek-ai/cordis' {
40
+ interface Context {
41
+ /** The A2A service facade, provided while the plugin is mounted. */
42
+ a2a: A2AServiceLike
43
+ }
44
+
45
+ interface Events {
46
+ /**
47
+ * Policy seam for one inbound task, dispatched as a waterfall before the
48
+ * task starts. Listeners mutate the decision (attach `requiresApproval`,
49
+ * set `rejected`) and MUST call `next(decision)` to delegate; returning
50
+ * without `next()` short-circuits and the returned value is final.
51
+ * @mode waterfall
52
+ * @param decision — the mutable task decision.
53
+ * @param next - delegate the (possibly mutated) decision downstream.
54
+ * @returns the final decision after all listeners.
55
+ */
56
+ 'a2a/inbound-task'(this: unknown, decision: InboundTaskDecision, next: (d: InboundTaskDecision) => Promise<InboundTaskDecision> | InboundTaskDecision): Promise<InboundTaskDecision>
57
+ }
58
+ }