@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/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
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -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
|
+
}
|