Agent2Agent(A2A)v1.0.1 双端插件,用于 DeepSeek Harness。
English · 中文
本参考描述已交付的插件。设计讨论(中文,v1.0 多实例)见 design.md;接线相关的 implemented Agent Note 在 harness checkout 中。
插件以一条 Cordis 行(id a2a)挂载到任何装有标准宿主服务的 DSH profile 上。核心抽象是server 实例:一套可复用的框架——端点 + 身份/技能宣告 + preset 绑定 + 鉴权——入站方向每实例一份、出站连接每份一份。
┌─ GUI(浏览器, settings.section "A2A 连接") ─────────────────────────────┐
│ 入站 server 实例(创建/preset/技能/鉴权/启停/编辑) │
│ 出站 server 实例(创建/URL/preset/鉴权/超时/启停) │
│ 任务(按实例查看/取消)· 入站对端 · 会话 │
└───────────────────────────────────────┬─────────────────────────────────┘
│ loopback-only /a2a/api
┌─ Host 半区 ───────────────────────────▼─────────────────────────────────┐
│ InboundServerManager 每实例:preset 化会话池 + A2AServer + │
│ A2aRoutes(独立端点 + 卡片路由) │
│ OutboundServerManager 每实例:OutboundAgentRegistry(独立 agent 存储)│
│ + 远端 A2AClient + 工具 │
│ A2aDomain(tasks/contexts/agents/identity/inbound_servers/ │
│ outbound_servers)· ctx.a2a facade │
└─────────────────────────────────────────────────────────────────────────┘
.research/A2A 为权威):PascalCase 方法(SendMessage 等)、TASK_STATE_* / ROLE_* 枚举、完整 AgentCard 结构(supportedInterfaces、capabilities、securitySchemes、skills)、规范错误码表。@hanphone/dsh-a2a,经 dsh plugin add 安装(npm 或本地 tarball)。a2a 域而非插件配置。插件 Config 只承载宿主级默认值(baseUrl、subagentProvider、defaultTimeoutMs)。agentPresets.defaultId)。invocation.modelInvocable)的条目:agentPresets.standingKeyFor(preset) 取得该 preset standing scope key,ctx.skills.list({ scope }) 返回该 preset agent 实际可见的目录(preset 层 + 部署全局)。纯自动派生——v0.2 工具白名单与创建者手写宣告表单均删除;skills 服务缺失时兜底内置 chat 技能,保持最小组合可启动可验证。server.enabled / client.agents 等不再被读取。已实现方法:SendMessage(messageId 幂等、taskId 续接)、SendStreamingMessage(SSE,assistant/工具增量 + 事件 id)、GetTask、ListTasks(contextId / status / pageSize / pageToken / statusTimestampAfter / includeArtifacts)、CancelTask、SubscribeToTask(SSE,支持进程内 Last-Event-ID 回放)、GetExtendedAgentCard。Push 通知方法在方法面声明并按 PushNotificationNotSupported 应答。序列化按 ADR-001 ProtoJSON(TASK_STATE_* / ROLE_USER / ROLE_AGENT,camelCase 字段);传输校验 A2A-Version 头;错误使用规范错误码表。
InboundServerManager 拥有活跃实例集。每个持久化 InboundServerRecord(inbound_servers 表)在启动时成为一个活实例:
ContextSessionPool(装有 agents 时)——会话按 record.preset 经 agentPresets.resolve + mount 组装;ScopedTaskStore(按实例 serverId 过滤)上的 A2AServer,带独立端点(/a2a/<id>)与卡片路由(/a2a/<id>/agent-card.json);A2aRoutes 注册(启停)、ExecutorSet(session/subagent)、LiveInboundRegistry(对端监控)。管理器拥有完整生命周期:add(持久化 + 组装 + 启用)、update(持久化 + 重建卡片 + 换装)、setEnabled(路由启停)、remove(dispose + 反持久化)。技能按实例从所绑 preset 的技能目录派生(见设计决策 §5),无 skills 服务时兜底内置 chat;门禁的宣告技能白名单即派生列表,远端技能调用总能落到该实例的 preset 会话内。
POST /a2a/<id> (SendMessage)
→ parseRpc(JSON-RPC 2.0、A2A-Version、按实例鉴权)
→ server.ensureTask(message)
contextId + skill(metadata.skill ?? 'chat');任务记录来源 serverId
→ store.create → taskId a2a-<uuid>、SUBMITTED(急切持久化)
→ gate(input)
按实例的宣告技能白名单
ctx.waterfall('a2a/inbound-task', decision) # 否决/审计
→ runTask(record)
WORKING → executor.execute(...) # 事件 → store + SSE
成功 → artifact 'result' → COMPLETED
失败 → FAILED(携带执行器错误)
→ 取消路径:CancelTask / facade → abort signal → CANCELED
SubscribeToTask / SendStreamingMessage 在订阅时补发当前状态帧,然后流式推送更新与终态任务。
contextId 一个 DSH 会话,按实例 preset 组装;结果 = 落定回复。ctx.subagents,工具调用过程以 artifact 事件流式回传。没有 agent 循环时 server 仍以可读拒绝应答。会话层让每个 contextId 的 DSH 会话可观测、可回收,且完全不动 A2A 协议面(协议只认识 task/contextId)。
onSessionOpened 钩子(恰在 justOpened 时触发)经 TaskStore.setContextSession 持久化 contextId → sessionId 绑定(内存先行,保持域写链可见)。绑定写入失败绝不放倒进行中的任务——记录日志继续。SessionView 行按 contextId 聚合:共享任务存储给出计数、running/idle、首次/最近活动,再折叠活跃观测——每实例 ContextSessionRegistry 的流式计数(由 A2AServer 的 SSE 开/关钩子喂养)与会池句柄存在性。数据来源优先级:实时优先、任务存储推导兜底;没有 agent 循环(无会池)的组合仍能从任务记录渲染会话表;重启后因任务持久化而行的降级形态仍在(无句柄/无流式),只有内存观测被清空,与对端注册表一致。cancelSessionTasks(contextId) 经既有 A2AServer.abort 路径中止该 context 的全部非终态任务(不新造基础设施);closeSession(contextId) 额外经 ContextSessionPool.disposeContext 释放该 context 的活跃句柄并移出会池。关闭语义:关闭是内存级资源释放,不是会话墓碑——A2A 没有”已关闭上下文”概念,同一 contextId 之后再来任务经 agentFor 自动重开新句柄、永不拒绝;重开后的 DSH 会话是否恢复持久化历史属宿主行为,不在本插件控制内。A2AServer 把它创建的每条任务记录盖上实例 id(TaskStore.create.serverId),并通过 ScopedTaskStore 让 GetTask/ListTasks/CancelTask/SubscribeToTask 只能看到本实例任务;context 绑定与派生 session id 也带实例命名空间,两个实例可安全复用同一客户端 contextId。任务还有可配置执行超时,实例有请求/请求体/并发限制。facade 暴露 listSessions / cancelSessionTasks / closeSession;会话也随 status() 快照(sessions 字段)与回环 API(session.cancel / session.close,与所有控制动作一样仅回环)输出。
任务与绑定存于 a2a 存储域(DomainFacility.open → Domain → 类型化 KvTable),JSON 编码记录。表:tasks(每条记录带可选来源实例 serverId)、contexts、agents(按实例的出站 agent 记录)、identity、inbound_servers、outbound_servers。DomainTaskStore 自持写链可见的活跃视图,使同步读能看到自身写入。
OutboundServerManager 拥有活跃连接集。每个持久化 OutboundServerRecord(outbound_servers 表)在启动时成为一个 OutboundAgentRegistry,带独立按实例 AgentStore(agents 表,键 out:<id>)。管理器拥有完整生命周期:add(持久化 + 连接)、setEnabled、refresh、remove(dispose + 反持久化);viewFor 折合注册表视图与记录(GUI 读 OutboundServerView,含 preset 与 per-skill 视图)。出站工具支持远端流式、abort 传播(CancelTask)、瞬时失败重试、taskId 续接;连接按配置间隔自动刷新 AgentCard;出站 preset 仍是未来本地衔接会话的持久化元数据;工具直接调用远端。
每个已连接实例的每个技能经 registerAgentTools 映射为模型工具 a2a__<name>__<skill>(规范化、冲突哈希)。执行工具以稳定 per-caller contextId 运行 A2AClient.sendMessage 并返回落定任务文本。P0 出站工具直接调用远端;preset 化本地衔接会话组装为文档化扩展点。
src/client/)— React 插件,经 ctx.slots.inject 注册为 settings.section(”A2A 连接”),由 DSH web shell 加载其客户端 bundle(样式在 client/dashboard.css.ts,--dsw-alias-* token + @container 响应式)。三个 Tab:入站 Servers(preset 选择器只列真实 roster 预设并默认选中部署默认、派生技能 chips、Bearer Token 输入)、出站 Servers(两阶段”导入→预览→连接”)、连接与任务(对端 + 会话 + 任务,分别以 crew 区块纵向堆叠)。/a2a/api)— GET 返回快照(入站/出站 server 视图、任务、对端、会话);GET /a2a/api/presets 返回 agent-preset 名单供选择器使用;POST 派发控制动作(inbound.create/update/remove/enable/disable/setAuth、outbound.create/update/remove/enable/disable/refresh/setAuth/discover、task.cancel、inbound.close、session.cancel/session.close)。非回环调用 403。GUI、/a2a 命令与 ctx.a2a 消费方共用同一 facade 实现。ctx.credentials.set → 托管 .env/凭据库,目录 0o700)写入;实例记录只保留自动生成的变量名(A2A_INBOUND_<id> / A2A_OUTBOUND_<id>)。运行时 resolveAuthToken 每操作分层读取(凭据 → 进程环境),外部 export 同名变量仍兼容。token 明文绝不进 a2a 域、插件配置或 AgentCard,GUI 不回显。默认组合把 a2a 域路由到 JSON 后端。可在同一 patch 层把域路由到 storage-sqlite 以启用 SQLite。
该包刻意不是 harness checkout 的 workspace 成员:自带 package.json 与依赖图,@deepseek-ai/* 为 peer 依赖,zod 为唯一自有运行时依赖。它通过项目引用消费 harness 源码图的已构建 lib/types 声明做类型检查;全新 checkout 需先构建宿主聚合。
tests/unit/ — 协议常量、JSON-RPC/SSE 帧、卡片组装与技能默认值、store(含写链可见性回归)、执行器解析、A2A server(dispatch、gate、auth、cancel、streaming)、入站注册表、会话注册表(流式计数、视图聚合含无会池降级路径)、agent 会话池(has/disposeContext/重开)、出站 client 与注册表(stub fetch)、面板 API。tests/composition/ — 在真实 Cordis Context 上以 stub 宿主服务跑 apply():多实例组装、按实例路由注册、宣告技能门禁、a2a/inbound-task 否决、任务持久化、出站工具注册、实例 CRUD/删除。