From 744bb00f886d8177ae8ffb8ba8990d4176cad5ab Mon Sep 17 00:00:00 2001 From: Victor Vargas Date: Thu, 9 Jul 2026 12:08:32 -0700 Subject: [PATCH] feat(agent): add SubAgent runtime for nested, specialized agent loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements docs/phase2.md §5 (Sub-agents), pulled forward from the harness's item 2 work: SubAgent/SubAgentRegistry let a caller run a nested agent.Loop with its own persona/tools/iteration cap and get its final response back. Run doesn't set Approver/Sandbox, so a single Ask approval on the caller's own delegating tool covers the whole nested run (Ask-permission tools execute unprompted when Config.Approver is nil). rony-harness consumes this for its delegate tool (builder/planner). --- AGENTS.md | 2 +- docs/phase2.es.md | 10 +++--- docs/phase2.md | 10 +++--- pkg/agent/subagent.go | 46 +++++++++++++++++++++++++++ pkg/agent/subagent_test.go | 64 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 pkg/agent/subagent.go create mode 100644 pkg/agent/subagent_test.go diff --git a/AGENTS.md b/AGENTS.md index 87ec800..8e7b4bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,4 +85,4 @@ Reusable skills for any AI agent live in `.agents/skills//SKILL.md` — th ## Phase 2 awareness -Phase 2 features (MCP server/client, full RAG pipeline with Qdrant/sqlite-vec backends, skills system, sub-agents, observability) are planned but not in scope for initial implementation. Do not start implementing phase 2 code unless explicitly asked. Reference `docs/phase2.md` for spec when needed. +**Phase 2 is now in progress** (started 2026-07-09). Sub-agents (`docs/phase2.md` §5) landed first: `pkg/agent.SubAgent`/`SubAgentRegistry` (`pkg/agent/subagent.go`) let a caller run a specialized, nested `agent.Loop` and get its final response back — the harness uses this for its `delegate` tool (builder/planner). MCP server/client, full RAG pipeline with Qdrant/sqlite-vec backends, skills system, and observability remain unimplemented; don't start those unless explicitly asked. Reference `docs/phase2.md` for spec when needed. diff --git a/docs/phase2.es.md b/docs/phase2.es.md index 245be7a..931e6ae 100644 --- a/docs/phase2.es.md +++ b/docs/phase2.es.md @@ -6,7 +6,7 @@ **Versión:** 1.0 **Autor:** Victor Hugo Vargas **Fecha:** 2026-06-28 -**Estado:** Features avanzadas (post-MVP) +**Estado:** Features avanzadas (post-MVP) — en progreso desde 2026-07-09; Sub-agents (§5) ya implementado > 📚 **Documentos relacionados:** > - [`architecture.md`](./architecture.md) — Core interfaces (LLMClient, Tool, Agent Loop, etc.) @@ -29,7 +29,7 @@ Estas son features que **van después del MVP**. La separación es deliberada: - 🔌 **MCP Server completo** (Tools + Resources + Prompts + Sampling, Streamable HTTP) - 🧠 **RAG completo** (vector DB, episodic/semantic/procedural memory) - 📚 **Skills system** (SKILL.md on-demand) -- 🤖 **Sub-agents** (explore, code-review, general) +- 🤖 **Sub-agents** ✅ (`pkg/agent.SubAgent`/`SubAgentRegistry`; `rony-harness` lo usa para sus sub-agentes `builder`/`planner` en vez del trío `explore`/`code-review`/`general` de abajo — mismo mecanismo, set por defecto distinto, elegido según `rony-harness/TODO.md` §2) - 🔀 **Multi-provider con routing** (fallback chain, routing por task) - 🔒 **Sandbox avanzado** (network egress, prompt injection defense, secret redaction) - 📊 **Observability** (OpenTelemetry, cost tracking, trace visualization) @@ -339,11 +339,13 @@ type Registry interface { ## 🤖 5. Sub-agents +> ✅ **Implementado** (2026-07-09): `pkg/agent/subagent.go` tiene `SubAgent` (Name, Description, Persona, Tools, MaxIterations) y `SubAgentRegistry`, siguiendo §5.1–5.3. `SubAgent.Run` arma el `agent.Config` anidado y llama a `Loop.Run` — no setea `Approver`/`Sandbox`, así que una sola aprobación `Ask` sobre la tool tipo "delegate" del caller cubre toda la corrida anidada (las tools con Ask se ejecutan sin preguntar cuando `Config.Approver` es nil — ver `executeTool` en `pkg/agent/loop.go`). El campo `Model` y el código de `DefaultSubAgents`/registro de abajo son ilustrativos; `rony-harness` arma sus propios dos sub-agentes (`builder`, `planner`) en vez de eso — ver `rony-harness/TODO.md` §2 e `internal/cli/delegate_tool.go` en ese repo. + ### 5.1 Concepto Sub-agentes especializados que el agente principal invoca como tools. -### 5.2 Sub-agents Predefinidos +### 5.2 Sub-agents Predefinidos (ilustrativo — no es lo implementado; ver nota arriba) ```go var DefaultSubAgents = []SubAgent{ @@ -696,7 +698,7 @@ import "github.com/tetratelabs/wazero" ### Semana 10: Skills + Sub-agents - [ ] SKILL.md discovery - [ ] Auto-load por description match -- [ ] Sub-agents: explore, code-review, general +- [x] Sub-agents: `SubAgent`/`SubAgentRegistry` + `Run` (el harness arma `builder`/`planner` con su tool `delegate`) ### Semana 11: Sandbox Avanzado + Observability - [ ] Network egress policy diff --git a/docs/phase2.md b/docs/phase2.md index 5e86c69..e91ec3f 100644 --- a/docs/phase2.md +++ b/docs/phase2.md @@ -3,7 +3,7 @@ **Version:** 1.0 **Author:** Victor Hugo Vargas **Date:** 2026-06-28 -**Status:** Advanced features (post-MVP) +**Status:** Advanced features (post-MVP) — in progress since 2026-07-09; Sub-agents (§5) shipped > 📚 **Related documents:** > - [`architecture.md`](./architecture.md) — Core interfaces (LLMClient, Tool, Agent Loop, etc.) @@ -26,7 +26,7 @@ These are features that **come after the MVP**. The separation is deliberate: - 🔌 **Full MCP Server** (Tools + Resources + Prompts + Sampling, Streamable HTTP) - 🧠 **Full RAG** (vector DB, episodic/semantic/procedural memory) - 📚 **Skills system** (SKILL.md on-demand) -- 🤖 **Sub-agents** (explore, code-review, general) +- 🤖 **Sub-agents** ✅ (`pkg/agent.SubAgent`/`SubAgentRegistry`; the harness's `rony-harness` consumes this for its `builder`/`planner` sub-agents instead of the `explore`/`code-review`/`general` trio sketched below — same mechanism, different default set, chosen per `rony-harness/TODO.md` §2) - 🔀 **Multi-provider with routing** (fallback chain, routing per task) - 🔒 **Advanced sandbox** (network egress, prompt injection defense, secret redaction) - 📊 **Observability** (OpenTelemetry, cost tracking, trace visualization) @@ -336,11 +336,13 @@ type Registry interface { ## 🤖 5. Sub-agents +> ✅ **Implemented** (2026-07-09): `pkg/agent/subagent.go` has `SubAgent` (Name, Description, Persona, Tools, MaxIterations) and `SubAgentRegistry`, matching §5.1–5.3 below. `SubAgent.Run` builds the nested `agent.Config` and calls `Loop.Run` — no `Approver`/`Sandbox` is set on it, so a single `Ask` approval on the caller's delegate-style tool covers the whole nested run (Ask-gated tools execute unprompted when `Config.Approver` is nil — see `pkg/agent/loop.go`'s `executeTool`). The `Model` field and `DefaultSubAgents`/registry-building code below are illustrative; `rony-harness` builds its own two sub-agents (`builder`, `planner`) instead — see `rony-harness/TODO.md` §2 and `internal/cli/delegate_tool.go` there. + ### 5.1 Concept Specialized sub-agents that the main agent invokes as tools. -### 5.2 Default sub-agents +### 5.2 Default sub-agents (illustrative — not what's implemented; see the note above) ```go var DefaultSubAgents = []SubAgent{ @@ -695,7 +697,7 @@ import "github.com/tetratelabs/wazero" ### Week 10: Skills + Sub-agents - [ ] SKILL.md discovery - [ ] Auto-load by description match -- [ ] Sub-agents: explore, code-review, general +- [x] Sub-agents: `SubAgent`/`SubAgentRegistry` + `Run` (harness wires `builder`/`planner` via its `delegate` tool) ### Week 11: Advanced Sandbox + Observability - [ ] Network egress policy diff --git a/pkg/agent/subagent.go b/pkg/agent/subagent.go new file mode 100644 index 0000000..e30c57f --- /dev/null +++ b/pkg/agent/subagent.go @@ -0,0 +1,46 @@ +package agent + +import ( + "context" + + "github.com/VictorVargas/rony-llm-agent/pkg/llm" + "github.com/VictorVargas/rony-llm-agent/pkg/persona" + "github.com/VictorVargas/rony-llm-agent/pkg/tools" +) + +// SubAgent describes a specialized agent invocable via a delegate tool (see +// docs/phase2.md §5). The caller (the harness) is responsible for building +// Persona and Tools — the boundary is the same as for the main Loop: this +// package orchestrates, it doesn't decide personas or wire concrete tools. +type SubAgent struct { + Name string + Description string + Persona persona.Persona + Tools tools.Registry + MaxIterations int +} + +// Run executes the sub-agent's task to completion using llmClient and the +// given AGENTS.md content, and returns its final response. +func (s SubAgent) Run(ctx context.Context, llmClient llm.LLMClient, agentsMD string, task string) (Response, error) { + cfg := Config{ + LLM: llmClient, + Persona: s.Persona, + Tools: s.Tools, + MaxIters: s.MaxIterations, + AgentsMD: agentsMD, + } + if cfg.MaxIters == 0 { + cfg.MaxIters = DefaultMaxIterations + } + return New(cfg).Run(ctx, task) +} + +// SubAgentRegistry looks up SubAgents by name for the delegate tool. +type SubAgentRegistry map[string]SubAgent + +// Get returns the named sub-agent, if registered. +func (r SubAgentRegistry) Get(name string) (SubAgent, bool) { + s, ok := r[name] + return s, ok +} diff --git a/pkg/agent/subagent_test.go b/pkg/agent/subagent_test.go new file mode 100644 index 0000000..0ab9d14 --- /dev/null +++ b/pkg/agent/subagent_test.go @@ -0,0 +1,64 @@ +package agent + +import ( + "context" + "testing" + + llm "github.com/VictorVargas/rony-llm-agent/pkg/llm" + "github.com/VictorVargas/rony-llm-agent/pkg/persona" + "github.com/VictorVargas/rony-llm-agent/pkg/tools" +) + +func TestSubAgent_Run(t *testing.T) { + mockClient := &mockLLM{ + generateFunc: func(ctx context.Context, req llm.CompletionRequest) (llm.CompletionResponse, error) { + return llm.CompletionResponse{Content: "sub-agent done"}, nil + }, + } + + sa := SubAgent{ + Name: "planner", + Persona: persona.DefaultPersona(), + Tools: tools.NewRegistry(), + } + + resp, err := sa.Run(context.Background(), mockClient, "", "plan the task") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Content != "sub-agent done" { + t.Errorf("expected 'sub-agent done', got %q", resp.Content) + } +} + +func TestSubAgent_Run_DefaultsMaxIterations(t *testing.T) { + mockClient := &mockLLM{ + generateFunc: func(ctx context.Context, req llm.CompletionRequest) (llm.CompletionResponse, error) { + return llm.CompletionResponse{Content: "done"}, nil + }, + } + + sa := SubAgent{Persona: persona.DefaultPersona(), Tools: tools.NewRegistry()} + if sa.MaxIterations != 0 { + t.Fatalf("expected zero-value MaxIterations for this test, got %d", sa.MaxIterations) + } + + if _, err := sa.Run(context.Background(), mockClient, "", "hi"); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSubAgentRegistry_Get(t *testing.T) { + reg := SubAgentRegistry{ + "builder": SubAgent{Name: "builder"}, + } + + got, ok := reg.Get("builder") + if !ok || got.Name != "builder" { + t.Fatalf("expected to find 'builder', got %+v, ok=%v", got, ok) + } + + if _, ok := reg.Get("missing"); ok { + t.Error("expected 'missing' to not be found") + } +}