Agent2Agent (A2A) Protocol v1.0.1 dual-end plugin for DeepSeek Harness — English · 中文
@hanphone/dsh-a2a is an independent, open-source A2A plugin that turns a
DeepSeek Harness profile into a multi-faced agent-to-agent citizen: it can
simultaneously serve multiple inbound A2A servers, each bound to its own
agent preset with its own endpoint, AgentCard, preset-derived skills and auth, and
connect to multiple outbound A2A servers, each with its own preset, whose
remote skills appear as model tools. Every server instance is created,
started, stopped, edited and removed entirely from the GUI — no config-file
editing.
Architecture and design decisions: docs/architecture.md. Tracking DeepSeek Harness releases: docs/harness-compatibility.md.
SendMessage, SendStreamingMessage, GetTask, ListTasks, CancelTask,
GetExtendedAgentCard, SubscribeToTask over JSON-RPC; SSE streaming with
catch-up frames; official TASK_STATE_* / ROLE_* enums and AgentCard
structure (including supportedInterfaces, capabilities, full error-code
table)./a2a/<id>), AgentCard route, authentication env, and skill
declarations.ptc, standard, minimal, …); the picker lists only
real roster presets (matching the in-app selector) and defaults to the
deployment default. Inbound tasks execute in sessions composed from that
preset through the standard agentPresets resolve+mount path.agentPresets.standingKeyFor + ctx.skills.list), derived automatically —
“the preset decides its skills; everything is a plugin”. No typed skill form;
a missing skills service falls back to the built-in chat skill. Remotes
call a skill via metadata.skill, and the preset session executes it
through its tool-skill loader.a2a__<name>__<skill> model tools.a2a storage domain (JSON
backend by default, SQLite per deployment choice); server-generated ids
survive restarts, and each task records the inbound server it arrived
through.session (one DSH session per contextId) and subagent
(delegates to ctx.subagents, streams tool-call artifacts back).a2a/inbound-task waterfall so policy plugins can veto or audit.contextId) maps to one live DSH session (a2a-<contextId>); the GUI
aggregates sessions by contextId (status, task counts, streaming, first/last
activity) and can cancel a session’s active tasks or close its live session
— close is an in-memory release, the next task on the same contextId simply
re-opens a fresh one. All-in-memory, like the peer registry; without an
agent loop the table still renders from task records (degraded)..env /
credential store, 0o700); the record keeps only an auto-generated env-var
name and the value never lands in the a2a domain or the AgentCard. Runtime
resolution layers credentials → process environment, so externally exported
env vars keep working.Config only carries host-level defaults
(baseUrl, subagentProvider, defaultTimeoutMs).dsh plugin --profile web add @hanphone/dsh-a2a
Any profile name works (web, custom profiles, headless etc.):
dsh plugin --profile <name> add @hanphone/dsh-a2a
cd dsh-a2a
pnpm build
npm pack
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-<version>.tgz
dsh plugin --profile web add @hanphone/dsh-a2a.pnpm dsh web or your profile launcher).Each inbound server listens on the profile’s webServer:
# the created instance's AgentCard (see the GUI for the exact id)
curl http://127.0.0.1:3080/a2a/<id>/agent-card.json
Send a task to an instance (its declared chat skill):
curl -X POST http://127.0.0.1:3080/a2a/<id> \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"SendMessage","params":{"message":{"role":"user","parts":[{"text":"hello"}],"metadata":{"skill":"chat"}}}}'
The browser half registers an A2A 连接 page under Settings, organized in three tabs. From it you can, without touching any file:
All dashboard traffic goes through the loopback-only /a2a/api route —
remote peers can never drive it.
The GUI covers instance management day-to-day. The plugin Config only has
host-level defaults, set through the profile’s user patch layer
($DSH_HOME/profiles/<name>/cordis.patch.yml) if you want to override them:
- id: a2a
config:
baseUrl: http://127.0.0.1:<port> # omit to derive from the webServer address
subagentProvider: in-process
defaultTimeoutMs: 60000 # default outbound connection timeout
Instances are not configured via patches — they are created in the GUI and
persisted in the a2a domain (inbound_servers / outbound_servers tables).
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), an agent loop
(@deepseek-ai/dsh-agent + @deepseek-ai/dsh-agent-loop), and agent presets
(@deepseek-ai/dsh-agent-presets; the preset pickers and preset-bound session
composition need it). The subagent executor additionally needs
@deepseek-ai/dsh-subagent.
The task and instance stores live 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:
- id: storage-domain
config:
backend: json
routes:
a2a: sqlite
- insert:
- id: storage-sqlite
name: '@deepseek-ai/dsh-storage-sqlite'
config:
path: /absolute/path/to/a2a.sqlite
A /a2a chat command mirrors the dashboard (a text backup to the GUI):
a2a status | presets | peers |
inbound list|create|remove|enable|disable |
outbound list|create|remove|enable|disable|refresh |
tasks | task get|cancel <id> |
sessions | session cancel|close <contextId> | help
InboundServerManager owns every instance: one preset-bound
session pool + A2AServer + routes per instance. Each instance persists in
the inbound_servers table and serves its own endpoint + AgentCard. Tasks
flow through a2a/inbound-task → executor → task store, with SSE frames
streamed to subscribers. A per-instance session registry observes SSE
lifecycle hooks; the facade folds those live observations over the shared
task store into per-contextId session views.OutboundServerManager owns every connection: one
OutboundAgentRegistry with an isolated agent store per instance,
persisted in the outbound_servers table. A2AClient discovers an
AgentCard, and each skill registers as a tool.settings.section) reads/writes
the loopback-only /a2a/api route; the host half feeds it snapshots of
inbound/outbound server views and the preset roster (/a2a/api/presets).See docs/architecture.md for the full design.
src/
api.ts # loopback dashboard API (/a2a/api, /a2a/api/presets)
index.ts # Cordis plugin entry (apply)
protocol.ts # A2A v1.0.1 protocol constants + types
jsonrpc.ts # JSON-RPC framing
servers/ # multi-instance managers
inbound-manager.ts # inbound server instances (CRUD, routes, lifecycle)
outbound-manager.ts # outbound connection instances (CRUD, tools)
server/ # single-instance internals: store, card, a2a-server,
# routes, executors, inbound-registry, session-registry
outbound/ # outbound internals: A2AClient, registry, tools
client/ # browser half: settings dashboard (React)
service.ts # ctx.a2a service facade
commands.ts # /a2a chat command
tests/
unit/ # protocol, framing, card, store, registry, server,
# client, api, inbound-registry, identity
composition/ # apply() on a real Cordis Context with stub host services
integration/ # real agent loop + JSONL persistence, mock LLM
types/ # compile-time mirror check against real DSH types
scripts/
e2e-a2a.mjs # hermetic two-instance A2A smoke
docs/ # public manual + architecture/compatibility/testing
cordis.patch.yml # bundle patch (mounts the plugin; instances are GUI-managed)
The public manual describes the current behavior in depth:
Deep dives: Architecture, Harness compatibility, and Testing.
The plugin typechecks against the harness source graph through project references; a harness checkout with its built host aggregate is required.
pnpm typecheck # host + client + compile-time mirror check
pnpm test # vitest run (unit + composition + real persistence)
pnpm build # tsc + tsdown → lib/index.js (host) + lib/client.js (browser)
pnpm test:e2e # hermetic two-instance A2A smoke against the built artifact
pnpm test:all # typecheck + test + build + e2e
Stub tests alone cannot see host-runtime contract drift. The layers, what each catches, and the e2e flow are in docs/testing.md.
This plugin was inspired by and developed alongside ryubyte/dsh-a2a, an earlier A2A plugin for DeepSeek Harness. That project’s design — dual-end scope, AgentCard advertisement, and the settings-dashboard pattern — set the direction this implementation follows. Our protocol layer, task store, and executor seam are independent implementations; the GUI management model owes a direct debt to ryubyte’s connection dashboard.
MIT