dsh-a2a
Verified@amaster.ai/dsh-a2a Β· v0.1.9 Β· MIT
A2A protocol (JSON-RPC + SSE) server plugin for DeepSeek Harness: expose dsh agents as A2A agents.
Install
dsh plugin add @amaster.ai/dsh-a2a Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Readme
@amaster.ai/dsh-a2a

A2A protocol server plugin for DeepSeek Harness (dsh): expose dsh agents as A2A agents β streaming turns, task cancel, agent card, and pluggable task-state stores. Speaks A2A 1.0 (JSON-RPC + SSE) on @a2a-js/sdk 1.x, with the SDK's opt-in v0.3 compatibility layer kept on so pre-1.0 clients keep working. Ported from the source project's packages/a2a-server.
dsh-a2a is the network boundary for a dsh agent. It gives A2A clients a stable task-oriented interface while the agent continues to use the profile's models, presets, tools, and workspace rules.
Install
dsh plugin --profile my-agent add @amaster.ai/dsh-a2a
Configuration
Disabled by default. Configure via the profile's cordis.patch.yml:
- insert:
- id: a2a
name: '@amaster.ai/dsh-a2a'
config:
enabled: true
host: 127.0.0.1 # no auth built in β keep loopback or front with a proxy
port: 41241
basePath: /a2a
cwd: /srv/agent-workspaces
uploadsDir: '' # file-part upload root; empty = <OS temp>/dsh-a2a-uploads/<date>
agent:
provider: '' # dsh provider/model for A2A sessions; empty = profile default
model: ''
preset: '' # agent preset mounted per task; empty = deployment default
card:
name: my-agent
description: My dsh agent over A2A
version: 0.1.0
publicUrl: https://agent.example.com
taskStore: redis # memory | redis | gcs
redis:
url: redis://127.0.0.1:6379
keyPrefix: a2a
ttlSeconds: 86400
gcs:
bucket: my-agent-archives
prefix: tasks
keyFilename: '' # GOOGLE_APPLICATION_CREDENTIALS path; empty = ADC
Endpoints
GET /.well-known/agent-card.jsonβ agent card: A2A 1.0 shape forA2A-Version: 1.0clients, the 0.3 shape for headerless clients (the legacy/.well-known/agent.jsonpath is served as an alias)POST <basePath>/β JSON-RPC. v1 methods:SendMessage(blocking by default,configuration.returnImmediately: truereturns after the first event),SendStreamingMessage(SSE),GetTask,ListTasks(filter + cursor pagination),CancelTask,SubscribeToTask(the current task is the first event; the bus stays alive while the task is interrupted, so interrupted tasks can be re-followed live). v0.3 spellings (message/send,message/stream,tasks/get,tasks/cancel,tasks/resubscribe) keep working through the compat layer. Legacytasks/getalso acceptscontextIdwithoutidon this path or root/, returning the latest stored task orresult: nullwhen absent.
Behavior notes
- One task = one dsh session. The A2A
contextIdIS the dsh session id. A completed turn endsinput-required, notcompletedβ the task is a conversation and stays continuable;CancelTaskand turn errors are terminal (canceled/failed), and the SDK rejects follow-ups addressed at a terminaltaskId(send with only thecontextIdto continue the session under a fresh task id). - Agents are full preset citizens. Each task's agent is created with the deployment's default model selection (
agentDefaultModel) and mounts its agent preset (the web profile keeps all tools inside presets β without one the agent would see an empty tool catalog).agent.presetpins a specific preset. - Streaming aggregation. Text deltas of a turn share one
messageId, so clients accumulate them into a single message; reasoning deltas ride a separatemessageIdand are markedmetadata.dshAgent.kind: 'thought'. The turn-final event's message carries the full assembled text, so blockingSendMessageclients read the answer fromresult.task.status.message. Tool calls/results are data parts markedtool-call/tool-result; token usage lands inmetadata.usageof the final event. A2A 1.0 has nofinalflag β terminal and interrupted states close the stream. - Message parts beyond text. File parts can carry inline bytes or a
url; the plugin downloads URLs over http/https, bounded to 64 MiB and 30 s. Supported images use the composed attachment store (ctx.attachments, e.g.@deepseek-ai/dsh-attachment-local) to reach vision-capable models. Non-image files use a configured materializer or persist underuploadsDir(default<OS temp>/dsh-a2a-uploads/<date>/, names sanitized cross-platform, collisions suffixed), even when an attachment store exists. The prompt references the resulting readable path inside a<document>tag. Ensure the local directory is readable by the agent's tools.dataparts become<data>JSON text. A turn never fails because a part kind is unsupported. - Remote execution files. A deployment can register
ctx.provide('a2aFileMaterializer', { materializeFile }), using the exportedA2aFileMaterializertype. The callback receives{ contextId, bytes, filename, mediaType }(with a sanitized basename) and returns{ readablePath }, a path readable by that context's file/shell tools. When configured, non-image FileParts use the materializer instead ofctx.attachments.saveFile(); the model receives the returned<document path="...">. Images keep the existing attachment handling. A failed callback yields a delivery note, never a host-path fallback. The deployment owns sandbox selection, upload, and lifetime; no E2B SDK is required by this plugin. - Local execution files. Without a remote materializer, inline and downloaded non-image FileParts persist under
uploadsDir; the<document>envelope carries the path, source URL, media type, size, and original filename when sanitization or a collision changes it. Images still use native attachment handling. - No approval bridge: dsh ships the mid-turn approval seam only as the optional
dsh-user-approvalpackage, which headless profiles do not compose, so tools that would ask are governed by the profile's own approval setup; the A2A side never enters a mid-turninput-required. - One in-flight message per task is the supported flow (send the next message after the turn-final event). dsh serializes queued follow-ups into successive turns, but concurrent requests share one event bus β a second in-flight request may resolve with the first turn's final event.
- Restart: persisted task shells survive in Redis/GCS. With
sessionPersistencecomposed, acontextIdalready present on disk resumes its dsh session after restart; a new id creates a new session. - Clear: the same-process gateway can call
ctx.get('a2aTasks').clearContext(contextId)before reportingmessages/clearsuccess. It cancels and drains the live turn, removes the binding and every TaskStore shell for that context, and returns the removed task IDs. The gateway owns the separate session-surface replacement and client-visible history-ID update.
Task stores
A2A task state (status + metadata) is separate from conversation history β use dsh-storage for the latter. Every backend persists a sanitized metadata shell (history/artifacts stripped) and saves only on task-state transitions, so token-rate stream events never reach the backend.
memory(default) β in-process, lost on restartredisβ task JSON under<keyPrefix>:tasks:<taskId>with a TTL; requires theioredispeergcsβ gzipped task JSON at<prefix>/<taskId>/metadata.json.gz(same layout as the source project'sGCSTaskStore); requires the@google-cloud/storagepeer.archiveWorkspace()(tar of the workspace) exists but is not wired to the lifecycle yet.
Security
dsh ships no authentication or authorization. The server binds 127.0.0.1 by default; if you expose it, put an authenticated reverse proxy in front and treat every agent as running with the host process's OS privileges. File parts carrying a url are fetched server-side (http/https only, bounded) β another reason to keep the endpoint off untrusted networks.
Compatibility
Pinned dsh/cordis versions live in the root compat matrix. Event payloads ride pre-release dsh APIs (@deepseek-ai/dsh-{agent,session,llm,attachment}@0.1.6-alpha.2 β the attachment store is an optional peer) β check the TODO(verify) markers in src/ before upgrading dsh.
License
MIT