feat(0144a): tool registry framework para device-mesh
Anade pkg/tools/devicemesh con Client HTTP al device_agent + ToolRegistry con 16 tools standard (exec, fs.*, git.*, docker.*, proc.*, pkg.*, shell.eval). RegisterBuiltins filtra por mode user/sudo via RequiresApproval flag. Hook al pkg/decision con ActionKindDeviceMesh + DeviceMeshAction. Runner soporta dispatch via NewRunnerWithDeviceMesh (back-compat NewRunner). Tests: 25 nuevos en devicemesh + 4 en runner. Build clean.
This commit is contained in:
+55
-3
@@ -3,15 +3,27 @@ package effects
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
"github.com/enmanuel/agents/pkg/tools/devicemesh"
|
||||
"github.com/enmanuel/agents/shell/logger"
|
||||
"github.com/enmanuel/agents/shell/ssh"
|
||||
)
|
||||
|
||||
// DeviceMeshCaller is the minimal interface that the Runner needs from a
|
||||
// devicemesh.ToolRegistry. It is an interface (rather than a concrete type)
|
||||
// so tests can mock without spinning up an HTTP server.
|
||||
type DeviceMeshCaller interface {
|
||||
Call(ctx context.Context, toolName string, input map[string]any) (any, error)
|
||||
}
|
||||
|
||||
// Compile-time check: the real registry satisfies the interface.
|
||||
var _ DeviceMeshCaller = (*devicemesh.ToolRegistry)(nil)
|
||||
|
||||
// Result holds the outcome of executing a single action.
|
||||
type Result struct {
|
||||
Action decision.Action
|
||||
@@ -32,16 +44,27 @@ type MatrixSender interface {
|
||||
|
||||
// Runner interprets actions and executes them.
|
||||
type Runner struct {
|
||||
matrix MatrixSender
|
||||
ssh *ssh.Executor
|
||||
logger *slog.Logger
|
||||
matrix MatrixSender
|
||||
ssh *ssh.Executor
|
||||
deviceMesh DeviceMeshCaller
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewRunner creates a Runner with the provided dependencies.
|
||||
// The device mesh tool registry is left nil; ActionKindDeviceMesh actions
|
||||
// will be rejected with a clear error. Use NewRunnerWithDeviceMesh to wire
|
||||
// the mesh caller.
|
||||
func NewRunner(matrix MatrixSender, ssh *ssh.Executor, logger *slog.Logger) *Runner {
|
||||
return &Runner{matrix: matrix, ssh: ssh, logger: logger}
|
||||
}
|
||||
|
||||
// NewRunnerWithDeviceMesh wires a Runner with a DeviceMeshCaller, enabling
|
||||
// ActionKindDeviceMesh dispatch. Used by the launcher when an agent has
|
||||
// cfg.DeviceMesh.Enabled = true (wiring lives in 0144c).
|
||||
func NewRunnerWithDeviceMesh(matrix MatrixSender, ssh *ssh.Executor, dm DeviceMeshCaller, logger *slog.Logger) *Runner {
|
||||
return &Runner{matrix: matrix, ssh: ssh, deviceMesh: dm, logger: logger}
|
||||
}
|
||||
|
||||
// Execute runs each action sequentially and returns results.
|
||||
func (r *Runner) Execute(ctx context.Context, roomID string, actions []decision.Action) []Result {
|
||||
r.logger.Debug("effects_batch", "room", roomID, "count", len(actions))
|
||||
@@ -89,7 +112,36 @@ func (r *Runner) executeOne(ctx context.Context, roomID string, a decision.Actio
|
||||
}
|
||||
return Result{Action: a, Output: output, Err: res.Err}
|
||||
|
||||
case decision.ActionKindDeviceMesh:
|
||||
if a.DeviceMesh == nil {
|
||||
return Result{Action: a, Err: fmt.Errorf("nil device_mesh action")}
|
||||
}
|
||||
if r.deviceMesh == nil {
|
||||
return Result{Action: a, Err: fmt.Errorf("device_mesh action received but Runner has no DeviceMeshCaller (build with NewRunnerWithDeviceMesh)")}
|
||||
}
|
||||
result, err := r.deviceMesh.Call(ctx, a.DeviceMesh.Tool, a.DeviceMesh.Input)
|
||||
output := formatDeviceMeshResult(result)
|
||||
return Result{Action: a, Output: output, Err: err}
|
||||
|
||||
default:
|
||||
return Result{Action: a, Err: fmt.Errorf("unhandled action kind: %s", a.Kind)}
|
||||
}
|
||||
}
|
||||
|
||||
// formatDeviceMeshResult renders the tool result as a stable JSON string
|
||||
// suitable for embedding in a tool_result message to the LLM. Errors during
|
||||
// marshaling collapse to a printable Go representation — never panic, never
|
||||
// drop data on the floor.
|
||||
func formatDeviceMeshResult(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user