package builtin // send_message.go 实现 Agent Teams peer-to-peer 通讯的 send_message 内置工具. // // 模块定位: // // send_message 工具让一个 Agent (Worker 或 Leader) 主动向同伴 Agent 投递消息, // 是 Agent Teams 从 hub-and-spoke (只能通过 Leader 中转) 升级到 peer-to-peer // (Worker 之间直接对话) 的关键原语. // // 核心设计决策: // 1. 反向依赖注入 (对齐 AgentExecutor 模式): // builtin 包定义 TeammateMessageSender 接口, engine 包实现. // 避免 builtin → engine 循环依赖. // 精妙之处(CLEVER): 同样的反转模式已经在 agent.go 用过 (AgentExecutor), // 此处复用风格, 工具系统对外暴露统一的"接口在 builtin, 实现在 engine"契约. // 2. Context 注入 sender 而非工具字段: // 每个 Worker 的 send_message 调用自带不同的"from"身份, 静态字段会串号. // Context 是 Go 惯例, request-scoped 值随 ctx 一起下传到工具 Execute. // 精妙之处(CLEVER): ctx.Value 查找开销为 O(n), 但工具调用频率低于 ms 级 // 用户感知, 开销可忽略; 收益是跨 Worker 零数据共享错配. // 3. Permission 默认 AutoApprove (Team 内信任模型): // Worker 是 Leader 自己 fork 的, 信任边界合理. 流氓行为由 Observer 事件 // 审计兜底 (消费层可订阅 teammate_message_received 做审计/告警/拦截). // 对齐 Anthropic mailbox 无审批的默认行为, 编程客户迁移零摩擦. // 替代方案: <默认 AskUser 每条消息都弹窗> - 否决: TUI dogfood 场景噪声过大. // 4. 跨行业中立的 payload 封装: // SendTeammateMessage 把 content string 包装成 {"content": content} JSON, // 消费者/观察者用固定 schema 解析, 不猜测编码. // 跨行业扩展: 金融场景想带 price/quantity 结构字段, 可提供 SendTeammateMessageStructured // 接口接 any payload, 不影响 send_message 基础用法. import ( "context" "encoding/json" "fmt" "git.flytoex.net/yuanwei/flyto-agent/pkg/inbox" "git.flytoex.net/yuanwei/flyto-agent/pkg/tools" ) // TeammateMessageSender 是 send_message 工具的后端接口. // engine 包的 teamMessageSender 结构体实现此接口, 通过 context 注入到工具 Execute. // // 升华改进(ELEVATED): 接口而非函数指针 -- // 实现可以携带状态 (router / from / 审计 hook), 消费层扩展空间大. // 替代方案: func(to, content, type) error (无状态, 无法扩展). type TeammateMessageSender interface { // SendTeammateMessage 向指定同伴 Agent 投递消息. // // msgType 推荐使用 inbox 包预定义的类型 (MsgTaskAssignment / MsgIdleNotification // 等 8 种), 也接受自定义字符串 (跨行业扩展 -- 金融可用 "trade_proposal", // 医疗可用 "diagnosis_query"). SendTeammateMessage(ctx context.Context, to, content string, msgType inbox.MessageType) error } // teammateMessageSenderKey 是 context 注入 sender 的 key 类型. // 使用不可导出的类型作为 key, 防止其他包冲突 (Go 惯例). type teammateMessageSenderKey struct{} // WithTeammateMessageSender 返回携带 sender 的派生 context. // 由 engine.Team.runWorker 调用, 给每个 Worker 的 ctx 绑定自己的 sender 身份. func WithTeammateMessageSender(ctx context.Context, sender TeammateMessageSender) context.Context { return context.WithValue(ctx, teammateMessageSenderKey{}, sender) } // teammateMessageSenderFromContext 从 ctx 取出 sender, 不存在返回 nil. // 导出版本使用未公开的 key, 避免外部包绕开 WithTeammateMessageSender 直接写入. func teammateMessageSenderFromContext(ctx context.Context) TeammateMessageSender { v := ctx.Value(teammateMessageSenderKey{}) if v == nil { return nil } s, _ := v.(TeammateMessageSender) return s } // sendMessageInput 是 send_message 工具的输入参数结构. type sendMessageInput struct { To string `json:"to"` // 接收方 agent 名称 (必填) Content string `json:"content"` // 消息正文 (必填) Type string `json:"type,omitempty"` // 消息类型, 默认 "task_assignment" } // sendMessageTool 是 send_message 内置工具的实现. type sendMessageTool struct{} // NewSendMessageTool 构造 send_message 工具实例. // 工具本身无状态, sender 通过 context 注入, 因此全局单例安全. func NewSendMessageTool() tools.Tool { return &sendMessageTool{} } // Name 返回工具名. func (t *sendMessageTool) Name() string { return "send_message" } // Description 返回给模型的工具说明. func (t *sendMessageTool) Description(ctx context.Context) string { return `Send a message to a teammate Agent in the same Team (Agent Teams peer-to-peer). Use this when you need to: - Ask a peer Agent for information or clarification mid-task - Delegate a sub-problem to a more specialized teammate - Hand off intermediate results to another teammate - Respond to a teammate's earlier message Parameters: - to (string, required): Name of the recipient Agent (must be a known teammate) - content (string, required): Plain text message content - type (string, optional): Message type hint ("task_assignment" / "idle_notification" / "permission_request" / "shutdown_request" / custom). Defaults to "task_assignment". Returns a short confirmation. If no Team is active (ctx carries no sender), returns an error. Note: Messages are delivered asynchronously. The recipient reads them at the start of their next turn. Do not expect an immediate reply within the same tool call.` } // InputSchema 返回工具输入的 JSON Schema. func (t *sendMessageTool) InputSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", "properties": { "to": { "type": "string", "description": "Recipient agent name" }, "content": { "type": "string", "description": "Message content (plain text)" }, "type": { "type": "string", "description": "Optional message type hint (e.g. 'task_assignment', 'idle_notification')", "default": "task_assignment" } }, "required": ["to", "content"] }`) } // Execute 执行 send_message 工具. // // 执行流程: // 1. 从 ctx 取 sender (Team 未接线时返回错误) // 2. 解析输入参数 // 3. 校验必填字段 // 4. 调用 sender.SendTeammateMessage 投递消息 // 5. 返回确认文本 func (t *sendMessageTool) Execute(ctx context.Context, input json.RawMessage, progress tools.ProgressFunc) (*tools.Result, error) { sender := teammateMessageSenderFromContext(ctx) if sender == nil { return &tools.Result{ Output: "send_message: current Agent is not part of a Team (no TeammateMessageSender in context). This tool is only usable when running inside engine.Team.RunWorkers.", IsError: true, }, nil } var req sendMessageInput if err := json.Unmarshal(input, &req); err != nil { return &tools.Result{ Output: fmt.Sprintf("send_message: invalid input: %v", err), IsError: true, }, nil } if req.To == "" { return &tools.Result{Output: "send_message: 'to' is required", IsError: true}, nil } if req.Content == "" { return &tools.Result{Output: "send_message: 'content' is required", IsError: true}, nil } msgType := inbox.MsgTaskAssignment if req.Type != "" { msgType = inbox.MessageType(req.Type) } if err := sender.SendTeammateMessage(ctx, req.To, req.Content, msgType); err != nil { return &tools.Result{ Output: fmt.Sprintf("send_message: delivery failed: %v", err), IsError: true, }, nil } return &tools.Result{ Output: fmt.Sprintf("Message delivered to %q (type=%s, content=%d bytes).\nThe recipient will read it at the start of their next turn.", req.To, msgType, len(req.Content)), }, nil } // Metadata 声明元数据 (可选接口). // // 精妙之处(CLEVER): AuditOperation="write" 把消息发送归类为写操作 -- // 消费层 AuditObserver 自动记为 write 操作, 不需要硬编码工具名. // ConcurrencySafe=true: Router.Send 内部是线程安全的 (MemoryInbox 用 channel). func (t *sendMessageTool) Metadata() tools.Metadata { return tools.Metadata{ ConcurrencySafe: true, ReadOnly: false, Destructive: false, PermissionClass: "generic", AuditOperation: "write", SearchHint: "teammate message", } } // 编译时接口检查 (对齐 memory.go 的 `var _ Inbox = (*MemoryInbox)(nil)` 模式). var _ tools.Tool = (*sendMessageTool)(nil) var _ tools.MetadataProvider = (*sendMessageTool)(nil)