feat(agent): add SubAgent runtime for nested, specialized agent loops

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).
This commit is contained in:
Victor Hugo Vargas Servin 2026-07-09 12:08:32 -07:00
parent 3b36ad2cf8
commit 744bb00f88
5 changed files with 123 additions and 9 deletions

View file

@ -85,4 +85,4 @@ Reusable skills for any AI agent live in `.agents/skills/<name>/SKILL.md` — th
## Phase 2 awareness ## 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.

View file

@ -6,7 +6,7 @@
**Versión:** 1.0 **Versión:** 1.0
**Autor:** Victor Hugo Vargas **Autor:** Victor Hugo Vargas
**Fecha:** 2026-06-28 **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:** > 📚 **Documentos relacionados:**
> - [`architecture.md`](./architecture.md) — Core interfaces (LLMClient, Tool, Agent Loop, etc.) > - [`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) - 🔌 **MCP Server completo** (Tools + Resources + Prompts + Sampling, Streamable HTTP)
- 🧠 **RAG completo** (vector DB, episodic/semantic/procedural memory) - 🧠 **RAG completo** (vector DB, episodic/semantic/procedural memory)
- 📚 **Skills system** (SKILL.md on-demand) - 📚 **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) - 🔀 **Multi-provider con routing** (fallback chain, routing por task)
- 🔒 **Sandbox avanzado** (network egress, prompt injection defense, secret redaction) - 🔒 **Sandbox avanzado** (network egress, prompt injection defense, secret redaction)
- 📊 **Observability** (OpenTelemetry, cost tracking, trace visualization) - 📊 **Observability** (OpenTelemetry, cost tracking, trace visualization)
@ -339,11 +339,13 @@ type Registry interface {
## 🤖 5. Sub-agents ## 🤖 5. Sub-agents
> ✅ **Implementado** (2026-07-09): `pkg/agent/subagent.go` tiene `SubAgent` (Name, Description, Persona, Tools, MaxIterations) y `SubAgentRegistry`, siguiendo §5.15.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 ### 5.1 Concepto
Sub-agentes especializados que el agente principal invoca como tools. 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 ```go
var DefaultSubAgents = []SubAgent{ var DefaultSubAgents = []SubAgent{
@ -696,7 +698,7 @@ import "github.com/tetratelabs/wazero"
### Semana 10: Skills + Sub-agents ### Semana 10: Skills + Sub-agents
- [ ] SKILL.md discovery - [ ] SKILL.md discovery
- [ ] Auto-load por description match - [ ] 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 ### Semana 11: Sandbox Avanzado + Observability
- [ ] Network egress policy - [ ] Network egress policy

View file

@ -3,7 +3,7 @@
**Version:** 1.0 **Version:** 1.0
**Author:** Victor Hugo Vargas **Author:** Victor Hugo Vargas
**Date:** 2026-06-28 **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:** > 📚 **Related documents:**
> - [`architecture.md`](./architecture.md) — Core interfaces (LLMClient, Tool, Agent Loop, etc.) > - [`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 MCP Server** (Tools + Resources + Prompts + Sampling, Streamable HTTP)
- 🧠 **Full RAG** (vector DB, episodic/semantic/procedural memory) - 🧠 **Full RAG** (vector DB, episodic/semantic/procedural memory)
- 📚 **Skills system** (SKILL.md on-demand) - 📚 **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) - 🔀 **Multi-provider with routing** (fallback chain, routing per task)
- 🔒 **Advanced sandbox** (network egress, prompt injection defense, secret redaction) - 🔒 **Advanced sandbox** (network egress, prompt injection defense, secret redaction)
- 📊 **Observability** (OpenTelemetry, cost tracking, trace visualization) - 📊 **Observability** (OpenTelemetry, cost tracking, trace visualization)
@ -336,11 +336,13 @@ type Registry interface {
## 🤖 5. Sub-agents ## 🤖 5. Sub-agents
> ✅ **Implemented** (2026-07-09): `pkg/agent/subagent.go` has `SubAgent` (Name, Description, Persona, Tools, MaxIterations) and `SubAgentRegistry`, matching §5.15.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 ### 5.1 Concept
Specialized sub-agents that the main agent invokes as tools. 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 ```go
var DefaultSubAgents = []SubAgent{ var DefaultSubAgents = []SubAgent{
@ -695,7 +697,7 @@ import "github.com/tetratelabs/wazero"
### Week 10: Skills + Sub-agents ### Week 10: Skills + Sub-agents
- [ ] SKILL.md discovery - [ ] SKILL.md discovery
- [ ] Auto-load by description match - [ ] 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 ### Week 11: Advanced Sandbox + Observability
- [ ] Network egress policy - [ ] Network egress policy

46
pkg/agent/subagent.go Normal file
View file

@ -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
}

View file

@ -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")
}
}