diff --git a/README.md b/README.md index 69681d4..7db7b87 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ - `nodes` 工具支持设备快捷参数:`facing`、`duration_ms`、`command` - 设备动作响应统一:`ok/code/error/payload`(code 示例:`ok` `unsupported_action` `transport_error`) - 设备 `payload` 规范字段:`media_type` `storage` `url|path|image` `meta` +- 支持 `agent_task`:主节点可向具备 `model` 能力的子节点下发任务,子节点返回执行结果 实现位置: - `pkg/nodes/types.go` diff --git a/README_EN.md b/README_EN.md index e40ac07..a55802f 100644 --- a/README_EN.md +++ b/README_EN.md @@ -50,6 +50,7 @@ A `nodes` tool control-plane PoC is now available: - `nodes` tool supports device shortcuts: `facing`, `duration_ms`, `command` - unified device response envelope: `ok/code/error/payload` (code examples: `ok`, `unsupported_action`, `transport_error`) - device `payload` normalized fields: `media_type` `storage` `url|path|image` `meta` +- supports `agent_task`: parent node can dispatch tasks to child nodes with `model` capability and receive execution results Implementation: - `pkg/nodes/types.go` diff --git a/cmd/clawgo/cmd_status.go b/cmd/clawgo/cmd_status.go index 52c129b..5805074 100644 --- a/cmd/clawgo/cmd_status.go +++ b/cmd/clawgo/cmd_status.go @@ -162,19 +162,20 @@ func statusCmd() { ns := nodes.DefaultManager().List() if len(ns) > 0 { online := 0 - caps := map[string]int{"run": 0, "camera": 0, "screen": 0, "location": 0, "canvas": 0} + caps := map[string]int{"run": 0, "model": 0, "camera": 0, "screen": 0, "location": 0, "canvas": 0} for _, n := range ns { if n.Online { online++ } if n.Capabilities.Run { caps["run"]++ } + if n.Capabilities.Model { caps["model"]++ } if n.Capabilities.Camera { caps["camera"]++ } if n.Capabilities.Screen { caps["screen"]++ } if n.Capabilities.Location { caps["location"]++ } if n.Capabilities.Canvas { caps["canvas"]++ } } fmt.Printf("Nodes: total=%d online=%d\n", len(ns), online) - fmt.Printf("Nodes Capabilities: run=%d camera=%d screen=%d location=%d canvas=%d\n", caps["run"], caps["camera"], caps["screen"], caps["location"], caps["canvas"]) + fmt.Printf("Nodes Capabilities: run=%d model=%d camera=%d screen=%d location=%d canvas=%d\n", caps["run"], caps["model"], caps["camera"], caps["screen"], caps["location"], caps["canvas"]) } } } diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 617eaaf..aae2b7a 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -79,7 +79,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers toolsRegistry.Register(tools.NewProcessTool(processManager)) nodesManager := nodes.DefaultManager() nodesManager.SetAuditPath(filepath.Join(workspace, "memory", "nodes-audit.jsonl")) - nodesManager.Upsert(nodes.NodeInfo{ID: "local", Name: "local", Capabilities: nodes.Capabilities{Run: true, Invoke: true, Camera: true, Screen: true, Location: true, Canvas: true}, Online: true}) + nodesManager.Upsert(nodes.NodeInfo{ID: "local", Name: "local", Capabilities: nodes.Capabilities{Run: true, Invoke: true, Model: true, Camera: true, Screen: true, Location: true, Canvas: true}, Models: []string{"local-sim"}, Online: true}) nodesManager.RegisterHandler("local", func(req nodes.Request) nodes.Response { switch req.Action { case "run": @@ -92,6 +92,8 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers payload["command"] = parts } return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: payload} + case "agent_task": + return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "simulated": true, "model": req.Model, "task": req.Task, "result": "local child-model simulated execution completed"}} case "camera_snap": return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "image", "storage": "inline", "facing": req.Args["facing"], "simulated": true, "meta": map[string]interface{}{"width": 1280, "height": 720}}} case "camera_clip": diff --git a/pkg/nodes/manager.go b/pkg/nodes/manager.go index d7128b8..e922879 100644 --- a/pkg/nodes/manager.go +++ b/pkg/nodes/manager.go @@ -137,6 +137,8 @@ func (m *Manager) SupportsAction(nodeID, action string) bool { switch action { case "run": return n.Capabilities.Run + case "agent_task": + return n.Capabilities.Model case "camera_snap", "camera_clip": return n.Capabilities.Camera case "screen_record", "screen_snapshot": @@ -162,6 +164,10 @@ func (m *Manager) PickFor(action string) (NodeInfo, bool) { if n.Capabilities.Run { return n, true } + case "agent_task": + if n.Capabilities.Model { + return n, true + } case "camera_snap", "camera_clip": if n.Capabilities.Camera { return n, true diff --git a/pkg/nodes/transport.go b/pkg/nodes/transport.go index 8c4ceb3..4d82637 100644 --- a/pkg/nodes/transport.go +++ b/pkg/nodes/transport.go @@ -75,6 +75,8 @@ func actionHTTPPath(action string) string { return "/run" case "invoke": return "/invoke" + case "agent_task": + return "/agent/task" case "camera_snap": return "/camera/snap" case "camera_clip": diff --git a/pkg/nodes/types.go b/pkg/nodes/types.go index 9205c86..fcc7e50 100644 --- a/pkg/nodes/types.go +++ b/pkg/nodes/types.go @@ -6,6 +6,7 @@ import "time" type Capabilities struct { Run bool `json:"run"` Invoke bool `json:"invoke"` + Model bool `json:"model"` Camera bool `json:"camera"` Screen bool `json:"screen"` Location bool `json:"location"` @@ -23,6 +24,7 @@ type NodeInfo struct { Token string `json:"token,omitempty"` Capabilities Capabilities `json:"capabilities"` Actions []string `json:"actions,omitempty"` + Models []string `json:"models,omitempty"` RegisteredAt time.Time `json:"registered_at,omitempty"` LastSeenAt time.Time `json:"last_seen_at"` Online bool `json:"online"` @@ -32,6 +34,8 @@ type NodeInfo struct { type Request struct { Action string `json:"action"` Node string `json:"node,omitempty"` + Task string `json:"task,omitempty"` + Model string `json:"model,omitempty"` Args map[string]interface{} `json:"args,omitempty"` } diff --git a/pkg/tools/nodes_tool.go b/pkg/tools/nodes_tool.go index b8f2a7e..90cc2b7 100644 --- a/pkg/tools/nodes_tool.go +++ b/pkg/tools/nodes_tool.go @@ -22,10 +22,12 @@ func (t *NodesTool) Description() string { } func (t *NodesTool) Parameters() map[string]interface{} { return map[string]interface{}{"type": "object", "properties": map[string]interface{}{ - "action": map[string]interface{}{"type": "string", "description": "status|describe|run|invoke|camera_snap|camera_clip|screen_record|screen_snapshot|location_get|canvas_snapshot|canvas_action"}, + "action": map[string]interface{}{"type": "string", "description": "status|describe|run|invoke|agent_task|camera_snap|camera_clip|screen_record|screen_snapshot|location_get|canvas_snapshot|canvas_action"}, "node": map[string]interface{}{"type": "string", "description": "target node id"}, "mode": map[string]interface{}{"type": "string", "description": "auto|p2p|relay (default auto)"}, "args": map[string]interface{}{"type": "object", "description": "action args"}, + "task": map[string]interface{}{"type": "string", "description": "agent_task content for child node model"}, + "model": map[string]interface{}{"type": "string", "description": "optional model for agent_task"}, "command": map[string]interface{}{"type": "array", "description": "run command array shortcut"}, "facing": map[string]interface{}{"type": "string", "description": "camera facing: front|back|both"}, "duration_ms": map[string]interface{}{"type": "integer", "description": "clip/record duration"}, @@ -95,12 +97,17 @@ func (t *NodesTool) Execute(ctx context.Context, args map[string]interface{}) (s } reqArgs["duration_ms"] = di } + task, _ := args["task"].(string) + model, _ := args["model"].(string) + if action == "agent_task" && strings.TrimSpace(task) == "" { + return "", fmt.Errorf("invalid_args: agent_task requires task") + } if action == "canvas_action" { if act, _ := reqArgs["action"].(string); strings.TrimSpace(act) == "" { return "", fmt.Errorf("invalid_args: canvas_action requires args.action") } } - resp, err := t.router.Dispatch(ctx, nodes.Request{Action: action, Node: nodeID, Args: reqArgs}, mode) + resp, err := t.router.Dispatch(ctx, nodes.Request{Action: action, Node: nodeID, Task: strings.TrimSpace(task), Model: strings.TrimSpace(model), Args: reqArgs}, mode) if err != nil { return "", err }