@amaster.ai/dsh-a2a
v0.1.16
Published
A2A protocol (JSON-RPC + SSE) server plugin for DeepSeek Harness: expose dsh agents as A2A agents.
Keywords
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-a2aConfiguration
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 = ADCEndpoints
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. By default, 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). - Same-task messages.
ctx.on('a2a/working-task-message', async (detail) => { ... })runs when another message targets an executing task, before waiting for that task's prior request. Its detail containsagent,contextId,taskId, the originala2aMessage(including itsmessageId),requestHeaders, and an abortsignal; the host can use it to end or revise pending continuation. Failure rejects the new request without enqueueing it. The plugin still serializes agent follow-ups. - Host admission and settlement.
ctx.on('a2a/before-followup', async (detail) => { ... })runs before every user message enters the agent inbox. Its detail containsagent,contextId,taskId, the originala2aMessage,dshMessageId,requestHeaders, and an abortsignal; rejection fails the A2A task without enqueueing the message. A host may inspect its own A2A message metadata here; the plugin does not interpret it. The existinga2a/message-admittedevent remains an observational tap after insertion. A host can registerctx.on('a2a/task-settlement', async ({ agent, contextId, taskId, reason, signal }, next) => { await hostWork; return next(); }). The hook starts after the first completed turn and may wait for any number of autonomous follow-ups, including none. The plugin streams their events under the same task ID and publishes the latest turn's final status when the hook resolves. Cancel, clear, and disposal abort the signal and release the waiter. Without this hook, the first completed turn settles as before. The host must end its wait when its work is done; the plugin does not infer that from product metadata. - 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. - Tool approvals. When the profile composes
@deepseek-ai/dsh-user-approvalwith policyask, A2A-owned agents answerapproval/requestby publishinginput-requiredwith a DataPart{ requestId, toolName, reason?, callId? }. Reply on the sametaskId/contextIdwith one DataPart{ requestId, callId?, outcome: "allowed-once" | "rejected" }. The reply settles the waiting tool call inside the original dsh turn; it does not create another user message or billing turn. Invalid, duplicate, and withdrawn replies are rejected.neverpolicy rejects inside dsh before the A2A answerer is called. The approval service is optional; without it,askfails closed as usual. - Custom approval payloads. A deployment may register
ctx.provide('a2aApprovalCodec', codec)before this plugin starts. The exportedA2aApprovalCodechasencode(request)anddecode(message)methods;decodereturns{ requestId?, callId?, outcome }only for a supported one-shot decision. A uniquecallIdcan replacerequestIdfor legacy clients. Reject unsupported outcomes in the decoder; never translateproceed_alwaysintoallowed-once. Other message types, including question, plan, and UI markup, remain outside this plugin. - Requests for one task execute in order. Autonomous turns covered by a settlement hook finish before the next user request starts. Approval confirmations settle a pending decision inside its current turn.
- 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 snapshots retain protocol history and artifacts; dsh-storage separately records the full conversation. Backends save on task-state transitions, including the complete reply at input-required, so token-rate stream events never reach Redis or GCS. While a task is working, in-process history accumulates text deltas for GetTask and resubscription; a restart before the next state change can lose those transient deltas.
memory(default) — in-process, lost on restartredis— task JSON under<keyPrefix>:tasks:<scope>:<taskId>with a TTL; requires theioredispeergcs— gzipped task JSON at<prefix>/<scope>/<taskId>/metadata.json.gz; requires the@google-cloud/storagepeer.archiveWorkspace()(tar of the workspace) exists but is not wired to the lifecycle yet.
<scope> is a hash of the A2A tenant and user. Older unscoped Redis/GCS task records are not read after this upgrade; migrate known records into their scope or let them expire before rollout.
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.2.0-rc.1 — the attachment store is an optional peer) — check the TODO(verify) markers in src/ before upgrading dsh.
License
MIT
