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:
parent
3b36ad2cf8
commit
744bb00f88
5 changed files with 123 additions and 9 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.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
|
### 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
|
||||||
|
|
|
||||||
|
|
@ -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.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
|
### 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
46
pkg/agent/subagent.go
Normal 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
|
||||||
|
}
|
||||||
64
pkg/agent/subagent_test.go
Normal file
64
pkg/agent/subagent_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue