mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-03 03:18:58 +08:00
feat: expand node agent routing and media artifacts
This commit is contained in:
@@ -28,15 +28,16 @@ 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|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", "items": map[string]interface{}{"type": "string"}, "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"},
|
||||
"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"},
|
||||
"artifact_paths": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "optional workspace-relative file paths to bring back as artifacts for agent_task"},
|
||||
"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", "items": map[string]interface{}{"type": "string"}, "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"},
|
||||
}, "required": []string{"action"}}
|
||||
}
|
||||
|
||||
@@ -66,26 +67,15 @@ func (t *NodesTool) Execute(ctx context.Context, args map[string]interface{}) (s
|
||||
b, _ := json.Marshal(t.manager.List())
|
||||
return string(b), nil
|
||||
default:
|
||||
if nodeID == "" {
|
||||
if picked, ok := t.manager.PickFor(action); ok {
|
||||
nodeID = picked.ID
|
||||
}
|
||||
}
|
||||
if nodeID == "" {
|
||||
return "", fmt.Errorf("no eligible node found for action=%s", action)
|
||||
}
|
||||
if !t.manager.SupportsAction(nodeID, action) {
|
||||
return "", fmt.Errorf("node %s does not support action=%s", nodeID, action)
|
||||
}
|
||||
if t.router == nil {
|
||||
return "", fmt.Errorf("nodes transport router not configured")
|
||||
}
|
||||
reqArgs := map[string]interface{}{}
|
||||
if raw, ok := args["args"].(map[string]interface{}); ok {
|
||||
for k, v := range raw {
|
||||
reqArgs[k] = v
|
||||
}
|
||||
}
|
||||
if rawPaths, ok := args["artifact_paths"].([]interface{}); ok && len(rawPaths) > 0 {
|
||||
reqArgs["artifact_paths"] = rawPaths
|
||||
}
|
||||
if cmd, ok := args["command"].([]interface{}); ok && len(cmd) > 0 {
|
||||
reqArgs["command"] = cmd
|
||||
}
|
||||
@@ -113,7 +103,21 @@ func (t *NodesTool) Execute(ctx context.Context, args map[string]interface{}) (s
|
||||
return "", fmt.Errorf("invalid_args: canvas_action requires args.action")
|
||||
}
|
||||
}
|
||||
if nodeID == "" {
|
||||
if picked, ok := t.manager.PickRequest(nodes.Request{Action: action, Task: task, Model: model, Args: reqArgs}, mode); ok {
|
||||
nodeID = picked.ID
|
||||
}
|
||||
}
|
||||
if nodeID == "" {
|
||||
return "", fmt.Errorf("no eligible node found for action=%s", action)
|
||||
}
|
||||
req := nodes.Request{Action: action, Node: nodeID, Task: task, Model: model, Args: reqArgs}
|
||||
if !t.manager.SupportsRequest(nodeID, req) {
|
||||
return "", fmt.Errorf("node %s does not support action=%s", nodeID, action)
|
||||
}
|
||||
if t.router == nil {
|
||||
return "", fmt.Errorf("nodes transport router not configured")
|
||||
}
|
||||
started := time.Now()
|
||||
resp, err := t.router.Dispatch(ctx, req, mode)
|
||||
durationMs := int(time.Since(started).Milliseconds())
|
||||
@@ -150,6 +154,12 @@ func (t *NodesTool) writeAudit(req nodes.Request, resp nodes.Response, mode stri
|
||||
if fallback, _ := resp.Payload["fallback_from"].(string); strings.TrimSpace(fallback) != "" {
|
||||
row["fallback_from"] = strings.TrimSpace(fallback)
|
||||
}
|
||||
if count, kinds := artifactAuditSummary(resp.Payload["artifacts"]); count > 0 {
|
||||
row["artifact_count"] = count
|
||||
if len(kinds) > 0 {
|
||||
row["artifact_kinds"] = kinds
|
||||
}
|
||||
}
|
||||
b, _ := json.Marshal(row)
|
||||
f, err := os.OpenFile(t.auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
@@ -158,3 +168,29 @@ func (t *NodesTool) writeAudit(req nodes.Request, resp nodes.Response, mode stri
|
||||
defer f.Close()
|
||||
_, _ = f.Write(append(b, '\n'))
|
||||
}
|
||||
|
||||
func artifactAuditSummary(raw interface{}) (int, []string) {
|
||||
items, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
if typed, ok := raw.([]map[string]interface{}); ok {
|
||||
items = make([]interface{}, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
kinds := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
row, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if kind, _ := row["kind"].(string); strings.TrimSpace(kind) != "" {
|
||||
kinds = append(kinds, strings.TrimSpace(kind))
|
||||
}
|
||||
}
|
||||
return len(items), kinds
|
||||
}
|
||||
|
||||
@@ -387,9 +387,10 @@ func (s *SubagentProfileStore) nodeProfileLocked(agentID string) (SubagentProfil
|
||||
if isLocalNode(node.ID) {
|
||||
continue
|
||||
}
|
||||
profile := profileFromNode(node, parentAgentID)
|
||||
if profile.AgentID == id {
|
||||
return profile, true
|
||||
for _, profile := range profilesFromNode(node, parentAgentID) {
|
||||
if profile.AgentID == id {
|
||||
return profile, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return SubagentProfile{}, false
|
||||
@@ -439,20 +440,18 @@ func (s *SubagentProfileStore) nodeProfilesLocked() []SubagentProfile {
|
||||
if isLocalNode(node.ID) {
|
||||
continue
|
||||
}
|
||||
profile := profileFromNode(node, parentAgentID)
|
||||
if profile.AgentID == "" {
|
||||
continue
|
||||
profiles := profilesFromNode(node, parentAgentID)
|
||||
for _, profile := range profiles {
|
||||
if profile.AgentID == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, profile)
|
||||
}
|
||||
out = append(out, profile)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func profileFromNode(node nodes.NodeInfo, parentAgentID string) SubagentProfile {
|
||||
agentID := nodeBranchAgentID(node.ID)
|
||||
if agentID == "" {
|
||||
return SubagentProfile{}
|
||||
}
|
||||
func profilesFromNode(node nodes.NodeInfo, parentAgentID string) []SubagentProfile {
|
||||
name := strings.TrimSpace(node.Name)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(node.ID)
|
||||
@@ -461,17 +460,39 @@ func profileFromNode(node nodes.NodeInfo, parentAgentID string) SubagentProfile
|
||||
if !node.Online {
|
||||
status = "disabled"
|
||||
}
|
||||
return normalizeSubagentProfile(SubagentProfile{
|
||||
AgentID: agentID,
|
||||
rootAgentID := nodeBranchAgentID(node.ID)
|
||||
if rootAgentID == "" {
|
||||
return nil
|
||||
}
|
||||
out := []SubagentProfile{normalizeSubagentProfile(SubagentProfile{
|
||||
AgentID: rootAgentID,
|
||||
Name: name + " Main Agent",
|
||||
Transport: "node",
|
||||
NodeID: strings.TrimSpace(node.ID),
|
||||
ParentAgentID: parentAgentID,
|
||||
Role: "remote_main",
|
||||
MemoryNamespace: agentID,
|
||||
MemoryNamespace: rootAgentID,
|
||||
Status: status,
|
||||
ManagedBy: "node_registry",
|
||||
})
|
||||
})}
|
||||
for _, agent := range node.Agents {
|
||||
agentID := normalizeSubagentIdentifier(agent.ID)
|
||||
if agentID == "" || agentID == "main" {
|
||||
continue
|
||||
}
|
||||
out = append(out, normalizeSubagentProfile(SubagentProfile{
|
||||
AgentID: nodeChildAgentID(node.ID, agentID),
|
||||
Name: nodeChildAgentDisplayName(name, agent),
|
||||
Transport: "node",
|
||||
NodeID: strings.TrimSpace(node.ID),
|
||||
ParentAgentID: rootAgentID,
|
||||
Role: strings.TrimSpace(agent.Role),
|
||||
MemoryNamespace: nodeChildAgentID(node.ID, agentID),
|
||||
Status: status,
|
||||
ManagedBy: "node_registry",
|
||||
}))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func nodeBranchAgentID(nodeID string) string {
|
||||
@@ -482,6 +503,27 @@ func nodeBranchAgentID(nodeID string) string {
|
||||
return "node." + id + ".main"
|
||||
}
|
||||
|
||||
func nodeChildAgentID(nodeID, agentID string) string {
|
||||
nodeID = normalizeSubagentIdentifier(nodeID)
|
||||
agentID = normalizeSubagentIdentifier(agentID)
|
||||
if nodeID == "" || agentID == "" {
|
||||
return ""
|
||||
}
|
||||
return "node." + nodeID + "." + agentID
|
||||
}
|
||||
|
||||
func nodeChildAgentDisplayName(nodeName string, agent nodes.AgentInfo) string {
|
||||
base := strings.TrimSpace(agent.DisplayName)
|
||||
if base == "" {
|
||||
base = strings.TrimSpace(agent.ID)
|
||||
}
|
||||
nodeName = strings.TrimSpace(nodeName)
|
||||
if nodeName == "" {
|
||||
return base
|
||||
}
|
||||
return nodeName + " / " + base
|
||||
}
|
||||
|
||||
func isLocalNode(nodeID string) bool {
|
||||
return normalizeSubagentIdentifier(nodeID) == "local"
|
||||
}
|
||||
|
||||
@@ -208,6 +208,10 @@ func TestSubagentProfileStoreIncludesNodeMainBranchProfiles(t *testing.T) {
|
||||
ID: "edge-dev",
|
||||
Name: "Edge Dev",
|
||||
Online: true,
|
||||
Agents: []nodes.AgentInfo{
|
||||
{ID: "main", DisplayName: "Main Agent", Role: "orchestrator", Type: "router"},
|
||||
{ID: "coder", DisplayName: "Code Agent", Role: "code", Type: "worker"},
|
||||
},
|
||||
Capabilities: nodes.Capabilities{
|
||||
Model: true,
|
||||
},
|
||||
@@ -227,6 +231,19 @@ func TestSubagentProfileStoreIncludesNodeMainBranchProfiles(t *testing.T) {
|
||||
if profile.ParentAgentID != "main" {
|
||||
t.Fatalf("expected main parent agent, got %+v", profile)
|
||||
}
|
||||
childProfile, ok, err := store.Get("node.edge-dev.coder")
|
||||
if err != nil {
|
||||
t.Fatalf("get child profile failed: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("expected child node-backed profile")
|
||||
}
|
||||
if childProfile.ManagedBy != "node_registry" || childProfile.Transport != "node" || childProfile.NodeID != "edge-dev" {
|
||||
t.Fatalf("unexpected child node profile: %+v", childProfile)
|
||||
}
|
||||
if childProfile.ParentAgentID != "node.edge-dev.main" {
|
||||
t.Fatalf("expected child profile to attach to remote main, got %+v", childProfile)
|
||||
}
|
||||
if _, err := store.Upsert(SubagentProfile{AgentID: profile.AgentID}); err == nil {
|
||||
t.Fatalf("expected node-managed upsert to fail")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user