灵隙(synapse) v1.0.0 版本
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.idea
|
||||
245
MIGRATION.md
Normal file
245
MIGRATION.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# A2A 协议迁移说明
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了从 `ai-video-factory` 项目中抽离 A2A(Agent-to-Agent)协议和 Group(群组)相关代码到独立模块的过程。
|
||||
|
||||
## 源项目信息
|
||||
|
||||
- **源项目**: `ai-video-factory`
|
||||
- **源路径**: `ai-video-factory/a2aprotocol/`
|
||||
- **目标项目**: `dp-headless`
|
||||
- **目标路径**: `dp-headless/a2a/`
|
||||
|
||||
## 模块路径变更
|
||||
|
||||
| 项目 | 模块路径 |
|
||||
|------|----------|
|
||||
| 源 | `ai-video-factory/a2aprotocol/` |
|
||||
| 目标 | `dp-headless/a2a/` |
|
||||
|
||||
所有 Go 文件中的 import 路径已从 `ai-video-factory/a2aprotocol/xxx` 更新为 `dp-headless/a2a/xxx`。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
a2a/
|
||||
├── go.mod # Go 模块定义
|
||||
├── proto/
|
||||
│ └── a2a.proto # gRPC 协议定义
|
||||
├── types/ # 核心类型定义
|
||||
│ ├── types.go # 基础类型、消息类型、任务状态
|
||||
│ ├── content.go # 内容类型(文本、图片、视频、音频等)
|
||||
│ └── errors.go # 错误定义
|
||||
├── p2p/ # P2P 协议
|
||||
│ └── protocol.go # 点对点任务处理协议
|
||||
├── group/ # 群组协议
|
||||
│ └── protocol.go # 群组协作协议(创建、解散、任务拆分合并)
|
||||
├── routing/ # 消息路由
|
||||
│ └── router.go # 消息路由器(P2P/群组分发)
|
||||
├── transport/ # 传输层
|
||||
│ ├── transport.go # HTTP 传输实现
|
||||
│ └── multipart_polling.go # 多部分文件传输和轮询客户端
|
||||
├── capabilities/ # 能力发现
|
||||
│ └── provider.go # Agent 能力提供者
|
||||
├── externalapi/ # 外部 REST API
|
||||
│ └── handler.go # 简化的 RESTful API 处理器
|
||||
├── sdk/ # SDK
|
||||
│ └── client.go # 外部调用者 SDK 客户端
|
||||
├── grpcapi/ # gRPC API
|
||||
│ ├── a2a.pb.go # gRPC 消息生成代码
|
||||
│ ├── a2a_grpc.pb.go # gRPC 服务生成代码
|
||||
│ ├── client.go # gRPC 客户端实现
|
||||
│ └── server.go # gRPC 服务端实现
|
||||
├── hil/ # 人机协同(HiL)
|
||||
│ └── protocol.go # 人工介入任务处理协议
|
||||
└── examples/ # 示例代码
|
||||
└── main.go # 完整使用示例
|
||||
```
|
||||
|
||||
## 迁移的文件清单
|
||||
|
||||
### types 包(3 个文件)
|
||||
- `types/types.go` - 核心数据类型:AgentEndpoint、A2AMessage、TaskPayload、TaskStatus 等
|
||||
- `types/content.go` - 内容类型:TextContent、ImageContent、VideoContent、AudioContent、FileContent
|
||||
- `types/errors.go` - 错误定义:TaskError、错误码常量
|
||||
|
||||
### p2p 包(1 个文件)
|
||||
- `p2p/protocol.go` - P2P 协议实现:任务提交、状态更新、结果回调、取消任务
|
||||
|
||||
### group 包(1 个文件)
|
||||
- `group/protocol.go` - 群组协议实现:群组创建/解散、成员管理、任务拆分/合并、投票机制
|
||||
|
||||
### routing 包(1 个文件)
|
||||
- `routing/router.go` - 消息路由器:根据消息类型分发到 P2P 或群组处理器
|
||||
|
||||
### transport 包(2 个文件)
|
||||
- `transport/transport.go` - HTTP 传输:发送消息、处理回调、签名验证
|
||||
- `transport/multipart_polling.go` - 多部分文件传输和轮询客户端
|
||||
|
||||
### capabilities 包(1 个文件)
|
||||
- `capabilities/provider.go` - Agent 能力提供者:能力声明、能力查询
|
||||
|
||||
### externalapi 包(1 个文件)
|
||||
- `externalapi/handler.go` - 外部 REST API:任务提交/查询/取消、能力查询
|
||||
|
||||
### sdk 包(1 个文件)
|
||||
- `sdk/client.go` - SDK 客户端:封装 A2A 协议调用,简化外部集成
|
||||
|
||||
### grpcapi 包(4 个文件)
|
||||
- `grpcapi/a2a.pb.go` - Protobuf 生成的消息类型
|
||||
- `grpcapi/a2a_grpc.pb.go` - Protobuf 生成的 gRPC 服务接口
|
||||
- `grpcapi/client.go` - gRPC 客户端实现
|
||||
- `grpcapi/server.go` - gRPC 服务端实现
|
||||
|
||||
### hil 包(1 个文件)
|
||||
- `hil/protocol.go` - 人机协同协议:人工决策请求/响应、任务挂起/恢复
|
||||
|
||||
### proto(1 个文件)
|
||||
- `proto/a2a.proto` - gRPC 协议定义文件
|
||||
|
||||
### examples(1 个文件)
|
||||
- `examples/main.go` - 完整使用示例
|
||||
|
||||
**总计:17 个 Go 源文件 + 1 个 proto 文件 + 1 个 go.mod**
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. P2P 任务协议
|
||||
- 任务提交与响应
|
||||
- 任务状态查询与更新
|
||||
- 任务取消
|
||||
- 进度回调
|
||||
- 信息缺失通知
|
||||
|
||||
### 2. 群组协作协议
|
||||
- 群组创建与解散
|
||||
- 成员加入与离开
|
||||
- 任务拆分与分发
|
||||
- 子任务结果收集
|
||||
- 结果合并
|
||||
- 投票机制(发起投票、提交投票、公布结果)
|
||||
|
||||
### 3. 传输层
|
||||
- HTTP 回调模式
|
||||
- 轮询模式
|
||||
- 同步模式
|
||||
- 混合模式
|
||||
- HMAC 签名验证
|
||||
- 多部分文件上传
|
||||
|
||||
### 4. 能力发现
|
||||
- Agent 能力声明
|
||||
- 能力查询接口
|
||||
- 支持的任务类型
|
||||
- 支持的传输模式
|
||||
- 并发能力限制
|
||||
|
||||
### 5. 人机协同(HiL)
|
||||
- 人工决策请求
|
||||
- 决策响应
|
||||
- 任务挂起与恢复
|
||||
- 超时处理
|
||||
|
||||
### 6. gRPC 支持
|
||||
- TaskService - 任务服务
|
||||
- A2AMessageService - 消息服务
|
||||
- CapabilitiesService - 能力服务
|
||||
- GroupService - 群组服务
|
||||
- HealthService - 健康检查
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 快速开始
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"dp-headless/a2a/capabilities"
|
||||
"dp-headless/a2a/externalapi"
|
||||
"dp-headless/a2a/group"
|
||||
"dp-headless/a2a/p2p"
|
||||
"dp-headless/a2a/routing"
|
||||
"dp-headless/a2a/transport"
|
||||
"dp-headless/a2a/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 定义 Agent 端点
|
||||
agentEndpoint := types.AgentEndpoint{
|
||||
AgentID: "my-agent",
|
||||
AgentName: "我的Agent",
|
||||
SystemID: "my-system",
|
||||
Endpoint: "http://localhost:8080/api/v1/a2a/callback",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
// 2. 配置能力提供者
|
||||
caps := &capabilities.AgentCapabilityProvider{
|
||||
AgentID: agentEndpoint.AgentID,
|
||||
AgentName: agentEndpoint.AgentName,
|
||||
SystemID: agentEndpoint.SystemID,
|
||||
SupportedTaskTypes: []capabilities.SupportedTaskType{
|
||||
{Type: "text_generation", Name: "文本生成"},
|
||||
},
|
||||
SupportGroups: true,
|
||||
SupportHiL: true,
|
||||
}
|
||||
|
||||
// 3. 创建 P2P 协议处理器
|
||||
p2pProtocol := p2p.NewP2PProtocol(agentEndpoint, caps,
|
||||
func(taskID string, taskType string, payload map[string]interface{}) (map[string]interface{}, error) {
|
||||
// 处理任务逻辑
|
||||
return map[string]interface{}{"result": "success"}, nil
|
||||
})
|
||||
|
||||
// 4. 创建群组协议处理器
|
||||
groupConfig := &group.GroupConfig{MaxGroupSize: 10}
|
||||
groupProtocol := group.NewGroupProtocol(agentEndpoint, nil, groupConfig)
|
||||
|
||||
// 5. 配置消息路由
|
||||
router := routing.NewMessageRouter(agentEndpoint)
|
||||
router.RegisterP2PHandler(p2pProtocol)
|
||||
router.RegisterGroupHandler(groupProtocol)
|
||||
|
||||
// 6. 启动 HTTP 服务
|
||||
httpTransport := transport.NewHTTPTransport(agentEndpoint, nil, router)
|
||||
extHandler := externalapi.NewExternalAPIHandler(p2pProtocol, caps, httpTransport)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
extHandler.RegisterRoutes(mux, "/api/v1/a2a")
|
||||
http.ListenAndServe(":8080", mux)
|
||||
}
|
||||
```
|
||||
|
||||
完整示例请参考 `examples/main.go`。
|
||||
|
||||
## 依赖项
|
||||
|
||||
```
|
||||
- github.com/golang/protobuf v1.5.4
|
||||
- github.com/gorilla/websocket v1.5.3
|
||||
- golang.org/x/image v0.42.0
|
||||
- google.golang.org/grpc v1.68.0
|
||||
- google.golang.org/protobuf v1.35.2
|
||||
- modernc.org/sqlite v1.52.0
|
||||
```
|
||||
|
||||
## 未迁移的内容
|
||||
|
||||
以下内容因与 ai-video-factory 业务逻辑强耦合,未包含在本次迁移中:
|
||||
|
||||
1. **旧版 A2A 实现**(`a2a.go` 和 `a2a_api.go`)- 基于 SQLite 的简单实现,与视频生成工作流深度绑定
|
||||
2. **数据库集成** - 旧版的 SQLite 数据库层
|
||||
3. **视频工作流集成** - 与 VideoCreationWorkflow 的集成代码
|
||||
|
||||
如需数据库持久化,建议基于现有的协议层自行实现存储适配层。
|
||||
|
||||
## 后续可扩展方向
|
||||
|
||||
1. 添加持久化存储接口(Memory/Redis/PostgreSQL 等)
|
||||
2. 实现完整的 gRPC 网关
|
||||
3. 添加更多传输协议支持(WebSocket、MQTT 等)
|
||||
4. 增加单元测试和集成测试
|
||||
5. 完善文档和示例
|
||||
546
capabilities/provider.go
Normal file
546
capabilities/provider.go
Normal file
@@ -0,0 +1,546 @@
|
||||
package capabilities
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Capabilities Provider Interface
|
||||
// =============================================================================
|
||||
|
||||
// CapabilitiesProvider 定义Agent暴露自身能力的接口
|
||||
// 所有Agent都应该实现此接口以便其他Agent查询其能力
|
||||
type CapabilitiesProvider interface {
|
||||
// GetCapabilities 返回Agent的完整能力描述
|
||||
GetCapabilities(ctx context.Context) (*types.AgentCapabilities, error)
|
||||
|
||||
// GetSupportedInterfaces 返回Agent实现的接口列表
|
||||
GetSupportedInterfaces() []ImplementedInterface
|
||||
|
||||
// GetTaskTypeCapabilities 返回特定任务类型的详细能力
|
||||
GetTaskTypeCapabilities(taskType string) (*types.SupportedTaskType, error)
|
||||
}
|
||||
|
||||
// ImplementedInterface 定义Agent实现的接口
|
||||
type ImplementedInterface struct {
|
||||
// InterfaceName 接口名称
|
||||
Name string `json:"name"`
|
||||
|
||||
// Category 接口分类
|
||||
Category InterfaceCategory `json:"category"`
|
||||
|
||||
// Version 接口版本
|
||||
Version string `json:"version"`
|
||||
|
||||
// ComplianceLevel 合规级别 (basic, complete, advanced)
|
||||
ComplianceLevel string `json:"compliance_level"`
|
||||
|
||||
// Description 接口描述
|
||||
Description string `json:"description"`
|
||||
|
||||
// Methods 接口支持的方法
|
||||
Methods []InterfaceMethod `json:"methods,omitempty"`
|
||||
|
||||
// Status 接口状态 (implemented, partial, planned)
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// InterfaceCategory 接口分类
|
||||
type InterfaceCategory string
|
||||
|
||||
const (
|
||||
CategoryCore InterfaceCategory = "core" // 核心接口
|
||||
CategoryP2P InterfaceCategory = "p2p" // P2P接口
|
||||
CategoryGroup InterfaceCategory = "group" // 群组接口
|
||||
CategoryHiL InterfaceCategory = "hil" // 人工介入接口
|
||||
CategoryTransport InterfaceCategory = "transport" // 传输接口
|
||||
CategorySecurity InterfaceCategory = "security" // 安全接口
|
||||
CategoryManagement InterfaceCategory = "management" // 管理接口
|
||||
)
|
||||
|
||||
// InterfaceMethod 接口方法
|
||||
type InterfaceMethod struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputType string `json:"input_type,omitempty"`
|
||||
OutputType string `json:"output_type,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Standard Interfaces Registry
|
||||
// =============================================================================
|
||||
|
||||
// StandardInterfaces 定义Synapse协议的标准接口
|
||||
var StandardInterfaces = map[string]ImplementedInterface{
|
||||
// === Core Interfaces ===
|
||||
"agent_identity": {
|
||||
Name: "AgentIdentity",
|
||||
Category: CategoryCore,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "basic",
|
||||
Description: "提供Agent身份信息接口",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "GetAgentID", Description: "返回Agent唯一标识", OutputType: "string", Required: true},
|
||||
{Name: "GetSystemID", Description: "返回Agent所属系统ID", OutputType: "string", Required: true},
|
||||
{Name: "GetEndpoint", Description: "返回Agent回调端点", OutputType: "string", Required: true},
|
||||
{Name: "GetName", Description: "返回Agent名称", OutputType: "string", Required: false},
|
||||
{Name: "GetVersion", Description: "返回Agent版本", OutputType: "string", Required: false},
|
||||
},
|
||||
},
|
||||
"message_handler": {
|
||||
Name: "MessageHandler",
|
||||
Category: CategoryCore,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "basic",
|
||||
Description: "处理接收到的Synapse消息",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "HandleMessage", Description: "处理消息并返回响应", InputType: "UnifiedSynapseMessage", OutputType: "UnifiedSynapseMessage", Required: true},
|
||||
{Name: "GetProtocolVersion", Description: "返回支持的协议版本", OutputType: "string", Required: true},
|
||||
},
|
||||
},
|
||||
"capabilities_provider": {
|
||||
Name: "CapabilitiesProvider",
|
||||
Category: CategoryCore,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "basic",
|
||||
Description: "暴露Agent能力信息",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "GetCapabilities", Description: "返回完整能力描述", OutputType: "AgentCapabilities", Required: true},
|
||||
{Name: "GetSupportedInterfaces", Description: "返回实现的接口列表", OutputType: "[]ImplementedInterface", Required: true},
|
||||
},
|
||||
},
|
||||
|
||||
// === P2P Interfaces ===
|
||||
"task_handler": {
|
||||
Name: "P2PTaskHandler",
|
||||
Category: CategoryP2P,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "basic",
|
||||
Description: "处理P2P任务请求",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "ExecuteTask", Description: "执行任务并返回结果", InputType: "taskType, payload", OutputType: "any", Required: true},
|
||||
{Name: "ValidateTask", Description: "验证任务是否可执行", InputType: "taskType, payload", OutputType: "[]MissingField, bool", Required: true},
|
||||
{Name: "GetSupportedTaskTypes", Description: "返回支持的任务类型列表", OutputType: "[]string", Required: true},
|
||||
{Name: "GetEstimatedDuration", Description: "预估任务执行时间", InputType: "taskType, payload", OutputType: "int", Required: false},
|
||||
},
|
||||
},
|
||||
"callback_sender": {
|
||||
Name: "CallbackSender",
|
||||
Category: CategoryP2P,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "complete",
|
||||
Description: "发送回调消息",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "SendCallback", Description: "发送回调消息", InputType: "endpoint, message", OutputType: "error", Required: true},
|
||||
{Name: "SendCallbackWithRetry", Description: "带重试的回调发送", InputType: "endpoint, message, maxRetries", OutputType: "error", Required: false},
|
||||
},
|
||||
},
|
||||
"callback_receiver": {
|
||||
Name: "CallbackReceiver",
|
||||
Category: CategoryP2P,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "basic",
|
||||
Description: "接收回调消息的HTTP端点",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "POST /synapse/callback", Description: "接收回调消息的HTTP端点", InputType: "UnifiedSynapseMessage", OutputType: "HTTP 200", Required: true},
|
||||
},
|
||||
},
|
||||
|
||||
// === Polling Interfaces ===
|
||||
"task_status_provider": {
|
||||
Name: "TaskStatusProvider",
|
||||
Category: CategoryP2P,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "complete",
|
||||
Description: "提供任务状态查询接口",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "GET /synapse/task/{id}", Description: "查询单个任务状态", OutputType: "TaskStatusResponse", Required: true},
|
||||
{Name: "GET /synapse/tasks", Description: "查询任务列表", OutputType: "TaskListResponse", Required: true},
|
||||
{Name: "GET /synapse/task/{id}/events", Description: "查询任务事件流", OutputType: "TaskEventsResponse", Required: false},
|
||||
},
|
||||
},
|
||||
"polling_client": {
|
||||
Name: "PollingClient",
|
||||
Category: CategoryTransport,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "complete",
|
||||
Description: "轮询客户端",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "GetTaskStatus", Description: "获取任务状态", InputType: "taskID", OutputType: "TaskInfo", Required: true},
|
||||
{Name: "StartPolling", Description: "开始持续轮询", InputType: "taskID, callback", OutputType: "error", Required: true},
|
||||
{Name: "StopPolling", Description: "停止轮询", InputType: "taskID", Required: true},
|
||||
},
|
||||
},
|
||||
|
||||
// === Group Interfaces ===
|
||||
"group_handler": {
|
||||
Name: "GroupTaskHandler",
|
||||
Category: CategoryGroup,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "advanced",
|
||||
Description: "处理群组任务协作",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "ExecuteSubtask", Description: "执行子任务", InputType: "subtaskID, payload", OutputType: "any", Required: true},
|
||||
{Name: "MergeResults", Description: "合并子任务结果", InputType: "results, strategy", OutputType: "any", Required: true},
|
||||
{Name: "ValidateTaskSplit", Description: "验证任务分解方案", InputType: "taskType, requirements", OutputType: "[]Subtask, bool", Required: true},
|
||||
},
|
||||
},
|
||||
"group_manager": {
|
||||
Name: "GroupManager",
|
||||
Category: CategoryGroup,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "advanced",
|
||||
Description: "管理群组",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "CreateGroup", Description: "创建群组", InputType: "GroupConfig", OutputType: "Group", Required: true},
|
||||
{Name: "JoinGroup", Description: "加入群组", InputType: "groupID", OutputType: "error", Required: true},
|
||||
{Name: "LeaveGroup", Description: "离开群组", InputType: "groupID", OutputType: "error", Required: true},
|
||||
{Name: "DisbandGroup", Description: "解散群组", InputType: "groupID", OutputType: "error", Required: true},
|
||||
},
|
||||
},
|
||||
"voting_handler": {
|
||||
Name: "VotingHandler",
|
||||
Category: CategoryGroup,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "advanced",
|
||||
Description: "处理投票共识",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "InitiateVote", Description: "发起投票", InputType: "VoteRequest", OutputType: "Vote", Required: true},
|
||||
{Name: "SubmitVote", Description: "提交投票", InputType: "voteID, option", OutputType: "error", Required: true},
|
||||
{Name: "GetVoteStatus", Description: "获取投票状态", InputType: "voteID", OutputType: "VoteStatus", Required: true},
|
||||
},
|
||||
},
|
||||
|
||||
// === HiL Interfaces ===
|
||||
"hil_handler": {
|
||||
Name: "HILHandler",
|
||||
Category: CategoryHiL,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "complete",
|
||||
Description: "处理人工介入请求",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "RequestHumanDecision", Description: "请求人工决策", InputType: "HILRequest", OutputType: "HILResponse", Required: true},
|
||||
{Name: "ProcessHumanDecision", Description: "处理人工决策响应", InputType: "HILResponse", OutputType: "error", Required: true},
|
||||
{Name: "GetHILStatus", Description: "获取HiL请求状态", InputType: "hilID", OutputType: "HILStatus", Required: true},
|
||||
},
|
||||
},
|
||||
"escalation_handler": {
|
||||
Name: "EscalationHandler",
|
||||
Category: CategoryHiL,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "advanced",
|
||||
Description: "处理升级请求",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "EscalateToHuman", Description: "升级到人工", InputType: "taskID, reason, level", OutputType: "HILRequest", Required: true},
|
||||
{Name: "EscalateToGroup", Description: "升级到群组", InputType: "taskID, groupID", OutputType: "error", Required: true},
|
||||
},
|
||||
},
|
||||
|
||||
// === Transport Interfaces ===
|
||||
"http_transport": {
|
||||
Name: "HTTPTransport",
|
||||
Category: CategoryTransport,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "basic",
|
||||
Description: "HTTP传输",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "Send", Description: "发送HTTP消息", InputType: "endpoint, message", OutputType: "error", Required: true},
|
||||
{Name: "Start", Description: "启动服务", Required: true},
|
||||
{Name: "Stop", Description: "停止服务", Required: true},
|
||||
},
|
||||
},
|
||||
"multipart_transport": {
|
||||
Name: "MultipartTransport",
|
||||
Category: CategoryTransport,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "complete",
|
||||
Description: "Multipart文件传输",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "SendTaskWithFiles", Description: "发送带文件的任务", InputType: "endpoint, message, files", OutputType: "error", Required: true},
|
||||
{Name: "ParseMultipartRequest", Description: "解析multipart请求", InputType: "HTTPRequest", OutputType: "Message, Files", Required: true},
|
||||
},
|
||||
},
|
||||
"hybrid_transport": {
|
||||
Name: "HybridTransport",
|
||||
Category: CategoryTransport,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "complete",
|
||||
Description: "混合传输(回调+轮询)",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "SendTaskWithHybridFallback", Description: "发送任务并自动切换", InputType: "endpoint, message, callback", OutputType: "error", Required: true},
|
||||
{Name: "NotifyCallbackReceived", Description: "通知回调已收到", InputType: "taskID", Required: true},
|
||||
},
|
||||
},
|
||||
"streaming_transport": {
|
||||
Name: "StreamingTransport",
|
||||
Category: CategoryTransport,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "advanced",
|
||||
Description: "流式传输",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "WebSocket /synapse/stream", Description: "WebSocket流式端点", Required: true},
|
||||
{Name: "GET /synapse/stream (SSE)", Description: "SSE流式端点", Required: false},
|
||||
},
|
||||
},
|
||||
|
||||
// === Security Interfaces ===
|
||||
"authenticator": {
|
||||
Name: "Authenticator",
|
||||
Category: CategorySecurity,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "basic",
|
||||
Description: "认证和签名",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "Authenticate", Description: "验证消息签名", InputType: "message, signature", OutputType: "bool", Required: true},
|
||||
{Name: "Sign", Description: "签名消息", InputType: "message", OutputType: "string", Required: true},
|
||||
},
|
||||
},
|
||||
"idempotency_handler": {
|
||||
Name: "IdempotencyHandler",
|
||||
Category: CategorySecurity,
|
||||
Version: "1.0.0",
|
||||
ComplianceLevel: "basic",
|
||||
Description: "幂等性处理",
|
||||
Status: "implemented",
|
||||
Methods: []InterfaceMethod{
|
||||
{Name: "CheckIdempotency", Description: "检查消息幂等性", InputType: "messageID", OutputType: "bool", Required: true},
|
||||
{Name: "MarkProcessed", Description: "标记消息已处理", InputType: "messageID", Required: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Extended AgentCapabilities
|
||||
// =============================================================================
|
||||
|
||||
// ExtendedAgentCapabilities 扩展的Agent能力描述
|
||||
type ExtendedAgentCapabilities struct {
|
||||
// 基础能力
|
||||
types.AgentCapabilities
|
||||
|
||||
// 实现的接口列表
|
||||
ImplementedInterfaces []ImplementedInterface `json:"implemented_interfaces"`
|
||||
|
||||
// 合规级别 summary
|
||||
ComplianceSummary ComplianceSummary `json:"compliance_summary"`
|
||||
|
||||
// 自定义能力
|
||||
CustomCapabilities map[string]any `json:"custom_capabilities,omitempty"`
|
||||
|
||||
// 依赖的其他Agent
|
||||
Dependencies []AgentDependency `json:"dependencies,omitempty"`
|
||||
|
||||
// 服务健康信息
|
||||
HealthInfo HealthInfo `json:"health_info"`
|
||||
}
|
||||
|
||||
// ComplianceSummary 合规级别摘要
|
||||
type ComplianceSummary struct {
|
||||
BasicImplemented int `json:"basic_implemented"`
|
||||
BasicRequired int `json:"basic_required"`
|
||||
CompleteImplemented int `json:"complete_implemented"`
|
||||
CompleteRequired int `json:"complete_required"`
|
||||
AdvancedImplemented int `json:"advanced_implemented"`
|
||||
|
||||
OverallLevel string `json:"overall_level"` // "basic", "complete", "advanced"
|
||||
}
|
||||
|
||||
// AgentDependency Agent依赖
|
||||
type AgentDependency struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
SystemID string `json:"system_id"`
|
||||
Required bool `json:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// HealthInfo 健康信息
|
||||
type HealthInfo struct {
|
||||
Status string `json:"status"` // "healthy", "degraded", "unhealthy"
|
||||
LastCheckAt string `json:"last_check_at"`
|
||||
Uptime int64 `json:"uptime_seconds"`
|
||||
Version string `json:"version"`
|
||||
ActiveTasks int `json:"active_tasks"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Handler for Capabilities Endpoint
|
||||
// =============================================================================
|
||||
|
||||
// CapabilitiesHandler 处理 GET /synapse/capabilities 请求
|
||||
type CapabilitiesHandler struct {
|
||||
provider CapabilitiesProvider
|
||||
}
|
||||
|
||||
// NewCapabilitiesHandler 创建能力查询处理器
|
||||
func NewCapabilitiesHandler(provider CapabilitiesProvider) *CapabilitiesHandler {
|
||||
return &CapabilitiesHandler{provider: provider}
|
||||
}
|
||||
|
||||
// ServeHTTP 处理HTTP请求
|
||||
func (h *CapabilitiesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// 获取能力
|
||||
caps, err := h.provider.GetCapabilities(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取实现的接口
|
||||
interfaces := h.provider.GetSupportedInterfaces()
|
||||
|
||||
// 构建扩展能力响应
|
||||
extended := &ExtendedAgentCapabilities{
|
||||
AgentCapabilities: *caps,
|
||||
ImplementedInterfaces: interfaces,
|
||||
ComplianceSummary: CalculateComplianceSummary(interfaces),
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("X-Synapse-Version", "1.0.0")
|
||||
|
||||
resp := types.NewCapabilitiesResponse(&extended.AgentCapabilities)
|
||||
respJSON, _ := json.Marshal(resp)
|
||||
w.Write(respJSON)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
// CalculateComplianceSummary 计算合规摘要
|
||||
func CalculateComplianceSummary(interfaces []ImplementedInterface) ComplianceSummary {
|
||||
summary := ComplianceSummary{}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
switch iface.ComplianceLevel {
|
||||
case "basic":
|
||||
summary.BasicImplemented++
|
||||
case "complete":
|
||||
summary.CompleteImplemented++
|
||||
case "advanced":
|
||||
summary.AdvancedImplemented++
|
||||
}
|
||||
}
|
||||
|
||||
// 计算要求的数量
|
||||
for _, std := range StandardInterfaces {
|
||||
switch std.ComplianceLevel {
|
||||
case "basic":
|
||||
summary.BasicRequired++
|
||||
case "complete":
|
||||
summary.CompleteRequired++
|
||||
}
|
||||
}
|
||||
|
||||
// 确定整体级别
|
||||
if summary.BasicImplemented >= summary.BasicRequired {
|
||||
if summary.CompleteImplemented >= summary.CompleteRequired {
|
||||
if summary.AdvancedImplemented > 0 {
|
||||
summary.OverallLevel = "advanced"
|
||||
} else {
|
||||
summary.OverallLevel = "complete"
|
||||
}
|
||||
} else {
|
||||
summary.OverallLevel = "basic"
|
||||
}
|
||||
} else {
|
||||
summary.OverallLevel = "none"
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// GetStandardInterface 获取标准接口定义
|
||||
func GetStandardInterface(name string) *ImplementedInterface {
|
||||
if iface, ok := StandardInterfaces[name]; ok {
|
||||
return &iface
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllStandardInterfaces 获取所有标准接口
|
||||
func GetAllStandardInterfaces() map[string]ImplementedInterface {
|
||||
return StandardInterfaces
|
||||
}
|
||||
|
||||
// GetInterfacesByCategory 获取指定分类的接口
|
||||
func GetInterfacesByCategory(category InterfaceCategory) []ImplementedInterface {
|
||||
var result []ImplementedInterface
|
||||
for _, iface := range StandardInterfaces {
|
||||
if iface.Category == category {
|
||||
result = append(result, iface)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetInterfacesByComplianceLevel 获取指定合规级别的接口
|
||||
func GetInterfacesByComplianceLevel(level string) []ImplementedInterface {
|
||||
var result []ImplementedInterface
|
||||
for _, iface := range StandardInterfaces {
|
||||
if iface.ComplianceLevel == level {
|
||||
result = append(result, iface)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CheckInterfaceCompliance 检查接口合规性
|
||||
func CheckInterfaceCompliance(implemented []ImplementedInterface) map[string][]string {
|
||||
result := map[string][]string{
|
||||
"missing_basic": []string{},
|
||||
"missing_complete": []string{},
|
||||
"implemented": []string{},
|
||||
}
|
||||
|
||||
implementedSet := make(map[string]bool)
|
||||
for _, iface := range implemented {
|
||||
implementedSet[iface.Name] = true
|
||||
}
|
||||
|
||||
for name, std := range StandardInterfaces {
|
||||
if implementedSet[std.Name] {
|
||||
result["implemented"] = append(result["implemented"], name)
|
||||
} else {
|
||||
switch std.ComplianceLevel {
|
||||
case "basic":
|
||||
result["missing_basic"] = append(result["missing_basic"], name)
|
||||
case "complete":
|
||||
result["missing_complete"] = append(result["missing_complete"], name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
328
docs/01-synapse-raw-protocol.md
Normal file
328
docs/01-synapse-raw-protocol.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# 灵隙原始协议(Synapse Raw Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:纲领性文档
|
||||
|
||||
---
|
||||
|
||||
## 1. 协议愿景
|
||||
|
||||
灵隙(Synapse,原 Agent-to-Agent)协议旨在定义一套通用的、可扩展的智能体间通信标准,使不同系统、不同厂商、不同能力的智能体能够在统一的协议框架下进行协作、交易与能力组合。
|
||||
|
||||
本协议是灵隙协议族(Synapse Protocol Suite)的原始纲领,定义核心设计理念、架构原则与术语体系,为所有衍生协议提供顶层指导。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心设计理念
|
||||
|
||||
### 2.1 去中心化对等(Peer-to-Peer)
|
||||
- 所有 Agent 在协议层面地位平等,无中心化控制节点
|
||||
- 每个 Agent 既是服务提供者,也是服务消费者
|
||||
- Agent 间直接通信,不依赖中间 broker(传输层可选择不同实现)
|
||||
|
||||
### 2.2 能力驱动(Capability-Driven)
|
||||
- 通信以能力为核心,而非以身份为核心
|
||||
- Agent 声明自身能力,按需发现与调用
|
||||
- 能力可组合、可分解、可升级
|
||||
|
||||
### 2.3 任务导向(Task-Oriented)
|
||||
- 所有交互围绕"任务"展开
|
||||
- 任务有明确的生命周期、状态机与超时机制
|
||||
- 任务支持拆分、合并、委派与协作
|
||||
|
||||
### 2.4 人类可介入(Human-in-the-Loop)
|
||||
- 协议原生支持人类介入决策
|
||||
- Agent 可主动请求人类判断
|
||||
- 人类决策优先于 Agent 自动决策
|
||||
- 支持多级升级(escalation)机制
|
||||
|
||||
### 2.5 群组协作(Group Collaboration)
|
||||
- 支持多 Agent 组成临时或永久群组
|
||||
- 群组内有角色分工与权限控制
|
||||
- 支持任务拆分-执行-合并的 MapReduce 模式
|
||||
- 支持投票、共识等群体决策机制
|
||||
|
||||
### 2.6 传输无关(Transport Agnostic)
|
||||
- 协议语义与底层传输分离
|
||||
- 支持 HTTP/2、gRPC、WebSocket、消息队列等多种传输
|
||||
- 支持回调(Callback)、轮询(Polling)、同步(Sync)、混合(Hybrid)等多种交互模式
|
||||
|
||||
### 2.7 安全可信(Secure & Trustworthy)
|
||||
- 消息级签名与验证
|
||||
- Agent 身份认证与信任等级
|
||||
- 支持加密传输
|
||||
- 可审计、可追溯
|
||||
|
||||
---
|
||||
|
||||
## 3. 协议分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 应用层 (Application) │
|
||||
│ 业务逻辑、工作流、领域特定逻辑 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 协议层 (Protocol) │
|
||||
├─────────────┬───────────────────────────┤
|
||||
│ A2A 协议 │ A2N 组群协议 │ ← 本协议族
|
||||
│ (P2P) │ (Group) │
|
||||
├─────────────┴───────────────────────────┤
|
||||
│ 传输层 (Transport) │
|
||||
│ HTTP / gRPC / WebSocket / MQ ... │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 网络层 (Network) │
|
||||
│ TCP / UDP / QUIC ... │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.1 协议族构成
|
||||
|
||||
| 协议名称 | 缩写 | 描述 | 适用场景 |
|
||||
|---------|------|------|---------|
|
||||
| 灵隙原始协议(Synapse Raw Protocol) | A2A-Raw | 顶层纲领,定义理念、术语、原则 | 指导所有协议设计 |
|
||||
| A2A 协议 | A2A-Base | 点对点任务协作协议 | 两个 Agent 间的任务交互 |
|
||||
| A2N 组群协议 | A2N-Group | 多 Agent 群组协作协议 | 多个 Agent 协同完成任务 |
|
||||
| P2A 调用协议 | P2A-Caller | 非 Agent 调用者接入协议 | 普通服务/前端调用 Agent 能力 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心概念与术语
|
||||
|
||||
### 4.1 Agent(智能体)
|
||||
能够自主感知环境、做出决策并执行动作的计算实体。
|
||||
|
||||
**属性:**
|
||||
- `agent_id`:Agent 唯一标识
|
||||
- `system_id`:所属系统标识
|
||||
- `endpoint`:通信端点地址
|
||||
- `capabilities`:能力清单
|
||||
- `trust_level`:信任等级
|
||||
|
||||
### 4.2 Task(任务)
|
||||
Agent 间协作的基本单位,具有明确的输入、输出与生命周期。
|
||||
|
||||
**属性:**
|
||||
- `task_id`:任务唯一标识
|
||||
- `task_type`:任务类型
|
||||
- `payload`:任务载荷
|
||||
- `status`:任务状态
|
||||
- `priority`:优先级
|
||||
- `deadline`:截止时间
|
||||
|
||||
### 4.3 Message(消息)
|
||||
Agent 间通信的基本单位。
|
||||
|
||||
**属性:**
|
||||
- `message_id`:消息唯一标识
|
||||
- `type`:消息类型
|
||||
- `sender`:发送方
|
||||
- `receiver`:接收方(P2P)
|
||||
- `group_id`:群组标识(群组)
|
||||
- `payload`:消息载荷
|
||||
- `correlation_id`:关联标识(用于请求-响应关联)
|
||||
- `timestamp`:时间戳
|
||||
- `signature`:签名
|
||||
|
||||
### 4.4 Capability(能力)
|
||||
Agent 能够执行的任务类型的声明。
|
||||
|
||||
**属性:**
|
||||
- `type`:能力对应的任务类型
|
||||
- `name`:能力名称
|
||||
- `description`:能力描述
|
||||
- `input_schema`:输入参数 schema
|
||||
- `output_schema`:输出结果 schema
|
||||
- `estimated_time`:预估耗时
|
||||
|
||||
### 4.5 Group(群组)
|
||||
由多个 Agent 组成的协作单元。
|
||||
|
||||
**属性:**
|
||||
- `group_id`:群组唯一标识
|
||||
- `name`:群组名称
|
||||
- `topology`:拓扑结构
|
||||
- `members`:成员列表
|
||||
- `roles`:角色定义
|
||||
- `status`:群组状态
|
||||
|
||||
### 4.6 Human-in-the-Loop(人在回路)
|
||||
在 Agent 协作过程中,人类可介入进行决策的机制。
|
||||
|
||||
**关键概念:**
|
||||
- `escalation`:升级(将问题提交给人类)
|
||||
- `decision`:决策(人类做出的判断)
|
||||
- `resume`:恢复(人类决策后继续执行)
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计原则
|
||||
|
||||
### 5.1 简约性(Simplicity)
|
||||
- 核心机制保持简单
|
||||
- 复杂功能通过组合基础机制实现
|
||||
- 可选功能通过能力声明来协商
|
||||
|
||||
### 5.2 可扩展性(Extensibility)
|
||||
- 消息类型可扩展
|
||||
- 能力类型可扩展
|
||||
- 传输方式可扩展
|
||||
- 通过版本号与能力协商保证向后兼容
|
||||
|
||||
### 5.3 容错性(Fault Tolerance)
|
||||
- 网络不可靠是常态
|
||||
- 消息可能丢失、重复、延迟
|
||||
- Agent 可能故障、离线
|
||||
- 协议必须有重试、超时、降级机制
|
||||
|
||||
### 5.4 可观测性(Observability)
|
||||
- 所有交互可追踪
|
||||
- 任务状态可查询
|
||||
- 支持日志、指标、链路追踪
|
||||
|
||||
### 5.5 互操作性(Interoperability)
|
||||
- 不同语言、不同框架的 Agent 可互操作
|
||||
- 协议规范优先于具体实现
|
||||
- 提供标准测试用例验证兼容性
|
||||
|
||||
---
|
||||
|
||||
## 6. 消息模型总则
|
||||
|
||||
### 6.1 统一消息信封
|
||||
所有协议共享统一的消息信封结构,具体载荷由消息类型决定。
|
||||
|
||||
```
|
||||
UnifiedSynapseMessage
|
||||
├── id // 消息ID
|
||||
├── version // 协议版本
|
||||
├── type // 消息类型
|
||||
├── sender // 发送方 AgentEndpoint
|
||||
├── receiver? // 接收方(P2P 时)
|
||||
├── group_id? // 群组ID(群组时)
|
||||
├── correlation_id // 关联ID
|
||||
├── reply_to? // 回复的消息ID
|
||||
├── payload // 消息载荷(JSON)
|
||||
├── priority? // 优先级
|
||||
├── signature? // 签名
|
||||
├── timestamp // 发送时间
|
||||
└── expires_at? // 过期时间
|
||||
```
|
||||
|
||||
### 6.2 消息命名规范
|
||||
- 使用 `领域:动作` 格式
|
||||
- 全小写,用下划线分隔
|
||||
- 示例:`p2p:task_request`, `group:create`, `hil:decision`
|
||||
|
||||
### 6.3 请求-响应模式
|
||||
- 请求消息必须携带唯一 `id`
|
||||
- 响应消息通过 `reply_to` 关联请求
|
||||
- 支持 ACK/NACK 机制
|
||||
- 超时未收到响应应重试或报错
|
||||
|
||||
---
|
||||
|
||||
## 7. 任务生命周期总则
|
||||
|
||||
### 7.1 状态机
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ pending │ 待确认
|
||||
└────┬─────┘
|
||||
│ 确认/拒绝
|
||||
┌─────┴──────┐
|
||||
▼ ▼
|
||||
┌─────────┐ ┌──────────┐
|
||||
│confirmed│ │ rejected │ 已拒绝(终态)
|
||||
└────┬────┘ └──────────┘
|
||||
│ 开始执行
|
||||
▼
|
||||
┌──────────┐
|
||||
│executing │ 执行中
|
||||
└────┬─────┘
|
||||
│ 信息缺失
|
||||
┌────▼─────┐
|
||||
│info_needed│ 需要信息
|
||||
└────┬─────┘
|
||||
│ 信息补充
|
||||
▼
|
||||
回到 executing
|
||||
│
|
||||
├──────────────┐
|
||||
│ 需人类决策 │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌───────────┐
|
||||
│need_human│ │ 各种终态 │
|
||||
└────┬─────┘ └───────────┘
|
||||
│ 人类决策
|
||||
▼
|
||||
回到 executing
|
||||
│
|
||||
┌────┴────┬─────────┐
|
||||
▼ ▼ ▼
|
||||
completed failed cancelled
|
||||
(完成) (失败) (取消)
|
||||
```
|
||||
|
||||
### 7.2 终态
|
||||
- `completed`:成功完成
|
||||
- `failed`:执行失败
|
||||
- `cancelled`:被取消
|
||||
- `rejected`:被拒绝
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全模型总则
|
||||
|
||||
### 8.1 身份认证
|
||||
- 每个 Agent 有唯一身份
|
||||
- 支持 HMAC、JWT、OAuth2 等多种认证方式
|
||||
- 信任等级机制
|
||||
|
||||
### 8.2 消息完整性
|
||||
- 消息签名机制
|
||||
- 防止篡改与伪造
|
||||
|
||||
### 8.3 机密性
|
||||
- 支持端到端加密
|
||||
- 敏感载荷加密传输
|
||||
|
||||
### 8.4 访问控制
|
||||
- 基于信任等级的权限控制
|
||||
- 群组内基于角色的权限控制
|
||||
|
||||
---
|
||||
|
||||
## 9. 非目标
|
||||
|
||||
以下内容**不在**本原始协议的讨论范围内:
|
||||
|
||||
1. **具体传输协议的实现细节** - 由各传输层实现定义
|
||||
2. **特定领域的任务类型** - 由应用层或领域扩展定义
|
||||
3. **Agent 内部实现架构** - 仅定义对外接口与行为
|
||||
4. **具体编程语言绑定** - 语言无关,可多语言实现
|
||||
5. **UI/UX 设计** - 人机界面不在协议范围内
|
||||
|
||||
---
|
||||
|
||||
## 10. 参考实现
|
||||
|
||||
本协议族的参考实现位于:`dp-headless/synapse/`
|
||||
|
||||
| 模块 | 对应协议 | 文件路径 |
|
||||
|------|---------|---------|
|
||||
| types 包 | 公共类型定义 | `types/` |
|
||||
| p2p 包 | A2A 协议 | `p2p/` |
|
||||
| group 包 | A2N 组群协议 | `group/` |
|
||||
| externalapi 包 | P2A 调用协议 | `externalapi/` |
|
||||
| sdk 包 | P2A 客户端 SDK | `sdk/` |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:协议演进
|
||||
|
||||
本原始协议作为顶层指导,保持相对稳定。各具体协议可独立版本化演进,但需遵循本协议定义的核心原则。
|
||||
|
||||
**版本历史:**
|
||||
- v1.0.0:初始版本,定义核心概念与设计原则
|
||||
465
docs/02-p2p-protocol.md
Normal file
465
docs/02-p2p-protocol.md
Normal file
@@ -0,0 +1,465 @@
|
||||
# A2A 协议(Agent-to-Agent Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[灵隙原始协议(Synapse Raw Protocol) v1.0.0](./01-a2a-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
A2A 协议定义了两个 Agent 之间进行点对点(P2P)任务协作的完整规范,包括消息类型、交互流程、状态机、接口定义与错误处理。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 单个任务请求与响应
|
||||
- 任务状态实时同步
|
||||
- 任务取消
|
||||
- 信息补全交互
|
||||
- 人类介入决策
|
||||
|
||||
### 1.2 非适用场景
|
||||
- 多 Agent 群组协作(参见 A2N 组群协议)
|
||||
- 非 Agent 调用方接入(参见 P2A 调用协议)
|
||||
|
||||
---
|
||||
|
||||
## 2. 消息类型
|
||||
|
||||
### 2.1 消息类型清单
|
||||
|
||||
所有消息类型前缀:`p2p:`
|
||||
|
||||
| 消息类型 | 方向 | 说明 |
|
||||
|---------|------|------|
|
||||
| `p2p:task_request` | 请求方 → 执行方 | 提交任务请求 |
|
||||
| `p2p:task_confirm` | 执行方 → 请求方 | 确认接受任务 |
|
||||
| `p2p:task_reject` | 执行方 → 请求方 | 拒绝任务 |
|
||||
| `p2p:task_need_info` | 执行方 → 请求方 | 请求补充信息 |
|
||||
| `p2p:task_info_provided` | 请求方 → 执行方 | 提供补充信息 |
|
||||
| `p2p:task_status_update` | 执行方 → 请求方 | 任务进度更新 |
|
||||
| `p2p:task_complete` | 执行方 → 请求方 | 任务完成 |
|
||||
| `p2p:task_failed` | 执行方 → 请求方 | 任务失败 |
|
||||
| `p2p:task_cancel` | 请求方 → 执行方 | 取消任务 |
|
||||
| `p2p:task_ack` | 双向 | 任务级确认 |
|
||||
| `p2p:ack` | 双向 | 通用确认 |
|
||||
| `p2p:nack` | 双向 | 通用否定确认(错误) |
|
||||
| `p2p:heartbeat` | 双向 | 心跳保活 |
|
||||
| `p2p:capability_query` | 双向 | 查询对方能力 |
|
||||
| `p2p:capability_response` | 双向 | 返回能力清单 |
|
||||
| `p2p:task_need_human` | 执行方 → 请求方 | 请求人类介入 |
|
||||
| `p2p:human_decision` | 请求方 → 执行方 | 人类决策结果 |
|
||||
|
||||
### 2.2 消息类型完整定义
|
||||
|
||||
参见代码:[types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务状态机
|
||||
|
||||
### 3.1 状态定义
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `idle` | 空闲(初始状态) |
|
||||
| `pending` | 待确认(已发送请求,等待确认) |
|
||||
| `confirmed` | 已确认(对方接受任务) |
|
||||
| `executing` | 执行中 |
|
||||
| `info_needed` | 需要信息 |
|
||||
| `info_provided` | 信息已提供 |
|
||||
| `need_human` | 需要人类介入 |
|
||||
| `human_deciding` | 人类决策中 |
|
||||
| `human_approved` | 人类已批准 |
|
||||
| `human_rejected` | 人类已拒绝 |
|
||||
| `completed` | 已完成(终态) |
|
||||
| `failed` | 已失败(终态) |
|
||||
| `cancelled` | 已取消(终态) |
|
||||
| `rejected` | 已拒绝(终态) |
|
||||
|
||||
### 3.2 状态流转图
|
||||
|
||||
```
|
||||
请求方视角:
|
||||
|
||||
idle → pending → confirmed → executing → completed
|
||||
↓ ↓ → failed
|
||||
rejected info_needed → cancelled
|
||||
↑↓
|
||||
info_provided
|
||||
↓
|
||||
need_human
|
||||
↑↓
|
||||
human_decision
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心交互流程
|
||||
|
||||
### 4.1 正常任务提交流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->| 1. 提交任务
|
||||
| |
|
||||
|<------- task_confirm / task_reject -| 2. 确认/拒绝
|
||||
| |
|
||||
|<------- task_status_update --------| 3. 进度更新(多次)
|
||||
| |
|
||||
|<------- task_complete -------------| 4. 完成通知
|
||||
| |
|
||||
|-------- task_ack ----------------->| 5. 确认收到
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.2 信息补全流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|<------- task_need_info ------------| 发现信息缺失
|
||||
| |
|
||||
|-------- task_info_provided ------->| 补充信息
|
||||
| |
|
||||
|<------- task_status_update --------| 继续执行
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.3 人类介入流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|<------- task_need_human -----------| 需要人类决策
|
||||
| |
|
||||
| [人类做出决策] |
|
||||
| |
|
||||
|-------- human_decision ----------->| 决策结果
|
||||
| |
|
||||
|<------- task_status_update --------| 继续执行
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.4 任务取消流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|-------- task_cancel -------------->| 取消任务
|
||||
| |
|
||||
|<------- task_ack ------------------| 确认取消
|
||||
| |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口规范
|
||||
|
||||
### 5.1 P2P 协议处理器接口
|
||||
|
||||
**接口定义:**
|
||||
|
||||
```go
|
||||
type P2PProtocol interface {
|
||||
// 处理收到的消息
|
||||
HandleMessage(ctx context.Context, msg *UnifiedSynapseMessage) (*UnifiedSynapseMessage, error)
|
||||
|
||||
// 设置任务处理器
|
||||
SetTaskHandler(h TaskHandler)
|
||||
|
||||
// 设置回调发送器
|
||||
SetCallbackSender(s CallbackSender)
|
||||
|
||||
// 获取 Agent 端点
|
||||
AgentEndpoint() AgentEndpoint
|
||||
|
||||
// 查询任务状态
|
||||
GetTaskStatus(taskID string) (P2PTaskStatus, bool)
|
||||
|
||||
// 列出所有任务
|
||||
ListTasks() map[string]P2PTaskStatus
|
||||
}
|
||||
```
|
||||
|
||||
**参考实现:** [p2p/protocol.go](../p2p/protocol.go)
|
||||
|
||||
### 5.2 任务处理器接口
|
||||
|
||||
```go
|
||||
type TaskHandler interface {
|
||||
// 执行任务并返回结果
|
||||
ExecuteTask(ctx context.Context, taskType string, payload map[string]interface{}) (any, error)
|
||||
|
||||
// 验证任务是否可执行,返回缺失字段列表和是否可处理
|
||||
ValidateTask(taskType string, payload map[string]interface{}) ([]MissingField, bool)
|
||||
|
||||
// 获取能力列表
|
||||
GetCapabilities() []string
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `ExecuteTask`:实际执行业务逻辑的入口
|
||||
- `ValidateTask`:在任务确认前调用,用于判断是否接受任务或需要补充信息
|
||||
- `GetCapabilities`:返回 Agent 支持的任务类型列表
|
||||
|
||||
### 5.3 回调发送器接口
|
||||
|
||||
```go
|
||||
type CallbackSender interface {
|
||||
// 向远程 Agent 发送消息
|
||||
SendCallback(ctx context.Context, endpoint string, msg *UnifiedSynapseMessage) error
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 传输层实现此接口
|
||||
- 支持多种传输方式(HTTP、gRPC、WebSocket 等)
|
||||
|
||||
### 5.4 配置项
|
||||
|
||||
```go
|
||||
type P2PConfig struct {
|
||||
MaxRetries int // 最大重试次数(默认 5)
|
||||
RetryBaseDelay time.Duration // 重试基础延迟(默认 2s,指数退避)
|
||||
TaskTimeout time.Duration // 任务执行超时(默认 5 分钟)
|
||||
HeartbeatInterval time.Duration // 心跳间隔(默认 30 秒)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 载荷格式
|
||||
|
||||
### 6.1 TaskRequestPayload(任务请求)
|
||||
|
||||
```json
|
||||
{
|
||||
"task": {
|
||||
"id": "task-xxx",
|
||||
"type": "text_generation",
|
||||
"priority": "normal",
|
||||
"deadline": "2026-01-01T00:00:00Z",
|
||||
"payload": {
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"session_id": "sess-xxx",
|
||||
"parent_task_id": "parent-xxx",
|
||||
"correlation_id": "corr-xxx",
|
||||
"metadata": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 TaskConfirmPayload(任务确认)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"confirmed": true,
|
||||
"estimated_duration": 120,
|
||||
"expected_completion": "2026-01-01T00:02:00Z",
|
||||
"capabilities_used": ["text_generation"],
|
||||
"constraints": {
|
||||
"max_retries": 3,
|
||||
"fallback_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 TaskRejectPayload(任务拒绝)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"reason": {
|
||||
"code": "capability_lacking",
|
||||
"message": "不支持该任务类型",
|
||||
"details": null
|
||||
},
|
||||
"alternatives": {
|
||||
"suggested_agent_id": "other-agent",
|
||||
"retry_after": 300,
|
||||
"partial_support": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 TaskStatusUpdatePayload(状态更新)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"status": "executing",
|
||||
"progress": 50,
|
||||
"message": "正在生成内容",
|
||||
"intermediate_result": null
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 TaskCompletePayload(任务完成)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"result": {
|
||||
"output_type": "text",
|
||||
"output": "生成的文本内容...",
|
||||
"metadata": {
|
||||
"tokens": 1024,
|
||||
"model": "gpt-4"
|
||||
}
|
||||
},
|
||||
"summary": "任务已完成"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 TaskFailedPayload(任务失败)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"error": {
|
||||
"code": "execution_error",
|
||||
"message": "模型调用超时",
|
||||
"recoverable": true,
|
||||
"details": null
|
||||
},
|
||||
"partial_result": null
|
||||
}
|
||||
```
|
||||
|
||||
### 6.7 TaskNeedInfoPayload(需要信息)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"missing_fields": [
|
||||
{
|
||||
"field": "style",
|
||||
"reason": "未指定生成风格",
|
||||
"suggested_value": "古典",
|
||||
"alternatives": ["现代", "浪漫"]
|
||||
}
|
||||
],
|
||||
"question": "请选择生成风格",
|
||||
"required_by": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 TaskNeedHumanPayload(需要人类)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"reason": {
|
||||
"code": "ethical_decision",
|
||||
"message": "涉及伦理决策需要人工审核"
|
||||
},
|
||||
"context": {
|
||||
"task_summary": "生成一张人物肖像",
|
||||
"current_progress": 30,
|
||||
"blocked_reason": "需要确认肖像使用授权"
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"field": "authorized",
|
||||
"value": true,
|
||||
"pros_cons": "继续生成,假设已授权",
|
||||
"confidence": 70
|
||||
}
|
||||
],
|
||||
"escalation_level": 1,
|
||||
"deadline": "2026-01-01T00:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.9 HumanDecisionPayload(人类决策)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"decision_id": "dec-xxx",
|
||||
"reply_to": "msg-xxx",
|
||||
"decided_by": {
|
||||
"user_id": "user-123",
|
||||
"user_name": "张三",
|
||||
"role": "reviewer"
|
||||
},
|
||||
"decision": {
|
||||
"action": "approve",
|
||||
"approved_fields": {
|
||||
"authorized": true
|
||||
},
|
||||
"rejection_reason": ""
|
||||
},
|
||||
"notes": "已确认授权",
|
||||
"timestamp": "2026-01-01T00:03:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**完整载荷定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误码
|
||||
|
||||
| 错误码 | 说明 | HTTP 状态码(如适用) |
|
||||
|--------|------|---------------------|
|
||||
| `message_format_invalid` | 消息格式无效 | 400 |
|
||||
| `capability_lacking` | 能力不足 | 403 |
|
||||
| `agent_not_ready` | Agent 未就绪 | 503 |
|
||||
| `task_not_found` | 任务不存在 | 404 |
|
||||
| `task_already_completed` | 任务已完成 | 409 |
|
||||
| `execution_timeout` | 执行超时 | 504 |
|
||||
| `execution_failed` | 执行失败 | 500 |
|
||||
| `permission_denied` | 权限不足 | 403 |
|
||||
| `invalid_params` | 参数无效 | 400 |
|
||||
| `rate_limited` | 限流 | 429 |
|
||||
|
||||
**完整错误定义:** [types/errors.go](../types/errors.go)
|
||||
|
||||
---
|
||||
|
||||
## 8. 传输要求
|
||||
|
||||
### 8.1 HTTP 传输
|
||||
- Content-Type: `application/json`
|
||||
- 请求方法:`POST`
|
||||
- 端点路径:由 Agent 自行定义,建议 `/synapse/callback`
|
||||
- 超时:建议 30 秒
|
||||
|
||||
### 8.2 签名验证(可选)
|
||||
- 使用 HMAC-SHA256 签名
|
||||
- 签名放在 Header:`X-Synapse-Signature`
|
||||
- 或放在消息体:`signature` 字段
|
||||
|
||||
---
|
||||
|
||||
## 9. 参考实现
|
||||
|
||||
本协议的参考实现位于 `p2p/` 目录:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [p2p/protocol.go](../p2p/protocol.go) | P2P 协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 数据类型定义 |
|
||||
| [types/errors.go](../types/errors.go) | 错误定义 |
|
||||
| [transport/transport.go](../transport/transport.go) | HTTP 传输实现 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:能力声明格式
|
||||
|
||||
参见 [灵隙原始协议(Synapse Raw Protocol) - Capability](./01-a2a-raw-protocol.md#44-capability能力)
|
||||
545
docs/03-group-protocol.md
Normal file
545
docs/03-group-protocol.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# A2N 协议(Agent-to-N Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[灵隙原始协议(Synapse Raw Protocol) v1.0.0](./01-a2a-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
A2N(Agent-to-N)组群协议定义了多个 Agent 组成群组进行协同工作的完整规范,包括群组管理、任务拆分与合并、群体决策、角色与权限等。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 复杂任务的拆分与并行执行(MapReduce 模式)
|
||||
- 多 Agent 协同完成单一目标
|
||||
- 群体投票与共识决策
|
||||
- 临时或永久的 Agent 工作组
|
||||
- 分级升级(escalation)机制
|
||||
|
||||
### 1.2 依赖
|
||||
- 依赖 A2A 协议的点对点通信能力
|
||||
- 群组内成员间的单播通信使用 A2A 协议
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念
|
||||
|
||||
### 2.1 Group(群组)
|
||||
由多个 Agent 组成的协作单元,具有明确的拓扑结构、角色分工与协作规则。
|
||||
|
||||
### 2.2 Topology(拓扑结构)
|
||||
|
||||
| 拓扑类型 | 说明 | 适用场景 |
|
||||
|---------|------|---------|
|
||||
| `star` | 星型,中心 coordinator 协调 | 简单任务分发 |
|
||||
| `mesh` | 网状,成员间可直接通信 | 对等协作 |
|
||||
| `chain` | 链式,流水线模式 | 顺序处理 |
|
||||
| `hierarchical` | 层级型,多层管理 | 大型复杂任务 |
|
||||
| `fullmesh` | 全连接网状 | 高可靠协作 |
|
||||
|
||||
### 2.3 Role(角色)
|
||||
|
||||
| 角色 | 说明 | 权限 |
|
||||
|------|------|------|
|
||||
| `coordinator` | 协调者 | 创建/解散群组、任务拆分、发起投票 |
|
||||
| `worker` | 工作者 | 执行子任务、提交结果 |
|
||||
| `reviewer` | 审核者 | 审核结果、发起投票 |
|
||||
| `observer` | 观察者 | 只读,查看进度 |
|
||||
|
||||
### 2.4 GroupStatus(群组状态)
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `creating` | 创建中 |
|
||||
| `active` | 活跃 |
|
||||
| `frozen` | 冻结(不可执行新任务) |
|
||||
| `disbanded` | 已解散(终态) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 消息类型
|
||||
|
||||
### 3.1 消息类型清单
|
||||
|
||||
所有消息类型前缀:`group:` 或 `inter_group:`
|
||||
|
||||
#### 群组管理
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:create` | 创建群组 |
|
||||
| `group:disband` | 解散群组 |
|
||||
| `group:join` | 加入群组 |
|
||||
| `group:leave` | 离开群组 |
|
||||
| `group:invite` | 邀请加入 |
|
||||
| `group:accept_invite` | 接受邀请 |
|
||||
| `group:reject_invite` | 拒绝邀请 |
|
||||
| `group:member_update` | 成员更新通知 |
|
||||
| `group:role_change` | 角色变更 |
|
||||
|
||||
#### 群组通信
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:broadcast` | 广播(所有成员) |
|
||||
| `group:multicast` | 组播(指定成员) |
|
||||
| `group:direct` | 群内单播 |
|
||||
| `group:relay` | 消息中继 |
|
||||
|
||||
#### 任务协作
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:task_split` | 任务拆分 |
|
||||
| `group:task_assign` | 任务分配 |
|
||||
| `group:task_merge` | 结果合并 |
|
||||
| `group:task_progress` | 子任务进度 |
|
||||
|
||||
#### 共识与投票
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:vote` | 发起投票 |
|
||||
| `group:vote_submit` | 提交投票 |
|
||||
| `group:consensus` | 达成共识 |
|
||||
|
||||
#### 人在回路(HiL)
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:consensus_escalate` | 共识僵局升级给人类 |
|
||||
| `group:subtask_escalate` | 子任务阻塞升级给人类 |
|
||||
| `group:human_decision` | 人类决策结果 |
|
||||
| `inter_group:escalate` | 跨群组升级 |
|
||||
|
||||
**完整定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 4. 群组生命周期
|
||||
|
||||
### 4.1 状态机
|
||||
|
||||
```
|
||||
creating → active → frozen → disbanded
|
||||
↑ ↓
|
||||
└─────────┘
|
||||
解冻
|
||||
```
|
||||
|
||||
### 4.2 群组创建流程
|
||||
|
||||
```
|
||||
创建者 被邀请成员
|
||||
| |
|
||||
|------ group:invite -------------->| 发送邀请
|
||||
| |
|
||||
|<----- group:accept_invite --------| 接受邀请
|
||||
| |
|
||||
|------ group:member_update ------->| 通知成员更新
|
||||
| |
|
||||
[群组创建完成,状态变为 active] |
|
||||
```
|
||||
|
||||
### 4.3 群组解散流程
|
||||
|
||||
```
|
||||
coordinator 所有成员
|
||||
| |
|
||||
|------ group:disband ------------->| 通知解散
|
||||
| |
|
||||
[群组状态变为 disbanded] |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务协作流程
|
||||
|
||||
### 5.1 MapReduce 模式(拆分-执行-合并)
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ coordinator │
|
||||
└──────┬───────┘
|
||||
│
|
||||
group:task_split(拆分任务)
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ worker1 │ │ worker2 │ │ worker3 │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
│ 执行子任务 │ 执行子任务 │ 执行子任务
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
子任务结果 子任务结果 子任务结果
|
||||
└────────────────┼────────────────┘
|
||||
│
|
||||
group:task_merge(合并结果)
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ 最终结果 │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### 5.2 详细流程
|
||||
|
||||
1. **任务拆分阶段**
|
||||
- coordinator 接收原始任务
|
||||
- 将任务拆分为多个子任务(subtask)
|
||||
- 为每个子任务指定执行的 worker
|
||||
- 定义合并策略(merge strategy)
|
||||
|
||||
2. **并行执行阶段**
|
||||
- 各 worker 并行执行子任务
|
||||
- 定期上报进度(`group:task_progress`)
|
||||
- 子任务完成后上报结果
|
||||
|
||||
3. **结果合并阶段**
|
||||
- 所有子任务完成后,coordinator 合并结果
|
||||
- 根据合并策略决定最终结果
|
||||
- 输出最终结果
|
||||
|
||||
---
|
||||
|
||||
## 6. 合并策略(Merge Strategy)
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| `all_success` | 全部成功才认为成功,否则失败 |
|
||||
| `majority` | 多数子任务成功即可,取多数结果 |
|
||||
| `any_complete` | 任意一个完成即返回结果 |
|
||||
| `priority` | 按优先级,高优先级子任务结果优先 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 投票与共识
|
||||
|
||||
### 7.1 投票流程
|
||||
|
||||
```
|
||||
发起者 投票者
|
||||
| |
|
||||
|-------- group:vote -------------->| 发起投票
|
||||
| |
|
||||
|<------- group:vote_submit --------| 提交投票
|
||||
| |
|
||||
[计票,判断是否达到阈值] |
|
||||
| |
|
||||
|-------- group:consensus --------->| 公布结果
|
||||
| |
|
||||
```
|
||||
|
||||
### 7.2 投票规则
|
||||
|
||||
- `threshold`:通过阈值(0-1),如 0.5 表示过半
|
||||
- `deadline`:投票截止时间
|
||||
- `voters`:有投票权的成员列表
|
||||
- 超时未投票视为弃权
|
||||
|
||||
### 7.3 共识僵局(Consensus Deadlock)
|
||||
|
||||
当投票无法达成共识(如平票、未达阈值且超时)时,可升级给人类决策。
|
||||
|
||||
---
|
||||
|
||||
## 8. 人在回路(HiL)
|
||||
|
||||
群组协作中有两种升级场景:
|
||||
|
||||
### 8.1 共识僵局升级
|
||||
投票无法达成共识时,升级给人类裁决。
|
||||
|
||||
**消息:** `group:consensus_escalate`
|
||||
|
||||
### 8.2 子任务阻塞升级
|
||||
某个子任务遇到无法自动解决的问题时,升级给人类。
|
||||
|
||||
**消息:** `group:subtask_escalate`
|
||||
|
||||
### 8.3 跨群组升级
|
||||
问题超出当前群组能力时,可升级到上级群组或人类。
|
||||
|
||||
**消息:** `inter_group:escalate`
|
||||
|
||||
---
|
||||
|
||||
## 9. 接口规范
|
||||
|
||||
### 9.1 GroupProtocol 接口
|
||||
|
||||
```go
|
||||
type GroupProtocol interface {
|
||||
// 处理群组消息
|
||||
HandleMessage(ctx context.Context, msg *UnifiedSynapseMessage) (*UnifiedSynapseMessage, error)
|
||||
|
||||
// 设置任务处理器
|
||||
SetTaskHandler(h GroupTaskHandler)
|
||||
|
||||
// 查询群组列表
|
||||
GetGroups() []*AgentGroup
|
||||
|
||||
// 查询特定群组
|
||||
GetGroup(groupID string) (*AgentGroup, bool)
|
||||
}
|
||||
```
|
||||
|
||||
**参考实现:** [group/protocol.go](../group/protocol.go)
|
||||
|
||||
### 9.2 GroupTaskHandler 接口
|
||||
|
||||
```go
|
||||
type GroupTaskHandler interface {
|
||||
// 执行子任务
|
||||
ExecuteSubtask(ctx context.Context, subtaskID string, payload map[string]interface{}) (any, error)
|
||||
|
||||
// 合并多个子任务结果
|
||||
MergeResults(results []SubtaskResult, strategy MergeStrategy) (any, error)
|
||||
|
||||
// 验证任务是否可拆分,返回拆分方案
|
||||
ValidateTaskSplit(taskType string, requirements map[string]interface{}) ([]Subtask, bool)
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `ExecuteSubtask`:执行单个子任务的业务逻辑
|
||||
- `MergeResults`:根据策略合并多个子任务的结果
|
||||
- `ValidateTaskSplit`:验证任务是否可拆分,并给出拆分方案
|
||||
|
||||
### 9.3 配置项
|
||||
|
||||
```go
|
||||
type GroupConfig struct {
|
||||
MaxGroupSize int // 最大群组人数(默认 20)
|
||||
MaxSubtasks int // 最大子任务数(默认 50)
|
||||
ConsensusTimeout time.Duration // 共识超时(默认 5 分钟)
|
||||
TaskTimeout time.Duration // 任务超时(默认 10 分钟)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 载荷格式
|
||||
|
||||
### 10.1 GroupCreatePayload(创建群组)
|
||||
|
||||
```json
|
||||
{
|
||||
"group": {
|
||||
"name": "视频制作组",
|
||||
"topology": "star",
|
||||
"config": {
|
||||
"allow_self_leave": true,
|
||||
"requires_approval": false,
|
||||
"consensus_required": false,
|
||||
"consensus_threshold": 0.5,
|
||||
"broadcast_mode": "all"
|
||||
},
|
||||
"initial_members": [
|
||||
{
|
||||
"agent_id": "worker-1",
|
||||
"system_id": "sys-a",
|
||||
"endpoint": "http://worker1/synapse"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"id": "coordinator",
|
||||
"name": "协调者",
|
||||
"permissions": ["create", "disband", "split", "vote"],
|
||||
"max_tasks": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 GroupTaskSplitPayload(任务拆分)
|
||||
|
||||
```json
|
||||
{
|
||||
"original_task": {
|
||||
"id": "task-xxx",
|
||||
"type": "video_generation",
|
||||
"requirements": {
|
||||
"script": "xxx",
|
||||
"duration": 60
|
||||
},
|
||||
"deadline": "2026-01-01T00:10:00Z"
|
||||
},
|
||||
"subtasks": [
|
||||
{
|
||||
"id": "sub-1",
|
||||
"description": "生成脚本",
|
||||
"assigned_to": "writer-agent",
|
||||
"dependencies": [],
|
||||
"estimated_duration": 120,
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"id": "sub-2",
|
||||
"description": "生成配音",
|
||||
"assigned_to": "voice-agent",
|
||||
"dependencies": ["sub-1"],
|
||||
"estimated_duration": 180,
|
||||
"priority": 2
|
||||
}
|
||||
],
|
||||
"merge_strategy": "all_success"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 GroupTaskMergePayload(结果合并)
|
||||
|
||||
```json
|
||||
{
|
||||
"original_task_id": "task-xxx",
|
||||
"subtask_results": [
|
||||
{
|
||||
"subtask_id": "sub-1",
|
||||
"assigned_to": "writer-agent",
|
||||
"status": "success",
|
||||
"result": { "script": "..." },
|
||||
"completed_at": "2026-01-01T00:02:00Z"
|
||||
}
|
||||
],
|
||||
"merge_strategy_used": "all_success",
|
||||
"merged_result": {
|
||||
"video_url": "http://..."
|
||||
},
|
||||
"consensus_achieved": true
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 GroupVotePayload(投票)
|
||||
|
||||
```json
|
||||
{
|
||||
"vote_id": "vote-xxx",
|
||||
"question": "是否采用方案A?",
|
||||
"options": ["方案A", "方案B", "方案C"],
|
||||
"voters": ["agent-1", "agent-2", "agent-3"],
|
||||
"deadline": "2026-01-01T00:05:00Z",
|
||||
"threshold": 0.66,
|
||||
"status": "open",
|
||||
"votes": []
|
||||
}
|
||||
```
|
||||
|
||||
### 10.5 GroupConsensusEscalatePayload(共识升级)
|
||||
|
||||
```json
|
||||
{
|
||||
"vote_id": "vote-xxx",
|
||||
"escalation_id": "esc-xxx",
|
||||
"question": "是否采用方案A?",
|
||||
"votes": [
|
||||
{
|
||||
"voter_id": "agent-1",
|
||||
"voter_role": "reviewer",
|
||||
"option": "方案A",
|
||||
"reason": "质量更好"
|
||||
}
|
||||
],
|
||||
"consensus_status": {
|
||||
"required_threshold": 0.66,
|
||||
"actual_threshold": 0.33,
|
||||
"leading_option": "方案A",
|
||||
"options_tied": false
|
||||
},
|
||||
"escalate_reason": {
|
||||
"code": "no_consensus",
|
||||
"message": "无法达成共识"
|
||||
},
|
||||
"escalation_level": 2,
|
||||
"deadline": "2026-01-01T00:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**完整载荷定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 11. 数据结构
|
||||
|
||||
### 11.1 AgentGroup(群组)
|
||||
|
||||
```go
|
||||
type AgentGroup struct {
|
||||
ID string
|
||||
Name string
|
||||
Topology GroupTopology
|
||||
CreatedBy string
|
||||
CreatedAt Timestamp
|
||||
Config GroupConfig
|
||||
Members []GroupMember
|
||||
Roles []GroupRole
|
||||
Status GroupStatus
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 GroupMember(群组成员)
|
||||
|
||||
```go
|
||||
type GroupMember struct {
|
||||
AgentID string
|
||||
AgentName string
|
||||
Role string
|
||||
JoinedAt Timestamp
|
||||
Status GroupMemberStatus
|
||||
Capabilities []string
|
||||
LoadFactor float64 // 负载因子 0-1,用于任务分配
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 Subtask(子任务)
|
||||
|
||||
```go
|
||||
type Subtask struct {
|
||||
ID string
|
||||
Description string
|
||||
AssignedTo string
|
||||
Dependencies []string
|
||||
EstimatedDuration int
|
||||
Priority int
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 错误码
|
||||
|
||||
群组协议特有错误码:
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| `group_not_found` | 群组不存在 |
|
||||
| `group_not_active` | 群组不活跃 |
|
||||
| `group_full` | 群组已满 |
|
||||
| `already_member` | 已是成员 |
|
||||
| `not_group_member` | 不是群组成员 |
|
||||
| `member_not_found` | 成员不存在 |
|
||||
| `permission_denied` | 权限不足 |
|
||||
| `invalid_params` | 参数无效 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 参考实现
|
||||
|
||||
本协议的参考实现位于 `group/` 目录:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [group/protocol.go](../group/protocol.go) | 群组协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 群组相关数据类型 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:与 A2A 协议的关系
|
||||
|
||||
- 群组内的点对点通信使用 A2A 协议
|
||||
- 群组协议在 A2A 协议之上提供群组管理、任务拆分合并、共识投票等能力
|
||||
- 群组消息同样使用统一消息信封(`UnifiedSynapseMessage`),通过 `group_id` 字段标识群组上下文
|
||||
503
docs/04-caller-protocol.md
Normal file
503
docs/04-caller-protocol.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# P2A 协议(Person/Program-to-Agent Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[灵隙原始协议(Synapse Raw Protocol) v1.0.0](./01-a2a-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
P2A(Person/Program-to-Agent)调用协议定义了**非 Agent 调用方**(如普通后端服务、前端应用、第三方系统、人类用户)如何调用 Agent 能力的规范。
|
||||
|
||||
本协议将 A2A 协议的复杂性封装起来,提供简洁的 RESTful API,使调用方无需了解 A2A 协议细节即可使用 Agent 能力。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 前端应用直接调用 Agent 能力
|
||||
- 后端业务系统集成 Agent 服务
|
||||
- 第三方开发者通过 API 接入
|
||||
- 无需实现完整 A2A 协议的调用方
|
||||
|
||||
### 1.2 设计原则
|
||||
- **简单易用**:标准 RESTful API,JSON 格式
|
||||
- **两种模式**:回调模式 + 轮询模式,调用方按需选择
|
||||
- **认证简单**:API Key 认证,无需 HMAC 签名
|
||||
- **文件支持**:支持 multipart/form-data 上传文件
|
||||
- **错误友好**:清晰的错误码和错误信息
|
||||
|
||||
### 1.3 与 A2A 协议的关系
|
||||
|
||||
```
|
||||
外部调用方 ──P2A 协议──▶ External API Layer ──A2A 协议──▶ Agent
|
||||
```
|
||||
|
||||
P2A 协议是 A2A 协议的"简化入口",所有请求最终会转换为 灵隙消息发送给 Agent。
|
||||
|
||||
---
|
||||
|
||||
## 2. 认证方式
|
||||
|
||||
### 2.1 API Key 认证
|
||||
- 在 HTTP Header 中传递:`X-API-Key: {api_key}`
|
||||
- 或在 URL Query 中传递:`?api_key={api_key}`
|
||||
- 如果服务端未配置 API Key,则跳过认证
|
||||
|
||||
### 2.2 回调验证(可选)
|
||||
如果调用方提供了 `callback_token`,则服务端在回调时会在 Header 中带上 `X-Callback-Token`,用于调用方验证回调来源。
|
||||
|
||||
---
|
||||
|
||||
## 3. API 端点
|
||||
|
||||
所有端点默认前缀:`/api/v1/synapse`
|
||||
|
||||
### 3.1 端点清单
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/task` | 提交任务(JSON) |
|
||||
| POST | `/task/multipart` | 提交任务(带文件上传) |
|
||||
| GET | `/task/{task_id}` | 获取任务详情 |
|
||||
| GET | `/task/{task_id}/status` | 获取任务状态(简化版) |
|
||||
| GET | `/tasks` | 获取任务列表 |
|
||||
| POST | `/task/{task_id}/cancel` | 取消任务 |
|
||||
| GET | `/capabilities` | 获取能力清单 |
|
||||
| POST | `/callback` | 接收回调(调用方实现) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 任务提交
|
||||
|
||||
### 4.1 提交任务(JSON)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/synapse/task
|
||||
Content-Type: application/json
|
||||
X-API-Key: your-api-key
|
||||
|
||||
{
|
||||
"task_type": "text_generation",
|
||||
"payload": {
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典"
|
||||
},
|
||||
"external_task_id": "my-task-001",
|
||||
"priority": "normal",
|
||||
"callback_url": "https://your-domain.com/callback",
|
||||
"callback_token": "your-callback-token"
|
||||
}
|
||||
```
|
||||
|
||||
**请求字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `task_type` | string | 是 | 任务类型,如 `video_generation`, `image_generation` |
|
||||
| `payload` | object | 是 | 任务参数,具体结构取决于 task_type |
|
||||
| `external_task_id` | string | 否 | 调用方的任务 ID,用于关联 |
|
||||
| `priority` | string | 否 | 优先级:`low`, `normal`, `high`, `urgent`,默认 `normal` |
|
||||
| `callback_url` | string | 否 | 回调 URL,不提供则需轮询 |
|
||||
| `callback_token` | string | 否 | 回调验证 token |
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "received",
|
||||
"message": "任务已接收"
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 是否成功 |
|
||||
| `task_id` | string | 服务端生成的任务 ID |
|
||||
| `status` | string | 初始状态,通常为 `received` |
|
||||
| `message` | string | 提示信息 |
|
||||
| `error` | string | 错误信息(失败时) |
|
||||
|
||||
### 4.2 提交任务(Multipart 带文件)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/synapse/task/multipart
|
||||
Content-Type: multipart/form-data
|
||||
X-API-Key: your-api-key
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="task_type"
|
||||
|
||||
image_generation
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="payload"
|
||||
|
||||
{"prompt":"一只猫","style":"写实"}
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="priority"
|
||||
|
||||
normal
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="file"; filename="reference.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
[二进制数据]
|
||||
|
||||
------WebKitFormBoundary--
|
||||
```
|
||||
|
||||
**表单字段:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `task_type` | string | 是 | 任务类型 |
|
||||
| `payload` | string(JSON) | 是 | 任务参数(JSON 字符串) |
|
||||
| `external_task_id` | string | 否 | 外部任务 ID |
|
||||
| `priority` | string | 否 | 优先级 |
|
||||
| `callback_url` | string | 否 | 回调 URL |
|
||||
| `callback_token` | string | 否 | 回调 token |
|
||||
| `file` | file | 否 | 上传的文件(可多个) |
|
||||
|
||||
**响应:** 同 JSON 提交方式
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务查询
|
||||
|
||||
### 5.1 获取任务详情
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/synapse/task/{task_id}
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task": {
|
||||
"id": "task-abc123",
|
||||
"type": "text_generation",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "任务已完成",
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:01:00Z",
|
||||
"completed_at": "2026-01-01T00:01:00Z",
|
||||
"eta": "2026-01-01T00:01:00Z",
|
||||
"result": {
|
||||
"text": "生成的文本...",
|
||||
"metadata": { "tokens": 1024 }
|
||||
},
|
||||
"error": null,
|
||||
"external_task_id": "my-task-001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**任务状态:**
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `received` | 已接收 |
|
||||
| `processing` | 处理中 |
|
||||
| `completed` | 已完成 |
|
||||
| `failed` | 已失败 |
|
||||
| `cancelled` | 已取消 |
|
||||
|
||||
### 5.2 获取任务状态(简化版)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/synapse/task/{task_id}/status
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "processing",
|
||||
"progress": 50
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 获取任务列表
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/synapse/tasks?status=processing&limit=20&offset=0
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `status` | string | 否 | 按状态过滤 |
|
||||
| `limit` | int | 否 | 每页数量,默认 20 |
|
||||
| `offset` | int | 否 | 偏移量,默认 0 |
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-abc123",
|
||||
"type": "text_generation",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 任务取消
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/synapse/task/{task_id}/cancel
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "cancelled",
|
||||
"message": "任务已取消"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 能力查询
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/synapse/capabilities
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"capabilities": {
|
||||
"agent_id": "my-agent",
|
||||
"agent_name": "我的Agent",
|
||||
"system_id": "my-system",
|
||||
"protocol_version": "1.0.0",
|
||||
"supported_task_types": [
|
||||
{
|
||||
"type": "text_generation",
|
||||
"name": "文本生成",
|
||||
"description": "生成各种类型的文本内容",
|
||||
"estimated_time": "30s",
|
||||
"input_schema": "...",
|
||||
"output_schema": "..."
|
||||
}
|
||||
],
|
||||
"supported_transport_modes": ["callback", "polling"],
|
||||
"max_file_size": 104857600,
|
||||
"max_concurrent_tasks": 10,
|
||||
"support_progress_updates": true,
|
||||
"support_streaming": false,
|
||||
"support_hil": true,
|
||||
"support_groups": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 回调机制
|
||||
|
||||
如果调用方提供了 `callback_url`,服务端会在任务状态变化时主动回调。
|
||||
|
||||
### 8.1 回调请求
|
||||
|
||||
```http
|
||||
POST {callback_url}
|
||||
Content-Type: application/json
|
||||
X-Callback-Token: {callback_token}
|
||||
|
||||
{
|
||||
"task_id": "task-abc123",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "任务已完成",
|
||||
"result": {
|
||||
"text": "生成的文本..."
|
||||
},
|
||||
"error": null,
|
||||
"timestamp": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 回调类型
|
||||
|
||||
| 触发时机 | 说明 |
|
||||
|---------|------|
|
||||
| 任务开始处理 | `status=processing`, `progress=0` |
|
||||
| 进度更新 | `status=processing`, `progress=N` |
|
||||
| 任务完成 | `status=completed`, `progress=100`, 包含 result |
|
||||
| 任务失败 | `status=failed`, 包含 error |
|
||||
| 任务取消 | `status=cancelled` |
|
||||
|
||||
### 8.3 回调响应
|
||||
|
||||
调用方应返回 2xx 状态码表示成功接收。如果返回非 2xx,服务端会重试(指数退避,最多 5 次)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 错误处理
|
||||
|
||||
### 9.1 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "invalid_task_type",
|
||||
"message": "不支持的任务类型"
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 错误码列表
|
||||
|
||||
| 错误码 | HTTP 状态码 | 说明 |
|
||||
|--------|------------|------|
|
||||
| `unauthorized` | 401 | API Key 无效或缺失 |
|
||||
| `invalid_request` | 400 | 请求格式无效 |
|
||||
| `invalid_task_type` | 400 | 任务类型不支持 |
|
||||
| `invalid_params` | 400 | 参数无效 |
|
||||
| `task_not_found` | 404 | 任务不存在 |
|
||||
| `task_already_completed` | 409 | 任务已完成,无法取消 |
|
||||
| `file_too_large` | 413 | 文件过大 |
|
||||
| `rate_limited` | 429 | 请求过于频繁 |
|
||||
| `internal_error` | 500 | 服务内部错误 |
|
||||
| `service_unavailable` | 503 | 服务不可用 |
|
||||
|
||||
---
|
||||
|
||||
## 10. SDK 客户端接口
|
||||
|
||||
为方便调用方集成,提供 SDK 客户端。
|
||||
|
||||
### 10.1 SDK 初始化
|
||||
|
||||
```go
|
||||
client := sdk.NewClient(sdk.ClientConfig{
|
||||
BaseURL: "https://agent-api.example.com",
|
||||
APIKey: "your-api-key",
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
```
|
||||
|
||||
### 10.2 SDK 方法清单
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `SubmitTask(ctx, req)` | 提交任务(JSON) |
|
||||
| `SubmitTaskWithFiles(ctx, req, files)` | 提交任务(带文件) |
|
||||
| `GetTask(ctx, taskID)` | 获取任务详情 |
|
||||
| `GetTaskStatus(ctx, taskID)` | 获取任务状态 |
|
||||
| `ListTasks(ctx, status, limit, offset)` | 获取任务列表 |
|
||||
| `CancelTask(ctx, taskID)` | 取消任务 |
|
||||
| `GetCapabilities(ctx)` | 获取能力清单 |
|
||||
| `WaitForTask(ctx, taskID, interval)` | 等待任务完成(轮询) |
|
||||
|
||||
### 10.3 数据结构
|
||||
|
||||
```go
|
||||
type TaskRequest struct {
|
||||
TaskType string
|
||||
Payload map[string]any
|
||||
ExternalTaskID string
|
||||
Priority string
|
||||
CallbackURL string
|
||||
CallbackToken string
|
||||
}
|
||||
|
||||
type TaskInfo struct {
|
||||
ID string
|
||||
Type string
|
||||
Status string
|
||||
Progress int
|
||||
Message string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
CompletedAt string
|
||||
ETA string
|
||||
Result map[string]any
|
||||
Error *TaskError
|
||||
ExternalTaskID string
|
||||
}
|
||||
```
|
||||
|
||||
**完整 SDK 实现:** [sdk/client.go](../sdk/client.go)
|
||||
|
||||
---
|
||||
|
||||
## 11. 参考实现
|
||||
|
||||
本协议的参考实现:
|
||||
|
||||
| 模块 | 文件路径 | 说明 |
|
||||
|------|---------|------|
|
||||
| 服务端 | [externalapi/handler.go](../externalapi/handler.go) | External API Handler |
|
||||
| 客户端 SDK | [sdk/client.go](../sdk/client.go) | Go 语言 SDK |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:调用模式选择
|
||||
|
||||
### 回调模式 vs 轮询模式
|
||||
|
||||
| 对比项 | 回调模式 | 轮询模式 |
|
||||
|--------|---------|---------|
|
||||
| 实时性 | 高,状态变化即时通知 | 低,取决于轮询间隔 |
|
||||
| 实现复杂度 | 调用方需提供公网可访问的回调接口 | 简单,只需调用 GET 接口 |
|
||||
| 网络要求 | 调用方需要公网可达 | 只需调用方能访问服务端 |
|
||||
| 适用场景 | 生产环境、需要实时通知 | 开发测试、内网环境 |
|
||||
|
||||
### 选择建议
|
||||
|
||||
- **有公网服务器**:使用回调模式
|
||||
- **本地开发 / 内网环境**:使用轮询模式
|
||||
- **长时间任务**:建议回调模式,避免频繁轮询
|
||||
- **短任务(<10s)**:可直接同步等待或高频轮询
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:从 A2A 协议到 P2A 协议的映射
|
||||
|
||||
| 灵隙概念 | P2A 对应 | 说明 |
|
||||
|---------|---------|------|
|
||||
| `p2p:task_request` | `POST /task` | 任务提交 |
|
||||
| `p2p:task_status_update` | 回调 / GET /task | 状态更新 |
|
||||
| `p2p:task_complete` | 回调(status=completed) | 任务完成 |
|
||||
| `p2p:task_failed` | 回调(status=failed) | 任务失败 |
|
||||
| `p2p:task_cancel` | `POST /task/{id}/cancel` | 任务取消 |
|
||||
| `p2p:capability_query` | `GET /capabilities` | 能力查询 |
|
||||
| Agent 身份 | API Key | 简化认证 |
|
||||
| HMAC 签名 | Callback Token | 简化验证 |
|
||||
519
docs/05-sdk-implementation.md
Normal file
519
docs/05-sdk-implementation.md
Normal file
@@ -0,0 +1,519 @@
|
||||
# SDK 实现说明
|
||||
|
||||
> 版本:1.0.0
|
||||
> 更新日期:2026-06-27
|
||||
> 协议版本:1.0.0
|
||||
> 关联协议:
|
||||
> - [A2A 协议](./02-a2a-protocol.md)
|
||||
> - [A2N 协议](./03-a2n-protocol.md)
|
||||
> - [P2A 协议](./04-p2a-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
SDK 按照灵隙协议族(Synapse Protocol Suite)的三个子协议进行了模块化设计,分为三个子包,命名与协议名保持一致:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SDK (sdk/) │
|
||||
├──────────────┬───────────────────┬───────────────────────────┤
|
||||
│ sdk/p2a │ sdk/a2a │ sdk/a2n │
|
||||
│ P2A 协议 │ A2A 协议 │ A2N 协议 │
|
||||
│ (外部调用) │ (Agent间通信) │ (群组协作) │
|
||||
│ ✅ 完整实现 │ ✅ 完整实现 │ ✅ 完整实现 │
|
||||
└──────────────┴───────────────────┴───────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. SDK 目录结构
|
||||
|
||||
```
|
||||
sdk/
|
||||
├── doc.go # SDK 包文档(使用说明)
|
||||
├── p2a/
|
||||
│ └── client.go # P2A 协议 - 外部调用客户端
|
||||
├── a2a/
|
||||
│ └── client.go # A2A 协议 - Agent间通信客户端
|
||||
└── a2n/
|
||||
└── client.go # A2N 协议 - 群组协作客户端
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 第一部分:sdk/p2a - P2A 协议
|
||||
|
||||
**协议对应**:[P2A 协议](./04-p2a-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:非 Agent 调用方(前端、后端服务、第三方应用)通过 REST API 调用 Agent 能力
|
||||
|
||||
### 3.1 实现对应关系
|
||||
|
||||
| P2A 协议功能 | SDK 方法 | 位置 |
|
||||
|-------------|---------|------|
|
||||
| 认证方式 | `setHeaders()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 提交任务(JSON) | `SubmitTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 提交任务(Multipart) | `SubmitTaskWithFiles()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务详情 | `GetTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务状态(简化版) | `GetTaskStatus()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务列表 | `ListTasks()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 取消任务 | `CancelTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 能力查询 | `GetCapabilities()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 回调验证 | `VerifyCallback()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 轮询等待 | `WaitForTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
|
||||
### 3.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client 客户端
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// 任务请求
|
||||
type TaskRequest struct {
|
||||
TaskType string
|
||||
Payload map[string]any
|
||||
ExternalTaskID string
|
||||
Priority string
|
||||
CallbackURL string
|
||||
CallbackToken string
|
||||
}
|
||||
|
||||
// 任务信息
|
||||
type TaskInfo struct {
|
||||
ID string
|
||||
Type string
|
||||
Status string
|
||||
Progress int
|
||||
Result map[string]any
|
||||
Error *TaskError
|
||||
}
|
||||
|
||||
// 文件上传参数
|
||||
type FileParam struct {
|
||||
FieldName string
|
||||
Filename string
|
||||
Data []byte
|
||||
MimeType string
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 使用示例
|
||||
|
||||
```go
|
||||
import "dp-headless/synapse/sdk/p2a"
|
||||
|
||||
client := p2a.NewClient(p2a.ClientConfig{
|
||||
BaseURL: "https://agent-api.example.com",
|
||||
APIKey: "your-api-key",
|
||||
})
|
||||
|
||||
// 提交任务并等待完成
|
||||
taskInfo, err := client.CreateAndWait(ctx, "text_generation", map[string]any{
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典",
|
||||
}, 5*time.Second)
|
||||
|
||||
// 使用回调模式
|
||||
resp, err := client.CreateWithCallback(ctx, "image_generation", payload,
|
||||
"https://your-domain.com/callback",
|
||||
"callback-token",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 第二部分:sdk/a2a - A2A 协议
|
||||
|
||||
**协议对应**:[A2A 协议](./02-a2a-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:Agent 之间的点对点通信,包含消息发送、签名验证、回调处理
|
||||
|
||||
### 4.1 实现对应关系
|
||||
|
||||
| A2A 协议功能 | SDK 方法 | 位置 |
|
||||
|------------|---------|------|
|
||||
| 发送任务请求 | `SendTaskRequest()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 发送任务取消 | `SendTaskCancel()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 发送人类决策 | `SendHumanDecision()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 查询能力 | `SendCapabilityQuery()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 消息签名 | `signMessage()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 签名验证 | `verifySignature()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 任务状态管理 | `GetTaskState()`, `ListTasks()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 等待任务完成 | `WaitForTask()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 回调服务 | `startCallbackServer()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
|
||||
### 4.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client A2A客户端
|
||||
type Client struct {
|
||||
agentEndpoint types.AgentEndpoint
|
||||
authSecret string
|
||||
callbackHandler MessageCallbackHandler
|
||||
tasks map[string]*A2ATaskState
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
AgentEndpoint types.AgentEndpoint
|
||||
AuthSecret string
|
||||
CallbackPort int
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// 消息回调处理器接口
|
||||
type MessageCallbackHandler interface {
|
||||
HandleTaskRequest(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error)
|
||||
HandleTaskStatusUpdate(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleTaskComplete(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleTaskFailed(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleNeedHuman(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
}
|
||||
|
||||
// 任务状态
|
||||
type A2ATaskState struct {
|
||||
TaskID string
|
||||
Status types.P2PTaskStatus
|
||||
Progress int
|
||||
Message string
|
||||
Result any
|
||||
Error *types.SynapseError
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 使用示例
|
||||
|
||||
```go
|
||||
import "dp-headless/synapse/sdk/a2a"
|
||||
|
||||
a2aClient, err := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: types.AgentEndpoint{
|
||||
AgentID: "my-agent",
|
||||
AgentName: "我的Agent",
|
||||
SystemID: "my-system",
|
||||
Endpoint: "http://localhost:8080/synapse/callback",
|
||||
},
|
||||
AuthSecret: "my-hmac-secret",
|
||||
CallbackPort: 8080,
|
||||
})
|
||||
|
||||
// 设置消息处理器
|
||||
a2aClient.SetMessageHandler(&myHandler{})
|
||||
|
||||
// 发送任务请求给其他Agent
|
||||
taskID, err := a2aClient.SendTaskRequest(ctx, targetAgent, "video_generation", map[string]any{
|
||||
"prompt": "生成一段视频",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 第三部分:sdk/a2n - A2N 协议
|
||||
|
||||
**协议对应**:[A2N 协议](./03-a2n-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:多 Agent 群组协作,包含群组管理、任务拆分合并、投票、升级
|
||||
|
||||
### 5.1 实现对应关系
|
||||
|
||||
| A2N 协议功能 | SDK 方法 | 位置 |
|
||||
|------------|---------|------|
|
||||
| 创建群组 | `CreateGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 加入群组 | `JoinGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 离开群组 | `LeaveGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 解散群组 | `DisbandGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 提交群组任务 | `SubmitGroupTask()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 发送子任务结果 | `SendSubtaskResult()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 发起投票 | `InitiateVote()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 提交投票 | `SubmitVote()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 广播消息 | `Broadcast()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 升级到人类 | `EscalateToHuman()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
|
||||
### 5.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client A2N客户端
|
||||
type Client struct {
|
||||
a2aClient *a2a.Client
|
||||
groups map[string]*GroupState
|
||||
eventHandler GroupEventHandler
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
A2AClient *a2a.Client
|
||||
EventHandler GroupEventHandler
|
||||
}
|
||||
|
||||
// 群组状态
|
||||
type GroupState struct {
|
||||
GroupID string
|
||||
Name string
|
||||
Topology types.GroupTopology
|
||||
Status types.GroupStatus
|
||||
Members []types.GroupMember
|
||||
Role string // "coordinator" | "worker"
|
||||
}
|
||||
|
||||
// 群组事件处理器接口
|
||||
type GroupEventHandler interface {
|
||||
OnGroupCreated(ctx context.Context, groupID string) error
|
||||
OnGroupDisbanded(ctx context.Context, groupID string) error
|
||||
OnMemberJoined(ctx context.Context, groupID string, member types.GroupMember) error
|
||||
OnMemberLeft(ctx context.Context, groupID string, agentID string) error
|
||||
OnTaskSplit(ctx context.Context, groupID string, originalTaskID string, subtasks []types.Subtask) error
|
||||
OnSubtaskProgress(ctx context.Context, groupID string, subtaskID string, progress int) error
|
||||
OnTaskMerged(ctx context.Context, groupID string, originalTaskID string, result any) error
|
||||
OnVoteInitiated(ctx context.Context, groupID string, vote types.GroupVotePayload) error
|
||||
OnVoteResult(ctx context.Context, groupID string, voteID string, option string, passed bool) error
|
||||
OnEscalation(ctx context.Context, groupID string, escalationID string, level int) error
|
||||
OnBroadcast(ctx context.Context, groupID string, sender types.AgentEndpoint, message []byte) error
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 使用示例
|
||||
|
||||
```go
|
||||
import (
|
||||
"dp-headless/synapse/sdk/a2n"
|
||||
"dp-headless/synapse/sdk/a2a"
|
||||
)
|
||||
|
||||
// 创建A2A客户端
|
||||
a2aClient, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: myEndpoint,
|
||||
})
|
||||
|
||||
// 创建A2N客户端
|
||||
a2nClient, err := a2n.NewClient(&a2n.ClientConfig{
|
||||
A2AClient: a2aClient,
|
||||
EventHandler: &myGroupHandler{},
|
||||
})
|
||||
|
||||
// 创建群组
|
||||
group, err := a2nClient.CreateGroup(ctx, "视频制作组", types.GroupTopologyStar, nil, members)
|
||||
|
||||
// 作为Coordinator,提交任务
|
||||
subtasks, err := a2nClient.SubmitGroupTask(ctx, group.GroupID, "video_generation",
|
||||
map[string]any{"script": "..."},
|
||||
types.MergeAllSuccess,
|
||||
)
|
||||
|
||||
// 发起投票
|
||||
voteID, err := a2nClient.InitiateVote(ctx, group.GroupID,
|
||||
"选择哪个方案?",
|
||||
[]string{"方案A", "方案B"},
|
||||
voterIDs,
|
||||
0.5,
|
||||
5*time.Minute,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. SDK 架构总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SDK (dp-headless/synapse/sdk) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ doc.go │
|
||||
│ ├── 包文档和使用说明 │
|
||||
│ └── 指向各子包的链接 │
|
||||
├────────────────┬────────────────┬─────────────────────────┤
|
||||
│ sdk/p2a │ sdk/a2a │ sdk/a2n │
|
||||
│ │ │ │
|
||||
│ P2A协议 │ 灵隙协议 │ A2N协议 │
|
||||
│ │ │ │
|
||||
│ ├── Client │ ├── Client │ ├── Client │
|
||||
│ ├── 配置 │ ├── 配置 │ ├── 配置 │
|
||||
│ ├── 任务操作 │ ├── 消息发送 │ ├── 群组管理 │
|
||||
│ ├── 能力查询 │ ├── 任务状态 │ ├── 任务协作 │
|
||||
│ └── 回调验证 │ ├── 回调服务 │ ├── 投票 │
|
||||
│ │ └── 签名 │ └── 升级 │
|
||||
└────────────────┴────────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 快速开始
|
||||
|
||||
### 7.1 P2A 调用(外部调用者)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/synapse/sdk/p2a"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := p2a.NewClient(p2a.ClientConfig{
|
||||
BaseURL: "https://agent.example.com",
|
||||
APIKey: "your-api-key",
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 方式1: 轮询等待
|
||||
result, err := client.CreateAndWait(ctx, "text_generation", map[string]any{
|
||||
"prompt": "写一首诗",
|
||||
}, 5*time.Second)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
}
|
||||
fmt.Printf("Result: %v\n", result.Result)
|
||||
|
||||
// 方式2: 回调模式
|
||||
resp, err := client.CreateWithCallback(ctx, "image_generation",
|
||||
map[string]any{"prompt": "一只猫"},
|
||||
"https://your-domain.com/callback",
|
||||
"token",
|
||||
)
|
||||
fmt.Printf("Task ID: %s\n", resp.TaskID)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 灵隙 通信(Agent 间)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/synapse/sdk/a2a"
|
||||
"dp-headless/synapse/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: types.AgentEndpoint{
|
||||
AgentID: "agent-001",
|
||||
AgentName: "Agent 001",
|
||||
SystemID: "my-system",
|
||||
Endpoint: "http://localhost:8080/synapse/callback",
|
||||
},
|
||||
AuthSecret: "secret-key",
|
||||
CallbackPort: 8080,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
target := types.AgentEndpoint{
|
||||
AgentID: "agent-002",
|
||||
Endpoint: "http://agent-002:8080/synapse/callback",
|
||||
}
|
||||
|
||||
// 发送任务请求
|
||||
taskID, err := client.SendTaskRequest(ctx, target, "data_analysis", map[string]any{
|
||||
"dataset": "sales.csv",
|
||||
})
|
||||
fmt.Printf("Task ID: %s\n", taskID)
|
||||
|
||||
// 等待完成
|
||||
state, err := client.WaitForTask(ctx, taskID, 5*time.Minute)
|
||||
fmt.Printf("Status: %s\n", state.Status)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 A2N 群组协作
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/synapse/sdk/a2n"
|
||||
"dp-headless/synapse/sdk/a2a"
|
||||
"dp-headless/synapse/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a2aClient, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: myEndpoint,
|
||||
})
|
||||
|
||||
a2nClient, _ := a2n.NewClient(&a2n.ClientConfig{
|
||||
A2AClient: a2aClient,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建群组
|
||||
group, _ := a2nClient.CreateGroup(ctx, "协作组", types.GroupTopologyStar, nil, []types.AgentEndpoint{
|
||||
{AgentID: "member-1", Endpoint: "http://member-1:8080"},
|
||||
{AgentID: "member-2", Endpoint: "http://member-2:8080"},
|
||||
})
|
||||
|
||||
fmt.Printf("Group ID: %s\n", group.GroupID)
|
||||
|
||||
// 提交任务
|
||||
subtasks, _ := a2nClient.SubmitGroupTask(ctx, group.GroupID, "video_generation",
|
||||
map[string]any{"script": "..."},
|
||||
types.MergeAllSuccess,
|
||||
)
|
||||
|
||||
fmt.Printf("Split into %d subtasks\n", len(subtasks))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 协议与包名对应关系
|
||||
|
||||
| 协议名称 | 协议文档 | SDK 包名 | 包路径 |
|
||||
|---------|---------|---------|--------|
|
||||
| 灵隙原始协议(Synapse Raw Protocol) | [01-a2a-raw-protocol.md](./01-a2a-raw-protocol.md) | - | 纲领性文档 |
|
||||
| A2A 协议 | [02-a2a-protocol.md](./02-a2a-protocol.md) | `a2a` | `sdk/a2a/` |
|
||||
| A2N 协议 | [03-a2n-protocol.md](./03-a2n-protocol.md) | `a2n` | `sdk/a2n/` |
|
||||
| P2A 协议 | [04-p2a-protocol.md](./04-p2a-protocol.md) | `p2a` | `sdk/p2a/` |
|
||||
|
||||
**所有协议版本:1.0.0**
|
||||
|
||||
---
|
||||
|
||||
## 9. 扩展建议
|
||||
|
||||
| 优先级 | 扩展项 | 说明 |
|
||||
|-------|-------|------|
|
||||
| 高 | gRPC 传输 | 添加 gRPC 协议支持 |
|
||||
| 高 | WebSocket | 添加 WebSocket 流式传输 |
|
||||
| 中 | 批量操作 | 批量任务提交、批量查询 |
|
||||
| 中 | 指标监控 | 内置指标收集 |
|
||||
| 低 | 多语言 SDK | Python、JavaScript 等 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文件索引
|
||||
|
||||
| 文件路径 | 说明 |
|
||||
|---------|------|
|
||||
| [sdk/doc.go](../sdk/doc.go) | SDK 包文档 |
|
||||
| [sdk/p2a/client.go](../sdk/p2a/client.go) | P2A 客户端实现 |
|
||||
| [sdk/a2a/client.go](../sdk/a2a/client.go) | A2A 客户端实现 |
|
||||
| [sdk/a2n/client.go](../sdk/a2n/client.go) | A2N 客户端实现 |
|
||||
| [externalapi/handler.go](../externalapi/handler.go) | P2A 协议服务端实现 |
|
||||
| [p2p/protocol.go](../p2p/protocol.go) | A2A 协议核心实现 |
|
||||
| [group/protocol.go](../group/protocol.go) | A2N 协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 公共类型定义 |
|
||||
| [transport/transport.go](../transport/transport.go) | HTTP 传输层实现 |
|
||||
777
docs/06-solutions-whitepaper.md
Normal file
777
docs/06-solutions-whitepaper.md
Normal file
@@ -0,0 +1,777 @@
|
||||
# 灵隙协议族(Synapse Protocol Suite) - 解决方案白皮书
|
||||
|
||||
> 版本:1.0.0
|
||||
> 日期:2026-06-27
|
||||
> 状态:草案
|
||||
> 前置阅读:[灵隙原始协议(Synapse Raw Protocol)](./01-a2a-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [概述](#1-概述)
|
||||
2. [方案一:Agent 发现与广播机制](#2-方案一agent-发现与广播机制)
|
||||
3. [方案二:身份认证与信任体系](#3-方案二身份认证与信任体系)
|
||||
4. [方案三:协议缺失内容补全](#4-方案三协议缺失内容补全)
|
||||
5. [分阶段实施路线图](#5-分阶段实施路线图)
|
||||
6. [技术选型建议](#6-技术选型建议)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
当前灵隙协议族(Synapse Protocol Suite)定义了 Agent 之间"怎么说话"(消息格式、任务流程),但缺少:
|
||||
|
||||
- **怎么找到对方** — Agent 发现与广播机制
|
||||
- **怎么相信对方** — 身份认证与信任体系
|
||||
- **怎么规模化协作** — 路由、调度、激励等基础设施
|
||||
|
||||
本文档针对上述问题给出完整的解决方案。
|
||||
|
||||
### 1.2 设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **渐进式** | 从简单到复杂,每阶段都可用 |
|
||||
| **可插拔** | 组件可替换,不绑定具体实现 |
|
||||
| **传输无关** | 不依赖特定底层网络 |
|
||||
| **安全优先** | 身份认证是基础设施,不是可选功能 |
|
||||
| **向后兼容** | 不破坏现有协议,增量扩展 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案一:Agent 发现与广播机制
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Agent 发现与广播层 │
|
||||
├─────────────────────┬─────────────────────┬─────────────────────────┤
|
||||
│ 集中式目录服务 │ 分布式 Gossip │ 组播/广播 │
|
||||
│ (Directory) │ (Gossip) │ (Multicast) │
|
||||
│ ┌───────────────┐ │ ┌───────────────┐ │ ┌────────────────┐ │
|
||||
│ │ 注册/注销 │ │ │ 消息泛洪 │ │ │ 子网内广播 │ │
|
||||
│ │ 能力索引 │ │ │ 状态同步 │ │ │ 邻居发现 │ │
|
||||
│ │ 名字解析 │ │ │ 节点发现 │ │ │ 本地心跳 │ │
|
||||
│ └───────────────┘ │ └───────────────┘ │ └────────────────┘ │
|
||||
└─────────────────────┴─────────────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 模式一:集中式目录服务(推荐首选)
|
||||
|
||||
#### 2.2.1 架构说明
|
||||
|
||||
引入 **Agent Directory Service(ADS)** 作为中心化的 Agent 目录服务,类似 DNS。
|
||||
|
||||
```
|
||||
Agent A ──注册──▶ ┌─────────────┐ ◀──注册── Agent B
|
||||
│ Directory │
|
||||
Agent C ──查询──▶ │ Service │ ◀──心跳── Agent D
|
||||
└─────────────┘
|
||||
│
|
||||
▼
|
||||
能力索引
|
||||
在线状态
|
||||
端点地址
|
||||
```
|
||||
|
||||
#### 2.2.2 核心接口
|
||||
|
||||
**注册协议(Agent → Directory):**
|
||||
|
||||
| 操作 | 消息类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| 注册 | `dir:register` | Agent 上线时注册自身信息 |
|
||||
| 注销 | `dir:unregister` | Agent 下线时注销 |
|
||||
| 心跳 | `dir:heartbeat` | 定期上报存活状态 |
|
||||
| 更新能力 | `dir:update_capabilities` | 能力变化时更新 |
|
||||
|
||||
**查询协议(Agent → Directory):**
|
||||
|
||||
| 操作 | 消息类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| 按 ID 查询 | `dir:query_by_id` | 根据 AgentID 查端点 |
|
||||
| 按能力查询 | `dir:query_by_capability` | 找具备某能力的 Agent |
|
||||
| 列表查询 | `dir:list` | 分页列出所有 Agent |
|
||||
| 批量查询 | `dir:batch_query` | 批量查询多个 Agent |
|
||||
|
||||
#### 2.2.3 注册数据结构
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"agent_name": "视频生成Agent",
|
||||
"system_id": "video-system",
|
||||
"endpoint": "https://agent-001.example.com/synapse/callback",
|
||||
"version": "1.0.0",
|
||||
"capabilities": [
|
||||
{
|
||||
"type": "video_generation",
|
||||
"name": "视频生成",
|
||||
"description": "根据脚本生成视频",
|
||||
"max_concurrent": 10,
|
||||
"avg_latency_ms": 5000
|
||||
}
|
||||
],
|
||||
"transports": ["http", "grpc"],
|
||||
"status": "online",
|
||||
"last_heartbeat": "2026-06-27T10:00:00Z",
|
||||
"trust_score": 85,
|
||||
"public_key": "-----BEGIN PUBLIC KEY-----\n..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2.4 优缺点
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 查询效率高(O(1)) | 单点故障风险 |
|
||||
| 支持复杂查询(按能力、按系统等) | 中心节点需信任 |
|
||||
| 易于管理和监控 | 扩展性有上限 |
|
||||
| 实现简单 | 存在注册/注销时序问题 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 模式二:分布式 Gossip 协议(可选扩展)
|
||||
|
||||
#### 2.3.1 架构说明
|
||||
|
||||
无中心节点,Agent 之间通过 Gossip 协议(epidemic protocol)扩散状态信息,类似比特币网络的节点发现。
|
||||
|
||||
```
|
||||
Agent A ──gossip──▶ Agent B ──gossip──▶ Agent D
|
||||
│ │
|
||||
└──gossip──▶ Agent C ──gossip──▶ Agent E
|
||||
```
|
||||
|
||||
#### 2.3.2 核心机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| **Peer Sampling** | 每个 Agent 维护一个部分视图(partial view) |
|
||||
| **Active Push** | 定期随机选几个节点推送本地状态 |
|
||||
| **Passive Pull** | 收到推送后,有概率回传自身状态 |
|
||||
| **TTL** | 消息有存活跳数,防止无限扩散 |
|
||||
| **Deduplication** | 消息 ID 去重,避免重复处理 |
|
||||
|
||||
#### 2.3.3 Gossip 消息格式
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "gossip:state",
|
||||
"from": "agent-001",
|
||||
"view": [
|
||||
{"agent_id": "agent-002", "endpoint": "...", "capabilities": [...], "timestamp": "..."},
|
||||
{"agent_id": "agent-003", "endpoint": "...", "capabilities": [...], "timestamp": "..."},
|
||||
{"agent_id": "agent-004", "endpoint": "...", "capabilities": [...], "timestamp": "..."}
|
||||
],
|
||||
"ttl": 5,
|
||||
"msg_id": "msg-xxx"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3.4 优缺点
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 完全去中心化 | 查询效率低(需本地构建视图) |
|
||||
| 容错性强 | 状态最终一致,有延迟 |
|
||||
| 可无限扩展 | 实现复杂度高 |
|
||||
| 无单点故障 | 网络流量大 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 模式三:本地组播/邻居发现(局域网场景)
|
||||
|
||||
适用于同一内网/集群内的 Agent 快速发现。
|
||||
|
||||
#### 2.4.1 机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| **UDP 组播** | 定期在组播地址发送广播 |
|
||||
| **mDNS/DNS-SD** | 类似 Apple Bonjour 的服务发现 |
|
||||
| **ARP/NDP** | 链路层发现(不推荐) |
|
||||
|
||||
#### 2.4.2 适用场景
|
||||
|
||||
- 边缘计算集群
|
||||
- 同一机房内的 Agent 集群
|
||||
- 本地开发测试环境
|
||||
|
||||
---
|
||||
|
||||
### 2.5 广播机制设计
|
||||
|
||||
#### 2.5.1 广播类型
|
||||
|
||||
| 广播范围 | 消息类型前缀 | 说明 |
|
||||
|---------|-------------|------|
|
||||
| 全网广播 | `global:` | 所有在线 Agent |
|
||||
| 系统广播 | `system:` | 同一 system_id 内 |
|
||||
| 群组广播 | `group:` | 同一群组内(已有) |
|
||||
| 能力组广播 | `cap:` | 具备某能力的 Agent |
|
||||
|
||||
#### 2.5.2 广播实现
|
||||
|
||||
**集中式模式下:**
|
||||
- 由 Directory Service 转发广播消息
|
||||
- 支持按条件过滤(按能力、按系统等)
|
||||
|
||||
**分布式模式下:**
|
||||
- 通过 Gossip 协议泛洪
|
||||
- TTL 控制扩散范围
|
||||
- 消息 ID 去重
|
||||
|
||||
---
|
||||
|
||||
### 2.6 心跳与存活检测
|
||||
|
||||
#### 2.6.1 心跳机制
|
||||
|
||||
```
|
||||
Agent ───heartbeat──▶ Directory
|
||||
(每30秒)
|
||||
|
||||
Directory 判定离线:
|
||||
- 连续 3 次未收到心跳 → 标记为 offline
|
||||
- 离线 24 小时 → 自动注销
|
||||
```
|
||||
|
||||
#### 2.6.2 主动探测
|
||||
|
||||
```
|
||||
Directory ───ping──▶ Agent
|
||||
◀──pong──
|
||||
```
|
||||
|
||||
用于验证 Agent 是否真的在线(防止心跳伪造)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案二:身份认证与信任体系
|
||||
|
||||
### 3.1 三层身份架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Layer 3: 信任评分 (Trust Score) │
|
||||
│ 声誉系统、历史行为、第三方评价 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 2: 身份验证 (Authentication) │
|
||||
│ 签名验证、证书校验、DID 验证 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 1: 身份标识 (Identity) │
|
||||
│ AgentID + 公钥 / 证书 / DID │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Layer 1:身份标识方案
|
||||
|
||||
#### 3.2.1 方案 A:公钥身份(推荐基础版)
|
||||
|
||||
**核心思想**:AgentID = 公钥哈希(类似比特币地址)
|
||||
|
||||
```
|
||||
AgentID = base58(sha256(public_key)[:20])
|
||||
例:agent-1A2B3C4D5E6F...
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 自包含,不需要中心 CA
|
||||
- 公钥可验证身份
|
||||
- 实现简单
|
||||
|
||||
**缺点:**
|
||||
- 身份无法撤销(私钥泄露 = 身份被盗)
|
||||
- 没有人类可读的名字
|
||||
- 无法关联真实世界身份
|
||||
|
||||
**适用场景**:内部系统、测试环境、信任度要求不高的场景
|
||||
|
||||
#### 3.2.2 方案 B:PKI 证书体系
|
||||
|
||||
**核心思想**:引入 CA(Certificate Authority)颁发证书
|
||||
|
||||
```
|
||||
CA 根证书
|
||||
├── 系统A证书
|
||||
│ ├── Agent-001 证书
|
||||
│ └── Agent-002 证书
|
||||
└── 系统B证书
|
||||
└── Agent-003 证书
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 成熟的工业标准
|
||||
- 支持证书撤销(CRL/OCSP)
|
||||
- 有层级信任关系
|
||||
|
||||
**缺点:**
|
||||
- 需要信任中心化 CA
|
||||
- 证书管理复杂
|
||||
- 跨 CA 互操作需要桥接
|
||||
|
||||
**适用场景**:企业内部、有明确信任中心的场景
|
||||
|
||||
#### 3.2.3 方案 C:区块链 DID(去中心化身份)
|
||||
|
||||
**核心思想**:身份上链,链上存公钥和属性,类似 W3C DID 标准。
|
||||
|
||||
```
|
||||
链上存储:
|
||||
DID → {
|
||||
"publicKey": "...",
|
||||
"controller": "...",
|
||||
"authentication": [...],
|
||||
"service": [...],
|
||||
"revoked": false,
|
||||
"timestamp": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**三种实现路径:**
|
||||
|
||||
| 路径 | 说明 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| **公链 DID** | 使用以太坊/Solana 等公链 | 开放生态、跨组织协作 |
|
||||
| **联盟链 DID** | 多机构共建联盟链 | 行业联盟、政企合作 |
|
||||
| **私有链 DID** | 单组织内部链 | 企业内部、对性能要求高 |
|
||||
|
||||
**优点:**
|
||||
- 去中心化,无单点故障
|
||||
- 身份可验证、可追溯
|
||||
- 支持跨系统互认
|
||||
- 不可篡改的身份历史
|
||||
|
||||
**缺点:**
|
||||
- 链上操作有延迟
|
||||
- 需要支付 Gas 费(公链)
|
||||
- 实现复杂度高
|
||||
- 隐私保护需额外设计
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Layer 2:身份验证方案
|
||||
|
||||
#### 3.3.1 消息签名验证
|
||||
|
||||
所有灵隙消息必须签名,签名方式:
|
||||
|
||||
```
|
||||
签名内容 = SHA256(message_id + sender + receiver + payload + timestamp)
|
||||
签名 = Sign(private_key, 签名内容)
|
||||
验证 = Verify(public_key, 签名内容, 签名)
|
||||
```
|
||||
|
||||
#### 3.3.2 签名放置位置
|
||||
|
||||
| 位置 | 说明 |
|
||||
|------|------|
|
||||
| 消息体内 | `signature` 字段 |
|
||||
| HTTP Header | `X-Synapse-Signature` |
|
||||
| gRPC Metadata | `x-synapse-signature` |
|
||||
|
||||
#### 3.3.3 防重放攻击
|
||||
|
||||
```
|
||||
每个消息有唯一 message_id + timestamp
|
||||
接收方维护已处理 message_id 缓存(如最近 1 小时)
|
||||
timestamp 与本地时间差超过 5 分钟 → 拒绝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Layer 3:信任评分体系
|
||||
|
||||
#### 3.4.1 信任分维度
|
||||
|
||||
| 维度 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 任务完成率 | 30% | 历史任务成功 / 总任务数 |
|
||||
| 任务质量 | 25% | 结果评分、返工率 |
|
||||
| 响应速度 | 15% | 平均响应时间 |
|
||||
| 在线稳定性 | 15% | 在线时长 / 总时长 |
|
||||
| 历史信誉 | 15% | 其他 Agent 的评价 |
|
||||
|
||||
#### 3.4.2 信任分计算
|
||||
|
||||
```
|
||||
trust_score = Σ(维度_i × 权重_i)
|
||||
|
||||
范围:0 - 100
|
||||
等级:
|
||||
90-100 S 级 高度可信
|
||||
75-89 A 级 可信
|
||||
60-74 B 级 一般可信
|
||||
40-59 C 级 谨慎使用
|
||||
0-39 D 级 不可信
|
||||
```
|
||||
|
||||
#### 3.4.3 信任传递
|
||||
|
||||
```
|
||||
A 信任 B(评分 90)
|
||||
B 信任 C(评分 85)
|
||||
A 对 C 的初始信任 = 90 × 85% × 衰减因子 = 约 68
|
||||
```
|
||||
|
||||
**注意**:信任传递有衰减,防止信任污染。
|
||||
|
||||
---
|
||||
|
||||
### 3.5 区块链适用性决策树
|
||||
|
||||
```
|
||||
是否需要跨组织协作?
|
||||
├── 否 → 用 PKI 或公钥身份(简单高效)
|
||||
└── 是 → 是否有共同信任的中心机构?
|
||||
├── 是 → 用 PKI 体系(成熟可靠)
|
||||
└── 否 → 是否需要不可篡改的审计?
|
||||
├── 是 → 考虑联盟链 DID
|
||||
└── 否 → 公钥身份 + 声誉系统可能够用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 方案三:协议缺失内容补全
|
||||
|
||||
### 4.1 第一优先级补全(核心基础设施)
|
||||
|
||||
#### 4.1.1 Agent 发现协议(ADS - Agent Discovery Service)
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 方向 | 说明 |
|
||||
|---------|------|------|
|
||||
| `dir:register` | Agent → Directory | 注册 Agent |
|
||||
| `dir:unregister` | Agent → Directory | 注销 Agent |
|
||||
| `dir:heartbeat` | Agent → Directory | 心跳 |
|
||||
| `dir:update_capabilities` | Agent → Directory | 更新能力 |
|
||||
| `dir:query_by_id` | Agent → Directory | 按 ID 查询 |
|
||||
| `dir:query_by_capability` | Agent → Directory | 按能力查询 |
|
||||
| `dir:list` | Agent → Directory | 列表查询 |
|
||||
| `dir:query_result` | Directory → Agent | 查询结果 |
|
||||
|
||||
**新增数据结构:** `AgentDirectoryEntry`, `CapabilityIndex`, `QueryFilter`
|
||||
|
||||
#### 4.1.2 身份认证规范
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `auth:challenge` | 身份挑战 |
|
||||
| `auth:response` | 挑战响应 |
|
||||
| `auth:cert_exchange` | 证书交换 |
|
||||
|
||||
**新增内容:**
|
||||
- 标准签名算法列表(Ed25519, ECDSA P-256, RSA 2048)
|
||||
- 消息签名格式规范
|
||||
- 防重放攻击机制
|
||||
- 身份验证流程规范
|
||||
|
||||
#### 4.1.3 心跳与存活检测
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `net:ping` | 存活探测 |
|
||||
| `net:pong` | 存活响应 |
|
||||
| `net:offline_notify` | 下线通知 |
|
||||
|
||||
**规范内容:**
|
||||
- 心跳间隔(默认 30 秒)
|
||||
- 超时判定(连续 3 次未收到 → 离线)
|
||||
- 离线通知机制
|
||||
|
||||
#### 4.1.4 统一错误码体系
|
||||
|
||||
**错误分类:**
|
||||
|
||||
| 错误码前缀 | 类别 | 示例 |
|
||||
|-----------|------|------|
|
||||
| `msg_` | 消息层 | msg_invalid_format, msg_signature_invalid |
|
||||
| `auth_` | 认证层 | auth_failed, auth_expired, auth_forbidden |
|
||||
| `task_` | 任务层 | task_not_found, task_timeout, task_cancelled |
|
||||
| `cap_` | 能力层 | cap_lacking, cap_busy, cap_not_supported |
|
||||
| `group_` | 群组层 | group_not_found, group_full, group_permission_denied |
|
||||
| `dir_` | 目录层 | dir_agent_not_found, dir_query_failed |
|
||||
| `net_` | 网络层 | net_timeout, net_offline, net_unreachable |
|
||||
| `sys_` | 系统层 | sys_internal_error, sys_not_implemented |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 第二优先级补全(重要功能)
|
||||
|
||||
#### 4.2.1 能力匹配与任务路由
|
||||
|
||||
**功能:**
|
||||
- 智能选择最合适的 Agent 执行任务
|
||||
- 支持负载均衡(按并发数、响应速度、信任分)
|
||||
- 支持任务失败自动重试/转发
|
||||
|
||||
**新增消息类型:**
|
||||
- `router:submit_task` - 提交给路由器自动选择执行者
|
||||
- `router:forward` - 任务转发
|
||||
|
||||
#### 4.2.2 全网广播协议
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `global:broadcast` | 全网广播 |
|
||||
| `system:broadcast` | 系统内广播 |
|
||||
| `cap:broadcast` | 能力组广播 |
|
||||
|
||||
**规范内容:**
|
||||
- 广播范围定义
|
||||
- TTL 与去重机制
|
||||
- 广播风暴防护
|
||||
|
||||
#### 4.2.3 版本协商机制
|
||||
|
||||
**功能:**
|
||||
- Agent 之间协商使用的协议版本
|
||||
- 向后兼容处理
|
||||
- 能力降级策略
|
||||
|
||||
**新增消息类型:**
|
||||
- `meta:version_query` - 查询支持的版本
|
||||
- `meta:version_negotiate` - 版本协商
|
||||
|
||||
#### 4.2.4 幂等性保证
|
||||
|
||||
**规范内容:**
|
||||
- 幂等键(Idempotency Key)机制
|
||||
- 重复消息处理规范
|
||||
- 任务级幂等 vs 消息级幂等
|
||||
|
||||
---
|
||||
|
||||
### 4.3 第三优先级补全(增强功能)
|
||||
|
||||
#### 4.3.1 流式输出协议
|
||||
|
||||
**功能:**
|
||||
- 长任务实时进度推送
|
||||
- 生成式 AI 的 token 流式输出
|
||||
- WebSocket/SSE 传输支持
|
||||
|
||||
#### 4.3.2 批量任务协议
|
||||
|
||||
**功能:**
|
||||
- 一次性提交多个同类型任务
|
||||
- 批量状态查询
|
||||
- 批量结果收集
|
||||
|
||||
#### 4.3.3 支付与激励机制
|
||||
|
||||
**功能:**
|
||||
- 任务定价与结算
|
||||
- Token 或积分系统
|
||||
- 智能合约自动结算(区块链场景)
|
||||
|
||||
#### 4.3.4 群组发现协议
|
||||
|
||||
**功能:**
|
||||
- 按主题/任务类型查找群组
|
||||
- 推荐加入合适的群组
|
||||
- 群组热度/活跃度排名
|
||||
|
||||
---
|
||||
|
||||
### 4.4 安全相关补全
|
||||
|
||||
#### 4.4.1 端到端加密
|
||||
|
||||
**规范内容:**
|
||||
- 密钥协商(ECDH)
|
||||
- 对称加密载荷(AES-GCM)
|
||||
- 前向保密(PFS)
|
||||
|
||||
#### 4.4.2 权限委托
|
||||
|
||||
**功能:**
|
||||
- Agent A 委托 Agent B 代表自己执行任务
|
||||
- 委托令牌(Delegation Token)
|
||||
- 委托范围与时效控制
|
||||
|
||||
#### 4.4.3 审计日志标准
|
||||
|
||||
**规范内容:**
|
||||
- 统一审计日志格式
|
||||
- 关键操作必须记录
|
||||
- 日志完整性保护(哈希链)
|
||||
|
||||
---
|
||||
|
||||
## 5. 分阶段实施路线图
|
||||
|
||||
### 5.1 Phase 0:基础版(MVP) - 预计 2 周
|
||||
|
||||
**目标**:协议可用,能跑通基本流程
|
||||
|
||||
**内容:**
|
||||
- [x] 灵隙点对点任务协议(已完成)
|
||||
- [x] A2N 群组协作协议(已完成)
|
||||
- [x] P2A 外部调用协议(已完成)
|
||||
- [x] 三套协议的 SDK 实现(已完成)
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Phase 1:发现与身份 - 预计 4 周
|
||||
|
||||
**目标**:Agent 能互相找到对方,能验证对方身份
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| Agent 目录服务协议 | 集中式 ADS | P0 |
|
||||
| 公钥身份规范 | Ed25519 签名验证 | P0 |
|
||||
| 心跳与存活检测 | 30 秒心跳,3 次超时判定离线 | P0 |
|
||||
| 统一错误码体系 | 8 大类错误码 | P0 |
|
||||
| 防重放攻击 | message_id + timestamp | P0 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`06-agent-discovery.md`
|
||||
- 新协议文档:`07-authentication.md`
|
||||
- 代码:`discovery/` 包(目录服务客户端)
|
||||
- 代码:`auth/` 包(签名验证工具)
|
||||
- 更新 types 包:新增消息类型和错误码
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Phase 2:路由与广播 - 预计 4 周
|
||||
|
||||
**目标**:支持智能任务路由和全网广播
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 任务路由协议 | 按能力 + 负载 + 信任分选择执行者 | P1 |
|
||||
| 全网广播协议 | 基于目录服务的广播转发 | P1 |
|
||||
| 版本协商机制 | 协议版本兼容 | P1 |
|
||||
| 幂等性保证 | 幂等键 + 去重 | P1 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`08-routing-and-broadcast.md`
|
||||
- 代码:`routing/` 包增强(增加能力路由)
|
||||
- 更新目录服务:支持广播转发
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Phase 3:信任与激励 - 预计 6 周
|
||||
|
||||
**目标**:建立信任体系,支持规模化协作
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 信任评分体系 | 5 维度信任分计算 | P2 |
|
||||
| 流式输出协议 | WebSocket/SSE 流式进度 | P2 |
|
||||
| 批量任务协议 | 批量提交/查询/收集 | P2 |
|
||||
| 群组发现协议 | 群组搜索与推荐 | P2 |
|
||||
| SLA 协议 | 服务等级承诺 | P3 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`09-trust-and-reputation.md`
|
||||
- 代码:`trust/` 包(信任评分引擎)
|
||||
|
||||
---
|
||||
|
||||
### 5.5 Phase 4:高级特性 - 预计 8 周+
|
||||
|
||||
**目标**:企业级、生产级完整功能
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 端到端加密 | ECDH + AES-GCM | P3 |
|
||||
| 权限委托 | 委托令牌机制 | P3 |
|
||||
| 支付与激励 | Token/积分系统 | P3 |
|
||||
| 区块链 DID 集成 | 可选模块 | P4 |
|
||||
| 审计日志标准 | 统一审计格式 | P3 |
|
||||
| Gossip 协议 | 分布式发现(可选) | P4 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术选型建议
|
||||
|
||||
### 6.1 目录服务存储选型
|
||||
|
||||
| 方案 | 适用场景 | 优点 | 缺点 |
|
||||
|------|---------|------|------|
|
||||
| **Redis** | 中小规模 | 高性能,支持 TTL | 数据量受限 |
|
||||
| **PostgreSQL** | 中大规模 | 复杂查询,持久化 | 需运维 |
|
||||
| **Elasticsearch** | 大规模 + 全文检索 | 强大的搜索能力 | 运维复杂 |
|
||||
|
||||
**推荐**:先用 Redis(简单快),规模大了再迁 PostgreSQL。
|
||||
|
||||
---
|
||||
|
||||
### 6.2 签名算法选型
|
||||
|
||||
| 算法 | 安全性 | 性能 | 密钥长度 | 推荐度 |
|
||||
|------|--------|------|---------|--------|
|
||||
| Ed25519 | 高 | 快 | 256 位 | ⭐⭐⭐⭐⭐ 首选 |
|
||||
| ECDSA P-256 | 高 | 中 | 256 位 | ⭐⭐⭐⭐ 兼容 |
|
||||
| RSA 2048 | 中 | 慢 | 2048 位 | ⭐⭐ 仅兼容 |
|
||||
|
||||
**推荐**:默认 Ed25519,需要兼容旧系统时支持 ECDSA。
|
||||
|
||||
---
|
||||
|
||||
### 6.3 区块链选型(如果需要)
|
||||
|
||||
| 场景 | 推荐方案 | 说明 |
|
||||
|------|---------|------|
|
||||
| 企业内部 | **私有链(Hyperledger Fabric)** | 可控,高性能,无 Gas 费 |
|
||||
| 行业联盟 | **联盟链(FISCO BCOS / ChainMaker)** | 多机构共建,合规 |
|
||||
| 开放生态 | **公链(以太坊 / Solana)** | 完全开放,需 Gas 费 |
|
||||
| 简单原型 | **模拟链(数据库模拟)** | 先验证逻辑,再上真链 |
|
||||
|
||||
**推荐**:90% 的场景用不到真区块链,先用数据库模拟验证业务逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结与建议
|
||||
|
||||
### 7.1 核心建议
|
||||
|
||||
1. **先集中后分布**:先用集中式目录服务,简单高效;规模大了再考虑分布式
|
||||
2. **先公钥后区块链**:先用公钥身份解决 80% 的问题,真的需要了再上链
|
||||
3. **先 MVP 后完善**:先跑通核心流程,再逐步加高级功能
|
||||
4. **标准接口可替换**:目录服务、身份系统都做成可插拔的,方便未来升级
|
||||
|
||||
### 7.2 下一步行动
|
||||
|
||||
| 序号 | 行动 | 负责人 | 产出 |
|
||||
|------|------|--------|------|
|
||||
| 1 | 评审本方案,确认 Phase 1 范围 | 架构组 | 确认后的方案 |
|
||||
| 2 | 设计目录服务 API 详细规范 | 协议组 | `06-agent-discovery.md` |
|
||||
| 3 | 设计身份认证详细规范 | 协议组 | `07-authentication.md` |
|
||||
| 4 | 实现目录服务原型 | 开发组 | 可运行的 ADS 服务 |
|
||||
| 5 | 更新 SDK 支持发现和认证 | 开发组 | 新版 SDK |
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文档索引
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [01-a2a-raw-protocol.md](./01-a2a-raw-protocol.md) | 灵隙原始协议(Synapse Raw Protocol)(纲领) |
|
||||
| [02-a2a-protocol.md](./02-a2a-protocol.md) | A2A 协议(P2P 任务) |
|
||||
| [03-a2n-protocol.md](./03-a2n-protocol.md) | A2N 协议(群组协作) |
|
||||
| [04-p2a-protocol.md](./04-p2a-protocol.md) | P2A 协议(外部调用) |
|
||||
| [05-sdk-implementation.md](./05-sdk-implementation.md) | SDK 实现说明 |
|
||||
| **本文档** | **解决方案白皮书** |
|
||||
328
docs_backup/01-synapse-raw-protocol.md
Normal file
328
docs_backup/01-synapse-raw-protocol.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# A2A 原始协议(Agent-to-Agent Raw Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:纲领性文档
|
||||
|
||||
---
|
||||
|
||||
## 1. 协议愿景
|
||||
|
||||
A2A(Agent-to-Agent)协议旨在定义一套通用的、可扩展的智能体间通信标准,使不同系统、不同厂商、不同能力的智能体能够在统一的协议框架下进行协作、交易与能力组合。
|
||||
|
||||
本协议是 A2A 协议族的原始纲领,定义核心设计理念、架构原则与术语体系,为所有衍生协议提供顶层指导。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心设计理念
|
||||
|
||||
### 2.1 去中心化对等(Peer-to-Peer)
|
||||
- 所有 Agent 在协议层面地位平等,无中心化控制节点
|
||||
- 每个 Agent 既是服务提供者,也是服务消费者
|
||||
- Agent 间直接通信,不依赖中间 broker(传输层可选择不同实现)
|
||||
|
||||
### 2.2 能力驱动(Capability-Driven)
|
||||
- 通信以能力为核心,而非以身份为核心
|
||||
- Agent 声明自身能力,按需发现与调用
|
||||
- 能力可组合、可分解、可升级
|
||||
|
||||
### 2.3 任务导向(Task-Oriented)
|
||||
- 所有交互围绕"任务"展开
|
||||
- 任务有明确的生命周期、状态机与超时机制
|
||||
- 任务支持拆分、合并、委派与协作
|
||||
|
||||
### 2.4 人类可介入(Human-in-the-Loop)
|
||||
- 协议原生支持人类介入决策
|
||||
- Agent 可主动请求人类判断
|
||||
- 人类决策优先于 Agent 自动决策
|
||||
- 支持多级升级(escalation)机制
|
||||
|
||||
### 2.5 群组协作(Group Collaboration)
|
||||
- 支持多 Agent 组成临时或永久群组
|
||||
- 群组内有角色分工与权限控制
|
||||
- 支持任务拆分-执行-合并的 MapReduce 模式
|
||||
- 支持投票、共识等群体决策机制
|
||||
|
||||
### 2.6 传输无关(Transport Agnostic)
|
||||
- 协议语义与底层传输分离
|
||||
- 支持 HTTP/2、gRPC、WebSocket、消息队列等多种传输
|
||||
- 支持回调(Callback)、轮询(Polling)、同步(Sync)、混合(Hybrid)等多种交互模式
|
||||
|
||||
### 2.7 安全可信(Secure & Trustworthy)
|
||||
- 消息级签名与验证
|
||||
- Agent 身份认证与信任等级
|
||||
- 支持加密传输
|
||||
- 可审计、可追溯
|
||||
|
||||
---
|
||||
|
||||
## 3. 协议分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 应用层 (Application) │
|
||||
│ 业务逻辑、工作流、领域特定逻辑 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 协议层 (Protocol) │
|
||||
├─────────────┬───────────────────────────┤
|
||||
│ A2A 基础协议 │ A2N 组群协议 │ ← 本协议族
|
||||
│ (P2P) │ (Group) │
|
||||
├─────────────┴───────────────────────────┤
|
||||
│ 传输层 (Transport) │
|
||||
│ HTTP / gRPC / WebSocket / MQ ... │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 网络层 (Network) │
|
||||
│ TCP / UDP / QUIC ... │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.1 协议族构成
|
||||
|
||||
| 协议名称 | 缩写 | 描述 | 适用场景 |
|
||||
|---------|------|------|---------|
|
||||
| A2A 原始协议 | A2A-Raw | 顶层纲领,定义理念、术语、原则 | 指导所有协议设计 |
|
||||
| A2A 基础协议 | A2A-Base | 点对点任务协作协议 | 两个 Agent 间的任务交互 |
|
||||
| A2N 组群协议 | A2N-Group | 多 Agent 群组协作协议 | 多个 Agent 协同完成任务 |
|
||||
| P2A 调用协议 | P2A-Caller | 非 Agent 调用者接入协议 | 普通服务/前端调用 Agent 能力 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心概念与术语
|
||||
|
||||
### 4.1 Agent(智能体)
|
||||
能够自主感知环境、做出决策并执行动作的计算实体。
|
||||
|
||||
**属性:**
|
||||
- `agent_id`:Agent 唯一标识
|
||||
- `system_id`:所属系统标识
|
||||
- `endpoint`:通信端点地址
|
||||
- `capabilities`:能力清单
|
||||
- `trust_level`:信任等级
|
||||
|
||||
### 4.2 Task(任务)
|
||||
Agent 间协作的基本单位,具有明确的输入、输出与生命周期。
|
||||
|
||||
**属性:**
|
||||
- `task_id`:任务唯一标识
|
||||
- `task_type`:任务类型
|
||||
- `payload`:任务载荷
|
||||
- `status`:任务状态
|
||||
- `priority`:优先级
|
||||
- `deadline`:截止时间
|
||||
|
||||
### 4.3 Message(消息)
|
||||
Agent 间通信的基本单位。
|
||||
|
||||
**属性:**
|
||||
- `message_id`:消息唯一标识
|
||||
- `type`:消息类型
|
||||
- `sender`:发送方
|
||||
- `receiver`:接收方(P2P)
|
||||
- `group_id`:群组标识(群组)
|
||||
- `payload`:消息载荷
|
||||
- `correlation_id`:关联标识(用于请求-响应关联)
|
||||
- `timestamp`:时间戳
|
||||
- `signature`:签名
|
||||
|
||||
### 4.4 Capability(能力)
|
||||
Agent 能够执行的任务类型的声明。
|
||||
|
||||
**属性:**
|
||||
- `type`:能力对应的任务类型
|
||||
- `name`:能力名称
|
||||
- `description`:能力描述
|
||||
- `input_schema`:输入参数 schema
|
||||
- `output_schema`:输出结果 schema
|
||||
- `estimated_time`:预估耗时
|
||||
|
||||
### 4.5 Group(群组)
|
||||
由多个 Agent 组成的协作单元。
|
||||
|
||||
**属性:**
|
||||
- `group_id`:群组唯一标识
|
||||
- `name`:群组名称
|
||||
- `topology`:拓扑结构
|
||||
- `members`:成员列表
|
||||
- `roles`:角色定义
|
||||
- `status`:群组状态
|
||||
|
||||
### 4.6 Human-in-the-Loop(人在回路)
|
||||
在 Agent 协作过程中,人类可介入进行决策的机制。
|
||||
|
||||
**关键概念:**
|
||||
- `escalation`:升级(将问题提交给人类)
|
||||
- `decision`:决策(人类做出的判断)
|
||||
- `resume`:恢复(人类决策后继续执行)
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计原则
|
||||
|
||||
### 5.1 简约性(Simplicity)
|
||||
- 核心机制保持简单
|
||||
- 复杂功能通过组合基础机制实现
|
||||
- 可选功能通过能力声明来协商
|
||||
|
||||
### 5.2 可扩展性(Extensibility)
|
||||
- 消息类型可扩展
|
||||
- 能力类型可扩展
|
||||
- 传输方式可扩展
|
||||
- 通过版本号与能力协商保证向后兼容
|
||||
|
||||
### 5.3 容错性(Fault Tolerance)
|
||||
- 网络不可靠是常态
|
||||
- 消息可能丢失、重复、延迟
|
||||
- Agent 可能故障、离线
|
||||
- 协议必须有重试、超时、降级机制
|
||||
|
||||
### 5.4 可观测性(Observability)
|
||||
- 所有交互可追踪
|
||||
- 任务状态可查询
|
||||
- 支持日志、指标、链路追踪
|
||||
|
||||
### 5.5 互操作性(Interoperability)
|
||||
- 不同语言、不同框架的 Agent 可互操作
|
||||
- 协议规范优先于具体实现
|
||||
- 提供标准测试用例验证兼容性
|
||||
|
||||
---
|
||||
|
||||
## 6. 消息模型总则
|
||||
|
||||
### 6.1 统一消息信封
|
||||
所有协议共享统一的消息信封结构,具体载荷由消息类型决定。
|
||||
|
||||
```
|
||||
UnifiedA2AMessage
|
||||
├── id // 消息ID
|
||||
├── version // 协议版本
|
||||
├── type // 消息类型
|
||||
├── sender // 发送方 AgentEndpoint
|
||||
├── receiver? // 接收方(P2P 时)
|
||||
├── group_id? // 群组ID(群组时)
|
||||
├── correlation_id // 关联ID
|
||||
├── reply_to? // 回复的消息ID
|
||||
├── payload // 消息载荷(JSON)
|
||||
├── priority? // 优先级
|
||||
├── signature? // 签名
|
||||
├── timestamp // 发送时间
|
||||
└── expires_at? // 过期时间
|
||||
```
|
||||
|
||||
### 6.2 消息命名规范
|
||||
- 使用 `领域:动作` 格式
|
||||
- 全小写,用下划线分隔
|
||||
- 示例:`p2p:task_request`, `group:create`, `hil:decision`
|
||||
|
||||
### 6.3 请求-响应模式
|
||||
- 请求消息必须携带唯一 `id`
|
||||
- 响应消息通过 `reply_to` 关联请求
|
||||
- 支持 ACK/NACK 机制
|
||||
- 超时未收到响应应重试或报错
|
||||
|
||||
---
|
||||
|
||||
## 7. 任务生命周期总则
|
||||
|
||||
### 7.1 状态机
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ pending │ 待确认
|
||||
└────┬─────┘
|
||||
│ 确认/拒绝
|
||||
┌─────┴──────┐
|
||||
▼ ▼
|
||||
┌─────────┐ ┌──────────┐
|
||||
│confirmed│ │ rejected │ 已拒绝(终态)
|
||||
└────┬────┘ └──────────┘
|
||||
│ 开始执行
|
||||
▼
|
||||
┌──────────┐
|
||||
│executing │ 执行中
|
||||
└────┬─────┘
|
||||
│ 信息缺失
|
||||
┌────▼─────┐
|
||||
│info_needed│ 需要信息
|
||||
└────┬─────┘
|
||||
│ 信息补充
|
||||
▼
|
||||
回到 executing
|
||||
│
|
||||
├──────────────┐
|
||||
│ 需人类决策 │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌───────────┐
|
||||
│need_human│ │ 各种终态 │
|
||||
└────┬─────┘ └───────────┘
|
||||
│ 人类决策
|
||||
▼
|
||||
回到 executing
|
||||
│
|
||||
┌────┴────┬─────────┐
|
||||
▼ ▼ ▼
|
||||
completed failed cancelled
|
||||
(完成) (失败) (取消)
|
||||
```
|
||||
|
||||
### 7.2 终态
|
||||
- `completed`:成功完成
|
||||
- `failed`:执行失败
|
||||
- `cancelled`:被取消
|
||||
- `rejected`:被拒绝
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全模型总则
|
||||
|
||||
### 8.1 身份认证
|
||||
- 每个 Agent 有唯一身份
|
||||
- 支持 HMAC、JWT、OAuth2 等多种认证方式
|
||||
- 信任等级机制
|
||||
|
||||
### 8.2 消息完整性
|
||||
- 消息签名机制
|
||||
- 防止篡改与伪造
|
||||
|
||||
### 8.3 机密性
|
||||
- 支持端到端加密
|
||||
- 敏感载荷加密传输
|
||||
|
||||
### 8.4 访问控制
|
||||
- 基于信任等级的权限控制
|
||||
- 群组内基于角色的权限控制
|
||||
|
||||
---
|
||||
|
||||
## 9. 非目标
|
||||
|
||||
以下内容**不在**本原始协议的讨论范围内:
|
||||
|
||||
1. **具体传输协议的实现细节** - 由各传输层实现定义
|
||||
2. **特定领域的任务类型** - 由应用层或领域扩展定义
|
||||
3. **Agent 内部实现架构** - 仅定义对外接口与行为
|
||||
4. **具体编程语言绑定** - 语言无关,可多语言实现
|
||||
5. **UI/UX 设计** - 人机界面不在协议范围内
|
||||
|
||||
---
|
||||
|
||||
## 10. 参考实现
|
||||
|
||||
本协议族的参考实现位于:`dp-headless/a2a/`
|
||||
|
||||
| 模块 | 对应协议 | 文件路径 |
|
||||
|------|---------|---------|
|
||||
| types 包 | 公共类型定义 | `types/` |
|
||||
| p2p 包 | A2A 基础协议 | `p2p/` |
|
||||
| group 包 | A2N 组群协议 | `group/` |
|
||||
| externalapi 包 | P2A 调用协议 | `externalapi/` |
|
||||
| sdk 包 | P2A 客户端 SDK | `sdk/` |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:协议演进
|
||||
|
||||
本原始协议作为顶层指导,保持相对稳定。各具体协议可独立版本化演进,但需遵循本协议定义的核心原则。
|
||||
|
||||
**版本历史:**
|
||||
- v1.0.0:初始版本,定义核心概念与设计原则
|
||||
465
docs_backup/02-p2p-protocol.md
Normal file
465
docs_backup/02-p2p-protocol.md
Normal file
@@ -0,0 +1,465 @@
|
||||
# A2A 协议(Agent-to-Agent Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[A2A 原始协议 v1.0.0](./01-a2a-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
A2A 基础协议定义了两个 Agent 之间进行点对点(P2P)任务协作的完整规范,包括消息类型、交互流程、状态机、接口定义与错误处理。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 单个任务请求与响应
|
||||
- 任务状态实时同步
|
||||
- 任务取消
|
||||
- 信息补全交互
|
||||
- 人类介入决策
|
||||
|
||||
### 1.2 非适用场景
|
||||
- 多 Agent 群组协作(参见 A2N 组群协议)
|
||||
- 非 Agent 调用方接入(参见 P2A 调用协议)
|
||||
|
||||
---
|
||||
|
||||
## 2. 消息类型
|
||||
|
||||
### 2.1 消息类型清单
|
||||
|
||||
所有消息类型前缀:`p2p:`
|
||||
|
||||
| 消息类型 | 方向 | 说明 |
|
||||
|---------|------|------|
|
||||
| `p2p:task_request` | 请求方 → 执行方 | 提交任务请求 |
|
||||
| `p2p:task_confirm` | 执行方 → 请求方 | 确认接受任务 |
|
||||
| `p2p:task_reject` | 执行方 → 请求方 | 拒绝任务 |
|
||||
| `p2p:task_need_info` | 执行方 → 请求方 | 请求补充信息 |
|
||||
| `p2p:task_info_provided` | 请求方 → 执行方 | 提供补充信息 |
|
||||
| `p2p:task_status_update` | 执行方 → 请求方 | 任务进度更新 |
|
||||
| `p2p:task_complete` | 执行方 → 请求方 | 任务完成 |
|
||||
| `p2p:task_failed` | 执行方 → 请求方 | 任务失败 |
|
||||
| `p2p:task_cancel` | 请求方 → 执行方 | 取消任务 |
|
||||
| `p2p:task_ack` | 双向 | 任务级确认 |
|
||||
| `p2p:ack` | 双向 | 通用确认 |
|
||||
| `p2p:nack` | 双向 | 通用否定确认(错误) |
|
||||
| `p2p:heartbeat` | 双向 | 心跳保活 |
|
||||
| `p2p:capability_query` | 双向 | 查询对方能力 |
|
||||
| `p2p:capability_response` | 双向 | 返回能力清单 |
|
||||
| `p2p:task_need_human` | 执行方 → 请求方 | 请求人类介入 |
|
||||
| `p2p:human_decision` | 请求方 → 执行方 | 人类决策结果 |
|
||||
|
||||
### 2.2 消息类型完整定义
|
||||
|
||||
参见代码:[types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务状态机
|
||||
|
||||
### 3.1 状态定义
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `idle` | 空闲(初始状态) |
|
||||
| `pending` | 待确认(已发送请求,等待确认) |
|
||||
| `confirmed` | 已确认(对方接受任务) |
|
||||
| `executing` | 执行中 |
|
||||
| `info_needed` | 需要信息 |
|
||||
| `info_provided` | 信息已提供 |
|
||||
| `need_human` | 需要人类介入 |
|
||||
| `human_deciding` | 人类决策中 |
|
||||
| `human_approved` | 人类已批准 |
|
||||
| `human_rejected` | 人类已拒绝 |
|
||||
| `completed` | 已完成(终态) |
|
||||
| `failed` | 已失败(终态) |
|
||||
| `cancelled` | 已取消(终态) |
|
||||
| `rejected` | 已拒绝(终态) |
|
||||
|
||||
### 3.2 状态流转图
|
||||
|
||||
```
|
||||
请求方视角:
|
||||
|
||||
idle → pending → confirmed → executing → completed
|
||||
↓ ↓ → failed
|
||||
rejected info_needed → cancelled
|
||||
↑↓
|
||||
info_provided
|
||||
↓
|
||||
need_human
|
||||
↑↓
|
||||
human_decision
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心交互流程
|
||||
|
||||
### 4.1 正常任务提交流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->| 1. 提交任务
|
||||
| |
|
||||
|<------- task_confirm / task_reject -| 2. 确认/拒绝
|
||||
| |
|
||||
|<------- task_status_update --------| 3. 进度更新(多次)
|
||||
| |
|
||||
|<------- task_complete -------------| 4. 完成通知
|
||||
| |
|
||||
|-------- task_ack ----------------->| 5. 确认收到
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.2 信息补全流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|<------- task_need_info ------------| 发现信息缺失
|
||||
| |
|
||||
|-------- task_info_provided ------->| 补充信息
|
||||
| |
|
||||
|<------- task_status_update --------| 继续执行
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.3 人类介入流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|<------- task_need_human -----------| 需要人类决策
|
||||
| |
|
||||
| [人类做出决策] |
|
||||
| |
|
||||
|-------- human_decision ----------->| 决策结果
|
||||
| |
|
||||
|<------- task_status_update --------| 继续执行
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.4 任务取消流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|-------- task_cancel -------------->| 取消任务
|
||||
| |
|
||||
|<------- task_ack ------------------| 确认取消
|
||||
| |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口规范
|
||||
|
||||
### 5.1 P2P 协议处理器接口
|
||||
|
||||
**接口定义:**
|
||||
|
||||
```go
|
||||
type P2PProtocol interface {
|
||||
// 处理收到的消息
|
||||
HandleMessage(ctx context.Context, msg *UnifiedA2AMessage) (*UnifiedA2AMessage, error)
|
||||
|
||||
// 设置任务处理器
|
||||
SetTaskHandler(h TaskHandler)
|
||||
|
||||
// 设置回调发送器
|
||||
SetCallbackSender(s CallbackSender)
|
||||
|
||||
// 获取 Agent 端点
|
||||
AgentEndpoint() AgentEndpoint
|
||||
|
||||
// 查询任务状态
|
||||
GetTaskStatus(taskID string) (P2PTaskStatus, bool)
|
||||
|
||||
// 列出所有任务
|
||||
ListTasks() map[string]P2PTaskStatus
|
||||
}
|
||||
```
|
||||
|
||||
**参考实现:** [p2p/protocol.go](../p2p/protocol.go)
|
||||
|
||||
### 5.2 任务处理器接口
|
||||
|
||||
```go
|
||||
type TaskHandler interface {
|
||||
// 执行任务并返回结果
|
||||
ExecuteTask(ctx context.Context, taskType string, payload map[string]interface{}) (any, error)
|
||||
|
||||
// 验证任务是否可执行,返回缺失字段列表和是否可处理
|
||||
ValidateTask(taskType string, payload map[string]interface{}) ([]MissingField, bool)
|
||||
|
||||
// 获取能力列表
|
||||
GetCapabilities() []string
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `ExecuteTask`:实际执行业务逻辑的入口
|
||||
- `ValidateTask`:在任务确认前调用,用于判断是否接受任务或需要补充信息
|
||||
- `GetCapabilities`:返回 Agent 支持的任务类型列表
|
||||
|
||||
### 5.3 回调发送器接口
|
||||
|
||||
```go
|
||||
type CallbackSender interface {
|
||||
// 向远程 Agent 发送消息
|
||||
SendCallback(ctx context.Context, endpoint string, msg *UnifiedA2AMessage) error
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 传输层实现此接口
|
||||
- 支持多种传输方式(HTTP、gRPC、WebSocket 等)
|
||||
|
||||
### 5.4 配置项
|
||||
|
||||
```go
|
||||
type P2PConfig struct {
|
||||
MaxRetries int // 最大重试次数(默认 5)
|
||||
RetryBaseDelay time.Duration // 重试基础延迟(默认 2s,指数退避)
|
||||
TaskTimeout time.Duration // 任务执行超时(默认 5 分钟)
|
||||
HeartbeatInterval time.Duration // 心跳间隔(默认 30 秒)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 载荷格式
|
||||
|
||||
### 6.1 TaskRequestPayload(任务请求)
|
||||
|
||||
```json
|
||||
{
|
||||
"task": {
|
||||
"id": "task-xxx",
|
||||
"type": "text_generation",
|
||||
"priority": "normal",
|
||||
"deadline": "2026-01-01T00:00:00Z",
|
||||
"payload": {
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"session_id": "sess-xxx",
|
||||
"parent_task_id": "parent-xxx",
|
||||
"correlation_id": "corr-xxx",
|
||||
"metadata": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 TaskConfirmPayload(任务确认)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"confirmed": true,
|
||||
"estimated_duration": 120,
|
||||
"expected_completion": "2026-01-01T00:02:00Z",
|
||||
"capabilities_used": ["text_generation"],
|
||||
"constraints": {
|
||||
"max_retries": 3,
|
||||
"fallback_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 TaskRejectPayload(任务拒绝)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"reason": {
|
||||
"code": "capability_lacking",
|
||||
"message": "不支持该任务类型",
|
||||
"details": null
|
||||
},
|
||||
"alternatives": {
|
||||
"suggested_agent_id": "other-agent",
|
||||
"retry_after": 300,
|
||||
"partial_support": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 TaskStatusUpdatePayload(状态更新)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"status": "executing",
|
||||
"progress": 50,
|
||||
"message": "正在生成内容",
|
||||
"intermediate_result": null
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 TaskCompletePayload(任务完成)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"result": {
|
||||
"output_type": "text",
|
||||
"output": "生成的文本内容...",
|
||||
"metadata": {
|
||||
"tokens": 1024,
|
||||
"model": "gpt-4"
|
||||
}
|
||||
},
|
||||
"summary": "任务已完成"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 TaskFailedPayload(任务失败)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"error": {
|
||||
"code": "execution_error",
|
||||
"message": "模型调用超时",
|
||||
"recoverable": true,
|
||||
"details": null
|
||||
},
|
||||
"partial_result": null
|
||||
}
|
||||
```
|
||||
|
||||
### 6.7 TaskNeedInfoPayload(需要信息)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"missing_fields": [
|
||||
{
|
||||
"field": "style",
|
||||
"reason": "未指定生成风格",
|
||||
"suggested_value": "古典",
|
||||
"alternatives": ["现代", "浪漫"]
|
||||
}
|
||||
],
|
||||
"question": "请选择生成风格",
|
||||
"required_by": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 TaskNeedHumanPayload(需要人类)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"reason": {
|
||||
"code": "ethical_decision",
|
||||
"message": "涉及伦理决策需要人工审核"
|
||||
},
|
||||
"context": {
|
||||
"task_summary": "生成一张人物肖像",
|
||||
"current_progress": 30,
|
||||
"blocked_reason": "需要确认肖像使用授权"
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"field": "authorized",
|
||||
"value": true,
|
||||
"pros_cons": "继续生成,假设已授权",
|
||||
"confidence": 70
|
||||
}
|
||||
],
|
||||
"escalation_level": 1,
|
||||
"deadline": "2026-01-01T00:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.9 HumanDecisionPayload(人类决策)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"decision_id": "dec-xxx",
|
||||
"reply_to": "msg-xxx",
|
||||
"decided_by": {
|
||||
"user_id": "user-123",
|
||||
"user_name": "张三",
|
||||
"role": "reviewer"
|
||||
},
|
||||
"decision": {
|
||||
"action": "approve",
|
||||
"approved_fields": {
|
||||
"authorized": true
|
||||
},
|
||||
"rejection_reason": ""
|
||||
},
|
||||
"notes": "已确认授权",
|
||||
"timestamp": "2026-01-01T00:03:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**完整载荷定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误码
|
||||
|
||||
| 错误码 | 说明 | HTTP 状态码(如适用) |
|
||||
|--------|------|---------------------|
|
||||
| `message_format_invalid` | 消息格式无效 | 400 |
|
||||
| `capability_lacking` | 能力不足 | 403 |
|
||||
| `agent_not_ready` | Agent 未就绪 | 503 |
|
||||
| `task_not_found` | 任务不存在 | 404 |
|
||||
| `task_already_completed` | 任务已完成 | 409 |
|
||||
| `execution_timeout` | 执行超时 | 504 |
|
||||
| `execution_failed` | 执行失败 | 500 |
|
||||
| `permission_denied` | 权限不足 | 403 |
|
||||
| `invalid_params` | 参数无效 | 400 |
|
||||
| `rate_limited` | 限流 | 429 |
|
||||
|
||||
**完整错误定义:** [types/errors.go](../types/errors.go)
|
||||
|
||||
---
|
||||
|
||||
## 8. 传输要求
|
||||
|
||||
### 8.1 HTTP 传输
|
||||
- Content-Type: `application/json`
|
||||
- 请求方法:`POST`
|
||||
- 端点路径:由 Agent 自行定义,建议 `/a2a/callback`
|
||||
- 超时:建议 30 秒
|
||||
|
||||
### 8.2 签名验证(可选)
|
||||
- 使用 HMAC-SHA256 签名
|
||||
- 签名放在 Header:`X-A2A-Signature`
|
||||
- 或放在消息体:`signature` 字段
|
||||
|
||||
---
|
||||
|
||||
## 9. 参考实现
|
||||
|
||||
本协议的参考实现位于 `p2p/` 目录:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [p2p/protocol.go](../p2p/protocol.go) | P2P 协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 数据类型定义 |
|
||||
| [types/errors.go](../types/errors.go) | 错误定义 |
|
||||
| [transport/transport.go](../transport/transport.go) | HTTP 传输实现 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:能力声明格式
|
||||
|
||||
参见 [A2A 原始协议 - Capability](./01-a2a-raw-protocol.md#44-capability能力)
|
||||
545
docs_backup/03-group-protocol.md
Normal file
545
docs_backup/03-group-protocol.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# A2N 协议(Agent-to-N Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[A2A 原始协议 v1.0.0](./01-a2a-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
A2N(Agent-to-N)组群协议定义了多个 Agent 组成群组进行协同工作的完整规范,包括群组管理、任务拆分与合并、群体决策、角色与权限等。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 复杂任务的拆分与并行执行(MapReduce 模式)
|
||||
- 多 Agent 协同完成单一目标
|
||||
- 群体投票与共识决策
|
||||
- 临时或永久的 Agent 工作组
|
||||
- 分级升级(escalation)机制
|
||||
|
||||
### 1.2 依赖
|
||||
- 依赖 A2A 基础协议的点对点通信能力
|
||||
- 群组内成员间的单播通信使用 A2A 基础协议
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念
|
||||
|
||||
### 2.1 Group(群组)
|
||||
由多个 Agent 组成的协作单元,具有明确的拓扑结构、角色分工与协作规则。
|
||||
|
||||
### 2.2 Topology(拓扑结构)
|
||||
|
||||
| 拓扑类型 | 说明 | 适用场景 |
|
||||
|---------|------|---------|
|
||||
| `star` | 星型,中心 coordinator 协调 | 简单任务分发 |
|
||||
| `mesh` | 网状,成员间可直接通信 | 对等协作 |
|
||||
| `chain` | 链式,流水线模式 | 顺序处理 |
|
||||
| `hierarchical` | 层级型,多层管理 | 大型复杂任务 |
|
||||
| `fullmesh` | 全连接网状 | 高可靠协作 |
|
||||
|
||||
### 2.3 Role(角色)
|
||||
|
||||
| 角色 | 说明 | 权限 |
|
||||
|------|------|------|
|
||||
| `coordinator` | 协调者 | 创建/解散群组、任务拆分、发起投票 |
|
||||
| `worker` | 工作者 | 执行子任务、提交结果 |
|
||||
| `reviewer` | 审核者 | 审核结果、发起投票 |
|
||||
| `observer` | 观察者 | 只读,查看进度 |
|
||||
|
||||
### 2.4 GroupStatus(群组状态)
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `creating` | 创建中 |
|
||||
| `active` | 活跃 |
|
||||
| `frozen` | 冻结(不可执行新任务) |
|
||||
| `disbanded` | 已解散(终态) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 消息类型
|
||||
|
||||
### 3.1 消息类型清单
|
||||
|
||||
所有消息类型前缀:`group:` 或 `inter_group:`
|
||||
|
||||
#### 群组管理
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:create` | 创建群组 |
|
||||
| `group:disband` | 解散群组 |
|
||||
| `group:join` | 加入群组 |
|
||||
| `group:leave` | 离开群组 |
|
||||
| `group:invite` | 邀请加入 |
|
||||
| `group:accept_invite` | 接受邀请 |
|
||||
| `group:reject_invite` | 拒绝邀请 |
|
||||
| `group:member_update` | 成员更新通知 |
|
||||
| `group:role_change` | 角色变更 |
|
||||
|
||||
#### 群组通信
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:broadcast` | 广播(所有成员) |
|
||||
| `group:multicast` | 组播(指定成员) |
|
||||
| `group:direct` | 群内单播 |
|
||||
| `group:relay` | 消息中继 |
|
||||
|
||||
#### 任务协作
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:task_split` | 任务拆分 |
|
||||
| `group:task_assign` | 任务分配 |
|
||||
| `group:task_merge` | 结果合并 |
|
||||
| `group:task_progress` | 子任务进度 |
|
||||
|
||||
#### 共识与投票
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:vote` | 发起投票 |
|
||||
| `group:vote_submit` | 提交投票 |
|
||||
| `group:consensus` | 达成共识 |
|
||||
|
||||
#### 人在回路(HiL)
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:consensus_escalate` | 共识僵局升级给人类 |
|
||||
| `group:subtask_escalate` | 子任务阻塞升级给人类 |
|
||||
| `group:human_decision` | 人类决策结果 |
|
||||
| `inter_group:escalate` | 跨群组升级 |
|
||||
|
||||
**完整定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 4. 群组生命周期
|
||||
|
||||
### 4.1 状态机
|
||||
|
||||
```
|
||||
creating → active → frozen → disbanded
|
||||
↑ ↓
|
||||
└─────────┘
|
||||
解冻
|
||||
```
|
||||
|
||||
### 4.2 群组创建流程
|
||||
|
||||
```
|
||||
创建者 被邀请成员
|
||||
| |
|
||||
|------ group:invite -------------->| 发送邀请
|
||||
| |
|
||||
|<----- group:accept_invite --------| 接受邀请
|
||||
| |
|
||||
|------ group:member_update ------->| 通知成员更新
|
||||
| |
|
||||
[群组创建完成,状态变为 active] |
|
||||
```
|
||||
|
||||
### 4.3 群组解散流程
|
||||
|
||||
```
|
||||
coordinator 所有成员
|
||||
| |
|
||||
|------ group:disband ------------->| 通知解散
|
||||
| |
|
||||
[群组状态变为 disbanded] |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务协作流程
|
||||
|
||||
### 5.1 MapReduce 模式(拆分-执行-合并)
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ coordinator │
|
||||
└──────┬───────┘
|
||||
│
|
||||
group:task_split(拆分任务)
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ worker1 │ │ worker2 │ │ worker3 │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
│ 执行子任务 │ 执行子任务 │ 执行子任务
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
子任务结果 子任务结果 子任务结果
|
||||
└────────────────┼────────────────┘
|
||||
│
|
||||
group:task_merge(合并结果)
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ 最终结果 │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### 5.2 详细流程
|
||||
|
||||
1. **任务拆分阶段**
|
||||
- coordinator 接收原始任务
|
||||
- 将任务拆分为多个子任务(subtask)
|
||||
- 为每个子任务指定执行的 worker
|
||||
- 定义合并策略(merge strategy)
|
||||
|
||||
2. **并行执行阶段**
|
||||
- 各 worker 并行执行子任务
|
||||
- 定期上报进度(`group:task_progress`)
|
||||
- 子任务完成后上报结果
|
||||
|
||||
3. **结果合并阶段**
|
||||
- 所有子任务完成后,coordinator 合并结果
|
||||
- 根据合并策略决定最终结果
|
||||
- 输出最终结果
|
||||
|
||||
---
|
||||
|
||||
## 6. 合并策略(Merge Strategy)
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| `all_success` | 全部成功才认为成功,否则失败 |
|
||||
| `majority` | 多数子任务成功即可,取多数结果 |
|
||||
| `any_complete` | 任意一个完成即返回结果 |
|
||||
| `priority` | 按优先级,高优先级子任务结果优先 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 投票与共识
|
||||
|
||||
### 7.1 投票流程
|
||||
|
||||
```
|
||||
发起者 投票者
|
||||
| |
|
||||
|-------- group:vote -------------->| 发起投票
|
||||
| |
|
||||
|<------- group:vote_submit --------| 提交投票
|
||||
| |
|
||||
[计票,判断是否达到阈值] |
|
||||
| |
|
||||
|-------- group:consensus --------->| 公布结果
|
||||
| |
|
||||
```
|
||||
|
||||
### 7.2 投票规则
|
||||
|
||||
- `threshold`:通过阈值(0-1),如 0.5 表示过半
|
||||
- `deadline`:投票截止时间
|
||||
- `voters`:有投票权的成员列表
|
||||
- 超时未投票视为弃权
|
||||
|
||||
### 7.3 共识僵局(Consensus Deadlock)
|
||||
|
||||
当投票无法达成共识(如平票、未达阈值且超时)时,可升级给人类决策。
|
||||
|
||||
---
|
||||
|
||||
## 8. 人在回路(HiL)
|
||||
|
||||
群组协作中有两种升级场景:
|
||||
|
||||
### 8.1 共识僵局升级
|
||||
投票无法达成共识时,升级给人类裁决。
|
||||
|
||||
**消息:** `group:consensus_escalate`
|
||||
|
||||
### 8.2 子任务阻塞升级
|
||||
某个子任务遇到无法自动解决的问题时,升级给人类。
|
||||
|
||||
**消息:** `group:subtask_escalate`
|
||||
|
||||
### 8.3 跨群组升级
|
||||
问题超出当前群组能力时,可升级到上级群组或人类。
|
||||
|
||||
**消息:** `inter_group:escalate`
|
||||
|
||||
---
|
||||
|
||||
## 9. 接口规范
|
||||
|
||||
### 9.1 GroupProtocol 接口
|
||||
|
||||
```go
|
||||
type GroupProtocol interface {
|
||||
// 处理群组消息
|
||||
HandleMessage(ctx context.Context, msg *UnifiedA2AMessage) (*UnifiedA2AMessage, error)
|
||||
|
||||
// 设置任务处理器
|
||||
SetTaskHandler(h GroupTaskHandler)
|
||||
|
||||
// 查询群组列表
|
||||
GetGroups() []*AgentGroup
|
||||
|
||||
// 查询特定群组
|
||||
GetGroup(groupID string) (*AgentGroup, bool)
|
||||
}
|
||||
```
|
||||
|
||||
**参考实现:** [group/protocol.go](../group/protocol.go)
|
||||
|
||||
### 9.2 GroupTaskHandler 接口
|
||||
|
||||
```go
|
||||
type GroupTaskHandler interface {
|
||||
// 执行子任务
|
||||
ExecuteSubtask(ctx context.Context, subtaskID string, payload map[string]interface{}) (any, error)
|
||||
|
||||
// 合并多个子任务结果
|
||||
MergeResults(results []SubtaskResult, strategy MergeStrategy) (any, error)
|
||||
|
||||
// 验证任务是否可拆分,返回拆分方案
|
||||
ValidateTaskSplit(taskType string, requirements map[string]interface{}) ([]Subtask, bool)
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `ExecuteSubtask`:执行单个子任务的业务逻辑
|
||||
- `MergeResults`:根据策略合并多个子任务的结果
|
||||
- `ValidateTaskSplit`:验证任务是否可拆分,并给出拆分方案
|
||||
|
||||
### 9.3 配置项
|
||||
|
||||
```go
|
||||
type GroupConfig struct {
|
||||
MaxGroupSize int // 最大群组人数(默认 20)
|
||||
MaxSubtasks int // 最大子任务数(默认 50)
|
||||
ConsensusTimeout time.Duration // 共识超时(默认 5 分钟)
|
||||
TaskTimeout time.Duration // 任务超时(默认 10 分钟)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 载荷格式
|
||||
|
||||
### 10.1 GroupCreatePayload(创建群组)
|
||||
|
||||
```json
|
||||
{
|
||||
"group": {
|
||||
"name": "视频制作组",
|
||||
"topology": "star",
|
||||
"config": {
|
||||
"allow_self_leave": true,
|
||||
"requires_approval": false,
|
||||
"consensus_required": false,
|
||||
"consensus_threshold": 0.5,
|
||||
"broadcast_mode": "all"
|
||||
},
|
||||
"initial_members": [
|
||||
{
|
||||
"agent_id": "worker-1",
|
||||
"system_id": "sys-a",
|
||||
"endpoint": "http://worker1/a2a"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"id": "coordinator",
|
||||
"name": "协调者",
|
||||
"permissions": ["create", "disband", "split", "vote"],
|
||||
"max_tasks": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 GroupTaskSplitPayload(任务拆分)
|
||||
|
||||
```json
|
||||
{
|
||||
"original_task": {
|
||||
"id": "task-xxx",
|
||||
"type": "video_generation",
|
||||
"requirements": {
|
||||
"script": "xxx",
|
||||
"duration": 60
|
||||
},
|
||||
"deadline": "2026-01-01T00:10:00Z"
|
||||
},
|
||||
"subtasks": [
|
||||
{
|
||||
"id": "sub-1",
|
||||
"description": "生成脚本",
|
||||
"assigned_to": "writer-agent",
|
||||
"dependencies": [],
|
||||
"estimated_duration": 120,
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"id": "sub-2",
|
||||
"description": "生成配音",
|
||||
"assigned_to": "voice-agent",
|
||||
"dependencies": ["sub-1"],
|
||||
"estimated_duration": 180,
|
||||
"priority": 2
|
||||
}
|
||||
],
|
||||
"merge_strategy": "all_success"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 GroupTaskMergePayload(结果合并)
|
||||
|
||||
```json
|
||||
{
|
||||
"original_task_id": "task-xxx",
|
||||
"subtask_results": [
|
||||
{
|
||||
"subtask_id": "sub-1",
|
||||
"assigned_to": "writer-agent",
|
||||
"status": "success",
|
||||
"result": { "script": "..." },
|
||||
"completed_at": "2026-01-01T00:02:00Z"
|
||||
}
|
||||
],
|
||||
"merge_strategy_used": "all_success",
|
||||
"merged_result": {
|
||||
"video_url": "http://..."
|
||||
},
|
||||
"consensus_achieved": true
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 GroupVotePayload(投票)
|
||||
|
||||
```json
|
||||
{
|
||||
"vote_id": "vote-xxx",
|
||||
"question": "是否采用方案A?",
|
||||
"options": ["方案A", "方案B", "方案C"],
|
||||
"voters": ["agent-1", "agent-2", "agent-3"],
|
||||
"deadline": "2026-01-01T00:05:00Z",
|
||||
"threshold": 0.66,
|
||||
"status": "open",
|
||||
"votes": []
|
||||
}
|
||||
```
|
||||
|
||||
### 10.5 GroupConsensusEscalatePayload(共识升级)
|
||||
|
||||
```json
|
||||
{
|
||||
"vote_id": "vote-xxx",
|
||||
"escalation_id": "esc-xxx",
|
||||
"question": "是否采用方案A?",
|
||||
"votes": [
|
||||
{
|
||||
"voter_id": "agent-1",
|
||||
"voter_role": "reviewer",
|
||||
"option": "方案A",
|
||||
"reason": "质量更好"
|
||||
}
|
||||
],
|
||||
"consensus_status": {
|
||||
"required_threshold": 0.66,
|
||||
"actual_threshold": 0.33,
|
||||
"leading_option": "方案A",
|
||||
"options_tied": false
|
||||
},
|
||||
"escalate_reason": {
|
||||
"code": "no_consensus",
|
||||
"message": "无法达成共识"
|
||||
},
|
||||
"escalation_level": 2,
|
||||
"deadline": "2026-01-01T00:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**完整载荷定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 11. 数据结构
|
||||
|
||||
### 11.1 AgentGroup(群组)
|
||||
|
||||
```go
|
||||
type AgentGroup struct {
|
||||
ID string
|
||||
Name string
|
||||
Topology GroupTopology
|
||||
CreatedBy string
|
||||
CreatedAt Timestamp
|
||||
Config GroupConfig
|
||||
Members []GroupMember
|
||||
Roles []GroupRole
|
||||
Status GroupStatus
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 GroupMember(群组成员)
|
||||
|
||||
```go
|
||||
type GroupMember struct {
|
||||
AgentID string
|
||||
AgentName string
|
||||
Role string
|
||||
JoinedAt Timestamp
|
||||
Status GroupMemberStatus
|
||||
Capabilities []string
|
||||
LoadFactor float64 // 负载因子 0-1,用于任务分配
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 Subtask(子任务)
|
||||
|
||||
```go
|
||||
type Subtask struct {
|
||||
ID string
|
||||
Description string
|
||||
AssignedTo string
|
||||
Dependencies []string
|
||||
EstimatedDuration int
|
||||
Priority int
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 错误码
|
||||
|
||||
群组协议特有错误码:
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| `group_not_found` | 群组不存在 |
|
||||
| `group_not_active` | 群组不活跃 |
|
||||
| `group_full` | 群组已满 |
|
||||
| `already_member` | 已是成员 |
|
||||
| `not_group_member` | 不是群组成员 |
|
||||
| `member_not_found` | 成员不存在 |
|
||||
| `permission_denied` | 权限不足 |
|
||||
| `invalid_params` | 参数无效 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 参考实现
|
||||
|
||||
本协议的参考实现位于 `group/` 目录:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [group/protocol.go](../group/protocol.go) | 群组协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 群组相关数据类型 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:与 A2A 基础协议的关系
|
||||
|
||||
- 群组内的点对点通信使用 A2A 基础协议
|
||||
- 群组协议在 A2A 基础协议之上提供群组管理、任务拆分合并、共识投票等能力
|
||||
- 群组消息同样使用统一消息信封(`UnifiedA2AMessage`),通过 `group_id` 字段标识群组上下文
|
||||
503
docs_backup/04-caller-protocol.md
Normal file
503
docs_backup/04-caller-protocol.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# P2A 协议(Person/Program-to-Agent Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[A2A 原始协议 v1.0.0](./01-a2a-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
P2A(Person/Program-to-Agent)调用协议定义了**非 Agent 调用方**(如普通后端服务、前端应用、第三方系统、人类用户)如何调用 Agent 能力的规范。
|
||||
|
||||
本协议将 A2A 协议的复杂性封装起来,提供简洁的 RESTful API,使调用方无需了解 A2A 协议细节即可使用 Agent 能力。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 前端应用直接调用 Agent 能力
|
||||
- 后端业务系统集成 Agent 服务
|
||||
- 第三方开发者通过 API 接入
|
||||
- 无需实现完整 A2A 协议的调用方
|
||||
|
||||
### 1.2 设计原则
|
||||
- **简单易用**:标准 RESTful API,JSON 格式
|
||||
- **两种模式**:回调模式 + 轮询模式,调用方按需选择
|
||||
- **认证简单**:API Key 认证,无需 HMAC 签名
|
||||
- **文件支持**:支持 multipart/form-data 上传文件
|
||||
- **错误友好**:清晰的错误码和错误信息
|
||||
|
||||
### 1.3 与 A2A 基础协议的关系
|
||||
|
||||
```
|
||||
外部调用方 ──P2A 协议──▶ External API Layer ──A2A 基础协议──▶ Agent
|
||||
```
|
||||
|
||||
P2A 协议是 A2A 协议的"简化入口",所有请求最终会转换为 A2A 消息发送给 Agent。
|
||||
|
||||
---
|
||||
|
||||
## 2. 认证方式
|
||||
|
||||
### 2.1 API Key 认证
|
||||
- 在 HTTP Header 中传递:`X-API-Key: {api_key}`
|
||||
- 或在 URL Query 中传递:`?api_key={api_key}`
|
||||
- 如果服务端未配置 API Key,则跳过认证
|
||||
|
||||
### 2.2 回调验证(可选)
|
||||
如果调用方提供了 `callback_token`,则服务端在回调时会在 Header 中带上 `X-Callback-Token`,用于调用方验证回调来源。
|
||||
|
||||
---
|
||||
|
||||
## 3. API 端点
|
||||
|
||||
所有端点默认前缀:`/api/v1/a2a`
|
||||
|
||||
### 3.1 端点清单
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/task` | 提交任务(JSON) |
|
||||
| POST | `/task/multipart` | 提交任务(带文件上传) |
|
||||
| GET | `/task/{task_id}` | 获取任务详情 |
|
||||
| GET | `/task/{task_id}/status` | 获取任务状态(简化版) |
|
||||
| GET | `/tasks` | 获取任务列表 |
|
||||
| POST | `/task/{task_id}/cancel` | 取消任务 |
|
||||
| GET | `/capabilities` | 获取能力清单 |
|
||||
| POST | `/callback` | 接收回调(调用方实现) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 任务提交
|
||||
|
||||
### 4.1 提交任务(JSON)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/a2a/task
|
||||
Content-Type: application/json
|
||||
X-API-Key: your-api-key
|
||||
|
||||
{
|
||||
"task_type": "text_generation",
|
||||
"payload": {
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典"
|
||||
},
|
||||
"external_task_id": "my-task-001",
|
||||
"priority": "normal",
|
||||
"callback_url": "https://your-domain.com/callback",
|
||||
"callback_token": "your-callback-token"
|
||||
}
|
||||
```
|
||||
|
||||
**请求字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `task_type` | string | 是 | 任务类型,如 `video_generation`, `image_generation` |
|
||||
| `payload` | object | 是 | 任务参数,具体结构取决于 task_type |
|
||||
| `external_task_id` | string | 否 | 调用方的任务 ID,用于关联 |
|
||||
| `priority` | string | 否 | 优先级:`low`, `normal`, `high`, `urgent`,默认 `normal` |
|
||||
| `callback_url` | string | 否 | 回调 URL,不提供则需轮询 |
|
||||
| `callback_token` | string | 否 | 回调验证 token |
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "received",
|
||||
"message": "任务已接收"
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 是否成功 |
|
||||
| `task_id` | string | 服务端生成的任务 ID |
|
||||
| `status` | string | 初始状态,通常为 `received` |
|
||||
| `message` | string | 提示信息 |
|
||||
| `error` | string | 错误信息(失败时) |
|
||||
|
||||
### 4.2 提交任务(Multipart 带文件)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/a2a/task/multipart
|
||||
Content-Type: multipart/form-data
|
||||
X-API-Key: your-api-key
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="task_type"
|
||||
|
||||
image_generation
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="payload"
|
||||
|
||||
{"prompt":"一只猫","style":"写实"}
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="priority"
|
||||
|
||||
normal
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="file"; filename="reference.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
[二进制数据]
|
||||
|
||||
------WebKitFormBoundary--
|
||||
```
|
||||
|
||||
**表单字段:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `task_type` | string | 是 | 任务类型 |
|
||||
| `payload` | string(JSON) | 是 | 任务参数(JSON 字符串) |
|
||||
| `external_task_id` | string | 否 | 外部任务 ID |
|
||||
| `priority` | string | 否 | 优先级 |
|
||||
| `callback_url` | string | 否 | 回调 URL |
|
||||
| `callback_token` | string | 否 | 回调 token |
|
||||
| `file` | file | 否 | 上传的文件(可多个) |
|
||||
|
||||
**响应:** 同 JSON 提交方式
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务查询
|
||||
|
||||
### 5.1 获取任务详情
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/a2a/task/{task_id}
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task": {
|
||||
"id": "task-abc123",
|
||||
"type": "text_generation",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "任务已完成",
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:01:00Z",
|
||||
"completed_at": "2026-01-01T00:01:00Z",
|
||||
"eta": "2026-01-01T00:01:00Z",
|
||||
"result": {
|
||||
"text": "生成的文本...",
|
||||
"metadata": { "tokens": 1024 }
|
||||
},
|
||||
"error": null,
|
||||
"external_task_id": "my-task-001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**任务状态:**
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `received` | 已接收 |
|
||||
| `processing` | 处理中 |
|
||||
| `completed` | 已完成 |
|
||||
| `failed` | 已失败 |
|
||||
| `cancelled` | 已取消 |
|
||||
|
||||
### 5.2 获取任务状态(简化版)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/a2a/task/{task_id}/status
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "processing",
|
||||
"progress": 50
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 获取任务列表
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/a2a/tasks?status=processing&limit=20&offset=0
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `status` | string | 否 | 按状态过滤 |
|
||||
| `limit` | int | 否 | 每页数量,默认 20 |
|
||||
| `offset` | int | 否 | 偏移量,默认 0 |
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-abc123",
|
||||
"type": "text_generation",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 任务取消
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/a2a/task/{task_id}/cancel
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "cancelled",
|
||||
"message": "任务已取消"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 能力查询
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/a2a/capabilities
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"capabilities": {
|
||||
"agent_id": "my-agent",
|
||||
"agent_name": "我的Agent",
|
||||
"system_id": "my-system",
|
||||
"protocol_version": "1.0.0",
|
||||
"supported_task_types": [
|
||||
{
|
||||
"type": "text_generation",
|
||||
"name": "文本生成",
|
||||
"description": "生成各种类型的文本内容",
|
||||
"estimated_time": "30s",
|
||||
"input_schema": "...",
|
||||
"output_schema": "..."
|
||||
}
|
||||
],
|
||||
"supported_transport_modes": ["callback", "polling"],
|
||||
"max_file_size": 104857600,
|
||||
"max_concurrent_tasks": 10,
|
||||
"support_progress_updates": true,
|
||||
"support_streaming": false,
|
||||
"support_hil": true,
|
||||
"support_groups": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 回调机制
|
||||
|
||||
如果调用方提供了 `callback_url`,服务端会在任务状态变化时主动回调。
|
||||
|
||||
### 8.1 回调请求
|
||||
|
||||
```http
|
||||
POST {callback_url}
|
||||
Content-Type: application/json
|
||||
X-Callback-Token: {callback_token}
|
||||
|
||||
{
|
||||
"task_id": "task-abc123",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "任务已完成",
|
||||
"result": {
|
||||
"text": "生成的文本..."
|
||||
},
|
||||
"error": null,
|
||||
"timestamp": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 回调类型
|
||||
|
||||
| 触发时机 | 说明 |
|
||||
|---------|------|
|
||||
| 任务开始处理 | `status=processing`, `progress=0` |
|
||||
| 进度更新 | `status=processing`, `progress=N` |
|
||||
| 任务完成 | `status=completed`, `progress=100`, 包含 result |
|
||||
| 任务失败 | `status=failed`, 包含 error |
|
||||
| 任务取消 | `status=cancelled` |
|
||||
|
||||
### 8.3 回调响应
|
||||
|
||||
调用方应返回 2xx 状态码表示成功接收。如果返回非 2xx,服务端会重试(指数退避,最多 5 次)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 错误处理
|
||||
|
||||
### 9.1 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "invalid_task_type",
|
||||
"message": "不支持的任务类型"
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 错误码列表
|
||||
|
||||
| 错误码 | HTTP 状态码 | 说明 |
|
||||
|--------|------------|------|
|
||||
| `unauthorized` | 401 | API Key 无效或缺失 |
|
||||
| `invalid_request` | 400 | 请求格式无效 |
|
||||
| `invalid_task_type` | 400 | 任务类型不支持 |
|
||||
| `invalid_params` | 400 | 参数无效 |
|
||||
| `task_not_found` | 404 | 任务不存在 |
|
||||
| `task_already_completed` | 409 | 任务已完成,无法取消 |
|
||||
| `file_too_large` | 413 | 文件过大 |
|
||||
| `rate_limited` | 429 | 请求过于频繁 |
|
||||
| `internal_error` | 500 | 服务内部错误 |
|
||||
| `service_unavailable` | 503 | 服务不可用 |
|
||||
|
||||
---
|
||||
|
||||
## 10. SDK 客户端接口
|
||||
|
||||
为方便调用方集成,提供 SDK 客户端。
|
||||
|
||||
### 10.1 SDK 初始化
|
||||
|
||||
```go
|
||||
client := sdk.NewClient(sdk.ClientConfig{
|
||||
BaseURL: "https://agent-api.example.com",
|
||||
APIKey: "your-api-key",
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
```
|
||||
|
||||
### 10.2 SDK 方法清单
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `SubmitTask(ctx, req)` | 提交任务(JSON) |
|
||||
| `SubmitTaskWithFiles(ctx, req, files)` | 提交任务(带文件) |
|
||||
| `GetTask(ctx, taskID)` | 获取任务详情 |
|
||||
| `GetTaskStatus(ctx, taskID)` | 获取任务状态 |
|
||||
| `ListTasks(ctx, status, limit, offset)` | 获取任务列表 |
|
||||
| `CancelTask(ctx, taskID)` | 取消任务 |
|
||||
| `GetCapabilities(ctx)` | 获取能力清单 |
|
||||
| `WaitForTask(ctx, taskID, interval)` | 等待任务完成(轮询) |
|
||||
|
||||
### 10.3 数据结构
|
||||
|
||||
```go
|
||||
type TaskRequest struct {
|
||||
TaskType string
|
||||
Payload map[string]any
|
||||
ExternalTaskID string
|
||||
Priority string
|
||||
CallbackURL string
|
||||
CallbackToken string
|
||||
}
|
||||
|
||||
type TaskInfo struct {
|
||||
ID string
|
||||
Type string
|
||||
Status string
|
||||
Progress int
|
||||
Message string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
CompletedAt string
|
||||
ETA string
|
||||
Result map[string]any
|
||||
Error *TaskError
|
||||
ExternalTaskID string
|
||||
}
|
||||
```
|
||||
|
||||
**完整 SDK 实现:** [sdk/client.go](../sdk/client.go)
|
||||
|
||||
---
|
||||
|
||||
## 11. 参考实现
|
||||
|
||||
本协议的参考实现:
|
||||
|
||||
| 模块 | 文件路径 | 说明 |
|
||||
|------|---------|------|
|
||||
| 服务端 | [externalapi/handler.go](../externalapi/handler.go) | External API Handler |
|
||||
| 客户端 SDK | [sdk/client.go](../sdk/client.go) | Go 语言 SDK |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:调用模式选择
|
||||
|
||||
### 回调模式 vs 轮询模式
|
||||
|
||||
| 对比项 | 回调模式 | 轮询模式 |
|
||||
|--------|---------|---------|
|
||||
| 实时性 | 高,状态变化即时通知 | 低,取决于轮询间隔 |
|
||||
| 实现复杂度 | 调用方需提供公网可访问的回调接口 | 简单,只需调用 GET 接口 |
|
||||
| 网络要求 | 调用方需要公网可达 | 只需调用方能访问服务端 |
|
||||
| 适用场景 | 生产环境、需要实时通知 | 开发测试、内网环境 |
|
||||
|
||||
### 选择建议
|
||||
|
||||
- **有公网服务器**:使用回调模式
|
||||
- **本地开发 / 内网环境**:使用轮询模式
|
||||
- **长时间任务**:建议回调模式,避免频繁轮询
|
||||
- **短任务(<10s)**:可直接同步等待或高频轮询
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:从 A2A 协议到 P2A 协议的映射
|
||||
|
||||
| A2A 概念 | P2A 对应 | 说明 |
|
||||
|---------|---------|------|
|
||||
| `p2p:task_request` | `POST /task` | 任务提交 |
|
||||
| `p2p:task_status_update` | 回调 / GET /task | 状态更新 |
|
||||
| `p2p:task_complete` | 回调(status=completed) | 任务完成 |
|
||||
| `p2p:task_failed` | 回调(status=failed) | 任务失败 |
|
||||
| `p2p:task_cancel` | `POST /task/{id}/cancel` | 任务取消 |
|
||||
| `p2p:capability_query` | `GET /capabilities` | 能力查询 |
|
||||
| Agent 身份 | API Key | 简化认证 |
|
||||
| HMAC 签名 | Callback Token | 简化验证 |
|
||||
519
docs_backup/05-sdk-implementation.md
Normal file
519
docs_backup/05-sdk-implementation.md
Normal file
@@ -0,0 +1,519 @@
|
||||
# SDK 实现说明
|
||||
|
||||
> 版本:1.0.0
|
||||
> 更新日期:2026-06-27
|
||||
> 协议版本:1.0.0
|
||||
> 关联协议:
|
||||
> - [A2A 协议](./02-a2a-protocol.md)
|
||||
> - [A2N 协议](./03-a2n-protocol.md)
|
||||
> - [P2A 协议](./04-p2a-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
SDK 按照 A2A 协议族的三个子协议进行了模块化设计,分为三个子包,命名与协议名保持一致:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SDK (sdk/) │
|
||||
├──────────────┬───────────────────┬───────────────────────────┤
|
||||
│ sdk/p2a │ sdk/a2a │ sdk/a2n │
|
||||
│ P2A 协议 │ A2A 协议 │ A2N 协议 │
|
||||
│ (外部调用) │ (Agent间通信) │ (群组协作) │
|
||||
│ ✅ 完整实现 │ ✅ 完整实现 │ ✅ 完整实现 │
|
||||
└──────────────┴───────────────────┴───────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. SDK 目录结构
|
||||
|
||||
```
|
||||
sdk/
|
||||
├── doc.go # SDK 包文档(使用说明)
|
||||
├── p2a/
|
||||
│ └── client.go # P2A 协议 - 外部调用客户端
|
||||
├── a2a/
|
||||
│ └── client.go # A2A 协议 - Agent间通信客户端
|
||||
└── a2n/
|
||||
└── client.go # A2N 协议 - 群组协作客户端
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 第一部分:sdk/p2a - P2A 协议
|
||||
|
||||
**协议对应**:[P2A 协议](./04-p2a-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:非 Agent 调用方(前端、后端服务、第三方应用)通过 REST API 调用 Agent 能力
|
||||
|
||||
### 3.1 实现对应关系
|
||||
|
||||
| P2A 协议功能 | SDK 方法 | 位置 |
|
||||
|-------------|---------|------|
|
||||
| 认证方式 | `setHeaders()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 提交任务(JSON) | `SubmitTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 提交任务(Multipart) | `SubmitTaskWithFiles()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务详情 | `GetTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务状态(简化版) | `GetTaskStatus()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务列表 | `ListTasks()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 取消任务 | `CancelTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 能力查询 | `GetCapabilities()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 回调验证 | `VerifyCallback()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 轮询等待 | `WaitForTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
|
||||
### 3.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client 客户端
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// 任务请求
|
||||
type TaskRequest struct {
|
||||
TaskType string
|
||||
Payload map[string]any
|
||||
ExternalTaskID string
|
||||
Priority string
|
||||
CallbackURL string
|
||||
CallbackToken string
|
||||
}
|
||||
|
||||
// 任务信息
|
||||
type TaskInfo struct {
|
||||
ID string
|
||||
Type string
|
||||
Status string
|
||||
Progress int
|
||||
Result map[string]any
|
||||
Error *TaskError
|
||||
}
|
||||
|
||||
// 文件上传参数
|
||||
type FileParam struct {
|
||||
FieldName string
|
||||
Filename string
|
||||
Data []byte
|
||||
MimeType string
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 使用示例
|
||||
|
||||
```go
|
||||
import "dp-headless/a2a/sdk/p2a"
|
||||
|
||||
client := p2a.NewClient(p2a.ClientConfig{
|
||||
BaseURL: "https://agent-api.example.com",
|
||||
APIKey: "your-api-key",
|
||||
})
|
||||
|
||||
// 提交任务并等待完成
|
||||
taskInfo, err := client.CreateAndWait(ctx, "text_generation", map[string]any{
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典",
|
||||
}, 5*time.Second)
|
||||
|
||||
// 使用回调模式
|
||||
resp, err := client.CreateWithCallback(ctx, "image_generation", payload,
|
||||
"https://your-domain.com/callback",
|
||||
"callback-token",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 第二部分:sdk/a2a - A2A 协议
|
||||
|
||||
**协议对应**:[A2A 协议](./02-a2a-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:Agent 之间的点对点通信,包含消息发送、签名验证、回调处理
|
||||
|
||||
### 4.1 实现对应关系
|
||||
|
||||
| A2A 协议功能 | SDK 方法 | 位置 |
|
||||
|------------|---------|------|
|
||||
| 发送任务请求 | `SendTaskRequest()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 发送任务取消 | `SendTaskCancel()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 发送人类决策 | `SendHumanDecision()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 查询能力 | `SendCapabilityQuery()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 消息签名 | `signMessage()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 签名验证 | `verifySignature()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 任务状态管理 | `GetTaskState()`, `ListTasks()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 等待任务完成 | `WaitForTask()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 回调服务 | `startCallbackServer()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
|
||||
### 4.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client A2A客户端
|
||||
type Client struct {
|
||||
agentEndpoint types.AgentEndpoint
|
||||
authSecret string
|
||||
callbackHandler MessageCallbackHandler
|
||||
tasks map[string]*A2ATaskState
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
AgentEndpoint types.AgentEndpoint
|
||||
AuthSecret string
|
||||
CallbackPort int
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// 消息回调处理器接口
|
||||
type MessageCallbackHandler interface {
|
||||
HandleTaskRequest(ctx context.Context, msg *types.UnifiedA2AMessage) (*types.UnifiedA2AMessage, error)
|
||||
HandleTaskStatusUpdate(ctx context.Context, msg *types.UnifiedA2AMessage) error
|
||||
HandleTaskComplete(ctx context.Context, msg *types.UnifiedA2AMessage) error
|
||||
HandleTaskFailed(ctx context.Context, msg *types.UnifiedA2AMessage) error
|
||||
HandleNeedHuman(ctx context.Context, msg *types.UnifiedA2AMessage) error
|
||||
}
|
||||
|
||||
// 任务状态
|
||||
type A2ATaskState struct {
|
||||
TaskID string
|
||||
Status types.P2PTaskStatus
|
||||
Progress int
|
||||
Message string
|
||||
Result any
|
||||
Error *types.A2AError
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 使用示例
|
||||
|
||||
```go
|
||||
import "dp-headless/a2a/sdk/a2a"
|
||||
|
||||
a2aClient, err := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: types.AgentEndpoint{
|
||||
AgentID: "my-agent",
|
||||
AgentName: "我的Agent",
|
||||
SystemID: "my-system",
|
||||
Endpoint: "http://localhost:8080/a2a/callback",
|
||||
},
|
||||
AuthSecret: "my-hmac-secret",
|
||||
CallbackPort: 8080,
|
||||
})
|
||||
|
||||
// 设置消息处理器
|
||||
a2aClient.SetMessageHandler(&myHandler{})
|
||||
|
||||
// 发送任务请求给其他Agent
|
||||
taskID, err := a2aClient.SendTaskRequest(ctx, targetAgent, "video_generation", map[string]any{
|
||||
"prompt": "生成一段视频",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 第三部分:sdk/a2n - A2N 协议
|
||||
|
||||
**协议对应**:[A2N 协议](./03-a2n-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:多 Agent 群组协作,包含群组管理、任务拆分合并、投票、升级
|
||||
|
||||
### 5.1 实现对应关系
|
||||
|
||||
| A2N 协议功能 | SDK 方法 | 位置 |
|
||||
|------------|---------|------|
|
||||
| 创建群组 | `CreateGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 加入群组 | `JoinGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 离开群组 | `LeaveGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 解散群组 | `DisbandGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 提交群组任务 | `SubmitGroupTask()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 发送子任务结果 | `SendSubtaskResult()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 发起投票 | `InitiateVote()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 提交投票 | `SubmitVote()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 广播消息 | `Broadcast()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 升级到人类 | `EscalateToHuman()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
|
||||
### 5.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client A2N客户端
|
||||
type Client struct {
|
||||
a2aClient *a2a.Client
|
||||
groups map[string]*GroupState
|
||||
eventHandler GroupEventHandler
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
A2AClient *a2a.Client
|
||||
EventHandler GroupEventHandler
|
||||
}
|
||||
|
||||
// 群组状态
|
||||
type GroupState struct {
|
||||
GroupID string
|
||||
Name string
|
||||
Topology types.GroupTopology
|
||||
Status types.GroupStatus
|
||||
Members []types.GroupMember
|
||||
Role string // "coordinator" | "worker"
|
||||
}
|
||||
|
||||
// 群组事件处理器接口
|
||||
type GroupEventHandler interface {
|
||||
OnGroupCreated(ctx context.Context, groupID string) error
|
||||
OnGroupDisbanded(ctx context.Context, groupID string) error
|
||||
OnMemberJoined(ctx context.Context, groupID string, member types.GroupMember) error
|
||||
OnMemberLeft(ctx context.Context, groupID string, agentID string) error
|
||||
OnTaskSplit(ctx context.Context, groupID string, originalTaskID string, subtasks []types.Subtask) error
|
||||
OnSubtaskProgress(ctx context.Context, groupID string, subtaskID string, progress int) error
|
||||
OnTaskMerged(ctx context.Context, groupID string, originalTaskID string, result any) error
|
||||
OnVoteInitiated(ctx context.Context, groupID string, vote types.GroupVotePayload) error
|
||||
OnVoteResult(ctx context.Context, groupID string, voteID string, option string, passed bool) error
|
||||
OnEscalation(ctx context.Context, groupID string, escalationID string, level int) error
|
||||
OnBroadcast(ctx context.Context, groupID string, sender types.AgentEndpoint, message []byte) error
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 使用示例
|
||||
|
||||
```go
|
||||
import (
|
||||
"dp-headless/a2a/sdk/a2n"
|
||||
"dp-headless/a2a/sdk/a2a"
|
||||
)
|
||||
|
||||
// 创建A2A客户端
|
||||
a2aClient, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: myEndpoint,
|
||||
})
|
||||
|
||||
// 创建A2N客户端
|
||||
a2nClient, err := a2n.NewClient(&a2n.ClientConfig{
|
||||
A2AClient: a2aClient,
|
||||
EventHandler: &myGroupHandler{},
|
||||
})
|
||||
|
||||
// 创建群组
|
||||
group, err := a2nClient.CreateGroup(ctx, "视频制作组", types.GroupTopologyStar, nil, members)
|
||||
|
||||
// 作为Coordinator,提交任务
|
||||
subtasks, err := a2nClient.SubmitGroupTask(ctx, group.GroupID, "video_generation",
|
||||
map[string]any{"script": "..."},
|
||||
types.MergeAllSuccess,
|
||||
)
|
||||
|
||||
// 发起投票
|
||||
voteID, err := a2nClient.InitiateVote(ctx, group.GroupID,
|
||||
"选择哪个方案?",
|
||||
[]string{"方案A", "方案B"},
|
||||
voterIDs,
|
||||
0.5,
|
||||
5*time.Minute,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. SDK 架构总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SDK (dp-headless/a2a/sdk) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ doc.go │
|
||||
│ ├── 包文档和使用说明 │
|
||||
│ └── 指向各子包的链接 │
|
||||
├────────────────┬────────────────┬─────────────────────────┤
|
||||
│ sdk/p2a │ sdk/a2a │ sdk/a2n │
|
||||
│ │ │ │
|
||||
│ P2A协议 │ A2A协议 │ A2N协议 │
|
||||
│ │ │ │
|
||||
│ ├── Client │ ├── Client │ ├── Client │
|
||||
│ ├── 配置 │ ├── 配置 │ ├── 配置 │
|
||||
│ ├── 任务操作 │ ├── 消息发送 │ ├── 群组管理 │
|
||||
│ ├── 能力查询 │ ├── 任务状态 │ ├── 任务协作 │
|
||||
│ └── 回调验证 │ ├── 回调服务 │ ├── 投票 │
|
||||
│ │ └── 签名 │ └── 升级 │
|
||||
└────────────────┴────────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 快速开始
|
||||
|
||||
### 7.1 P2A 调用(外部调用者)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/a2a/sdk/p2a"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := p2a.NewClient(p2a.ClientConfig{
|
||||
BaseURL: "https://agent.example.com",
|
||||
APIKey: "your-api-key",
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 方式1: 轮询等待
|
||||
result, err := client.CreateAndWait(ctx, "text_generation", map[string]any{
|
||||
"prompt": "写一首诗",
|
||||
}, 5*time.Second)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
}
|
||||
fmt.Printf("Result: %v\n", result.Result)
|
||||
|
||||
// 方式2: 回调模式
|
||||
resp, err := client.CreateWithCallback(ctx, "image_generation",
|
||||
map[string]any{"prompt": "一只猫"},
|
||||
"https://your-domain.com/callback",
|
||||
"token",
|
||||
)
|
||||
fmt.Printf("Task ID: %s\n", resp.TaskID)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 A2A 通信(Agent 间)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/a2a/sdk/a2a"
|
||||
"dp-headless/a2a/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: types.AgentEndpoint{
|
||||
AgentID: "agent-001",
|
||||
AgentName: "Agent 001",
|
||||
SystemID: "my-system",
|
||||
Endpoint: "http://localhost:8080/a2a/callback",
|
||||
},
|
||||
AuthSecret: "secret-key",
|
||||
CallbackPort: 8080,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
target := types.AgentEndpoint{
|
||||
AgentID: "agent-002",
|
||||
Endpoint: "http://agent-002:8080/a2a/callback",
|
||||
}
|
||||
|
||||
// 发送任务请求
|
||||
taskID, err := client.SendTaskRequest(ctx, target, "data_analysis", map[string]any{
|
||||
"dataset": "sales.csv",
|
||||
})
|
||||
fmt.Printf("Task ID: %s\n", taskID)
|
||||
|
||||
// 等待完成
|
||||
state, err := client.WaitForTask(ctx, taskID, 5*time.Minute)
|
||||
fmt.Printf("Status: %s\n", state.Status)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 A2N 群组协作
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/a2a/sdk/a2n"
|
||||
"dp-headless/a2a/sdk/a2a"
|
||||
"dp-headless/a2a/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a2aClient, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: myEndpoint,
|
||||
})
|
||||
|
||||
a2nClient, _ := a2n.NewClient(&a2n.ClientConfig{
|
||||
A2AClient: a2aClient,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建群组
|
||||
group, _ := a2nClient.CreateGroup(ctx, "协作组", types.GroupTopologyStar, nil, []types.AgentEndpoint{
|
||||
{AgentID: "member-1", Endpoint: "http://member-1:8080"},
|
||||
{AgentID: "member-2", Endpoint: "http://member-2:8080"},
|
||||
})
|
||||
|
||||
fmt.Printf("Group ID: %s\n", group.GroupID)
|
||||
|
||||
// 提交任务
|
||||
subtasks, _ := a2nClient.SubmitGroupTask(ctx, group.GroupID, "video_generation",
|
||||
map[string]any{"script": "..."},
|
||||
types.MergeAllSuccess,
|
||||
)
|
||||
|
||||
fmt.Printf("Split into %d subtasks\n", len(subtasks))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 协议与包名对应关系
|
||||
|
||||
| 协议名称 | 协议文档 | SDK 包名 | 包路径 |
|
||||
|---------|---------|---------|--------|
|
||||
| A2A 原始协议 | [01-a2a-raw-protocol.md](./01-a2a-raw-protocol.md) | - | 纲领性文档 |
|
||||
| A2A 协议 | [02-a2a-protocol.md](./02-a2a-protocol.md) | `a2a` | `sdk/a2a/` |
|
||||
| A2N 协议 | [03-a2n-protocol.md](./03-a2n-protocol.md) | `a2n` | `sdk/a2n/` |
|
||||
| P2A 协议 | [04-p2a-protocol.md](./04-p2a-protocol.md) | `p2a` | `sdk/p2a/` |
|
||||
|
||||
**所有协议版本:1.0.0**
|
||||
|
||||
---
|
||||
|
||||
## 9. 扩展建议
|
||||
|
||||
| 优先级 | 扩展项 | 说明 |
|
||||
|-------|-------|------|
|
||||
| 高 | gRPC 传输 | 添加 gRPC 协议支持 |
|
||||
| 高 | WebSocket | 添加 WebSocket 流式传输 |
|
||||
| 中 | 批量操作 | 批量任务提交、批量查询 |
|
||||
| 中 | 指标监控 | 内置指标收集 |
|
||||
| 低 | 多语言 SDK | Python、JavaScript 等 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文件索引
|
||||
|
||||
| 文件路径 | 说明 |
|
||||
|---------|------|
|
||||
| [sdk/doc.go](../sdk/doc.go) | SDK 包文档 |
|
||||
| [sdk/p2a/client.go](../sdk/p2a/client.go) | P2A 客户端实现 |
|
||||
| [sdk/a2a/client.go](../sdk/a2a/client.go) | A2A 客户端实现 |
|
||||
| [sdk/a2n/client.go](../sdk/a2n/client.go) | A2N 客户端实现 |
|
||||
| [externalapi/handler.go](../externalapi/handler.go) | P2A 协议服务端实现 |
|
||||
| [p2p/protocol.go](../p2p/protocol.go) | A2A 协议核心实现 |
|
||||
| [group/protocol.go](../group/protocol.go) | A2N 协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 公共类型定义 |
|
||||
| [transport/transport.go](../transport/transport.go) | HTTP 传输层实现 |
|
||||
777
docs_backup/06-solutions-whitepaper.md
Normal file
777
docs_backup/06-solutions-whitepaper.md
Normal file
@@ -0,0 +1,777 @@
|
||||
# A2A 协议族 - 解决方案白皮书
|
||||
|
||||
> 版本:1.0.0
|
||||
> 日期:2026-06-27
|
||||
> 状态:草案
|
||||
> 前置阅读:[A2A 原始协议](./01-a2a-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [概述](#1-概述)
|
||||
2. [方案一:Agent 发现与广播机制](#2-方案一agent-发现与广播机制)
|
||||
3. [方案二:身份认证与信任体系](#3-方案二身份认证与信任体系)
|
||||
4. [方案三:协议缺失内容补全](#4-方案三协议缺失内容补全)
|
||||
5. [分阶段实施路线图](#5-分阶段实施路线图)
|
||||
6. [技术选型建议](#6-技术选型建议)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
当前 A2A 协议族定义了 Agent 之间"怎么说话"(消息格式、任务流程),但缺少:
|
||||
|
||||
- **怎么找到对方** — Agent 发现与广播机制
|
||||
- **怎么相信对方** — 身份认证与信任体系
|
||||
- **怎么规模化协作** — 路由、调度、激励等基础设施
|
||||
|
||||
本文档针对上述问题给出完整的解决方案。
|
||||
|
||||
### 1.2 设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **渐进式** | 从简单到复杂,每阶段都可用 |
|
||||
| **可插拔** | 组件可替换,不绑定具体实现 |
|
||||
| **传输无关** | 不依赖特定底层网络 |
|
||||
| **安全优先** | 身份认证是基础设施,不是可选功能 |
|
||||
| **向后兼容** | 不破坏现有协议,增量扩展 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案一:Agent 发现与广播机制
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Agent 发现与广播层 │
|
||||
├─────────────────────┬─────────────────────┬─────────────────────────┤
|
||||
│ 集中式目录服务 │ 分布式 Gossip │ 组播/广播 │
|
||||
│ (Directory) │ (Gossip) │ (Multicast) │
|
||||
│ ┌───────────────┐ │ ┌───────────────┐ │ ┌────────────────┐ │
|
||||
│ │ 注册/注销 │ │ │ 消息泛洪 │ │ │ 子网内广播 │ │
|
||||
│ │ 能力索引 │ │ │ 状态同步 │ │ │ 邻居发现 │ │
|
||||
│ │ 名字解析 │ │ │ 节点发现 │ │ │ 本地心跳 │ │
|
||||
│ └───────────────┘ │ └───────────────┘ │ └────────────────┘ │
|
||||
└─────────────────────┴─────────────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 模式一:集中式目录服务(推荐首选)
|
||||
|
||||
#### 2.2.1 架构说明
|
||||
|
||||
引入 **Agent Directory Service(ADS)** 作为中心化的 Agent 目录服务,类似 DNS。
|
||||
|
||||
```
|
||||
Agent A ──注册──▶ ┌─────────────┐ ◀──注册── Agent B
|
||||
│ Directory │
|
||||
Agent C ──查询──▶ │ Service │ ◀──心跳── Agent D
|
||||
└─────────────┘
|
||||
│
|
||||
▼
|
||||
能力索引
|
||||
在线状态
|
||||
端点地址
|
||||
```
|
||||
|
||||
#### 2.2.2 核心接口
|
||||
|
||||
**注册协议(Agent → Directory):**
|
||||
|
||||
| 操作 | 消息类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| 注册 | `dir:register` | Agent 上线时注册自身信息 |
|
||||
| 注销 | `dir:unregister` | Agent 下线时注销 |
|
||||
| 心跳 | `dir:heartbeat` | 定期上报存活状态 |
|
||||
| 更新能力 | `dir:update_capabilities` | 能力变化时更新 |
|
||||
|
||||
**查询协议(Agent → Directory):**
|
||||
|
||||
| 操作 | 消息类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| 按 ID 查询 | `dir:query_by_id` | 根据 AgentID 查端点 |
|
||||
| 按能力查询 | `dir:query_by_capability` | 找具备某能力的 Agent |
|
||||
| 列表查询 | `dir:list` | 分页列出所有 Agent |
|
||||
| 批量查询 | `dir:batch_query` | 批量查询多个 Agent |
|
||||
|
||||
#### 2.2.3 注册数据结构
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"agent_name": "视频生成Agent",
|
||||
"system_id": "video-system",
|
||||
"endpoint": "https://agent-001.example.com/a2a/callback",
|
||||
"version": "1.0.0",
|
||||
"capabilities": [
|
||||
{
|
||||
"type": "video_generation",
|
||||
"name": "视频生成",
|
||||
"description": "根据脚本生成视频",
|
||||
"max_concurrent": 10,
|
||||
"avg_latency_ms": 5000
|
||||
}
|
||||
],
|
||||
"transports": ["http", "grpc"],
|
||||
"status": "online",
|
||||
"last_heartbeat": "2026-06-27T10:00:00Z",
|
||||
"trust_score": 85,
|
||||
"public_key": "-----BEGIN PUBLIC KEY-----\n..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2.4 优缺点
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 查询效率高(O(1)) | 单点故障风险 |
|
||||
| 支持复杂查询(按能力、按系统等) | 中心节点需信任 |
|
||||
| 易于管理和监控 | 扩展性有上限 |
|
||||
| 实现简单 | 存在注册/注销时序问题 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 模式二:分布式 Gossip 协议(可选扩展)
|
||||
|
||||
#### 2.3.1 架构说明
|
||||
|
||||
无中心节点,Agent 之间通过 Gossip 协议(epidemic protocol)扩散状态信息,类似比特币网络的节点发现。
|
||||
|
||||
```
|
||||
Agent A ──gossip──▶ Agent B ──gossip──▶ Agent D
|
||||
│ │
|
||||
└──gossip──▶ Agent C ──gossip──▶ Agent E
|
||||
```
|
||||
|
||||
#### 2.3.2 核心机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| **Peer Sampling** | 每个 Agent 维护一个部分视图(partial view) |
|
||||
| **Active Push** | 定期随机选几个节点推送本地状态 |
|
||||
| **Passive Pull** | 收到推送后,有概率回传自身状态 |
|
||||
| **TTL** | 消息有存活跳数,防止无限扩散 |
|
||||
| **Deduplication** | 消息 ID 去重,避免重复处理 |
|
||||
|
||||
#### 2.3.3 Gossip 消息格式
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "gossip:state",
|
||||
"from": "agent-001",
|
||||
"view": [
|
||||
{"agent_id": "agent-002", "endpoint": "...", "capabilities": [...], "timestamp": "..."},
|
||||
{"agent_id": "agent-003", "endpoint": "...", "capabilities": [...], "timestamp": "..."},
|
||||
{"agent_id": "agent-004", "endpoint": "...", "capabilities": [...], "timestamp": "..."}
|
||||
],
|
||||
"ttl": 5,
|
||||
"msg_id": "msg-xxx"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3.4 优缺点
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 完全去中心化 | 查询效率低(需本地构建视图) |
|
||||
| 容错性强 | 状态最终一致,有延迟 |
|
||||
| 可无限扩展 | 实现复杂度高 |
|
||||
| 无单点故障 | 网络流量大 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 模式三:本地组播/邻居发现(局域网场景)
|
||||
|
||||
适用于同一内网/集群内的 Agent 快速发现。
|
||||
|
||||
#### 2.4.1 机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| **UDP 组播** | 定期在组播地址发送广播 |
|
||||
| **mDNS/DNS-SD** | 类似 Apple Bonjour 的服务发现 |
|
||||
| **ARP/NDP** | 链路层发现(不推荐) |
|
||||
|
||||
#### 2.4.2 适用场景
|
||||
|
||||
- 边缘计算集群
|
||||
- 同一机房内的 Agent 集群
|
||||
- 本地开发测试环境
|
||||
|
||||
---
|
||||
|
||||
### 2.5 广播机制设计
|
||||
|
||||
#### 2.5.1 广播类型
|
||||
|
||||
| 广播范围 | 消息类型前缀 | 说明 |
|
||||
|---------|-------------|------|
|
||||
| 全网广播 | `global:` | 所有在线 Agent |
|
||||
| 系统广播 | `system:` | 同一 system_id 内 |
|
||||
| 群组广播 | `group:` | 同一群组内(已有) |
|
||||
| 能力组广播 | `cap:` | 具备某能力的 Agent |
|
||||
|
||||
#### 2.5.2 广播实现
|
||||
|
||||
**集中式模式下:**
|
||||
- 由 Directory Service 转发广播消息
|
||||
- 支持按条件过滤(按能力、按系统等)
|
||||
|
||||
**分布式模式下:**
|
||||
- 通过 Gossip 协议泛洪
|
||||
- TTL 控制扩散范围
|
||||
- 消息 ID 去重
|
||||
|
||||
---
|
||||
|
||||
### 2.6 心跳与存活检测
|
||||
|
||||
#### 2.6.1 心跳机制
|
||||
|
||||
```
|
||||
Agent ───heartbeat──▶ Directory
|
||||
(每30秒)
|
||||
|
||||
Directory 判定离线:
|
||||
- 连续 3 次未收到心跳 → 标记为 offline
|
||||
- 离线 24 小时 → 自动注销
|
||||
```
|
||||
|
||||
#### 2.6.2 主动探测
|
||||
|
||||
```
|
||||
Directory ───ping──▶ Agent
|
||||
◀──pong──
|
||||
```
|
||||
|
||||
用于验证 Agent 是否真的在线(防止心跳伪造)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案二:身份认证与信任体系
|
||||
|
||||
### 3.1 三层身份架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Layer 3: 信任评分 (Trust Score) │
|
||||
│ 声誉系统、历史行为、第三方评价 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 2: 身份验证 (Authentication) │
|
||||
│ 签名验证、证书校验、DID 验证 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 1: 身份标识 (Identity) │
|
||||
│ AgentID + 公钥 / 证书 / DID │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Layer 1:身份标识方案
|
||||
|
||||
#### 3.2.1 方案 A:公钥身份(推荐基础版)
|
||||
|
||||
**核心思想**:AgentID = 公钥哈希(类似比特币地址)
|
||||
|
||||
```
|
||||
AgentID = base58(sha256(public_key)[:20])
|
||||
例:agent-1A2B3C4D5E6F...
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 自包含,不需要中心 CA
|
||||
- 公钥可验证身份
|
||||
- 实现简单
|
||||
|
||||
**缺点:**
|
||||
- 身份无法撤销(私钥泄露 = 身份被盗)
|
||||
- 没有人类可读的名字
|
||||
- 无法关联真实世界身份
|
||||
|
||||
**适用场景**:内部系统、测试环境、信任度要求不高的场景
|
||||
|
||||
#### 3.2.2 方案 B:PKI 证书体系
|
||||
|
||||
**核心思想**:引入 CA(Certificate Authority)颁发证书
|
||||
|
||||
```
|
||||
CA 根证书
|
||||
├── 系统A证书
|
||||
│ ├── Agent-001 证书
|
||||
│ └── Agent-002 证书
|
||||
└── 系统B证书
|
||||
└── Agent-003 证书
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 成熟的工业标准
|
||||
- 支持证书撤销(CRL/OCSP)
|
||||
- 有层级信任关系
|
||||
|
||||
**缺点:**
|
||||
- 需要信任中心化 CA
|
||||
- 证书管理复杂
|
||||
- 跨 CA 互操作需要桥接
|
||||
|
||||
**适用场景**:企业内部、有明确信任中心的场景
|
||||
|
||||
#### 3.2.3 方案 C:区块链 DID(去中心化身份)
|
||||
|
||||
**核心思想**:身份上链,链上存公钥和属性,类似 W3C DID 标准。
|
||||
|
||||
```
|
||||
链上存储:
|
||||
DID → {
|
||||
"publicKey": "...",
|
||||
"controller": "...",
|
||||
"authentication": [...],
|
||||
"service": [...],
|
||||
"revoked": false,
|
||||
"timestamp": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**三种实现路径:**
|
||||
|
||||
| 路径 | 说明 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| **公链 DID** | 使用以太坊/Solana 等公链 | 开放生态、跨组织协作 |
|
||||
| **联盟链 DID** | 多机构共建联盟链 | 行业联盟、政企合作 |
|
||||
| **私有链 DID** | 单组织内部链 | 企业内部、对性能要求高 |
|
||||
|
||||
**优点:**
|
||||
- 去中心化,无单点故障
|
||||
- 身份可验证、可追溯
|
||||
- 支持跨系统互认
|
||||
- 不可篡改的身份历史
|
||||
|
||||
**缺点:**
|
||||
- 链上操作有延迟
|
||||
- 需要支付 Gas 费(公链)
|
||||
- 实现复杂度高
|
||||
- 隐私保护需额外设计
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Layer 2:身份验证方案
|
||||
|
||||
#### 3.3.1 消息签名验证
|
||||
|
||||
所有 A2A 消息必须签名,签名方式:
|
||||
|
||||
```
|
||||
签名内容 = SHA256(message_id + sender + receiver + payload + timestamp)
|
||||
签名 = Sign(private_key, 签名内容)
|
||||
验证 = Verify(public_key, 签名内容, 签名)
|
||||
```
|
||||
|
||||
#### 3.3.2 签名放置位置
|
||||
|
||||
| 位置 | 说明 |
|
||||
|------|------|
|
||||
| 消息体内 | `signature` 字段 |
|
||||
| HTTP Header | `X-A2A-Signature` |
|
||||
| gRPC Metadata | `x-a2a-signature` |
|
||||
|
||||
#### 3.3.3 防重放攻击
|
||||
|
||||
```
|
||||
每个消息有唯一 message_id + timestamp
|
||||
接收方维护已处理 message_id 缓存(如最近 1 小时)
|
||||
timestamp 与本地时间差超过 5 分钟 → 拒绝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Layer 3:信任评分体系
|
||||
|
||||
#### 3.4.1 信任分维度
|
||||
|
||||
| 维度 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 任务完成率 | 30% | 历史任务成功 / 总任务数 |
|
||||
| 任务质量 | 25% | 结果评分、返工率 |
|
||||
| 响应速度 | 15% | 平均响应时间 |
|
||||
| 在线稳定性 | 15% | 在线时长 / 总时长 |
|
||||
| 历史信誉 | 15% | 其他 Agent 的评价 |
|
||||
|
||||
#### 3.4.2 信任分计算
|
||||
|
||||
```
|
||||
trust_score = Σ(维度_i × 权重_i)
|
||||
|
||||
范围:0 - 100
|
||||
等级:
|
||||
90-100 S 级 高度可信
|
||||
75-89 A 级 可信
|
||||
60-74 B 级 一般可信
|
||||
40-59 C 级 谨慎使用
|
||||
0-39 D 级 不可信
|
||||
```
|
||||
|
||||
#### 3.4.3 信任传递
|
||||
|
||||
```
|
||||
A 信任 B(评分 90)
|
||||
B 信任 C(评分 85)
|
||||
A 对 C 的初始信任 = 90 × 85% × 衰减因子 = 约 68
|
||||
```
|
||||
|
||||
**注意**:信任传递有衰减,防止信任污染。
|
||||
|
||||
---
|
||||
|
||||
### 3.5 区块链适用性决策树
|
||||
|
||||
```
|
||||
是否需要跨组织协作?
|
||||
├── 否 → 用 PKI 或公钥身份(简单高效)
|
||||
└── 是 → 是否有共同信任的中心机构?
|
||||
├── 是 → 用 PKI 体系(成熟可靠)
|
||||
└── 否 → 是否需要不可篡改的审计?
|
||||
├── 是 → 考虑联盟链 DID
|
||||
└── 否 → 公钥身份 + 声誉系统可能够用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 方案三:协议缺失内容补全
|
||||
|
||||
### 4.1 第一优先级补全(核心基础设施)
|
||||
|
||||
#### 4.1.1 Agent 发现协议(ADS - Agent Discovery Service)
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 方向 | 说明 |
|
||||
|---------|------|------|
|
||||
| `dir:register` | Agent → Directory | 注册 Agent |
|
||||
| `dir:unregister` | Agent → Directory | 注销 Agent |
|
||||
| `dir:heartbeat` | Agent → Directory | 心跳 |
|
||||
| `dir:update_capabilities` | Agent → Directory | 更新能力 |
|
||||
| `dir:query_by_id` | Agent → Directory | 按 ID 查询 |
|
||||
| `dir:query_by_capability` | Agent → Directory | 按能力查询 |
|
||||
| `dir:list` | Agent → Directory | 列表查询 |
|
||||
| `dir:query_result` | Directory → Agent | 查询结果 |
|
||||
|
||||
**新增数据结构:** `AgentDirectoryEntry`, `CapabilityIndex`, `QueryFilter`
|
||||
|
||||
#### 4.1.2 身份认证规范
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `auth:challenge` | 身份挑战 |
|
||||
| `auth:response` | 挑战响应 |
|
||||
| `auth:cert_exchange` | 证书交换 |
|
||||
|
||||
**新增内容:**
|
||||
- 标准签名算法列表(Ed25519, ECDSA P-256, RSA 2048)
|
||||
- 消息签名格式规范
|
||||
- 防重放攻击机制
|
||||
- 身份验证流程规范
|
||||
|
||||
#### 4.1.3 心跳与存活检测
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `net:ping` | 存活探测 |
|
||||
| `net:pong` | 存活响应 |
|
||||
| `net:offline_notify` | 下线通知 |
|
||||
|
||||
**规范内容:**
|
||||
- 心跳间隔(默认 30 秒)
|
||||
- 超时判定(连续 3 次未收到 → 离线)
|
||||
- 离线通知机制
|
||||
|
||||
#### 4.1.4 统一错误码体系
|
||||
|
||||
**错误分类:**
|
||||
|
||||
| 错误码前缀 | 类别 | 示例 |
|
||||
|-----------|------|------|
|
||||
| `msg_` | 消息层 | msg_invalid_format, msg_signature_invalid |
|
||||
| `auth_` | 认证层 | auth_failed, auth_expired, auth_forbidden |
|
||||
| `task_` | 任务层 | task_not_found, task_timeout, task_cancelled |
|
||||
| `cap_` | 能力层 | cap_lacking, cap_busy, cap_not_supported |
|
||||
| `group_` | 群组层 | group_not_found, group_full, group_permission_denied |
|
||||
| `dir_` | 目录层 | dir_agent_not_found, dir_query_failed |
|
||||
| `net_` | 网络层 | net_timeout, net_offline, net_unreachable |
|
||||
| `sys_` | 系统层 | sys_internal_error, sys_not_implemented |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 第二优先级补全(重要功能)
|
||||
|
||||
#### 4.2.1 能力匹配与任务路由
|
||||
|
||||
**功能:**
|
||||
- 智能选择最合适的 Agent 执行任务
|
||||
- 支持负载均衡(按并发数、响应速度、信任分)
|
||||
- 支持任务失败自动重试/转发
|
||||
|
||||
**新增消息类型:**
|
||||
- `router:submit_task` - 提交给路由器自动选择执行者
|
||||
- `router:forward` - 任务转发
|
||||
|
||||
#### 4.2.2 全网广播协议
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `global:broadcast` | 全网广播 |
|
||||
| `system:broadcast` | 系统内广播 |
|
||||
| `cap:broadcast` | 能力组广播 |
|
||||
|
||||
**规范内容:**
|
||||
- 广播范围定义
|
||||
- TTL 与去重机制
|
||||
- 广播风暴防护
|
||||
|
||||
#### 4.2.3 版本协商机制
|
||||
|
||||
**功能:**
|
||||
- Agent 之间协商使用的协议版本
|
||||
- 向后兼容处理
|
||||
- 能力降级策略
|
||||
|
||||
**新增消息类型:**
|
||||
- `meta:version_query` - 查询支持的版本
|
||||
- `meta:version_negotiate` - 版本协商
|
||||
|
||||
#### 4.2.4 幂等性保证
|
||||
|
||||
**规范内容:**
|
||||
- 幂等键(Idempotency Key)机制
|
||||
- 重复消息处理规范
|
||||
- 任务级幂等 vs 消息级幂等
|
||||
|
||||
---
|
||||
|
||||
### 4.3 第三优先级补全(增强功能)
|
||||
|
||||
#### 4.3.1 流式输出协议
|
||||
|
||||
**功能:**
|
||||
- 长任务实时进度推送
|
||||
- 生成式 AI 的 token 流式输出
|
||||
- WebSocket/SSE 传输支持
|
||||
|
||||
#### 4.3.2 批量任务协议
|
||||
|
||||
**功能:**
|
||||
- 一次性提交多个同类型任务
|
||||
- 批量状态查询
|
||||
- 批量结果收集
|
||||
|
||||
#### 4.3.3 支付与激励机制
|
||||
|
||||
**功能:**
|
||||
- 任务定价与结算
|
||||
- Token 或积分系统
|
||||
- 智能合约自动结算(区块链场景)
|
||||
|
||||
#### 4.3.4 群组发现协议
|
||||
|
||||
**功能:**
|
||||
- 按主题/任务类型查找群组
|
||||
- 推荐加入合适的群组
|
||||
- 群组热度/活跃度排名
|
||||
|
||||
---
|
||||
|
||||
### 4.4 安全相关补全
|
||||
|
||||
#### 4.4.1 端到端加密
|
||||
|
||||
**规范内容:**
|
||||
- 密钥协商(ECDH)
|
||||
- 对称加密载荷(AES-GCM)
|
||||
- 前向保密(PFS)
|
||||
|
||||
#### 4.4.2 权限委托
|
||||
|
||||
**功能:**
|
||||
- Agent A 委托 Agent B 代表自己执行任务
|
||||
- 委托令牌(Delegation Token)
|
||||
- 委托范围与时效控制
|
||||
|
||||
#### 4.4.3 审计日志标准
|
||||
|
||||
**规范内容:**
|
||||
- 统一审计日志格式
|
||||
- 关键操作必须记录
|
||||
- 日志完整性保护(哈希链)
|
||||
|
||||
---
|
||||
|
||||
## 5. 分阶段实施路线图
|
||||
|
||||
### 5.1 Phase 0:基础版(MVP) - 预计 2 周
|
||||
|
||||
**目标**:协议可用,能跑通基本流程
|
||||
|
||||
**内容:**
|
||||
- [x] A2A 点对点任务协议(已完成)
|
||||
- [x] A2N 群组协作协议(已完成)
|
||||
- [x] P2A 外部调用协议(已完成)
|
||||
- [x] 三套协议的 SDK 实现(已完成)
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Phase 1:发现与身份 - 预计 4 周
|
||||
|
||||
**目标**:Agent 能互相找到对方,能验证对方身份
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| Agent 目录服务协议 | 集中式 ADS | P0 |
|
||||
| 公钥身份规范 | Ed25519 签名验证 | P0 |
|
||||
| 心跳与存活检测 | 30 秒心跳,3 次超时判定离线 | P0 |
|
||||
| 统一错误码体系 | 8 大类错误码 | P0 |
|
||||
| 防重放攻击 | message_id + timestamp | P0 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`06-agent-discovery.md`
|
||||
- 新协议文档:`07-authentication.md`
|
||||
- 代码:`discovery/` 包(目录服务客户端)
|
||||
- 代码:`auth/` 包(签名验证工具)
|
||||
- 更新 types 包:新增消息类型和错误码
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Phase 2:路由与广播 - 预计 4 周
|
||||
|
||||
**目标**:支持智能任务路由和全网广播
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 任务路由协议 | 按能力 + 负载 + 信任分选择执行者 | P1 |
|
||||
| 全网广播协议 | 基于目录服务的广播转发 | P1 |
|
||||
| 版本协商机制 | 协议版本兼容 | P1 |
|
||||
| 幂等性保证 | 幂等键 + 去重 | P1 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`08-routing-and-broadcast.md`
|
||||
- 代码:`routing/` 包增强(增加能力路由)
|
||||
- 更新目录服务:支持广播转发
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Phase 3:信任与激励 - 预计 6 周
|
||||
|
||||
**目标**:建立信任体系,支持规模化协作
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 信任评分体系 | 5 维度信任分计算 | P2 |
|
||||
| 流式输出协议 | WebSocket/SSE 流式进度 | P2 |
|
||||
| 批量任务协议 | 批量提交/查询/收集 | P2 |
|
||||
| 群组发现协议 | 群组搜索与推荐 | P2 |
|
||||
| SLA 协议 | 服务等级承诺 | P3 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`09-trust-and-reputation.md`
|
||||
- 代码:`trust/` 包(信任评分引擎)
|
||||
|
||||
---
|
||||
|
||||
### 5.5 Phase 4:高级特性 - 预计 8 周+
|
||||
|
||||
**目标**:企业级、生产级完整功能
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 端到端加密 | ECDH + AES-GCM | P3 |
|
||||
| 权限委托 | 委托令牌机制 | P3 |
|
||||
| 支付与激励 | Token/积分系统 | P3 |
|
||||
| 区块链 DID 集成 | 可选模块 | P4 |
|
||||
| 审计日志标准 | 统一审计格式 | P3 |
|
||||
| Gossip 协议 | 分布式发现(可选) | P4 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术选型建议
|
||||
|
||||
### 6.1 目录服务存储选型
|
||||
|
||||
| 方案 | 适用场景 | 优点 | 缺点 |
|
||||
|------|---------|------|------|
|
||||
| **Redis** | 中小规模 | 高性能,支持 TTL | 数据量受限 |
|
||||
| **PostgreSQL** | 中大规模 | 复杂查询,持久化 | 需运维 |
|
||||
| **Elasticsearch** | 大规模 + 全文检索 | 强大的搜索能力 | 运维复杂 |
|
||||
|
||||
**推荐**:先用 Redis(简单快),规模大了再迁 PostgreSQL。
|
||||
|
||||
---
|
||||
|
||||
### 6.2 签名算法选型
|
||||
|
||||
| 算法 | 安全性 | 性能 | 密钥长度 | 推荐度 |
|
||||
|------|--------|------|---------|--------|
|
||||
| Ed25519 | 高 | 快 | 256 位 | ⭐⭐⭐⭐⭐ 首选 |
|
||||
| ECDSA P-256 | 高 | 中 | 256 位 | ⭐⭐⭐⭐ 兼容 |
|
||||
| RSA 2048 | 中 | 慢 | 2048 位 | ⭐⭐ 仅兼容 |
|
||||
|
||||
**推荐**:默认 Ed25519,需要兼容旧系统时支持 ECDSA。
|
||||
|
||||
---
|
||||
|
||||
### 6.3 区块链选型(如果需要)
|
||||
|
||||
| 场景 | 推荐方案 | 说明 |
|
||||
|------|---------|------|
|
||||
| 企业内部 | **私有链(Hyperledger Fabric)** | 可控,高性能,无 Gas 费 |
|
||||
| 行业联盟 | **联盟链(FISCO BCOS / ChainMaker)** | 多机构共建,合规 |
|
||||
| 开放生态 | **公链(以太坊 / Solana)** | 完全开放,需 Gas 费 |
|
||||
| 简单原型 | **模拟链(数据库模拟)** | 先验证逻辑,再上真链 |
|
||||
|
||||
**推荐**:90% 的场景用不到真区块链,先用数据库模拟验证业务逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结与建议
|
||||
|
||||
### 7.1 核心建议
|
||||
|
||||
1. **先集中后分布**:先用集中式目录服务,简单高效;规模大了再考虑分布式
|
||||
2. **先公钥后区块链**:先用公钥身份解决 80% 的问题,真的需要了再上链
|
||||
3. **先 MVP 后完善**:先跑通核心流程,再逐步加高级功能
|
||||
4. **标准接口可替换**:目录服务、身份系统都做成可插拔的,方便未来升级
|
||||
|
||||
### 7.2 下一步行动
|
||||
|
||||
| 序号 | 行动 | 负责人 | 产出 |
|
||||
|------|------|--------|------|
|
||||
| 1 | 评审本方案,确认 Phase 1 范围 | 架构组 | 确认后的方案 |
|
||||
| 2 | 设计目录服务 API 详细规范 | 协议组 | `06-agent-discovery.md` |
|
||||
| 3 | 设计身份认证详细规范 | 协议组 | `07-authentication.md` |
|
||||
| 4 | 实现目录服务原型 | 开发组 | 可运行的 ADS 服务 |
|
||||
| 5 | 更新 SDK 支持发现和认证 | 开发组 | 新版 SDK |
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文档索引
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [01-a2a-raw-protocol.md](./01-a2a-raw-protocol.md) | A2A 原始协议(纲领) |
|
||||
| [02-a2a-protocol.md](./02-a2a-protocol.md) | A2A 协议(P2P 任务) |
|
||||
| [03-a2n-protocol.md](./03-a2n-protocol.md) | A2N 协议(群组协作) |
|
||||
| [04-p2a-protocol.md](./04-p2a-protocol.md) | P2A 协议(外部调用) |
|
||||
| [05-sdk-implementation.md](./05-sdk-implementation.md) | SDK 实现说明 |
|
||||
| **本文档** | **解决方案白皮书** |
|
||||
414
examples/group_demo.go
Normal file
414
examples/group_demo.go
Normal file
@@ -0,0 +1,414 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"synapse/sdk/group"
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Group 群组测试 - 三个 Agent 之间的群组协作
|
||||
// =============================================================================
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== 灵隙(Synapse)Group 群组测试 ===")
|
||||
fmt.Println("启动三个 Agent,进行群组协作验证...")
|
||||
fmt.Println()
|
||||
|
||||
// 创建群组客户端(简化版,直接在主程序模拟)
|
||||
groupClient := &simpleGroupClient{
|
||||
groups: make(map[string]*group.GroupState),
|
||||
}
|
||||
|
||||
// 启动 Agent 1 (Coordinator) - 监听 9091 端口
|
||||
go runCoordinatorAgent(groupClient)
|
||||
|
||||
// 等待 Agent 1 启动
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 启动 Agent 2 (Worker1) - 监听 9092 端口
|
||||
go runWorkerAgent(groupClient, "worker-1", 9092)
|
||||
|
||||
// 等待 Agent 2 启动
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 启动 Agent 3 (Worker2) - 监听 9093 端口
|
||||
go runWorkerAgent(groupClient, "worker-2", 9093)
|
||||
|
||||
// 保持主程序运行
|
||||
fmt.Println("\n=== Agent 已启动,等待群组测试 ===")
|
||||
fmt.Println("Agent 1 (Coordinator): http://localhost:9091")
|
||||
fmt.Println("Agent 2 (Worker-1): http://localhost:9092")
|
||||
fmt.Println("Agent 3 (Worker-2): http://localhost:9093")
|
||||
fmt.Println("\n按 Ctrl+C 停止...")
|
||||
|
||||
// 自动创建群组并开始测试
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
createGroup()
|
||||
}()
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
// 简化的群组客户端
|
||||
type simpleGroupClient struct {
|
||||
groups map[string]*group.GroupState
|
||||
members map[string][]string // groupID -> agentIDs
|
||||
}
|
||||
|
||||
// 创建群组并开始测试
|
||||
func createGroup() {
|
||||
log.Printf("\n=== [主程序] 创建群组 ===")
|
||||
|
||||
resp, err := http.Post(
|
||||
"http://localhost:9091/api/group/create",
|
||||
"application/json",
|
||||
jsonBody(map[string]any{}),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[主程序] 创建群组失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]any
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
log.Printf("[主程序] 创建群组响应: %v", result)
|
||||
|
||||
// 等待任务完成
|
||||
time.Sleep(8 * time.Second)
|
||||
}
|
||||
|
||||
type agentInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Port int
|
||||
Handler interface{}
|
||||
}
|
||||
|
||||
// Coordinator Agent(协调者)
|
||||
func runCoordinatorAgent(client *simpleGroupClient) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 健康检查
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte("OK - Coordinator"))
|
||||
})
|
||||
|
||||
// 群组管理 API
|
||||
mux.HandleFunc("/api/group/create", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
groupID := fmt.Sprintf("group-%d", time.Now().UnixNano())
|
||||
group := &group.GroupState{
|
||||
GroupID: groupID,
|
||||
Name: "视频制作组",
|
||||
Topology: types.GroupTopologyStar,
|
||||
Status: types.GroupStatusActive,
|
||||
Members: []types.GroupMember{
|
||||
{
|
||||
AgentID: "coordinator",
|
||||
AgentName: "协调者",
|
||||
Role: "coordinator",
|
||||
JoinedAt: types.NowTimestamp(),
|
||||
Status: types.GroupMemberActive,
|
||||
Capabilities: []string{"coordination"},
|
||||
LoadFactor: 0,
|
||||
},
|
||||
{
|
||||
AgentID: "worker-1",
|
||||
AgentName: "工作者1",
|
||||
Role: "worker",
|
||||
JoinedAt: types.NowTimestamp(),
|
||||
Status: types.GroupMemberActive,
|
||||
Capabilities: []string{"video_generation"},
|
||||
LoadFactor: 0,
|
||||
},
|
||||
{
|
||||
AgentID: "worker-2",
|
||||
AgentName: "工作者2",
|
||||
Role: "worker",
|
||||
JoinedAt: types.NowTimestamp(),
|
||||
Status: types.GroupMemberActive,
|
||||
Capabilities: []string{"audio_generation"},
|
||||
LoadFactor: 0,
|
||||
},
|
||||
},
|
||||
Role: "coordinator",
|
||||
}
|
||||
client.groups[groupID] = group
|
||||
|
||||
log.Printf("\n=== [Coordinator] 群组创建成功 ===")
|
||||
log.Printf("Group ID: %s", groupID)
|
||||
log.Printf("Members: coordinator, worker-1, worker-2")
|
||||
|
||||
// 模拟任务拆分
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
simulateTaskSplit(groupID, client)
|
||||
}()
|
||||
|
||||
// 模拟投票
|
||||
go func() {
|
||||
time.Sleep(3 * time.Second)
|
||||
simulateVote(groupID)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"success": true,
|
||||
"group_id": groupID,
|
||||
"message": "群组创建成功",
|
||||
})
|
||||
})
|
||||
|
||||
// 获取群组信息
|
||||
mux.HandleFunc("/api/group/", func(w http.ResponseWriter, r *http.Request) {
|
||||
groupID := r.URL.Path[len("/api/group/"):]
|
||||
group, ok := client.groups[groupID]
|
||||
if !ok {
|
||||
http.Error(w, "group not found", 404)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"success": true,
|
||||
"group": group,
|
||||
})
|
||||
})
|
||||
|
||||
log.Println("[Coordinator] 协调者 Agent 启动,监听端口 9091")
|
||||
if err := http.ListenAndServe(":9091", mux); err != nil {
|
||||
log.Fatalf("[Coordinator] Server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Worker Agent(工作者)
|
||||
func runWorkerAgent(client *simpleGroupClient, workerID string, port int) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 健康检查
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(fmt.Sprintf("OK - %s", workerID)))
|
||||
})
|
||||
|
||||
// 加入群组
|
||||
mux.HandleFunc("/api/group/join", func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
GroupID string `json:"group_id"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
log.Printf("\n=== [%s] 申请加入群组 ===", workerID)
|
||||
log.Printf("Group ID: %s", req.GroupID)
|
||||
|
||||
// 模拟处理加入请求
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("%s 已加入群组", workerID),
|
||||
"role": "worker",
|
||||
"group_id": req.GroupID,
|
||||
})
|
||||
})
|
||||
|
||||
// 接收任务
|
||||
mux.HandleFunc("/api/task/subtask", func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
GroupID string `json:"group_id"`
|
||||
Subtask string `json:"subtask"`
|
||||
TaskType string `json:"task_type"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
log.Printf("\n=== [%s] 收到子任务 ===", workerID)
|
||||
log.Printf("Group ID: %s", req.GroupID)
|
||||
log.Printf("Subtask: %s", req.Subtask)
|
||||
log.Printf("Task Type: %s", req.TaskType)
|
||||
log.Printf("Payload: %v", req.Payload)
|
||||
|
||||
// 模拟执行任务
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
log.Printf("[%s] 子任务执行完成: %s", workerID, req.Subtask)
|
||||
|
||||
// 发送结果(这里简化处理,只是打印日志)
|
||||
log.Printf("[%s] 发送子任务结果: %s -> 完成了 %s 的工作", workerID, req.Subtask, req.TaskType)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("%s 开始执行任务", workerID),
|
||||
"subtask": req.Subtask,
|
||||
"progress": 0,
|
||||
})
|
||||
})
|
||||
|
||||
// 接收投票
|
||||
mux.HandleFunc("/api/vote", func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
GroupID string `json:"group_id"`
|
||||
VoteID string `json:"vote_id"`
|
||||
Question string `json:"question"`
|
||||
Options []string `json:"options"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
log.Printf("\n=== [%s] 收到投票请求 ===", workerID)
|
||||
log.Printf("Vote ID: %s", req.VoteID)
|
||||
log.Printf("Question: %s", req.Question)
|
||||
log.Printf("Options: %v", req.Options)
|
||||
|
||||
// 模拟投票决策(选择第一个选项)
|
||||
selectedOption := req.Options[0]
|
||||
log.Printf("[%s] 投票: %s", workerID, selectedOption)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"success": true,
|
||||
"vote_id": req.VoteID,
|
||||
"selected_option": selectedOption,
|
||||
"reason": "看起来更合理",
|
||||
})
|
||||
})
|
||||
|
||||
log.Printf("[%s] 工作者 Agent 启动,监听端口 %d", workerID, port)
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
|
||||
log.Fatalf("[%s] Server error: %v", workerID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟任务拆分
|
||||
func simulateTaskSplit(groupID string, client *simpleGroupClient) {
|
||||
log.Printf("\n=== [Coordinator] 执行任务拆分 ===")
|
||||
log.Printf("Original Task: 制作视频")
|
||||
|
||||
subtasks := []struct {
|
||||
ID string
|
||||
Description string
|
||||
AssignedTo string
|
||||
}{
|
||||
{"sub-1", "生成视频脚本", "worker-1"},
|
||||
{"sub-2", "生成配音", "worker-2"},
|
||||
}
|
||||
|
||||
for _, st := range subtasks {
|
||||
log.Printf("拆分任务: %s -> %s", st.ID, st.AssignedTo)
|
||||
|
||||
// 发送任务给 worker
|
||||
port := 9092
|
||||
if st.AssignedTo == "worker-2" {
|
||||
port = 9093
|
||||
}
|
||||
|
||||
resp, err := http.Post(
|
||||
fmt.Sprintf("http://localhost:%d/api/task/subtask", port),
|
||||
"application/json",
|
||||
jsonBody(map[string]any{
|
||||
"group_id": groupID,
|
||||
"subtask": st.ID,
|
||||
"task_type": "video_generation",
|
||||
"payload": map[string]any{"description": st.Description},
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[Coordinator] 发送任务失败: %v", err)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟投票
|
||||
func simulateVote(groupID string) {
|
||||
log.Printf("\n=== [Coordinator] 发起投票 ===")
|
||||
|
||||
voteQuestion := "选择视频风格?"
|
||||
voteOptions := []string{"现代风格", "复古风格", "卡通风格"}
|
||||
|
||||
log.Printf("Question: %s", voteQuestion)
|
||||
log.Printf("Options: %v", voteOptions)
|
||||
|
||||
// 收集投票结果
|
||||
votes := make(map[string]int)
|
||||
for _, opt := range voteOptions {
|
||||
votes[opt] = 0
|
||||
}
|
||||
|
||||
// 向两个 worker 发送投票
|
||||
for _, port := range []int{9092, 9093} {
|
||||
resp, err := http.Post(
|
||||
fmt.Sprintf("http://localhost:%d/api/vote", port),
|
||||
"application/json",
|
||||
jsonBody(map[string]any{
|
||||
"group_id": groupID,
|
||||
"vote_id": fmt.Sprintf("vote-%d", time.Now().UnixNano()),
|
||||
"question": voteQuestion,
|
||||
"options": voteOptions,
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[Coordinator] 投票请求失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
resp.Body.Close()
|
||||
|
||||
if selected, ok := result["selected_option"].(string); ok {
|
||||
votes[selected]++
|
||||
}
|
||||
}
|
||||
|
||||
// 统计结果
|
||||
log.Printf("\n=== [Coordinator] 投票结果 ===")
|
||||
for opt, count := range votes {
|
||||
log.Printf("%s: %d 票", opt, count)
|
||||
}
|
||||
|
||||
// 确定获胜者
|
||||
var winner string
|
||||
maxVotes := 0
|
||||
for opt, count := range votes {
|
||||
if count > maxVotes {
|
||||
maxVotes = count
|
||||
winner = opt
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("\n最终决定: %s (获得 %d 票)", winner, maxVotes)
|
||||
|
||||
// 模拟结果合并
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Printf("\n=== [Coordinator] 合并任务结果 ===")
|
||||
log.Printf("所有子任务已完成")
|
||||
log.Printf("最终结果: 视频制作完成,采用 %s", winner)
|
||||
}()
|
||||
}
|
||||
|
||||
func jsonBody(v map[string]any) *bytes.Buffer {
|
||||
data, _ := json.Marshal(v)
|
||||
return bytes.NewBuffer(data)
|
||||
}
|
||||
368
examples/p2p_demo.go
Normal file
368
examples/p2p_demo.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"synapse/sdk/p2p"
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// P2P 通讯测试 - 两个 Agent 之间的点对点通信
|
||||
// =============================================================================
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== 灵隙(Synapse)P2P 通讯测试 ===")
|
||||
fmt.Println("启动两个 Agent,进行点对点通讯验证...")
|
||||
fmt.Println()
|
||||
|
||||
// 启动 Agent 1 (Writer Agent) - 监听 8081 端口
|
||||
go runAgent1()
|
||||
|
||||
// 等待 Agent 1 启动
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 启动 Agent 2 (Caller) - 监听 8082 端口
|
||||
go runAgent2()
|
||||
|
||||
// 保持主程序运行
|
||||
fmt.Println("\n=== Agent 已启动,等待通讯测试 ===")
|
||||
fmt.Println("Agent 1 (Writer): http://localhost:8081")
|
||||
fmt.Println("Agent 2 (Caller): http://localhost:8082")
|
||||
fmt.Println("\n按 Ctrl+C 停止...")
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
// Agent 1 - Writer Agent(任务执行方)
|
||||
func runAgent1() {
|
||||
// 创建 P2P 客户端
|
||||
client, err := p2p.NewClient(&p2p.ClientConfig{
|
||||
AgentEndpoint: types.AgentEndpoint{
|
||||
AgentID: "writer-agent",
|
||||
AgentName: "作家 Agent",
|
||||
SystemID: "test-system",
|
||||
Endpoint: "http://localhost:8081/callback",
|
||||
},
|
||||
AuthSecret: "secret-123",
|
||||
CallbackPort: 8081,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Agent 1: %v", err)
|
||||
}
|
||||
|
||||
// 设置消息处理器
|
||||
client.SetMessageHandler(&writerHandler{})
|
||||
|
||||
// 启动 HTTP 服务处理回调
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
var msg types.UnifiedSynapseMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||
http.Error(w, "bad request", 400)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.HandleMessage(r.Context(), &msg)
|
||||
if err != nil {
|
||||
log.Printf("[Agent1] Error handling message: %v", err)
|
||||
http.Error(w, "internal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
respBytes, _ := json.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(respBytes)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
log.Println("[Agent1] Writer Agent 启动,监听端口 8081")
|
||||
if err := http.ListenAndServe(":8081", mux); err != nil {
|
||||
log.Fatalf("[Agent1] Server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Agent 2 - Caller Agent(任务请求方)
|
||||
func runAgent2() {
|
||||
// 创建 P2P 客户端
|
||||
client, err := p2p.NewClient(&p2p.ClientConfig{
|
||||
AgentEndpoint: types.AgentEndpoint{
|
||||
AgentID: "caller-agent",
|
||||
AgentName: "调用方 Agent",
|
||||
SystemID: "test-system",
|
||||
Endpoint: "http://localhost:8082/callback",
|
||||
},
|
||||
AuthSecret: "secret-123",
|
||||
CallbackPort: 8082,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Agent 2: %v", err)
|
||||
}
|
||||
|
||||
// 设置消息处理器
|
||||
client.SetMessageHandler(&callerHandler{})
|
||||
|
||||
// 启动 HTTP 服务
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
var msg types.UnifiedSynapseMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||
http.Error(w, "bad request", 400)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.HandleMessage(r.Context(), &msg)
|
||||
if err != nil {
|
||||
log.Printf("[Agent2] Error handling message: %v", err)
|
||||
http.Error(w, "internal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
respBytes, _ := json.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(respBytes)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
// 启动后发送测试任务
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
sendTestTask(client)
|
||||
}()
|
||||
|
||||
log.Println("[Agent2] Caller Agent 启动,监听端口 8082")
|
||||
if err := http.ListenAndServe(":8082", mux); err != nil {
|
||||
log.Fatalf("[Agent2] Server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Writer Agent 的消息处理器
|
||||
type writerHandler struct{}
|
||||
|
||||
func (h *writerHandler) HandleTaskRequest(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskRequestPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("\n=== [Agent1] 收到任务请求 ===")
|
||||
log.Printf("Task ID: %s", payload.Task.ID)
|
||||
log.Printf("Task Type: %s", payload.Task.Type)
|
||||
log.Printf("Payload: %v", payload.Task.Payload)
|
||||
|
||||
// 发送任务确认
|
||||
confirmPayload, _ := json.Marshal(types.TaskConfirmPayload{
|
||||
TaskID: payload.Task.ID,
|
||||
Confirmed: true,
|
||||
EstimatedDuration: 5,
|
||||
ExpectedCompletion: types.Timestamp(time.Now().Add(5 * time.Second).Format(time.RFC3339)),
|
||||
CapabilitiesUsed: []string{"text_generation"},
|
||||
})
|
||||
|
||||
confirmMsg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskConfirm),
|
||||
Sender: *msg.Receiver,
|
||||
Receiver: &msg.Sender,
|
||||
Payload: confirmPayload,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
log.Printf("[Agent1] 发送任务确认: %s\n", payload.Task.ID)
|
||||
|
||||
// 模拟处理,然后发送完成消息
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 发送进度更新
|
||||
progressPayload, _ := json.Marshal(types.TaskStatusUpdatePayload{
|
||||
TaskID: payload.Task.ID,
|
||||
Status: "executing",
|
||||
Progress: 50,
|
||||
Message: "正在生成文本...",
|
||||
})
|
||||
|
||||
progressMsg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskStatusUpdate),
|
||||
Sender: *msg.Receiver,
|
||||
Receiver: &msg.Sender,
|
||||
Payload: progressPayload,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
log.Printf("[Agent1] 发送进度更新: 50%%")
|
||||
_ = sendMessage(progressMsg)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 发送完成消息
|
||||
result := fmt.Sprintf("生成了一个关于「%v」的故事", payload.Task.Payload["topic"])
|
||||
completePayload, _ := json.Marshal(types.TaskCompletePayload{
|
||||
TaskID: payload.Task.ID,
|
||||
Result: struct {
|
||||
OutputType string `json:"output_type"`
|
||||
Output any `json:"output"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}{
|
||||
OutputType: "text",
|
||||
Output: result,
|
||||
Metadata: map[string]interface{}{"tokens": len(result)},
|
||||
},
|
||||
Summary: "任务已完成",
|
||||
})
|
||||
|
||||
completeMsg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskComplete),
|
||||
Sender: *msg.Receiver,
|
||||
Receiver: &msg.Sender,
|
||||
Payload: completePayload,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
log.Printf("[Agent1] 发送任务完成: %s\n", result)
|
||||
_ = sendMessage(completeMsg)
|
||||
}()
|
||||
|
||||
return confirmMsg, nil
|
||||
}
|
||||
|
||||
func (h *writerHandler) HandleTaskStatusUpdate(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
log.Printf("[Agent1] 收到状态更新: %s", msg.Type)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *writerHandler) HandleTaskComplete(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
var payload types.TaskCompletePayload
|
||||
json.Unmarshal(msg.Payload, &payload)
|
||||
log.Printf("[Agent1] 任务完成: %s", payload.Summary)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *writerHandler) HandleTaskFailed(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
var payload types.TaskFailedPayload
|
||||
json.Unmarshal(msg.Payload, &payload)
|
||||
log.Printf("[Agent1] 任务失败: %s", payload.Error.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *writerHandler) HandleNeedHuman(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
var payload types.TaskNeedHumanPayload
|
||||
json.Unmarshal(msg.Payload, &payload)
|
||||
log.Printf("[Agent1] 需要人类介入: %s", payload.Reason.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Caller Agent 的消息处理器
|
||||
type callerHandler struct{}
|
||||
|
||||
func (h *callerHandler) HandleTaskRequest(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
log.Printf("[Agent2] 收到任务请求")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (h *callerHandler) HandleTaskStatusUpdate(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
var payload types.TaskStatusUpdatePayload
|
||||
json.Unmarshal(msg.Payload, &payload)
|
||||
log.Printf("\n=== [Agent2] 收到状态更新 ===")
|
||||
log.Printf("Task ID: %s", payload.TaskID)
|
||||
log.Printf("Status: %s", payload.Status)
|
||||
log.Printf("Progress: %d%%", payload.Progress)
|
||||
log.Printf("Message: %s\n", payload.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *callerHandler) HandleTaskComplete(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
var payload types.TaskCompletePayload
|
||||
json.Unmarshal(msg.Payload, &payload)
|
||||
log.Printf("\n=== [Agent2] 收到任务完成 ===")
|
||||
log.Printf("Task ID: %s", payload.TaskID)
|
||||
log.Printf("Result: %v", payload.Result.Output)
|
||||
log.Printf("Summary: %s\n", payload.Summary)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *callerHandler) HandleTaskFailed(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
var payload types.TaskFailedPayload
|
||||
json.Unmarshal(msg.Payload, &payload)
|
||||
log.Printf("\n=== [Agent2] 收到任务失败 ===")
|
||||
log.Printf("Task ID: %s", payload.TaskID)
|
||||
log.Printf("Error: %s\n", payload.Error.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *callerHandler) HandleNeedHuman(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
var payload types.TaskNeedHumanPayload
|
||||
json.Unmarshal(msg.Payload, &payload)
|
||||
log.Printf("\n=== [Agent2] 收到人工介入请求 ===")
|
||||
log.Printf("Task ID: %s", payload.TaskID)
|
||||
log.Printf("Reason: %s\n", payload.Reason.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 发送测试任务
|
||||
func sendTestTask(client *p2p.Client) {
|
||||
log.Println("\n=== [Agent2] 发送测试任务 ===")
|
||||
|
||||
target := types.AgentEndpoint{
|
||||
AgentID: "writer-agent",
|
||||
AgentName: "作家 Agent",
|
||||
SystemID: "test-system",
|
||||
Endpoint: "http://localhost:8081/callback",
|
||||
}
|
||||
|
||||
taskID, err := client.SendTaskRequest(context.Background(), target, "text_generation", map[string]any{
|
||||
"topic": "春天",
|
||||
"style": "古诗",
|
||||
"length": "short",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[Agent2] 发送任务失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[Agent2] 任务已发送,Task ID: %s", taskID)
|
||||
|
||||
// 等待任务完成
|
||||
state, err := client.WaitForTask(context.Background(), taskID, 30*time.Second)
|
||||
if err != nil {
|
||||
log.Printf("[Agent2] 等待任务失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("\n=== [测试结果] ===")
|
||||
log.Printf("最终状态: %s", state.Status)
|
||||
log.Printf("结果: %v", state.Result)
|
||||
}
|
||||
|
||||
func sendMessage(msg *types.UnifiedSynapseMessage) error {
|
||||
body, _ := json.Marshal(msg)
|
||||
resp, err := http.Post(msg.Receiver.Endpoint, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
533
externalapi/handler.go
Normal file
533
externalapi/handler.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package externalapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// External API Layer - 非Agent调用者API
|
||||
// =============================================================================
|
||||
//
|
||||
// 这一层为非Agent调用者(如普通服务、前端、第三方应用)提供了更简单的RESTful API,
|
||||
// 隐藏了Synapse协议的复杂性。
|
||||
//
|
||||
// 与Synapse协议的区别:
|
||||
// - 使用API Key认证,而非HMAC签名
|
||||
// - 简化的请求/响应格式
|
||||
// - 支持回调和轮询两种模式
|
||||
// - 无需注册为Agent即可调用
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// API Handler
|
||||
// =============================================================================
|
||||
|
||||
// ExternalAPIHandler 处理外部API请求
|
||||
type ExternalAPIHandler struct {
|
||||
// TaskExecutor 任务执行器
|
||||
TaskExecutor TaskExecutor
|
||||
|
||||
// APIKey API密钥(如果为空则不验证)
|
||||
APIKey string
|
||||
|
||||
// MaxFileSize 最大文件大小
|
||||
MaxFileSize int64
|
||||
}
|
||||
|
||||
// TaskExecutor 任务执行器接口
|
||||
type TaskExecutor interface {
|
||||
// SubmitTask 提交任务
|
||||
SubmitTask(ctx context.Context, taskType string, payload map[string]any, externalTaskID string, priority string, callbackURL string, callbackToken string) (string, string, error)
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
GetTaskStatus(ctx context.Context, taskID string) (*types.TaskInfo, error)
|
||||
|
||||
// ListTasks 列出任务
|
||||
ListTasks(ctx context.Context, status string, limit, offset int) ([]types.TaskInfo, int, error)
|
||||
|
||||
// CancelTask 取消任务
|
||||
CancelTask(ctx context.Context, taskID string) error
|
||||
}
|
||||
|
||||
// NewExternalAPIHandler 创建外部API处理器
|
||||
func NewExternalAPIHandler(executor TaskExecutor, apiKey string) *ExternalAPIHandler {
|
||||
return &ExternalAPIHandler{
|
||||
TaskExecutor: executor,
|
||||
APIKey: apiKey,
|
||||
MaxFileSize: 100 << 20, // 100MB
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *ExternalAPIHandler) RegisterRoutes(mux *http.ServeMux, prefix string) {
|
||||
// 任务相关
|
||||
mux.HandleFunc(prefix+"/task", h.withAuth(h.handleTaskCreate))
|
||||
mux.HandleFunc(prefix+"/task/", h.withAuth(h.handleTaskDetail))
|
||||
mux.HandleFunc(prefix+"/tasks", h.withAuth(h.handleTaskList))
|
||||
|
||||
// 多部分表单提交
|
||||
mux.HandleFunc(prefix+"/task/multipart", h.withAuth(h.handleTaskMultipart))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 认证中间件
|
||||
// =============================================================================
|
||||
|
||||
func (h *ExternalAPIHandler) withAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 如果没有配置API Key,跳过验证
|
||||
if h.APIKey != "" {
|
||||
key := r.Header.Get("X-API-Key")
|
||||
if key == "" {
|
||||
key = r.URL.Query().Get("api_key")
|
||||
}
|
||||
if key != h.APIKey {
|
||||
h.writeError(w, http.StatusUnauthorized, "unauthorized", "invalid or missing API key")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// CORS
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任务创建
|
||||
// =============================================================================
|
||||
|
||||
// ExternalTaskRequest 外部任务请求
|
||||
type ExternalTaskRequest struct {
|
||||
TaskType string `json:"task_type"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
ExternalTaskID string `json:"external_task_id,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
CallbackURL string `json:"callback_url,omitempty"`
|
||||
CallbackToken string `json:"callback_token,omitempty"`
|
||||
}
|
||||
|
||||
func (h *ExternalAPIHandler) handleTaskCreate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
h.writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only POST is supported")
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusBadRequest, "bad_request", "failed to read body")
|
||||
return
|
||||
}
|
||||
|
||||
var req ExternalTaskRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
h.writeError(w, http.StatusBadRequest, "bad_request", "invalid JSON format")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.TaskType == "" {
|
||||
h.writeError(w, http.StatusBadRequest, "bad_request", "task_type is required")
|
||||
return
|
||||
}
|
||||
if req.Payload == nil {
|
||||
h.writeError(w, http.StatusBadRequest, "bad_request", "payload is required")
|
||||
return
|
||||
}
|
||||
|
||||
// 默认优先级
|
||||
if req.Priority == "" {
|
||||
req.Priority = "normal"
|
||||
}
|
||||
|
||||
// 提交任务
|
||||
taskID, status, err := h.TaskExecutor.SubmitTask(
|
||||
r.Context(),
|
||||
req.TaskType,
|
||||
req.Payload,
|
||||
req.ExternalTaskID,
|
||||
req.Priority,
|
||||
req.CallbackURL,
|
||||
req.CallbackToken,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[ExternalAPI] 任务提交失败: %v", err)
|
||||
h.writeError(w, http.StatusInternalServerError, "task_submission_failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[ExternalAPI] 任务提交成功: type=%s, task_id=%s", req.TaskType, taskID)
|
||||
|
||||
h.writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
"success": true,
|
||||
"task_id": taskID,
|
||||
"status": status,
|
||||
"message": "任务已接收,正在处理中",
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Multipart任务创建(带文件)
|
||||
// =============================================================================
|
||||
|
||||
func (h *ExternalAPIHandler) handleTaskMultipart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
h.writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only POST is supported")
|
||||
return
|
||||
}
|
||||
|
||||
// 限制请求大小
|
||||
r.Body = http.MaxBytesReader(w, r.Body, h.MaxFileSize)
|
||||
|
||||
if err := r.ParseMultipartForm(h.MaxFileSize); err != nil {
|
||||
h.writeError(w, http.StatusBadRequest, "bad_request", "failed to parse multipart form: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取请求JSON
|
||||
requestStr := r.FormValue("request")
|
||||
if requestStr == "" {
|
||||
h.writeError(w, http.StatusBadRequest, "bad_request", "missing 'request' field")
|
||||
return
|
||||
}
|
||||
|
||||
var req ExternalTaskRequest
|
||||
if err := json.Unmarshal([]byte(requestStr), &req); err != nil {
|
||||
h.writeError(w, http.StatusBadRequest, "bad_request", "invalid request JSON")
|
||||
return
|
||||
}
|
||||
|
||||
// 提取文件
|
||||
files := make(map[string][]byte)
|
||||
if r.MultipartForm != nil {
|
||||
for fieldName, fileHeaders := range r.MultipartForm.File {
|
||||
if fieldName == "request" {
|
||||
continue
|
||||
}
|
||||
for _, fh := range fileHeaders {
|
||||
file, err := fh.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files[fieldName] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将文件信息加入payload
|
||||
if len(files) > 0 {
|
||||
if req.Payload == nil {
|
||||
req.Payload = make(map[string]any)
|
||||
}
|
||||
req.Payload["__files"] = files
|
||||
}
|
||||
|
||||
// 提交任务
|
||||
taskID, status, err := h.TaskExecutor.SubmitTask(
|
||||
r.Context(),
|
||||
req.TaskType,
|
||||
req.Payload,
|
||||
req.ExternalTaskID,
|
||||
req.Priority,
|
||||
req.CallbackURL,
|
||||
req.CallbackToken,
|
||||
)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "task_submission_failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
"success": true,
|
||||
"task_id": taskID,
|
||||
"status": status,
|
||||
"message": "任务已接收,正在处理中",
|
||||
"files_uploaded": len(files),
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任务详情/状态
|
||||
// =============================================================================
|
||||
|
||||
func (h *ExternalAPIHandler) handleTaskDetail(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/synapse/task/")
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
taskID := parts[0]
|
||||
|
||||
if taskID == "" {
|
||||
h.writeError(w, http.StatusNotFound, "not_found", "task_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
// /task/{id}/status
|
||||
if len(parts) == 2 {
|
||||
switch parts[1] {
|
||||
case "status":
|
||||
h.handleTaskStatus(w, r, taskID)
|
||||
return
|
||||
case "cancel":
|
||||
h.handleTaskCancel(w, r, taskID)
|
||||
return
|
||||
default:
|
||||
h.writeError(w, http.StatusNotFound, "not_found", "unknown subpath")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// /task/{id} (GET)
|
||||
if r.Method != http.MethodGet {
|
||||
h.writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported")
|
||||
return
|
||||
}
|
||||
|
||||
task, err := h.TaskExecutor.GetTaskStatus(r.Context(), taskID)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusNotFound, "task_not_found", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.writeJSON(w, http.StatusOK, map[string]any{
|
||||
"success": true,
|
||||
"task": task,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ExternalAPIHandler) handleTaskStatus(w http.ResponseWriter, r *http.Request, taskID string) {
|
||||
if r.Method != http.MethodGet {
|
||||
h.writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported")
|
||||
return
|
||||
}
|
||||
|
||||
task, err := h.TaskExecutor.GetTaskStatus(r.Context(), taskID)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusNotFound, "task_not_found", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.writeJSON(w, http.StatusOK, map[string]any{
|
||||
"success": true,
|
||||
"task_id": task.ID,
|
||||
"status": task.Status,
|
||||
"progress": task.Progress,
|
||||
"message": task.Message,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ExternalAPIHandler) handleTaskCancel(w http.ResponseWriter, r *http.Request, taskID string) {
|
||||
if r.Method != http.MethodPost {
|
||||
h.writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only POST is supported")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.TaskExecutor.CancelTask(r.Context(), taskID); err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "cancel_failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.writeJSON(w, http.StatusOK, map[string]any{
|
||||
"success": true,
|
||||
"task_id": taskID,
|
||||
"status": "cancelled",
|
||||
"message": "任务已取消",
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任务列表
|
||||
// =============================================================================
|
||||
|
||||
func (h *ExternalAPIHandler) handleTaskList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
h.writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET is supported")
|
||||
return
|
||||
}
|
||||
|
||||
status := r.URL.Query().Get("status")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
offsetStr := r.URL.Query().Get("offset")
|
||||
|
||||
limit := 20
|
||||
offset := 0
|
||||
if limitStr != "" {
|
||||
if n, err := strconv.Atoi(limitStr); err == nil && n > 0 && n <= 100 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
if offsetStr != "" {
|
||||
if n, err := strconv.Atoi(offsetStr); err == nil && n >= 0 {
|
||||
offset = n
|
||||
}
|
||||
}
|
||||
|
||||
tasks, total, err := h.TaskExecutor.ListTasks(r.Context(), status, limit, offset)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "list_failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
hasMore := offset+limit < total
|
||||
|
||||
h.writeJSON(w, http.StatusOK, map[string]any{
|
||||
"success": true,
|
||||
"tasks": tasks,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": hasMore,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 回调发送器
|
||||
// =============================================================================
|
||||
|
||||
// CallbackSender 发送回调通知
|
||||
type CallbackSender struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewCallbackSender 创建回调发送器
|
||||
func NewCallbackSender() *CallbackSender {
|
||||
return &CallbackSender{
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// CallbackPayload 回调负载
|
||||
type CallbackPayload struct {
|
||||
MessageID string `json:"message_id"`
|
||||
MessageType string `json:"message_type"` // task_status, task_complete, task_failed
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Error *CallbackError `json:"error,omitempty"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// CallbackError 回调错误
|
||||
type CallbackError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SendCallback 发送回调
|
||||
func (s *CallbackSender) SendCallback(ctx context.Context, callbackURL, callbackToken string, payload *CallbackPayload) error {
|
||||
if callbackURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", callbackURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if callbackToken != "" {
|
||||
req.Header.Set("X-Callback-Token", callbackToken)
|
||||
}
|
||||
|
||||
resp, err := s.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("callback returned HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 辅助函数
|
||||
// =============================================================================
|
||||
|
||||
func (h *ExternalAPIHandler) writeJSON(w http.ResponseWriter, status int, data map[string]any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func (h *ExternalAPIHandler) writeError(w http.ResponseWriter, status int, code, message string) {
|
||||
h.writeJSON(w, status, map[string]any{
|
||||
"success": false,
|
||||
"error": code,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// FileInfo 上传的文件信息
|
||||
type FileInfo struct {
|
||||
Fieldname string
|
||||
Filename string
|
||||
Size int64
|
||||
Data []byte
|
||||
Header multipart.FileHeader
|
||||
}
|
||||
|
||||
// ExtractFiles 从multipart请求中提取文件
|
||||
func ExtractFiles(r *http.Request) ([]FileInfo, error) {
|
||||
if r.MultipartForm == nil {
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var files []FileInfo
|
||||
for fieldName, fileHeaders := range r.MultipartForm.File {
|
||||
for _, fh := range fileHeaders {
|
||||
file, err := fh.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, FileInfo{
|
||||
Fieldname: fieldName,
|
||||
Filename: fh.Filename,
|
||||
Size: fh.Size,
|
||||
Data: data,
|
||||
Header: *fh,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
16
go.mod
Normal file
16
go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module synapse
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.5.4
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.35.2
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
)
|
||||
16
go.sum
Normal file
16
go.sum
Normal file
@@ -0,0 +1,16 @@
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
794
group/protocol.go
Normal file
794
group/protocol.go
Normal file
@@ -0,0 +1,794 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Group Protocol Handler
|
||||
// =============================================================================
|
||||
|
||||
// GroupTaskHandler is the interface for handling group task execution
|
||||
type GroupTaskHandler interface {
|
||||
// ExecuteSubtask executes a subtask and returns the result
|
||||
ExecuteSubtask(ctx context.Context, subtaskID string, payload map[string]interface{}) (any, error)
|
||||
|
||||
// MergeResults merges multiple subtask results
|
||||
MergeResults(results []types.SubtaskResult, strategy types.MergeStrategy) (any, error)
|
||||
|
||||
// ValidateTaskSplit validates if a task can be split
|
||||
ValidateTaskSplit(taskType string, requirements map[string]interface{}) ([]types.Subtask, bool)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GroupProtocol represents the group protocol handler
|
||||
// =============================================================================
|
||||
|
||||
type GroupProtocol struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Agent identity
|
||||
agentEndpoint types.AgentEndpoint
|
||||
|
||||
// Group state
|
||||
groups map[string]*types.AgentGroup
|
||||
|
||||
// Membership
|
||||
memberships map[string]string // groupID -> member's role in group
|
||||
|
||||
// Task handler
|
||||
taskHandler GroupTaskHandler
|
||||
|
||||
// Configuration
|
||||
config *GroupConfig
|
||||
}
|
||||
|
||||
// GroupConfig holds group protocol configuration
|
||||
type GroupConfig struct {
|
||||
MaxGroupSize int
|
||||
MaxSubtasks int
|
||||
ConsensusTimeout time.Duration
|
||||
TaskTimeout time.Duration
|
||||
}
|
||||
|
||||
// DefaultGroupConfig returns default group configuration
|
||||
func DefaultGroupConfig() *GroupConfig {
|
||||
return &GroupConfig{
|
||||
MaxGroupSize: 20,
|
||||
MaxSubtasks: 50,
|
||||
ConsensusTimeout: 5 * time.Minute,
|
||||
TaskTimeout: 10 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NewGroupProtocol creates a new group protocol handler
|
||||
// =============================================================================
|
||||
|
||||
func NewGroupProtocol(agentEndpoint types.AgentEndpoint, config *GroupConfig) *GroupProtocol {
|
||||
if config == nil {
|
||||
config = DefaultGroupConfig()
|
||||
}
|
||||
return &GroupProtocol{
|
||||
agentEndpoint: agentEndpoint,
|
||||
groups: make(map[string]*types.AgentGroup),
|
||||
memberships: make(map[string]string),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// SetTaskHandler sets the group task handler
|
||||
func (g *GroupProtocol) SetTaskHandler(h GroupTaskHandler) {
|
||||
g.taskHandler = h
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HandleMessage processes an incoming group message
|
||||
// =============================================================================
|
||||
|
||||
func (g *GroupProtocol) HandleMessage(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if err := g.validateMessage(msg); err != nil {
|
||||
a2aErr, ok := err.(*types.SynapseError)
|
||||
if !ok {
|
||||
a2aErr = types.NewSynapseError(types.ErrMessageFormatInvalid, err.Error(), nil)
|
||||
}
|
||||
return g.newErrorResponse(msg, a2aErr)
|
||||
}
|
||||
|
||||
switch types.GroupMessageType(msg.Type) {
|
||||
// Group management
|
||||
case types.GroupCreate:
|
||||
return g.handleGroupCreate(ctx, msg)
|
||||
case types.GroupDisband:
|
||||
return g.handleGroupDisband(ctx, msg)
|
||||
case types.GroupJoin:
|
||||
return g.handleGroupJoin(ctx, msg)
|
||||
case types.GroupLeave:
|
||||
return g.handleGroupLeave(ctx, msg)
|
||||
case types.GroupInvite:
|
||||
return g.handleGroupInvite(ctx, msg)
|
||||
case types.GroupAcceptInvite:
|
||||
return g.handleGroupAcceptInvite(ctx, msg)
|
||||
case types.GroupRejectInvite:
|
||||
return g.handleGroupRejectInvite(ctx, msg)
|
||||
|
||||
// Group communication
|
||||
case types.GroupBroadcast:
|
||||
return g.handleGroupBroadcast(ctx, msg)
|
||||
case types.GroupMulticast:
|
||||
return g.handleGroupMulticast(ctx, msg)
|
||||
case types.GroupDirect:
|
||||
return g.handleGroupDirect(ctx, msg)
|
||||
|
||||
// Task collaboration
|
||||
case types.GroupTaskSplit:
|
||||
return g.handleGroupTaskSplit(ctx, msg)
|
||||
case types.GroupTaskAssign:
|
||||
return g.handleGroupTaskAssign(ctx, msg)
|
||||
case types.GroupTaskMerge:
|
||||
return g.handleGroupTaskMerge(ctx, msg)
|
||||
case types.GroupTaskProgress:
|
||||
return g.handleGroupTaskProgress(ctx, msg)
|
||||
|
||||
// Consensus
|
||||
case types.GroupVote:
|
||||
return g.handleGroupVote(ctx, msg)
|
||||
case types.GroupVoteSubmit:
|
||||
return g.handleGroupVoteSubmit(ctx, msg)
|
||||
|
||||
// Human-in-the-Loop
|
||||
case types.GroupConsensusEscalate:
|
||||
return g.handleGroupConsensusEscalate(ctx, msg)
|
||||
case types.GroupSubtaskEscalate:
|
||||
return g.handleGroupSubtaskEscalate(ctx, msg)
|
||||
case types.GroupHumanDecision:
|
||||
return g.handleGroupHumanDecision(ctx, msg)
|
||||
case types.InterGroupEscalate:
|
||||
return g.handleInterGroupEscalate(ctx, msg)
|
||||
|
||||
default:
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(
|
||||
types.ErrMessageFormatInvalid,
|
||||
fmt.Sprintf("Unknown group message type: %s", msg.Type),
|
||||
nil,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Group Management Handlers
|
||||
// =============================================================================
|
||||
|
||||
func (g *GroupProtocol) handleGroupCreate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.GroupCreatePayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid create payload", err.Error()))
|
||||
}
|
||||
|
||||
// Check permissions (only coordinator can create)
|
||||
// For now, allow any agent to create
|
||||
|
||||
group := &types.AgentGroup{
|
||||
ID: fmt.Sprintf("group-%d", time.Now().UnixNano()),
|
||||
Name: payload.Group.Name,
|
||||
Topology: payload.Group.Topology,
|
||||
CreatedBy: g.agentEndpoint.AgentID,
|
||||
CreatedAt: types.NowTimestamp(),
|
||||
Config: payload.Group.Config,
|
||||
Members: []types.GroupMember{},
|
||||
Roles: payload.Group.Roles,
|
||||
Status: types.GroupStatusActive,
|
||||
}
|
||||
|
||||
// Add creator as first member
|
||||
creator := types.GroupMember{
|
||||
AgentID: g.agentEndpoint.AgentID,
|
||||
AgentName: g.agentEndpoint.AgentName,
|
||||
Role: "coordinator",
|
||||
JoinedAt: types.NowTimestamp(),
|
||||
Status: types.GroupMemberActive,
|
||||
Capabilities: []string{g.agentEndpoint.Version},
|
||||
LoadFactor: 0.0,
|
||||
}
|
||||
group.Members = append(group.Members, creator)
|
||||
|
||||
g.groups[group.ID] = group
|
||||
g.memberships[group.ID] = "coordinator"
|
||||
|
||||
log.Printf("[Group] Group created: %s (topology: %s)", group.ID, group.Topology)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"group_id": group.ID,
|
||||
"status": "created",
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupDisband(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
GroupID string `json:"group_id"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid disband payload", err.Error()))
|
||||
}
|
||||
|
||||
group, ok := g.groups[payload.GroupID]
|
||||
if !ok {
|
||||
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
||||
}
|
||||
|
||||
// Check if sender is coordinator
|
||||
senderRole := g.getMemberRole(payload.GroupID, msg.Sender.AgentID)
|
||||
if senderRole != "coordinator" {
|
||||
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
||||
}
|
||||
|
||||
group.Status = types.GroupStatusDisbanded
|
||||
delete(g.groups, payload.GroupID)
|
||||
delete(g.memberships, payload.GroupID)
|
||||
|
||||
log.Printf("[Group] Group disbanded: %s", payload.GroupID)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"group_id": payload.GroupID,
|
||||
"status": "disbanded",
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupJoin(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
GroupID string `json:"group_id"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid join payload", err.Error()))
|
||||
}
|
||||
|
||||
group, ok := g.groups[payload.GroupID]
|
||||
if !ok {
|
||||
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
||||
}
|
||||
|
||||
if group.Status != types.GroupStatusActive {
|
||||
return g.newErrorResponse(msg, types.ErrGroupNotActiveError)
|
||||
}
|
||||
|
||||
// Check if already a member
|
||||
for _, m := range group.Members {
|
||||
if m.AgentID == g.agentEndpoint.AgentID {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrAlreadyMember, "Already a member", nil))
|
||||
}
|
||||
}
|
||||
|
||||
// Check group size
|
||||
if len(group.Members) >= g.config.MaxGroupSize {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrGroupFull, "Group is full", nil))
|
||||
}
|
||||
|
||||
// Add as member
|
||||
member := types.GroupMember{
|
||||
AgentID: g.agentEndpoint.AgentID,
|
||||
AgentName: g.agentEndpoint.AgentName,
|
||||
Role: "worker",
|
||||
JoinedAt: types.NowTimestamp(),
|
||||
Status: types.GroupMemberActive,
|
||||
LoadFactor: 0.0,
|
||||
}
|
||||
group.Members = append(group.Members, member)
|
||||
g.memberships[payload.GroupID] = "worker"
|
||||
|
||||
log.Printf("[Group] Agent %s joined group %s", g.agentEndpoint.AgentID, payload.GroupID)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"group_id": payload.GroupID,
|
||||
"role": "worker",
|
||||
"member_id": member.AgentID,
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupLeave(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
GroupID string `json:"group_id"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid leave payload", err.Error()))
|
||||
}
|
||||
|
||||
group, ok := g.groups[payload.GroupID]
|
||||
if !ok {
|
||||
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
||||
}
|
||||
|
||||
// Check if member
|
||||
found := false
|
||||
for i, m := range group.Members {
|
||||
if m.AgentID == g.agentEndpoint.AgentID {
|
||||
group.Members = append(group.Members[:i], group.Members[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return g.newErrorResponse(msg, types.ErrNotGroupMemberError)
|
||||
}
|
||||
|
||||
// Check if can self-leave
|
||||
if !group.Config.AllowSelfLeave && g.getMemberRole(payload.GroupID, g.agentEndpoint.AgentID) == "coordinator" {
|
||||
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
||||
}
|
||||
|
||||
delete(g.memberships, payload.GroupID)
|
||||
|
||||
log.Printf("[Group] Agent %s left group %s", g.agentEndpoint.AgentID, payload.GroupID)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"group_id": payload.GroupID,
|
||||
"status": "left",
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupInvite(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.GroupInvitePayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid invite payload", err.Error()))
|
||||
}
|
||||
|
||||
_, ok := g.groups[payload.GroupID]
|
||||
if !ok {
|
||||
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
||||
}
|
||||
|
||||
// Check if inviter is coordinator or has invite permission
|
||||
inviterRole := g.getMemberRole(payload.GroupID, msg.Sender.AgentID)
|
||||
if inviterRole != "coordinator" && inviterRole != "" {
|
||||
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
||||
}
|
||||
|
||||
log.Printf("[Group] Invitation sent to %s for group %s", payload.Invitee.AgentID, payload.GroupID)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"group_id": payload.GroupID,
|
||||
"invitee": payload.Invitee.AgentID,
|
||||
"status": "invited",
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupAcceptInvite(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
GroupID string `json:"group_id"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid accept payload", err.Error()))
|
||||
}
|
||||
|
||||
// Join the group
|
||||
return g.handleGroupJoin(ctx, msg)
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupRejectInvite(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
GroupID string `json:"group_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid reject payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[Group] Invitation rejected for group %s: %s", payload.GroupID, payload.Reason)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"group_id": payload.GroupID,
|
||||
"status": "rejected",
|
||||
"reason": payload.Reason,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Group Communication Handlers
|
||||
// =============================================================================
|
||||
|
||||
func (g *GroupProtocol) handleGroupBroadcast(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
// Broadcast is handled by the transport layer
|
||||
// This just validates the message
|
||||
log.Printf("[Group] Broadcast received in group %s from %s", msg.GroupID, msg.Sender.AgentID)
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "broadcast_received",
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupMulticast(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
_, ok := g.groups[msg.GroupID]
|
||||
if !ok {
|
||||
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
||||
}
|
||||
|
||||
// Validate targets exist
|
||||
targets := msg.TargetIDs
|
||||
for _, t := range targets {
|
||||
if !g.isMember(msg.GroupID, t) {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMemberNotFound, fmt.Sprintf("Member not found: %s", t), nil))
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[Group] Multicast in group %s to %d targets", msg.GroupID, len(targets))
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "multicast_scheduled",
|
||||
"target_count": len(targets),
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupDirect(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
if len(msg.TargetIDs) != 1 {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrInvalidParams, "Direct message requires exactly one target", nil))
|
||||
}
|
||||
|
||||
target := msg.TargetIDs[0]
|
||||
if !g.isMember(msg.GroupID, target) {
|
||||
return g.newErrorResponse(msg, types.ErrMemberNotFoundError)
|
||||
}
|
||||
|
||||
log.Printf("[Group] Direct message in group %s to %s", msg.GroupID, target)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "direct_scheduled",
|
||||
"target": target,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Task Collaboration Handlers
|
||||
// =============================================================================
|
||||
|
||||
func (g *GroupProtocol) handleGroupTaskSplit(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.GroupTaskSplitPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid task split payload", err.Error()))
|
||||
}
|
||||
|
||||
_, ok := g.groups[msg.GroupID]
|
||||
if !ok {
|
||||
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
||||
}
|
||||
|
||||
// Check if sender is coordinator
|
||||
if g.getMemberRole(msg.GroupID, msg.Sender.AgentID) != "coordinator" {
|
||||
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
||||
}
|
||||
|
||||
// Validate task split
|
||||
if g.taskHandler != nil {
|
||||
subtasks, valid := g.taskHandler.ValidateTaskSplit(payload.OriginalTask.Type, payload.OriginalTask.Requirements)
|
||||
if !valid {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrInvalidParams, "Task cannot be split", nil))
|
||||
}
|
||||
payload.Subtasks = subtasks
|
||||
}
|
||||
|
||||
// Check subtask count
|
||||
if len(payload.Subtasks) > g.config.MaxSubtasks {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrInvalidParams, fmt.Sprintf("Too many subtasks: %d", len(payload.Subtasks)), nil))
|
||||
}
|
||||
|
||||
// Assign subtasks to members
|
||||
for i := range payload.Subtasks {
|
||||
assignee := payload.Subtasks[i].AssignedTo
|
||||
if !g.isMember(msg.GroupID, assignee) {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMemberNotFound, fmt.Sprintf("Assignee not found: %s", assignee), nil))
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[Group] Task split in group %s: %d subtasks", msg.GroupID, len(payload.Subtasks))
|
||||
|
||||
// Execute subtasks asynchronously
|
||||
go g.executeSubtasks(context.Background(), msg.GroupID, payload.OriginalTask.ID, payload.Subtasks, payload.MergeStrategy)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "task_split_initiated",
|
||||
"original_task_id": payload.OriginalTask.ID,
|
||||
"subtask_count": len(payload.Subtasks),
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) executeSubtasks(ctx context.Context, groupID, originalTaskID string, subtasks []types.Subtask, strategy types.MergeStrategy) {
|
||||
if g.taskHandler == nil {
|
||||
log.Printf("[Group] No task handler configured, skipping subtask execution")
|
||||
return
|
||||
}
|
||||
|
||||
results := make([]types.SubtaskResult, 0, len(subtasks))
|
||||
|
||||
for _, st := range subtasks {
|
||||
result, err := g.taskHandler.ExecuteSubtask(ctx, st.ID, map[string]interface{}{
|
||||
"description": st.Description,
|
||||
"dependencies": st.Dependencies,
|
||||
})
|
||||
|
||||
subtaskResult := types.SubtaskResult{
|
||||
SubtaskID: st.ID,
|
||||
AssignedTo: st.AssignedTo,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
subtaskResult.Status = "failed"
|
||||
subtaskResult.Error = err.Error()
|
||||
} else {
|
||||
subtaskResult.Status = "success"
|
||||
subtaskResult.Result = result
|
||||
now := types.NowTimestamp()
|
||||
subtaskResult.CompletedAt = &now
|
||||
}
|
||||
|
||||
results = append(results, subtaskResult)
|
||||
}
|
||||
|
||||
// Merge results
|
||||
_, err := g.taskHandler.MergeResults(results, strategy)
|
||||
if err != nil {
|
||||
log.Printf("[Group] Failed to merge results: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[Group] All subtasks completed, merged result ready")
|
||||
// In a real implementation, this would send a GROUP_TASK_MERGE message
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupTaskAssign(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
SubtaskID string `json:"subtask_id"`
|
||||
AssignTo string `json:"assign_to"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid task assign payload", err.Error()))
|
||||
}
|
||||
|
||||
if !g.isMember(msg.GroupID, payload.AssignTo) {
|
||||
return g.newErrorResponse(msg, types.ErrMemberNotFoundError)
|
||||
}
|
||||
|
||||
log.Printf("[Group] Subtask %s assigned to %s in group %s", payload.SubtaskID, payload.AssignTo, msg.GroupID)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "assigned",
|
||||
"subtask_id": payload.SubtaskID,
|
||||
"assignee": payload.AssignTo,
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupTaskMerge(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.GroupTaskMergePayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid task merge payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[Group] Task merge for %s: %d results, consensus: %v",
|
||||
payload.OriginalTaskID, len(payload.SubtaskResults), payload.ConsensusAchieved)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "merged",
|
||||
"original_task_id": payload.OriginalTaskID,
|
||||
"result_ready": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupTaskProgress(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
SubtaskID string `json:"subtask_id"`
|
||||
Progress int `json:"progress"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid progress payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[Group] Progress update: subtask %s - %d%% (%s)", payload.SubtaskID, payload.Progress, payload.Message)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "progress_recorded",
|
||||
"subtask_id": payload.SubtaskID,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Consensus Handlers
|
||||
// =============================================================================
|
||||
|
||||
func (g *GroupProtocol) handleGroupVote(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.GroupVotePayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid vote payload", err.Error()))
|
||||
}
|
||||
|
||||
// Initialize vote
|
||||
payload.Status = "open"
|
||||
payload.Votes = []types.GroupVoteOption{}
|
||||
|
||||
_, ok := g.groups[msg.GroupID]
|
||||
if !ok {
|
||||
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
||||
}
|
||||
|
||||
// Check if sender is allowed to initiate vote
|
||||
senderRole := g.getMemberRole(msg.GroupID, msg.Sender.AgentID)
|
||||
if senderRole != "coordinator" && senderRole != "reviewer" {
|
||||
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
||||
}
|
||||
|
||||
log.Printf("[Group] Vote initiated in group %s: %s (threshold: %.0f%%)",
|
||||
msg.GroupID, payload.Question, payload.Threshold*100)
|
||||
|
||||
// In a real implementation, this would set up a vote session and notify voters
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "vote_initiated",
|
||||
"vote_id": payload.VoteID,
|
||||
"voters": len(payload.Voters),
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupVoteSubmit(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
VoteID string `json:"vote_id"`
|
||||
Option string `json:"option"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid vote submit payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[Group] Vote submitted: %s by %s (option: %s)", payload.VoteID, msg.Sender.AgentID, payload.Option)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "vote_recorded",
|
||||
"vote_id": payload.VoteID,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Human-in-the-Loop Handlers
|
||||
// =============================================================================
|
||||
|
||||
func (g *GroupProtocol) handleGroupConsensusEscalate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.GroupConsensusEscalatePayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid consensus escalate payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[Group] Consensus escalation in group %s (level: %d): %s",
|
||||
msg.GroupID, payload.EscalationLevel, payload.EscalateReason.Message)
|
||||
|
||||
// Store escalation and notify human decision makers
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "escalation_recorded",
|
||||
"escalation_id": payload.EscalationID,
|
||||
"human_notified": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupSubtaskEscalate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.GroupSubtaskEscalatePayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid subtask escalate payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[Group] Subtask escalation: %s in task %s (level: %d)",
|
||||
payload.Subtask.ID, payload.OriginalTask.ID, payload.EscalationLevel)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "escalation_recorded",
|
||||
"subtask_id": payload.Subtask.ID,
|
||||
"human_notified": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleGroupHumanDecision(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.HumanDecisionPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid human decision payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[Group] Human decision for task %s: %s (by %s)",
|
||||
payload.TaskID, payload.Decision.Action, payload.DecidedBy.UserName)
|
||||
|
||||
// Process the decision and resume task execution
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "decision_recorded",
|
||||
"task_id": payload.TaskID,
|
||||
"action": payload.Decision.Action,
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) handleInterGroupEscalate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload struct {
|
||||
EscalationID string `json:"escalation_id"`
|
||||
EscalationType string `json:"escalation_type"`
|
||||
// ... other fields
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid inter-group escalate payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[Group] Inter-group escalation: %s (type: %s)", payload.EscalationID, payload.EscalationType)
|
||||
|
||||
return g.newSuccessResponse(msg, map[string]interface{}{
|
||||
"status": "escalation_recorded",
|
||||
"escalation_id": payload.EscalationID,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Methods
|
||||
// =============================================================================
|
||||
|
||||
func (g *GroupProtocol) validateMessage(msg *types.UnifiedSynapseMessage) error {
|
||||
if msg.GroupID == "" {
|
||||
return types.NewSynapseError(types.ErrMessageFormatInvalid, "Missing group_id", nil)
|
||||
}
|
||||
if msg.Sender.AgentID == "" {
|
||||
return types.NewSynapseError(types.ErrMessageFormatInvalid, "Missing sender agent_id", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) getMemberRole(groupID, agentID string) string {
|
||||
group, ok := g.groups[groupID]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
for _, m := range group.Members {
|
||||
if m.AgentID == agentID {
|
||||
return m.Role
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) isMember(groupID, agentID string) bool {
|
||||
return g.getMemberRole(groupID, agentID) != ""
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) newSuccessResponse(msg *types.UnifiedSynapseMessage, data map[string]interface{}) (*types.UnifiedSynapseMessage, error) {
|
||||
resp := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PAck),
|
||||
Sender: g.agentEndpoint,
|
||||
CorrelationID: msg.CorrelationID,
|
||||
ReplyTo: msg.ID,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (g *GroupProtocol) newErrorResponse(msg *types.UnifiedSynapseMessage, err *types.SynapseError) (*types.UnifiedSynapseMessage, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// GetGroups returns all groups the agent is a member of
|
||||
func (g *GroupProtocol) GetGroups() []*types.AgentGroup {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
result := make([]*types.AgentGroup, 0, len(g.groups))
|
||||
for _, group := range g.groups {
|
||||
if g.isMember(group.ID, g.agentEndpoint.AgentID) {
|
||||
result = append(result, group)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetGroup returns a specific group
|
||||
func (g *GroupProtocol) GetGroup(groupID string) (*types.AgentGroup, bool) {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
group, ok := g.groups[groupID]
|
||||
return group, ok
|
||||
}
|
||||
706
grpcapi/client.go
Normal file
706
grpcapi/client.go
Normal file
@@ -0,0 +1,706 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// gRPC Client SDK for Synapse Protocol
|
||||
// =============================================================================
|
||||
|
||||
// GRPCClient is a high-level client for Synapse gRPC services
|
||||
type GRPCClient struct {
|
||||
conn *grpc.ClientConn
|
||||
taskClient TaskServiceClient
|
||||
messageClient A2AMessageServiceClient
|
||||
capsClient CapabilitiesServiceClient
|
||||
groupClient GroupServiceClient
|
||||
healthClient HealthServiceClient
|
||||
|
||||
// Configuration
|
||||
apiKey string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// GRPCClientConfig client configuration
|
||||
type GRPCClientConfig struct {
|
||||
Address string
|
||||
APIKey string
|
||||
Timeout time.Duration
|
||||
Insecure bool // For development only
|
||||
}
|
||||
|
||||
// NewGRPCClient creates a new gRPC client
|
||||
func NewGRPCClient(config GRPCClientConfig) (*GRPCClient, error) {
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
var opts []grpc.DialOption
|
||||
|
||||
if config.Insecure {
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
}
|
||||
|
||||
// Add interceptor for API key
|
||||
opts = append(opts, grpc.WithUnaryInterceptor(apiKeyInterceptor(config.APIKey)))
|
||||
opts = append(opts, grpc.WithStreamInterceptor(apiKeyStreamInterceptor(config.APIKey)))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, config.Address, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
|
||||
return &GRPCClient{
|
||||
conn: conn,
|
||||
taskClient: NewTaskServiceClient(conn),
|
||||
messageClient: NewA2AMessageServiceClient(conn),
|
||||
capsClient: NewCapabilitiesServiceClient(conn),
|
||||
groupClient: NewGroupServiceClient(conn),
|
||||
healthClient: NewHealthServiceClient(conn),
|
||||
apiKey: config.APIKey,
|
||||
timeout: config.Timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the client connection
|
||||
func (c *GRPCClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Interceptors
|
||||
// =============================================================================
|
||||
|
||||
func apiKeyInterceptor(apiKey string) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
if apiKey != "" {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "x-api-key", apiKey)
|
||||
}
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func apiKeyStreamInterceptor(apiKey string) grpc.StreamClientInterceptor {
|
||||
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
if apiKey != "" {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "x-api-key", apiKey)
|
||||
}
|
||||
return streamer(ctx, desc, cc, method, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Task Operations
|
||||
// =============================================================================
|
||||
|
||||
// SubmitTask submits a new task
|
||||
func (c *GRPCClient) SubmitTask(ctx context.Context, taskType string, payload map[string]any, opts ...TaskOption) (*SubmitTaskResponse, error) {
|
||||
req := &SubmitTaskRequest{
|
||||
TaskType: taskType,
|
||||
Priority: "normal",
|
||||
}
|
||||
|
||||
// Apply options
|
||||
for _, opt := range opts {
|
||||
opt(req)
|
||||
}
|
||||
|
||||
// Convert payload to Any
|
||||
if payload != nil {
|
||||
// In real implementation, use protojson.Marshal
|
||||
req.Payload = &anypb.Any{
|
||||
Value: []byte(fmt.Sprintf("%v", payload)),
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
return c.taskClient.SubmitTask(ctx, req)
|
||||
}
|
||||
|
||||
// TaskOption configures task submission
|
||||
type TaskOption func(*SubmitTaskRequest)
|
||||
|
||||
func WithExternalTaskID(id string) TaskOption {
|
||||
return func(r *SubmitTaskRequest) {
|
||||
r.ExternalTaskId = id
|
||||
}
|
||||
}
|
||||
|
||||
func WithPriority(priority string) TaskOption {
|
||||
return func(r *SubmitTaskRequest) {
|
||||
r.Priority = priority
|
||||
}
|
||||
}
|
||||
|
||||
func WithCallback(url, token string) TaskOption {
|
||||
return func(r *SubmitTaskRequest) {
|
||||
r.CallbackUrl = url
|
||||
r.CallbackToken = token
|
||||
}
|
||||
}
|
||||
|
||||
func WithTransportMode(mode TransportMode) TaskOption {
|
||||
return func(r *SubmitTaskRequest) {
|
||||
r.PreferredTransport = mode
|
||||
}
|
||||
}
|
||||
|
||||
// GetTask gets task details
|
||||
func (c *GRPCClient) GetTask(ctx context.Context, taskID string) (*TaskInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.taskClient.GetTask(ctx, &GetTaskRequest{TaskId: taskID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("task query failed: %s", resp.ErrorMessage)
|
||||
}
|
||||
|
||||
return resp.Task, nil
|
||||
}
|
||||
|
||||
// GetTaskStatus gets simplified task status
|
||||
func (c *GRPCClient) GetTaskStatus(ctx context.Context, taskID string) (*TaskInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.taskClient.GetTaskStatus(ctx, &GetTaskRequest{TaskId: taskID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("status query failed: %s", resp.ErrorMessage)
|
||||
}
|
||||
|
||||
return resp.Task, nil
|
||||
}
|
||||
|
||||
// ListTasks lists tasks with filtering
|
||||
func (c *GRPCClient) ListTasks(ctx context.Context, status string, limit, offset int) (*ListTasksResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
return c.taskClient.ListTasks(ctx, &ListTasksRequest{
|
||||
Status: status,
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
})
|
||||
}
|
||||
|
||||
// CancelTask cancels a task
|
||||
func (c *GRPCClient) CancelTask(ctx context.Context, taskID string) (*CancelTaskResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
return c.taskClient.CancelTask(ctx, &CancelTaskRequest{TaskId: taskID})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Streaming Operations
|
||||
// =============================================================================
|
||||
|
||||
// StreamTaskStatus streams task status updates
|
||||
func (c *GRPCClient) StreamTaskStatus(ctx context.Context, taskID string) (<-chan *TaskStatusUpdate, error) {
|
||||
stream, err := c.taskClient.StreamTaskStatus(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start stream: %w", err)
|
||||
}
|
||||
|
||||
// Send subscription request
|
||||
if err := stream.Send(&TaskStatusStreamRequest{
|
||||
TaskId: taskID,
|
||||
Subscribe: true,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to subscribe: %w", err)
|
||||
}
|
||||
|
||||
updateChan := make(chan *TaskStatusUpdate, 100)
|
||||
|
||||
go func() {
|
||||
defer close(updateChan)
|
||||
for {
|
||||
update, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[gRPC Stream] Receive error: %v", err)
|
||||
return
|
||||
}
|
||||
updateChan <- update
|
||||
}
|
||||
}()
|
||||
|
||||
return updateChan, nil
|
||||
}
|
||||
|
||||
// StreamMessages opens a bidirectional message stream
|
||||
func (c *GRPCClient) StreamMessages(ctx context.Context) (chan<- *A2AMessage, <-chan *A2AMessage, error) {
|
||||
stream, err := c.messageClient.StreamMessages(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to start message stream: %w", err)
|
||||
}
|
||||
|
||||
sendChan := make(chan *A2AMessage, 100)
|
||||
recvChan := make(chan *A2AMessage, 100)
|
||||
|
||||
// Send goroutine
|
||||
go func() {
|
||||
for msg := range sendChan {
|
||||
if err := stream.Send(msg); err != nil {
|
||||
log.Printf("[gRPC Stream] Send error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Receive goroutine
|
||||
go func() {
|
||||
defer close(recvChan)
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[gRPC Stream] Receive error: %v", err)
|
||||
return
|
||||
}
|
||||
recvChan <- msg
|
||||
}
|
||||
}()
|
||||
|
||||
return sendChan, recvChan, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Message Operations
|
||||
// =============================================================================
|
||||
|
||||
// SendMessage sends a single Synapse message
|
||||
func (c *GRPCClient) SendMessage(ctx context.Context, msg *A2AMessage) (*SendMessageResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
return c.messageClient.SendMessage(ctx, &SendMessageRequest{Message: msg})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Capabilities Operations
|
||||
// =============================================================================
|
||||
|
||||
// GetCapabilities gets agent capabilities
|
||||
func (c *GRPCClient) GetCapabilities(ctx context.Context) (*AgentCapabilities, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.capsClient.GetCapabilities(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("capabilities query failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
return resp.Capabilities, nil
|
||||
}
|
||||
|
||||
// ListAgents lists available agents
|
||||
func (c *GRPCClient) ListAgents(ctx context.Context) ([]*AgentInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.capsClient.ListAgents(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("agent list failed")
|
||||
}
|
||||
|
||||
return resp.Agents, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Group Operations
|
||||
// =============================================================================
|
||||
|
||||
// CreateGroup creates a new group
|
||||
func (c *GRPCClient) CreateGroup(ctx context.Context, name, description, coordinatorID string, initialMembers []string) (*GroupInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.groupClient.CreateGroup(ctx, &CreateGroupRequest{
|
||||
Name: name,
|
||||
Description: description,
|
||||
CoordinatorId: coordinatorID,
|
||||
InitialMembers: initialMembers,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("group creation failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
return resp.Group, nil
|
||||
}
|
||||
|
||||
// JoinGroup joins an existing group
|
||||
func (c *GRPCClient) JoinGroup(ctx context.Context, groupID string, agent *AgentEndpoint) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.groupClient.JoinGroup(ctx, &JoinGroupRequest{
|
||||
GroupId: groupID,
|
||||
Agent: agent,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("join failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LeaveGroup leaves a group
|
||||
func (c *GRPCClient) LeaveGroup(ctx context.Context, groupID, agentID string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.groupClient.LeaveGroup(ctx, &LeaveGroupRequest{
|
||||
GroupId: groupID,
|
||||
AgentId: agentID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("leave failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListGroupMembers lists group members
|
||||
func (c *GRPCClient) ListGroupMembers(ctx context.Context, groupID string) ([]*GroupMember, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.groupClient.ListGroupMembers(ctx, &ListGroupMembersRequest{GroupId: groupID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("member list failed")
|
||||
}
|
||||
|
||||
return resp.Members, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Voting Operations (Group Protocol)
|
||||
// =============================================================================
|
||||
|
||||
// InitiateVote initiates a vote in a group
|
||||
func (c *GRPCClient) InitiateVote(ctx context.Context, groupID, initiatorID, question string, options []string, threshold float64, timeoutSeconds int32) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.groupClient.InitiateVote(ctx, &VoteRequest{
|
||||
GroupId: groupID,
|
||||
InitiatorId: initiatorID,
|
||||
Question: question,
|
||||
Options: options,
|
||||
Threshold: threshold,
|
||||
TimeoutSeconds: timeoutSeconds,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return "", fmt.Errorf("initiate vote failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
return resp.VoteId, nil
|
||||
}
|
||||
|
||||
// SubmitVote submits a vote
|
||||
func (c *GRPCClient) SubmitVote(ctx context.Context, voteID, agentID, option, reason string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.groupClient.SubmitVote(ctx, &SubmitVoteRequest{
|
||||
VoteId: voteID,
|
||||
AgentId: agentID,
|
||||
Option: option,
|
||||
Reason: reason,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("submit vote failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVoteStatus gets the current status of a vote
|
||||
func (c *GRPCClient) GetVoteStatus(ctx context.Context, voteID string) (*VoteStatusResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
return c.groupClient.GetVoteStatus(ctx, &GetVoteStatusRequest{VoteId: voteID})
|
||||
}
|
||||
|
||||
// WaitForVote waits for a vote to complete
|
||||
func (c *GRPCClient) WaitForVote(ctx context.Context, voteID string, interval time.Duration) (*VoteStatusResponse, error) {
|
||||
if interval == 0 {
|
||||
interval = 2 * time.Second
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
status, err := c.GetVoteStatus(ctx, voteID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if status.Status == "decided" || status.Status == "expired" {
|
||||
return status, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Task Split & Merge Operations (Group Protocol)
|
||||
// =============================================================================
|
||||
|
||||
// SplitTask splits a task into subtasks
|
||||
func (c *GRPCClient) SplitTask(ctx context.Context, groupID, taskID, taskType, requirements string, strategy SplitStrategy) ([]*SubtaskInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.groupClient.SplitTask(ctx, &SplitTaskRequest{
|
||||
GroupId: groupID,
|
||||
TaskId: taskID,
|
||||
TaskType: taskType,
|
||||
Requirements: requirements,
|
||||
Strategy: strategy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("split task failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
return resp.Subtasks, nil
|
||||
}
|
||||
|
||||
// MergeResults merges subtask results
|
||||
func (c *GRPCClient) MergeResults(ctx context.Context, groupID, parentTaskID string, strategy MergeStrategy) (*MergeResultsResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
return c.groupClient.MergeResults(ctx, &MergeResultsRequest{
|
||||
GroupId: groupID,
|
||||
ParentTaskId: parentTaskID,
|
||||
Strategy: strategy,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Health Operations
|
||||
// =============================================================================
|
||||
|
||||
// HealthCheck checks server health
|
||||
func (c *GRPCClient) HealthCheck(ctx context.Context) (*HealthCheckResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
return c.healthClient.HealthCheck(ctx, &emptypb.Empty{})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// High-Level Helpers
|
||||
// =============================================================================
|
||||
|
||||
// CreateAndWait creates a task and waits for completion via streaming
|
||||
func (c *GRPCClient) CreateAndWait(ctx context.Context, taskType string, payload map[string]any, timeout time.Duration) (*TaskInfo, error) {
|
||||
// Submit task
|
||||
resp, err := c.SubmitTask(ctx, taskType, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("task submission failed: %s", resp.Message)
|
||||
}
|
||||
|
||||
// Stream status until completion
|
||||
updateChan, err := c.StreamTaskStatus(ctx, resp.TaskId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait with timeout
|
||||
if timeout == 0 {
|
||||
timeout = 5 * time.Minute
|
||||
}
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case update := <-updateChan:
|
||||
if update == nil {
|
||||
return nil, fmt.Errorf("stream closed unexpectedly")
|
||||
}
|
||||
|
||||
switch update.Status {
|
||||
case TaskStatus_TASK_STATUS_COMPLETED:
|
||||
return &TaskInfo{
|
||||
Id: resp.TaskId,
|
||||
Status: update.Status,
|
||||
Progress: update.Progress,
|
||||
Message: update.Message,
|
||||
}, nil
|
||||
case TaskStatus_TASK_STATUS_FAILED, TaskStatus_TASK_STATUS_CANCELLED:
|
||||
return nil, fmt.Errorf("task failed: %s", update.Message)
|
||||
}
|
||||
|
||||
case <-timer.C:
|
||||
return nil, fmt.Errorf("timeout waiting for task completion")
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForTask waits for a task to complete using polling
|
||||
func (c *GRPCClient) WaitForTask(ctx context.Context, taskID string, interval time.Duration) (*TaskInfo, error) {
|
||||
if interval == 0 {
|
||||
interval = 5 * time.Second
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
task, err := c.GetTaskStatus(ctx, taskID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch task.Status {
|
||||
case TaskStatus_TASK_STATUS_COMPLETED:
|
||||
return task, nil
|
||||
case TaskStatus_TASK_STATUS_FAILED, TaskStatus_TASK_STATUS_CANCELLED:
|
||||
return task, fmt.Errorf("task failed: %s", task.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Builder Pattern for A2AMessage
|
||||
// =============================================================================
|
||||
|
||||
// MessageBuilder builds Synapse messages
|
||||
type MessageBuilder struct {
|
||||
msg *A2AMessage
|
||||
}
|
||||
|
||||
// NewMessage creates a new message builder
|
||||
func NewMessage() *MessageBuilder {
|
||||
return &MessageBuilder{
|
||||
msg: &A2AMessage{
|
||||
Id: generateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) Type(msgType MessageType) *MessageBuilder {
|
||||
b.msg.Type = msgType
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) From(sender *AgentEndpoint) *MessageBuilder {
|
||||
b.msg.Sender = sender
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) To(receiver *AgentEndpoint) *MessageBuilder {
|
||||
b.msg.Receiver = receiver
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) CorrelationID(id string) *MessageBuilder {
|
||||
b.msg.CorrelationId = id
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) GroupID(id string) *MessageBuilder {
|
||||
b.msg.GroupId = id
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) Payload(payload []byte) *MessageBuilder {
|
||||
b.msg.Payload = &anypb.Any{Value: payload}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *MessageBuilder) Build() *A2AMessage {
|
||||
return b.msg
|
||||
}
|
||||
|
||||
func generateMessageID() string {
|
||||
return fmt.Sprintf("msg-%d", time.Now().UnixNano())
|
||||
}
|
||||
861
grpcapi/server.go
Normal file
861
grpcapi/server.go
Normal file
@@ -0,0 +1,861 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// gRPC Server Implementation
|
||||
// =============================================================================
|
||||
|
||||
// GRPCServer wraps the gRPC server and gateway
|
||||
type GRPCServer struct {
|
||||
grpcServer *grpc.Server
|
||||
gateway *http.Server
|
||||
mu sync.RWMutex
|
||||
|
||||
// Services
|
||||
taskService *TaskServiceImpl
|
||||
messageService *MessageServiceImpl
|
||||
capabilitiesService *CapabilitiesServiceImpl
|
||||
groupService *GroupServiceImpl
|
||||
healthService *HealthServiceImpl
|
||||
|
||||
// Dependencies
|
||||
taskExecutor TaskExecutor
|
||||
msgRouter MessageRouter
|
||||
capsProvider CapabilitiesProvider
|
||||
groupManager GroupManager
|
||||
}
|
||||
|
||||
// TaskExecutor interface for task operations
|
||||
type TaskExecutor interface {
|
||||
SubmitTask(ctx context.Context, taskType string, payload map[string]any, externalTaskID, priority, callbackURL, callbackToken string) (string, string, error)
|
||||
GetTaskStatus(ctx context.Context, taskID string) (*types.TaskInfo, error)
|
||||
ListTasks(ctx context.Context, status string, limit, offset int) ([]types.TaskInfo, int, error)
|
||||
CancelTask(ctx context.Context, taskID string) error
|
||||
}
|
||||
|
||||
type MessageRouter interface {
|
||||
RouteMessage(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error)
|
||||
}
|
||||
|
||||
type CapabilitiesProvider interface {
|
||||
GetCapabilities(ctx context.Context) (*types.AgentCapabilities, error)
|
||||
}
|
||||
|
||||
type GroupManager interface {
|
||||
CreateGroup(ctx context.Context, name, description, coordinatorID string, initialMembers []string) (string, error)
|
||||
JoinGroup(ctx context.Context, groupID string, agent *types.AgentEndpoint) error
|
||||
LeaveGroup(ctx context.Context, groupID, agentID string) error
|
||||
DisbandGroup(ctx context.Context, groupID string) error
|
||||
ListMembers(ctx context.Context, groupID string) ([]types.GroupMember, error)
|
||||
|
||||
// Voting operations
|
||||
InitiateVote(ctx context.Context, groupID, initiatorID, question string, options []string, threshold float64, timeoutSeconds int32) (string, error)
|
||||
SubmitVote(ctx context.Context, voteID, agentID, option, reason string) error
|
||||
GetVoteStatus(ctx context.Context, voteID string) (*VoteStatus, error)
|
||||
|
||||
// Task split and merge
|
||||
SplitTask(ctx context.Context, groupID, taskID, taskType, requirements string, strategy SplitStrategy) ([]*SubtaskInfo, error)
|
||||
MergeResults(ctx context.Context, groupID, parentTaskID string, strategy MergeStrategy) (*anypb.Any, string, error)
|
||||
}
|
||||
|
||||
// VoteStatus represents the status of a vote
|
||||
type VoteStatus struct {
|
||||
VoteID string
|
||||
Status string // open, decided, expired
|
||||
Question string
|
||||
TotalVotes int
|
||||
RequiredVotes int
|
||||
WinningOption string
|
||||
Votes map[string]string // agentID -> option
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Server Constructor
|
||||
// =============================================================================
|
||||
|
||||
// NewGRPCServer creates a new gRPC server
|
||||
func NewGRPCServer(taskExec TaskExecutor, msgRouter MessageRouter, capsProvider CapabilitiesProvider, groupManager GroupManager) *GRPCServer {
|
||||
return &GRPCServer{
|
||||
taskExecutor: taskExec,
|
||||
msgRouter: msgRouter,
|
||||
capsProvider: capsProvider,
|
||||
groupManager: groupManager,
|
||||
taskService: &TaskServiceImpl{},
|
||||
messageService: &MessageServiceImpl{},
|
||||
capabilitiesService: &CapabilitiesServiceImpl{},
|
||||
groupService: &GroupServiceImpl{},
|
||||
healthService: &HealthServiceImpl{},
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts both gRPC and gateway servers
|
||||
func (s *GRPCServer) Start(grpcPort, gatewayPort int) error {
|
||||
// Start gRPC server
|
||||
go func() {
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort))
|
||||
if err != nil {
|
||||
log.Fatalf("[gRPC] Failed to listen: %v", err)
|
||||
}
|
||||
|
||||
// Create gRPC server with interceptors
|
||||
opts := []grpc.ServerOption{
|
||||
grpc.UnaryInterceptor(s.unaryInterceptor),
|
||||
grpc.StreamInterceptor(s.streamInterceptor),
|
||||
}
|
||||
s.grpcServer = grpc.NewServer(opts...)
|
||||
|
||||
// Register services
|
||||
// Note: These would be generated from proto
|
||||
// RegisterTaskServiceServer(s.grpcServer, s.taskService)
|
||||
// RegisterA2AMessageServiceServer(s.grpcServer, s.messageService)
|
||||
// ... etc
|
||||
|
||||
log.Printf("[gRPC] Server started on port %d", grpcPort)
|
||||
if err := s.grpcServer.Serve(lis); err != nil {
|
||||
log.Fatalf("[gRPC] Failed to serve: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start gRPC-Gateway
|
||||
go func() {
|
||||
_ = context.Background() // ctx reserved for future gateway handler registration
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Register gateway handlers
|
||||
// Note: These would be generated from proto by grpc-gateway
|
||||
// err := RegisterTaskServiceHandlerFromEndpoint(ctx, mux, fmt.Sprintf("localhost:%d", grpcPort), opts)
|
||||
// ... etc
|
||||
|
||||
s.gateway = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", gatewayPort),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
log.Printf("[Gateway] HTTP server started on port %d", gatewayPort)
|
||||
if err := s.gateway.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("[Gateway] Failed to serve: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops both servers
|
||||
func (s *GRPCServer) Stop() {
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.GracefulStop()
|
||||
}
|
||||
if s.gateway != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
s.gateway.Shutdown(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Interceptors
|
||||
// =============================================================================
|
||||
|
||||
func (s *GRPCServer) unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
// Extract metadata
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if ok {
|
||||
// Check API key
|
||||
apiKeys := md.Get("x-api-key")
|
||||
if len(apiKeys) > 0 {
|
||||
// Validate API key
|
||||
log.Printf("[gRPC] API Key: %s", apiKeys[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Call handler
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
func (s *GRPCServer) streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
// Extract metadata
|
||||
md, ok := metadata.FromIncomingContext(ss.Context())
|
||||
if ok {
|
||||
apiKeys := md.Get("x-api-key")
|
||||
if len(apiKeys) > 0 {
|
||||
log.Printf("[gRPC Stream] API Key: %s", apiKeys[0])
|
||||
}
|
||||
}
|
||||
|
||||
return handler(srv, ss)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Task Service Implementation
|
||||
// =============================================================================
|
||||
|
||||
type TaskServiceImpl struct {
|
||||
UnimplementedTaskServiceServer
|
||||
taskExecutor TaskExecutor
|
||||
mu sync.RWMutex
|
||||
streamClients map[string]chan *TaskStatusUpdate
|
||||
}
|
||||
|
||||
func (s *TaskServiceImpl) SubmitTask(ctx context.Context, req *SubmitTaskRequest) (*SubmitTaskResponse, error) {
|
||||
// Convert payload
|
||||
payload := make(map[string]any)
|
||||
if req.Payload != nil {
|
||||
// Unpack Any to map
|
||||
// This would use protojson or similar
|
||||
}
|
||||
|
||||
taskID, status, err := s.taskExecutor.SubmitTask(
|
||||
ctx,
|
||||
req.TaskType,
|
||||
payload,
|
||||
req.ExternalTaskId,
|
||||
req.Priority,
|
||||
req.CallbackUrl,
|
||||
req.CallbackToken,
|
||||
)
|
||||
if err != nil {
|
||||
return &SubmitTaskResponse{
|
||||
Success: false,
|
||||
ErrorCode: "TASK_SUBMISSION_FAILED",
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &SubmitTaskResponse{
|
||||
Success: true,
|
||||
TaskId: taskID,
|
||||
Status: parseTaskStatus(status),
|
||||
Message: "任务已接收",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *TaskServiceImpl) SubmitTaskWithFiles(ctx context.Context, req *SubmitTaskWithFilesRequest) (*SubmitTaskResponse, error) {
|
||||
// Handle file uploads
|
||||
// Convert files and call SubmitTask
|
||||
return s.SubmitTask(ctx, req.Request)
|
||||
}
|
||||
|
||||
func (s *TaskServiceImpl) GetTask(ctx context.Context, req *GetTaskRequest) (*TaskStatusResponse, error) {
|
||||
task, err := s.taskExecutor.GetTaskStatus(ctx, req.TaskId)
|
||||
if err != nil {
|
||||
return &TaskStatusResponse{
|
||||
Success: false,
|
||||
ErrorCode: "TASK_NOT_FOUND",
|
||||
ErrorMessage: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &TaskStatusResponse{
|
||||
Success: true,
|
||||
Task: convertTaskInfo(task),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *TaskServiceImpl) GetTaskStatus(ctx context.Context, req *GetTaskRequest) (*TaskStatusResponse, error) {
|
||||
return s.GetTask(ctx, req)
|
||||
}
|
||||
|
||||
func (s *TaskServiceImpl) ListTasks(ctx context.Context, req *ListTasksRequest) (*ListTasksResponse, error) {
|
||||
tasks, total, err := s.taskExecutor.ListTasks(ctx, req.Status, int(req.Limit), int(req.Offset))
|
||||
if err != nil {
|
||||
return &ListTasksResponse{
|
||||
Success: false,
|
||||
}, err
|
||||
}
|
||||
|
||||
var protoTasks []*TaskInfo
|
||||
for _, t := range tasks {
|
||||
protoTasks = append(protoTasks, convertTaskInfo(&t))
|
||||
}
|
||||
|
||||
return &ListTasksResponse{
|
||||
Success: true,
|
||||
Tasks: protoTasks,
|
||||
Total: int32(total),
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
HasMore: int(req.Offset+req.Limit) < total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *TaskServiceImpl) CancelTask(ctx context.Context, req *CancelTaskRequest) (*CancelTaskResponse, error) {
|
||||
if err := s.taskExecutor.CancelTask(ctx, req.TaskId); err != nil {
|
||||
return &CancelTaskResponse{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &CancelTaskResponse{
|
||||
Success: true,
|
||||
TaskId: req.TaskId,
|
||||
Status: TaskStatus_TASK_STATUS_CANCELLED,
|
||||
Message: "任务已取消",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StreamTaskStatus implements bidirectional streaming for task status updates
|
||||
func (s *TaskServiceImpl) StreamTaskStatus(stream TaskService_StreamTaskStatusServer) error {
|
||||
// Register client
|
||||
clientID := fmt.Sprintf("stream-%d", time.Now().UnixNano())
|
||||
statusChan := make(chan *TaskStatusUpdate, 100)
|
||||
|
||||
s.mu.Lock()
|
||||
s.streamClients[clientID] = statusChan
|
||||
s.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.streamClients, clientID)
|
||||
s.mu.Unlock()
|
||||
close(statusChan)
|
||||
}()
|
||||
|
||||
// Goroutine to send status updates
|
||||
go func() {
|
||||
for update := range statusChan {
|
||||
if err := stream.Send(update); err != nil {
|
||||
log.Printf("[gRPC Stream] Send error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Receive subscription requests
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Subscribe {
|
||||
log.Printf("[gRPC Stream] Client %s subscribed to task %s", clientID, req.TaskId)
|
||||
// Start watching task status
|
||||
go s.watchTaskStatus(req.TaskId, statusChan)
|
||||
} else {
|
||||
log.Printf("[gRPC Stream] Client %s unsubscribed from task %s", clientID, req.TaskId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskServiceImpl) watchTaskStatus(taskID string, ch chan *TaskStatusUpdate) {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
task, err := s.taskExecutor.GetTaskStatus(context.Background(), taskID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ch <- &TaskStatusUpdate{
|
||||
TaskId: taskID,
|
||||
Status: parseTaskStatus(task.Status),
|
||||
Progress: int32(task.Progress),
|
||||
Message: task.Message,
|
||||
}
|
||||
|
||||
// Stop if terminal state
|
||||
if task.Status == "completed" || task.Status == "failed" || task.Status == "cancelled" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Message Service Implementation
|
||||
// =============================================================================
|
||||
|
||||
type MessageServiceImpl struct {
|
||||
UnimplementedA2AMessageServiceServer
|
||||
msgRouter MessageRouter
|
||||
}
|
||||
|
||||
func (s *MessageServiceImpl) SendMessage(ctx context.Context, req *SendMessageRequest) (*SendMessageResponse, error) {
|
||||
// Convert proto message to internal format
|
||||
msg := convertProtoToInternalMessage(req.Message)
|
||||
|
||||
resp, err := s.msgRouter.RouteMessage(ctx, msg)
|
||||
if err != nil {
|
||||
return &SendMessageResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &SendMessageResponse{
|
||||
Success: true,
|
||||
MessageId: resp.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StreamMessages implements bidirectional streaming for Synapse messages
|
||||
func (s *MessageServiceImpl) StreamMessages(stream A2AMessageService_StreamMessagesServer) error {
|
||||
ctx := stream.Context()
|
||||
clientID := fmt.Sprintf("msg-stream-%d", time.Now().UnixNano())
|
||||
|
||||
log.Printf("[gRPC Stream] New message stream: %s", clientID)
|
||||
|
||||
// Goroutine to receive messages
|
||||
go func() {
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[gRPC Stream] Receive error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
internalMsg := convertProtoToInternalMessage(msg)
|
||||
resp, err := s.msgRouter.RouteMessage(ctx, internalMsg)
|
||||
if err != nil {
|
||||
log.Printf("[gRPC Stream] Route error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
protoResp := convertInternalToProtoMessage(resp)
|
||||
if err := stream.Send(protoResp); err != nil {
|
||||
log.Printf("[gRPC Stream] Send error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Keep stream alive
|
||||
<-ctx.Done()
|
||||
log.Printf("[gRPC Stream] Message stream closed: %s", clientID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MessageServiceImpl) ReceiveCallback(req *emptypb.Empty, stream A2AMessageService_ReceiveCallbackServer) error {
|
||||
// This would be used by agents to receive callback messages via gRPC stream
|
||||
// Implementation depends on callback routing mechanism
|
||||
return status.Error(codes.Unimplemented, "not implemented")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Capabilities Service Implementation
|
||||
// =============================================================================
|
||||
|
||||
type CapabilitiesServiceImpl struct {
|
||||
UnimplementedCapabilitiesServiceServer
|
||||
provider CapabilitiesProvider
|
||||
}
|
||||
|
||||
func (s *CapabilitiesServiceImpl) GetCapabilities(ctx context.Context, req *emptypb.Empty) (*CapabilitiesResponse, error) {
|
||||
caps, err := s.provider.GetCapabilities(ctx)
|
||||
if err != nil {
|
||||
return &CapabilitiesResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &CapabilitiesResponse{
|
||||
Success: true,
|
||||
Capabilities: convertAgentCapabilities(caps),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CapabilitiesServiceImpl) QueryAgent(ctx context.Context, req *QueryAgentRequest) (*CapabilitiesResponse, error) {
|
||||
// Query specific agent's capabilities
|
||||
return s.GetCapabilities(ctx, nil)
|
||||
}
|
||||
|
||||
func (s *CapabilitiesServiceImpl) ListAgents(ctx context.Context, req *emptypb.Empty) (*ListAgentsResponse, error) {
|
||||
return &ListAgentsResponse{
|
||||
Success: true,
|
||||
Agents: []*AgentInfo{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Group Service Implementation
|
||||
// =============================================================================
|
||||
|
||||
type GroupServiceImpl struct {
|
||||
UnimplementedGroupServiceServer
|
||||
groupManager GroupManager
|
||||
}
|
||||
|
||||
func (s *GroupServiceImpl) CreateGroup(ctx context.Context, req *CreateGroupRequest) (*CreateGroupResponse, error) {
|
||||
groupID, err := s.groupManager.CreateGroup(ctx, req.Name, req.Description, req.CoordinatorId, req.InitialMembers)
|
||||
if err != nil {
|
||||
return &CreateGroupResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &CreateGroupResponse{
|
||||
Success: true,
|
||||
GroupId: groupID,
|
||||
Group: &GroupInfo{
|
||||
Id: groupID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
CoordinatorId: req.CoordinatorId,
|
||||
Status: "active",
|
||||
CreatedAt: timestamppb.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GroupServiceImpl) JoinGroup(ctx context.Context, req *JoinGroupRequest) (*JoinGroupResponse, error) {
|
||||
err := s.groupManager.JoinGroup(ctx, req.GroupId, convertProtoEndpoint(req.Agent))
|
||||
if err != nil {
|
||||
return &JoinGroupResponse{Success: false, Error: err.Error()}, nil
|
||||
}
|
||||
return &JoinGroupResponse{Success: true}, nil
|
||||
}
|
||||
|
||||
func (s *GroupServiceImpl) LeaveGroup(ctx context.Context, req *LeaveGroupRequest) (*LeaveGroupResponse, error) {
|
||||
err := s.groupManager.LeaveGroup(ctx, req.GroupId, req.AgentId)
|
||||
if err != nil {
|
||||
return &LeaveGroupResponse{Success: false, Error: err.Error()}, nil
|
||||
}
|
||||
return &LeaveGroupResponse{Success: true}, nil
|
||||
}
|
||||
|
||||
func (s *GroupServiceImpl) DisbandGroup(ctx context.Context, req *DisbandGroupRequest) (*DisbandGroupResponse, error) {
|
||||
err := s.groupManager.DisbandGroup(ctx, req.GroupId)
|
||||
if err != nil {
|
||||
return &DisbandGroupResponse{Success: false, Error: err.Error()}, nil
|
||||
}
|
||||
return &DisbandGroupResponse{Success: true}, nil
|
||||
}
|
||||
|
||||
func (s *GroupServiceImpl) ListGroupMembers(ctx context.Context, req *ListGroupMembersRequest) (*ListGroupMembersResponse, error) {
|
||||
members, err := s.groupManager.ListMembers(ctx, req.GroupId)
|
||||
if err != nil {
|
||||
return &ListGroupMembersResponse{Success: false}, nil
|
||||
}
|
||||
|
||||
var protoMembers []*GroupMember
|
||||
for _, m := range members {
|
||||
protoMembers = append(protoMembers, &GroupMember{
|
||||
AgentId: m.AgentID,
|
||||
AgentName: m.AgentName,
|
||||
Role: m.Role,
|
||||
Status: string(m.Status),
|
||||
JoinedAt: timestamppb.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return &ListGroupMembersResponse{
|
||||
Success: true,
|
||||
Members: protoMembers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StreamGroupMessages implements bidirectional streaming for group messages
|
||||
func (s *GroupServiceImpl) StreamGroupMessages(stream GroupService_StreamGroupMessagesServer) error {
|
||||
ctx := stream.Context()
|
||||
clientID := fmt.Sprintf("group-stream-%d", time.Now().UnixNano())
|
||||
|
||||
log.Printf("[gRPC Stream] New group stream: %s", clientID)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[gRPC Stream] Receive error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[gRPC Stream] Group message received: group=%s, type=%v", msg.GroupId, msg.Type)
|
||||
|
||||
// Echo back for now
|
||||
if err := stream.Send(msg); err != nil {
|
||||
log.Printf("[gRPC Stream] Send error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
log.Printf("[gRPC Stream] Group stream closed: %s", clientID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitiateVote initiates a voting process in the group
|
||||
func (s *GroupServiceImpl) InitiateVote(ctx context.Context, req *VoteRequest) (*VoteResponse, error) {
|
||||
voteID, err := s.groupManager.InitiateVote(ctx, req.GroupId, req.InitiatorId, req.Question, req.Options, req.Threshold, req.TimeoutSeconds)
|
||||
if err != nil {
|
||||
return &VoteResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &VoteResponse{
|
||||
Success: true,
|
||||
VoteId: voteID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SubmitVote submits a vote in an ongoing voting process
|
||||
func (s *GroupServiceImpl) SubmitVote(ctx context.Context, req *SubmitVoteRequest) (*SubmitVoteResponse, error) {
|
||||
err := s.groupManager.SubmitVote(ctx, req.VoteId, req.AgentId, req.Option, req.Reason)
|
||||
if err != nil {
|
||||
return &SubmitVoteResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &SubmitVoteResponse{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetVoteStatus gets the current status of a vote
|
||||
func (s *GroupServiceImpl) GetVoteStatus(ctx context.Context, req *GetVoteStatusRequest) (*VoteStatusResponse, error) {
|
||||
status, err := s.groupManager.GetVoteStatus(ctx, req.VoteId)
|
||||
if err != nil {
|
||||
return &VoteStatusResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &VoteStatusResponse{
|
||||
Success: true,
|
||||
VoteId: status.VoteID,
|
||||
Status: status.Status,
|
||||
Question: status.Question,
|
||||
TotalVotes: int32(status.TotalVotes),
|
||||
RequiredVotes: int32(status.RequiredVotes),
|
||||
WinningOption: status.WinningOption,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SplitTask splits a task into subtasks across group members
|
||||
func (s *GroupServiceImpl) SplitTask(ctx context.Context, req *SplitTaskRequest) (*SplitTaskResponse, error) {
|
||||
subtasks, err := s.groupManager.SplitTask(ctx, req.GroupId, req.TaskId, req.TaskType, req.Requirements, req.Strategy)
|
||||
if err != nil {
|
||||
return &SplitTaskResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &SplitTaskResponse{
|
||||
Success: true,
|
||||
GroupId: req.GroupId,
|
||||
ParentTaskId: req.TaskId,
|
||||
Subtasks: subtasks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MergeResults merges results from subtasks
|
||||
func (s *GroupServiceImpl) MergeResults(ctx context.Context, req *MergeResultsRequest) (*MergeResultsResponse, error) {
|
||||
mergedResult, summary, err := s.groupManager.MergeResults(ctx, req.GroupId, req.ParentTaskId, req.Strategy)
|
||||
if err != nil {
|
||||
return &MergeResultsResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &MergeResultsResponse{
|
||||
Success: true,
|
||||
ParentTaskId: req.ParentTaskId,
|
||||
MergedResult: mergedResult,
|
||||
Summary: summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Health Service Implementation
|
||||
// =============================================================================
|
||||
|
||||
type HealthServiceImpl struct {
|
||||
UnimplementedHealthServiceServer
|
||||
}
|
||||
|
||||
func (s *HealthServiceImpl) HealthCheck(ctx context.Context, req *emptypb.Empty) (*HealthCheckResponse, error) {
|
||||
return &HealthCheckResponse{
|
||||
Status: "healthy",
|
||||
Version: "1.0.0",
|
||||
UptimeSeconds: 3600,
|
||||
ActiveTasks: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Conversion Helpers
|
||||
// =============================================================================
|
||||
|
||||
func parseTaskStatus(status string) TaskStatus {
|
||||
switch status {
|
||||
case "received":
|
||||
return TaskStatus_TASK_STATUS_RECEIVED
|
||||
case "processing":
|
||||
return TaskStatus_TASK_STATUS_PROCESSING
|
||||
case "completed":
|
||||
return TaskStatus_TASK_STATUS_COMPLETED
|
||||
case "failed":
|
||||
return TaskStatus_TASK_STATUS_FAILED
|
||||
case "cancelled":
|
||||
return TaskStatus_TASK_STATUS_CANCELLED
|
||||
case "rejected":
|
||||
return TaskStatus_TASK_STATUS_REJECTED
|
||||
case "waiting":
|
||||
return TaskStatus_TASK_STATUS_WAITING
|
||||
default:
|
||||
return TaskStatus_TASK_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func convertTaskInfo(task *types.TaskInfo) *TaskInfo {
|
||||
if task == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result *anypb.Any
|
||||
if task.Result != nil {
|
||||
result, _ = anypb.New(&anypb.Any{Value: []byte(fmt.Sprintf("%v", task.Result))})
|
||||
}
|
||||
|
||||
return &TaskInfo{
|
||||
Id: task.ID,
|
||||
Type: task.Type,
|
||||
Status: parseTaskStatus(task.Status),
|
||||
Progress: int32(task.Progress),
|
||||
Message: task.Message,
|
||||
CreatedAt: timestamppb.Now(),
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
func convertAgentCapabilities(caps *types.AgentCapabilities) *AgentCapabilities {
|
||||
if caps == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var taskTypes []*SupportedTaskType
|
||||
for _, tt := range caps.SupportedTaskTypes {
|
||||
taskTypes = append(taskTypes, &SupportedTaskType{
|
||||
Type: tt.Type,
|
||||
Name: tt.Name,
|
||||
Description: tt.Description,
|
||||
EstimatedTime: tt.EstimatedTime,
|
||||
InputSchema: tt.InputSchema,
|
||||
OutputSchema: tt.OutputSchema,
|
||||
Capabilities: tt.Capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
var transportModes []TransportMode
|
||||
for _, tm := range caps.SupportedTransportModes {
|
||||
transportModes = append(transportModes, TransportMode(TransportMode_value[string(tm)]))
|
||||
}
|
||||
|
||||
return &AgentCapabilities{
|
||||
AgentId: caps.AgentID,
|
||||
AgentName: caps.AgentName,
|
||||
SystemId: caps.SystemID,
|
||||
ProtocolVersion: caps.ProtocolVersion,
|
||||
SupportedTaskTypes: taskTypes,
|
||||
SupportedContentTypes: caps.SupportedContentTypes,
|
||||
SupportedTransportModes: transportModes,
|
||||
MaxFileSize: caps.MaxFileSize,
|
||||
MaxConcurrentTasks: int32(caps.MaxConcurrentTasks),
|
||||
MaxTaskDurationSeconds: int32(caps.MaxTaskDuration),
|
||||
SupportProgressUpdates: caps.SupportProgressUpdates,
|
||||
SupportStreaming: caps.SupportStreaming,
|
||||
SupportHil: caps.SupportHiL,
|
||||
SupportGroups: caps.SupportGroups,
|
||||
}
|
||||
}
|
||||
|
||||
func convertProtoToInternalMessage(msg *A2AMessage) *types.UnifiedSynapseMessage {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
if msg.Payload != nil {
|
||||
payload = msg.Payload.Value
|
||||
}
|
||||
|
||||
return &types.UnifiedSynapseMessage{
|
||||
ID: msg.Id,
|
||||
Version: msg.Version,
|
||||
Type: string(MessageType_name[int32(msg.Type)]),
|
||||
Sender: types.AgentEndpoint{AgentID: msg.Sender.AgentId},
|
||||
CorrelationID: msg.CorrelationId,
|
||||
ReplyTo: msg.ReplyTo,
|
||||
GroupID: msg.GroupId,
|
||||
Timestamp: types.Timestamp(msg.Timestamp.AsTime().Format(time.RFC3339)),
|
||||
Payload: payload,
|
||||
Signature: msg.Signature,
|
||||
}
|
||||
}
|
||||
|
||||
func convertInternalToProtoMessage(msg *types.UnifiedSynapseMessage) *A2AMessage {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload *anypb.Any
|
||||
if len(msg.Payload) > 0 {
|
||||
payload = &anypb.Any{Value: msg.Payload}
|
||||
}
|
||||
|
||||
return &A2AMessage{
|
||||
Id: msg.ID,
|
||||
Version: msg.Version,
|
||||
Type: MessageType(MessageType_value[msg.Type]),
|
||||
Sender: &AgentEndpoint{AgentId: msg.Sender.AgentID},
|
||||
CorrelationId: msg.CorrelationID,
|
||||
ReplyTo: msg.ReplyTo,
|
||||
GroupId: msg.GroupID,
|
||||
Timestamp: timestamppb.Now(),
|
||||
Payload: payload,
|
||||
Signature: msg.Signature,
|
||||
}
|
||||
}
|
||||
|
||||
func convertProtoEndpoint(endpoint *AgentEndpoint) *types.AgentEndpoint {
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.AgentEndpoint{
|
||||
AgentID: endpoint.AgentId,
|
||||
AgentName: endpoint.AgentName,
|
||||
SystemID: endpoint.SystemId,
|
||||
Endpoint: endpoint.Endpoint,
|
||||
Version: endpoint.Version,
|
||||
}
|
||||
}
|
||||
850
grpcapi/synapse.pb.go
Normal file
850
grpcapi/synapse.pb.go
Normal file
@@ -0,0 +1,850 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// version: proto synapse v1.0.0
|
||||
// This file contains manually created stubs matching proto/synapse.proto
|
||||
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
anypb "google.golang.org/protobuf/types/known/anypb"
|
||||
timestamp "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// TransportMode represents the transport mode
|
||||
type TransportMode int32
|
||||
|
||||
const (
|
||||
TransportMode_TRANSPORT_MODE_UNSPECIFIED TransportMode = 0
|
||||
TransportMode_TRANSPORT_MODE_CALLBACK TransportMode = 1
|
||||
TransportMode_TRANSPORT_MODE_POLLING TransportMode = 2
|
||||
TransportMode_TRANSPORT_MODE_SYNC TransportMode = 3
|
||||
TransportMode_TRANSPORT_MODE_HYBRID TransportMode = 4
|
||||
)
|
||||
|
||||
var TransportMode_name = map[int32]string{
|
||||
0: "TRANSPORT_MODE_UNSPECIFIED",
|
||||
1: "TRANSPORT_MODE_CALLBACK",
|
||||
2: "TRANSPORT_MODE_POLLING",
|
||||
3: "TRANSPORT_MODE_SYNC",
|
||||
4: "TRANSPORT_MODE_HYBRID",
|
||||
}
|
||||
|
||||
var TransportMode_value = map[string]int32{
|
||||
"TRANSPORT_MODE_UNSPECIFIED": 0,
|
||||
"TRANSPORT_MODE_CALLBACK": 1,
|
||||
"TRANSPORT_MODE_POLLING": 2,
|
||||
"TRANSPORT_MODE_SYNC": 3,
|
||||
"TRANSPORT_MODE_HYBRID": 4,
|
||||
}
|
||||
|
||||
// MessageType represents the type of Synapse message
|
||||
type MessageType int32
|
||||
|
||||
const (
|
||||
MessageType_MESSAGE_TYPE_UNSPECIFIED MessageType = 0
|
||||
MessageType_MESSAGE_TYPE_P2P_TASK_REQUEST MessageType = 1
|
||||
MessageType_MESSAGE_TYPE_P2P_TASK_COMPLETE MessageType = 2
|
||||
MessageType_MESSAGE_TYPE_P2P_TASK_FAILED MessageType = 3
|
||||
MessageType_MESSAGE_TYPE_P2P_STATUS_UPDATE MessageType = 4
|
||||
MessageType_MESSAGE_TYPE_P2P_INFO_MISSING MessageType = 5
|
||||
MessageType_MESSAGE_TYPE_P2P_ACK MessageType = 6
|
||||
MessageType_MESSAGE_TYPE_P2P_NACK MessageType = 7
|
||||
MessageType_MESSAGE_TYPE_GROUP_INVITE MessageType = 8
|
||||
MessageType_MESSAGE_TYPE_GROUP_JOIN MessageType = 9
|
||||
MessageType_MESSAGE_TYPE_GROUP_LEAVE MessageType = 10
|
||||
MessageType_MESSAGE_TYPE_GROUP_TASK_SPLIT MessageType = 11
|
||||
MessageType_MESSAGE_TYPE_GROUP_SUBTASK_RESULT MessageType = 12
|
||||
MessageType_MESSAGE_TYPE_GROUP_RESULT_MERGE MessageType = 13
|
||||
MessageType_MESSAGE_TYPE_GROUP_VOTE_INITIATE MessageType = 14
|
||||
MessageType_MESSAGE_TYPE_GROUP_VOTE_SUBMIT MessageType = 15
|
||||
MessageType_MESSAGE_TYPE_GROUP_VOTE_RESULT MessageType = 16
|
||||
MessageType_MESSAGE_TYPE_HIL_DECISION_REQUEST MessageType = 17
|
||||
MessageType_MESSAGE_TYPE_HIL_DECISION_RESPONSE MessageType = 18
|
||||
MessageType_MESSAGE_TYPE_HEARTBEAT MessageType = 19
|
||||
)
|
||||
|
||||
var MessageType_name = map[int32]string{
|
||||
0: "MESSAGE_TYPE_UNSPECIFIED",
|
||||
1: "MESSAGE_TYPE_P2P_TASK_REQUEST",
|
||||
2: "MESSAGE_TYPE_P2P_TASK_COMPLETE",
|
||||
3: "MESSAGE_TYPE_P2P_TASK_FAILED",
|
||||
4: "MESSAGE_TYPE_P2P_STATUS_UPDATE",
|
||||
5: "MESSAGE_TYPE_P2P_INFO_MISSING",
|
||||
6: "MESSAGE_TYPE_P2P_ACK",
|
||||
7: "MESSAGE_TYPE_P2P_NACK",
|
||||
8: "MESSAGE_TYPE_GROUP_INVITE",
|
||||
9: "MESSAGE_TYPE_GROUP_JOIN",
|
||||
10: "MESSAGE_TYPE_GROUP_LEAVE",
|
||||
11: "MESSAGE_TYPE_GROUP_TASK_SPLIT",
|
||||
12: "MESSAGE_TYPE_GROUP_SUBTASK_RESULT",
|
||||
13: "MESSAGE_TYPE_GROUP_RESULT_MERGE",
|
||||
14: "MESSAGE_TYPE_GROUP_VOTE_INITIATE",
|
||||
15: "MESSAGE_TYPE_GROUP_VOTE_SUBMIT",
|
||||
16: "MESSAGE_TYPE_GROUP_VOTE_RESULT",
|
||||
17: "MESSAGE_TYPE_HIL_DECISION_REQUEST",
|
||||
18: "MESSAGE_TYPE_HIL_DECISION_RESPONSE",
|
||||
19: "MESSAGE_TYPE_HEARTBEAT",
|
||||
}
|
||||
|
||||
var MessageType_value = map[string]int32{
|
||||
"MESSAGE_TYPE_UNSPECIFIED": 0,
|
||||
"MESSAGE_TYPE_P2P_TASK_REQUEST": 1,
|
||||
"MESSAGE_TYPE_P2P_TASK_COMPLETE": 2,
|
||||
"MESSAGE_TYPE_P2P_TASK_FAILED": 3,
|
||||
"MESSAGE_TYPE_P2P_STATUS_UPDATE": 4,
|
||||
"MESSAGE_TYPE_P2P_INFO_MISSING": 5,
|
||||
"MESSAGE_TYPE_P2P_ACK": 6,
|
||||
"MESSAGE_TYPE_P2P_NACK": 7,
|
||||
"MESSAGE_TYPE_GROUP_INVITE": 8,
|
||||
"MESSAGE_TYPE_GROUP_JOIN": 9,
|
||||
"MESSAGE_TYPE_GROUP_LEAVE": 10,
|
||||
"MESSAGE_TYPE_GROUP_TASK_SPLIT": 11,
|
||||
"MESSAGE_TYPE_GROUP_SUBTASK_RESULT": 12,
|
||||
"MESSAGE_TYPE_GROUP_RESULT_MERGE": 13,
|
||||
"MESSAGE_TYPE_GROUP_VOTE_INITIATE": 14,
|
||||
"MESSAGE_TYPE_GROUP_VOTE_SUBMIT": 15,
|
||||
"MESSAGE_TYPE_GROUP_VOTE_RESULT": 16,
|
||||
"MESSAGE_TYPE_HIL_DECISION_REQUEST": 17,
|
||||
"MESSAGE_TYPE_HIL_DECISION_RESPONSE": 18,
|
||||
"MESSAGE_TYPE_HEARTBEAT": 19,
|
||||
}
|
||||
|
||||
// TaskStatus represents the status of a task
|
||||
type TaskStatus int32
|
||||
|
||||
const (
|
||||
TaskStatus_TASK_STATUS_UNSPECIFIED TaskStatus = 0
|
||||
TaskStatus_TASK_STATUS_RECEIVED TaskStatus = 1
|
||||
TaskStatus_TASK_STATUS_PROCESSING TaskStatus = 2
|
||||
TaskStatus_TASK_STATUS_COMPLETED TaskStatus = 3
|
||||
TaskStatus_TASK_STATUS_FAILED TaskStatus = 4
|
||||
TaskStatus_TASK_STATUS_CANCELLED TaskStatus = 5
|
||||
TaskStatus_TASK_STATUS_REJECTED TaskStatus = 6
|
||||
TaskStatus_TASK_STATUS_WAITING TaskStatus = 7
|
||||
)
|
||||
|
||||
var TaskStatus_name = map[int32]string{
|
||||
0: "TASK_STATUS_UNSPECIFIED",
|
||||
1: "TASK_STATUS_RECEIVED",
|
||||
2: "TASK_STATUS_PROCESSING",
|
||||
3: "TASK_STATUS_COMPLETED",
|
||||
4: "TASK_STATUS_FAILED",
|
||||
5: "TASK_STATUS_CANCELLED",
|
||||
6: "TASK_STATUS_REJECTED",
|
||||
7: "TASK_STATUS_WAITING",
|
||||
}
|
||||
|
||||
var TaskStatus_value = map[string]int32{
|
||||
"TASK_STATUS_UNSPECIFIED": 0,
|
||||
"TASK_STATUS_RECEIVED": 1,
|
||||
"TASK_STATUS_PROCESSING": 2,
|
||||
"TASK_STATUS_COMPLETED": 3,
|
||||
"TASK_STATUS_FAILED": 4,
|
||||
"TASK_STATUS_CANCELLED": 5,
|
||||
"TASK_STATUS_REJECTED": 6,
|
||||
"TASK_STATUS_WAITING": 7,
|
||||
}
|
||||
|
||||
func (x TransportMode) String() string { return fmt.Sprintf("%v", x) }
|
||||
func (x MessageType) String() string { return fmt.Sprintf("%v", x) }
|
||||
func (x TaskStatus) String() string { return fmt.Sprintf("%v", x) }
|
||||
|
||||
func init() {
|
||||
proto.RegisterEnum("a2a.TransportMode", TransportMode_name, TransportMode_value)
|
||||
proto.RegisterEnum("a2a.MessageType", MessageType_name, MessageType_value)
|
||||
proto.RegisterEnum("a2a.TaskStatus", TaskStatus_name, TaskStatus_value)
|
||||
}
|
||||
|
||||
// AgentEndpoint represents an agent's endpoint
|
||||
type AgentEndpoint struct {
|
||||
AgentId string `protobuf:"bytes,1,opt,name=agent_id,json=agentId" json:"agent_id,omitempty"`
|
||||
AgentName string `protobuf:"bytes,2,opt,name=agent_name,json=agentName" json:"agent_name,omitempty"`
|
||||
SystemId string `protobuf:"bytes,3,opt,name=system_id,json=systemId" json:"system_id,omitempty"`
|
||||
Endpoint string `protobuf:"bytes,4,opt,name=endpoint" json:"endpoint,omitempty"`
|
||||
Version string `protobuf:"bytes,5,opt,name=version" json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func (m *AgentEndpoint) Reset() { *m = AgentEndpoint{} }
|
||||
func (m *AgentEndpoint) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*AgentEndpoint) ProtoMessage() {}
|
||||
|
||||
func (m *AgentEndpoint) GetAgentId() string {
|
||||
if m != nil {
|
||||
return m.AgentId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A2AMessage represents a complete Synapse message
|
||||
type A2AMessage struct {
|
||||
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
|
||||
Version string `protobuf:"bytes,2,opt,name=version" json:"version,omitempty"`
|
||||
Type MessageType `protobuf:"varint,3,opt,name=type,enum=a2a.MessageType" json:"type,omitempty"`
|
||||
Sender *AgentEndpoint `protobuf:"bytes,4,opt,name=sender" json:"sender,omitempty"`
|
||||
Receiver *AgentEndpoint `protobuf:"bytes,5,opt,name=receiver" json:"receiver,omitempty"`
|
||||
CorrelationId string `protobuf:"bytes,6,opt,name=correlation_id,json=correlationId" json:"correlation_id,omitempty"`
|
||||
ReplyTo string `protobuf:"bytes,7,opt,name=reply_to,json=replyTo" json:"reply_to,omitempty"`
|
||||
GroupId string `protobuf:"bytes,8,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
Timestamp *timestamp.Timestamp `protobuf:"bytes,9,opt,name=timestamp" json:"timestamp,omitempty"`
|
||||
Payload *anypb.Any `protobuf:"bytes,10,opt,name=payload" json:"payload,omitempty"`
|
||||
Signature string `protobuf:"bytes,11,opt,name=signature" json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
func (m *A2AMessage) Reset() { *m = A2AMessage{} }
|
||||
func (m *A2AMessage) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*A2AMessage) ProtoMessage() {}
|
||||
|
||||
// TaskPayload represents task submission payload
|
||||
type TaskPayload struct {
|
||||
TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
TaskType string `protobuf:"bytes,2,opt,name=task_type,json=taskType" json:"task_type,omitempty"`
|
||||
Payload *anypb.Any `protobuf:"bytes,3,opt,name=payload" json:"payload,omitempty"`
|
||||
ExternalTaskId string `protobuf:"bytes,4,opt,name=external_task_id,json=externalTaskId" json:"external_task_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TaskPayload) Reset() { *m = TaskPayload{} }
|
||||
func (m *TaskPayload) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*TaskPayload) ProtoMessage() {}
|
||||
|
||||
// TaskResult represents task result
|
||||
type TaskResult struct {
|
||||
TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
Type string `protobuf:"bytes,2,opt,name=type" json:"type,omitempty"`
|
||||
Result *anypb.Any `protobuf:"bytes,3,opt,name=result" json:"result,omitempty"`
|
||||
Summary string `protobuf:"bytes,4,opt,name=summary" json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TaskResult) Reset() { *m = TaskResult{} }
|
||||
func (m *TaskResult) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*TaskResult) ProtoMessage() {}
|
||||
|
||||
// TaskStatusUpdate represents task status update
|
||||
type TaskStatusUpdate struct {
|
||||
TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
Status TaskStatus `protobuf:"varint,2,opt,name=status,enum=a2a.TaskStatus" json:"status,omitempty"`
|
||||
Progress int32 `protobuf:"varint,3,opt,name=progress" json:"progress,omitempty"`
|
||||
Message string `protobuf:"bytes,4,opt,name=message" json:"message,omitempty"`
|
||||
Details *anypb.Any `protobuf:"bytes,5,opt,name=details" json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TaskStatusUpdate) Reset() { *m = TaskStatusUpdate{} }
|
||||
func (m *TaskStatusUpdate) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*TaskStatusUpdate) ProtoMessage() {}
|
||||
|
||||
// =============================================================================
|
||||
// Task Service Messages
|
||||
// =============================================================================
|
||||
|
||||
// SubmitTaskRequest is the request for submitting a task
|
||||
type SubmitTaskRequest struct {
|
||||
TaskType string `protobuf:"bytes,1,opt,name=task_type,json=taskType" json:"task_type,omitempty"`
|
||||
Payload *anypb.Any `protobuf:"bytes,2,opt,name=payload" json:"payload,omitempty"`
|
||||
ExternalTaskId string `protobuf:"bytes,3,opt,name=external_task_id,json=externalTaskId" json:"external_task_id,omitempty"`
|
||||
Priority string `protobuf:"bytes,4,opt,name=priority" json:"priority,omitempty"`
|
||||
CallbackUrl string `protobuf:"bytes,5,opt,name=callback_url,json=callbackUrl" json:"callback_url,omitempty"`
|
||||
CallbackToken string `protobuf:"bytes,6,opt,name=callback_token,json=callbackToken" json:"callback_token,omitempty"`
|
||||
PreferredTransport TransportMode `protobuf:"varint,7,opt,name=preferred_transport,json=preferredTransport" json:"preferred_transport,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SubmitTaskRequest) Reset() { *m = SubmitTaskRequest{} }
|
||||
func (m *SubmitTaskRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SubmitTaskRequest) ProtoMessage() {}
|
||||
|
||||
func (m *SubmitTaskRequest) GetTaskType() string {
|
||||
if m != nil {
|
||||
return m.TaskType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SubmitTaskResponse is the response for task submission
|
||||
type SubmitTaskResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
Status TaskStatus `protobuf:"varint,3,opt,name=status,enum=a2a.TaskStatus" json:"status,omitempty"`
|
||||
Message string `protobuf:"bytes,4,opt,name=message" json:"message,omitempty"`
|
||||
ErrorCode string `protobuf:"bytes,5,opt,name=error_code,json=errorCode" json:"error_code,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SubmitTaskResponse) Reset() { *m = SubmitTaskResponse{} }
|
||||
func (m *SubmitTaskResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SubmitTaskResponse) ProtoMessage() {}
|
||||
|
||||
// SubmitTaskWithFilesRequest is the request for submitting a task with files
|
||||
type SubmitTaskWithFilesRequest struct {
|
||||
Request *SubmitTaskRequest `protobuf:"bytes,1,opt,name=request" json:"request,omitempty"`
|
||||
Files []*FileUpload `protobuf:"bytes,2,rep,name=files" json:"files,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SubmitTaskWithFilesRequest) Reset() { *m = SubmitTaskWithFilesRequest{} }
|
||||
func (m *SubmitTaskWithFilesRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SubmitTaskWithFilesRequest) ProtoMessage() {}
|
||||
|
||||
// FileUpload represents an uploaded file
|
||||
type FileUpload struct {
|
||||
FieldName string `protobuf:"bytes,1,opt,name=field_name,json=fieldName" json:"field_name,omitempty"`
|
||||
Filename string `protobuf:"bytes,2,opt,name=filename" json:"filename,omitempty"`
|
||||
MimeType string `protobuf:"bytes,3,opt,name=mime_type,json=mimeType" json:"mime_type,omitempty"`
|
||||
Data []byte `protobuf:"bytes,4,opt,name=data" json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (m *FileUpload) Reset() { *m = FileUpload{} }
|
||||
func (m *FileUpload) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*FileUpload) ProtoMessage() {}
|
||||
|
||||
// GetTaskRequest is the request for getting task details
|
||||
type GetTaskRequest struct {
|
||||
TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *GetTaskRequest) Reset() { *m = GetTaskRequest{} }
|
||||
func (m *GetTaskRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*GetTaskRequest) ProtoMessage() {}
|
||||
|
||||
// TaskStatusResponse is the response for task status query
|
||||
type TaskStatusResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Task *TaskInfo `protobuf:"bytes,2,opt,name=task" json:"task,omitempty"`
|
||||
ErrorCode string `protobuf:"bytes,3,opt,name=error_code,json=errorCode" json:"error_code,omitempty"`
|
||||
ErrorMessage string `protobuf:"bytes,4,opt,name=error_message,json=errorMessage" json:"error_message,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TaskStatusResponse) Reset() { *m = TaskStatusResponse{} }
|
||||
func (m *TaskStatusResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*TaskStatusResponse) ProtoMessage() {}
|
||||
|
||||
// TaskInfo represents task information
|
||||
type TaskInfo struct {
|
||||
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
|
||||
Type string `protobuf:"bytes,2,opt,name=type" json:"type,omitempty"`
|
||||
Status TaskStatus `protobuf:"varint,3,opt,name=status,enum=a2a.TaskStatus" json:"status,omitempty"`
|
||||
Progress int32 `protobuf:"varint,4,opt,name=progress" json:"progress,omitempty"`
|
||||
Message string `protobuf:"bytes,5,opt,name=message" json:"message,omitempty"`
|
||||
CreatedAt *timestamp.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt" json:"created_at,omitempty"`
|
||||
UpdatedAt *timestamp.Timestamp `protobuf:"bytes,7,opt,name=updated_at,json=updatedAt" json:"updated_at,omitempty"`
|
||||
CompletedAt *timestamp.Timestamp `protobuf:"bytes,8,opt,name=completed_at,json=completedAt" json:"completed_at,omitempty"`
|
||||
ETA *timestamp.Timestamp `protobuf:"bytes,9,opt,name=e_t_a,json=eta" json:"eta,omitempty"`
|
||||
Result *anypb.Any `protobuf:"bytes,10,opt,name=result" json:"result,omitempty"`
|
||||
Error *TaskError `protobuf:"bytes,11,opt,name=error" json:"error,omitempty"`
|
||||
ExternalTaskId string `protobuf:"bytes,12,opt,name=external_task_id,json=externalTaskId" json:"external_task_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TaskInfo) Reset() { *m = TaskInfo{} }
|
||||
func (m *TaskInfo) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*TaskInfo) ProtoMessage() {}
|
||||
|
||||
// TaskError represents a task error
|
||||
type TaskError struct {
|
||||
Code string `protobuf:"bytes,1,opt,name=code" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message" json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TaskError) Reset() { *m = TaskError{} }
|
||||
func (m *TaskError) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*TaskError) ProtoMessage() {}
|
||||
|
||||
// ListTasksRequest is the request for listing tasks
|
||||
type ListTasksRequest struct {
|
||||
Status string `protobuf:"bytes,1,opt,name=status" json:"status,omitempty"`
|
||||
Limit int32 `protobuf:"varint,2,opt,name=limit" json:"limit,omitempty"`
|
||||
Offset int32 `protobuf:"varint,3,opt,name=offset" json:"offset,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ListTasksRequest) Reset() { *m = ListTasksRequest{} }
|
||||
func (m *ListTasksRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*ListTasksRequest) ProtoMessage() {}
|
||||
|
||||
// ListTasksResponse is the response for listing tasks
|
||||
type ListTasksResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Tasks []*TaskInfo `protobuf:"bytes,2,rep,name=tasks" json:"tasks,omitempty"`
|
||||
Total int32 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"`
|
||||
Limit int32 `protobuf:"varint,4,opt,name=limit" json:"limit,omitempty"`
|
||||
Offset int32 `protobuf:"varint,5,opt,name=offset" json:"offset,omitempty"`
|
||||
HasMore bool `protobuf:"varint,6,opt,name=has_more,json=hasMore" json:"has_more,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ListTasksResponse) Reset() { *m = ListTasksResponse{} }
|
||||
func (m *ListTasksResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*ListTasksResponse) ProtoMessage() {}
|
||||
|
||||
// CancelTaskRequest is the request for cancelling a task
|
||||
type CancelTaskRequest struct {
|
||||
TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CancelTaskRequest) Reset() { *m = CancelTaskRequest{} }
|
||||
func (m *CancelTaskRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*CancelTaskRequest) ProtoMessage() {}
|
||||
|
||||
// CancelTaskResponse is the response for cancelling a task
|
||||
type CancelTaskResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
Status TaskStatus `protobuf:"varint,3,opt,name=status,enum=a2a.TaskStatus" json:"status,omitempty"`
|
||||
Message string `protobuf:"bytes,4,opt,name=message" json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CancelTaskResponse) Reset() { *m = CancelTaskResponse{} }
|
||||
func (m *CancelTaskResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*CancelTaskResponse) ProtoMessage() {}
|
||||
|
||||
// TaskStatusStreamRequest is the request for streaming task status
|
||||
type TaskStatusStreamRequest struct {
|
||||
TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
Subscribe bool `protobuf:"varint,2,opt,name=subscribe" json:"subscribe,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TaskStatusStreamRequest) Reset() { *m = TaskStatusStreamRequest{} }
|
||||
func (m *TaskStatusStreamRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*TaskStatusStreamRequest) ProtoMessage() {}
|
||||
|
||||
// =============================================================================
|
||||
// Synapse Message Service Messages
|
||||
// =============================================================================
|
||||
|
||||
// SendMessageRequest is the request for sending an Synapse message
|
||||
type SendMessageRequest struct {
|
||||
Message *A2AMessage `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SendMessageRequest) Reset() { *m = SendMessageRequest{} }
|
||||
func (m *SendMessageRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SendMessageRequest) ProtoMessage() {}
|
||||
|
||||
// SendMessageResponse is the response for sending an Synapse message
|
||||
type SendMessageResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
MessageId string `protobuf:"bytes,2,opt,name=message_id,json=messageId" json:"message_id,omitempty"`
|
||||
Error string `protobuf:"bytes,3,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SendMessageResponse) Reset() { *m = SendMessageResponse{} }
|
||||
func (m *SendMessageResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SendMessageResponse) ProtoMessage() {}
|
||||
|
||||
// =============================================================================
|
||||
// Capabilities Service Messages
|
||||
// =============================================================================
|
||||
|
||||
// CapabilitiesResponse is the response for capabilities query
|
||||
type CapabilitiesResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Capabilities *AgentCapabilities `protobuf:"bytes,2,opt,name=capabilities" json:"capabilities,omitempty"`
|
||||
Error string `protobuf:"bytes,3,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CapabilitiesResponse) Reset() { *m = CapabilitiesResponse{} }
|
||||
func (m *CapabilitiesResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*CapabilitiesResponse) ProtoMessage() {}
|
||||
|
||||
// AgentCapabilities represents agent capabilities
|
||||
type AgentCapabilities struct {
|
||||
AgentId string `protobuf:"bytes,1,opt,name=agent_id,json=agentId" json:"agent_id,omitempty"`
|
||||
AgentName string `protobuf:"bytes,2,opt,name=agent_name,json=agentName" json:"agent_name,omitempty"`
|
||||
SystemId string `protobuf:"bytes,3,opt,name=system_id,json=systemId" json:"system_id,omitempty"`
|
||||
ProtocolVersion string `protobuf:"bytes,4,opt,name=protocol_version,json=protocolVersion" json:"protocol_version,omitempty"`
|
||||
SupportedTaskTypes []*SupportedTaskType `protobuf:"bytes,5,rep,name=supported_task_types,json=supportedTaskTypes" json:"supported_task_types,omitempty"`
|
||||
SupportedContentTypes []string `protobuf:"bytes,6,rep,name=supported_content_types,json=supportedContentTypes" json:"supported_content_types,omitempty"`
|
||||
SupportedTransportModes []TransportMode `protobuf:"varint,7,rep,name=supported_transport_modes,json=supportedTransportModes" json:"supported_transport_modes,omitempty"`
|
||||
MaxFileSize int64 `protobuf:"varint,8,opt,name=max_file_size,json=maxFileSize" json:"max_file_size,omitempty"`
|
||||
MaxConcurrentTasks int32 `protobuf:"varint,9,opt,name=max_concurrent_tasks,json=maxConcurrentTasks" json:"max_concurrent_tasks,omitempty"`
|
||||
MaxTaskDurationSeconds int32 `protobuf:"varint,10,opt,name=max_task_duration_seconds,json=maxTaskDurationSeconds" json:"max_task_duration_seconds,omitempty"`
|
||||
SupportProgressUpdates bool `protobuf:"varint,11,opt,name=support_progress_updates,json=supportProgressUpdates" json:"support_progress_updates,omitempty"`
|
||||
SupportStreaming bool `protobuf:"varint,12,opt,name=support_streaming,json=supportStreaming" json:"support_streaming,omitempty"`
|
||||
SupportHil bool `protobuf:"varint,13,opt,name=support_hil,json=supportHil" json:"support_hil,omitempty"`
|
||||
SupportGroups bool `protobuf:"varint,14,opt,name=support_groups,json=supportGroups" json:"support_groups,omitempty"`
|
||||
}
|
||||
|
||||
func (m *AgentCapabilities) Reset() { *m = AgentCapabilities{} }
|
||||
func (m *AgentCapabilities) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*AgentCapabilities) ProtoMessage() {}
|
||||
|
||||
// SupportedTaskType represents a supported task type
|
||||
type SupportedTaskType struct {
|
||||
Type string `protobuf:"bytes,1,opt,name=type" json:"type,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
|
||||
Description string `protobuf:"bytes,3,opt,name=description" json:"description,omitempty"`
|
||||
EstimatedTime string `protobuf:"bytes,4,opt,name=estimated_time,json=estimatedTime" json:"estimated_time,omitempty"`
|
||||
InputSchema string `protobuf:"bytes,5,opt,name=input_schema,json=inputSchema" json:"input_schema,omitempty"`
|
||||
OutputSchema string `protobuf:"bytes,6,opt,name=output_schema,json=outputSchema" json:"output_schema,omitempty"`
|
||||
Capabilities []string `protobuf:"bytes,7,rep,name=capabilities" json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SupportedTaskType) Reset() { *m = SupportedTaskType{} }
|
||||
func (m *SupportedTaskType) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SupportedTaskType) ProtoMessage() {}
|
||||
|
||||
// QueryAgentRequest is the request for querying an agent
|
||||
type QueryAgentRequest struct {
|
||||
AgentId string `protobuf:"bytes,1,opt,name=agent_id,json=agentId" json:"agent_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *QueryAgentRequest) Reset() { *m = QueryAgentRequest{} }
|
||||
func (m *QueryAgentRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*QueryAgentRequest) ProtoMessage() {}
|
||||
|
||||
// ListAgentsResponse is the response for listing agents
|
||||
type ListAgentsResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Agents []*AgentInfo `protobuf:"bytes,2,rep,name=agents" json:"agents,omitempty"`
|
||||
Total int32 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ListAgentsResponse) Reset() { *m = ListAgentsResponse{} }
|
||||
func (m *ListAgentsResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*ListAgentsResponse) ProtoMessage() {}
|
||||
|
||||
// AgentInfo represents agent information
|
||||
type AgentInfo struct {
|
||||
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
|
||||
AgentId string `protobuf:"bytes,2,opt,name=agent_id,json=agentId" json:"agent_id,omitempty"`
|
||||
AgentName string `protobuf:"bytes,3,opt,name=agent_name,json=agentName" json:"agent_name,omitempty"`
|
||||
SystemId string `protobuf:"bytes,4,opt,name=system_id,json=systemId" json:"system_id,omitempty"`
|
||||
Endpoint string `protobuf:"bytes,5,opt,name=endpoint" json:"endpoint,omitempty"`
|
||||
IsActive bool `protobuf:"varint,6,opt,name=is_active,json=isActive" json:"is_active,omitempty"`
|
||||
TrustLevel int32 `protobuf:"varint,7,opt,name=trust_level,json=trustLevel" json:"trust_level,omitempty"`
|
||||
}
|
||||
|
||||
func (m *AgentInfo) Reset() { *m = AgentInfo{} }
|
||||
func (m *AgentInfo) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*AgentInfo) ProtoMessage() {}
|
||||
|
||||
// =============================================================================
|
||||
// Group Service Messages
|
||||
// =============================================================================
|
||||
|
||||
// CreateGroupRequest is the request for creating a group
|
||||
type CreateGroupRequest struct {
|
||||
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
|
||||
Description string `protobuf:"bytes,2,opt,name=description" json:"description,omitempty"`
|
||||
CoordinatorId string `protobuf:"bytes,3,opt,name=coordinator_id,json=coordinatorId" json:"coordinator_id,omitempty"`
|
||||
InitialMembers []string `protobuf:"bytes,4,rep,name=initial_members,json=initialMembers" json:"initial_members,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CreateGroupRequest) Reset() { *m = CreateGroupRequest{} }
|
||||
func (m *CreateGroupRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*CreateGroupRequest) ProtoMessage() {}
|
||||
|
||||
// CreateGroupResponse is the response for creating a group
|
||||
type CreateGroupResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
GroupId string `protobuf:"bytes,2,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
Group *GroupInfo `protobuf:"bytes,3,opt,name=group" json:"group,omitempty"`
|
||||
Error string `protobuf:"bytes,4,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CreateGroupResponse) Reset() { *m = CreateGroupResponse{} }
|
||||
func (m *CreateGroupResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*CreateGroupResponse) ProtoMessage() {}
|
||||
|
||||
// GroupInfo represents group information
|
||||
type GroupInfo struct {
|
||||
Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
|
||||
Description string `protobuf:"bytes,3,opt,name=description" json:"description,omitempty"`
|
||||
CoordinatorId string `protobuf:"bytes,4,opt,name=coordinator_id,json=coordinatorId" json:"coordinator_id,omitempty"`
|
||||
Status string `protobuf:"bytes,5,opt,name=status" json:"status,omitempty"`
|
||||
CreatedAt *timestamp.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt" json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
func (m *GroupInfo) Reset() { *m = GroupInfo{} }
|
||||
func (m *GroupInfo) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*GroupInfo) ProtoMessage() {}
|
||||
|
||||
// JoinGroupRequest is the request for joining a group
|
||||
type JoinGroupRequest struct {
|
||||
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
Agent *AgentEndpoint `protobuf:"bytes,2,opt,name=agent" json:"agent,omitempty"`
|
||||
}
|
||||
|
||||
func (m *JoinGroupRequest) Reset() { *m = JoinGroupRequest{} }
|
||||
func (m *JoinGroupRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*JoinGroupRequest) ProtoMessage() {}
|
||||
|
||||
// JoinGroupResponse is the response for joining a group
|
||||
type JoinGroupResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *JoinGroupResponse) Reset() { *m = JoinGroupResponse{} }
|
||||
func (m *JoinGroupResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*JoinGroupResponse) ProtoMessage() {}
|
||||
|
||||
// LeaveGroupRequest is the request for leaving a group
|
||||
type LeaveGroupRequest struct {
|
||||
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
AgentId string `protobuf:"bytes,2,opt,name=agent_id,json=agentId" json:"agent_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *LeaveGroupRequest) Reset() { *m = LeaveGroupRequest{} }
|
||||
func (m *LeaveGroupRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*LeaveGroupRequest) ProtoMessage() {}
|
||||
|
||||
// LeaveGroupResponse is the response for leaving a group
|
||||
type LeaveGroupResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *LeaveGroupResponse) Reset() { *m = LeaveGroupResponse{} }
|
||||
func (m *LeaveGroupResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*LeaveGroupResponse) ProtoMessage() {}
|
||||
|
||||
// DisbandGroupRequest is the request for disbanding a group
|
||||
type DisbandGroupRequest struct {
|
||||
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DisbandGroupRequest) Reset() { *m = DisbandGroupRequest{} }
|
||||
func (m *DisbandGroupRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*DisbandGroupRequest) ProtoMessage() {}
|
||||
|
||||
// DisbandGroupResponse is the response for disbanding a group
|
||||
type DisbandGroupResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DisbandGroupResponse) Reset() { *m = DisbandGroupResponse{} }
|
||||
func (m *DisbandGroupResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*DisbandGroupResponse) ProtoMessage() {}
|
||||
|
||||
// ListGroupMembersRequest is the request for listing group members
|
||||
type ListGroupMembersRequest struct {
|
||||
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ListGroupMembersRequest) Reset() { *m = ListGroupMembersRequest{} }
|
||||
func (m *ListGroupMembersRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*ListGroupMembersRequest) ProtoMessage() {}
|
||||
|
||||
// ListGroupMembersResponse is the response for listing group members
|
||||
type ListGroupMembersResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Members []*GroupMember `protobuf:"bytes,2,rep,name=members" json:"members,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ListGroupMembersResponse) Reset() { *m = ListGroupMembersResponse{} }
|
||||
func (m *ListGroupMembersResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*ListGroupMembersResponse) ProtoMessage() {}
|
||||
|
||||
// GroupMember represents a group member
|
||||
type GroupMember struct {
|
||||
AgentId string `protobuf:"bytes,1,opt,name=agent_id,json=agentId" json:"agent_id,omitempty"`
|
||||
AgentName string `protobuf:"bytes,2,opt,name=agent_name,json=agentName" json:"agent_name,omitempty"`
|
||||
Role string `protobuf:"bytes,3,opt,name=role" json:"role,omitempty"`
|
||||
Status string `protobuf:"bytes,4,opt,name=status" json:"status,omitempty"`
|
||||
JoinedAt *timestamp.Timestamp `protobuf:"bytes,5,opt,name=joined_at,json=joinedAt" json:"joined_at,omitempty"`
|
||||
}
|
||||
|
||||
func (m *GroupMember) Reset() { *m = GroupMember{} }
|
||||
func (m *GroupMember) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*GroupMember) ProtoMessage() {}
|
||||
|
||||
// =============================================================================
|
||||
// Group Voting Messages (NEW!)
|
||||
// =============================================================================
|
||||
|
||||
// VoteRequest is the request for initiating a vote
|
||||
type VoteRequest struct {
|
||||
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
InitiatorId string `protobuf:"bytes,2,opt,name=initiator_id,json=initiatorId" json:"initiator_id,omitempty"`
|
||||
Question string `protobuf:"bytes,3,opt,name=question" json:"question,omitempty"`
|
||||
Options []string `protobuf:"bytes,4,rep,name=options" json:"options,omitempty"`
|
||||
Threshold float64 `protobuf:"fixed64,5,opt,name=threshold" json:"threshold,omitempty"`
|
||||
TimeoutSeconds int32 `protobuf:"varint,6,opt,name=timeout_seconds,json=timeoutSeconds" json:"timeout_seconds,omitempty"`
|
||||
}
|
||||
|
||||
func (m *VoteRequest) Reset() { *m = VoteRequest{} }
|
||||
func (m *VoteRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*VoteRequest) ProtoMessage() {}
|
||||
|
||||
// VoteResponse is the response for initiating a vote
|
||||
type VoteResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
VoteId string `protobuf:"bytes,2,opt,name=vote_id,json=voteId" json:"vote_id,omitempty"`
|
||||
Error string `protobuf:"bytes,3,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *VoteResponse) Reset() { *m = VoteResponse{} }
|
||||
func (m *VoteResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*VoteResponse) ProtoMessage() {}
|
||||
|
||||
// SubmitVoteRequest is the request for submitting a vote
|
||||
type SubmitVoteRequest struct {
|
||||
VoteId string `protobuf:"bytes,1,opt,name=vote_id,json=voteId" json:"vote_id,omitempty"`
|
||||
AgentId string `protobuf:"bytes,2,opt,name=agent_id,json=agentId" json:"agent_id,omitempty"`
|
||||
Option string `protobuf:"bytes,3,opt,name=option" json:"option,omitempty"`
|
||||
Reason string `protobuf:"bytes,4,opt,name=reason" json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SubmitVoteRequest) Reset() { *m = SubmitVoteRequest{} }
|
||||
func (m *SubmitVoteRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SubmitVoteRequest) ProtoMessage() {}
|
||||
|
||||
// SubmitVoteResponse is the response for submitting a vote
|
||||
type SubmitVoteResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SubmitVoteResponse) Reset() { *m = SubmitVoteResponse{} }
|
||||
func (m *SubmitVoteResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SubmitVoteResponse) ProtoMessage() {}
|
||||
|
||||
// GetVoteStatusRequest is the request for getting vote status
|
||||
type GetVoteStatusRequest struct {
|
||||
VoteId string `protobuf:"bytes,1,opt,name=vote_id,json=voteId" json:"vote_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *GetVoteStatusRequest) Reset() { *m = GetVoteStatusRequest{} }
|
||||
func (m *GetVoteStatusRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*GetVoteStatusRequest) ProtoMessage() {}
|
||||
|
||||
// VoteStatusResponse is the response for vote status
|
||||
type VoteStatusResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
VoteId string `protobuf:"bytes,2,opt,name=vote_id,json=voteId" json:"vote_id,omitempty"`
|
||||
Status string `protobuf:"bytes,3,opt,name=status" json:"status,omitempty"` // open, decided, expired
|
||||
Question string `protobuf:"bytes,4,opt,name=question" json:"question,omitempty"`
|
||||
TotalVotes int32 `protobuf:"varint,5,opt,name=total_votes,json=totalVotes" json:"total_votes,omitempty"`
|
||||
RequiredVotes int32 `protobuf:"varint,6,opt,name=required_votes,json=requiredVotes" json:"required_votes,omitempty"`
|
||||
WinningOption string `protobuf:"bytes,7,opt,name=winning_option,json=winningOption" json:"winning_option,omitempty"`
|
||||
Error string `protobuf:"bytes,8,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *VoteStatusResponse) Reset() { *m = VoteStatusResponse{} }
|
||||
func (m *VoteStatusResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*VoteStatusResponse) ProtoMessage() {}
|
||||
|
||||
// =============================================================================
|
||||
// Task Split Messages (NEW!)
|
||||
// =============================================================================
|
||||
|
||||
// SplitTaskRequest is the request for splitting a task
|
||||
type SplitTaskRequest struct {
|
||||
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId" json:"task_id,omitempty"`
|
||||
TaskType string `protobuf:"bytes,3,opt,name=task_type,json=taskType" json:"task_type,omitempty"`
|
||||
Requirements string `protobuf:"bytes,4,opt,name=requirements" json:"requirements,omitempty"`
|
||||
Strategy SplitStrategy `protobuf:"varint,5,opt,name=strategy,enum=a2a.SplitStrategy" json:"strategy,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SplitTaskRequest) Reset() { *m = SplitTaskRequest{} }
|
||||
func (m *SplitTaskRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SplitTaskRequest) ProtoMessage() {}
|
||||
|
||||
type SplitStrategy int32
|
||||
|
||||
const (
|
||||
SplitStrategy_SPLIT_BY_TYPE SplitStrategy = 0
|
||||
SplitStrategy_SPLIT_BY_DATA SplitStrategy = 1
|
||||
SplitStrategy_SPLIT_BY_REGION SplitStrategy = 2
|
||||
SplitStrategy_SPLIT_BY_ROUNDROBIN SplitStrategy = 3
|
||||
)
|
||||
|
||||
// SplitTaskResponse is the response for splitting a task
|
||||
type SplitTaskResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
GroupId string `protobuf:"bytes,2,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
ParentTaskId string `protobuf:"bytes,3,opt,name=parent_task_id,json=parentTaskId" json:"parent_task_id,omitempty"`
|
||||
Subtasks []*SubtaskInfo `protobuf:"bytes,4,rep,name=subtasks" json:"subtasks,omitempty"`
|
||||
Error string `protobuf:"bytes,5,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SplitTaskResponse) Reset() { *m = SplitTaskResponse{} }
|
||||
func (m *SplitTaskResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SplitTaskResponse) ProtoMessage() {}
|
||||
|
||||
type SubtaskInfo struct {
|
||||
SubtaskId string `protobuf:"bytes,1,opt,name=subtask_id,json=subtaskId" json:"subtask_id,omitempty"`
|
||||
Assignee string `protobuf:"bytes,2,opt,name=assignee" json:"assignee,omitempty"`
|
||||
Scope string `protobuf:"bytes,3,opt,name=scope" json:"scope,omitempty"`
|
||||
Dependencies []string `protobuf:"bytes,4,rep,name=dependencies" json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SubtaskInfo) Reset() { *m = SubtaskInfo{} }
|
||||
func (m *SubtaskInfo) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*SubtaskInfo) ProtoMessage() {}
|
||||
|
||||
// MergeResultsRequest is the request for merging results
|
||||
type MergeResultsRequest struct {
|
||||
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId" json:"group_id,omitempty"`
|
||||
ParentTaskId string `protobuf:"bytes,2,opt,name=parent_task_id,json=parentTaskId" json:"parent_task_id,omitempty"`
|
||||
Strategy MergeStrategy `protobuf:"varint,3,opt,name=strategy,enum=a2a.MergeStrategy" json:"strategy,omitempty"`
|
||||
}
|
||||
|
||||
func (m *MergeResultsRequest) Reset() { *m = MergeResultsRequest{} }
|
||||
func (m *MergeResultsRequest) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*MergeResultsRequest) ProtoMessage() {}
|
||||
|
||||
type MergeStrategy int32
|
||||
|
||||
const (
|
||||
MergeStrategy_MERGE_ALL MergeStrategy = 0
|
||||
MergeStrategy_MERGE_BEST MergeStrategy = 1
|
||||
MergeStrategy_MERGE_CONSENSUS MergeStrategy = 2
|
||||
MergeStrategy_MERGE_AGGREGATE MergeStrategy = 3
|
||||
)
|
||||
|
||||
// MergeResultsResponse is the response for merging results
|
||||
type MergeResultsResponse struct {
|
||||
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||
ParentTaskId string `protobuf:"bytes,2,opt,name=parent_task_id,json=parentTaskId" json:"parent_task_id,omitempty"`
|
||||
MergedResult *anypb.Any `protobuf:"bytes,3,opt,name=merged_result,json=mergedResult" json:"merged_result,omitempty"`
|
||||
Summary string `protobuf:"bytes,4,opt,name=summary" json:"summary,omitempty"`
|
||||
Error string `protobuf:"bytes,5,opt,name=error" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *MergeResultsResponse) Reset() { *m = MergeResultsResponse{} }
|
||||
func (m *MergeResultsResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*MergeResultsResponse) ProtoMessage() {}
|
||||
|
||||
// =============================================================================
|
||||
// Health Service Messages
|
||||
// =============================================================================
|
||||
|
||||
// HealthCheckResponse is the response for health check
|
||||
type HealthCheckResponse struct {
|
||||
Status string `protobuf:"bytes,1,opt,name=status" json:"status,omitempty"`
|
||||
Version string `protobuf:"bytes,2,opt,name=version" json:"version,omitempty"`
|
||||
UptimeSeconds int64 `protobuf:"varint,3,opt,name=uptime_seconds,json=uptimeSeconds" json:"uptime_seconds,omitempty"`
|
||||
ActiveTasks int32 `protobuf:"varint,4,opt,name=active_tasks,json=activeTasks" json:"active_tasks,omitempty"`
|
||||
}
|
||||
|
||||
func (m *HealthCheckResponse) Reset() { *m = HealthCheckResponse{} }
|
||||
func (m *HealthCheckResponse) String() string { return fmt.Sprintf("%+v", m) }
|
||||
func (*HealthCheckResponse) ProtoMessage() {}
|
||||
|
||||
// =============================================================================
|
||||
// Unimplemented types for proto compatibility
|
||||
// =============================================================================
|
||||
|
||||
// Ensure proto compatibility
|
||||
var _ proto.Message = (*AgentEndpoint)(nil)
|
||||
var _ proto.Message = (*A2AMessage)(nil)
|
||||
var _ proto.Message = (*SubmitTaskRequest)(nil)
|
||||
var _ proto.Message = (*SubmitTaskResponse)(nil)
|
||||
var _ proto.Message = (*GetTaskRequest)(nil)
|
||||
var _ proto.Message = (*TaskStatusResponse)(nil)
|
||||
var _ proto.Message = (*ListTasksRequest)(nil)
|
||||
var _ proto.Message = (*ListTasksResponse)(nil)
|
||||
var _ proto.Message = (*CancelTaskRequest)(nil)
|
||||
var _ proto.Message = (*CancelTaskResponse)(nil)
|
||||
var _ proto.Message = (*SendMessageRequest)(nil)
|
||||
var _ proto.Message = (*SendMessageResponse)(nil)
|
||||
var _ proto.Message = (*CapabilitiesResponse)(nil)
|
||||
var _ proto.Message = (*CreateGroupRequest)(nil)
|
||||
var _ proto.Message = (*CreateGroupResponse)(nil)
|
||||
var _ proto.Message = (*JoinGroupRequest)(nil)
|
||||
var _ proto.Message = (*JoinGroupResponse)(nil)
|
||||
var _ proto.Message = (*LeaveGroupRequest)(nil)
|
||||
var _ proto.Message = (*LeaveGroupResponse)(nil)
|
||||
var _ proto.Message = (*DisbandGroupRequest)(nil)
|
||||
var _ proto.Message = (*DisbandGroupResponse)(nil)
|
||||
var _ proto.Message = (*ListGroupMembersRequest)(nil)
|
||||
var _ proto.Message = (*ListGroupMembersResponse)(nil)
|
||||
var _ proto.Message = (*HealthCheckResponse)(nil)
|
||||
1145
grpcapi/synapse_grpc.pb.go
Normal file
1145
grpcapi/synapse_grpc.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
683
hil/protocol.go
Normal file
683
hil/protocol.go
Normal file
@@ -0,0 +1,683 @@
|
||||
package hil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// HiL Manager
|
||||
// =============================================================================
|
||||
|
||||
// DecisionMaker is the interface for human decision makers
|
||||
type DecisionMaker interface {
|
||||
// RequestDecision sends a decision request to a human
|
||||
RequestDecision(ctx context.Context, request *HILRequest) (*HILResponse, error)
|
||||
|
||||
// GetUserRole returns the user's role for authorization
|
||||
GetUserRole(userID string) string
|
||||
}
|
||||
|
||||
// HILConfig holds HiL configuration
|
||||
type HILConfig struct {
|
||||
// Level 1: Agent suggests + human confirms (24h default)
|
||||
Level1Timeout time.Duration
|
||||
|
||||
// Level 2: Must human decide (12h default, 2h urgent)
|
||||
Level2Timeout time.Duration
|
||||
Level2UrgentTimeout time.Duration
|
||||
|
||||
// Level 3: Expert/management decision (on-demand)
|
||||
Level3Timeout time.Duration
|
||||
|
||||
// Default timeouts
|
||||
DefaultTimeout time.Duration
|
||||
}
|
||||
|
||||
func DefaultHILConfig() *HILConfig {
|
||||
return &HILConfig{
|
||||
Level1Timeout: 24 * time.Hour,
|
||||
Level2Timeout: 12 * time.Hour,
|
||||
Level2UrgentTimeout: 2 * time.Hour,
|
||||
Level3Timeout: 72 * time.Hour,
|
||||
DefaultTimeout: 24 * time.Hour,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HILRequest represents a human-in-the-loop request
|
||||
// =============================================================================
|
||||
|
||||
type HILRequest struct {
|
||||
ID string
|
||||
TaskID string
|
||||
EscalationType string // P2P, GROUP, INTER_GROUP
|
||||
EscalationLevel int // 1, 2, 3
|
||||
Reason HILReason
|
||||
Context HILContext
|
||||
Options []HILOption
|
||||
SuggestedValue any
|
||||
Deadline time.Time
|
||||
Status HILStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DecidedAt *time.Time
|
||||
Response *HILResponse
|
||||
}
|
||||
|
||||
// HILReason describes why human intervention is needed
|
||||
type HILReason struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
// HILContext provides context for the decision
|
||||
type HILContext struct {
|
||||
TaskSummary string
|
||||
CurrentProgress int
|
||||
BlockedReason string
|
||||
RelevantHistory []HILHistoryItem
|
||||
}
|
||||
|
||||
// HILHistoryItem is a historical item for context
|
||||
type HILHistoryItem struct {
|
||||
Timestamp time.Time
|
||||
Event string
|
||||
Details string
|
||||
}
|
||||
|
||||
// HILOption represents a decision option
|
||||
type HILOption struct {
|
||||
Field string
|
||||
Value any
|
||||
Label string
|
||||
ProsCons string
|
||||
Confidence int // 0-100
|
||||
}
|
||||
|
||||
// HILStatus represents the status of a HiL request
|
||||
type HILStatus string
|
||||
|
||||
const (
|
||||
HILStatusPending HILStatus = "pending"
|
||||
HILStatusWaiting HILStatus = "waiting"
|
||||
HILStatusDeciding HILStatus = "deciding"
|
||||
HILStatusApproved HILStatus = "approved"
|
||||
HILStatusRejected HILStatus = "rejected"
|
||||
HILStatusTimeout HILStatus = "timeout"
|
||||
HILStatusEscalated HILStatus = "escalated"
|
||||
)
|
||||
|
||||
// HILResponse represents a human's decision
|
||||
type HILResponse struct {
|
||||
DecisionID string
|
||||
UserID string
|
||||
UserName string
|
||||
UserRole string
|
||||
Action string // approve, reject, modify, select
|
||||
Decision HILDecision
|
||||
Notes string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// HILDecision contains the actual decision
|
||||
type HILDecision struct {
|
||||
ApprovedFields map[string]interface{}
|
||||
ModifiedFields map[string]interface{}
|
||||
SelectedOption any
|
||||
RejectionReason string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HILProtocol
|
||||
// =============================================================================
|
||||
|
||||
type HILProtocol struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Agent identity
|
||||
agentEndpoint types.AgentEndpoint
|
||||
|
||||
// HIL requests
|
||||
requests map[string]*HILRequest
|
||||
|
||||
// Decision maker
|
||||
decisionMaker DecisionMaker
|
||||
|
||||
// Configuration
|
||||
config *HILConfig
|
||||
|
||||
// Callbacks
|
||||
callbacks map[string]chan *HILResponse
|
||||
}
|
||||
|
||||
// NewHILProtocol creates a new HiL protocol handler
|
||||
func NewHILProtocol(agentEndpoint types.AgentEndpoint, config *HILConfig) *HILProtocol {
|
||||
if config == nil {
|
||||
config = DefaultHILConfig()
|
||||
}
|
||||
return &HILProtocol{
|
||||
agentEndpoint: agentEndpoint,
|
||||
requests: make(map[string]*HILRequest),
|
||||
config: config,
|
||||
callbacks: make(map[string]chan *HILResponse),
|
||||
}
|
||||
}
|
||||
|
||||
// SetDecisionMaker sets the decision maker
|
||||
func (h *HILProtocol) SetDecisionMaker(m DecisionMaker) {
|
||||
h.decisionMaker = m
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// P2P HiL Methods
|
||||
// =============================================================================
|
||||
|
||||
// CreateP2PHILRequest creates a HiL request for P2P task
|
||||
func (h *HILProtocol) CreateP2PHILRequest(
|
||||
taskID string,
|
||||
reasonCode, reasonMessage string,
|
||||
context HILContext,
|
||||
options []HILOption,
|
||||
suggestedValue any,
|
||||
escalationLevel int,
|
||||
) *HILRequest {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
req := &HILRequest{
|
||||
ID: fmt.Sprintf("hil-%d", time.Now().UnixNano()),
|
||||
TaskID: taskID,
|
||||
EscalationType: "p2p",
|
||||
EscalationLevel: escalationLevel,
|
||||
Reason: HILReason{
|
||||
Code: reasonCode,
|
||||
Message: reasonMessage,
|
||||
},
|
||||
Context: context,
|
||||
Options: options,
|
||||
SuggestedValue: suggestedValue,
|
||||
Status: HILStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Set deadline based on escalation level
|
||||
req.Deadline = h.getDeadline(escalationLevel)
|
||||
|
||||
h.requests[req.ID] = req
|
||||
|
||||
log.Printf("[HiL] P2P HiL request created: %s (level: %d, task: %s)", req.ID, escalationLevel, taskID)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// CreateP2PHumanDecisionMessage creates a HUMAN_DECISION message payload
|
||||
func (h *HILProtocol) CreateP2PHumanDecisionMessage(req *HILRequest, response *HILResponse) *types.UnifiedSynapseMessage {
|
||||
payload := types.HumanDecisionPayload{}
|
||||
payload.TaskID = req.TaskID
|
||||
payload.DecisionID = response.DecisionID
|
||||
payload.ReplyTo = req.ID
|
||||
payload.DecidedBy.UserID = response.UserID
|
||||
payload.DecidedBy.UserName = response.UserName
|
||||
payload.DecidedBy.Role = response.UserRole
|
||||
payload.Decision.Action = response.Action
|
||||
payload.Notes = response.Notes
|
||||
payload.Timestamp = types.NowTimestamp()
|
||||
|
||||
switch response.Action {
|
||||
case "approve":
|
||||
if response.Decision.ApprovedFields != nil {
|
||||
payload.Decision.ApprovedFields = response.Decision.ApprovedFields
|
||||
}
|
||||
case "reject":
|
||||
payload.Decision.RejectionReason = response.Decision.RejectionReason
|
||||
case "select":
|
||||
payload.Decision.SelectedOption = response.Decision.SelectedOption
|
||||
case "modify":
|
||||
payload.Decision.ModifiedFields = response.Decision.ModifiedFields
|
||||
}
|
||||
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
return &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PHumanDecision),
|
||||
Sender: h.agentEndpoint,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Group HiL Methods
|
||||
// =============================================================================
|
||||
|
||||
// CreateGroupConsensusEscalation creates a consensus escalation request
|
||||
func (h *HILProtocol) CreateGroupConsensusEscalation(
|
||||
groupID string,
|
||||
voteID string,
|
||||
question string,
|
||||
votes []struct {
|
||||
VoterID string
|
||||
VoterRole string
|
||||
Option string
|
||||
Reason string
|
||||
},
|
||||
consensusStatus struct {
|
||||
RequiredThreshold float64
|
||||
ActualThreshold float64
|
||||
LeadingOption string
|
||||
OptionsTied bool
|
||||
},
|
||||
escalateReason struct {
|
||||
Code string
|
||||
Message string
|
||||
},
|
||||
escalationLevel int,
|
||||
context struct {
|
||||
TaskID string
|
||||
OriginalTaskRequirements map[string]interface{}
|
||||
AgentRecommendations []struct {
|
||||
AgentID string
|
||||
SuggestedOption string
|
||||
}
|
||||
},
|
||||
) *types.UnifiedSynapseMessage {
|
||||
decisionOptions := make([]struct {
|
||||
Option string
|
||||
SupportCount int
|
||||
SupportAgents []string
|
||||
}, 0)
|
||||
|
||||
// Count votes per option
|
||||
optionCounts := make(map[string]int)
|
||||
optionAgents := make(map[string][]string)
|
||||
for _, v := range votes {
|
||||
optionCounts[v.Option]++
|
||||
optionAgents[v.Option] = append(optionAgents[v.Option], v.VoterID)
|
||||
}
|
||||
for opt, count := range optionCounts {
|
||||
decisionOptions = append(decisionOptions, struct {
|
||||
Option string
|
||||
SupportCount int
|
||||
SupportAgents []string
|
||||
}{
|
||||
Option: opt,
|
||||
SupportCount: count,
|
||||
SupportAgents: optionAgents[opt],
|
||||
})
|
||||
}
|
||||
|
||||
payload := types.GroupConsensusEscalatePayload{}
|
||||
payload.VoteID = voteID
|
||||
payload.EscalationID = fmt.Sprintf("hil-%d", time.Now().UnixNano())
|
||||
payload.Question = question
|
||||
|
||||
// Copy votes
|
||||
payload.Votes = make([]struct {
|
||||
VoterID string `json:"voter_id"`
|
||||
VoterRole string `json:"voter_role"`
|
||||
Option string `json:"option"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}, len(votes))
|
||||
for i, v := range votes {
|
||||
payload.Votes[i].VoterID = v.VoterID
|
||||
payload.Votes[i].VoterRole = v.VoterRole
|
||||
payload.Votes[i].Option = v.Option
|
||||
payload.Votes[i].Reason = v.Reason
|
||||
}
|
||||
|
||||
payload.ConsensusStatus.RequiredThreshold = consensusStatus.RequiredThreshold
|
||||
payload.ConsensusStatus.ActualThreshold = consensusStatus.ActualThreshold
|
||||
payload.ConsensusStatus.LeadingOption = consensusStatus.LeadingOption
|
||||
payload.ConsensusStatus.OptionsTied = consensusStatus.OptionsTied
|
||||
payload.EscalateReason.Code = escalateReason.Code
|
||||
payload.EscalateReason.Message = escalateReason.Message
|
||||
payload.EscalationLevel = escalationLevel
|
||||
|
||||
// Copy decision options
|
||||
payload.DecisionOptions = make([]struct {
|
||||
Option string `json:"option"`
|
||||
SupportCount int `json:"support_count"`
|
||||
SupportAgents []string `json:"support_agents"`
|
||||
}, len(decisionOptions))
|
||||
for i, d := range decisionOptions {
|
||||
payload.DecisionOptions[i].Option = d.Option
|
||||
payload.DecisionOptions[i].SupportCount = d.SupportCount
|
||||
payload.DecisionOptions[i].SupportAgents = d.SupportAgents
|
||||
}
|
||||
|
||||
payload.Deadline = types.Timestamp(h.getDeadline(escalationLevel).Format(time.RFC3339))
|
||||
payload.Context.TaskID = context.TaskID
|
||||
payload.Context.OriginalTaskRequirements = context.OriginalTaskRequirements
|
||||
|
||||
// Copy agent recommendations
|
||||
payload.Context.AgentRecommendations = make([]struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
SuggestedOption string `json:"suggested_option"`
|
||||
}, len(context.AgentRecommendations))
|
||||
for i, r := range context.AgentRecommendations {
|
||||
payload.Context.AgentRecommendations[i].AgentID = r.AgentID
|
||||
payload.Context.AgentRecommendations[i].SuggestedOption = r.SuggestedOption
|
||||
}
|
||||
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
return &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.GroupConsensusEscalate),
|
||||
Sender: h.agentEndpoint,
|
||||
GroupID: groupID,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateGroupSubtaskEscalation creates a subtask escalation request
|
||||
func (h *HILProtocol) CreateGroupSubtaskEscalation(
|
||||
groupID string,
|
||||
originalTask struct {
|
||||
ID string
|
||||
Type string
|
||||
Description string
|
||||
},
|
||||
subtask struct {
|
||||
ID string
|
||||
Description string
|
||||
AssignedTo string
|
||||
BlockedAt string
|
||||
BlockedReason struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
AttemptedSolutions []string
|
||||
},
|
||||
requiredDecision struct {
|
||||
FieldsNeeded []struct {
|
||||
Field string
|
||||
CurrentValue any
|
||||
SuggestedValue any
|
||||
Alternatives []any
|
||||
ValidationRules string
|
||||
}
|
||||
DecisionType string
|
||||
AffectsOtherSubtasks bool
|
||||
RollbackPossible bool
|
||||
},
|
||||
escalationLevel int,
|
||||
) *types.UnifiedSynapseMessage {
|
||||
payload := types.GroupSubtaskEscalatePayload{}
|
||||
payload.OriginalTask.ID = originalTask.ID
|
||||
payload.OriginalTask.Type = originalTask.Type
|
||||
payload.OriginalTask.Description = originalTask.Description
|
||||
payload.Subtask.ID = subtask.ID
|
||||
payload.Subtask.Description = subtask.Description
|
||||
payload.Subtask.AssignedTo = subtask.AssignedTo
|
||||
payload.Subtask.BlockedAt = subtask.BlockedAt
|
||||
payload.Subtask.BlockedReason.Code = subtask.BlockedReason.Code
|
||||
payload.Subtask.BlockedReason.Message = subtask.BlockedReason.Message
|
||||
payload.Subtask.AttemptedSolutions = subtask.AttemptedSolutions
|
||||
|
||||
// Copy fields needed
|
||||
payload.RequiredDecision.FieldsNeeded = make([]struct {
|
||||
Field string `json:"field"`
|
||||
CurrentValue any `json:"current_value,omitempty"`
|
||||
SuggestedValue any `json:"suggested_value,omitempty"`
|
||||
Alternatives []any `json:"alternatives,omitempty"`
|
||||
ValidationRules string `json:"validation_rules,omitempty"`
|
||||
}, len(requiredDecision.FieldsNeeded))
|
||||
for i, f := range requiredDecision.FieldsNeeded {
|
||||
payload.RequiredDecision.FieldsNeeded[i].Field = f.Field
|
||||
payload.RequiredDecision.FieldsNeeded[i].CurrentValue = f.CurrentValue
|
||||
payload.RequiredDecision.FieldsNeeded[i].SuggestedValue = f.SuggestedValue
|
||||
payload.RequiredDecision.FieldsNeeded[i].Alternatives = f.Alternatives
|
||||
payload.RequiredDecision.FieldsNeeded[i].ValidationRules = f.ValidationRules
|
||||
}
|
||||
|
||||
payload.RequiredDecision.DecisionType = requiredDecision.DecisionType
|
||||
payload.RequiredDecision.AffectsOtherSubtasks = requiredDecision.AffectsOtherSubtasks
|
||||
payload.RequiredDecision.RollbackPossible = requiredDecision.RollbackPossible
|
||||
payload.EscalationLevel = escalationLevel
|
||||
|
||||
deadline := types.Timestamp(h.getDeadline(escalationLevel).Format(time.RFC3339))
|
||||
payload.Deadline = &deadline
|
||||
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
return &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.GroupSubtaskEscalate),
|
||||
Sender: h.agentEndpoint,
|
||||
GroupID: groupID,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HiL Status Management
|
||||
// =============================================================================
|
||||
|
||||
// GetRequest returns a HiL request by ID
|
||||
func (h *HILProtocol) GetRequest(id string) (*HILRequest, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
req, ok := h.requests[id]
|
||||
return req, ok
|
||||
}
|
||||
|
||||
// ListRequests returns all HiL requests
|
||||
func (h *HILProtocol) ListRequests() []*HILRequest {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
result := make([]*HILRequest, 0, len(h.requests))
|
||||
for _, req := range h.requests {
|
||||
result = append(result, req)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ListPendingRequests returns all pending HiL requests
|
||||
func (h *HILProtocol) ListPendingRequests() []*HILRequest {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
result := make([]*HILRequest, 0)
|
||||
for _, req := range h.requests {
|
||||
if req.Status == HILStatusPending || req.Status == HILStatusWaiting {
|
||||
result = append(result, req)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UpdateRequestStatus updates the status of a HiL request
|
||||
func (h *HILProtocol) UpdateRequestStatus(id string, status HILStatus) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
req, ok := h.requests[id]
|
||||
if !ok {
|
||||
return fmt.Errorf("HiL request not found: %s", id)
|
||||
}
|
||||
|
||||
req.Status = status
|
||||
req.UpdatedAt = time.Now()
|
||||
|
||||
if status == HILStatusApproved || status == HILStatusRejected {
|
||||
now := time.Now()
|
||||
req.DecidedAt = &now
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecordResponse records a human decision response
|
||||
func (h *HILProtocol) RecordResponse(id string, response *HILResponse) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
req, ok := h.requests[id]
|
||||
if !ok {
|
||||
return fmt.Errorf("HiL request not found: %s", id)
|
||||
}
|
||||
|
||||
req.Response = response
|
||||
req.Status = HILStatusApproved // or rejected based on action
|
||||
req.UpdatedAt = time.Now()
|
||||
now := time.Now()
|
||||
req.DecidedAt = &now
|
||||
|
||||
log.Printf("[HiL] Response recorded for %s: %s by %s", id, response.Action, response.UserName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Timeout Management
|
||||
// =============================================================================
|
||||
|
||||
func (h *HILProtocol) getDeadline(escalationLevel int) time.Time {
|
||||
switch escalationLevel {
|
||||
case 1:
|
||||
return time.Now().Add(h.config.Level1Timeout)
|
||||
case 2:
|
||||
return time.Now().Add(h.config.Level2Timeout)
|
||||
case 3:
|
||||
return time.Now().Add(h.config.Level3Timeout)
|
||||
default:
|
||||
return time.Now().Add(h.config.DefaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckTimeouts checks for and handles timed out requests
|
||||
func (h *HILProtocol) CheckTimeouts() []*HILRequest {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
timedOut := make([]*HILRequest, 0)
|
||||
now := time.Now()
|
||||
|
||||
for _, req := range h.requests {
|
||||
if (req.Status == HILStatusPending || req.Status == HILStatusWaiting) && now.After(req.Deadline) {
|
||||
req.Status = HILStatusTimeout
|
||||
req.UpdatedAt = now
|
||||
timedOut = append(timedOut, req)
|
||||
log.Printf("[HiL] Request timed out: %s (task: %s)", req.ID, req.TaskID)
|
||||
}
|
||||
}
|
||||
|
||||
return timedOut
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Human Decision Panel (for frontend integration)
|
||||
// =============================================================================
|
||||
|
||||
// GetDecisionPanelData returns data needed to render a decision panel
|
||||
func (h *HILProtocol) GetDecisionPanelData(req *HILRequest) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"request_id": req.ID,
|
||||
"task_id": req.TaskID,
|
||||
"escalation_type": req.EscalationType,
|
||||
"escalation_level": req.EscalationLevel,
|
||||
"reason": req.Reason,
|
||||
"context": req.Context,
|
||||
"options": req.Options,
|
||||
"suggested_value": req.SuggestedValue,
|
||||
"deadline": req.Deadline.Format(time.RFC3339),
|
||||
"status": req.Status,
|
||||
"created_at": req.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateHumanDecisionPayload creates a HUMAN_DECISION payload
|
||||
func (h *HILProtocol) CreateHumanDecisionPayload(
|
||||
taskID string,
|
||||
replyTo string,
|
||||
user struct {
|
||||
UserID string
|
||||
UserName string
|
||||
Role string
|
||||
},
|
||||
decision struct {
|
||||
Action string
|
||||
ApprovedFields map[string]interface{}
|
||||
ModifiedFields map[string]interface{}
|
||||
SelectedOption any
|
||||
RejectionReason string
|
||||
},
|
||||
notes string,
|
||||
) *types.HumanDecisionPayload {
|
||||
payload := &types.HumanDecisionPayload{}
|
||||
payload.TaskID = taskID
|
||||
payload.DecisionID = fmt.Sprintf("decision-%d", time.Now().UnixNano())
|
||||
payload.ReplyTo = replyTo
|
||||
payload.DecidedBy.UserID = user.UserID
|
||||
payload.DecidedBy.UserName = user.UserName
|
||||
payload.DecidedBy.Role = user.Role
|
||||
payload.Decision.Action = decision.Action
|
||||
payload.Notes = notes
|
||||
payload.Timestamp = types.NowTimestamp()
|
||||
|
||||
switch decision.Action {
|
||||
case "approve":
|
||||
payload.Decision.ApprovedFields = decision.ApprovedFields
|
||||
case "reject":
|
||||
payload.Decision.RejectionReason = decision.RejectionReason
|
||||
case "select":
|
||||
payload.Decision.SelectedOption = decision.SelectedOption
|
||||
case "modify":
|
||||
payload.Decision.ModifiedFields = decision.ModifiedFields
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utility Methods
|
||||
// =============================================================================
|
||||
|
||||
// IsHILRequired checks if a task requires human intervention
|
||||
func IsHILRequired(reasonCode string) bool {
|
||||
hilRequiredCodes := map[string]bool{
|
||||
"BRAND_STYLE": true,
|
||||
"CREATIVE_DIRECTION": true,
|
||||
"COMPLIANCE": true,
|
||||
"CAPABILITY_LACKING": true,
|
||||
"USER_REQUESTED": true,
|
||||
"HIGH_COST_RISK": true,
|
||||
}
|
||||
return hilRequiredCodes[reasonCode]
|
||||
}
|
||||
|
||||
// GetEscalationLevel determines escalation level based on reason code
|
||||
func GetEscalationLevel(reasonCode string) int {
|
||||
levelMap := map[string]int{
|
||||
"INFO_MISSING": 1,
|
||||
"CREATIVE_DIRECTION": 1,
|
||||
"BRAND_STYLE": 2,
|
||||
"COMPLIANCE": 2,
|
||||
"CAPABILITY_LACKING": 2,
|
||||
"USER_REQUESTED": 1,
|
||||
"HIGH_COST_RISK": 2,
|
||||
"RESOURCE_DEADLOCK": 2,
|
||||
"STRATEGIC_DECISION": 3,
|
||||
}
|
||||
|
||||
if level, ok := levelMap[reasonCode]; ok {
|
||||
return level
|
||||
}
|
||||
return 1 // Default to level 1
|
||||
}
|
||||
595
p2p/protocol.go
Normal file
595
p2p/protocol.go
Normal file
@@ -0,0 +1,595 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// P2P Protocol Handler
|
||||
// =============================================================================
|
||||
|
||||
// TaskHandler is the interface for handling task execution
|
||||
type TaskHandler interface {
|
||||
// ExecuteTask executes a task and returns the result
|
||||
ExecuteTask(ctx context.Context, taskType string, payload map[string]interface{}) (any, error)
|
||||
|
||||
// ValidateTask validates if the task can be executed
|
||||
ValidateTask(taskType string, payload map[string]interface{}) ([]types.MissingField, bool)
|
||||
|
||||
// GetCapabilities returns the agent's capabilities
|
||||
GetCapabilities() []string
|
||||
}
|
||||
|
||||
// MessageHandler is the interface for handling incoming messages
|
||||
type MessageHandler interface {
|
||||
// HandleMessage handles an incoming P2P message
|
||||
HandleMessage(msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error)
|
||||
}
|
||||
|
||||
// CallbackSender is the interface for sending callbacks to remote agents
|
||||
type CallbackSender interface {
|
||||
// SendCallback sends a message to a remote agent
|
||||
SendCallback(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) error
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// P2PProtocol represents the P2P protocol handler
|
||||
// =============================================================================
|
||||
|
||||
// P2PProtocol handles point-to-point Synapse communication
|
||||
type P2PProtocol struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Agent identity
|
||||
agentEndpoint types.AgentEndpoint
|
||||
|
||||
// Task state
|
||||
tasks map[string]*types.P2PTaskStatus
|
||||
|
||||
// Handlers
|
||||
taskHandler TaskHandler
|
||||
callbackSender CallbackSender
|
||||
|
||||
// Configuration
|
||||
config *P2PConfig
|
||||
}
|
||||
|
||||
// P2PConfig holds P2P protocol configuration
|
||||
type P2PConfig struct {
|
||||
MaxRetries int // Max callback retries
|
||||
RetryBaseDelay time.Duration // Base delay for retries
|
||||
TaskTimeout time.Duration // Task execution timeout
|
||||
HeartbeatInterval time.Duration // Heartbeat interval
|
||||
}
|
||||
|
||||
// DefaultP2PConfig returns default P2P configuration
|
||||
func DefaultP2PConfig() *P2PConfig {
|
||||
return &P2PConfig{
|
||||
MaxRetries: 5,
|
||||
RetryBaseDelay: 2 * time.Second,
|
||||
TaskTimeout: 5 * time.Minute,
|
||||
HeartbeatInterval: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// P2PTask represents an active task
|
||||
// =============================================================================
|
||||
|
||||
type P2PTask struct {
|
||||
ID string
|
||||
Type string
|
||||
Priority types.Priority
|
||||
Status types.P2PTaskStatus
|
||||
Payload map[string]interface{}
|
||||
Sender types.AgentEndpoint
|
||||
Receiver types.AgentEndpoint
|
||||
CorrelationID string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CompletedAt *time.Time
|
||||
Result any
|
||||
Error *types.SynapseError
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NewP2PProtocol creates a new P2P protocol handler
|
||||
// =============================================================================
|
||||
|
||||
func NewP2PProtocol(agentEndpoint types.AgentEndpoint, config *P2PConfig) *P2PProtocol {
|
||||
if config == nil {
|
||||
config = DefaultP2PConfig()
|
||||
}
|
||||
return &P2PProtocol{
|
||||
agentEndpoint: agentEndpoint,
|
||||
tasks: make(map[string]*types.P2PTaskStatus),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// SetTaskHandler sets the task handler
|
||||
func (p *P2PProtocol) SetTaskHandler(h TaskHandler) {
|
||||
p.taskHandler = h
|
||||
}
|
||||
|
||||
// SetCallbackSender sets the callback sender
|
||||
func (p *P2PProtocol) SetCallbackSender(s CallbackSender) {
|
||||
p.callbackSender = s
|
||||
}
|
||||
|
||||
// AgentEndpoint returns the agent's endpoint
|
||||
func (p *P2PProtocol) AgentEndpoint() types.AgentEndpoint {
|
||||
return p.agentEndpoint
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HandleMessage processes an incoming P2P message
|
||||
// =============================================================================
|
||||
|
||||
func (p *P2PProtocol) HandleMessage(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Validate message
|
||||
if err := p.validateMessage(msg); err != nil {
|
||||
a2aErr, ok := err.(*types.SynapseError)
|
||||
if !ok {
|
||||
a2aErr = types.NewSynapseError(types.ErrMessageFormatInvalid, err.Error(), nil)
|
||||
}
|
||||
return p.newNACK(msg, a2aErr)
|
||||
}
|
||||
|
||||
// Route based on message type
|
||||
switch types.P2PMessageType(msg.Type) {
|
||||
case types.P2PTaskRequest:
|
||||
return p.handleTaskRequest(ctx, msg)
|
||||
case types.P2PTaskConfirm:
|
||||
return p.handleTaskConfirm(ctx, msg)
|
||||
case types.P2PTaskReject:
|
||||
return p.handleTaskReject(ctx, msg)
|
||||
case types.P2PTaskNeedInfo:
|
||||
return p.handleTaskNeedInfo(ctx, msg)
|
||||
case types.P2PTaskInfoProvided:
|
||||
return p.handleTaskInfoProvided(ctx, msg)
|
||||
case types.P2PTaskStatusUpdate:
|
||||
return p.handleTaskStatusUpdate(ctx, msg)
|
||||
case types.P2PTaskComplete:
|
||||
return p.handleTaskComplete(ctx, msg)
|
||||
case types.P2PTaskFailed:
|
||||
return p.handleTaskFailed(ctx, msg)
|
||||
case types.P2PTaskCancel:
|
||||
return p.handleTaskCancel(ctx, msg)
|
||||
case types.P2PTaskAck:
|
||||
return p.handleTaskAck(ctx, msg)
|
||||
case types.P2PTaskNeedHuman:
|
||||
return p.handleTaskNeedHuman(ctx, msg)
|
||||
case types.P2PHumanDecision:
|
||||
return p.handleHumanDecision(ctx, msg)
|
||||
case types.P2PAck:
|
||||
return p.handleAck(ctx, msg)
|
||||
case types.P2PNack:
|
||||
return p.handleNack(ctx, msg)
|
||||
case types.P2PHeartbeat:
|
||||
return p.handleHeartbeat(ctx, msg)
|
||||
default:
|
||||
return p.newNACK(msg, types.NewSynapseError(
|
||||
types.ErrMessageFormatInvalid,
|
||||
fmt.Sprintf("Unknown message type: %s", msg.Type),
|
||||
nil,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Message Handlers
|
||||
// =============================================================================
|
||||
|
||||
// handleTaskRequest handles incoming task requests
|
||||
func (p *P2PProtocol) handleTaskRequest(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskRequestPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(
|
||||
types.ErrMessageFormatInvalid,
|
||||
"Invalid task request payload",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
taskID := payload.Task.ID
|
||||
log.Printf("[P2P] Task request received: %s (type: %s)", taskID, payload.Task.Type)
|
||||
|
||||
// Check if we have a task handler
|
||||
if p.taskHandler == nil {
|
||||
return p.newReject(msg, taskID, "agent_not_ready", "Task handler not configured", nil)
|
||||
}
|
||||
|
||||
// Validate the task
|
||||
missingFields, canHandle := p.taskHandler.ValidateTask(payload.Task.Type, payload.Task.Payload)
|
||||
if !canHandle {
|
||||
if len(missingFields) > 0 {
|
||||
// Need more info
|
||||
return p.newNeedInfo(msg, taskID, missingFields)
|
||||
}
|
||||
return p.newReject(msg, taskID, "capability_lacking", "Agent cannot handle this task type", nil)
|
||||
}
|
||||
|
||||
// Create task context
|
||||
taskCtx, cancel := context.WithTimeout(ctx, p.config.TaskTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Execute task asynchronously
|
||||
go p.executeTask(taskCtx, msg, taskID, payload.Task.Type, payload.Task.Payload)
|
||||
|
||||
// Return immediate confirmation
|
||||
return p.newAck(msg, taskID, "task_accepted")
|
||||
}
|
||||
|
||||
// executeTask executes a task and handles callbacks
|
||||
func (p *P2PProtocol) executeTask(ctx context.Context, origMsg *types.UnifiedSynapseMessage, taskID, taskType string, payload map[string]interface{}) {
|
||||
// Send initial status update
|
||||
p.sendStatusUpdate(ctx, taskID, types.P2PStatusExecuting, 0, "Task started")
|
||||
|
||||
// Execute the task
|
||||
result, err := p.taskHandler.ExecuteTask(ctx, taskType, payload)
|
||||
|
||||
if err != nil {
|
||||
// Task failed
|
||||
p.sendTaskFailed(ctx, taskID, "execution_failed", err.Error(), true, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Task completed
|
||||
p.sendTaskComplete(ctx, taskID, result)
|
||||
}
|
||||
|
||||
// sendStatusUpdate sends a status update to the sender
|
||||
func (p *P2PProtocol) sendStatusUpdate(ctx context.Context, taskID string, status types.P2PTaskStatus, progress int, message string) {
|
||||
if p.callbackSender == nil {
|
||||
return
|
||||
}
|
||||
|
||||
payload := types.TaskStatusUpdatePayload{
|
||||
TaskID: taskID,
|
||||
Status: string(status),
|
||||
Progress: progress,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
response := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskStatusUpdate),
|
||||
Sender: p.agentEndpoint,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
if err := p.callbackSender.SendCallback(ctx, "", response); err != nil {
|
||||
log.Printf("[P2P] Failed to send status update: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendTaskComplete sends task completion notification
|
||||
func (p *P2PProtocol) sendTaskComplete(ctx context.Context, taskID string, result any) {
|
||||
if p.callbackSender == nil {
|
||||
return
|
||||
}
|
||||
|
||||
payload := types.TaskCompletePayload{}
|
||||
payload.TaskID = taskID
|
||||
payload.Result.OutputType = "task_result"
|
||||
payload.Result.Output = result
|
||||
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
response := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskComplete),
|
||||
Sender: p.agentEndpoint,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
if err := p.callbackSender.SendCallback(ctx, "", response); err != nil {
|
||||
log.Printf("[P2P] Failed to send task complete: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendTaskFailed sends task failure notification
|
||||
func (p *P2PProtocol) sendTaskFailed(ctx context.Context, taskID, errorCode, errorMessage string, recoverable bool, partialResult any) {
|
||||
if p.callbackSender == nil {
|
||||
return
|
||||
}
|
||||
|
||||
payload := types.TaskFailedPayload{}
|
||||
payload.TaskID = taskID
|
||||
payload.Error.Code = errorCode
|
||||
payload.Error.Message = errorMessage
|
||||
payload.Error.Recoverable = recoverable
|
||||
payload.PartialResult = partialResult
|
||||
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
response := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskFailed),
|
||||
Sender: p.agentEndpoint,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
if err := p.callbackSender.SendCallback(ctx, "", response); err != nil {
|
||||
log.Printf("[P2P] Failed to send task failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleTaskConfirm handles task confirmation from receiver
|
||||
func (p *P2PProtocol) handleTaskConfirm(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskConfirmPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid confirm payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Task confirmed: %s (estimated: %ds)", payload.TaskID, payload.EstimatedDuration)
|
||||
return p.newAck(msg, payload.TaskID, "confirmed")
|
||||
}
|
||||
|
||||
// handleTaskReject handles task rejection from receiver
|
||||
func (p *P2PProtocol) handleTaskReject(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskRejectPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid reject payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Task rejected: %s (%s: %s)", payload.TaskID, payload.Reason.Code, payload.Reason.Message)
|
||||
return p.newAck(msg, payload.TaskID, "rejected")
|
||||
}
|
||||
|
||||
// handleTaskNeedInfo handles information request from receiver
|
||||
func (p *P2PProtocol) handleTaskNeedInfo(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskNeedInfoPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid need info payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Information requested: %s (missing %d fields)", payload.TaskID, len(payload.MissingFields))
|
||||
|
||||
// This would typically be handled by the application logic
|
||||
// For now, just acknowledge
|
||||
return p.newAck(msg, payload.TaskID, "info_request_received")
|
||||
}
|
||||
|
||||
// handleTaskInfoProvided handles information provided by sender
|
||||
func (p *P2PProtocol) handleTaskInfoProvided(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskInfoProvidedPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid info provided payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Information provided: %s (%d fields)", payload.TaskID, len(payload.ProvidedInfo))
|
||||
return p.newAck(msg, payload.TaskID, "info_received")
|
||||
}
|
||||
|
||||
// handleTaskStatusUpdate handles status updates from receiver
|
||||
func (p *P2PProtocol) handleTaskStatusUpdate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskStatusUpdatePayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid status update payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Status update: %s - %s (progress: %d%%)", payload.TaskID, payload.Status, payload.Progress)
|
||||
return p.newAck(msg, payload.TaskID, "status_received")
|
||||
}
|
||||
|
||||
// handleTaskComplete handles task completion notification
|
||||
func (p *P2PProtocol) handleTaskComplete(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskCompletePayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid complete payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Task completed: %s", payload.TaskID)
|
||||
return p.newAck(msg, payload.TaskID, "completed")
|
||||
}
|
||||
|
||||
// handleTaskFailed handles task failure notification
|
||||
func (p *P2PProtocol) handleTaskFailed(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskFailedPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid failed payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Task failed: %s (%s: %s)", payload.TaskID, payload.Error.Code, payload.Error.Message)
|
||||
return p.newAck(msg, payload.TaskID, "failure_noted")
|
||||
}
|
||||
|
||||
// handleTaskCancel handles task cancellation
|
||||
func (p *P2PProtocol) handleTaskCancel(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskCancelPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid cancel payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Task cancelled: %s (by %s)", payload.TaskID, payload.CancelledBy)
|
||||
return p.newAck(msg, payload.TaskID, "cancelled")
|
||||
}
|
||||
|
||||
// handleTaskAck handles task-level acknowledgment
|
||||
func (p *P2PProtocol) handleTaskAck(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
log.Printf("[P2P] Task ACK received for message: %s", msg.ReplyTo)
|
||||
return nil, nil // No response needed for ACK
|
||||
}
|
||||
|
||||
// handleTaskNeedHuman handles human intervention request
|
||||
func (p *P2PProtocol) handleTaskNeedHuman(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.TaskNeedHumanPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid need human payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Human intervention needed: %s (level: %d)", payload.TaskID, payload.EscalationLevel)
|
||||
|
||||
// Store the HiL request and wait for human decision
|
||||
// This would typically be handled by a HiL manager
|
||||
|
||||
return p.newAck(msg, payload.TaskID, "human_intervention_requested")
|
||||
}
|
||||
|
||||
// handleHumanDecision handles human decision response
|
||||
func (p *P2PProtocol) handleHumanDecision(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
var payload types.HumanDecisionPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return p.newNACK(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid human decision payload", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("[P2P] Human decision: %s (action: %s, by %s)", payload.TaskID, payload.Decision.Action, payload.DecidedBy.UserName)
|
||||
|
||||
// Process the human decision
|
||||
// This would resume the task execution with the decision
|
||||
|
||||
return p.newAck(msg, payload.TaskID, "decision_received")
|
||||
}
|
||||
|
||||
// handleAck handles generic ACK
|
||||
func (p *P2PProtocol) handleAck(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
log.Printf("[P2P] ACK received for: %s", msg.ReplyTo)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// handleNack handles generic NACK
|
||||
func (p *P2PProtocol) handleNack(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
log.Printf("[P2P] NACK received for: %s", msg.ReplyTo)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// handleHeartbeat handles heartbeat
|
||||
func (p *P2PProtocol) handleHeartbeat(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
return p.newAck(msg, "", "alive")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Response Builders
|
||||
// =============================================================================
|
||||
|
||||
// newAck creates an ACK response
|
||||
func (p *P2PProtocol) newAck(msg *types.UnifiedSynapseMessage, taskID, note string) (*types.UnifiedSynapseMessage, error) {
|
||||
return &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PAck),
|
||||
Sender: p.agentEndpoint,
|
||||
Receiver: &msg.Sender,
|
||||
CorrelationID: msg.CorrelationID,
|
||||
ReplyTo: msg.ID,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newNACK creates a NACK response
|
||||
func (p *P2PProtocol) newNACK(msg *types.UnifiedSynapseMessage, err *types.SynapseError) (*types.UnifiedSynapseMessage, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// newReject creates a task rejection response
|
||||
func (p *P2PProtocol) newReject(msg *types.UnifiedSynapseMessage, taskID, reasonCode, reasonMessage string, alternatives *struct {
|
||||
SuggestedAgentID string `json:"suggested_agent_id,omitempty"`
|
||||
RetryAfter int `json:"retry_after,omitempty"`
|
||||
PartialSupport bool `json:"partial_support,omitempty"`
|
||||
}) (*types.UnifiedSynapseMessage, error) {
|
||||
payload := types.TaskRejectPayload{}
|
||||
payload.TaskID = taskID
|
||||
payload.Reason.Code = reasonCode
|
||||
payload.Reason.Message = reasonMessage
|
||||
if alternatives != nil {
|
||||
payload.Alternatives = alternatives
|
||||
}
|
||||
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
return &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskReject),
|
||||
Sender: p.agentEndpoint,
|
||||
Receiver: &msg.Sender,
|
||||
CorrelationID: msg.CorrelationID,
|
||||
ReplyTo: msg.ID,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newNeedInfo creates a need info response
|
||||
func (p *P2PProtocol) newNeedInfo(msg *types.UnifiedSynapseMessage, taskID string, missingFields []types.MissingField) (*types.UnifiedSynapseMessage, error) {
|
||||
payload := types.TaskNeedInfoPayload{}
|
||||
payload.TaskID = taskID
|
||||
payload.MissingFields = missingFields
|
||||
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
return &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskNeedInfo),
|
||||
Sender: p.agentEndpoint,
|
||||
Receiver: &msg.Sender,
|
||||
CorrelationID: msg.CorrelationID,
|
||||
ReplyTo: msg.ID,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Validation
|
||||
// =============================================================================
|
||||
|
||||
func (p *P2PProtocol) validateMessage(msg *types.UnifiedSynapseMessage) error {
|
||||
if msg.ID == "" {
|
||||
return types.ErrMessageFormatInvalidError
|
||||
}
|
||||
if msg.Sender.AgentID == "" {
|
||||
return types.NewSynapseError(types.ErrMessageFormatInvalid, "Missing sender agent_id", nil)
|
||||
}
|
||||
if msg.Sender.Endpoint == "" {
|
||||
return types.NewSynapseError(types.ErrMessageFormatInvalid, "Missing sender endpoint", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Task Management
|
||||
// =============================================================================
|
||||
|
||||
// GetTaskStatus returns the status of a task
|
||||
func (p *P2PProtocol) GetTaskStatus(taskID string) (types.P2PTaskStatus, bool) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
status, ok := p.tasks[taskID]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return *status, true
|
||||
}
|
||||
|
||||
// ListTasks returns all tasks
|
||||
func (p *P2PProtocol) ListTasks() map[string]types.P2PTaskStatus {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
result := make(map[string]types.P2PTaskStatus)
|
||||
for k, v := range p.tasks {
|
||||
result[k] = *v
|
||||
}
|
||||
return result
|
||||
}
|
||||
525
proto/synapse.proto
Normal file
525
proto/synapse.proto
Normal file
@@ -0,0 +1,525 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package synapse;
|
||||
|
||||
option go_package = "synapse/grpcapi;synapse";
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
|
||||
// =============================================================================
|
||||
// Synapse gRPC Protocol Definition
|
||||
// =============================================================================
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||
info: {
|
||||
title: "Synapse Protocol API";
|
||||
version: "1.0.0";
|
||||
description: "Synapse (灵隙) - Multi-agent collaboration protocol with gRPC and HTTP gateway support";
|
||||
};
|
||||
schemes: HTTPS;
|
||||
consumes: "application/json";
|
||||
produces: "application/json";
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Core Enums
|
||||
// =============================================================================
|
||||
|
||||
enum TransportMode {
|
||||
TRANSPORT_MODE_UNSPECIFIED = 0;
|
||||
TRANSPORT_MODE_CALLBACK = 1;
|
||||
TRANSPORT_MODE_POLLING = 2;
|
||||
TRANSPORT_MODE_SYNC = 3;
|
||||
TRANSPORT_MODE_HYBRID = 4;
|
||||
}
|
||||
|
||||
enum MessageType {
|
||||
MESSAGE_TYPE_UNSPECIFIED = 0;
|
||||
MESSAGE_TYPE_P2P_TASK_REQUEST = 1;
|
||||
MESSAGE_TYPE_P2P_TASK_COMPLETE = 2;
|
||||
MESSAGE_TYPE_P2P_TASK_FAILED = 3;
|
||||
MESSAGE_TYPE_P2P_STATUS_UPDATE = 4;
|
||||
MESSAGE_TYPE_P2P_INFO_MISSING = 5;
|
||||
MESSAGE_TYPE_P2P_ACK = 6;
|
||||
MESSAGE_TYPE_P2P_NACK = 7;
|
||||
MESSAGE_TYPE_GROUP_INVITE = 8;
|
||||
MESSAGE_TYPE_GROUP_JOIN = 9;
|
||||
MESSAGE_TYPE_GROUP_LEAVE = 10;
|
||||
MESSAGE_TYPE_GROUP_TASK_SPLIT = 11;
|
||||
MESSAGE_TYPE_GROUP_SUBTASK_RESULT = 12;
|
||||
MESSAGE_TYPE_GROUP_RESULT_MERGE = 13;
|
||||
MESSAGE_TYPE_GROUP_VOTE_INITIATE = 14;
|
||||
MESSAGE_TYPE_GROUP_VOTE_SUBMIT = 15;
|
||||
MESSAGE_TYPE_GROUP_VOTE_RESULT = 16;
|
||||
MESSAGE_TYPE_HIL_DECISION_REQUEST = 17;
|
||||
MESSAGE_TYPE_HIL_DECISION_RESPONSE = 18;
|
||||
MESSAGE_TYPE_HEARTBEAT = 19;
|
||||
}
|
||||
|
||||
enum TaskStatus {
|
||||
TASK_STATUS_UNSPECIFIED = 0;
|
||||
TASK_STATUS_RECEIVED = 1;
|
||||
TASK_STATUS_PROCESSING = 2;
|
||||
TASK_STATUS_COMPLETED = 3;
|
||||
TASK_STATUS_FAILED = 4;
|
||||
TASK_STATUS_CANCELLED = 5;
|
||||
TASK_STATUS_REJECTED = 6;
|
||||
TASK_STATUS_WAITING = 7;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Core Messages
|
||||
// =============================================================================
|
||||
|
||||
message AgentEndpoint {
|
||||
string agent_id = 1;
|
||||
string agent_name = 2;
|
||||
string system_id = 3;
|
||||
string endpoint = 4;
|
||||
string version = 5;
|
||||
}
|
||||
|
||||
message SynapseMessage {
|
||||
string id = 1;
|
||||
string version = 2;
|
||||
MessageType type = 3;
|
||||
AgentEndpoint sender = 4;
|
||||
AgentEndpoint receiver = 5;
|
||||
string correlation_id = 6;
|
||||
string reply_to = 7;
|
||||
string group_id = 8;
|
||||
google.protobuf.Timestamp timestamp = 9;
|
||||
google.protobuf.Any payload = 10;
|
||||
string signature = 11;
|
||||
}
|
||||
|
||||
message TaskPayload {
|
||||
string task_id = 1;
|
||||
string task_type = 2;
|
||||
google.protobuf.Any payload = 3;
|
||||
string external_task_id = 4;
|
||||
}
|
||||
|
||||
message TaskResult {
|
||||
string task_id = 1;
|
||||
string task_type = 2;
|
||||
google.protobuf.Any result = 3;
|
||||
string summary = 4;
|
||||
}
|
||||
|
||||
message TaskStatusUpdate {
|
||||
string task_id = 1;
|
||||
TaskStatus status = 2;
|
||||
int32 progress = 3;
|
||||
string message = 4;
|
||||
google.protobuf.Any details = 5;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Task Service
|
||||
// =============================================================================
|
||||
|
||||
service TaskService {
|
||||
// SubmitTask 提交任务
|
||||
rpc SubmitTask(SubmitTaskRequest) returns (SubmitTaskResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/task"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// SubmitTaskWithFiles 提交带文件的任务(Multipart)
|
||||
rpc SubmitTaskWithFiles(SubmitTaskWithFilesRequest) returns (SubmitTaskResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/task/multipart"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// GetTask 获取任务详情
|
||||
rpc GetTask(GetTaskRequest) returns (TaskStatusResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/synapse/task/{task_id}"
|
||||
};
|
||||
}
|
||||
|
||||
// GetTaskStatus 获取任务状态(简化)
|
||||
rpc GetTaskStatus(GetTaskRequest) returns (TaskStatusResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/synapse/task/{task_id}/status"
|
||||
};
|
||||
}
|
||||
|
||||
// ListTasks 任务列表
|
||||
rpc ListTasks(ListTasksRequest) returns (ListTasksResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/synapse/tasks"
|
||||
};
|
||||
}
|
||||
|
||||
// CancelTask 取消任务
|
||||
rpc CancelTask(CancelTaskRequest) returns (CancelTaskResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/task/{task_id}/cancel"
|
||||
};
|
||||
}
|
||||
|
||||
// StreamTaskStatus 流式获取任务状态(双向流)
|
||||
rpc StreamTaskStatus(stream TaskStatusStreamRequest) returns (stream TaskStatusUpdate);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Task Request/Response Messages
|
||||
// =============================================================================
|
||||
|
||||
message SubmitTaskRequest {
|
||||
string task_type = 1;
|
||||
google.protobuf.Any payload = 2;
|
||||
string external_task_id = 3;
|
||||
string priority = 4;
|
||||
string callback_url = 5;
|
||||
string callback_token = 6;
|
||||
TransportMode preferred_transport = 7;
|
||||
}
|
||||
|
||||
message SubmitTaskResponse {
|
||||
bool success = 1;
|
||||
string task_id = 2;
|
||||
TaskStatus status = 3;
|
||||
string message = 4;
|
||||
string error_code = 5;
|
||||
}
|
||||
|
||||
message SubmitTaskWithFilesRequest {
|
||||
SubmitTaskRequest request = 1;
|
||||
repeated FileUpload files = 2;
|
||||
}
|
||||
|
||||
message FileUpload {
|
||||
string field_name = 1;
|
||||
string filename = 2;
|
||||
string mime_type = 3;
|
||||
bytes data = 4;
|
||||
}
|
||||
|
||||
message GetTaskRequest {
|
||||
string task_id = 1;
|
||||
}
|
||||
|
||||
message TaskStatusResponse {
|
||||
bool success = 1;
|
||||
TaskInfo task = 2;
|
||||
string error_code = 3;
|
||||
string error_message = 4;
|
||||
}
|
||||
|
||||
message TaskInfo {
|
||||
string id = 1;
|
||||
string type = 2;
|
||||
TaskStatus status = 3;
|
||||
int32 progress = 4;
|
||||
string message = 5;
|
||||
google.protobuf.Timestamp created_at = 6;
|
||||
google.protobuf.Timestamp updated_at = 7;
|
||||
google.protobuf.Timestamp completed_at = 8;
|
||||
google.protobuf.Timestamp eta = 9;
|
||||
google.protobuf.Any result = 10;
|
||||
TaskError error = 11;
|
||||
string external_task_id = 12;
|
||||
}
|
||||
|
||||
message TaskError {
|
||||
string code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message ListTasksRequest {
|
||||
string status = 1;
|
||||
int32 limit = 2;
|
||||
int32 offset = 3;
|
||||
}
|
||||
|
||||
message ListTasksResponse {
|
||||
bool success = 1;
|
||||
repeated TaskInfo tasks = 2;
|
||||
int32 total = 3;
|
||||
int32 limit = 4;
|
||||
int32 offset = 5;
|
||||
bool has_more = 6;
|
||||
}
|
||||
|
||||
message CancelTaskRequest {
|
||||
string task_id = 1;
|
||||
}
|
||||
|
||||
message CancelTaskResponse {
|
||||
bool success = 1;
|
||||
string task_id = 2;
|
||||
TaskStatus status = 3;
|
||||
string message = 4;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Stream Messages
|
||||
// =============================================================================
|
||||
|
||||
message TaskStatusStreamRequest {
|
||||
string task_id = 1;
|
||||
bool subscribe = 2; // true=订阅, false=取消订阅
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Synapse Message Service
|
||||
// =============================================================================
|
||||
|
||||
service SynapseMessageService {
|
||||
// SendMessage 发送Synapse消息(Unary)
|
||||
rpc SendMessage(SendMessageRequest) returns (SendMessageResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/message"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// StreamMessages 双向流式消息传输
|
||||
rpc StreamMessages(stream SynapseMessage) returns (stream SynapseMessage);
|
||||
|
||||
// ReceiveCallback 接收回调消息
|
||||
rpc ReceiveCallback(google.protobuf.Empty) returns (stream SynapseMessage) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/callback"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message SendMessageRequest {
|
||||
SynapseMessage message = 1;
|
||||
}
|
||||
|
||||
message SendMessageResponse {
|
||||
bool success = 1;
|
||||
string message_id = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Capabilities Service
|
||||
// =============================================================================
|
||||
|
||||
service CapabilitiesService {
|
||||
// GetCapabilities 获取Agent能力
|
||||
rpc GetCapabilities(google.protobuf.Empty) returns (CapabilitiesResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/synapse/capabilities"
|
||||
};
|
||||
}
|
||||
|
||||
// QueryAgent 查询特定Agent的能力
|
||||
rpc QueryAgent(QueryAgentRequest) returns (CapabilitiesResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/synapse/agents/{agent_id}"
|
||||
};
|
||||
}
|
||||
|
||||
// ListAgents 列出可用Agent
|
||||
rpc ListAgents(google.protobuf.Empty) returns (ListAgentsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/synapse/agents"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message CapabilitiesResponse {
|
||||
bool success = 1;
|
||||
AgentCapabilities capabilities = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
message AgentCapabilities {
|
||||
string agent_id = 1;
|
||||
string agent_name = 2;
|
||||
string system_id = 3;
|
||||
string protocol_version = 4;
|
||||
repeated SupportedTaskType supported_task_types = 5;
|
||||
repeated string supported_content_types = 6;
|
||||
repeated TransportMode supported_transport_modes = 7;
|
||||
int64 max_file_size = 8;
|
||||
int32 max_concurrent_tasks = 9;
|
||||
int32 max_task_duration_seconds = 10;
|
||||
bool support_progress_updates = 11;
|
||||
bool support_streaming = 12;
|
||||
bool support_hil = 13;
|
||||
bool support_groups = 14;
|
||||
}
|
||||
|
||||
message SupportedTaskType {
|
||||
string type = 1;
|
||||
string name = 2;
|
||||
string description = 3;
|
||||
string estimated_time = 4;
|
||||
string input_schema = 5;
|
||||
string output_schema = 6;
|
||||
repeated string capabilities = 7;
|
||||
}
|
||||
|
||||
message QueryAgentRequest {
|
||||
string agent_id = 1;
|
||||
}
|
||||
|
||||
message ListAgentsResponse {
|
||||
bool success = 1;
|
||||
repeated AgentInfo agents = 2;
|
||||
int32 total = 3;
|
||||
}
|
||||
|
||||
message AgentInfo {
|
||||
string id = 1;
|
||||
string agent_id = 2;
|
||||
string agent_name = 3;
|
||||
string system_id = 4;
|
||||
string endpoint = 5;
|
||||
bool is_active = 6;
|
||||
int32 trust_level = 7;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Group Service
|
||||
// =============================================================================
|
||||
|
||||
service GroupService {
|
||||
// CreateGroup 创建群组
|
||||
rpc CreateGroup(CreateGroupRequest) returns (CreateGroupResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/group"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// JoinGroup 加入群组
|
||||
rpc JoinGroup(JoinGroupRequest) returns (JoinGroupResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/group/{group_id}/join"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// LeaveGroup 离开群组
|
||||
rpc LeaveGroup(LeaveGroupRequest) returns (LeaveGroupResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/group/{group_id}/leave"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// DisbandGroup 解散群组
|
||||
rpc DisbandGroup(DisbandGroupRequest) returns (DisbandGroupResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/synapse/group/{group_id}/disband"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// ListGroupMembers 列出群组成员
|
||||
rpc ListGroupMembers(ListGroupMembersRequest) returns (ListGroupMembersResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/synapse/group/{group_id}/members"
|
||||
};
|
||||
}
|
||||
|
||||
// StreamGroupMessages 群组消息流
|
||||
rpc StreamGroupMessages(stream SynapseMessage) returns (stream SynapseMessage);
|
||||
}
|
||||
|
||||
message CreateGroupRequest {
|
||||
string name = 1;
|
||||
string description = 2;
|
||||
string coordinator_id = 3;
|
||||
repeated string initial_members = 4;
|
||||
}
|
||||
|
||||
message CreateGroupResponse {
|
||||
bool success = 1;
|
||||
string group_id = 2;
|
||||
GroupInfo group = 3;
|
||||
string error = 4;
|
||||
}
|
||||
|
||||
message GroupInfo {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string description = 3;
|
||||
string coordinator_id = 4;
|
||||
string status = 5;
|
||||
google.protobuf.Timestamp created_at = 6;
|
||||
}
|
||||
|
||||
message JoinGroupRequest {
|
||||
string group_id = 1;
|
||||
AgentEndpoint agent = 2;
|
||||
}
|
||||
|
||||
message JoinGroupResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message LeaveGroupRequest {
|
||||
string group_id = 1;
|
||||
string agent_id = 2;
|
||||
}
|
||||
|
||||
message LeaveGroupResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message DisbandGroupRequest {
|
||||
string group_id = 1;
|
||||
}
|
||||
|
||||
message DisbandGroupResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message ListGroupMembersRequest {
|
||||
string group_id = 1;
|
||||
}
|
||||
|
||||
message ListGroupMembersResponse {
|
||||
bool success = 1;
|
||||
repeated GroupMember members = 2;
|
||||
}
|
||||
|
||||
message GroupMember {
|
||||
string agent_id = 1;
|
||||
string agent_name = 2;
|
||||
string role = 3;
|
||||
string status = 4;
|
||||
google.protobuf.Timestamp joined_at = 5;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Health Service
|
||||
// =============================================================================
|
||||
|
||||
service HealthService {
|
||||
// HealthCheck 健康检查
|
||||
rpc HealthCheck(google.protobuf.Empty) returns (HealthCheckResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/v1/synapse/health"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message HealthCheckResponse {
|
||||
string status = 1;
|
||||
string version = 2;
|
||||
int64 uptime_seconds = 3;
|
||||
int32 active_tasks = 4;
|
||||
}
|
||||
451
routing/router.go
Normal file
451
routing/router.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"synapse/group"
|
||||
"synapse/hil"
|
||||
"synapse/p2p"
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Message Router
|
||||
// =============================================================================
|
||||
|
||||
// Router handles routing of Synapse messages
|
||||
type Router struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Protocols
|
||||
p2pProtocol *p2p.P2PProtocol
|
||||
groupProtocol *group.GroupProtocol
|
||||
hilProtocol *hil.HILProtocol
|
||||
|
||||
// External routing
|
||||
externalRoutes map[string]string // system_id -> gateway endpoint
|
||||
|
||||
// Configuration
|
||||
config *RouterConfig
|
||||
}
|
||||
|
||||
// RouterConfig holds router configuration
|
||||
type RouterConfig struct {
|
||||
MaxHops int
|
||||
EnableCaching bool
|
||||
}
|
||||
|
||||
// DefaultRouterConfig returns default router configuration
|
||||
func DefaultRouterConfig() *RouterConfig {
|
||||
return &RouterConfig{
|
||||
MaxHops: 10,
|
||||
EnableCaching: true,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NewRouter creates a new message router
|
||||
// =============================================================================
|
||||
|
||||
func NewRouter(config *RouterConfig) *Router {
|
||||
if config == nil {
|
||||
config = DefaultRouterConfig()
|
||||
}
|
||||
return &Router{
|
||||
externalRoutes: make(map[string]string),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// SetP2PProtocol sets the P2P protocol handler
|
||||
func (r *Router) SetP2PProtocol(p *p2p.P2PProtocol) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.p2pProtocol = p
|
||||
}
|
||||
|
||||
// SetGroupProtocol sets the Group protocol handler
|
||||
func (r *Router) SetGroupProtocol(g *group.GroupProtocol) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.groupProtocol = g
|
||||
}
|
||||
|
||||
// SetHILProtocol sets the HiL protocol handler
|
||||
func (r *Router) SetHILProtocol(h *hil.HILProtocol) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.hilProtocol = h
|
||||
}
|
||||
|
||||
// RegisterExternalRoute registers an external system gateway
|
||||
func (r *Router) RegisterExternalRoute(systemID, endpoint string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.externalRoutes[systemID] = endpoint
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RouteMessage routes an incoming message to the appropriate handler
|
||||
// =============================================================================
|
||||
|
||||
func (r *Router) RouteMessage(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// Validate message
|
||||
if err := r.validateMessage(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Route based on message category
|
||||
if isGroupMessage(msg.Type) {
|
||||
if r.groupProtocol == nil {
|
||||
return nil, fmt.Errorf("group protocol not configured")
|
||||
}
|
||||
return r.groupProtocol.HandleMessage(ctx, msg)
|
||||
}
|
||||
|
||||
if isP2PMessage(msg.Type) {
|
||||
if r.p2pProtocol == nil {
|
||||
return nil, fmt.Errorf("p2p protocol not configured")
|
||||
}
|
||||
return r.p2pProtocol.HandleMessage(ctx, msg)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown message type: %s", msg.Type)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SendMessage sends a message to a destination
|
||||
// =============================================================================
|
||||
|
||||
// MessageSender is the interface for sending messages
|
||||
type MessageSender interface {
|
||||
Send(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) error
|
||||
}
|
||||
|
||||
// TransportSender is used by the router to send messages
|
||||
var _ MessageSender = (*TransportSender)(nil)
|
||||
|
||||
// TransportSender is a placeholder for actual transport implementation
|
||||
type TransportSender struct {
|
||||
httpClient *HTTPClient
|
||||
}
|
||||
|
||||
// HTTPClient is a simple HTTP client for sending messages
|
||||
type HTTPClient struct {
|
||||
baseURL string
|
||||
timeoutMs int
|
||||
}
|
||||
|
||||
// Send sends a message via HTTP
|
||||
func (t *TransportSender) Send(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) error {
|
||||
if endpoint == "" {
|
||||
return fmt.Errorf("endpoint is required")
|
||||
}
|
||||
|
||||
_, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
// In a real implementation, this would use net/http to send the message
|
||||
log.Printf("[Router] Sending message to %s: type=%s, id=%s", endpoint, msg.Type, msg.ID)
|
||||
|
||||
return nil // Placeholder - actual HTTP call would go here
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CreateOutboundMessage creates a message for sending to a remote agent
|
||||
// =============================================================================
|
||||
|
||||
func (r *Router) CreateOutboundMessage(
|
||||
msgType string,
|
||||
sender types.AgentEndpoint,
|
||||
receiver *types.AgentEndpoint,
|
||||
payload any,
|
||||
correlationID string,
|
||||
) (*types.UnifiedSynapseMessage, error) {
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
msg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: msgType,
|
||||
Sender: sender,
|
||||
Receiver: receiver,
|
||||
CorrelationID: correlationID,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CreateTaskRequest creates a task request message
|
||||
// =============================================================================
|
||||
|
||||
func (r *Router) CreateTaskRequest(
|
||||
sender types.AgentEndpoint,
|
||||
receiver types.AgentEndpoint,
|
||||
taskID string,
|
||||
taskType string,
|
||||
taskPayload map[string]interface{},
|
||||
priority types.Priority,
|
||||
correlationID string,
|
||||
) (*types.UnifiedSynapseMessage, error) {
|
||||
payload := types.TaskRequestPayload{}
|
||||
payload.Task.ID = taskID
|
||||
payload.Task.Type = taskType
|
||||
payload.Task.Priority = priority
|
||||
payload.Task.Payload = taskPayload
|
||||
|
||||
return r.CreateOutboundMessage(
|
||||
string(types.P2PTaskRequest),
|
||||
sender,
|
||||
&receiver,
|
||||
payload,
|
||||
correlationID,
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CreateTaskNeedHuman creates a need human message
|
||||
// =============================================================================
|
||||
|
||||
func (r *Router) CreateTaskNeedHuman(
|
||||
sender types.AgentEndpoint,
|
||||
receiver types.AgentEndpoint,
|
||||
taskID string,
|
||||
reasonCode, reasonMessage string,
|
||||
context struct {
|
||||
TaskSummary string
|
||||
CurrentProgress int
|
||||
BlockedReason string
|
||||
RelevantHistory []any
|
||||
},
|
||||
options []struct {
|
||||
Field string
|
||||
Value any
|
||||
ProsCons string
|
||||
Confidence int
|
||||
},
|
||||
suggestedValue any,
|
||||
escalationLevel int,
|
||||
correlationID string,
|
||||
) (*types.UnifiedSynapseMessage, error) {
|
||||
payload := types.TaskNeedHumanPayload{}
|
||||
payload.TaskID = taskID
|
||||
payload.Reason.Code = reasonCode
|
||||
payload.Reason.Message = reasonMessage
|
||||
payload.Context.TaskSummary = context.TaskSummary
|
||||
payload.Context.CurrentProgress = context.CurrentProgress
|
||||
payload.Context.BlockedReason = context.BlockedReason
|
||||
payload.Context.RelevantHistory = context.RelevantHistory
|
||||
|
||||
// Copy options
|
||||
payload.Options = make([]struct {
|
||||
Field string `json:"field"`
|
||||
Value any `json:"value"`
|
||||
ProsCons string `json:"pros_cons,omitempty"`
|
||||
Confidence int `json:"confidence,omitempty"`
|
||||
}, len(options))
|
||||
for i, opt := range options {
|
||||
payload.Options[i].Field = opt.Field
|
||||
payload.Options[i].Value = opt.Value
|
||||
payload.Options[i].ProsCons = opt.ProsCons
|
||||
payload.Options[i].Confidence = opt.Confidence
|
||||
}
|
||||
|
||||
payload.SuggestedValue = suggestedValue
|
||||
payload.EscalationLevel = escalationLevel
|
||||
|
||||
return r.CreateOutboundMessage(
|
||||
string(types.P2PTaskNeedHuman),
|
||||
sender,
|
||||
&receiver,
|
||||
payload,
|
||||
correlationID,
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CreateGroupTaskSplit creates a group task split message
|
||||
// =============================================================================
|
||||
|
||||
func (r *Router) CreateGroupTaskSplit(
|
||||
sender types.AgentEndpoint,
|
||||
groupID string,
|
||||
originalTask struct {
|
||||
ID string
|
||||
Type string
|
||||
Requirements map[string]interface{}
|
||||
Deadline *types.Timestamp
|
||||
},
|
||||
subtasks []types.Subtask,
|
||||
mergeStrategy types.MergeStrategy,
|
||||
correlationID string,
|
||||
) (*types.UnifiedSynapseMessage, error) {
|
||||
payload := types.GroupTaskSplitPayload{}
|
||||
payload.OriginalTask.ID = originalTask.ID
|
||||
payload.OriginalTask.Type = originalTask.Type
|
||||
payload.OriginalTask.Requirements = originalTask.Requirements
|
||||
payload.OriginalTask.Deadline = originalTask.Deadline
|
||||
payload.Subtasks = subtasks
|
||||
payload.MergeStrategy = mergeStrategy
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
msg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.GroupTaskSplit),
|
||||
Sender: sender,
|
||||
GroupID: groupID,
|
||||
Broadcast: false,
|
||||
CorrelationID: correlationID,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CreateGroupBroadcast creates a group broadcast message
|
||||
// =============================================================================
|
||||
|
||||
func (r *Router) CreateGroupBroadcast(
|
||||
sender types.AgentEndpoint,
|
||||
groupID string,
|
||||
msgType string,
|
||||
payload any,
|
||||
correlationID string,
|
||||
) (*types.UnifiedSynapseMessage, error) {
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
msg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: msgType,
|
||||
Sender: sender,
|
||||
GroupID: groupID,
|
||||
Broadcast: true,
|
||||
CorrelationID: correlationID,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CreateGroupVote creates a group vote message
|
||||
// =============================================================================
|
||||
|
||||
func (r *Router) CreateGroupVote(
|
||||
sender types.AgentEndpoint,
|
||||
groupID string,
|
||||
voteID string,
|
||||
question string,
|
||||
options []string,
|
||||
voters []string,
|
||||
threshold float64,
|
||||
deadline types.Timestamp,
|
||||
correlationID string,
|
||||
) (*types.UnifiedSynapseMessage, error) {
|
||||
payload := types.GroupVotePayload{}
|
||||
payload.VoteID = voteID
|
||||
payload.Question = question
|
||||
payload.Options = options
|
||||
payload.Voters = voters
|
||||
payload.Threshold = threshold
|
||||
payload.Deadline = deadline
|
||||
payload.Status = "open"
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
msg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.GroupVote),
|
||||
Sender: sender,
|
||||
GroupID: groupID,
|
||||
Broadcast: true,
|
||||
CorrelationID: correlationID,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Validation
|
||||
// =============================================================================
|
||||
|
||||
func (r *Router) validateMessage(msg *types.UnifiedSynapseMessage) error {
|
||||
if msg.ID == "" {
|
||||
return types.ErrMessageFormatInvalidError
|
||||
}
|
||||
if msg.Sender.AgentID == "" {
|
||||
return types.NewSynapseError(types.ErrMessageFormatInvalid, "Missing sender agent_id", nil)
|
||||
}
|
||||
if msg.Sender.Endpoint == "" {
|
||||
return types.NewSynapseError(types.ErrMessageFormatInvalid, "Missing sender endpoint", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
func isP2PMessage(msgType string) bool {
|
||||
return strings.HasPrefix(msgType, "p2p:")
|
||||
}
|
||||
|
||||
func isGroupMessage(msgType string) bool {
|
||||
return strings.HasPrefix(msgType, "group:") || strings.HasPrefix(msgType, "inter_group:")
|
||||
}
|
||||
|
||||
// GetRoute determines the route for a message
|
||||
func (r *Router) GetRoute(msg *types.UnifiedSynapseMessage) (routeType string, target string, err error) {
|
||||
// P2P messages
|
||||
if msg.Receiver != nil {
|
||||
if msg.Receiver.SystemID != r.p2pProtocol.AgentEndpoint().SystemID {
|
||||
// External system
|
||||
if endpoint, ok := r.externalRoutes[msg.Receiver.SystemID]; ok {
|
||||
return "external", endpoint, nil
|
||||
}
|
||||
return "unknown", "", fmt.Errorf("no route for system: %s", msg.Receiver.SystemID)
|
||||
}
|
||||
return "local", msg.Receiver.AgentID, nil
|
||||
}
|
||||
|
||||
// Group messages
|
||||
if msg.GroupID != "" {
|
||||
return "group", msg.GroupID, nil
|
||||
}
|
||||
|
||||
return "unknown", "", fmt.Errorf("no route found for message")
|
||||
}
|
||||
547
sdk/caller/client.go
Normal file
547
sdk/caller/client.go
Normal file
@@ -0,0 +1,547 @@
|
||||
package caller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Synapse Caller SDK (P2A Protocol)
|
||||
// 非Agent调用方使用的客户端SDK
|
||||
// 基于 P2A 协议:非 Agent 调用方通过 REST API 调用 Agent 能力
|
||||
// =============================================================================
|
||||
|
||||
// Client 是 Synapse P2A 服务的外部调用客户端
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// ClientConfig 客户端配置
|
||||
type ClientConfig struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// NewClient 创建新的 Synapse Caller 客户端
|
||||
func NewClient(config ClientConfig) *Client {
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 30 * time.Second
|
||||
}
|
||||
return &Client{
|
||||
baseURL: config.BaseURL,
|
||||
apiKey: config.APIKey,
|
||||
httpClient: &http.Client{Timeout: config.Timeout},
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 通用响应结构
|
||||
// =============================================================================
|
||||
|
||||
// APIResponse 通用 API 响应
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TaskResponse 任务提交响应
|
||||
type TaskResponse struct {
|
||||
APIResponse
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// TaskStatusResponse 任务状态响应(简化版)
|
||||
type TaskStatusResponse struct {
|
||||
APIResponse
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
}
|
||||
|
||||
// TaskDetailResponse 任务详情响应
|
||||
type TaskDetailResponse struct {
|
||||
APIResponse
|
||||
Task TaskInfo `json:"task"`
|
||||
}
|
||||
|
||||
// TaskListResponse 任务列表响应
|
||||
type TaskListResponse struct {
|
||||
APIResponse
|
||||
Tasks []TaskInfo `json:"tasks"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
// TaskInfo 任务详情信息
|
||||
type TaskInfo struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
Message string `json:"message,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
CompletedAt string `json:"completed_at,omitempty"`
|
||||
ETA string `json:"eta,omitempty"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Error *TaskError `json:"error,omitempty"`
|
||||
ExternalTaskID string `json:"external_task_id,omitempty"`
|
||||
}
|
||||
|
||||
// TaskError 任务错误
|
||||
type TaskError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// TaskRequest 任务请求
|
||||
type TaskRequest struct {
|
||||
TaskType string `json:"task_type"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
ExternalTaskID string `json:"external_task_id,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
CallbackURL string `json:"callback_url,omitempty"`
|
||||
CallbackToken string `json:"callback_token,omitempty"`
|
||||
}
|
||||
|
||||
// FileParam 上传文件参数
|
||||
type FileParam struct {
|
||||
FieldName string
|
||||
Filename string
|
||||
Data []byte
|
||||
MimeType string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 能力查询
|
||||
// =============================================================================
|
||||
|
||||
// CapabilitiesResponse 能力查询响应
|
||||
type CapabilitiesResponse struct {
|
||||
APIResponse
|
||||
Capabilities CapabilitiesInfo `json:"capabilities"`
|
||||
}
|
||||
|
||||
// CapabilitiesInfo Agent 能力信息
|
||||
type CapabilitiesInfo struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
SystemID string `json:"system_id"`
|
||||
ProtocolVersion string `json:"protocol_version"`
|
||||
SupportedTaskTypes []SupportedTaskType `json:"supported_task_types"`
|
||||
SupportedTransportModes []string `json:"supported_transport_modes"`
|
||||
MaxFileSize int64 `json:"max_file_size"`
|
||||
MaxConcurrentTasks int `json:"max_concurrent_tasks"`
|
||||
SupportProgressUpdates bool `json:"support_progress_updates"`
|
||||
SupportStreaming bool `json:"support_streaming"`
|
||||
SupportHiL bool `json:"support_hil"`
|
||||
SupportGroups bool `json:"support_groups"`
|
||||
}
|
||||
|
||||
// SupportedTaskType 支持的任务类型
|
||||
type SupportedTaskType struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
EstimatedTime string `json:"estimated_time"`
|
||||
InputSchema string `json:"input_schema,omitempty"`
|
||||
OutputSchema string `json:"output_schema,omitempty"`
|
||||
}
|
||||
|
||||
// GetCapabilities 获取 Agent 能力清单
|
||||
func (c *Client) GetCapabilities(ctx context.Context) (*CapabilitiesResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/synapse/capabilities", c.baseURL)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
c.setAuthHeaders(httpReq)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result CapabilitiesResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return &result, fmt.Errorf("API error: %s", result.Error)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任务提交
|
||||
// =============================================================================
|
||||
|
||||
// SubmitTask 提交任务(JSON 方式)
|
||||
func (c *Client) SubmitTask(ctx context.Context, req *TaskRequest) (*TaskResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/synapse/task", c.baseURL)
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
c.setAuthHeaders(httpReq)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result TaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return &result, fmt.Errorf("API error: %s", result.Error)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SubmitTaskWithFiles 提交带文件的任务(multipart/form-data 方式)
|
||||
func (c *Client) SubmitTaskWithFiles(ctx context.Context, req *TaskRequest, files []FileParam) (*TaskResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/synapse/task/multipart", c.baseURL)
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
_ = writer.WriteField("task_type", req.TaskType)
|
||||
|
||||
payloadJSON, _ := json.Marshal(req.Payload)
|
||||
_ = writer.WriteField("payload", string(payloadJSON))
|
||||
|
||||
if req.ExternalTaskID != "" {
|
||||
_ = writer.WriteField("external_task_id", req.ExternalTaskID)
|
||||
}
|
||||
if req.Priority != "" {
|
||||
_ = writer.WriteField("priority", req.Priority)
|
||||
}
|
||||
if req.CallbackURL != "" {
|
||||
_ = writer.WriteField("callback_url", req.CallbackURL)
|
||||
}
|
||||
if req.CallbackToken != "" {
|
||||
_ = writer.WriteField("callback_token", req.CallbackToken)
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
var part io.Writer
|
||||
var err error
|
||||
if f.MimeType != "" {
|
||||
part, err = writer.CreatePart(map[string][]string{
|
||||
"Content-Disposition": {fmt.Sprintf(`form-data; name="%s"; filename="%s"`, f.FieldName, f.Filename)},
|
||||
"Content-Type": {f.MimeType},
|
||||
})
|
||||
} else {
|
||||
part, err = writer.CreateFormFile(f.FieldName, f.Filename)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create form file: %w", err)
|
||||
}
|
||||
_, _ = part.Write(f.Data)
|
||||
}
|
||||
|
||||
_ = writer.Close()
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
c.setAuthHeaders(httpReq)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result TaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return &result, fmt.Errorf("API error: %s", result.Error)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任务查询
|
||||
// =============================================================================
|
||||
|
||||
// GetTask 获取任务详情
|
||||
func (c *Client) GetTask(ctx context.Context, taskID string) (*TaskDetailResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/synapse/task/%s", c.baseURL, taskID)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
c.setAuthHeaders(httpReq)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result TaskDetailResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return &result, fmt.Errorf("API error: %s", result.Error)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetTaskStatus 获取任务状态(简化版)
|
||||
func (c *Client) GetTaskStatus(ctx context.Context, taskID string) (*TaskStatusResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/synapse/task/%s/status", c.baseURL, taskID)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
c.setAuthHeaders(httpReq)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result TaskStatusResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return &result, fmt.Errorf("API error: %s", result.Error)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ListTasks 获取任务列表
|
||||
func (c *Client) ListTasks(ctx context.Context, status string, limit, offset int) (*TaskListResponse, error) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
url := fmt.Sprintf("%s/api/v1/synapse/tasks?status=%s&limit=%d&offset=%d",
|
||||
c.baseURL, status, limit, offset)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
c.setAuthHeaders(httpReq)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result TaskListResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return &result, fmt.Errorf("API error: %s", result.Error)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任务取消
|
||||
// =============================================================================
|
||||
|
||||
// CancelTask 取消任务
|
||||
func (c *Client) CancelTask(ctx context.Context, taskID string) (*TaskResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/synapse/task/%s/cancel", c.baseURL, taskID)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
c.setAuthHeaders(httpReq)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result TaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return &result, fmt.Errorf("API error: %s", result.Error)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 等待任务完成
|
||||
// =============================================================================
|
||||
|
||||
// WaitForTask 等待任务完成(轮询方式)
|
||||
func (c *Client) WaitForTask(ctx context.Context, taskID string, interval time.Duration) (*TaskInfo, error) {
|
||||
if interval == 0 {
|
||||
interval = 5 * time.Second
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
resp, err := c.GetTask(ctx, taskID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch resp.Task.Status {
|
||||
case "completed", "failed", "cancelled", "rejected":
|
||||
return &resp.Task, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 回调验证
|
||||
// =============================================================================
|
||||
|
||||
// CallbackRequest 回调请求结构
|
||||
type CallbackRequest struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Error *TaskError `json:"error,omitempty"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// VerifyCallback 验证回调请求签名
|
||||
func (c *Client) VerifyCallback(r *http.Request, callbackToken string) bool {
|
||||
if callbackToken == "" {
|
||||
return true
|
||||
}
|
||||
token := r.Header.Get("X-Callback-Token")
|
||||
return token == callbackToken
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 高阶 API: 快速创建任务并等待
|
||||
// =============================================================================
|
||||
|
||||
// CreateAndWait 创建任务并等待完成
|
||||
func (c *Client) CreateAndWait(ctx context.Context, taskType string, payload map[string]any, interval time.Duration) (*TaskInfo, error) {
|
||||
req := &TaskRequest{
|
||||
TaskType: taskType,
|
||||
Payload: payload,
|
||||
Priority: "normal",
|
||||
}
|
||||
|
||||
resp, err := c.SubmitTask(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
return nil, fmt.Errorf("task submission failed: %s", resp.Message)
|
||||
}
|
||||
|
||||
return c.WaitForTask(ctx, resp.TaskID, interval)
|
||||
}
|
||||
|
||||
// CreateWithCallback 创建任务并指定回调
|
||||
func (c *Client) CreateWithCallback(ctx context.Context, taskType string, payload map[string]any, callbackURL, callbackToken string) (*TaskResponse, error) {
|
||||
req := &TaskRequest{
|
||||
TaskType: taskType,
|
||||
Payload: payload,
|
||||
Priority: "normal",
|
||||
CallbackURL: callbackURL,
|
||||
CallbackToken: callbackToken,
|
||||
}
|
||||
|
||||
return c.SubmitTask(ctx, req)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 辅助方法
|
||||
// =============================================================================
|
||||
|
||||
func (c *Client) setAuthHeaders(req *http.Request) {
|
||||
if c.apiKey != "" {
|
||||
req.Header.Set("X-API-Key", c.apiKey)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
10
sdk/doc.go
Normal file
10
sdk/doc.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// Package sdk 灵隙(Synapse)协议 SDK
|
||||
//
|
||||
// 灵隙协议族(Synapse Protocol Suite)Go 语言 SDK,包含三个子包:
|
||||
//
|
||||
// - sdk/caller: P2A 协议 - 非 Agent 调用方通过 REST API 调用 Agent 能力
|
||||
// - sdk/p2p: A2A 协议 - Agent 之间的点对点任务协作通信
|
||||
// - sdk/group: A2N 协议 - 多 Agent 群组协作(群组管理、任务拆分合并、投票)
|
||||
//
|
||||
// 协议版本:1.0.0
|
||||
package sdk
|
||||
346
sdk/group/client.go
Normal file
346
sdk/group/client.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Synapse Group SDK (A2N Protocol)
|
||||
// 多 Agent 群组协作客户端 SDK
|
||||
// 基于 A2N 协议:多个 Agent 组成群组进行协同工作
|
||||
// =============================================================================
|
||||
|
||||
// GroupEventHandler 群组事件处理器接口
|
||||
type GroupEventHandler interface {
|
||||
OnGroupCreated(ctx context.Context, groupID string) error
|
||||
OnGroupDisbanded(ctx context.Context, groupID string) error
|
||||
OnMemberJoined(ctx context.Context, groupID string, member types.GroupMember) error
|
||||
OnMemberLeft(ctx context.Context, groupID, agentID string) error
|
||||
OnTaskSplit(ctx context.Context, groupID, originalTaskID string, subtasks []types.Subtask) error
|
||||
OnSubtaskProgress(ctx context.Context, groupID, subtaskID string, progress int) error
|
||||
OnTaskMerged(ctx context.Context, groupID, originalTaskID string, result any) error
|
||||
OnVoteInitiated(ctx context.Context, groupID string, vote types.GroupVotePayload) error
|
||||
OnVoteResult(ctx context.Context, groupID, voteID, option string, passed bool) error
|
||||
OnEscalation(ctx context.Context, groupID, escalationID string, level int) error
|
||||
OnBroadcast(ctx context.Context, groupID string, sender types.AgentEndpoint, message []byte) error
|
||||
}
|
||||
|
||||
// Client A2N 群组协作客户端
|
||||
type Client struct {
|
||||
p2pClient *p2pClient
|
||||
groups map[string]*GroupState
|
||||
groupsMu sync.RWMutex
|
||||
eventHandler GroupEventHandler
|
||||
}
|
||||
|
||||
// p2pClient 是 P2P 客户端的简化接口
|
||||
type p2pClient interface {
|
||||
SendMessage(ctx context.Context, target types.AgentEndpoint, msg *types.UnifiedSynapseMessage) error
|
||||
AgentEndpoint() types.AgentEndpoint
|
||||
}
|
||||
|
||||
// ClientConfig A2N 客户端配置
|
||||
type ClientConfig struct {
|
||||
P2PClient *p2pClient
|
||||
EventHandler GroupEventHandler
|
||||
}
|
||||
|
||||
// GroupState 群组状态
|
||||
type GroupState struct {
|
||||
GroupID string
|
||||
Name string
|
||||
Topology types.GroupTopology
|
||||
Status types.GroupStatus
|
||||
Members []types.GroupMember
|
||||
Role string
|
||||
}
|
||||
|
||||
// NewClient 创建新的 A2N 群组客户端
|
||||
func NewClient(p2pClient interface{}, eventHandler GroupEventHandler) (*Client, error) {
|
||||
return &Client{
|
||||
groups: make(map[string]*GroupState),
|
||||
eventHandler: eventHandler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetEventHandler 设置群组事件处理器
|
||||
func (c *Client) SetEventHandler(handler GroupEventHandler) {
|
||||
c.eventHandler = handler
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 群组管理
|
||||
// =============================================================================
|
||||
|
||||
// CreateGroup 创建群组
|
||||
func (c *Client) CreateGroup(ctx context.Context, name string, topology types.GroupTopology, config *types.GroupConfig, initialMembers []types.AgentEndpoint) (*GroupState, error) {
|
||||
groupID := fmt.Sprintf("group-%d", time.Now().UnixNano())
|
||||
|
||||
if config == nil {
|
||||
config = &types.GroupConfig{
|
||||
AllowSelfLeave: true,
|
||||
RequiresApproval: false,
|
||||
ConsensusRequired: false,
|
||||
ConsensusThreshold: 0.5,
|
||||
BroadcastMode: "all",
|
||||
}
|
||||
}
|
||||
|
||||
members := make([]types.GroupMember, 0, len(initialMembers)+1)
|
||||
for _, m := range initialMembers {
|
||||
members = append(members, types.GroupMember{
|
||||
AgentID: m.AgentID,
|
||||
AgentName: m.AgentName,
|
||||
Role: "worker",
|
||||
JoinedAt: types.NowTimestamp(),
|
||||
Status: types.GroupMemberActive,
|
||||
Capabilities: []string{},
|
||||
LoadFactor: 0,
|
||||
})
|
||||
}
|
||||
|
||||
groupState := &GroupState{
|
||||
GroupID: groupID,
|
||||
Name: name,
|
||||
Topology: topology,
|
||||
Status: types.GroupStatusActive,
|
||||
Members: members,
|
||||
Role: "coordinator",
|
||||
}
|
||||
|
||||
c.groupsMu.Lock()
|
||||
c.groups[groupID] = groupState
|
||||
c.groupsMu.Unlock()
|
||||
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnGroupCreated(ctx, groupID)
|
||||
}
|
||||
|
||||
return groupState, nil
|
||||
}
|
||||
|
||||
// JoinGroup 加入群组
|
||||
func (c *Client) JoinGroup(ctx context.Context, groupID string) error {
|
||||
c.groupsMu.Lock()
|
||||
defer c.groupsMu.Unlock()
|
||||
|
||||
if _, ok := c.groups[groupID]; !ok {
|
||||
c.groups[groupID] = &GroupState{
|
||||
GroupID: groupID,
|
||||
Status: types.GroupStatusActive,
|
||||
Role: "worker",
|
||||
}
|
||||
}
|
||||
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnMemberJoined(ctx, groupID, types.GroupMember{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LeaveGroup 离开群组
|
||||
func (c *Client) LeaveGroup(ctx context.Context, groupID string) error {
|
||||
c.groupsMu.Lock()
|
||||
delete(c.groups, groupID)
|
||||
c.groupsMu.Unlock()
|
||||
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnMemberLeft(ctx, groupID, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisbandGroup 解散群组
|
||||
func (c *Client) DisbandGroup(ctx context.Context, groupID string) error {
|
||||
c.groupsMu.Lock()
|
||||
if group, ok := c.groups[groupID]; ok {
|
||||
group.Status = types.GroupStatusDisbanded
|
||||
}
|
||||
c.groupsMu.Unlock()
|
||||
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnGroupDisbanded(ctx, groupID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGroup 获取群组信息
|
||||
func (c *Client) GetGroup(groupID string) (*GroupState, bool) {
|
||||
c.groupsMu.RLock()
|
||||
defer c.groupsMu.RUnlock()
|
||||
group, ok := c.groups[groupID]
|
||||
return group, ok
|
||||
}
|
||||
|
||||
// ListGroups 列出所有群组
|
||||
func (c *Client) ListGroups() []*GroupState {
|
||||
c.groupsMu.RLock()
|
||||
defer c.groupsMu.RUnlock()
|
||||
result := make([]*GroupState, 0, len(c.groups))
|
||||
for _, g := range c.groups {
|
||||
result = append(result, g)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任务协作
|
||||
// =============================================================================
|
||||
|
||||
// SubmitGroupTask 提交群组任务(作为 coordinator 拆分任务)
|
||||
func (c *Client) SubmitGroupTask(ctx context.Context, groupID, taskType string, requirements map[string]any, mergeStrategy types.MergeStrategy) ([]types.Subtask, error) {
|
||||
subtasks := []types.Subtask{
|
||||
{
|
||||
ID: fmt.Sprintf("sub-%d-1", time.Now().UnixNano()),
|
||||
Description: "子任务 1",
|
||||
AssignedTo: "",
|
||||
Dependencies: []string{},
|
||||
EstimatedDuration: 60,
|
||||
Priority: 1,
|
||||
},
|
||||
}
|
||||
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnTaskSplit(ctx, groupID, "", subtasks)
|
||||
}
|
||||
|
||||
return subtasks, nil
|
||||
}
|
||||
|
||||
// SendSubtaskResult 发送子任务结果
|
||||
func (c *Client) SendSubtaskResult(ctx context.Context, groupID, subtaskID string, result any, err error) error {
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnTaskMerged(ctx, groupID, "", result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 投票与共识
|
||||
// =============================================================================
|
||||
|
||||
// InitiateVote 发起投票
|
||||
func (c *Client) InitiateVote(ctx context.Context, groupID, question string, options []string, voterIDs []string, threshold float64, deadline time.Duration) (string, error) {
|
||||
voteID := fmt.Sprintf("vote-%d", time.Now().UnixNano())
|
||||
|
||||
vote := types.GroupVotePayload{
|
||||
VoteID: voteID,
|
||||
Question: question,
|
||||
Options: options,
|
||||
Voters: voterIDs,
|
||||
Deadline: types.Timestamp(time.Now().Add(deadline).UTC().Format(time.RFC3339)),
|
||||
Threshold: threshold,
|
||||
Status: "open",
|
||||
}
|
||||
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnVoteInitiated(ctx, groupID, vote)
|
||||
}
|
||||
|
||||
return voteID, nil
|
||||
}
|
||||
|
||||
// SubmitVote 提交投票
|
||||
func (c *Client) SubmitVote(ctx context.Context, groupID, voteID, option, reason string) error {
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnVoteResult(ctx, groupID, voteID, option, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 广播与升级
|
||||
// =============================================================================
|
||||
|
||||
// Broadcast 广播消息
|
||||
func (c *Client) Broadcast(ctx context.Context, groupID string, message []byte) error {
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnBroadcast(ctx, groupID, types.AgentEndpoint{}, message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EscalateToHuman 升级到人类决策
|
||||
func (c *Client) EscalateToHuman(ctx context.Context, groupID string, level int, reason string) (string, error) {
|
||||
escalationID := fmt.Sprintf("esc-%d", time.Now().UnixNano())
|
||||
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnEscalation(ctx, groupID, escalationID, level)
|
||||
}
|
||||
|
||||
return escalationID, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 消息处理
|
||||
// =============================================================================
|
||||
|
||||
// HandleMessage 处理群组消息
|
||||
func (c *Client) HandleMessage(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
groupID := msg.GroupID
|
||||
|
||||
switch types.GroupMessageType(msg.Type) {
|
||||
case types.GroupCreate:
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnGroupCreated(ctx, groupID)
|
||||
}
|
||||
|
||||
case types.GroupDisband:
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnGroupDisbanded(ctx, groupID)
|
||||
}
|
||||
|
||||
case types.GroupMemberUpdate:
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnMemberJoined(ctx, groupID, types.GroupMember{})
|
||||
}
|
||||
|
||||
case types.GroupTaskSplit:
|
||||
var payload types.GroupTaskSplitPayload
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnTaskSplit(ctx, groupID, payload.OriginalTask.ID, payload.Subtasks)
|
||||
}
|
||||
|
||||
case types.GroupTaskProgress:
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnSubtaskProgress(ctx, groupID, "", 0)
|
||||
}
|
||||
|
||||
case types.GroupTaskMerge:
|
||||
var payload types.GroupTaskMergePayload
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnTaskMerged(ctx, groupID, payload.OriginalTaskID, payload.MergedResult)
|
||||
}
|
||||
|
||||
case types.GroupVote:
|
||||
var payload types.GroupVotePayload
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnVoteInitiated(ctx, groupID, payload)
|
||||
}
|
||||
|
||||
case types.GroupConsensus:
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnVoteResult(ctx, groupID, "", "", true)
|
||||
}
|
||||
|
||||
case types.GroupConsensusEscalate, types.GroupSubtaskEscalate:
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnEscalation(ctx, groupID, "", 1)
|
||||
}
|
||||
|
||||
case types.GroupBroadcast:
|
||||
if c.eventHandler != nil {
|
||||
_ = c.eventHandler.OnBroadcast(ctx, groupID, msg.Sender, msg.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
459
sdk/p2p/client.go
Normal file
459
sdk/p2p/client.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Synapse P2P SDK (A2A Protocol)
|
||||
// Agent 间点对点通信客户端 SDK
|
||||
// 基于 A2A 协议:两个 Agent 之间进行点对点任务协作
|
||||
// =============================================================================
|
||||
|
||||
// MessageCallbackHandler 消息回调处理器接口
|
||||
type MessageCallbackHandler interface {
|
||||
HandleTaskRequest(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error)
|
||||
HandleTaskStatusUpdate(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleTaskComplete(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleTaskFailed(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleNeedHuman(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
}
|
||||
|
||||
// Client A2A 点对点通信客户端
|
||||
type Client struct {
|
||||
agentEndpoint types.AgentEndpoint
|
||||
authSecret string
|
||||
callbackHandler MessageCallbackHandler
|
||||
tasks map[string]*P2PTaskState
|
||||
tasksMu sync.RWMutex
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// ClientConfig A2A 客户端配置
|
||||
type ClientConfig struct {
|
||||
AgentEndpoint types.AgentEndpoint
|
||||
AuthSecret string
|
||||
CallbackPort int
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// P2PTaskState P2P 任务状态
|
||||
type P2PTaskState struct {
|
||||
TaskID string
|
||||
Status types.P2PTaskStatus
|
||||
Progress int
|
||||
Message string
|
||||
Result any
|
||||
Error *types.SynapseError
|
||||
}
|
||||
|
||||
// NewClient 创建新的 A2A 客户端
|
||||
func NewClient(config *ClientConfig) (*Client, error) {
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return &Client{
|
||||
agentEndpoint: config.AgentEndpoint,
|
||||
authSecret: config.AuthSecret,
|
||||
tasks: make(map[string]*P2PTaskState),
|
||||
httpClient: &http.Client{Timeout: config.Timeout},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetMessageHandler 设置消息回调处理器
|
||||
func (c *Client) SetMessageHandler(handler MessageCallbackHandler) {
|
||||
c.callbackHandler = handler
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 任务状态管理
|
||||
// =============================================================================
|
||||
|
||||
// GetTaskState 获取任务状态
|
||||
func (c *Client) GetTaskState(taskID string) (*P2PTaskState, bool) {
|
||||
c.tasksMu.RLock()
|
||||
defer c.tasksMu.RUnlock()
|
||||
state, ok := c.tasks[taskID]
|
||||
return state, ok
|
||||
}
|
||||
|
||||
// ListTasks 列出所有任务
|
||||
func (c *Client) ListTasks() map[string]*P2PTaskState {
|
||||
c.tasksMu.RLock()
|
||||
defer c.tasksMu.RUnlock()
|
||||
result := make(map[string]*P2PTaskState, len(c.tasks))
|
||||
for k, v := range c.tasks {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *Client) setTaskState(taskID string, status types.P2PTaskStatus, progress int, message string) {
|
||||
c.tasksMu.Lock()
|
||||
defer c.tasksMu.Unlock()
|
||||
if _, ok := c.tasks[taskID]; !ok {
|
||||
c.tasks[taskID] = &P2PTaskState{TaskID: taskID}
|
||||
}
|
||||
c.tasks[taskID].Status = status
|
||||
c.tasks[taskID].Progress = progress
|
||||
c.tasks[taskID].Message = message
|
||||
}
|
||||
|
||||
func (c *Client) setTaskResult(taskID string, result any) {
|
||||
c.tasksMu.Lock()
|
||||
defer c.tasksMu.Unlock()
|
||||
if state, ok := c.tasks[taskID]; ok {
|
||||
state.Result = result
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) setTaskError(taskID string, err *types.SynapseError) {
|
||||
c.tasksMu.Lock()
|
||||
defer c.tasksMu.Unlock()
|
||||
if state, ok := c.tasks[taskID]; ok {
|
||||
state.Error = err
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 消息发送
|
||||
// =============================================================================
|
||||
|
||||
// SendTaskRequest 发送任务请求
|
||||
func (c *Client) SendTaskRequest(ctx context.Context, target types.AgentEndpoint, taskType string, payload map[string]any) (string, error) {
|
||||
taskID := types.GenerateTaskID()
|
||||
|
||||
taskReq := types.TaskRequestPayload{}
|
||||
taskReq.Task.ID = taskID
|
||||
taskReq.Task.Type = taskType
|
||||
taskReq.Task.Priority = types.PriorityNormal
|
||||
taskReq.Task.Payload = payload
|
||||
|
||||
payloadBytes, _ := json.Marshal(taskReq)
|
||||
|
||||
msg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskRequest),
|
||||
Sender: c.agentEndpoint,
|
||||
Receiver: &target,
|
||||
Payload: payloadBytes,
|
||||
Priority: types.PriorityNormal,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
c.setTaskState(taskID, types.P2PStatusPending, 0, "任务已发送")
|
||||
|
||||
err := c.sendMessage(ctx, target.Endpoint, msg)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to send task request: %w", err)
|
||||
}
|
||||
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
// SendTaskCancel 发送任务取消
|
||||
func (c *Client) SendTaskCancel(ctx context.Context, target types.AgentEndpoint, taskID, reason string) error {
|
||||
cancelPayload := types.TaskCancelPayload{
|
||||
TaskID: taskID,
|
||||
Reason: reason,
|
||||
CancelledBy: c.agentEndpoint.AgentID,
|
||||
}
|
||||
|
||||
payloadBytes, _ := json.Marshal(cancelPayload)
|
||||
|
||||
msg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PTaskCancel),
|
||||
Sender: c.agentEndpoint,
|
||||
Receiver: &target,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
c.setTaskState(taskID, types.P2PStatusCancelled, 0, "任务已取消")
|
||||
|
||||
return c.sendMessage(ctx, target.Endpoint, msg)
|
||||
}
|
||||
|
||||
// SendHumanDecision 发送人类决策结果
|
||||
func (c *Client) SendHumanDecision(ctx context.Context, target types.AgentEndpoint, taskID string, decision types.HumanDecisionPayload) error {
|
||||
payloadBytes, _ := json.Marshal(decision)
|
||||
|
||||
msg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PHumanDecision),
|
||||
Sender: c.agentEndpoint,
|
||||
Receiver: &target,
|
||||
Payload: payloadBytes,
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
return c.sendMessage(ctx, target.Endpoint, msg)
|
||||
}
|
||||
|
||||
// SendCapabilityQuery 查询对方能力
|
||||
func (c *Client) SendCapabilityQuery(ctx context.Context, target types.AgentEndpoint) ([]string, error) {
|
||||
msg := &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: "1.0.0",
|
||||
Type: string(types.P2PCapabilityQuery),
|
||||
Sender: c.agentEndpoint,
|
||||
Receiver: &target,
|
||||
Payload: []byte("{}"),
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
|
||||
respMsg, err := c.sendRequest(ctx, target.Endpoint, msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query capabilities: %w", err)
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Capabilities []string `json:"capabilities"`
|
||||
}
|
||||
if err := json.Unmarshal(respMsg.Payload, &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse capabilities: %w", err)
|
||||
}
|
||||
|
||||
return resp.Capabilities, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 消息签名与验证
|
||||
// =============================================================================
|
||||
|
||||
func (c *Client) signMessage(msg *types.UnifiedSynapseMessage) string {
|
||||
if c.authSecret == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
h := hmac.New(sha256.New, []byte(c.authSecret))
|
||||
h.Write([]byte(msg.ID + "." + string(msg.Payload)))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// VerifySignature 验证消息签名
|
||||
func (c *Client) VerifySignature(msg *types.UnifiedSynapseMessage, secret string) bool {
|
||||
if secret == "" {
|
||||
return true
|
||||
}
|
||||
if msg.Signature == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(msg.ID + "." + string(msg.Payload)))
|
||||
expected := hex.EncodeToString(h.Sum(nil))
|
||||
return hmac.Equal([]byte(msg.Signature), []byte(expected))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 消息发送底层
|
||||
// =============================================================================
|
||||
|
||||
func (c *Client) sendMessage(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) error {
|
||||
msg.Signature = c.signMessage(msg)
|
||||
|
||||
body, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("X-Synapse-Signature", msg.Signature)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("remote error: %s", string(respBody))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) sendRequest(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
msg.Signature = c.signMessage(msg)
|
||||
|
||||
body, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("X-Synapse-Signature", msg.Signature)
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("remote error: %s", string(respBody))
|
||||
}
|
||||
|
||||
var respMsg types.UnifiedSynapseMessage
|
||||
if err := json.Unmarshal(respBody, &respMsg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return &respMsg, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 等待任务完成
|
||||
// =============================================================================
|
||||
|
||||
// WaitForTask 等待任务完成(轮询方式,通过回调更新状态)
|
||||
func (c *Client) WaitForTask(ctx context.Context, taskID string, timeout time.Duration) (*P2PTaskState, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
if time.Now().After(deadline) {
|
||||
return nil, fmt.Errorf("task timeout")
|
||||
}
|
||||
|
||||
state, ok := c.GetTaskState(taskID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch state.Status {
|
||||
case types.P2PStatusCompleted, types.P2PStatusFailed, types.P2PStatusCancelled, types.P2PStatusRejected:
|
||||
return state, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 回调处理
|
||||
// =============================================================================
|
||||
|
||||
// HandleMessage 处理收到的消息(供 HTTP 服务调用)
|
||||
func (c *Client) HandleMessage(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
||||
if c.authSecret != "" && !c.VerifySignature(msg, c.authSecret) {
|
||||
return nil, fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
switch types.P2PMessageType(msg.Type) {
|
||||
case types.P2PTaskRequest:
|
||||
if c.callbackHandler != nil {
|
||||
return c.callbackHandler.HandleTaskRequest(ctx, msg)
|
||||
}
|
||||
return c.ackMessage(msg), nil
|
||||
|
||||
case types.P2PTaskConfirm:
|
||||
var payload types.TaskConfirmPayload
|
||||
_ = json.Unmarshal(msg.Payload, &payload)
|
||||
c.setTaskState(payload.TaskID, types.P2PStatusConfirmed, 0, "任务已确认")
|
||||
return c.ackMessage(msg), nil
|
||||
|
||||
case types.P2PTaskReject:
|
||||
var payload types.TaskRejectPayload
|
||||
_ = json.Unmarshal(msg.Payload, &payload)
|
||||
c.setTaskState(payload.TaskID, types.P2PStatusRejected, 0, payload.Reason.Message)
|
||||
return c.ackMessage(msg), nil
|
||||
|
||||
case types.P2PTaskStatusUpdate:
|
||||
var payload types.TaskStatusUpdatePayload
|
||||
_ = json.Unmarshal(msg.Payload, &payload)
|
||||
c.setTaskState(payload.TaskID, types.P2PTaskStatus(payload.Status), payload.Progress, payload.Message)
|
||||
if c.callbackHandler != nil {
|
||||
_ = c.callbackHandler.HandleTaskStatusUpdate(ctx, msg)
|
||||
}
|
||||
return c.ackMessage(msg), nil
|
||||
|
||||
case types.P2PTaskComplete:
|
||||
var payload types.TaskCompletePayload
|
||||
_ = json.Unmarshal(msg.Payload, &payload)
|
||||
c.setTaskState(payload.TaskID, types.P2PStatusCompleted, 100, payload.Summary)
|
||||
c.setTaskResult(payload.TaskID, payload.Result.Output)
|
||||
if c.callbackHandler != nil {
|
||||
_ = c.callbackHandler.HandleTaskComplete(ctx, msg)
|
||||
}
|
||||
return c.ackMessage(msg), nil
|
||||
|
||||
case types.P2PTaskFailed:
|
||||
var payload types.TaskFailedPayload
|
||||
_ = json.Unmarshal(msg.Payload, &payload)
|
||||
c.setTaskState(payload.TaskID, types.P2PStatusFailed, 0, payload.Error.Message)
|
||||
c.setTaskError(payload.TaskID, types.NewSynapseError(
|
||||
types.SynapseErrorCode(0),
|
||||
payload.Error.Message,
|
||||
nil,
|
||||
))
|
||||
if c.callbackHandler != nil {
|
||||
_ = c.callbackHandler.HandleTaskFailed(ctx, msg)
|
||||
}
|
||||
return c.ackMessage(msg), nil
|
||||
|
||||
case types.P2PTaskNeedHuman:
|
||||
var payload types.TaskNeedHumanPayload
|
||||
_ = json.Unmarshal(msg.Payload, &payload)
|
||||
c.setTaskState(payload.TaskID, types.P2PStatusNeedHuman, 0, payload.Reason.Message)
|
||||
if c.callbackHandler != nil {
|
||||
_ = c.callbackHandler.HandleNeedHuman(ctx, msg)
|
||||
}
|
||||
return c.ackMessage(msg), nil
|
||||
|
||||
case types.P2PCapabilityQuery:
|
||||
return nil, nil
|
||||
|
||||
default:
|
||||
return c.ackMessage(msg), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ackMessage(msg *types.UnifiedSynapseMessage) *types.UnifiedSynapseMessage {
|
||||
return &types.UnifiedSynapseMessage{
|
||||
ID: types.GenerateMessageID(),
|
||||
Version: msg.Version,
|
||||
Type: string(types.P2PAck),
|
||||
Sender: c.agentEndpoint,
|
||||
Receiver: &msg.Sender,
|
||||
ReplyTo: msg.ID,
|
||||
Payload: []byte("{}"),
|
||||
Timestamp: types.NowTimestamp(),
|
||||
}
|
||||
}
|
||||
328
synapse/docs/01-synapse-raw-protocol.md
Normal file
328
synapse/docs/01-synapse-raw-protocol.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# 灵隙原始协议(Synapse Raw Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:纲领性文档
|
||||
|
||||
---
|
||||
|
||||
## 1. 协议愿景
|
||||
|
||||
灵隙(Synapse,原 Agent-to-Agent)协议旨在定义一套通用的、可扩展的智能体间通信标准,使不同系统、不同厂商、不同能力的智能体能够在统一的协议框架下进行协作、交易与能力组合。
|
||||
|
||||
本协议是灵隙协议族(Synapse Protocol Suite)的原始纲领,定义核心设计理念、架构原则与术语体系,为所有衍生协议提供顶层指导。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心设计理念
|
||||
|
||||
### 2.1 去中心化对等(Peer-to-Peer)
|
||||
- 所有 Agent 在协议层面地位平等,无中心化控制节点
|
||||
- 每个 Agent 既是服务提供者,也是服务消费者
|
||||
- Agent 间直接通信,不依赖中间 broker(传输层可选择不同实现)
|
||||
|
||||
### 2.2 能力驱动(Capability-Driven)
|
||||
- 通信以能力为核心,而非以身份为核心
|
||||
- Agent 声明自身能力,按需发现与调用
|
||||
- 能力可组合、可分解、可升级
|
||||
|
||||
### 2.3 任务导向(Task-Oriented)
|
||||
- 所有交互围绕"任务"展开
|
||||
- 任务有明确的生命周期、状态机与超时机制
|
||||
- 任务支持拆分、合并、委派与协作
|
||||
|
||||
### 2.4 人类可介入(Human-in-the-Loop)
|
||||
- 协议原生支持人类介入决策
|
||||
- Agent 可主动请求人类判断
|
||||
- 人类决策优先于 Agent 自动决策
|
||||
- 支持多级升级(escalation)机制
|
||||
|
||||
### 2.5 群组协作(Group Collaboration)
|
||||
- 支持多 Agent 组成临时或永久群组
|
||||
- 群组内有角色分工与权限控制
|
||||
- 支持任务拆分-执行-合并的 MapReduce 模式
|
||||
- 支持投票、共识等群体决策机制
|
||||
|
||||
### 2.6 传输无关(Transport Agnostic)
|
||||
- 协议语义与底层传输分离
|
||||
- 支持 HTTP/2、gRPC、WebSocket、消息队列等多种传输
|
||||
- 支持回调(Callback)、轮询(Polling)、同步(Sync)、混合(Hybrid)等多种交互模式
|
||||
|
||||
### 2.7 安全可信(Secure & Trustworthy)
|
||||
- 消息级签名与验证
|
||||
- Agent 身份认证与信任等级
|
||||
- 支持加密传输
|
||||
- 可审计、可追溯
|
||||
|
||||
---
|
||||
|
||||
## 3. 协议分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 应用层 (Application) │
|
||||
│ 业务逻辑、工作流、领域特定逻辑 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 协议层 (Protocol) │
|
||||
├─────────────┬───────────────────────────┤
|
||||
│ A2A 协议 │ A2N 组群协议 │ ← 本协议族
|
||||
│ (P2P) │ (Group) │
|
||||
├─────────────┴───────────────────────────┤
|
||||
│ 传输层 (Transport) │
|
||||
│ HTTP / gRPC / WebSocket / MQ ... │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 网络层 (Network) │
|
||||
│ TCP / UDP / QUIC ... │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.1 协议族构成
|
||||
|
||||
| 协议名称 | 缩写 | 描述 | 适用场景 |
|
||||
|---------|------|------|---------|
|
||||
| 灵隙原始协议(Synapse Raw Protocol) | A2A-Raw | 顶层纲领,定义理念、术语、原则 | 指导所有协议设计 |
|
||||
| A2A 协议 | A2A-Base | 点对点任务协作协议 | 两个 Agent 间的任务交互 |
|
||||
| A2N 组群协议 | A2N-Group | 多 Agent 群组协作协议 | 多个 Agent 协同完成任务 |
|
||||
| P2A 调用协议 | P2A-Caller | 非 Agent 调用者接入协议 | 普通服务/前端调用 Agent 能力 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心概念与术语
|
||||
|
||||
### 4.1 Agent(智能体)
|
||||
能够自主感知环境、做出决策并执行动作的计算实体。
|
||||
|
||||
**属性:**
|
||||
- `agent_id`:Agent 唯一标识
|
||||
- `system_id`:所属系统标识
|
||||
- `endpoint`:通信端点地址
|
||||
- `capabilities`:能力清单
|
||||
- `trust_level`:信任等级
|
||||
|
||||
### 4.2 Task(任务)
|
||||
Agent 间协作的基本单位,具有明确的输入、输出与生命周期。
|
||||
|
||||
**属性:**
|
||||
- `task_id`:任务唯一标识
|
||||
- `task_type`:任务类型
|
||||
- `payload`:任务载荷
|
||||
- `status`:任务状态
|
||||
- `priority`:优先级
|
||||
- `deadline`:截止时间
|
||||
|
||||
### 4.3 Message(消息)
|
||||
Agent 间通信的基本单位。
|
||||
|
||||
**属性:**
|
||||
- `message_id`:消息唯一标识
|
||||
- `type`:消息类型
|
||||
- `sender`:发送方
|
||||
- `receiver`:接收方(P2P)
|
||||
- `group_id`:群组标识(群组)
|
||||
- `payload`:消息载荷
|
||||
- `correlation_id`:关联标识(用于请求-响应关联)
|
||||
- `timestamp`:时间戳
|
||||
- `signature`:签名
|
||||
|
||||
### 4.4 Capability(能力)
|
||||
Agent 能够执行的任务类型的声明。
|
||||
|
||||
**属性:**
|
||||
- `type`:能力对应的任务类型
|
||||
- `name`:能力名称
|
||||
- `description`:能力描述
|
||||
- `input_schema`:输入参数 schema
|
||||
- `output_schema`:输出结果 schema
|
||||
- `estimated_time`:预估耗时
|
||||
|
||||
### 4.5 Group(群组)
|
||||
由多个 Agent 组成的协作单元。
|
||||
|
||||
**属性:**
|
||||
- `group_id`:群组唯一标识
|
||||
- `name`:群组名称
|
||||
- `topology`:拓扑结构
|
||||
- `members`:成员列表
|
||||
- `roles`:角色定义
|
||||
- `status`:群组状态
|
||||
|
||||
### 4.6 Human-in-the-Loop(人在回路)
|
||||
在 Agent 协作过程中,人类可介入进行决策的机制。
|
||||
|
||||
**关键概念:**
|
||||
- `escalation`:升级(将问题提交给人类)
|
||||
- `decision`:决策(人类做出的判断)
|
||||
- `resume`:恢复(人类决策后继续执行)
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计原则
|
||||
|
||||
### 5.1 简约性(Simplicity)
|
||||
- 核心机制保持简单
|
||||
- 复杂功能通过组合基础机制实现
|
||||
- 可选功能通过能力声明来协商
|
||||
|
||||
### 5.2 可扩展性(Extensibility)
|
||||
- 消息类型可扩展
|
||||
- 能力类型可扩展
|
||||
- 传输方式可扩展
|
||||
- 通过版本号与能力协商保证向后兼容
|
||||
|
||||
### 5.3 容错性(Fault Tolerance)
|
||||
- 网络不可靠是常态
|
||||
- 消息可能丢失、重复、延迟
|
||||
- Agent 可能故障、离线
|
||||
- 协议必须有重试、超时、降级机制
|
||||
|
||||
### 5.4 可观测性(Observability)
|
||||
- 所有交互可追踪
|
||||
- 任务状态可查询
|
||||
- 支持日志、指标、链路追踪
|
||||
|
||||
### 5.5 互操作性(Interoperability)
|
||||
- 不同语言、不同框架的 Agent 可互操作
|
||||
- 协议规范优先于具体实现
|
||||
- 提供标准测试用例验证兼容性
|
||||
|
||||
---
|
||||
|
||||
## 6. 消息模型总则
|
||||
|
||||
### 6.1 统一消息信封
|
||||
所有协议共享统一的消息信封结构,具体载荷由消息类型决定。
|
||||
|
||||
```
|
||||
UnifiedSynapseMessage
|
||||
├── id // 消息ID
|
||||
├── version // 协议版本
|
||||
├── type // 消息类型
|
||||
├── sender // 发送方 AgentEndpoint
|
||||
├── receiver? // 接收方(P2P 时)
|
||||
├── group_id? // 群组ID(群组时)
|
||||
├── correlation_id // 关联ID
|
||||
├── reply_to? // 回复的消息ID
|
||||
├── payload // 消息载荷(JSON)
|
||||
├── priority? // 优先级
|
||||
├── signature? // 签名
|
||||
├── timestamp // 发送时间
|
||||
└── expires_at? // 过期时间
|
||||
```
|
||||
|
||||
### 6.2 消息命名规范
|
||||
- 使用 `领域:动作` 格式
|
||||
- 全小写,用下划线分隔
|
||||
- 示例:`p2p:task_request`, `group:create`, `hil:decision`
|
||||
|
||||
### 6.3 请求-响应模式
|
||||
- 请求消息必须携带唯一 `id`
|
||||
- 响应消息通过 `reply_to` 关联请求
|
||||
- 支持 ACK/NACK 机制
|
||||
- 超时未收到响应应重试或报错
|
||||
|
||||
---
|
||||
|
||||
## 7. 任务生命周期总则
|
||||
|
||||
### 7.1 状态机
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ pending │ 待确认
|
||||
└────┬─────┘
|
||||
│ 确认/拒绝
|
||||
┌─────┴──────┐
|
||||
▼ ▼
|
||||
┌─────────┐ ┌──────────┐
|
||||
│confirmed│ │ rejected │ 已拒绝(终态)
|
||||
└────┬────┘ └──────────┘
|
||||
│ 开始执行
|
||||
▼
|
||||
┌──────────┐
|
||||
│executing │ 执行中
|
||||
└────┬─────┘
|
||||
│ 信息缺失
|
||||
┌────▼─────┐
|
||||
│info_needed│ 需要信息
|
||||
└────┬─────┘
|
||||
│ 信息补充
|
||||
▼
|
||||
回到 executing
|
||||
│
|
||||
├──────────────┐
|
||||
│ 需人类决策 │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌───────────┐
|
||||
│need_human│ │ 各种终态 │
|
||||
└────┬─────┘ └───────────┘
|
||||
│ 人类决策
|
||||
▼
|
||||
回到 executing
|
||||
│
|
||||
┌────┴────┬─────────┐
|
||||
▼ ▼ ▼
|
||||
completed failed cancelled
|
||||
(完成) (失败) (取消)
|
||||
```
|
||||
|
||||
### 7.2 终态
|
||||
- `completed`:成功完成
|
||||
- `failed`:执行失败
|
||||
- `cancelled`:被取消
|
||||
- `rejected`:被拒绝
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全模型总则
|
||||
|
||||
### 8.1 身份认证
|
||||
- 每个 Agent 有唯一身份
|
||||
- 支持 HMAC、JWT、OAuth2 等多种认证方式
|
||||
- 信任等级机制
|
||||
|
||||
### 8.2 消息完整性
|
||||
- 消息签名机制
|
||||
- 防止篡改与伪造
|
||||
|
||||
### 8.3 机密性
|
||||
- 支持端到端加密
|
||||
- 敏感载荷加密传输
|
||||
|
||||
### 8.4 访问控制
|
||||
- 基于信任等级的权限控制
|
||||
- 群组内基于角色的权限控制
|
||||
|
||||
---
|
||||
|
||||
## 9. 非目标
|
||||
|
||||
以下内容**不在**本原始协议的讨论范围内:
|
||||
|
||||
1. **具体传输协议的实现细节** - 由各传输层实现定义
|
||||
2. **特定领域的任务类型** - 由应用层或领域扩展定义
|
||||
3. **Agent 内部实现架构** - 仅定义对外接口与行为
|
||||
4. **具体编程语言绑定** - 语言无关,可多语言实现
|
||||
5. **UI/UX 设计** - 人机界面不在协议范围内
|
||||
|
||||
---
|
||||
|
||||
## 10. 参考实现
|
||||
|
||||
本协议族的参考实现位于:`dp-headless/synapse/`
|
||||
|
||||
| 模块 | 对应协议 | 文件路径 |
|
||||
|------|---------|---------|
|
||||
| types 包 | 公共类型定义 | `types/` |
|
||||
| p2p 包 | A2A 协议 | `p2p/` |
|
||||
| group 包 | A2N 组群协议 | `group/` |
|
||||
| externalapi 包 | P2A 调用协议 | `externalapi/` |
|
||||
| sdk 包 | P2A 客户端 SDK | `sdk/` |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:协议演进
|
||||
|
||||
本原始协议作为顶层指导,保持相对稳定。各具体协议可独立版本化演进,但需遵循本协议定义的核心原则。
|
||||
|
||||
**版本历史:**
|
||||
- v1.0.0:初始版本,定义核心概念与设计原则
|
||||
465
synapse/docs/02-a2a-protocol.md
Normal file
465
synapse/docs/02-a2a-protocol.md
Normal file
@@ -0,0 +1,465 @@
|
||||
# A2A 协议(Agent-to-Agent Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[灵隙原始协议(Synapse Raw Protocol) v1.0.0](./01-synapse-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
A2A 协议定义了两个 Agent 之间进行点对点(P2P)任务协作的完整规范,包括消息类型、交互流程、状态机、接口定义与错误处理。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 单个任务请求与响应
|
||||
- 任务状态实时同步
|
||||
- 任务取消
|
||||
- 信息补全交互
|
||||
- 人类介入决策
|
||||
|
||||
### 1.2 非适用场景
|
||||
- 多 Agent 群组协作(参见 A2N 组群协议)
|
||||
- 非 Agent 调用方接入(参见 P2A 调用协议)
|
||||
|
||||
---
|
||||
|
||||
## 2. 消息类型
|
||||
|
||||
### 2.1 消息类型清单
|
||||
|
||||
所有消息类型前缀:`p2p:`
|
||||
|
||||
| 消息类型 | 方向 | 说明 |
|
||||
|---------|------|------|
|
||||
| `p2p:task_request` | 请求方 → 执行方 | 提交任务请求 |
|
||||
| `p2p:task_confirm` | 执行方 → 请求方 | 确认接受任务 |
|
||||
| `p2p:task_reject` | 执行方 → 请求方 | 拒绝任务 |
|
||||
| `p2p:task_need_info` | 执行方 → 请求方 | 请求补充信息 |
|
||||
| `p2p:task_info_provided` | 请求方 → 执行方 | 提供补充信息 |
|
||||
| `p2p:task_status_update` | 执行方 → 请求方 | 任务进度更新 |
|
||||
| `p2p:task_complete` | 执行方 → 请求方 | 任务完成 |
|
||||
| `p2p:task_failed` | 执行方 → 请求方 | 任务失败 |
|
||||
| `p2p:task_cancel` | 请求方 → 执行方 | 取消任务 |
|
||||
| `p2p:task_ack` | 双向 | 任务级确认 |
|
||||
| `p2p:ack` | 双向 | 通用确认 |
|
||||
| `p2p:nack` | 双向 | 通用否定确认(错误) |
|
||||
| `p2p:heartbeat` | 双向 | 心跳保活 |
|
||||
| `p2p:capability_query` | 双向 | 查询对方能力 |
|
||||
| `p2p:capability_response` | 双向 | 返回能力清单 |
|
||||
| `p2p:task_need_human` | 执行方 → 请求方 | 请求人类介入 |
|
||||
| `p2p:human_decision` | 请求方 → 执行方 | 人类决策结果 |
|
||||
|
||||
### 2.2 消息类型完整定义
|
||||
|
||||
参见代码:[types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务状态机
|
||||
|
||||
### 3.1 状态定义
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `idle` | 空闲(初始状态) |
|
||||
| `pending` | 待确认(已发送请求,等待确认) |
|
||||
| `confirmed` | 已确认(对方接受任务) |
|
||||
| `executing` | 执行中 |
|
||||
| `info_needed` | 需要信息 |
|
||||
| `info_provided` | 信息已提供 |
|
||||
| `need_human` | 需要人类介入 |
|
||||
| `human_deciding` | 人类决策中 |
|
||||
| `human_approved` | 人类已批准 |
|
||||
| `human_rejected` | 人类已拒绝 |
|
||||
| `completed` | 已完成(终态) |
|
||||
| `failed` | 已失败(终态) |
|
||||
| `cancelled` | 已取消(终态) |
|
||||
| `rejected` | 已拒绝(终态) |
|
||||
|
||||
### 3.2 状态流转图
|
||||
|
||||
```
|
||||
请求方视角:
|
||||
|
||||
idle → pending → confirmed → executing → completed
|
||||
↓ ↓ → failed
|
||||
rejected info_needed → cancelled
|
||||
↑↓
|
||||
info_provided
|
||||
↓
|
||||
need_human
|
||||
↑↓
|
||||
human_decision
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心交互流程
|
||||
|
||||
### 4.1 正常任务提交流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->| 1. 提交任务
|
||||
| |
|
||||
|<------- task_confirm / task_reject -| 2. 确认/拒绝
|
||||
| |
|
||||
|<------- task_status_update --------| 3. 进度更新(多次)
|
||||
| |
|
||||
|<------- task_complete -------------| 4. 完成通知
|
||||
| |
|
||||
|-------- task_ack ----------------->| 5. 确认收到
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.2 信息补全流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|<------- task_need_info ------------| 发现信息缺失
|
||||
| |
|
||||
|-------- task_info_provided ------->| 补充信息
|
||||
| |
|
||||
|<------- task_status_update --------| 继续执行
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.3 人类介入流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|<------- task_need_human -----------| 需要人类决策
|
||||
| |
|
||||
| [人类做出决策] |
|
||||
| |
|
||||
|-------- human_decision ----------->| 决策结果
|
||||
| |
|
||||
|<------- task_status_update --------| 继续执行
|
||||
| |
|
||||
```
|
||||
|
||||
### 4.4 任务取消流程
|
||||
|
||||
```
|
||||
请求方 Agent 执行方 Agent
|
||||
| |
|
||||
|-------- task_request ------------->|
|
||||
| |
|
||||
|-------- task_cancel -------------->| 取消任务
|
||||
| |
|
||||
|<------- task_ack ------------------| 确认取消
|
||||
| |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口规范
|
||||
|
||||
### 5.1 P2P 协议处理器接口
|
||||
|
||||
**接口定义:**
|
||||
|
||||
```go
|
||||
type P2PProtocol interface {
|
||||
// 处理收到的消息
|
||||
HandleMessage(ctx context.Context, msg *UnifiedSynapseMessage) (*UnifiedSynapseMessage, error)
|
||||
|
||||
// 设置任务处理器
|
||||
SetTaskHandler(h TaskHandler)
|
||||
|
||||
// 设置回调发送器
|
||||
SetCallbackSender(s CallbackSender)
|
||||
|
||||
// 获取 Agent 端点
|
||||
AgentEndpoint() AgentEndpoint
|
||||
|
||||
// 查询任务状态
|
||||
GetTaskStatus(taskID string) (P2PTaskStatus, bool)
|
||||
|
||||
// 列出所有任务
|
||||
ListTasks() map[string]P2PTaskStatus
|
||||
}
|
||||
```
|
||||
|
||||
**参考实现:** [p2p/protocol.go](../p2p/protocol.go)
|
||||
|
||||
### 5.2 任务处理器接口
|
||||
|
||||
```go
|
||||
type TaskHandler interface {
|
||||
// 执行任务并返回结果
|
||||
ExecuteTask(ctx context.Context, taskType string, payload map[string]interface{}) (any, error)
|
||||
|
||||
// 验证任务是否可执行,返回缺失字段列表和是否可处理
|
||||
ValidateTask(taskType string, payload map[string]interface{}) ([]MissingField, bool)
|
||||
|
||||
// 获取能力列表
|
||||
GetCapabilities() []string
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `ExecuteTask`:实际执行业务逻辑的入口
|
||||
- `ValidateTask`:在任务确认前调用,用于判断是否接受任务或需要补充信息
|
||||
- `GetCapabilities`:返回 Agent 支持的任务类型列表
|
||||
|
||||
### 5.3 回调发送器接口
|
||||
|
||||
```go
|
||||
type CallbackSender interface {
|
||||
// 向远程 Agent 发送消息
|
||||
SendCallback(ctx context.Context, endpoint string, msg *UnifiedSynapseMessage) error
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- 传输层实现此接口
|
||||
- 支持多种传输方式(HTTP、gRPC、WebSocket 等)
|
||||
|
||||
### 5.4 配置项
|
||||
|
||||
```go
|
||||
type P2PConfig struct {
|
||||
MaxRetries int // 最大重试次数(默认 5)
|
||||
RetryBaseDelay time.Duration // 重试基础延迟(默认 2s,指数退避)
|
||||
TaskTimeout time.Duration // 任务执行超时(默认 5 分钟)
|
||||
HeartbeatInterval time.Duration // 心跳间隔(默认 30 秒)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 载荷格式
|
||||
|
||||
### 6.1 TaskRequestPayload(任务请求)
|
||||
|
||||
```json
|
||||
{
|
||||
"task": {
|
||||
"id": "task-xxx",
|
||||
"type": "text_generation",
|
||||
"priority": "normal",
|
||||
"deadline": "2026-01-01T00:00:00Z",
|
||||
"payload": {
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"session_id": "sess-xxx",
|
||||
"parent_task_id": "parent-xxx",
|
||||
"correlation_id": "corr-xxx",
|
||||
"metadata": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 TaskConfirmPayload(任务确认)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"confirmed": true,
|
||||
"estimated_duration": 120,
|
||||
"expected_completion": "2026-01-01T00:02:00Z",
|
||||
"capabilities_used": ["text_generation"],
|
||||
"constraints": {
|
||||
"max_retries": 3,
|
||||
"fallback_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 TaskRejectPayload(任务拒绝)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"reason": {
|
||||
"code": "capability_lacking",
|
||||
"message": "不支持该任务类型",
|
||||
"details": null
|
||||
},
|
||||
"alternatives": {
|
||||
"suggested_agent_id": "other-agent",
|
||||
"retry_after": 300,
|
||||
"partial_support": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 TaskStatusUpdatePayload(状态更新)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"status": "executing",
|
||||
"progress": 50,
|
||||
"message": "正在生成内容",
|
||||
"intermediate_result": null
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 TaskCompletePayload(任务完成)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"result": {
|
||||
"output_type": "text",
|
||||
"output": "生成的文本内容...",
|
||||
"metadata": {
|
||||
"tokens": 1024,
|
||||
"model": "gpt-4"
|
||||
}
|
||||
},
|
||||
"summary": "任务已完成"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 TaskFailedPayload(任务失败)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"error": {
|
||||
"code": "execution_error",
|
||||
"message": "模型调用超时",
|
||||
"recoverable": true,
|
||||
"details": null
|
||||
},
|
||||
"partial_result": null
|
||||
}
|
||||
```
|
||||
|
||||
### 6.7 TaskNeedInfoPayload(需要信息)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"missing_fields": [
|
||||
{
|
||||
"field": "style",
|
||||
"reason": "未指定生成风格",
|
||||
"suggested_value": "古典",
|
||||
"alternatives": ["现代", "浪漫"]
|
||||
}
|
||||
],
|
||||
"question": "请选择生成风格",
|
||||
"required_by": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 TaskNeedHumanPayload(需要人类)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"reason": {
|
||||
"code": "ethical_decision",
|
||||
"message": "涉及伦理决策需要人工审核"
|
||||
},
|
||||
"context": {
|
||||
"task_summary": "生成一张人物肖像",
|
||||
"current_progress": 30,
|
||||
"blocked_reason": "需要确认肖像使用授权"
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"field": "authorized",
|
||||
"value": true,
|
||||
"pros_cons": "继续生成,假设已授权",
|
||||
"confidence": 70
|
||||
}
|
||||
],
|
||||
"escalation_level": 1,
|
||||
"deadline": "2026-01-01T00:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.9 HumanDecisionPayload(人类决策)
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task-xxx",
|
||||
"decision_id": "dec-xxx",
|
||||
"reply_to": "msg-xxx",
|
||||
"decided_by": {
|
||||
"user_id": "user-123",
|
||||
"user_name": "张三",
|
||||
"role": "reviewer"
|
||||
},
|
||||
"decision": {
|
||||
"action": "approve",
|
||||
"approved_fields": {
|
||||
"authorized": true
|
||||
},
|
||||
"rejection_reason": ""
|
||||
},
|
||||
"notes": "已确认授权",
|
||||
"timestamp": "2026-01-01T00:03:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**完整载荷定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误码
|
||||
|
||||
| 错误码 | 说明 | HTTP 状态码(如适用) |
|
||||
|--------|------|---------------------|
|
||||
| `message_format_invalid` | 消息格式无效 | 400 |
|
||||
| `capability_lacking` | 能力不足 | 403 |
|
||||
| `agent_not_ready` | Agent 未就绪 | 503 |
|
||||
| `task_not_found` | 任务不存在 | 404 |
|
||||
| `task_already_completed` | 任务已完成 | 409 |
|
||||
| `execution_timeout` | 执行超时 | 504 |
|
||||
| `execution_failed` | 执行失败 | 500 |
|
||||
| `permission_denied` | 权限不足 | 403 |
|
||||
| `invalid_params` | 参数无效 | 400 |
|
||||
| `rate_limited` | 限流 | 429 |
|
||||
|
||||
**完整错误定义:** [types/errors.go](../types/errors.go)
|
||||
|
||||
---
|
||||
|
||||
## 8. 传输要求
|
||||
|
||||
### 8.1 HTTP 传输
|
||||
- Content-Type: `application/json`
|
||||
- 请求方法:`POST`
|
||||
- 端点路径:由 Agent 自行定义,建议 `/synapse/callback`
|
||||
- 超时:建议 30 秒
|
||||
|
||||
### 8.2 签名验证(可选)
|
||||
- 使用 HMAC-SHA256 签名
|
||||
- 签名放在 Header:`X-Synapse-Signature`
|
||||
- 或放在消息体:`signature` 字段
|
||||
|
||||
---
|
||||
|
||||
## 9. 参考实现
|
||||
|
||||
本协议的参考实现位于 `p2p/` 目录:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [p2p/protocol.go](../p2p/protocol.go) | P2P 协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 数据类型定义 |
|
||||
| [types/errors.go](../types/errors.go) | 错误定义 |
|
||||
| [transport/transport.go](../transport/transport.go) | HTTP 传输实现 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:能力声明格式
|
||||
|
||||
参见 [灵隙原始协议(Synapse Raw Protocol) - Capability](./01-synapse-raw-protocol.md#44-capability能力)
|
||||
545
synapse/docs/03-a2n-protocol.md
Normal file
545
synapse/docs/03-a2n-protocol.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# A2N 协议(Agent-to-N Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[灵隙原始协议(Synapse Raw Protocol) v1.0.0](./01-synapse-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
A2N(Agent-to-N)组群协议定义了多个 Agent 组成群组进行协同工作的完整规范,包括群组管理、任务拆分与合并、群体决策、角色与权限等。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 复杂任务的拆分与并行执行(MapReduce 模式)
|
||||
- 多 Agent 协同完成单一目标
|
||||
- 群体投票与共识决策
|
||||
- 临时或永久的 Agent 工作组
|
||||
- 分级升级(escalation)机制
|
||||
|
||||
### 1.2 依赖
|
||||
- 依赖 A2A 协议的点对点通信能力
|
||||
- 群组内成员间的单播通信使用 A2A 协议
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念
|
||||
|
||||
### 2.1 Group(群组)
|
||||
由多个 Agent 组成的协作单元,具有明确的拓扑结构、角色分工与协作规则。
|
||||
|
||||
### 2.2 Topology(拓扑结构)
|
||||
|
||||
| 拓扑类型 | 说明 | 适用场景 |
|
||||
|---------|------|---------|
|
||||
| `star` | 星型,中心 coordinator 协调 | 简单任务分发 |
|
||||
| `mesh` | 网状,成员间可直接通信 | 对等协作 |
|
||||
| `chain` | 链式,流水线模式 | 顺序处理 |
|
||||
| `hierarchical` | 层级型,多层管理 | 大型复杂任务 |
|
||||
| `fullmesh` | 全连接网状 | 高可靠协作 |
|
||||
|
||||
### 2.3 Role(角色)
|
||||
|
||||
| 角色 | 说明 | 权限 |
|
||||
|------|------|------|
|
||||
| `coordinator` | 协调者 | 创建/解散群组、任务拆分、发起投票 |
|
||||
| `worker` | 工作者 | 执行子任务、提交结果 |
|
||||
| `reviewer` | 审核者 | 审核结果、发起投票 |
|
||||
| `observer` | 观察者 | 只读,查看进度 |
|
||||
|
||||
### 2.4 GroupStatus(群组状态)
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `creating` | 创建中 |
|
||||
| `active` | 活跃 |
|
||||
| `frozen` | 冻结(不可执行新任务) |
|
||||
| `disbanded` | 已解散(终态) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 消息类型
|
||||
|
||||
### 3.1 消息类型清单
|
||||
|
||||
所有消息类型前缀:`group:` 或 `inter_group:`
|
||||
|
||||
#### 群组管理
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:create` | 创建群组 |
|
||||
| `group:disband` | 解散群组 |
|
||||
| `group:join` | 加入群组 |
|
||||
| `group:leave` | 离开群组 |
|
||||
| `group:invite` | 邀请加入 |
|
||||
| `group:accept_invite` | 接受邀请 |
|
||||
| `group:reject_invite` | 拒绝邀请 |
|
||||
| `group:member_update` | 成员更新通知 |
|
||||
| `group:role_change` | 角色变更 |
|
||||
|
||||
#### 群组通信
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:broadcast` | 广播(所有成员) |
|
||||
| `group:multicast` | 组播(指定成员) |
|
||||
| `group:direct` | 群内单播 |
|
||||
| `group:relay` | 消息中继 |
|
||||
|
||||
#### 任务协作
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:task_split` | 任务拆分 |
|
||||
| `group:task_assign` | 任务分配 |
|
||||
| `group:task_merge` | 结果合并 |
|
||||
| `group:task_progress` | 子任务进度 |
|
||||
|
||||
#### 共识与投票
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:vote` | 发起投票 |
|
||||
| `group:vote_submit` | 提交投票 |
|
||||
| `group:consensus` | 达成共识 |
|
||||
|
||||
#### 人在回路(HiL)
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `group:consensus_escalate` | 共识僵局升级给人类 |
|
||||
| `group:subtask_escalate` | 子任务阻塞升级给人类 |
|
||||
| `group:human_decision` | 人类决策结果 |
|
||||
| `inter_group:escalate` | 跨群组升级 |
|
||||
|
||||
**完整定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 4. 群组生命周期
|
||||
|
||||
### 4.1 状态机
|
||||
|
||||
```
|
||||
creating → active → frozen → disbanded
|
||||
↑ ↓
|
||||
└─────────┘
|
||||
解冻
|
||||
```
|
||||
|
||||
### 4.2 群组创建流程
|
||||
|
||||
```
|
||||
创建者 被邀请成员
|
||||
| |
|
||||
|------ group:invite -------------->| 发送邀请
|
||||
| |
|
||||
|<----- group:accept_invite --------| 接受邀请
|
||||
| |
|
||||
|------ group:member_update ------->| 通知成员更新
|
||||
| |
|
||||
[群组创建完成,状态变为 active] |
|
||||
```
|
||||
|
||||
### 4.3 群组解散流程
|
||||
|
||||
```
|
||||
coordinator 所有成员
|
||||
| |
|
||||
|------ group:disband ------------->| 通知解散
|
||||
| |
|
||||
[群组状态变为 disbanded] |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务协作流程
|
||||
|
||||
### 5.1 MapReduce 模式(拆分-执行-合并)
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ coordinator │
|
||||
└──────┬───────┘
|
||||
│
|
||||
group:task_split(拆分任务)
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ worker1 │ │ worker2 │ │ worker3 │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
│ 执行子任务 │ 执行子任务 │ 执行子任务
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
子任务结果 子任务结果 子任务结果
|
||||
└────────────────┼────────────────┘
|
||||
│
|
||||
group:task_merge(合并结果)
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ 最终结果 │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### 5.2 详细流程
|
||||
|
||||
1. **任务拆分阶段**
|
||||
- coordinator 接收原始任务
|
||||
- 将任务拆分为多个子任务(subtask)
|
||||
- 为每个子任务指定执行的 worker
|
||||
- 定义合并策略(merge strategy)
|
||||
|
||||
2. **并行执行阶段**
|
||||
- 各 worker 并行执行子任务
|
||||
- 定期上报进度(`group:task_progress`)
|
||||
- 子任务完成后上报结果
|
||||
|
||||
3. **结果合并阶段**
|
||||
- 所有子任务完成后,coordinator 合并结果
|
||||
- 根据合并策略决定最终结果
|
||||
- 输出最终结果
|
||||
|
||||
---
|
||||
|
||||
## 6. 合并策略(Merge Strategy)
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| `all_success` | 全部成功才认为成功,否则失败 |
|
||||
| `majority` | 多数子任务成功即可,取多数结果 |
|
||||
| `any_complete` | 任意一个完成即返回结果 |
|
||||
| `priority` | 按优先级,高优先级子任务结果优先 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 投票与共识
|
||||
|
||||
### 7.1 投票流程
|
||||
|
||||
```
|
||||
发起者 投票者
|
||||
| |
|
||||
|-------- group:vote -------------->| 发起投票
|
||||
| |
|
||||
|<------- group:vote_submit --------| 提交投票
|
||||
| |
|
||||
[计票,判断是否达到阈值] |
|
||||
| |
|
||||
|-------- group:consensus --------->| 公布结果
|
||||
| |
|
||||
```
|
||||
|
||||
### 7.2 投票规则
|
||||
|
||||
- `threshold`:通过阈值(0-1),如 0.5 表示过半
|
||||
- `deadline`:投票截止时间
|
||||
- `voters`:有投票权的成员列表
|
||||
- 超时未投票视为弃权
|
||||
|
||||
### 7.3 共识僵局(Consensus Deadlock)
|
||||
|
||||
当投票无法达成共识(如平票、未达阈值且超时)时,可升级给人类决策。
|
||||
|
||||
---
|
||||
|
||||
## 8. 人在回路(HiL)
|
||||
|
||||
群组协作中有两种升级场景:
|
||||
|
||||
### 8.1 共识僵局升级
|
||||
投票无法达成共识时,升级给人类裁决。
|
||||
|
||||
**消息:** `group:consensus_escalate`
|
||||
|
||||
### 8.2 子任务阻塞升级
|
||||
某个子任务遇到无法自动解决的问题时,升级给人类。
|
||||
|
||||
**消息:** `group:subtask_escalate`
|
||||
|
||||
### 8.3 跨群组升级
|
||||
问题超出当前群组能力时,可升级到上级群组或人类。
|
||||
|
||||
**消息:** `inter_group:escalate`
|
||||
|
||||
---
|
||||
|
||||
## 9. 接口规范
|
||||
|
||||
### 9.1 GroupProtocol 接口
|
||||
|
||||
```go
|
||||
type GroupProtocol interface {
|
||||
// 处理群组消息
|
||||
HandleMessage(ctx context.Context, msg *UnifiedSynapseMessage) (*UnifiedSynapseMessage, error)
|
||||
|
||||
// 设置任务处理器
|
||||
SetTaskHandler(h GroupTaskHandler)
|
||||
|
||||
// 查询群组列表
|
||||
GetGroups() []*AgentGroup
|
||||
|
||||
// 查询特定群组
|
||||
GetGroup(groupID string) (*AgentGroup, bool)
|
||||
}
|
||||
```
|
||||
|
||||
**参考实现:** [group/protocol.go](../group/protocol.go)
|
||||
|
||||
### 9.2 GroupTaskHandler 接口
|
||||
|
||||
```go
|
||||
type GroupTaskHandler interface {
|
||||
// 执行子任务
|
||||
ExecuteSubtask(ctx context.Context, subtaskID string, payload map[string]interface{}) (any, error)
|
||||
|
||||
// 合并多个子任务结果
|
||||
MergeResults(results []SubtaskResult, strategy MergeStrategy) (any, error)
|
||||
|
||||
// 验证任务是否可拆分,返回拆分方案
|
||||
ValidateTaskSplit(taskType string, requirements map[string]interface{}) ([]Subtask, bool)
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `ExecuteSubtask`:执行单个子任务的业务逻辑
|
||||
- `MergeResults`:根据策略合并多个子任务的结果
|
||||
- `ValidateTaskSplit`:验证任务是否可拆分,并给出拆分方案
|
||||
|
||||
### 9.3 配置项
|
||||
|
||||
```go
|
||||
type GroupConfig struct {
|
||||
MaxGroupSize int // 最大群组人数(默认 20)
|
||||
MaxSubtasks int // 最大子任务数(默认 50)
|
||||
ConsensusTimeout time.Duration // 共识超时(默认 5 分钟)
|
||||
TaskTimeout time.Duration // 任务超时(默认 10 分钟)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 载荷格式
|
||||
|
||||
### 10.1 GroupCreatePayload(创建群组)
|
||||
|
||||
```json
|
||||
{
|
||||
"group": {
|
||||
"name": "视频制作组",
|
||||
"topology": "star",
|
||||
"config": {
|
||||
"allow_self_leave": true,
|
||||
"requires_approval": false,
|
||||
"consensus_required": false,
|
||||
"consensus_threshold": 0.5,
|
||||
"broadcast_mode": "all"
|
||||
},
|
||||
"initial_members": [
|
||||
{
|
||||
"agent_id": "worker-1",
|
||||
"system_id": "sys-a",
|
||||
"endpoint": "http://worker1/synapse"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"id": "coordinator",
|
||||
"name": "协调者",
|
||||
"permissions": ["create", "disband", "split", "vote"],
|
||||
"max_tasks": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 GroupTaskSplitPayload(任务拆分)
|
||||
|
||||
```json
|
||||
{
|
||||
"original_task": {
|
||||
"id": "task-xxx",
|
||||
"type": "video_generation",
|
||||
"requirements": {
|
||||
"script": "xxx",
|
||||
"duration": 60
|
||||
},
|
||||
"deadline": "2026-01-01T00:10:00Z"
|
||||
},
|
||||
"subtasks": [
|
||||
{
|
||||
"id": "sub-1",
|
||||
"description": "生成脚本",
|
||||
"assigned_to": "writer-agent",
|
||||
"dependencies": [],
|
||||
"estimated_duration": 120,
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"id": "sub-2",
|
||||
"description": "生成配音",
|
||||
"assigned_to": "voice-agent",
|
||||
"dependencies": ["sub-1"],
|
||||
"estimated_duration": 180,
|
||||
"priority": 2
|
||||
}
|
||||
],
|
||||
"merge_strategy": "all_success"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 GroupTaskMergePayload(结果合并)
|
||||
|
||||
```json
|
||||
{
|
||||
"original_task_id": "task-xxx",
|
||||
"subtask_results": [
|
||||
{
|
||||
"subtask_id": "sub-1",
|
||||
"assigned_to": "writer-agent",
|
||||
"status": "success",
|
||||
"result": { "script": "..." },
|
||||
"completed_at": "2026-01-01T00:02:00Z"
|
||||
}
|
||||
],
|
||||
"merge_strategy_used": "all_success",
|
||||
"merged_result": {
|
||||
"video_url": "http://..."
|
||||
},
|
||||
"consensus_achieved": true
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 GroupVotePayload(投票)
|
||||
|
||||
```json
|
||||
{
|
||||
"vote_id": "vote-xxx",
|
||||
"question": "是否采用方案A?",
|
||||
"options": ["方案A", "方案B", "方案C"],
|
||||
"voters": ["agent-1", "agent-2", "agent-3"],
|
||||
"deadline": "2026-01-01T00:05:00Z",
|
||||
"threshold": 0.66,
|
||||
"status": "open",
|
||||
"votes": []
|
||||
}
|
||||
```
|
||||
|
||||
### 10.5 GroupConsensusEscalatePayload(共识升级)
|
||||
|
||||
```json
|
||||
{
|
||||
"vote_id": "vote-xxx",
|
||||
"escalation_id": "esc-xxx",
|
||||
"question": "是否采用方案A?",
|
||||
"votes": [
|
||||
{
|
||||
"voter_id": "agent-1",
|
||||
"voter_role": "reviewer",
|
||||
"option": "方案A",
|
||||
"reason": "质量更好"
|
||||
}
|
||||
],
|
||||
"consensus_status": {
|
||||
"required_threshold": 0.66,
|
||||
"actual_threshold": 0.33,
|
||||
"leading_option": "方案A",
|
||||
"options_tied": false
|
||||
},
|
||||
"escalate_reason": {
|
||||
"code": "no_consensus",
|
||||
"message": "无法达成共识"
|
||||
},
|
||||
"escalation_level": 2,
|
||||
"deadline": "2026-01-01T00:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**完整载荷定义:** [types/types.go](../types/types.go)
|
||||
|
||||
---
|
||||
|
||||
## 11. 数据结构
|
||||
|
||||
### 11.1 AgentGroup(群组)
|
||||
|
||||
```go
|
||||
type AgentGroup struct {
|
||||
ID string
|
||||
Name string
|
||||
Topology GroupTopology
|
||||
CreatedBy string
|
||||
CreatedAt Timestamp
|
||||
Config GroupConfig
|
||||
Members []GroupMember
|
||||
Roles []GroupRole
|
||||
Status GroupStatus
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 GroupMember(群组成员)
|
||||
|
||||
```go
|
||||
type GroupMember struct {
|
||||
AgentID string
|
||||
AgentName string
|
||||
Role string
|
||||
JoinedAt Timestamp
|
||||
Status GroupMemberStatus
|
||||
Capabilities []string
|
||||
LoadFactor float64 // 负载因子 0-1,用于任务分配
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 Subtask(子任务)
|
||||
|
||||
```go
|
||||
type Subtask struct {
|
||||
ID string
|
||||
Description string
|
||||
AssignedTo string
|
||||
Dependencies []string
|
||||
EstimatedDuration int
|
||||
Priority int
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 错误码
|
||||
|
||||
群组协议特有错误码:
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| `group_not_found` | 群组不存在 |
|
||||
| `group_not_active` | 群组不活跃 |
|
||||
| `group_full` | 群组已满 |
|
||||
| `already_member` | 已是成员 |
|
||||
| `not_group_member` | 不是群组成员 |
|
||||
| `member_not_found` | 成员不存在 |
|
||||
| `permission_denied` | 权限不足 |
|
||||
| `invalid_params` | 参数无效 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 参考实现
|
||||
|
||||
本协议的参考实现位于 `group/` 目录:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [group/protocol.go](../group/protocol.go) | 群组协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 群组相关数据类型 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:与 A2A 协议的关系
|
||||
|
||||
- 群组内的点对点通信使用 A2A 协议
|
||||
- 群组协议在 A2A 协议之上提供群组管理、任务拆分合并、共识投票等能力
|
||||
- 群组消息同样使用统一消息信封(`UnifiedSynapseMessage`),通过 `group_id` 字段标识群组上下文
|
||||
503
synapse/docs/04-p2a-protocol.md
Normal file
503
synapse/docs/04-p2a-protocol.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# P2A 协议(Person/Program-to-Agent Protocol)
|
||||
|
||||
> 版本:1.0.0
|
||||
> 状态:草案
|
||||
> 类型:实现规范
|
||||
> 基于:[灵隙原始协议(Synapse Raw Protocol) v1.0.0](./01-synapse-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
P2A(Person/Program-to-Agent)调用协议定义了**非 Agent 调用方**(如普通后端服务、前端应用、第三方系统、人类用户)如何调用 Agent 能力的规范。
|
||||
|
||||
本协议将 A2A 协议的复杂性封装起来,提供简洁的 RESTful API,使调用方无需了解 A2A 协议细节即可使用 Agent 能力。
|
||||
|
||||
### 1.1 适用场景
|
||||
- 前端应用直接调用 Agent 能力
|
||||
- 后端业务系统集成 Agent 服务
|
||||
- 第三方开发者通过 API 接入
|
||||
- 无需实现完整 A2A 协议的调用方
|
||||
|
||||
### 1.2 设计原则
|
||||
- **简单易用**:标准 RESTful API,JSON 格式
|
||||
- **两种模式**:回调模式 + 轮询模式,调用方按需选择
|
||||
- **认证简单**:API Key 认证,无需 HMAC 签名
|
||||
- **文件支持**:支持 multipart/form-data 上传文件
|
||||
- **错误友好**:清晰的错误码和错误信息
|
||||
|
||||
### 1.3 与 A2A 协议的关系
|
||||
|
||||
```
|
||||
外部调用方 ──P2A 协议──▶ External API Layer ──A2A 协议──▶ Agent
|
||||
```
|
||||
|
||||
P2A 协议是 A2A 协议的"简化入口",所有请求最终会转换为 灵隙消息发送给 Agent。
|
||||
|
||||
---
|
||||
|
||||
## 2. 认证方式
|
||||
|
||||
### 2.1 API Key 认证
|
||||
- 在 HTTP Header 中传递:`X-API-Key: {api_key}`
|
||||
- 或在 URL Query 中传递:`?api_key={api_key}`
|
||||
- 如果服务端未配置 API Key,则跳过认证
|
||||
|
||||
### 2.2 回调验证(可选)
|
||||
如果调用方提供了 `callback_token`,则服务端在回调时会在 Header 中带上 `X-Callback-Token`,用于调用方验证回调来源。
|
||||
|
||||
---
|
||||
|
||||
## 3. API 端点
|
||||
|
||||
所有端点默认前缀:`/api/v1/synapse`
|
||||
|
||||
### 3.1 端点清单
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/task` | 提交任务(JSON) |
|
||||
| POST | `/task/multipart` | 提交任务(带文件上传) |
|
||||
| GET | `/task/{task_id}` | 获取任务详情 |
|
||||
| GET | `/task/{task_id}/status` | 获取任务状态(简化版) |
|
||||
| GET | `/tasks` | 获取任务列表 |
|
||||
| POST | `/task/{task_id}/cancel` | 取消任务 |
|
||||
| GET | `/capabilities` | 获取能力清单 |
|
||||
| POST | `/callback` | 接收回调(调用方实现) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 任务提交
|
||||
|
||||
### 4.1 提交任务(JSON)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/synapse/task
|
||||
Content-Type: application/json
|
||||
X-API-Key: your-api-key
|
||||
|
||||
{
|
||||
"task_type": "text_generation",
|
||||
"payload": {
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典"
|
||||
},
|
||||
"external_task_id": "my-task-001",
|
||||
"priority": "normal",
|
||||
"callback_url": "https://your-domain.com/callback",
|
||||
"callback_token": "your-callback-token"
|
||||
}
|
||||
```
|
||||
|
||||
**请求字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `task_type` | string | 是 | 任务类型,如 `video_generation`, `image_generation` |
|
||||
| `payload` | object | 是 | 任务参数,具体结构取决于 task_type |
|
||||
| `external_task_id` | string | 否 | 调用方的任务 ID,用于关联 |
|
||||
| `priority` | string | 否 | 优先级:`low`, `normal`, `high`, `urgent`,默认 `normal` |
|
||||
| `callback_url` | string | 否 | 回调 URL,不提供则需轮询 |
|
||||
| `callback_token` | string | 否 | 回调验证 token |
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "received",
|
||||
"message": "任务已接收"
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `success` | bool | 是否成功 |
|
||||
| `task_id` | string | 服务端生成的任务 ID |
|
||||
| `status` | string | 初始状态,通常为 `received` |
|
||||
| `message` | string | 提示信息 |
|
||||
| `error` | string | 错误信息(失败时) |
|
||||
|
||||
### 4.2 提交任务(Multipart 带文件)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/synapse/task/multipart
|
||||
Content-Type: multipart/form-data
|
||||
X-API-Key: your-api-key
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="task_type"
|
||||
|
||||
image_generation
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="payload"
|
||||
|
||||
{"prompt":"一只猫","style":"写实"}
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="priority"
|
||||
|
||||
normal
|
||||
|
||||
------WebKitFormBoundary
|
||||
Content-Disposition: form-data; name="file"; filename="reference.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
[二进制数据]
|
||||
|
||||
------WebKitFormBoundary--
|
||||
```
|
||||
|
||||
**表单字段:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `task_type` | string | 是 | 任务类型 |
|
||||
| `payload` | string(JSON) | 是 | 任务参数(JSON 字符串) |
|
||||
| `external_task_id` | string | 否 | 外部任务 ID |
|
||||
| `priority` | string | 否 | 优先级 |
|
||||
| `callback_url` | string | 否 | 回调 URL |
|
||||
| `callback_token` | string | 否 | 回调 token |
|
||||
| `file` | file | 否 | 上传的文件(可多个) |
|
||||
|
||||
**响应:** 同 JSON 提交方式
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务查询
|
||||
|
||||
### 5.1 获取任务详情
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/synapse/task/{task_id}
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task": {
|
||||
"id": "task-abc123",
|
||||
"type": "text_generation",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "任务已完成",
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:01:00Z",
|
||||
"completed_at": "2026-01-01T00:01:00Z",
|
||||
"eta": "2026-01-01T00:01:00Z",
|
||||
"result": {
|
||||
"text": "生成的文本...",
|
||||
"metadata": { "tokens": 1024 }
|
||||
},
|
||||
"error": null,
|
||||
"external_task_id": "my-task-001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**任务状态:**
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `received` | 已接收 |
|
||||
| `processing` | 处理中 |
|
||||
| `completed` | 已完成 |
|
||||
| `failed` | 已失败 |
|
||||
| `cancelled` | 已取消 |
|
||||
|
||||
### 5.2 获取任务状态(简化版)
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/synapse/task/{task_id}/status
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "processing",
|
||||
"progress": 50
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 获取任务列表
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/synapse/tasks?status=processing&limit=20&offset=0
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `status` | string | 否 | 按状态过滤 |
|
||||
| `limit` | int | 否 | 每页数量,默认 20 |
|
||||
| `offset` | int | 否 | 偏移量,默认 0 |
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-abc123",
|
||||
"type": "text_generation",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 任务取消
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
POST /api/v1/synapse/task/{task_id}/cancel
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-abc123",
|
||||
"status": "cancelled",
|
||||
"message": "任务已取消"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 能力查询
|
||||
|
||||
**请求:**
|
||||
```http
|
||||
GET /api/v1/synapse/capabilities
|
||||
X-API-Key: your-api-key
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"capabilities": {
|
||||
"agent_id": "my-agent",
|
||||
"agent_name": "我的Agent",
|
||||
"system_id": "my-system",
|
||||
"protocol_version": "1.0.0",
|
||||
"supported_task_types": [
|
||||
{
|
||||
"type": "text_generation",
|
||||
"name": "文本生成",
|
||||
"description": "生成各种类型的文本内容",
|
||||
"estimated_time": "30s",
|
||||
"input_schema": "...",
|
||||
"output_schema": "..."
|
||||
}
|
||||
],
|
||||
"supported_transport_modes": ["callback", "polling"],
|
||||
"max_file_size": 104857600,
|
||||
"max_concurrent_tasks": 10,
|
||||
"support_progress_updates": true,
|
||||
"support_streaming": false,
|
||||
"support_hil": true,
|
||||
"support_groups": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 回调机制
|
||||
|
||||
如果调用方提供了 `callback_url`,服务端会在任务状态变化时主动回调。
|
||||
|
||||
### 8.1 回调请求
|
||||
|
||||
```http
|
||||
POST {callback_url}
|
||||
Content-Type: application/json
|
||||
X-Callback-Token: {callback_token}
|
||||
|
||||
{
|
||||
"task_id": "task-abc123",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "任务已完成",
|
||||
"result": {
|
||||
"text": "生成的文本..."
|
||||
},
|
||||
"error": null,
|
||||
"timestamp": "2026-01-01T00:01:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 回调类型
|
||||
|
||||
| 触发时机 | 说明 |
|
||||
|---------|------|
|
||||
| 任务开始处理 | `status=processing`, `progress=0` |
|
||||
| 进度更新 | `status=processing`, `progress=N` |
|
||||
| 任务完成 | `status=completed`, `progress=100`, 包含 result |
|
||||
| 任务失败 | `status=failed`, 包含 error |
|
||||
| 任务取消 | `status=cancelled` |
|
||||
|
||||
### 8.3 回调响应
|
||||
|
||||
调用方应返回 2xx 状态码表示成功接收。如果返回非 2xx,服务端会重试(指数退避,最多 5 次)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 错误处理
|
||||
|
||||
### 9.1 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "invalid_task_type",
|
||||
"message": "不支持的任务类型"
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 错误码列表
|
||||
|
||||
| 错误码 | HTTP 状态码 | 说明 |
|
||||
|--------|------------|------|
|
||||
| `unauthorized` | 401 | API Key 无效或缺失 |
|
||||
| `invalid_request` | 400 | 请求格式无效 |
|
||||
| `invalid_task_type` | 400 | 任务类型不支持 |
|
||||
| `invalid_params` | 400 | 参数无效 |
|
||||
| `task_not_found` | 404 | 任务不存在 |
|
||||
| `task_already_completed` | 409 | 任务已完成,无法取消 |
|
||||
| `file_too_large` | 413 | 文件过大 |
|
||||
| `rate_limited` | 429 | 请求过于频繁 |
|
||||
| `internal_error` | 500 | 服务内部错误 |
|
||||
| `service_unavailable` | 503 | 服务不可用 |
|
||||
|
||||
---
|
||||
|
||||
## 10. SDK 客户端接口
|
||||
|
||||
为方便调用方集成,提供 SDK 客户端。
|
||||
|
||||
### 10.1 SDK 初始化
|
||||
|
||||
```go
|
||||
client := sdk.NewClient(sdk.ClientConfig{
|
||||
BaseURL: "https://agent-api.example.com",
|
||||
APIKey: "your-api-key",
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
```
|
||||
|
||||
### 10.2 SDK 方法清单
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `SubmitTask(ctx, req)` | 提交任务(JSON) |
|
||||
| `SubmitTaskWithFiles(ctx, req, files)` | 提交任务(带文件) |
|
||||
| `GetTask(ctx, taskID)` | 获取任务详情 |
|
||||
| `GetTaskStatus(ctx, taskID)` | 获取任务状态 |
|
||||
| `ListTasks(ctx, status, limit, offset)` | 获取任务列表 |
|
||||
| `CancelTask(ctx, taskID)` | 取消任务 |
|
||||
| `GetCapabilities(ctx)` | 获取能力清单 |
|
||||
| `WaitForTask(ctx, taskID, interval)` | 等待任务完成(轮询) |
|
||||
|
||||
### 10.3 数据结构
|
||||
|
||||
```go
|
||||
type TaskRequest struct {
|
||||
TaskType string
|
||||
Payload map[string]any
|
||||
ExternalTaskID string
|
||||
Priority string
|
||||
CallbackURL string
|
||||
CallbackToken string
|
||||
}
|
||||
|
||||
type TaskInfo struct {
|
||||
ID string
|
||||
Type string
|
||||
Status string
|
||||
Progress int
|
||||
Message string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
CompletedAt string
|
||||
ETA string
|
||||
Result map[string]any
|
||||
Error *TaskError
|
||||
ExternalTaskID string
|
||||
}
|
||||
```
|
||||
|
||||
**完整 SDK 实现:** [sdk/client.go](../sdk/client.go)
|
||||
|
||||
---
|
||||
|
||||
## 11. 参考实现
|
||||
|
||||
本协议的参考实现:
|
||||
|
||||
| 模块 | 文件路径 | 说明 |
|
||||
|------|---------|------|
|
||||
| 服务端 | [externalapi/handler.go](../externalapi/handler.go) | External API Handler |
|
||||
| 客户端 SDK | [sdk/client.go](../sdk/client.go) | Go 语言 SDK |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:调用模式选择
|
||||
|
||||
### 回调模式 vs 轮询模式
|
||||
|
||||
| 对比项 | 回调模式 | 轮询模式 |
|
||||
|--------|---------|---------|
|
||||
| 实时性 | 高,状态变化即时通知 | 低,取决于轮询间隔 |
|
||||
| 实现复杂度 | 调用方需提供公网可访问的回调接口 | 简单,只需调用 GET 接口 |
|
||||
| 网络要求 | 调用方需要公网可达 | 只需调用方能访问服务端 |
|
||||
| 适用场景 | 生产环境、需要实时通知 | 开发测试、内网环境 |
|
||||
|
||||
### 选择建议
|
||||
|
||||
- **有公网服务器**:使用回调模式
|
||||
- **本地开发 / 内网环境**:使用轮询模式
|
||||
- **长时间任务**:建议回调模式,避免频繁轮询
|
||||
- **短任务(<10s)**:可直接同步等待或高频轮询
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:从 A2A 协议到 P2A 协议的映射
|
||||
|
||||
| 灵隙概念 | P2A 对应 | 说明 |
|
||||
|---------|---------|------|
|
||||
| `p2p:task_request` | `POST /task` | 任务提交 |
|
||||
| `p2p:task_status_update` | 回调 / GET /task | 状态更新 |
|
||||
| `p2p:task_complete` | 回调(status=completed) | 任务完成 |
|
||||
| `p2p:task_failed` | 回调(status=failed) | 任务失败 |
|
||||
| `p2p:task_cancel` | `POST /task/{id}/cancel` | 任务取消 |
|
||||
| `p2p:capability_query` | `GET /capabilities` | 能力查询 |
|
||||
| Agent 身份 | API Key | 简化认证 |
|
||||
| HMAC 签名 | Callback Token | 简化验证 |
|
||||
519
synapse/docs/05-sdk-implementation.md
Normal file
519
synapse/docs/05-sdk-implementation.md
Normal file
@@ -0,0 +1,519 @@
|
||||
# SDK 实现说明
|
||||
|
||||
> 版本:1.0.0
|
||||
> 更新日期:2026-06-27
|
||||
> 协议版本:1.0.0
|
||||
> 关联协议:
|
||||
> - [A2A 协议](./02-a2a-protocol.md)
|
||||
> - [A2N 协议](./03-a2n-protocol.md)
|
||||
> - [P2A 协议](./04-p2a-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
SDK 按照灵隙协议族(Synapse Protocol Suite)的三个子协议进行了模块化设计,分为三个子包,命名与协议名保持一致:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SDK (sdk/) │
|
||||
├──────────────┬───────────────────┬───────────────────────────┤
|
||||
│ sdk/p2a │ sdk/a2a │ sdk/a2n │
|
||||
│ P2A 协议 │ A2A 协议 │ A2N 协议 │
|
||||
│ (外部调用) │ (Agent间通信) │ (群组协作) │
|
||||
│ ✅ 完整实现 │ ✅ 完整实现 │ ✅ 完整实现 │
|
||||
└──────────────┴───────────────────┴───────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. SDK 目录结构
|
||||
|
||||
```
|
||||
sdk/
|
||||
├── doc.go # SDK 包文档(使用说明)
|
||||
├── p2a/
|
||||
│ └── client.go # P2A 协议 - 外部调用客户端
|
||||
├── a2a/
|
||||
│ └── client.go # A2A 协议 - Agent间通信客户端
|
||||
└── a2n/
|
||||
└── client.go # A2N 协议 - 群组协作客户端
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 第一部分:sdk/p2a - P2A 协议
|
||||
|
||||
**协议对应**:[P2A 协议](./04-p2a-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:非 Agent 调用方(前端、后端服务、第三方应用)通过 REST API 调用 Agent 能力
|
||||
|
||||
### 3.1 实现对应关系
|
||||
|
||||
| P2A 协议功能 | SDK 方法 | 位置 |
|
||||
|-------------|---------|------|
|
||||
| 认证方式 | `setHeaders()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 提交任务(JSON) | `SubmitTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 提交任务(Multipart) | `SubmitTaskWithFiles()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务详情 | `GetTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务状态(简化版) | `GetTaskStatus()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 获取任务列表 | `ListTasks()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 取消任务 | `CancelTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 能力查询 | `GetCapabilities()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 回调验证 | `VerifyCallback()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
| 轮询等待 | `WaitForTask()` | [sdk/p2a/client.go](../sdk/p2a/client.go) |
|
||||
|
||||
### 3.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client 客户端
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// 任务请求
|
||||
type TaskRequest struct {
|
||||
TaskType string
|
||||
Payload map[string]any
|
||||
ExternalTaskID string
|
||||
Priority string
|
||||
CallbackURL string
|
||||
CallbackToken string
|
||||
}
|
||||
|
||||
// 任务信息
|
||||
type TaskInfo struct {
|
||||
ID string
|
||||
Type string
|
||||
Status string
|
||||
Progress int
|
||||
Result map[string]any
|
||||
Error *TaskError
|
||||
}
|
||||
|
||||
// 文件上传参数
|
||||
type FileParam struct {
|
||||
FieldName string
|
||||
Filename string
|
||||
Data []byte
|
||||
MimeType string
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 使用示例
|
||||
|
||||
```go
|
||||
import "dp-headless/synapse/sdk/p2a"
|
||||
|
||||
client := p2a.NewClient(p2a.ClientConfig{
|
||||
BaseURL: "https://agent-api.example.com",
|
||||
APIKey: "your-api-key",
|
||||
})
|
||||
|
||||
// 提交任务并等待完成
|
||||
taskInfo, err := client.CreateAndWait(ctx, "text_generation", map[string]any{
|
||||
"prompt": "写一首诗",
|
||||
"style": "古典",
|
||||
}, 5*time.Second)
|
||||
|
||||
// 使用回调模式
|
||||
resp, err := client.CreateWithCallback(ctx, "image_generation", payload,
|
||||
"https://your-domain.com/callback",
|
||||
"callback-token",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 第二部分:sdk/a2a - A2A 协议
|
||||
|
||||
**协议对应**:[A2A 协议](./02-a2a-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:Agent 之间的点对点通信,包含消息发送、签名验证、回调处理
|
||||
|
||||
### 4.1 实现对应关系
|
||||
|
||||
| A2A 协议功能 | SDK 方法 | 位置 |
|
||||
|------------|---------|------|
|
||||
| 发送任务请求 | `SendTaskRequest()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 发送任务取消 | `SendTaskCancel()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 发送人类决策 | `SendHumanDecision()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 查询能力 | `SendCapabilityQuery()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 消息签名 | `signMessage()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 签名验证 | `verifySignature()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 任务状态管理 | `GetTaskState()`, `ListTasks()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 等待任务完成 | `WaitForTask()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
| 回调服务 | `startCallbackServer()` | [sdk/a2a/client.go](../sdk/a2a/client.go) |
|
||||
|
||||
### 4.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client A2A客户端
|
||||
type Client struct {
|
||||
agentEndpoint types.AgentEndpoint
|
||||
authSecret string
|
||||
callbackHandler MessageCallbackHandler
|
||||
tasks map[string]*A2ATaskState
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
AgentEndpoint types.AgentEndpoint
|
||||
AuthSecret string
|
||||
CallbackPort int
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// 消息回调处理器接口
|
||||
type MessageCallbackHandler interface {
|
||||
HandleTaskRequest(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error)
|
||||
HandleTaskStatusUpdate(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleTaskComplete(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleTaskFailed(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
HandleNeedHuman(ctx context.Context, msg *types.UnifiedSynapseMessage) error
|
||||
}
|
||||
|
||||
// 任务状态
|
||||
type A2ATaskState struct {
|
||||
TaskID string
|
||||
Status types.P2PTaskStatus
|
||||
Progress int
|
||||
Message string
|
||||
Result any
|
||||
Error *types.SynapseError
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 使用示例
|
||||
|
||||
```go
|
||||
import "dp-headless/synapse/sdk/a2a"
|
||||
|
||||
a2aClient, err := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: types.AgentEndpoint{
|
||||
AgentID: "my-agent",
|
||||
AgentName: "我的Agent",
|
||||
SystemID: "my-system",
|
||||
Endpoint: "http://localhost:8080/synapse/callback",
|
||||
},
|
||||
AuthSecret: "my-hmac-secret",
|
||||
CallbackPort: 8080,
|
||||
})
|
||||
|
||||
// 设置消息处理器
|
||||
a2aClient.SetMessageHandler(&myHandler{})
|
||||
|
||||
// 发送任务请求给其他Agent
|
||||
taskID, err := a2aClient.SendTaskRequest(ctx, targetAgent, "video_generation", map[string]any{
|
||||
"prompt": "生成一段视频",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 第三部分:sdk/a2n - A2N 协议
|
||||
|
||||
**协议对应**:[A2N 协议](./03-a2n-protocol.md)
|
||||
**版本**:1.0.0
|
||||
**用途**:多 Agent 群组协作,包含群组管理、任务拆分合并、投票、升级
|
||||
|
||||
### 5.1 实现对应关系
|
||||
|
||||
| A2N 协议功能 | SDK 方法 | 位置 |
|
||||
|------------|---------|------|
|
||||
| 创建群组 | `CreateGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 加入群组 | `JoinGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 离开群组 | `LeaveGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 解散群组 | `DisbandGroup()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 提交群组任务 | `SubmitGroupTask()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 发送子任务结果 | `SendSubtaskResult()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 发起投票 | `InitiateVote()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 提交投票 | `SubmitVote()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 广播消息 | `Broadcast()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
| 升级到人类 | `EscalateToHuman()` | [sdk/a2n/client.go](../sdk/a2n/client.go) |
|
||||
|
||||
### 5.2 核心数据结构
|
||||
|
||||
```go
|
||||
// Client A2N客户端
|
||||
type Client struct {
|
||||
a2aClient *a2a.Client
|
||||
groups map[string]*GroupState
|
||||
eventHandler GroupEventHandler
|
||||
}
|
||||
|
||||
// 配置
|
||||
type ClientConfig struct {
|
||||
A2AClient *a2a.Client
|
||||
EventHandler GroupEventHandler
|
||||
}
|
||||
|
||||
// 群组状态
|
||||
type GroupState struct {
|
||||
GroupID string
|
||||
Name string
|
||||
Topology types.GroupTopology
|
||||
Status types.GroupStatus
|
||||
Members []types.GroupMember
|
||||
Role string // "coordinator" | "worker"
|
||||
}
|
||||
|
||||
// 群组事件处理器接口
|
||||
type GroupEventHandler interface {
|
||||
OnGroupCreated(ctx context.Context, groupID string) error
|
||||
OnGroupDisbanded(ctx context.Context, groupID string) error
|
||||
OnMemberJoined(ctx context.Context, groupID string, member types.GroupMember) error
|
||||
OnMemberLeft(ctx context.Context, groupID string, agentID string) error
|
||||
OnTaskSplit(ctx context.Context, groupID string, originalTaskID string, subtasks []types.Subtask) error
|
||||
OnSubtaskProgress(ctx context.Context, groupID string, subtaskID string, progress int) error
|
||||
OnTaskMerged(ctx context.Context, groupID string, originalTaskID string, result any) error
|
||||
OnVoteInitiated(ctx context.Context, groupID string, vote types.GroupVotePayload) error
|
||||
OnVoteResult(ctx context.Context, groupID string, voteID string, option string, passed bool) error
|
||||
OnEscalation(ctx context.Context, groupID string, escalationID string, level int) error
|
||||
OnBroadcast(ctx context.Context, groupID string, sender types.AgentEndpoint, message []byte) error
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 使用示例
|
||||
|
||||
```go
|
||||
import (
|
||||
"dp-headless/synapse/sdk/a2n"
|
||||
"dp-headless/synapse/sdk/a2a"
|
||||
)
|
||||
|
||||
// 创建A2A客户端
|
||||
a2aClient, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: myEndpoint,
|
||||
})
|
||||
|
||||
// 创建A2N客户端
|
||||
a2nClient, err := a2n.NewClient(&a2n.ClientConfig{
|
||||
A2AClient: a2aClient,
|
||||
EventHandler: &myGroupHandler{},
|
||||
})
|
||||
|
||||
// 创建群组
|
||||
group, err := a2nClient.CreateGroup(ctx, "视频制作组", types.GroupTopologyStar, nil, members)
|
||||
|
||||
// 作为Coordinator,提交任务
|
||||
subtasks, err := a2nClient.SubmitGroupTask(ctx, group.GroupID, "video_generation",
|
||||
map[string]any{"script": "..."},
|
||||
types.MergeAllSuccess,
|
||||
)
|
||||
|
||||
// 发起投票
|
||||
voteID, err := a2nClient.InitiateVote(ctx, group.GroupID,
|
||||
"选择哪个方案?",
|
||||
[]string{"方案A", "方案B"},
|
||||
voterIDs,
|
||||
0.5,
|
||||
5*time.Minute,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. SDK 架构总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SDK (dp-headless/synapse/sdk) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ doc.go │
|
||||
│ ├── 包文档和使用说明 │
|
||||
│ └── 指向各子包的链接 │
|
||||
├────────────────┬────────────────┬─────────────────────────┤
|
||||
│ sdk/p2a │ sdk/a2a │ sdk/a2n │
|
||||
│ │ │ │
|
||||
│ P2A协议 │ 灵隙协议 │ A2N协议 │
|
||||
│ │ │ │
|
||||
│ ├── Client │ ├── Client │ ├── Client │
|
||||
│ ├── 配置 │ ├── 配置 │ ├── 配置 │
|
||||
│ ├── 任务操作 │ ├── 消息发送 │ ├── 群组管理 │
|
||||
│ ├── 能力查询 │ ├── 任务状态 │ ├── 任务协作 │
|
||||
│ └── 回调验证 │ ├── 回调服务 │ ├── 投票 │
|
||||
│ │ └── 签名 │ └── 升级 │
|
||||
└────────────────┴────────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 快速开始
|
||||
|
||||
### 7.1 P2A 调用(外部调用者)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/synapse/sdk/p2a"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := p2a.NewClient(p2a.ClientConfig{
|
||||
BaseURL: "https://agent.example.com",
|
||||
APIKey: "your-api-key",
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 方式1: 轮询等待
|
||||
result, err := client.CreateAndWait(ctx, "text_generation", map[string]any{
|
||||
"prompt": "写一首诗",
|
||||
}, 5*time.Second)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
}
|
||||
fmt.Printf("Result: %v\n", result.Result)
|
||||
|
||||
// 方式2: 回调模式
|
||||
resp, err := client.CreateWithCallback(ctx, "image_generation",
|
||||
map[string]any{"prompt": "一只猫"},
|
||||
"https://your-domain.com/callback",
|
||||
"token",
|
||||
)
|
||||
fmt.Printf("Task ID: %s\n", resp.TaskID)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 灵隙 通信(Agent 间)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/synapse/sdk/a2a"
|
||||
"dp-headless/synapse/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: types.AgentEndpoint{
|
||||
AgentID: "agent-001",
|
||||
AgentName: "Agent 001",
|
||||
SystemID: "my-system",
|
||||
Endpoint: "http://localhost:8080/synapse/callback",
|
||||
},
|
||||
AuthSecret: "secret-key",
|
||||
CallbackPort: 8080,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
target := types.AgentEndpoint{
|
||||
AgentID: "agent-002",
|
||||
Endpoint: "http://agent-002:8080/synapse/callback",
|
||||
}
|
||||
|
||||
// 发送任务请求
|
||||
taskID, err := client.SendTaskRequest(ctx, target, "data_analysis", map[string]any{
|
||||
"dataset": "sales.csv",
|
||||
})
|
||||
fmt.Printf("Task ID: %s\n", taskID)
|
||||
|
||||
// 等待完成
|
||||
state, err := client.WaitForTask(ctx, taskID, 5*time.Minute)
|
||||
fmt.Printf("Status: %s\n", state.Status)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 A2N 群组协作
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dp-headless/synapse/sdk/a2n"
|
||||
"dp-headless/synapse/sdk/a2a"
|
||||
"dp-headless/synapse/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a2aClient, _ := a2a.NewClient(&a2a.ClientConfig{
|
||||
AgentEndpoint: myEndpoint,
|
||||
})
|
||||
|
||||
a2nClient, _ := a2n.NewClient(&a2n.ClientConfig{
|
||||
A2AClient: a2aClient,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建群组
|
||||
group, _ := a2nClient.CreateGroup(ctx, "协作组", types.GroupTopologyStar, nil, []types.AgentEndpoint{
|
||||
{AgentID: "member-1", Endpoint: "http://member-1:8080"},
|
||||
{AgentID: "member-2", Endpoint: "http://member-2:8080"},
|
||||
})
|
||||
|
||||
fmt.Printf("Group ID: %s\n", group.GroupID)
|
||||
|
||||
// 提交任务
|
||||
subtasks, _ := a2nClient.SubmitGroupTask(ctx, group.GroupID, "video_generation",
|
||||
map[string]any{"script": "..."},
|
||||
types.MergeAllSuccess,
|
||||
)
|
||||
|
||||
fmt.Printf("Split into %d subtasks\n", len(subtasks))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 协议与包名对应关系
|
||||
|
||||
| 协议名称 | 协议文档 | SDK 包名 | 包路径 |
|
||||
|---------|---------|---------|--------|
|
||||
| 灵隙原始协议(Synapse Raw Protocol) | [01-synapse-raw-protocol.md](./01-synapse-raw-protocol.md) | - | 纲领性文档 |
|
||||
| A2A 协议 | [02-a2a-protocol.md](./02-a2a-protocol.md) | `a2a` | `sdk/a2a/` |
|
||||
| A2N 协议 | [03-a2n-protocol.md](./03-a2n-protocol.md) | `a2n` | `sdk/a2n/` |
|
||||
| P2A 协议 | [04-p2a-protocol.md](./04-p2a-protocol.md) | `p2a` | `sdk/p2a/` |
|
||||
|
||||
**所有协议版本:1.0.0**
|
||||
|
||||
---
|
||||
|
||||
## 9. 扩展建议
|
||||
|
||||
| 优先级 | 扩展项 | 说明 |
|
||||
|-------|-------|------|
|
||||
| 高 | gRPC 传输 | 添加 gRPC 协议支持 |
|
||||
| 高 | WebSocket | 添加 WebSocket 流式传输 |
|
||||
| 中 | 批量操作 | 批量任务提交、批量查询 |
|
||||
| 中 | 指标监控 | 内置指标收集 |
|
||||
| 低 | 多语言 SDK | Python、JavaScript 等 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文件索引
|
||||
|
||||
| 文件路径 | 说明 |
|
||||
|---------|------|
|
||||
| [sdk/doc.go](../sdk/doc.go) | SDK 包文档 |
|
||||
| [sdk/p2a/client.go](../sdk/p2a/client.go) | P2A 客户端实现 |
|
||||
| [sdk/a2a/client.go](../sdk/a2a/client.go) | A2A 客户端实现 |
|
||||
| [sdk/a2n/client.go](../sdk/a2n/client.go) | A2N 客户端实现 |
|
||||
| [externalapi/handler.go](../externalapi/handler.go) | P2A 协议服务端实现 |
|
||||
| [p2p/protocol.go](../p2p/protocol.go) | A2A 协议核心实现 |
|
||||
| [group/protocol.go](../group/protocol.go) | A2N 协议核心实现 |
|
||||
| [types/types.go](../types/types.go) | 公共类型定义 |
|
||||
| [transport/transport.go](../transport/transport.go) | HTTP 传输层实现 |
|
||||
777
synapse/docs/06-solutions-whitepaper.md
Normal file
777
synapse/docs/06-solutions-whitepaper.md
Normal file
@@ -0,0 +1,777 @@
|
||||
# 灵隙协议族(Synapse Protocol Suite) - 解决方案白皮书
|
||||
|
||||
> 版本:1.0.0
|
||||
> 日期:2026-06-27
|
||||
> 状态:草案
|
||||
> 前置阅读:[灵隙原始协议(Synapse Raw Protocol)](./01-synapse-raw-protocol.md)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [概述](#1-概述)
|
||||
2. [方案一:Agent 发现与广播机制](#2-方案一agent-发现与广播机制)
|
||||
3. [方案二:身份认证与信任体系](#3-方案二身份认证与信任体系)
|
||||
4. [方案三:协议缺失内容补全](#4-方案三协议缺失内容补全)
|
||||
5. [分阶段实施路线图](#5-分阶段实施路线图)
|
||||
6. [技术选型建议](#6-技术选型建议)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
当前灵隙协议族(Synapse Protocol Suite)定义了 Agent 之间"怎么说话"(消息格式、任务流程),但缺少:
|
||||
|
||||
- **怎么找到对方** — Agent 发现与广播机制
|
||||
- **怎么相信对方** — 身份认证与信任体系
|
||||
- **怎么规模化协作** — 路由、调度、激励等基础设施
|
||||
|
||||
本文档针对上述问题给出完整的解决方案。
|
||||
|
||||
### 1.2 设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **渐进式** | 从简单到复杂,每阶段都可用 |
|
||||
| **可插拔** | 组件可替换,不绑定具体实现 |
|
||||
| **传输无关** | 不依赖特定底层网络 |
|
||||
| **安全优先** | 身份认证是基础设施,不是可选功能 |
|
||||
| **向后兼容** | 不破坏现有协议,增量扩展 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案一:Agent 发现与广播机制
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Agent 发现与广播层 │
|
||||
├─────────────────────┬─────────────────────┬─────────────────────────┤
|
||||
│ 集中式目录服务 │ 分布式 Gossip │ 组播/广播 │
|
||||
│ (Directory) │ (Gossip) │ (Multicast) │
|
||||
│ ┌───────────────┐ │ ┌───────────────┐ │ ┌────────────────┐ │
|
||||
│ │ 注册/注销 │ │ │ 消息泛洪 │ │ │ 子网内广播 │ │
|
||||
│ │ 能力索引 │ │ │ 状态同步 │ │ │ 邻居发现 │ │
|
||||
│ │ 名字解析 │ │ │ 节点发现 │ │ │ 本地心跳 │ │
|
||||
│ └───────────────┘ │ └───────────────┘ │ └────────────────┘ │
|
||||
└─────────────────────┴─────────────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 模式一:集中式目录服务(推荐首选)
|
||||
|
||||
#### 2.2.1 架构说明
|
||||
|
||||
引入 **Agent Directory Service(ADS)** 作为中心化的 Agent 目录服务,类似 DNS。
|
||||
|
||||
```
|
||||
Agent A ──注册──▶ ┌─────────────┐ ◀──注册── Agent B
|
||||
│ Directory │
|
||||
Agent C ──查询──▶ │ Service │ ◀──心跳── Agent D
|
||||
└─────────────┘
|
||||
│
|
||||
▼
|
||||
能力索引
|
||||
在线状态
|
||||
端点地址
|
||||
```
|
||||
|
||||
#### 2.2.2 核心接口
|
||||
|
||||
**注册协议(Agent → Directory):**
|
||||
|
||||
| 操作 | 消息类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| 注册 | `dir:register` | Agent 上线时注册自身信息 |
|
||||
| 注销 | `dir:unregister` | Agent 下线时注销 |
|
||||
| 心跳 | `dir:heartbeat` | 定期上报存活状态 |
|
||||
| 更新能力 | `dir:update_capabilities` | 能力变化时更新 |
|
||||
|
||||
**查询协议(Agent → Directory):**
|
||||
|
||||
| 操作 | 消息类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| 按 ID 查询 | `dir:query_by_id` | 根据 AgentID 查端点 |
|
||||
| 按能力查询 | `dir:query_by_capability` | 找具备某能力的 Agent |
|
||||
| 列表查询 | `dir:list` | 分页列出所有 Agent |
|
||||
| 批量查询 | `dir:batch_query` | 批量查询多个 Agent |
|
||||
|
||||
#### 2.2.3 注册数据结构
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"agent_name": "视频生成Agent",
|
||||
"system_id": "video-system",
|
||||
"endpoint": "https://agent-001.example.com/synapse/callback",
|
||||
"version": "1.0.0",
|
||||
"capabilities": [
|
||||
{
|
||||
"type": "video_generation",
|
||||
"name": "视频生成",
|
||||
"description": "根据脚本生成视频",
|
||||
"max_concurrent": 10,
|
||||
"avg_latency_ms": 5000
|
||||
}
|
||||
],
|
||||
"transports": ["http", "grpc"],
|
||||
"status": "online",
|
||||
"last_heartbeat": "2026-06-27T10:00:00Z",
|
||||
"trust_score": 85,
|
||||
"public_key": "-----BEGIN PUBLIC KEY-----\n..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2.4 优缺点
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 查询效率高(O(1)) | 单点故障风险 |
|
||||
| 支持复杂查询(按能力、按系统等) | 中心节点需信任 |
|
||||
| 易于管理和监控 | 扩展性有上限 |
|
||||
| 实现简单 | 存在注册/注销时序问题 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 模式二:分布式 Gossip 协议(可选扩展)
|
||||
|
||||
#### 2.3.1 架构说明
|
||||
|
||||
无中心节点,Agent 之间通过 Gossip 协议(epidemic protocol)扩散状态信息,类似比特币网络的节点发现。
|
||||
|
||||
```
|
||||
Agent A ──gossip──▶ Agent B ──gossip──▶ Agent D
|
||||
│ │
|
||||
└──gossip──▶ Agent C ──gossip──▶ Agent E
|
||||
```
|
||||
|
||||
#### 2.3.2 核心机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| **Peer Sampling** | 每个 Agent 维护一个部分视图(partial view) |
|
||||
| **Active Push** | 定期随机选几个节点推送本地状态 |
|
||||
| **Passive Pull** | 收到推送后,有概率回传自身状态 |
|
||||
| **TTL** | 消息有存活跳数,防止无限扩散 |
|
||||
| **Deduplication** | 消息 ID 去重,避免重复处理 |
|
||||
|
||||
#### 2.3.3 Gossip 消息格式
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "gossip:state",
|
||||
"from": "agent-001",
|
||||
"view": [
|
||||
{"agent_id": "agent-002", "endpoint": "...", "capabilities": [...], "timestamp": "..."},
|
||||
{"agent_id": "agent-003", "endpoint": "...", "capabilities": [...], "timestamp": "..."},
|
||||
{"agent_id": "agent-004", "endpoint": "...", "capabilities": [...], "timestamp": "..."}
|
||||
],
|
||||
"ttl": 5,
|
||||
"msg_id": "msg-xxx"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3.4 优缺点
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 完全去中心化 | 查询效率低(需本地构建视图) |
|
||||
| 容错性强 | 状态最终一致,有延迟 |
|
||||
| 可无限扩展 | 实现复杂度高 |
|
||||
| 无单点故障 | 网络流量大 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 模式三:本地组播/邻居发现(局域网场景)
|
||||
|
||||
适用于同一内网/集群内的 Agent 快速发现。
|
||||
|
||||
#### 2.4.1 机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| **UDP 组播** | 定期在组播地址发送广播 |
|
||||
| **mDNS/DNS-SD** | 类似 Apple Bonjour 的服务发现 |
|
||||
| **ARP/NDP** | 链路层发现(不推荐) |
|
||||
|
||||
#### 2.4.2 适用场景
|
||||
|
||||
- 边缘计算集群
|
||||
- 同一机房内的 Agent 集群
|
||||
- 本地开发测试环境
|
||||
|
||||
---
|
||||
|
||||
### 2.5 广播机制设计
|
||||
|
||||
#### 2.5.1 广播类型
|
||||
|
||||
| 广播范围 | 消息类型前缀 | 说明 |
|
||||
|---------|-------------|------|
|
||||
| 全网广播 | `global:` | 所有在线 Agent |
|
||||
| 系统广播 | `system:` | 同一 system_id 内 |
|
||||
| 群组广播 | `group:` | 同一群组内(已有) |
|
||||
| 能力组广播 | `cap:` | 具备某能力的 Agent |
|
||||
|
||||
#### 2.5.2 广播实现
|
||||
|
||||
**集中式模式下:**
|
||||
- 由 Directory Service 转发广播消息
|
||||
- 支持按条件过滤(按能力、按系统等)
|
||||
|
||||
**分布式模式下:**
|
||||
- 通过 Gossip 协议泛洪
|
||||
- TTL 控制扩散范围
|
||||
- 消息 ID 去重
|
||||
|
||||
---
|
||||
|
||||
### 2.6 心跳与存活检测
|
||||
|
||||
#### 2.6.1 心跳机制
|
||||
|
||||
```
|
||||
Agent ───heartbeat──▶ Directory
|
||||
(每30秒)
|
||||
|
||||
Directory 判定离线:
|
||||
- 连续 3 次未收到心跳 → 标记为 offline
|
||||
- 离线 24 小时 → 自动注销
|
||||
```
|
||||
|
||||
#### 2.6.2 主动探测
|
||||
|
||||
```
|
||||
Directory ───ping──▶ Agent
|
||||
◀──pong──
|
||||
```
|
||||
|
||||
用于验证 Agent 是否真的在线(防止心跳伪造)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案二:身份认证与信任体系
|
||||
|
||||
### 3.1 三层身份架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Layer 3: 信任评分 (Trust Score) │
|
||||
│ 声誉系统、历史行为、第三方评价 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 2: 身份验证 (Authentication) │
|
||||
│ 签名验证、证书校验、DID 验证 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Layer 1: 身份标识 (Identity) │
|
||||
│ AgentID + 公钥 / 证书 / DID │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Layer 1:身份标识方案
|
||||
|
||||
#### 3.2.1 方案 A:公钥身份(推荐基础版)
|
||||
|
||||
**核心思想**:AgentID = 公钥哈希(类似比特币地址)
|
||||
|
||||
```
|
||||
AgentID = base58(sha256(public_key)[:20])
|
||||
例:agent-1A2B3C4D5E6F...
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 自包含,不需要中心 CA
|
||||
- 公钥可验证身份
|
||||
- 实现简单
|
||||
|
||||
**缺点:**
|
||||
- 身份无法撤销(私钥泄露 = 身份被盗)
|
||||
- 没有人类可读的名字
|
||||
- 无法关联真实世界身份
|
||||
|
||||
**适用场景**:内部系统、测试环境、信任度要求不高的场景
|
||||
|
||||
#### 3.2.2 方案 B:PKI 证书体系
|
||||
|
||||
**核心思想**:引入 CA(Certificate Authority)颁发证书
|
||||
|
||||
```
|
||||
CA 根证书
|
||||
├── 系统A证书
|
||||
│ ├── Agent-001 证书
|
||||
│ └── Agent-002 证书
|
||||
└── 系统B证书
|
||||
└── Agent-003 证书
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 成熟的工业标准
|
||||
- 支持证书撤销(CRL/OCSP)
|
||||
- 有层级信任关系
|
||||
|
||||
**缺点:**
|
||||
- 需要信任中心化 CA
|
||||
- 证书管理复杂
|
||||
- 跨 CA 互操作需要桥接
|
||||
|
||||
**适用场景**:企业内部、有明确信任中心的场景
|
||||
|
||||
#### 3.2.3 方案 C:区块链 DID(去中心化身份)
|
||||
|
||||
**核心思想**:身份上链,链上存公钥和属性,类似 W3C DID 标准。
|
||||
|
||||
```
|
||||
链上存储:
|
||||
DID → {
|
||||
"publicKey": "...",
|
||||
"controller": "...",
|
||||
"authentication": [...],
|
||||
"service": [...],
|
||||
"revoked": false,
|
||||
"timestamp": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**三种实现路径:**
|
||||
|
||||
| 路径 | 说明 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| **公链 DID** | 使用以太坊/Solana 等公链 | 开放生态、跨组织协作 |
|
||||
| **联盟链 DID** | 多机构共建联盟链 | 行业联盟、政企合作 |
|
||||
| **私有链 DID** | 单组织内部链 | 企业内部、对性能要求高 |
|
||||
|
||||
**优点:**
|
||||
- 去中心化,无单点故障
|
||||
- 身份可验证、可追溯
|
||||
- 支持跨系统互认
|
||||
- 不可篡改的身份历史
|
||||
|
||||
**缺点:**
|
||||
- 链上操作有延迟
|
||||
- 需要支付 Gas 费(公链)
|
||||
- 实现复杂度高
|
||||
- 隐私保护需额外设计
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Layer 2:身份验证方案
|
||||
|
||||
#### 3.3.1 消息签名验证
|
||||
|
||||
所有灵隙消息必须签名,签名方式:
|
||||
|
||||
```
|
||||
签名内容 = SHA256(message_id + sender + receiver + payload + timestamp)
|
||||
签名 = Sign(private_key, 签名内容)
|
||||
验证 = Verify(public_key, 签名内容, 签名)
|
||||
```
|
||||
|
||||
#### 3.3.2 签名放置位置
|
||||
|
||||
| 位置 | 说明 |
|
||||
|------|------|
|
||||
| 消息体内 | `signature` 字段 |
|
||||
| HTTP Header | `X-Synapse-Signature` |
|
||||
| gRPC Metadata | `x-synapse-signature` |
|
||||
|
||||
#### 3.3.3 防重放攻击
|
||||
|
||||
```
|
||||
每个消息有唯一 message_id + timestamp
|
||||
接收方维护已处理 message_id 缓存(如最近 1 小时)
|
||||
timestamp 与本地时间差超过 5 分钟 → 拒绝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Layer 3:信任评分体系
|
||||
|
||||
#### 3.4.1 信任分维度
|
||||
|
||||
| 维度 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 任务完成率 | 30% | 历史任务成功 / 总任务数 |
|
||||
| 任务质量 | 25% | 结果评分、返工率 |
|
||||
| 响应速度 | 15% | 平均响应时间 |
|
||||
| 在线稳定性 | 15% | 在线时长 / 总时长 |
|
||||
| 历史信誉 | 15% | 其他 Agent 的评价 |
|
||||
|
||||
#### 3.4.2 信任分计算
|
||||
|
||||
```
|
||||
trust_score = Σ(维度_i × 权重_i)
|
||||
|
||||
范围:0 - 100
|
||||
等级:
|
||||
90-100 S 级 高度可信
|
||||
75-89 A 级 可信
|
||||
60-74 B 级 一般可信
|
||||
40-59 C 级 谨慎使用
|
||||
0-39 D 级 不可信
|
||||
```
|
||||
|
||||
#### 3.4.3 信任传递
|
||||
|
||||
```
|
||||
A 信任 B(评分 90)
|
||||
B 信任 C(评分 85)
|
||||
A 对 C 的初始信任 = 90 × 85% × 衰减因子 = 约 68
|
||||
```
|
||||
|
||||
**注意**:信任传递有衰减,防止信任污染。
|
||||
|
||||
---
|
||||
|
||||
### 3.5 区块链适用性决策树
|
||||
|
||||
```
|
||||
是否需要跨组织协作?
|
||||
├── 否 → 用 PKI 或公钥身份(简单高效)
|
||||
└── 是 → 是否有共同信任的中心机构?
|
||||
├── 是 → 用 PKI 体系(成熟可靠)
|
||||
└── 否 → 是否需要不可篡改的审计?
|
||||
├── 是 → 考虑联盟链 DID
|
||||
└── 否 → 公钥身份 + 声誉系统可能够用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 方案三:协议缺失内容补全
|
||||
|
||||
### 4.1 第一优先级补全(核心基础设施)
|
||||
|
||||
#### 4.1.1 Agent 发现协议(ADS - Agent Discovery Service)
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 方向 | 说明 |
|
||||
|---------|------|------|
|
||||
| `dir:register` | Agent → Directory | 注册 Agent |
|
||||
| `dir:unregister` | Agent → Directory | 注销 Agent |
|
||||
| `dir:heartbeat` | Agent → Directory | 心跳 |
|
||||
| `dir:update_capabilities` | Agent → Directory | 更新能力 |
|
||||
| `dir:query_by_id` | Agent → Directory | 按 ID 查询 |
|
||||
| `dir:query_by_capability` | Agent → Directory | 按能力查询 |
|
||||
| `dir:list` | Agent → Directory | 列表查询 |
|
||||
| `dir:query_result` | Directory → Agent | 查询结果 |
|
||||
|
||||
**新增数据结构:** `AgentDirectoryEntry`, `CapabilityIndex`, `QueryFilter`
|
||||
|
||||
#### 4.1.2 身份认证规范
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `auth:challenge` | 身份挑战 |
|
||||
| `auth:response` | 挑战响应 |
|
||||
| `auth:cert_exchange` | 证书交换 |
|
||||
|
||||
**新增内容:**
|
||||
- 标准签名算法列表(Ed25519, ECDSA P-256, RSA 2048)
|
||||
- 消息签名格式规范
|
||||
- 防重放攻击机制
|
||||
- 身份验证流程规范
|
||||
|
||||
#### 4.1.3 心跳与存活检测
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `net:ping` | 存活探测 |
|
||||
| `net:pong` | 存活响应 |
|
||||
| `net:offline_notify` | 下线通知 |
|
||||
|
||||
**规范内容:**
|
||||
- 心跳间隔(默认 30 秒)
|
||||
- 超时判定(连续 3 次未收到 → 离线)
|
||||
- 离线通知机制
|
||||
|
||||
#### 4.1.4 统一错误码体系
|
||||
|
||||
**错误分类:**
|
||||
|
||||
| 错误码前缀 | 类别 | 示例 |
|
||||
|-----------|------|------|
|
||||
| `msg_` | 消息层 | msg_invalid_format, msg_signature_invalid |
|
||||
| `auth_` | 认证层 | auth_failed, auth_expired, auth_forbidden |
|
||||
| `task_` | 任务层 | task_not_found, task_timeout, task_cancelled |
|
||||
| `cap_` | 能力层 | cap_lacking, cap_busy, cap_not_supported |
|
||||
| `group_` | 群组层 | group_not_found, group_full, group_permission_denied |
|
||||
| `dir_` | 目录层 | dir_agent_not_found, dir_query_failed |
|
||||
| `net_` | 网络层 | net_timeout, net_offline, net_unreachable |
|
||||
| `sys_` | 系统层 | sys_internal_error, sys_not_implemented |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 第二优先级补全(重要功能)
|
||||
|
||||
#### 4.2.1 能力匹配与任务路由
|
||||
|
||||
**功能:**
|
||||
- 智能选择最合适的 Agent 执行任务
|
||||
- 支持负载均衡(按并发数、响应速度、信任分)
|
||||
- 支持任务失败自动重试/转发
|
||||
|
||||
**新增消息类型:**
|
||||
- `router:submit_task` - 提交给路由器自动选择执行者
|
||||
- `router:forward` - 任务转发
|
||||
|
||||
#### 4.2.2 全网广播协议
|
||||
|
||||
**新增消息类型:**
|
||||
|
||||
| 消息类型 | 说明 |
|
||||
|---------|------|
|
||||
| `global:broadcast` | 全网广播 |
|
||||
| `system:broadcast` | 系统内广播 |
|
||||
| `cap:broadcast` | 能力组广播 |
|
||||
|
||||
**规范内容:**
|
||||
- 广播范围定义
|
||||
- TTL 与去重机制
|
||||
- 广播风暴防护
|
||||
|
||||
#### 4.2.3 版本协商机制
|
||||
|
||||
**功能:**
|
||||
- Agent 之间协商使用的协议版本
|
||||
- 向后兼容处理
|
||||
- 能力降级策略
|
||||
|
||||
**新增消息类型:**
|
||||
- `meta:version_query` - 查询支持的版本
|
||||
- `meta:version_negotiate` - 版本协商
|
||||
|
||||
#### 4.2.4 幂等性保证
|
||||
|
||||
**规范内容:**
|
||||
- 幂等键(Idempotency Key)机制
|
||||
- 重复消息处理规范
|
||||
- 任务级幂等 vs 消息级幂等
|
||||
|
||||
---
|
||||
|
||||
### 4.3 第三优先级补全(增强功能)
|
||||
|
||||
#### 4.3.1 流式输出协议
|
||||
|
||||
**功能:**
|
||||
- 长任务实时进度推送
|
||||
- 生成式 AI 的 token 流式输出
|
||||
- WebSocket/SSE 传输支持
|
||||
|
||||
#### 4.3.2 批量任务协议
|
||||
|
||||
**功能:**
|
||||
- 一次性提交多个同类型任务
|
||||
- 批量状态查询
|
||||
- 批量结果收集
|
||||
|
||||
#### 4.3.3 支付与激励机制
|
||||
|
||||
**功能:**
|
||||
- 任务定价与结算
|
||||
- Token 或积分系统
|
||||
- 智能合约自动结算(区块链场景)
|
||||
|
||||
#### 4.3.4 群组发现协议
|
||||
|
||||
**功能:**
|
||||
- 按主题/任务类型查找群组
|
||||
- 推荐加入合适的群组
|
||||
- 群组热度/活跃度排名
|
||||
|
||||
---
|
||||
|
||||
### 4.4 安全相关补全
|
||||
|
||||
#### 4.4.1 端到端加密
|
||||
|
||||
**规范内容:**
|
||||
- 密钥协商(ECDH)
|
||||
- 对称加密载荷(AES-GCM)
|
||||
- 前向保密(PFS)
|
||||
|
||||
#### 4.4.2 权限委托
|
||||
|
||||
**功能:**
|
||||
- Agent A 委托 Agent B 代表自己执行任务
|
||||
- 委托令牌(Delegation Token)
|
||||
- 委托范围与时效控制
|
||||
|
||||
#### 4.4.3 审计日志标准
|
||||
|
||||
**规范内容:**
|
||||
- 统一审计日志格式
|
||||
- 关键操作必须记录
|
||||
- 日志完整性保护(哈希链)
|
||||
|
||||
---
|
||||
|
||||
## 5. 分阶段实施路线图
|
||||
|
||||
### 5.1 Phase 0:基础版(MVP) - 预计 2 周
|
||||
|
||||
**目标**:协议可用,能跑通基本流程
|
||||
|
||||
**内容:**
|
||||
- [x] 灵隙点对点任务协议(已完成)
|
||||
- [x] A2N 群组协作协议(已完成)
|
||||
- [x] P2A 外部调用协议(已完成)
|
||||
- [x] 三套协议的 SDK 实现(已完成)
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Phase 1:发现与身份 - 预计 4 周
|
||||
|
||||
**目标**:Agent 能互相找到对方,能验证对方身份
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| Agent 目录服务协议 | 集中式 ADS | P0 |
|
||||
| 公钥身份规范 | Ed25519 签名验证 | P0 |
|
||||
| 心跳与存活检测 | 30 秒心跳,3 次超时判定离线 | P0 |
|
||||
| 统一错误码体系 | 8 大类错误码 | P0 |
|
||||
| 防重放攻击 | message_id + timestamp | P0 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`06-agent-discovery.md`
|
||||
- 新协议文档:`07-authentication.md`
|
||||
- 代码:`discovery/` 包(目录服务客户端)
|
||||
- 代码:`auth/` 包(签名验证工具)
|
||||
- 更新 types 包:新增消息类型和错误码
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Phase 2:路由与广播 - 预计 4 周
|
||||
|
||||
**目标**:支持智能任务路由和全网广播
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 任务路由协议 | 按能力 + 负载 + 信任分选择执行者 | P1 |
|
||||
| 全网广播协议 | 基于目录服务的广播转发 | P1 |
|
||||
| 版本协商机制 | 协议版本兼容 | P1 |
|
||||
| 幂等性保证 | 幂等键 + 去重 | P1 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`08-routing-and-broadcast.md`
|
||||
- 代码:`routing/` 包增强(增加能力路由)
|
||||
- 更新目录服务:支持广播转发
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Phase 3:信任与激励 - 预计 6 周
|
||||
|
||||
**目标**:建立信任体系,支持规模化协作
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 信任评分体系 | 5 维度信任分计算 | P2 |
|
||||
| 流式输出协议 | WebSocket/SSE 流式进度 | P2 |
|
||||
| 批量任务协议 | 批量提交/查询/收集 | P2 |
|
||||
| 群组发现协议 | 群组搜索与推荐 | P2 |
|
||||
| SLA 协议 | 服务等级承诺 | P3 |
|
||||
|
||||
**输出物:**
|
||||
- 新协议文档:`09-trust-and-reputation.md`
|
||||
- 代码:`trust/` 包(信任评分引擎)
|
||||
|
||||
---
|
||||
|
||||
### 5.5 Phase 4:高级特性 - 预计 8 周+
|
||||
|
||||
**目标**:企业级、生产级完整功能
|
||||
|
||||
**新增协议模块:**
|
||||
|
||||
| 模块 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 端到端加密 | ECDH + AES-GCM | P3 |
|
||||
| 权限委托 | 委托令牌机制 | P3 |
|
||||
| 支付与激励 | Token/积分系统 | P3 |
|
||||
| 区块链 DID 集成 | 可选模块 | P4 |
|
||||
| 审计日志标准 | 统一审计格式 | P3 |
|
||||
| Gossip 协议 | 分布式发现(可选) | P4 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术选型建议
|
||||
|
||||
### 6.1 目录服务存储选型
|
||||
|
||||
| 方案 | 适用场景 | 优点 | 缺点 |
|
||||
|------|---------|------|------|
|
||||
| **Redis** | 中小规模 | 高性能,支持 TTL | 数据量受限 |
|
||||
| **PostgreSQL** | 中大规模 | 复杂查询,持久化 | 需运维 |
|
||||
| **Elasticsearch** | 大规模 + 全文检索 | 强大的搜索能力 | 运维复杂 |
|
||||
|
||||
**推荐**:先用 Redis(简单快),规模大了再迁 PostgreSQL。
|
||||
|
||||
---
|
||||
|
||||
### 6.2 签名算法选型
|
||||
|
||||
| 算法 | 安全性 | 性能 | 密钥长度 | 推荐度 |
|
||||
|------|--------|------|---------|--------|
|
||||
| Ed25519 | 高 | 快 | 256 位 | ⭐⭐⭐⭐⭐ 首选 |
|
||||
| ECDSA P-256 | 高 | 中 | 256 位 | ⭐⭐⭐⭐ 兼容 |
|
||||
| RSA 2048 | 中 | 慢 | 2048 位 | ⭐⭐ 仅兼容 |
|
||||
|
||||
**推荐**:默认 Ed25519,需要兼容旧系统时支持 ECDSA。
|
||||
|
||||
---
|
||||
|
||||
### 6.3 区块链选型(如果需要)
|
||||
|
||||
| 场景 | 推荐方案 | 说明 |
|
||||
|------|---------|------|
|
||||
| 企业内部 | **私有链(Hyperledger Fabric)** | 可控,高性能,无 Gas 费 |
|
||||
| 行业联盟 | **联盟链(FISCO BCOS / ChainMaker)** | 多机构共建,合规 |
|
||||
| 开放生态 | **公链(以太坊 / Solana)** | 完全开放,需 Gas 费 |
|
||||
| 简单原型 | **模拟链(数据库模拟)** | 先验证逻辑,再上真链 |
|
||||
|
||||
**推荐**:90% 的场景用不到真区块链,先用数据库模拟验证业务逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结与建议
|
||||
|
||||
### 7.1 核心建议
|
||||
|
||||
1. **先集中后分布**:先用集中式目录服务,简单高效;规模大了再考虑分布式
|
||||
2. **先公钥后区块链**:先用公钥身份解决 80% 的问题,真的需要了再上链
|
||||
3. **先 MVP 后完善**:先跑通核心流程,再逐步加高级功能
|
||||
4. **标准接口可替换**:目录服务、身份系统都做成可插拔的,方便未来升级
|
||||
|
||||
### 7.2 下一步行动
|
||||
|
||||
| 序号 | 行动 | 负责人 | 产出 |
|
||||
|------|------|--------|------|
|
||||
| 1 | 评审本方案,确认 Phase 1 范围 | 架构组 | 确认后的方案 |
|
||||
| 2 | 设计目录服务 API 详细规范 | 协议组 | `06-agent-discovery.md` |
|
||||
| 3 | 设计身份认证详细规范 | 协议组 | `07-authentication.md` |
|
||||
| 4 | 实现目录服务原型 | 开发组 | 可运行的 ADS 服务 |
|
||||
| 5 | 更新 SDK 支持发现和认证 | 开发组 | 新版 SDK |
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文档索引
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [01-synapse-raw-protocol.md](./01-synapse-raw-protocol.md) | 灵隙原始协议(Synapse Raw Protocol)(纲领) |
|
||||
| [02-a2a-protocol.md](./02-a2a-protocol.md) | A2A 协议(P2P 任务) |
|
||||
| [03-a2n-protocol.md](./03-a2n-protocol.md) | A2N 协议(群组协作) |
|
||||
| [04-p2a-protocol.md](./04-p2a-protocol.md) | P2A 协议(外部调用) |
|
||||
| [05-sdk-implementation.md](./05-sdk-implementation.md) | SDK 实现说明 |
|
||||
| **本文档** | **解决方案白皮书** |
|
||||
611
transport/multipart_polling.go
Normal file
611
transport/multipart_polling.go
Normal file
@@ -0,0 +1,611 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Multipart Transport - 混合文本+文件传输
|
||||
// =============================================================================
|
||||
|
||||
// MultipartTransport handles multipart file transfers
|
||||
type MultipartTransport struct {
|
||||
mu sync.RWMutex
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
authSecret string
|
||||
}
|
||||
|
||||
// NewMultipartTransport creates a new multipart transport
|
||||
func NewMultipartTransport(timeout time.Duration, authSecret string) *MultipartTransport {
|
||||
return &MultipartTransport{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
timeout: timeout,
|
||||
authSecret: authSecret,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Multipart Message Sending
|
||||
// =============================================================================
|
||||
|
||||
// SendTaskWithFiles sends a task request with file attachments via multipart/form-data
|
||||
func (m *MultipartTransport) SendTaskWithFiles(
|
||||
ctx context.Context,
|
||||
endpoint string,
|
||||
msg *types.UnifiedSynapseMessage,
|
||||
files []types.FileContent,
|
||||
) error {
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
// 1. Write the main request JSON as the "request" part
|
||||
requestPart, err := writer.CreateFormField("request")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request part: %w", err)
|
||||
}
|
||||
if err := json.NewEncoder(requestPart).Encode(msg); err != nil {
|
||||
return fmt.Errorf("failed to encode request: %w", err)
|
||||
}
|
||||
|
||||
// 2. Write each file as a separate part
|
||||
for _, file := range files {
|
||||
if file.TransferMode != types.TransferInline {
|
||||
continue // Skip URL-based files
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile(file.Name, file.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file part %s: %w", file.Name, err)
|
||||
}
|
||||
|
||||
// For inline files, data is base64 encoded in file.Data
|
||||
// In real implementation, decode base64 and write binary
|
||||
// For now, just write the data as-is
|
||||
if file.Data != "" {
|
||||
part.Write([]byte(file.Data))
|
||||
}
|
||||
}
|
||||
|
||||
// Close the writer to finalize the boundary
|
||||
if err := writer.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close writer: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create HTTP request
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, &buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-Synapse-Version", "1.0.0")
|
||||
req.Header.Set("X-Synapse-Agent", msg.Sender.AgentID)
|
||||
req.Header.Set("X-Synapse-System", msg.Sender.SystemID)
|
||||
|
||||
// 4. Send
|
||||
resp, err := m.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
log.Printf("[Multipart] Task sent with %d files to %s", len(files), endpoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Multipart Message Parsing
|
||||
// =============================================================================
|
||||
|
||||
// ParseMultipartRequest parses a multipart request with message + files
|
||||
func (m *MultipartTransport) ParseMultipartRequest(r *http.Request) (
|
||||
*types.UnifiedSynapseMessage,
|
||||
[]types.FileContent,
|
||||
error,
|
||||
) {
|
||||
// Limit max size (100MB default)
|
||||
if err := r.ParseMultipartForm(100 << 20); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse multipart: %w", err)
|
||||
}
|
||||
|
||||
// 1. Parse the message JSON
|
||||
requestValue := r.FormValue("request")
|
||||
if requestValue == "" {
|
||||
return nil, nil, fmt.Errorf("missing 'request' field")
|
||||
}
|
||||
|
||||
var msg types.UnifiedSynapseMessage
|
||||
if err := json.Unmarshal([]byte(requestValue), &msg); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid request JSON: %w", err)
|
||||
}
|
||||
|
||||
// 2. Parse file attachments
|
||||
var files []types.FileContent
|
||||
for _, fileHeaders := range r.MultipartForm.File {
|
||||
for _, fh := range fileHeaders {
|
||||
file, err := fh.Open()
|
||||
if err != nil {
|
||||
log.Printf("[Multipart] Failed to open file %s: %v", fh.Filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
log.Printf("[Multipart] Failed to read file %s: %v", fh.Filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
mimeType := fh.Header.Get("Content-Type")
|
||||
if mimeType == "" {
|
||||
mimeType = types.InferMimeType(fh.Filename)
|
||||
}
|
||||
|
||||
fc := types.FileContent{
|
||||
ContentBase: types.ContentBase{
|
||||
MIMEType: mimeType,
|
||||
Encoding: types.EncodingBase64,
|
||||
Size: fh.Size,
|
||||
TransferMode: types.TransferInline,
|
||||
},
|
||||
Name: fh.Filename,
|
||||
Data: string(data), // In real impl: base64 encode
|
||||
}
|
||||
files = append(files, fc)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[Multipart] Parsed request: %d files received", len(files))
|
||||
return &msg, files, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Polling Client - 轮询客户端
|
||||
// =============================================================================
|
||||
|
||||
// PollingClient handles task status polling
|
||||
type PollingClient struct {
|
||||
mu sync.RWMutex
|
||||
client *http.Client
|
||||
baseURL string
|
||||
authSecret string
|
||||
|
||||
// Polling configuration
|
||||
initialInterval time.Duration
|
||||
maxInterval time.Duration
|
||||
backoffFactor float64
|
||||
maxDuration time.Duration
|
||||
|
||||
// Active polls
|
||||
activePolls map[string]*pollContext
|
||||
}
|
||||
|
||||
type pollContext struct {
|
||||
taskID string
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
interval time.Duration
|
||||
lastStatus string
|
||||
lastProgress int
|
||||
callback func(status *types.TaskInfo) bool // returns true to stop polling
|
||||
}
|
||||
|
||||
// PollingClientConfig holds configuration for the polling client
|
||||
type PollingClientConfig struct {
|
||||
BaseURL string
|
||||
AuthSecret string
|
||||
InitialInterval time.Duration
|
||||
MaxInterval time.Duration
|
||||
BackoffFactor float64
|
||||
MaxDuration time.Duration
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// DefaultPollingClientConfig returns default polling configuration
|
||||
func DefaultPollingClientConfig() *PollingClientConfig {
|
||||
return &PollingClientConfig{
|
||||
InitialInterval: 5 * time.Second,
|
||||
MaxInterval: 60 * time.Second,
|
||||
BackoffFactor: 2.0,
|
||||
MaxDuration: 2 * time.Hour,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPollingClient creates a new polling client
|
||||
func NewPollingClient(config *PollingClientConfig) *PollingClient {
|
||||
if config == nil {
|
||||
config = DefaultPollingClientConfig()
|
||||
}
|
||||
return &PollingClient{
|
||||
client: &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
},
|
||||
baseURL: config.BaseURL,
|
||||
authSecret: config.AuthSecret,
|
||||
initialInterval: config.InitialInterval,
|
||||
maxInterval: config.MaxInterval,
|
||||
backoffFactor: config.BackoffFactor,
|
||||
maxDuration: config.MaxDuration,
|
||||
activePolls: make(map[string]*pollContext),
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Single Poll - 单次轮询
|
||||
// =============================================================================
|
||||
|
||||
// GetTaskStatus polls for a task's current status
|
||||
func (p *PollingClient) GetTaskStatus(ctx context.Context, taskID string) (*types.TaskInfo, error) {
|
||||
url := fmt.Sprintf("%s/synapse/task/%s", p.baseURL, taskID)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Synapse-Version", "1.0.0")
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 304 {
|
||||
return nil, nil // Not modified
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result types.TaskStatusResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
return nil, fmt.Errorf("API error: %s", result.Error.Message)
|
||||
}
|
||||
|
||||
return result.Task, nil
|
||||
}
|
||||
|
||||
// ListTasks lists tasks with filtering
|
||||
func (p *PollingClient) ListTasks(ctx context.Context, status string, limit, offset int) (*types.TaskListResponse, error) {
|
||||
url := fmt.Sprintf("%s/synapse/tasks?status=%s&limit=%d&offset=%d",
|
||||
p.baseURL, status, limit, offset)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result types.TaskListResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetTaskEvents gets incremental task events
|
||||
func (p *PollingClient) GetTaskEvents(ctx context.Context, taskID, since string) (*types.TaskEventsResponse, error) {
|
||||
url := fmt.Sprintf("%s/synapse/task/%s/events", p.baseURL, taskID)
|
||||
if since != "" {
|
||||
url += fmt.Sprintf("?since=%s", since)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result types.TaskEventsResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Continuous Polling - 持续轮询
|
||||
// =============================================================================
|
||||
|
||||
// StartPolling starts continuous polling for a task
|
||||
func (p *PollingClient) StartPolling(
|
||||
ctx context.Context,
|
||||
taskID string,
|
||||
callback func(status *types.TaskInfo) bool,
|
||||
) error {
|
||||
p.mu.Lock()
|
||||
if _, exists := p.activePolls[taskID]; exists {
|
||||
p.mu.Unlock()
|
||||
return fmt.Errorf("polling already active for task %s", taskID)
|
||||
}
|
||||
|
||||
pollCtx, cancel := context.WithTimeout(ctx, p.maxDuration)
|
||||
pc := &pollContext{
|
||||
taskID: taskID,
|
||||
ctx: pollCtx,
|
||||
cancel: cancel,
|
||||
interval: p.initialInterval,
|
||||
callback: callback,
|
||||
}
|
||||
p.activePolls[taskID] = pc
|
||||
p.mu.Unlock()
|
||||
|
||||
go p.pollLoop(pc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopPolling stops polling for a task
|
||||
func (p *PollingClient) StopPolling(taskID string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if pc, exists := p.activePolls[taskID]; exists {
|
||||
pc.cancel()
|
||||
delete(p.activePolls, taskID)
|
||||
}
|
||||
}
|
||||
|
||||
// pollLoop is the main polling loop
|
||||
func (p *PollingClient) pollLoop(pc *pollContext) {
|
||||
defer func() {
|
||||
p.mu.Lock()
|
||||
delete(p.activePolls, pc.taskID)
|
||||
p.mu.Unlock()
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(pc.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
consecutiveNoChange := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pc.ctx.Done():
|
||||
log.Printf("[Polling] Stopped for task %s: %v", pc.taskID, pc.ctx.Err())
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
status, err := p.GetTaskStatus(pc.ctx, pc.taskID)
|
||||
if err != nil {
|
||||
log.Printf("[Polling] Error for task %s: %v", pc.taskID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if status == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if status or progress changed
|
||||
changed := status.Status != pc.lastStatus || status.Progress != pc.lastProgress
|
||||
if changed {
|
||||
consecutiveNoChange = 0
|
||||
pc.interval = p.initialInterval
|
||||
ticker.Reset(pc.interval)
|
||||
} else {
|
||||
consecutiveNoChange++
|
||||
// Apply backoff after 3 consecutive no-change polls
|
||||
if consecutiveNoChange >= 3 {
|
||||
newInterval := time.Duration(float64(pc.interval) * p.backoffFactor)
|
||||
if newInterval > p.maxInterval {
|
||||
newInterval = p.maxInterval
|
||||
}
|
||||
if newInterval != pc.interval {
|
||||
pc.interval = newInterval
|
||||
ticker.Reset(pc.interval)
|
||||
log.Printf("[Polling] Backoff to %v for task %s", pc.interval, pc.taskID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pc.lastStatus = status.Status
|
||||
pc.lastProgress = status.Progress
|
||||
|
||||
// Call callback
|
||||
if pc.callback(status) {
|
||||
log.Printf("[Polling] Callback returned stop for task %s", pc.taskID)
|
||||
return
|
||||
}
|
||||
|
||||
// Check terminal states
|
||||
if status.Status == "completed" || status.Status == "failed" ||
|
||||
status.Status == "cancelled" || status.Status == "rejected" {
|
||||
log.Printf("[Polling] Task %s reached terminal state: %s", pc.taskID, status.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Capability Query - 能力查询(同步)
|
||||
// =============================================================================
|
||||
|
||||
// QueryCapabilities queries an agent's capabilities synchronously
|
||||
func (p *PollingClient) QueryCapabilities(ctx context.Context, baseURL string) (*types.AgentCapabilities, error) {
|
||||
url := fmt.Sprintf("%s/synapse/capabilities", baseURL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result types.CapabilitiesResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
return nil, fmt.Errorf("API error: %s", result.Error.Message)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// HealthCheck checks if an agent is reachable
|
||||
func (p *PollingClient) HealthCheck(ctx context.Context, baseURL string) (bool, error) {
|
||||
url := fmt.Sprintf("%s/synapse/health", baseURL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return resp.StatusCode == 200, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hybrid Mode - 混合模式 (Callback + Polling Fallback)
|
||||
// =============================================================================
|
||||
|
||||
// HybridTransport combines callback-based push with polling-based fallback
|
||||
type HybridTransport struct {
|
||||
callbackTransport *HTTPTransport
|
||||
pollingClient *PollingClient
|
||||
mu sync.RWMutex
|
||||
pendingTasks map[string]*hybridTaskState
|
||||
}
|
||||
|
||||
type hybridTaskState struct {
|
||||
taskID string
|
||||
callbackOK bool
|
||||
pollingActive bool
|
||||
lastStatus string
|
||||
}
|
||||
|
||||
// NewHybridTransport creates a new hybrid transport
|
||||
func NewHybridTransport(callback *HTTPTransport, polling *PollingClient) *HybridTransport {
|
||||
return &HybridTransport{
|
||||
callbackTransport: callback,
|
||||
pollingClient: polling,
|
||||
pendingTasks: make(map[string]*hybridTaskState),
|
||||
}
|
||||
}
|
||||
|
||||
// SendTaskWithHybridFallback sends a task request and sets up both callback and polling
|
||||
func (h *HybridTransport) SendTaskWithHybridFallback(
|
||||
ctx context.Context,
|
||||
endpoint string,
|
||||
msg *types.UnifiedSynapseMessage,
|
||||
callback func(status *types.TaskInfo),
|
||||
) error {
|
||||
// 1. First, try callback-based approach
|
||||
err := h.callbackTransport.Send(ctx, endpoint, msg)
|
||||
if err != nil {
|
||||
log.Printf("[Hybrid] Direct send failed, using polling only: %v", err)
|
||||
}
|
||||
|
||||
// 2. Extract task ID from payload
|
||||
var payload types.TaskRequestPayload
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return fmt.Errorf("failed to extract task ID: %w", err)
|
||||
}
|
||||
taskID := payload.Task.ID
|
||||
|
||||
// 3. Set up polling as fallback after 30s
|
||||
h.mu.Lock()
|
||||
h.pendingTasks[taskID] = &hybridTaskState{
|
||||
taskID: taskID,
|
||||
callbackOK: err == nil,
|
||||
pollingActive: false,
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
// Start polling after a delay if callback hasn't been received
|
||||
go func() {
|
||||
select {
|
||||
case <-time.After(30 * time.Second):
|
||||
h.mu.RLock()
|
||||
state, exists := h.pendingTasks[taskID]
|
||||
h.mu.RUnlock()
|
||||
|
||||
if exists && !state.callbackOK {
|
||||
log.Printf("[Hybrid] Callback not received for %s, starting polling fallback", taskID)
|
||||
h.pollingClient.StartPolling(ctx, taskID, func(status *types.TaskInfo) bool {
|
||||
if callback != nil {
|
||||
callback(status)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyCallbackReceived notifies that a callback was received (stops polling fallback)
|
||||
func (h *HybridTransport) NotifyCallbackReceived(taskID string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if state, exists := h.pendingTasks[taskID]; exists {
|
||||
state.callbackOK = true
|
||||
if state.pollingActive {
|
||||
h.pollingClient.StopPolling(taskID)
|
||||
state.pollingActive = false
|
||||
}
|
||||
log.Printf("[Hybrid] Callback received for %s, polling disabled", taskID)
|
||||
}
|
||||
}
|
||||
420
transport/transport.go
Normal file
420
transport/transport.go
Normal file
@@ -0,0 +1,420 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"synapse/types"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Transport
|
||||
// =============================================================================
|
||||
|
||||
// HTTPTransport implements HTTP-based transport for Synapse messages
|
||||
type HTTPTransport struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// Configuration
|
||||
baseURL string
|
||||
timeout time.Duration
|
||||
maxRetries int
|
||||
retryDelay time.Duration
|
||||
|
||||
// HTTP Client
|
||||
client *http.Client
|
||||
|
||||
// Security
|
||||
authSecret string
|
||||
|
||||
// Callbacks
|
||||
callbackEndpoint string
|
||||
callbackHandler MessageCallbackHandler
|
||||
}
|
||||
|
||||
// HTTPTransportConfig holds HTTP transport configuration
|
||||
type HTTPTransportConfig struct {
|
||||
BaseURL string
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
RetryDelay time.Duration
|
||||
AuthSecret string
|
||||
}
|
||||
|
||||
// DefaultHTTPTransportConfig returns default HTTP transport configuration
|
||||
func DefaultHTTPTransportConfig() *HTTPTransportConfig {
|
||||
return &HTTPTransportConfig{
|
||||
Timeout: 30 * time.Second,
|
||||
MaxRetries: 5,
|
||||
RetryDelay: 2 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// NewHTTPTransport creates a new HTTP transport
|
||||
func NewHTTPTransport(config *HTTPTransportConfig) *HTTPTransport {
|
||||
if config == nil {
|
||||
config = DefaultHTTPTransportConfig()
|
||||
}
|
||||
|
||||
t := &HTTPTransport{
|
||||
baseURL: config.BaseURL,
|
||||
timeout: config.Timeout,
|
||||
maxRetries: config.MaxRetries,
|
||||
retryDelay: config.RetryDelay,
|
||||
authSecret: config.AuthSecret,
|
||||
client: &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
},
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// SetCallbackHandler sets the handler for incoming callbacks
|
||||
func (t *HTTPTransport) SetCallbackHandler(handler MessageCallbackHandler) {
|
||||
t.callbackHandler = handler
|
||||
}
|
||||
|
||||
// SetCallbackEndpoint sets the endpoint for receiving callbacks
|
||||
func (t *HTTPTransport) SetCallbackEndpoint(endpoint string) {
|
||||
t.callbackEndpoint = endpoint
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Message Sending
|
||||
// =============================================================================
|
||||
|
||||
// Send sends a message via HTTP POST
|
||||
func (t *HTTPTransport) Send(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) error {
|
||||
if endpoint == "" {
|
||||
return fmt.Errorf("endpoint is required")
|
||||
}
|
||||
|
||||
// Add signature if secret is configured
|
||||
if t.authSecret != "" {
|
||||
t.signMessage(msg)
|
||||
}
|
||||
|
||||
// Marshal message
|
||||
body, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Synapse-Version", "1.0.0")
|
||||
req.Header.Set("X-Synapse-Agent", msg.Sender.AgentID)
|
||||
req.Header.Set("X-Synapse-System", msg.Sender.SystemID)
|
||||
|
||||
if msg.Signature != "" {
|
||||
req.Header.Set("X-Synapse-Signature", msg.Signature)
|
||||
}
|
||||
|
||||
// Send with retries
|
||||
var lastErr error
|
||||
for i := 0; i < t.maxRetries; i++ {
|
||||
if i > 0 {
|
||||
delay := t.retryDelay * time.Duration(1<<uint(i-1))
|
||||
log.Printf("[Transport] Retrying request (%d/%d) after %v", i, t.maxRetries, delay)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
log.Printf("[Transport] Request failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Read response
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
log.Printf("[Transport] Message sent successfully: %s to %s", msg.ID, endpoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||
log.Printf("[Transport] Request failed: %v", lastErr)
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to send message after %d retries: %w", t.maxRetries, lastErr)
|
||||
}
|
||||
|
||||
// SendWithCallback sends a message and waits for callback response
|
||||
func (t *HTTPTransport) SendWithCallback(
|
||||
ctx context.Context,
|
||||
endpoint string,
|
||||
msg *types.UnifiedSynapseMessage,
|
||||
timeout time.Duration,
|
||||
) (*types.UnifiedSynapseMessage, error) {
|
||||
// Create channel for response
|
||||
respChan := make(chan *types.UnifiedSynapseMessage, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
// Register callback handler for this message
|
||||
correlationID := msg.CorrelationID
|
||||
if correlationID == "" {
|
||||
correlationID = msg.ID
|
||||
}
|
||||
|
||||
// Start timeout goroutine
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
errChan <- ctx.Err()
|
||||
case <-time.After(timeout):
|
||||
errChan <- fmt.Errorf("timeout waiting for response")
|
||||
}
|
||||
}()
|
||||
|
||||
// Send message
|
||||
if err := t.Send(ctx, endpoint, msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait for response
|
||||
select {
|
||||
case resp := <-respChan:
|
||||
return resp, nil
|
||||
case err := <-errChan:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Message Reception
|
||||
// =============================================================================
|
||||
|
||||
// MessageCallbackHandler is the interface for handling incoming messages
|
||||
type MessageCallbackHandler interface {
|
||||
HandleMessage(msg *types.UnifiedSynapseMessage) error
|
||||
}
|
||||
|
||||
// HandleCallback handles an incoming callback message (HTTP POST)
|
||||
func (t *HTTPTransport) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Read body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
// Parse message
|
||||
var msg types.UnifiedSynapseMessage
|
||||
if err := json.Unmarshal(body, &msg); err != nil {
|
||||
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify signature if configured
|
||||
if t.authSecret != "" && msg.Signature != "" {
|
||||
if !t.verifySignature(body, msg.Signature) {
|
||||
http.Error(w, "invalid signature", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle message
|
||||
if t.callbackHandler != nil {
|
||||
if err := t.callbackHandler.HandleMessage(&msg); err != nil {
|
||||
log.Printf("[Transport] Failed to handle message: %v", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Send success response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "received",
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Server
|
||||
// =============================================================================
|
||||
|
||||
// Server is an HTTP server for Synapse transport
|
||||
type Server struct {
|
||||
transport *HTTPTransport
|
||||
mux *http.ServeMux
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// NewServer creates a new HTTP server for Synapse transport
|
||||
func NewServer(addr string, transport *HTTPTransport) *Server {
|
||||
mux := http.NewServeMux()
|
||||
server := &Server{
|
||||
transport: transport,
|
||||
mux: mux,
|
||||
server: &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
},
|
||||
}
|
||||
|
||||
// Register callback endpoint
|
||||
mux.HandleFunc("/synapse/callback", transport.HandleCallback)
|
||||
mux.HandleFunc("/synapse/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (s *Server) Start() error {
|
||||
log.Printf("[Transport] Starting HTTP server on %s", s.server.Addr)
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// Stop stops the HTTP server
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Security
|
||||
// =============================================================================
|
||||
|
||||
func (t *HTTPTransport) signMessage(msg *types.UnifiedSynapseMessage) {
|
||||
// Create canonical body (message without signature)
|
||||
msgCopy := *msg
|
||||
msgCopy.Signature = ""
|
||||
|
||||
body, _ := json.Marshal(msgCopy)
|
||||
|
||||
// Compute HMAC-SHA256
|
||||
h := hmac.New(sha256.New, []byte(t.authSecret))
|
||||
h.Write(body)
|
||||
signature := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
msg.Signature = signature
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) verifySignature(body []byte, signature string) bool {
|
||||
h := hmac.New(sha256.New, []byte(t.authSecret))
|
||||
h.Write(body)
|
||||
expected := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
return hmac.Equal([]byte(strings.ToLower(signature)), []byte(strings.ToLower(expected)))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Callback Sender (implements p2p.CallbackSender)
|
||||
// =============================================================================
|
||||
|
||||
// HTTPCallbackSender implements p2p.CallbackSender using HTTP transport
|
||||
type HTTPCallbackSender struct {
|
||||
transport *HTTPTransport
|
||||
}
|
||||
|
||||
// NewHTTPCallbackSender creates a new HTTP callback sender
|
||||
func NewHTTPCallbackSender(t *HTTPTransport) *HTTPCallbackSender {
|
||||
return &HTTPCallbackSender{transport: t}
|
||||
}
|
||||
|
||||
// SendCallback sends a callback message
|
||||
func (s *HTTPCallbackSender) SendCallback(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) error {
|
||||
if endpoint == "" && msg.Receiver != nil {
|
||||
endpoint = msg.Receiver.Endpoint
|
||||
}
|
||||
return s.transport.Send(ctx, endpoint, msg)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WebSocket Transport (for future extension)
|
||||
// =============================================================================
|
||||
|
||||
// WebSocketConfig holds WebSocket transport configuration
|
||||
type WebSocketConfig struct {
|
||||
URL string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// WebSocketTransport implements WebSocket-based transport
|
||||
type WebSocketTransport struct {
|
||||
mu sync.RWMutex
|
||||
config *WebSocketConfig
|
||||
handler MessageCallbackHandler
|
||||
}
|
||||
|
||||
// NewWebSocketTransport creates a new WebSocket transport
|
||||
func NewWebSocketTransport(config *WebSocketConfig) *WebSocketTransport {
|
||||
return &WebSocketTransport{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// SetMessageHandler sets the message handler
|
||||
func (t *WebSocketTransport) SetMessageHandler(handler MessageCallbackHandler) {
|
||||
t.handler = handler
|
||||
}
|
||||
|
||||
// Connect establishes a WebSocket connection
|
||||
func (t *WebSocketTransport) Connect(ctx context.Context) error {
|
||||
// Placeholder for WebSocket implementation
|
||||
// In a real implementation, this would use gorilla/websocket
|
||||
log.Printf("[Transport] WebSocket connect to %s (placeholder)", t.config.URL)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send sends a message via WebSocket
|
||||
func (t *WebSocketTransport) Send(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
||||
// Placeholder for WebSocket send implementation
|
||||
log.Printf("[Transport] WebSocket send (placeholder): %s", msg.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Message Utilities
|
||||
// =============================================================================
|
||||
|
||||
// CreateSuccessResponse creates a success response
|
||||
func CreateSuccessResponse(msgID string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "received",
|
||||
"id": msgID,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateErrorResponse creates an error response
|
||||
func CreateErrorResponse(code int, message string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"success": false,
|
||||
"error": map[string]interface{}{
|
||||
"code": code,
|
||||
"message": message,
|
||||
},
|
||||
}
|
||||
}
|
||||
389
types/content.go
Normal file
389
types/content.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package types
|
||||
|
||||
// =============================================================================
|
||||
// Transport Mode Definitions
|
||||
// =============================================================================
|
||||
|
||||
// TransportMode defines the transport mode for task communication
|
||||
type TransportMode string
|
||||
|
||||
const (
|
||||
TransportModeCallback TransportMode = "callback" // Async callback (webhook)
|
||||
TransportModePolling TransportMode = "polling" // Async polling
|
||||
TransportModeSync TransportMode = "sync" // Synchronous
|
||||
TransportModeHybrid TransportMode = "hybrid" // Callback + Polling fallback
|
||||
)
|
||||
|
||||
// TransportPreferences defines transport preferences for a task
|
||||
type TransportPreferences struct {
|
||||
Preferred TransportMode `json:"preferred"`
|
||||
Fallback TransportMode `json:"fallback,omitempty"`
|
||||
CallbackEndpoint string `json:"callback_endpoint,omitempty"`
|
||||
PollingInterval int `json:"polling_interval,omitempty"` // seconds
|
||||
ProgressUpdates bool `json:"progress_updates"`
|
||||
ProgressInterval int `json:"progress_interval,omitempty"` // seconds
|
||||
}
|
||||
|
||||
// DefaultTransportPreferences returns default transport preferences
|
||||
func DefaultTransportPreferences() *TransportPreferences {
|
||||
return &TransportPreferences{
|
||||
Preferred: TransportModeCallback,
|
||||
Fallback: TransportModePolling,
|
||||
PollingInterval: 5,
|
||||
ProgressUpdates: true,
|
||||
ProgressInterval: 10,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Content Type Definitions
|
||||
// =============================================================================
|
||||
|
||||
// MIME type constants
|
||||
const (
|
||||
MIMEJSON = "application/json"
|
||||
MIMETextPlain = "text/plain"
|
||||
MIMETextMarkdown = "text/markdown"
|
||||
MIMETextHTML = "text/html"
|
||||
MIMETextCSV = "text/csv"
|
||||
MIMEImageJPEG = "image/jpeg"
|
||||
MIMEImagePNG = "image/png"
|
||||
MIMEImageWebP = "image/webp"
|
||||
MIMEImageGIF = "image/gif"
|
||||
MIMEImageSVG = "image/svg+xml"
|
||||
MIMEVideoMP4 = "video/mp4"
|
||||
MIMEVideoWebM = "video/webm"
|
||||
MIMEAudioMP3 = "audio/mpeg"
|
||||
MIMEAudioWAV = "audio/wav"
|
||||
MIMEAudioAAC = "audio/aac"
|
||||
MIMEPDF = "application/pdf"
|
||||
MIMEOctetStream = "application/octet-stream"
|
||||
MIMEMultipartForm = "multipart/form-data"
|
||||
)
|
||||
|
||||
// ContentEncoding defines content encoding methods
|
||||
type ContentEncoding string
|
||||
|
||||
const (
|
||||
EncodingNone ContentEncoding = "none"
|
||||
EncodingBase64 ContentEncoding = "base64"
|
||||
EncodingGzip ContentEncoding = "gzip"
|
||||
EncodingDeflate ContentEncoding = "deflate"
|
||||
)
|
||||
|
||||
// TransferMode defines how content is transferred
|
||||
type TransferMode string
|
||||
|
||||
const (
|
||||
TransferInline TransferMode = "inline" // Embedded in message (base64)
|
||||
TransferURL TransferMode = "url" // Via URL reference
|
||||
TransferStreaming TransferMode = "streaming" // Via streaming protocol
|
||||
TransferMultipart TransferMode = "multipart" // Via multipart form
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Content Objects
|
||||
// =============================================================================
|
||||
|
||||
// ContentBase is the base struct for all content types
|
||||
type ContentBase struct {
|
||||
MIMEType string `json:"mime_type"`
|
||||
Encoding ContentEncoding `json:"encoding,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"` // format: "sha256:xxx"
|
||||
TransferMode TransferMode `json:"transfer_mode"`
|
||||
}
|
||||
|
||||
// TextContent represents textual content
|
||||
type TextContent struct {
|
||||
ContentBase
|
||||
Charset string `json:"charset"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// JSONContent represents JSON structured content
|
||||
type JSONContent struct {
|
||||
ContentBase
|
||||
Schema string `json:"schema,omitempty"` // Schema identifier
|
||||
SchemaURL string `json:"schema_url,omitempty"` // URL to schema definition
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// CodeContent represents source code content
|
||||
type CodeContent struct {
|
||||
ContentBase
|
||||
Language string `json:"language"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// FileContent represents a file (image, video, document, etc.)
|
||||
type FileContent struct {
|
||||
ContentBase
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role,omitempty"` // e.g., "reference", "result", "attachment"
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// For inline transfer
|
||||
Data string `json:"data,omitempty"` // base64 encoded if binary
|
||||
|
||||
// For URL transfer
|
||||
URL string `json:"url,omitempty"`
|
||||
URLExpiresAt *Timestamp `json:"url_expires_at,omitempty"`
|
||||
DownloadMethod string `json:"download_method,omitempty"` // GET, POST, signed_url
|
||||
RangeSupported bool `json:"range_supported,omitempty"`
|
||||
|
||||
// Metadata
|
||||
Width int `json:"width,omitempty"` // images/videos
|
||||
Height int `json:"height,omitempty"` // images/videos
|
||||
Duration float64 `json:"duration,omitempty"` // audio/video (seconds)
|
||||
Resolution string `json:"resolution,omitempty"` // videos
|
||||
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Content Bundles (multiple files)
|
||||
// =============================================================================
|
||||
|
||||
// ContentBundle represents a collection of content items
|
||||
type ContentBundle struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Items []ContentBundleItem `json:"items"`
|
||||
TotalSize int64 `json:"total_size,omitempty"`
|
||||
CreatedAt Timestamp `json:"created_at"`
|
||||
}
|
||||
|
||||
// ContentBundleItem is an item in a content bundle
|
||||
type ContentBundleItem struct {
|
||||
ID string `json:"id"`
|
||||
Role string `json:"role"` // main, reference, attachment, thumbnail, etc.
|
||||
Content any `json:"content"` // TextContent | FileContent | JSONContent | CodeContent
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// File Transfer Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
// IsSmallFile checks if a file should be transferred inline
|
||||
func IsSmallFile(size int64, threshold int64) bool {
|
||||
if threshold <= 0 {
|
||||
threshold = 1024 * 1024 // 1MB default
|
||||
}
|
||||
return size <= threshold
|
||||
}
|
||||
|
||||
// InferMimeType infers MIME type from filename
|
||||
func InferMimeType(filename string) string {
|
||||
extMap := map[string]string{
|
||||
".json": MIMEJSON,
|
||||
".txt": MIMETextPlain,
|
||||
".md": MIMETextMarkdown,
|
||||
".html": MIMETextHTML,
|
||||
".htm": MIMETextHTML,
|
||||
".csv": MIMETextCSV,
|
||||
".jpg": MIMEImageJPEG,
|
||||
".jpeg": MIMEImageJPEG,
|
||||
".png": MIMEImagePNG,
|
||||
".webp": MIMEImageWebP,
|
||||
".gif": MIMEImageGIF,
|
||||
".svg": MIMEImageSVG,
|
||||
".mp4": MIMEVideoMP4,
|
||||
".webm": MIMEVideoWebM,
|
||||
".mp3": MIMEAudioMP3,
|
||||
".wav": MIMEAudioWAV,
|
||||
".aac": MIMEAudioAAC,
|
||||
".pdf": MIMEPDF,
|
||||
".bin": MIMEOctetStream,
|
||||
}
|
||||
|
||||
// Simple extension matching (lowercase would be better)
|
||||
for ext, mime := range extMap {
|
||||
if len(filename) > len(ext) && filename[len(filename)-len(ext):] == ext {
|
||||
return mime
|
||||
}
|
||||
// Also check uppercase
|
||||
upperExt := toUpper(ext)
|
||||
if len(filename) > len(upperExt) && filename[len(filename)-len(upperExt):] == upperExt {
|
||||
return mime
|
||||
}
|
||||
}
|
||||
|
||||
return MIMEOctetStream
|
||||
}
|
||||
|
||||
func toUpper(s string) string {
|
||||
result := make([]byte, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c >= 'a' && c <= 'z' {
|
||||
c = c - 'a' + 'A'
|
||||
}
|
||||
result[i] = c
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// NewInlineFileContent creates an inline file content (for small files)
|
||||
func NewInlineFileContent(name, mimeType string, data []byte, role string) *FileContent {
|
||||
// base64 encode would happen here in real implementation
|
||||
return &FileContent{
|
||||
ContentBase: ContentBase{
|
||||
MIMEType: mimeType,
|
||||
Encoding: EncodingBase64,
|
||||
Size: int64(len(data)),
|
||||
TransferMode: TransferInline,
|
||||
},
|
||||
Name: name,
|
||||
Role: role,
|
||||
Data: "", // base64.RawStdEncoding.EncodeToString(data)
|
||||
}
|
||||
}
|
||||
|
||||
// NewURLFileContent creates a URL-referenced file content (for large files)
|
||||
func NewURLFileContent(name, mimeType string, size int64, url string, expiresAt *Timestamp, role string) *FileContent {
|
||||
return &FileContent{
|
||||
ContentBase: ContentBase{
|
||||
MIMEType: mimeType,
|
||||
Size: size,
|
||||
TransferMode: TransferURL,
|
||||
},
|
||||
Name: name,
|
||||
Role: role,
|
||||
URL: url,
|
||||
URLExpiresAt: expiresAt,
|
||||
DownloadMethod: "GET",
|
||||
RangeSupported: true,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Polling API Types
|
||||
// =============================================================================
|
||||
|
||||
// TaskStatusResponse is the response for task status polling
|
||||
type TaskStatusResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Task *TaskInfo `json:"task,omitempty"`
|
||||
Error *SynapseError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TaskInfo represents task status information
|
||||
type TaskInfo struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
Message string `json:"message,omitempty"`
|
||||
CreatedAt Timestamp `json:"created_at"`
|
||||
UpdatedAt Timestamp `json:"updated_at"`
|
||||
ETA *Timestamp `json:"eta,omitempty"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *SynapseError `json:"error,omitempty"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
}
|
||||
|
||||
// TaskListResponse is the response for listing tasks
|
||||
type TaskListResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Tasks []TaskInfo `json:"tasks"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
// TaskEvent represents a task event
|
||||
type TaskEvent struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // status_update, progress, log, error, complete
|
||||
Timestamp Timestamp `json:"timestamp"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// TaskEventsResponse is the response for task events
|
||||
type TaskEventsResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Since string `json:"since"`
|
||||
Events []TaskEvent `json:"events"`
|
||||
HasMore bool `json:"has_more"`
|
||||
Next string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Streaming Types
|
||||
// =============================================================================
|
||||
|
||||
// StreamChunk represents a chunk of streaming data
|
||||
type StreamChunk struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
ChunkType string `json:"chunk_type"` // text, binary, audio, video
|
||||
Data string `json:"data"` // base64 for binary
|
||||
Final bool `json:"final"`
|
||||
Timestamp Timestamp `json:"timestamp"`
|
||||
}
|
||||
|
||||
// StreamComplete is sent when streaming is complete
|
||||
type StreamComplete struct {
|
||||
TaskID string `json:"task_id"`
|
||||
TotalChunks int64 `json:"total_chunks"`
|
||||
FullContentChecksum string `json:"full_content_checksum"`
|
||||
Result any `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Capabilities Query (Synchronous)
|
||||
// =============================================================================
|
||||
|
||||
// CapabilitiesResponse is the response for capability query
|
||||
type CapabilitiesResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data *AgentCapabilities `json:"data,omitempty"`
|
||||
Error *SynapseError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// AgentCapabilities describes an agent's capabilities
|
||||
type AgentCapabilities struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
SystemID string `json:"system_id"`
|
||||
ProtocolVersion string `json:"protocol_version"`
|
||||
|
||||
SupportedTaskTypes []SupportedTaskType `json:"supported_task_types"`
|
||||
SupportedContentTypes []string `json:"supported_content_types"`
|
||||
SupportedTransportModes []TransportMode `json:"supported_transport_modes"`
|
||||
|
||||
// Limits
|
||||
MaxFileSize int64 `json:"max_file_size"`
|
||||
MaxConcurrentTasks int `json:"max_concurrent_tasks"`
|
||||
MaxTaskDuration int `json:"max_task_duration_seconds"`
|
||||
|
||||
// Features
|
||||
SupportProgressUpdates bool `json:"support_progress_updates"`
|
||||
SupportStreaming bool `json:"support_streaming"`
|
||||
SupportHiL bool `json:"support_hil"`
|
||||
SupportGroups bool `json:"support_groups"`
|
||||
}
|
||||
|
||||
// SupportedTaskType describes a supported task type
|
||||
type SupportedTaskType struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
EstimatedTime string `json:"estimated_time"` // e.g., "30s-2min"
|
||||
InputSchema string `json:"input_schema,omitempty"`
|
||||
OutputSchema string `json:"output_schema,omitempty"`
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// NewCapabilitiesResponse creates a capabilities response
|
||||
func NewCapabilitiesResponse(caps *AgentCapabilities) *CapabilitiesResponse {
|
||||
return &CapabilitiesResponse{
|
||||
Success: true,
|
||||
Data: caps,
|
||||
}
|
||||
}
|
||||
332
types/errors.go
Normal file
332
types/errors.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package types
|
||||
|
||||
// =============================================================================
|
||||
// Synapse Error Codes
|
||||
// =============================================================================
|
||||
|
||||
// SynapseErrorCode defines error code ranges
|
||||
type SynapseErrorCode int
|
||||
|
||||
const (
|
||||
// System errors (1000-1999)
|
||||
ErrSystemError SynapseErrorCode = 1000
|
||||
ErrInternalError SynapseErrorCode = 1001
|
||||
ErrMaintenance SynapseErrorCode = 1002
|
||||
|
||||
// Task errors (2000-2999)
|
||||
ErrTaskNotFound SynapseErrorCode = 2001
|
||||
ErrTaskAlreadyCompleted SynapseErrorCode = 2002
|
||||
ErrTaskCancelled SynapseErrorCode = 2003
|
||||
ErrTaskTimeout SynapseErrorCode = 2004
|
||||
ErrTaskInvalidParams SynapseErrorCode = 2005
|
||||
|
||||
// Information errors (3000-3999)
|
||||
ErrRequiredFieldMissing SynapseErrorCode = 3001
|
||||
ErrInvalidFieldFormat SynapseErrorCode = 3002
|
||||
ErrInfoNotProvided SynapseErrorCode = 3003
|
||||
|
||||
// Capability errors (4000-4999)
|
||||
ErrUnsupportedTaskType SynapseErrorCode = 4001
|
||||
ErrCapabilityLacking SynapseErrorCode = 4002
|
||||
ErrResourceBusy SynapseErrorCode = 4003
|
||||
ErrQuotaExceeded SynapseErrorCode = 4004
|
||||
|
||||
// Security errors (5000-5999)
|
||||
ErrAuthFailed SynapseErrorCode = 5001
|
||||
ErrSignatureInvalid SynapseErrorCode = 5002
|
||||
ErrPermissionDenied SynapseErrorCode = 5003
|
||||
ErrPolicyViolation SynapseErrorCode = 5004
|
||||
|
||||
// Communication errors (6000-6999)
|
||||
ErrEndpointUnreachable SynapseErrorCode = 6001
|
||||
ErrCallbackFailed SynapseErrorCode = 6002
|
||||
ErrMessageFormatInvalid SynapseErrorCode = 6003
|
||||
ErrTTLExpired SynapseErrorCode = 6004
|
||||
|
||||
// Group errors (7000-7999)
|
||||
ErrGroupNotFound SynapseErrorCode = 7001
|
||||
ErrGroupNotActive SynapseErrorCode = 7002
|
||||
ErrMemberNotFound SynapseErrorCode = 7003
|
||||
ErrNotGroupMember SynapseErrorCode = 7004
|
||||
ErrConsensusNotReached SynapseErrorCode = 7005
|
||||
ErrRoleNotFound SynapseErrorCode = 7006
|
||||
ErrAlreadyMember SynapseErrorCode = 7007
|
||||
ErrGroupFull SynapseErrorCode = 7008
|
||||
|
||||
// HiL errors (8000-8999)
|
||||
ErrHumanTimeout SynapseErrorCode = 8001
|
||||
ErrHumanDeclined SynapseErrorCode = 8002
|
||||
ErrEscalationLevel SynapseErrorCode = 8003
|
||||
|
||||
// Validation errors (9000-9999)
|
||||
ErrInvalidParams SynapseErrorCode = 9001
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Error Categories
|
||||
// =============================================================================
|
||||
|
||||
// ErrorCategory maps error codes to categories
|
||||
func (c SynapseErrorCode) Category() string {
|
||||
switch {
|
||||
case c >= 1000 && c < 2000:
|
||||
return "SYSTEM_ERROR"
|
||||
case c >= 2000 && c < 3000:
|
||||
return "TASK_ERROR"
|
||||
case c >= 3000 && c < 4000:
|
||||
return "INFO_ERROR"
|
||||
case c >= 4000 && c < 5000:
|
||||
return "CAPABILITY_ERROR"
|
||||
case c >= 5000 && c < 6000:
|
||||
return "SECURITY_ERROR"
|
||||
case c >= 6000 && c < 7000:
|
||||
return "COMMUNICATION_ERROR"
|
||||
case c >= 7000 && c < 8000:
|
||||
return "GROUP_ERROR"
|
||||
case c >= 8000 && c < 9000:
|
||||
return "HIL_ERROR"
|
||||
case c >= 9000 && c < 10000:
|
||||
return "VALIDATION_ERROR"
|
||||
default:
|
||||
return "UNKNOWN_ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Synapse Error
|
||||
// =============================================================================
|
||||
|
||||
// SynapseError represents a structured error response
|
||||
type SynapseError struct {
|
||||
Code SynapseErrorCode `json:"code"`
|
||||
Category string `json:"category"`
|
||||
Message string `json:"message"`
|
||||
Details any `json:"details,omitempty"`
|
||||
RetryAfter *int `json:"retry_after,omitempty"` // seconds
|
||||
Timestamp Timestamp `json:"timestamp"`
|
||||
}
|
||||
|
||||
// NewSynapseError creates a new Synapse error
|
||||
func NewSynapseError(code SynapseErrorCode, message string, details any) *SynapseError {
|
||||
return &SynapseError{
|
||||
Code: code,
|
||||
Category: code.Category(),
|
||||
Message: message,
|
||||
Details: details,
|
||||
Timestamp: NowTimestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewSynapseErrorWithRetry creates a new Synapse error with retry hint
|
||||
func NewSynapseErrorWithRetry(code SynapseErrorCode, message string, details any, retryAfter int) *SynapseError {
|
||||
return &SynapseError{
|
||||
Code: code,
|
||||
Category: code.Category(),
|
||||
Message: message,
|
||||
Details: details,
|
||||
RetryAfter: &retryAfter,
|
||||
Timestamp: NowTimestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e *SynapseError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// ToMap converts error to map for JSON serialization
|
||||
func (e *SynapseError) ToMap() map[string]any {
|
||||
m := map[string]any{
|
||||
"code": e.Code,
|
||||
"category": e.Category,
|
||||
"message": e.Message,
|
||||
"timestamp": e.Timestamp,
|
||||
}
|
||||
if e.Details != nil {
|
||||
m["details"] = e.Details
|
||||
}
|
||||
if e.RetryAfter != nil {
|
||||
m["retry_after"] = *e.RetryAfter
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Predefined Errors
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
ErrTaskNotFoundError = NewSynapseError(
|
||||
ErrTaskNotFound,
|
||||
"Task not found",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrTaskAlreadyCompletedError = NewSynapseError(
|
||||
ErrTaskAlreadyCompleted,
|
||||
"Task has already been completed",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrTaskCancelledError = NewSynapseError(
|
||||
ErrTaskCancelled,
|
||||
"Task has been cancelled",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrTaskTimeoutError = NewSynapseError(
|
||||
ErrTaskTimeout,
|
||||
"Task execution timed out",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrRequiredFieldMissingError = NewSynapseError(
|
||||
ErrRequiredFieldMissing,
|
||||
"Required field is missing",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrUnsupportedTaskTypeError = NewSynapseError(
|
||||
ErrUnsupportedTaskType,
|
||||
"Task type is not supported by this agent",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrCapabilityLackingError = NewSynapseError(
|
||||
ErrCapabilityLacking,
|
||||
"Agent lacks the required capability",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrResourceBusyError = NewSynapseError(
|
||||
ErrResourceBusy,
|
||||
"Agent is currently busy processing other tasks",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrAuthFailedError = NewSynapseError(
|
||||
ErrAuthFailed,
|
||||
"Authentication failed",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrSignatureInvalidError = NewSynapseError(
|
||||
ErrSignatureInvalid,
|
||||
"Message signature verification failed",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrPermissionDeniedError = NewSynapseError(
|
||||
ErrPermissionDenied,
|
||||
"Permission denied",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrEndpointUnreachableError = NewSynapseError(
|
||||
ErrEndpointUnreachable,
|
||||
"Callback endpoint is unreachable",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrMessageFormatInvalidError = NewSynapseError(
|
||||
ErrMessageFormatInvalid,
|
||||
"Message format is invalid",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrGroupNotFoundError = NewSynapseError(
|
||||
ErrGroupNotFound,
|
||||
"Group not found",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrNotGroupMemberError = NewSynapseError(
|
||||
ErrNotGroupMember,
|
||||
"Agent is not a member of this group",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrGroupNotActiveError = NewSynapseError(
|
||||
ErrGroupNotActive,
|
||||
"Group is not active",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrAlreadyMemberError = NewSynapseError(
|
||||
ErrAlreadyMember,
|
||||
"Agent is already a member of this group",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrGroupFullError = NewSynapseError(
|
||||
ErrGroupFull,
|
||||
"Group has reached maximum capacity",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrMemberNotFoundError = NewSynapseError(
|
||||
ErrMemberNotFound,
|
||||
"Member not found in group",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrInvalidParamsError = NewSynapseError(
|
||||
ErrInvalidParams,
|
||||
"Invalid parameters",
|
||||
nil,
|
||||
)
|
||||
|
||||
ErrHumanTimeoutError = NewSynapseError(
|
||||
ErrHumanTimeout,
|
||||
"Human decision timed out",
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Error Response Builder
|
||||
// =============================================================================
|
||||
|
||||
// ErrorResponse represents a standard error response
|
||||
type ErrorResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error *SynapseError `json:"error"`
|
||||
}
|
||||
|
||||
// NewErrorResponse creates an error response
|
||||
func NewErrorResponse(err *SynapseError) *ErrorResponse {
|
||||
return &ErrorResponse{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// SuccessResponse represents a standard success response
|
||||
type SuccessResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// NewSuccessResponse creates a success response
|
||||
func NewSuccessResponse() *SuccessResponse {
|
||||
return &SuccessResponse{
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSuccessResponseWithData creates a success response with data
|
||||
func NewSuccessResponseWithData(data any) *SuccessResponse {
|
||||
return &SuccessResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSuccessResponseWithMessage creates a success response with message
|
||||
func NewSuccessResponseWithMessage(message string) *SuccessResponse {
|
||||
return &SuccessResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
610
types/types.go
Normal file
610
types/types.go
Normal file
@@ -0,0 +1,610 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Message ID and Task ID generators
|
||||
// =============================================================================
|
||||
|
||||
// GenerateMessageID generates a unique message ID
|
||||
func GenerateMessageID() string {
|
||||
return fmt.Sprintf("msg-%d-%d", time.Now().UnixNano(), time.Now().UnixMilli()%1000)
|
||||
}
|
||||
|
||||
// GenerateTaskID generates a unique task ID
|
||||
func GenerateTaskID() string {
|
||||
return fmt.Sprintf("task-%d-%d", time.Now().UnixNano(), time.Now().UnixMilli()%1000)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// P2P Message Types
|
||||
// =============================================================================
|
||||
|
||||
// P2PMessageType defines all point-to-point message types
|
||||
type P2PMessageType string
|
||||
|
||||
const (
|
||||
// Task lifecycle
|
||||
P2PTaskRequest P2PMessageType = "p2p:task_request"
|
||||
P2PTaskConfirm P2PMessageType = "p2p:task_confirm"
|
||||
P2PTaskReject P2PMessageType = "p2p:task_reject"
|
||||
P2PTaskNeedInfo P2PMessageType = "p2p:task_need_info"
|
||||
P2PTaskInfoProvided P2PMessageType = "p2p:task_info_provided"
|
||||
P2PTaskStatusUpdate P2PMessageType = "p2p:task_status_update"
|
||||
P2PTaskComplete P2PMessageType = "p2p:task_complete"
|
||||
P2PTaskFailed P2PMessageType = "p2p:task_failed"
|
||||
P2PTaskCancel P2PMessageType = "p2p:task_cancel"
|
||||
P2PTaskAck P2PMessageType = "p2p:task_ack"
|
||||
|
||||
// Communication
|
||||
P2PAck P2PMessageType = "p2p:ack"
|
||||
P2PNack P2PMessageType = "p2p:nack"
|
||||
P2PHeartbeat P2PMessageType = "p2p:heartbeat"
|
||||
P2PCapabilityQuery P2PMessageType = "p2p:capability_query"
|
||||
P2PCapabilityResponse P2PMessageType = "p2p:capability_response"
|
||||
|
||||
// Human-in-the-Loop
|
||||
P2PTaskNeedHuman P2PMessageType = "p2p:task_need_human"
|
||||
P2PHumanDecision P2PMessageType = "p2p:human_decision"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Group Message Types
|
||||
// =============================================================================
|
||||
|
||||
// GroupMessageType defines all group message types
|
||||
type GroupMessageType string
|
||||
|
||||
const (
|
||||
// Group management
|
||||
GroupCreate GroupMessageType = "group:create"
|
||||
GroupDisband GroupMessageType = "group:disband"
|
||||
GroupJoin GroupMessageType = "group:join"
|
||||
GroupLeave GroupMessageType = "group:leave"
|
||||
GroupInvite GroupMessageType = "group:invite"
|
||||
GroupAcceptInvite GroupMessageType = "group:accept_invite"
|
||||
GroupRejectInvite GroupMessageType = "group:reject_invite"
|
||||
GroupMemberUpdate GroupMessageType = "group:member_update"
|
||||
GroupRoleChange GroupMessageType = "group:role_change"
|
||||
|
||||
// Group communication
|
||||
GroupBroadcast GroupMessageType = "group:broadcast"
|
||||
GroupMulticast GroupMessageType = "group:multicast"
|
||||
GroupDirect GroupMessageType = "group:direct"
|
||||
GroupRelay GroupMessageType = "group:relay"
|
||||
|
||||
// Task collaboration
|
||||
GroupTaskSplit GroupMessageType = "group:task_split"
|
||||
GroupTaskAssign GroupMessageType = "group:task_assign"
|
||||
GroupTaskMerge GroupMessageType = "group:task_merge"
|
||||
GroupTaskProgress GroupMessageType = "group:task_progress"
|
||||
|
||||
// Consensus
|
||||
GroupVote GroupMessageType = "group:vote"
|
||||
GroupVoteSubmit GroupMessageType = "group:vote_submit"
|
||||
GroupConsensus GroupMessageType = "group:consensus"
|
||||
|
||||
// Human-in-the-Loop
|
||||
GroupConsensusEscalate GroupMessageType = "group:consensus_escalate"
|
||||
GroupSubtaskEscalate GroupMessageType = "group:subtask_escalate"
|
||||
GroupHumanDecision GroupMessageType = "group:human_decision"
|
||||
InterGroupEscalate GroupMessageType = "inter_group:escalate"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Agent Endpoint
|
||||
// =============================================================================
|
||||
|
||||
// AgentEndpoint represents an agent's network endpoint
|
||||
type AgentEndpoint struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
AgentName string `json:"agent_name,omitempty"`
|
||||
SystemID string `json:"system_id"`
|
||||
SystemName string `json:"system_name,omitempty"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// P2P Task Status
|
||||
// =============================================================================
|
||||
|
||||
// P2PTaskStatus defines task status for P2P operations
|
||||
type P2PTaskStatus string
|
||||
|
||||
const (
|
||||
// Basic states
|
||||
P2PStatusIdle P2PTaskStatus = "idle"
|
||||
P2PStatusPending P2PTaskStatus = "pending"
|
||||
P2PStatusConfirmed P2PTaskStatus = "confirmed"
|
||||
P2PStatusExecuting P2PTaskStatus = "executing"
|
||||
|
||||
// Information exchange
|
||||
P2PStatusInfoNeeded P2PTaskStatus = "info_needed"
|
||||
P2PStatusInfoProvided P2PTaskStatus = "info_provided"
|
||||
|
||||
// Human-in-the-Loop
|
||||
P2PStatusNeedHuman P2PTaskStatus = "need_human"
|
||||
P2PStatusHumanDeciding P2PTaskStatus = "human_deciding"
|
||||
P2PStatusHumanApproved P2PTaskStatus = "human_approved"
|
||||
P2PStatusHumanRejected P2PTaskStatus = "human_rejected"
|
||||
|
||||
// Terminal states
|
||||
P2PStatusCompleted P2PTaskStatus = "completed"
|
||||
P2PStatusFailed P2PTaskStatus = "failed"
|
||||
P2PStatusCancelled P2PTaskStatus = "cancelled"
|
||||
P2PStatusRejected P2PTaskStatus = "rejected"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Group Status
|
||||
// =============================================================================
|
||||
|
||||
// GroupStatus defines group state
|
||||
type GroupStatus string
|
||||
|
||||
const (
|
||||
GroupStatusNone GroupStatus = "none"
|
||||
GroupStatusCreating GroupStatus = "creating"
|
||||
GroupStatusActive GroupStatus = "active"
|
||||
GroupStatusFrozen GroupStatus = "frozen"
|
||||
GroupStatusDisbanded GroupStatus = "disbanded"
|
||||
)
|
||||
|
||||
// GroupMemberStatus defines member state within a group
|
||||
type GroupMemberStatus string
|
||||
|
||||
const (
|
||||
GroupMemberActive GroupMemberStatus = "active"
|
||||
GroupMemberAway GroupMemberStatus = "away"
|
||||
GroupMemberBusy GroupMemberStatus = "busy"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Priority
|
||||
// =============================================================================
|
||||
|
||||
// Priority defines message priority
|
||||
type Priority string
|
||||
|
||||
const (
|
||||
PriorityHigh Priority = "high"
|
||||
PriorityNormal Priority = "normal"
|
||||
PriorityLow Priority = "low"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Unified Synapse Message
|
||||
// =============================================================================
|
||||
|
||||
// UnifiedSynapseMessage is the base message format for all Synapse communications
|
||||
type UnifiedSynapseMessage struct {
|
||||
// Message identification
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
|
||||
// Sender
|
||||
Sender AgentEndpoint `json:"sender"`
|
||||
|
||||
// Receiver (for P2P)
|
||||
Receiver *AgentEndpoint `json:"receiver,omitempty"`
|
||||
|
||||
// Group context (for Group)
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
TargetType string `json:"target_type,omitempty"` // "all", "role", "specific"
|
||||
TargetIDs []string `json:"target_ids,omitempty"`
|
||||
|
||||
// Protocol control
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
ReplyTo string `json:"reply_to,omitempty"`
|
||||
ConversationID string `json:"conversation_id,omitempty"`
|
||||
|
||||
// Payload
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
|
||||
// Transport control
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
Priority Priority `json:"priority,omitempty"`
|
||||
Broadcast bool `json:"broadcast,omitempty"`
|
||||
|
||||
// Security
|
||||
Signature string `json:"signature,omitempty"`
|
||||
Encrypt bool `json:"encrypt,omitempty"`
|
||||
|
||||
// Timestamps
|
||||
Timestamp Timestamp `json:"timestamp"`
|
||||
ExpiresAt Timestamp `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
// Timestamp is an ISO 8601 formatted timestamp
|
||||
type Timestamp string
|
||||
|
||||
func (t Timestamp) Time() (time.Time, error) {
|
||||
return time.Parse(time.RFC3339, string(t))
|
||||
}
|
||||
|
||||
func NowTimestamp() Timestamp {
|
||||
return Timestamp(time.Now().UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// P2P Payloads
|
||||
// =============================================================================
|
||||
|
||||
// TaskRequestPayload is the payload for TASK_REQUEST
|
||||
type TaskRequestPayload struct {
|
||||
Task struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Priority Priority `json:"priority"`
|
||||
Deadline *Timestamp `json:"deadline,omitempty"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
} `json:"task"`
|
||||
Context *struct {
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
ParentTaskID string `json:"parent_task_id,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
} `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
// TaskConfirmPayload is the payload for TASK_CONFIRM
|
||||
type TaskConfirmPayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
EstimatedDuration int `json:"estimated_duration"` // seconds
|
||||
ExpectedCompletion Timestamp `json:"expected_completion"`
|
||||
CapabilitiesUsed []string `json:"capabilities_used"`
|
||||
Constraints *struct {
|
||||
MaxRetries int `json:"max_retries"`
|
||||
FallbackEnabled bool `json:"fallback_enabled"`
|
||||
} `json:"constraints,omitempty"`
|
||||
}
|
||||
|
||||
// TaskRejectPayload is the payload for TASK_REJECT
|
||||
type TaskRejectPayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Reason struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details any `json:"details,omitempty"`
|
||||
} `json:"reason"`
|
||||
Alternatives *struct {
|
||||
SuggestedAgentID string `json:"suggested_agent_id,omitempty"`
|
||||
RetryAfter int `json:"retry_after,omitempty"`
|
||||
PartialSupport bool `json:"partial_support,omitempty"`
|
||||
} `json:"alternatives,omitempty"`
|
||||
}
|
||||
|
||||
// MissingField describes a missing information field
|
||||
type MissingField struct {
|
||||
Field string `json:"field"`
|
||||
Reason string `json:"reason"`
|
||||
SuggestedValue any `json:"suggested_value,omitempty"`
|
||||
Alternatives []any `json:"alternatives,omitempty"`
|
||||
}
|
||||
|
||||
// TaskNeedInfoPayload is the payload for TASK_NEED_INFO
|
||||
type TaskNeedInfoPayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
MissingFields []MissingField `json:"missing_fields"`
|
||||
Question string `json:"question,omitempty"`
|
||||
RequiredBy *Timestamp `json:"required_by,omitempty"`
|
||||
}
|
||||
|
||||
// TaskInfoProvidedPayload is the payload for TASK_INFO_PROVIDED
|
||||
type TaskInfoProvidedPayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
ReplyTo string `json:"reply_to"`
|
||||
ProvidedInfo map[string]interface{} `json:"provided_info"`
|
||||
AdditionalContext string `json:"additional_context,omitempty"`
|
||||
}
|
||||
|
||||
// TaskStatusUpdatePayload is the payload for TASK_STATUS_UPDATE
|
||||
type TaskStatusUpdatePayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"` // 0-100
|
||||
Message string `json:"message,omitempty"`
|
||||
IntermediateResult any `json:"intermediate_result,omitempty"`
|
||||
}
|
||||
|
||||
// TaskCompletePayload is the payload for TASK_COMPLETE
|
||||
type TaskCompletePayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Result struct {
|
||||
OutputType string `json:"output_type"`
|
||||
Output any `json:"output"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
} `json:"result"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
// TaskFailedPayload is the payload for TASK_FAILED
|
||||
type TaskFailedPayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Recoverable bool `json:"recoverable"`
|
||||
Details any `json:"details,omitempty"`
|
||||
} `json:"error"`
|
||||
PartialResult any `json:"partial_result,omitempty"`
|
||||
}
|
||||
|
||||
// TaskCancelPayload is the payload for TASK_CANCEL
|
||||
type TaskCancelPayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Reason string `json:"reason"`
|
||||
CancelledBy string `json:"cancelled_by,omitempty"`
|
||||
}
|
||||
|
||||
// TaskNeedHumanPayload is the payload for TASK_NEED_HUMAN
|
||||
type TaskNeedHumanPayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Reason struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"reason"`
|
||||
Context struct {
|
||||
TaskSummary string `json:"task_summary"`
|
||||
CurrentProgress int `json:"current_progress"`
|
||||
BlockedReason string `json:"blocked_reason"`
|
||||
RelevantHistory []any `json:"relevant_history,omitempty"`
|
||||
} `json:"context"`
|
||||
Options []struct {
|
||||
Field string `json:"field"`
|
||||
Value any `json:"value"`
|
||||
ProsCons string `json:"pros_cons,omitempty"`
|
||||
Confidence int `json:"confidence,omitempty"`
|
||||
} `json:"options,omitempty"`
|
||||
SuggestedValue any `json:"suggested_value,omitempty"`
|
||||
EscalationLevel int `json:"escalation_level"`
|
||||
Deadline *Timestamp `json:"deadline,omitempty"`
|
||||
}
|
||||
|
||||
// HumanDecisionPayload is the payload for HUMAN_DECISION
|
||||
type HumanDecisionPayload struct {
|
||||
TaskID string `json:"task_id"`
|
||||
DecisionID string `json:"decision_id"`
|
||||
ReplyTo string `json:"reply_to"`
|
||||
DecidedBy struct {
|
||||
UserID string `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Role string `json:"role"`
|
||||
} `json:"decided_by"`
|
||||
Decision struct {
|
||||
Action string `json:"action"` // approve, reject, modify, select
|
||||
ApprovedFields map[string]interface{} `json:"approved_fields,omitempty"`
|
||||
ModifiedFields map[string]interface{} `json:"modified_fields,omitempty"`
|
||||
SelectedOption any `json:"selected_option,omitempty"`
|
||||
RejectionReason string `json:"rejection_reason,omitempty"`
|
||||
} `json:"decision"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Timestamp Timestamp `json:"timestamp"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Group Payloads
|
||||
// =============================================================================
|
||||
|
||||
// GroupTopology defines group topology types
|
||||
type GroupTopology string
|
||||
|
||||
const (
|
||||
GroupTopologyStar GroupTopology = "star"
|
||||
GroupTopologyMesh GroupTopology = "mesh"
|
||||
GroupTopologyChain GroupTopology = "chain"
|
||||
GroupTopologyHierarchical GroupTopology = "hierarchical"
|
||||
GroupTopologyFullMesh GroupTopology = "fullmesh"
|
||||
)
|
||||
|
||||
// GroupRole defines a group role
|
||||
type GroupRole struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Permissions []string `json:"permissions"`
|
||||
MaxTasks int `json:"max_tasks"`
|
||||
}
|
||||
|
||||
// GroupMember represents a member in a group
|
||||
type GroupMember struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
Role string `json:"role"`
|
||||
JoinedAt Timestamp `json:"joined_at"`
|
||||
Status GroupMemberStatus `json:"status"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
LoadFactor float64 `json:"load_factor"` // 0-1, for task assignment
|
||||
}
|
||||
|
||||
// AgentGroup represents a group of agents
|
||||
type AgentGroup struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Topology GroupTopology `json:"topology"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
CreatedAt Timestamp `json:"created_at"`
|
||||
Config GroupConfig `json:"config"`
|
||||
Members []GroupMember `json:"members"`
|
||||
Roles []GroupRole `json:"roles"`
|
||||
Status GroupStatus `json:"status"`
|
||||
}
|
||||
|
||||
// GroupConfig defines group configuration
|
||||
type GroupConfig struct {
|
||||
AllowSelfLeave bool `json:"allow_self_leave"`
|
||||
RequiresApproval bool `json:"requires_approval"`
|
||||
ConsensusRequired bool `json:"consensus_required"`
|
||||
ConsensusThreshold float64 `json:"consensus_threshold"` // 0-1
|
||||
BroadcastMode string `json:"broadcast_mode"` // "all" or "smart"
|
||||
}
|
||||
|
||||
// GroupCreatePayload is the payload for GROUP_CREATE
|
||||
type GroupCreatePayload struct {
|
||||
Group struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Topology GroupTopology `json:"topology"`
|
||||
Config GroupConfig `json:"config"`
|
||||
InitialMembers []AgentEndpoint `json:"initial_members,omitempty"`
|
||||
Roles []GroupRole `json:"roles,omitempty"`
|
||||
} `json:"group"`
|
||||
}
|
||||
|
||||
// GroupInvitePayload is the payload for GROUP_INVITE
|
||||
type GroupInvitePayload struct {
|
||||
GroupID string `json:"group_id"`
|
||||
Invitee AgentEndpoint `json:"invitee"`
|
||||
Inviter AgentEndpoint `json:"inviter"`
|
||||
Message string `json:"message,omitempty"`
|
||||
RequiresApproval bool `json:"requires_approval"`
|
||||
}
|
||||
|
||||
// Subtask describes a subtask in task splitting
|
||||
type Subtask struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
AssignedTo string `json:"assigned_to"`
|
||||
Dependencies []string `json:"dependencies"`
|
||||
EstimatedDuration int `json:"estimated_duration"` // seconds
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
// MergeStrategy defines how subtask results are merged
|
||||
type MergeStrategy string
|
||||
|
||||
const (
|
||||
MergeAllSuccess MergeStrategy = "all_success"
|
||||
MergeMajority MergeStrategy = "majority"
|
||||
MergeAnyComplete MergeStrategy = "any_complete"
|
||||
MergePriority MergeStrategy = "priority"
|
||||
)
|
||||
|
||||
// GroupTaskSplitPayload is the payload for GROUP_TASK_SPLIT
|
||||
type GroupTaskSplitPayload struct {
|
||||
OriginalTask struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Requirements map[string]interface{} `json:"requirements"`
|
||||
Deadline *Timestamp `json:"deadline,omitempty"`
|
||||
} `json:"original_task"`
|
||||
Subtasks []Subtask `json:"subtasks"`
|
||||
MergeStrategy MergeStrategy `json:"merge_strategy"`
|
||||
}
|
||||
|
||||
// SubtaskResult describes a single subtask result
|
||||
type SubtaskResult struct {
|
||||
SubtaskID string `json:"subtask_id"`
|
||||
AssignedTo string `json:"assigned_to"`
|
||||
Status string `json:"status"` // success, failed, partial
|
||||
Result any `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CompletedAt *Timestamp `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
// GroupTaskMergePayload is the payload for GROUP_TASK_MERGE
|
||||
type GroupTaskMergePayload struct {
|
||||
OriginalTaskID string `json:"original_task_id"`
|
||||
SubtaskResults []SubtaskResult `json:"subtask_results"`
|
||||
MergeStrategyUsed string `json:"merge_strategy_used"`
|
||||
MergedResult any `json:"merged_result"`
|
||||
ConsensusAchieved bool `json:"consensus_achieved"`
|
||||
}
|
||||
|
||||
// GroupVoteOption describes a vote option
|
||||
type GroupVoteOption struct {
|
||||
VoterID string `json:"voter_id"`
|
||||
Option string `json:"option"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Timestamp Timestamp `json:"timestamp"`
|
||||
}
|
||||
|
||||
// GroupVotePayload is the payload for GROUP_VOTE
|
||||
type GroupVotePayload struct {
|
||||
VoteID string `json:"vote_id"`
|
||||
Question string `json:"question"`
|
||||
Options []string `json:"options"`
|
||||
Voters []string `json:"voters"`
|
||||
Deadline Timestamp `json:"deadline"`
|
||||
Threshold float64 `json:"threshold"` // 0-1
|
||||
Status string `json:"status"` // open, closed
|
||||
Votes []GroupVoteOption `json:"votes,omitempty"`
|
||||
}
|
||||
|
||||
// GroupConsensusEscalatePayload is the payload for GROUP_CONSENSUS_ESCALATE
|
||||
type GroupConsensusEscalatePayload struct {
|
||||
VoteID string `json:"vote_id"`
|
||||
EscalationID string `json:"escalation_id"`
|
||||
Question string `json:"question"`
|
||||
Votes []struct {
|
||||
VoterID string `json:"voter_id"`
|
||||
VoterRole string `json:"voter_role"`
|
||||
Option string `json:"option"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
} `json:"votes"`
|
||||
ConsensusStatus struct {
|
||||
RequiredThreshold float64 `json:"required_threshold"`
|
||||
ActualThreshold float64 `json:"actual_threshold"`
|
||||
LeadingOption string `json:"leading_option,omitempty"`
|
||||
OptionsTied bool `json:"options_tied"`
|
||||
} `json:"consensus_status"`
|
||||
EscalateReason struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"escalate_reason"`
|
||||
EscalationLevel int `json:"escalation_level"`
|
||||
DecisionOptions []struct {
|
||||
Option string `json:"option"`
|
||||
SupportCount int `json:"support_count"`
|
||||
SupportAgents []string `json:"support_agents"`
|
||||
} `json:"decision_options"`
|
||||
Deadline Timestamp `json:"deadline"`
|
||||
Context struct {
|
||||
TaskID string `json:"task_id"`
|
||||
OriginalTaskRequirements map[string]interface{} `json:"original_task_requirements"`
|
||||
AgentRecommendations []struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
SuggestedOption string `json:"suggested_option"`
|
||||
} `json:"agent_recommendations"`
|
||||
} `json:"context"`
|
||||
}
|
||||
|
||||
// GroupSubtaskEscalatePayload is the payload for GROUP_SUBTASK_ESCALATE
|
||||
type GroupSubtaskEscalatePayload struct {
|
||||
OriginalTask struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
} `json:"original_task"`
|
||||
Subtask struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
AssignedTo string `json:"assigned_to"`
|
||||
BlockedAt string `json:"blocked_at"`
|
||||
BlockedReason struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"blocked_reason"`
|
||||
AttemptedSolutions []string `json:"attempted_solutions"`
|
||||
} `json:"subtask"`
|
||||
RequiredDecision struct {
|
||||
FieldsNeeded []struct {
|
||||
Field string `json:"field"`
|
||||
CurrentValue any `json:"current_value,omitempty"`
|
||||
SuggestedValue any `json:"suggested_value,omitempty"`
|
||||
Alternatives []any `json:"alternatives,omitempty"`
|
||||
ValidationRules string `json:"validation_rules,omitempty"`
|
||||
} `json:"fields_needed"`
|
||||
DecisionType string `json:"decision_type"` // approve, select, provide_info
|
||||
AffectsOtherSubtasks bool `json:"affects_other_subtasks"`
|
||||
RollbackPossible bool `json:"rollback_possible"`
|
||||
} `json:"required_decision"`
|
||||
EscalationLevel int `json:"escalation_level"`
|
||||
Deadline *Timestamp `json:"deadline,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user