feat(0144c): launcher wiring + adapter al tool-use loop LLM

Schema DeviceMeshConfig en AgentConfig. Adapter ToolsForLLM convierte
ToolSpec → tools.Tool transparente al LLM existente. URL via env var
override. tools_allowed filter. agent-wsl-lucas blank import en launcher.

LLM ve los tools como cualquier otra herramienta. Effects runner ya
soporta ActionKindDeviceMesh como fallback. Build + tests verdes.
This commit is contained in:
2026-05-24 14:07:13 +02:00
parent 4c5bf95def
commit 61606d450d
6 changed files with 457 additions and 2 deletions
+111
View File
@@ -209,3 +209,114 @@ skills:
t.Error("security.sanitize.enabled should be true")
}
}
// TestDeviceMeshConfig_Parse verifies that the device_mesh block parses into
// the expected DeviceMeshConfig pointer with both YAML key variants (host vs
// device_id, timeout_seconds vs client_timeout_s, tools_allowed list).
func TestDeviceMeshConfig_Parse(t *testing.T) {
const yamlBody = `
agent:
id: agent-home-wsl
name: home wsl
enabled: true
matrix:
homeserver: "https://matrix.example.com"
user_id: "@agent-home-wsl:matrix.example.com"
llm:
primary:
provider: anthropic
model: claude-sonnet
device_mesh:
enabled: true
device_id: home-wsl
mode: user
device_agent_url: "http://10.42.0.10:7474"
device_agent_url_env: AGENT_HOME_WSL_DEVICE_MESH_URL
manifest_id: manifest_home-wsl_v1
client_timeout_s: 60
tools_allowed:
- exec
- fs.read
- fs.list
`
var cfg AgentConfig
if err := yaml.Unmarshal([]byte(yamlBody), &cfg); err != nil {
t.Fatalf("parse: %v", err)
}
if cfg.DeviceMesh == nil {
t.Fatalf("expected DeviceMesh to be non-nil")
}
dm := cfg.DeviceMesh
if !dm.Enabled {
t.Error("enabled should be true")
}
if dm.DeviceID != "home-wsl" {
t.Errorf("device_id: got %q", dm.DeviceID)
}
if dm.ResolvedHost() != "home-wsl" {
t.Errorf("ResolvedHost(): got %q", dm.ResolvedHost())
}
if dm.Mode != "user" {
t.Errorf("mode: got %q", dm.Mode)
}
if dm.DeviceAgentURL != "http://10.42.0.10:7474" {
t.Errorf("device_agent_url: got %q", dm.DeviceAgentURL)
}
if dm.URLEnv != "AGENT_HOME_WSL_DEVICE_MESH_URL" {
t.Errorf("device_agent_url_env: got %q", dm.URLEnv)
}
if dm.ManifestID != "manifest_home-wsl_v1" {
t.Errorf("manifest_id: got %q", dm.ManifestID)
}
if dm.ResolvedTimeoutSeconds() != 60 {
t.Errorf("ResolvedTimeoutSeconds(): got %d", dm.ResolvedTimeoutSeconds())
}
if len(dm.ToolsAllowed) != 3 {
t.Errorf("tools_allowed: got %d entries", len(dm.ToolsAllowed))
}
}
// TestDeviceMeshConfig_Absent ensures the field stays nil when the block is
// not present in YAML — the runtime relies on the nil-check to short-circuit.
func TestDeviceMeshConfig_Absent(t *testing.T) {
const yamlBody = `
agent:
id: plain-bot
enabled: true
matrix:
homeserver: "https://matrix.example.com"
user_id: "@plain-bot:matrix.example.com"
llm:
primary:
provider: openai
model: gpt-4o
`
var cfg AgentConfig
if err := yaml.Unmarshal([]byte(yamlBody), &cfg); err != nil {
t.Fatalf("parse: %v", err)
}
if cfg.DeviceMesh != nil {
t.Errorf("expected nil DeviceMesh, got %+v", cfg.DeviceMesh)
}
}
// TestDeviceMeshConfig_TimeoutFallback verifies that timeout_seconds is used
// when client_timeout_s is absent.
func TestDeviceMeshConfig_TimeoutFallback(t *testing.T) {
dm := &DeviceMeshConfig{TimeoutSeconds: 45}
if got := dm.ResolvedTimeoutSeconds(); got != 45 {
t.Errorf("expected 45, got %d", got)
}
dm2 := &DeviceMeshConfig{ClientTimeoutS: 90}
if got := dm2.ResolvedTimeoutSeconds(); got != 90 {
t.Errorf("expected 90, got %d", got)
}
// TimeoutSeconds wins when both set.
dm3 := &DeviceMeshConfig{TimeoutSeconds: 30, ClientTimeoutS: 60}
if got := dm3.ResolvedTimeoutSeconds(); got != 30 {
t.Errorf("expected 30, got %d", got)
}
if (*DeviceMeshConfig)(nil).ResolvedTimeoutSeconds() != 0 {
t.Errorf("nil receiver should return 0")
}
}