# πŸ—οΈ Rony LLM Agent β€” Architecture **VersiΓ³n:** 1.0 **Autor:** Victor Hugo Vargas **Fecha:** 2026-06-28 **Estado:** EspecificaciΓ³n de arquitectura de la librerΓ­a > πŸ“š **Otros documentos:** > - [`README.md`](./README.md) β€” Overview de la librerΓ­a > - [`components.md`](./components.md) β€” Referencia detallada por paquete > - [`phase2.md`](./phase2.md) β€” Features avanzadas (MCP, RAG completo, Skills, etc.) > - [`../../METHODOLOGY.md`](../../METHODOLOGY.md) β€” CΓ³mo construimos (SDD + DDD + Hexagonal) > > πŸ“ **MetodologΓ­a:** Esta librerΓ­a sigue el enfoque **SDD + DDD + Hexagonal Architecture**. Ver [`METHODOLOGY.md`](../../METHODOLOGY.md) para los detalles. --- ## 🎯 1. VisiΓ³n de la LibrerΓ­a ### 1.1 ΒΏQuΓ© es rony-llm-agent? Una librerΓ­a Go que provee toda la lΓ³gica **genΓ©rica y reusable** para construir agentes basados en LLMs. No es un producto final β€” es el cimiento sobre el cual se construyen productos especΓ­ficos. **Productos que la consumen:** - [`VictorVargas/rony-harness`](https://github.com/VictorVargas/rony-harness) β€” AI agent harness para desarrollo de software (TUI CLI) - [`VictorVargas/rony-chat-bot`](https://github.com/VictorVargas/rony-chat-bot) β€” Chatbot HTTP para portfolios y sitios web ### 1.2 Principios de diseΓ±o - **Reusable, no opinionated.** No fuerza un tipo de UI, deployment, ni use case. - **Hexagonal puro.** Ports & adapters β€” toda dependencia externa va detrΓ‘s de una interfaz. - **Streaming-first.** Usa `iter.Seq2` de Go 1.23+ para streaming natural sin callbacks. - **Seguridad por defecto.** Sandbox de paths con `os.Root` (Go 1.24+) en filesystem. - **Zero magic.** No reflection, no codegen, no DSLs. Go idiomΓ‘tico y explΓ­cito. - **Configurable por YAML.** Sin surprises, todo lo que afecta comportamiento es declarativo. ### 1.3 Lo que NO es - ❌ No es un CLI β€” eso es responsabilidad del producto (ej. `harness`) - ❌ No es un servidor HTTP β€” eso es responsabilidad del producto (ej. `chat-bot`) - ❌ No fuerza un modelo o provider especΓ­fico β€” adapters intercambiables - ❌ No tiene estado persistente propio β€” eso lo maneja cada producto --- ## πŸ—οΈ 2. Arquitectura Hexagonal (Ports & Adapters) ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Productos que consumen β”‚ β”‚ (harness, chat-bot, dealer-bot, etc.) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ usan interfaces pΓΊblicas β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ rony-llm-agent (pkg/ β€” API pΓΊblica) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ agent β”‚ β”‚ llm β”‚ β”‚ persona β”‚ β”‚ tools β”‚ ... β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ usa ports (interfaces) β”‚ β”‚ β–Ό β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ PORTS (interfaces puras) β”‚ β”‚ β”‚ β”‚ LLMClient, VectorDB, Embedder, ToolRegistry, ... β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ implementadas por adapters β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Adapters (en pkg/*/internal/) β”‚ β”‚ β€’ providers/anthropic, openai, ollama, llamacpp β”‚ β”‚ β€’ backends/chroma, qdrant, sqlite β”‚ β”‚ β€’ embeddings/ollama, onnx β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### 2.1 Capas **Ports (interfaces puras)** - Definen QUΓ‰ hace el dominio, no CΓ“MO - Son contratos sin implementaciΓ³n - Permiten sustituir adapters sin tocar lΓ³gica de negocio **Domain (pkg/)** - Contiene toda la lΓ³gica reusable - NO depende de nada externo - Solo importa interfaces de sus propios ports **Adapters (internals de cada adapter)** - Implementaciones concretas de los ports - AquΓ­ viven las dependencias externas (HTTP clients, filesystem, DB drivers) --- ## 🧠 3. Core Interfaces ### 3.1 LLMClient (`pkg/llm/`) AbstracciΓ³n multi-provider. Los productos nunca tocan un SDK de provider directamente β€” siempre pasan por esta interfaz. ```go type LLMClient interface { Generate(ctx context.Context, req CompletionRequest) (CompletionResponse, error) Stream(ctx context.Context, req CompletionRequest) iter.Seq2[StreamChunk, error] Name() string Capabilities() ProviderCapabilities } type CompletionRequest struct { Messages []Message Tools []Tool ToolChoice ToolChoice // "auto" | "required" | "none" | ToolRef Model string Temperature *float32 MaxTokens *int Stop []string Metadata map[string]string } type CompletionResponse struct { ID string Content string ToolCalls []ToolCall Usage TokenUsage StopReason string // "end_turn" | "tool_use" | "max_tokens" | "stop_sequence" } type StreamChunk struct { Delta string ToolCalls []ToolCall // streamed incremental FinishReason string Usage TokenUsage // sΓ³lo en chunk final } type TokenUsage struct { InputTokens int OutputTokens int TotalTokens int } type ProviderCapabilities struct { SupportsTools bool SupportsVision bool SupportsJSON bool MaxContextWindow int } ``` **Ver detalles completos:** [`pkg/llm/README.md`](../pkg/llm/README.md) ### 3.2 Tool System (`pkg/tools/`) FunciΓ³n invocable por el LLM con JSON Schema, permisos, y sandbox. ```go type Tool struct { Name string Description string InputSchema json.RawMessage // JSON Schema draft-07+ Required []string Handler ToolHandler // func(ctx, args json.RawMessage) (Result, error) Permission Permission // Allow | Ask | Deny Examples []ToolExample // few-shot para el LLM } type ToolHandler func(ctx context.Context, args json.RawMessage) (ToolResult, error) type ToolResult struct { Content string IsError bool Metadata map[string]string Artifacts []Artifact } type ToolCall struct { ID string Name string Arguments json.RawMessage Thought string // opcional: chain-of-thought } type Permission int const ( Allow Permission = iota Ask Deny ) type ToolRegistry interface { Register(tool Tool) error Get(name string) (Tool, bool) List() []Tool Filter(policy PermissionPolicy) []Tool } ``` **Ver detalles completos:** [`pkg/tools/README.md`](../pkg/tools/README.md) ### 3.3 Agent Loop (`pkg/agent/`) Bucle iterativo entre LLM y ejecuciΓ³n de tools. Es el "cerebro" que orquesta todo. ```go type Loop interface { Run(ctx context.Context, input string) (Response, error) RunStream(ctx context.Context, input string) iter.Seq2[Chunk, error] } type Config struct { LLM llm.LLMClient Persona persona.Persona Tools tools.Registry Sandbox Sandbox MaxIters int Approver Approver // nil = auto-approve all OnIteration func(Iteration) // observability hook } type Response struct { Content string ToolCalls []tools.Call Iterations int Duration time.Duration TokenUsage llm.TokenUsage } ``` **Algoritmo:** ``` function RunAgent(userMessage, session): messages = append(session.Messages, userMessage) iteration = 0 while iteration < MaxIterations: iteration++ response = llm.Generate(messages, tools) if no tool calls: return response messages.append(assistantMsg(response)) for toolCall in response.ToolCalls: tool = registry.Get(toolCall.Name) # Approval gate if tool.Permission == Ask && !session.AutoApprove: if !cli.AskApproval(tool, toolCall): messages.append(denialMessage(toolCall)) continue # Sandbox validation if err := sandbox.ValidateToolCall(tool, toolCall); err != nil { messages.append(errorMessage(toolCall, err)) continue # Execute result, err := tool.Handler(ctx, toolCall.Arguments) # Truncate if needed result.Content = truncate(result.Content, MaxToolOutputBytes) messages.append(toolResultMessage(toolCall, result)) return ErrorResponse("max_iterations_exceeded") ``` **ParΓ‘metros:** | ParΓ‘metro | Default | DescripciΓ³n | |---|---|---| | `MaxIterations` | 50 | MΓ‘ximo de ciclos antes de cortar | | `MaxTokensPerSession` | 1,000,000 | Hard cap de tokens consumidos | | `MaxToolOutputBytes` | 50KB | Truncar outputs grandes | | `ToolTimeout` | 30s | Default, overrideable por tool | **Termination conditions:** 1. βœ… LLM devuelve respuesta sin `ToolCalls` (caso normal) 2. πŸ›‘ `MaxIterations` alcanzado 3. πŸ’° `MaxTokensPerSession` excedido 4. ⏱️ Timeout global 5. 🚫 Usuario aborta (`Ctrl+C` o `/stop`) **Ver detalles completos:** [`pkg/agent/README.md`](../pkg/agent/README.md) ### 3.4 Persona System (`pkg/persona/`) Ensamblador de system prompts combinando base + persona YAML + AGENTS.md. ```go type Persona struct { ID string Name string Tone string Style string Language string Constraints []string FewShot []llm.Message } type Loader interface { Load(ctx context.Context, configPath string) (Persona, error) Discover(ctx context.Context, workdir string) (Persona, error) } ``` **CΓ³mo se construye el system prompt final:** ``` 1. Base prompt (hardcoded en la librerΓ­a) 2. Persona YAML (configurable) 3. AGENTS.md del proyecto (descubierto en Γ‘rbol de directorios) 4. Working memory context (si hay compaction) ``` **AGENTS.md discovery:** Busca `./AGENTS.md`, sube al padre, etc., concatenando todos. TambiΓ©n incluye `~/.config/rony/AGENTS.md` como default global. **Ver detalles completos:** [`pkg/persona/README.md`](../pkg/persona/README.md) ### 3.5 Memory (`pkg/rag/`) Retrieval-Augmented Generation: memoria persistente y bΓΊsqueda semΓ‘ntica. ```go type Memory interface { Add(ctx context.Context, fragment Fragment) error Search(ctx context.Context, query string, topK int) ([]Fragment, error) Forget(ctx context.Context, id string) error } type Fragment struct { ID string Content string Vector []float32 Metadata map[string]string Timestamp time.Time ProjectID string } type Embedder interface { Embed(ctx context.Context, text string) ([]float32, error) Dimensions() int } ``` **Tipos de memoria:** | Tipo | QuΓ© guarda | Persistencia | |---|---|---| | **Working** | Mensajes de la sesiΓ³n actual | RAM | | **Episodic** | Eventos pasados | Vector DB | | **Semantic** | Conocimiento consolidado | Vector DB (curado) | | **Procedural** | Patrones de uso | Vector DB (auto-aprendido) | **Backends soportados:** - ChromaDB embedded (default) - Qdrant embedded - SQLite + sqlite-vec **Ver detalles completos:** [`pkg/rag/README.md`](../pkg/rag/README.md) ### 3.6 Sandbox (`pkg/tools/sandbox/`) Sandbox de filesystem a nivel kernel usando `os.Root` (Go 1.24+). ```go type Sandbox struct { workspace *os.Root // Go 1.24+ configDir *os.Root // ~/.config/rony/ } func (s *Sandbox) Open(path string) (*os.File, error) func (s *Sandbox) Create(path string) (*os.File, error) func (s *Sandbox) ReadFile(path string) ([]byte, error) func (s *Sandbox) WriteFile(path string, data []byte, perm os.FileMode) error ``` **Por quΓ© `os.Root` (Go 1.24+) es security-critical:** | Vector de ataque | `strings.HasPrefix` (naive) | `os.Root` | |---|---|---| | `../../../etc/passwd` | Bloqueado si abs path no tiene prefix | Bloqueado por kernel | | Symlink dentro de workspace β†’ `/etc/passwd` | Bloqueado solo si resolvemos manualmente | Bloqueado nativamente | | Race condition TOCTOU | Posible | Imposible (kernel-checked) | | Path encoding (`%2e%2e`) | No detectado | Detectado por stdlib | | Null bytes en path | Depende del OS | Manejado por stdlib | > πŸ”’ Por eso la librerΓ­a **requiere Go 1.26+** (ver [README Β§16.1.3.1](https://github.com/VictorVargas/rony-llm-agent/blob/main/docs/architecture.md#16131)). ### 3.7 Configuration (`pkg/config/`) Carga YAML con precedencia jerΓ‘rquica. ```go type Config struct { Model string Persona string Provider ProviderConfig Tools ToolPolicy Logging LoggingConfig Sandbox SandboxConfig } type Loader interface { Load(ctx context.Context, workdir string) (Config, error) } ``` **Orden de precedencia (mayor a menor):** ``` flags CLI > env vars > ./rony.yaml > ~/.config/rony/config.yaml > defaults ``` **Ver detalles completos:** [`pkg/config/README.md`](../pkg/config/README.md) --- ## πŸ”’ 4. Modelo de Seguridad ### 4.1 Amenazas cubiertas | Amenaza | MitigaciΓ³n en la librerΓ­a | |---|---| | Path traversal | `os.Root` sandbox | | Command injection | ValidaciΓ³n de comandos (delegado a productos que usan bash tool) | | Resource exhaustion | `MaxIterations`, `MaxTokensPerSession`, `MaxToolOutputBytes` | | Prompt injection | Wrap de contenido externo en tags `` (Phase 2) | | Secret leakage | `Redact()` funciΓ³n para logs (Phase 2) | | Tool misuse | Approval gates + permission policies | ### 4.2 Principio de mΓ­nimo privilegio - Tools tienen `Permission: Allow | Ask | Deny` - Default es `Allow` solo para tools read-only - `Ask` para tools que mutan estado - `Deny` es raro, usado para tools deshabilitados explΓ­citamente ### 4.3 Capas de defensa (Defense in depth) ``` Capa 1: Permission policy β†’ rechaza tools no autorizados Capa 2: Approval gate (Ask) β†’ usuario confirma antes de ejecutar Capa 3: Sandbox validation β†’ paths dentro de allowed roots Capa 4: Kernel enforcement (os.Root) β†’ garantΓ­a a nivel OS Capa 5: Output truncation β†’ no devuelve outputs enormes Capa 6: Secret redaction β†’ no expone secrets en logs/outputs ``` --- ## πŸ“ 5. Especificaciones TΓ©cnicas (SDD) ### 5.1 Requisitos Funcionales Core | ID | Requisito | ImplementaciΓ³n | |---|---|---| | LRF-001 | LLMClient interface | `pkg/llm/` | | LRF-002 | Multi-provider (OpenAI, Anthropic, Ollama, llama.cpp) | `pkg/llm/providers/` | | LRF-003 | Streaming nativo | `iter.Seq2` | | LRF-004 | Tool calling con JSON Schema | `pkg/tools/` | | LRF-005 | Agent loop con guardrails | `pkg/agent/` | | LRF-006 | Persona system + AGENTS.md | `pkg/persona/` | | LRF-007 | RAG memory | `pkg/rag/` | | LRF-008 | Sandbox de filesystem (kernel) | `pkg/tools/sandbox/` | | LRF-009 | Configuration con precedencia | `pkg/config/` | | LRF-010 | Approval gates | `pkg/agent/` | ### 5.2 Requisitos No Funcionales | ID | CategorΓ­a | Target | |---|---|---| | LRNF-001 | Go version mΓ­nimo | 1.26 (por os.Root) | | LRNF-002 | Cobertura de tests | β‰₯80% | | LRNF-003 | API stability | Semver estricto | | LRNF-004 | Dependencies mΓ­nimas | Solo stdlib + SDKs de provider | | LRNF-005 | Thread safety | Toda API pΓΊblica es safe para uso concurrente | | LRNF-006 | Context propagation | Toda llamada toma `context.Context` | --- ## πŸ§ͺ 6. Testing ### 6.1 Mock LLM Server Para tests deterministas, los productos pueden usar `pkg/llm/mock`: ```go import "github.com/VictorVargas/rony-llm-agent/pkg/llm/mock" mockClient := mock.New(mock.Responses{ {Match: "hola", Response: "Β‘Hola! ΒΏCΓ³mo estΓ‘s?"}, {Match: "*", Response: "default"}, }) ``` ### 6.2 Mock Memory ```go import "github.com/VictorVargas/rony-llm-agent/pkg/rag/mock" memory := mock.NewMemory([]rag.Fragment{ {Content: "doc1", ProjectID: "test"}, }) ``` ### 6.3 Tabla de tests crΓ­ticos (toda la librerΓ­a) | Test | Paquete | Prioridad | |---|---|---| | `LLMClient.Generate` retorna respuesta vΓ‘lida | `pkg/llm/` | Alta | | `LLMClient.Stream` emite chunks en orden | `pkg/llm/` | Alta | | `ToolRegistry.Register/Get/List` | `pkg/tools/` | Alta | | Agent loop termina sin tool calls | `pkg/agent/` | Alta | | Agent loop termina en max iterations | `pkg/agent/` | Alta | | Path sandbox rechaza `../../../etc/passwd` | `pkg/tools/sandbox/` | Alta | | Persona loader desde YAML | `pkg/persona/` | Alta | | AGENTS.md discovery (sube directorios) | `pkg/persona/` | Media | | Config precedence (env > file > defaults) | `pkg/config/` | Alta | | Memory Search retorna top-K por similitud | `pkg/rag/` | Alta | --- ## πŸ“š 7. Convenciones de cΓ³digo ### 7.1 Naming - **Packages:** lowercase, singular (`agent`, `tools`, `llm`) - **Interfaces:** terminan en sustantivo o capability (`Loop`, `LLMClient`, `Embedder`) - **Errors:** `ErrXXX` para sentinels, wrapped con `%w` - **Constructors:** `New` para constructor principal, `NewXxx` para variantes ### 7.2 Errors ```go // Sentinel errors var ( ErrToolNotFound = errors.New("tool not found") ErrSandboxViolation = errors.New("path outside allowed roots") ) // Wrapping if err != nil { return fmt.Errorf("loading persona %s: %w", name, err) } ``` ### 7.3 Context Toda funciΓ³n que pueda bloquear toma `ctx context.Context` como primer parΓ‘metro: ```go func (l *Loop) Run(ctx context.Context, input string) (Response, error) func (m *Memory) Search(ctx context.Context, query string, topK int) ([]Fragment, error) ``` --- ## πŸ”„ 8. Versionado - **Semver estricto** (`vMAJOR.MINOR.PATCH`) - **MAJOR:** breaking changes en `pkg/` (interfaces, signatures, tipos pΓΊblicos) - **MINOR:** nuevas features, nuevos paquetes, nuevos adapters - **PATCH:** bugfixes Los adapters privados (`pkg/llm/providers/openai/`) pueden cambiar sin bump de MAJOR si la interfaz `LLMClient` no cambia. --- ## πŸ“– 9. Referencias - **Go 1.26 release notes:** https://go.dev/doc/go1.26 - **`os.Root` documentation:** https://pkg.go.dev/os#Root - **`iter.Seq2` documentation:** https://pkg.go.dev/iter - **Hexagonal Architecture (Alistair Cockburn):** https://alistair.cockburn.us/hexagonal-architecture/ - **DDD (Eric Evans, 2003):** Domain-Driven Design - **ReAct Pattern (Yao et al., 2022):** https://arxiv.org/abs/2210.03629 --- ## πŸ”— Documentos relacionados - [`README.md`](./README.md) β€” Overview de la librerΓ­a - [`components.md`](./components.md) β€” Referencia detallada por paquete - [`phase2.md`](./phase2.md) β€” Features avanzadas (MCP, RAG completo, Skills, etc.) - [Productos que usan esta librerΓ­a](https://github.com/VictorVargas)