2026-06-29 06:22:15 +00:00
# 🏗️ rony-llm-agent — Architecture
2026-06-29 00:25:45 +00:00
**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.)
---
## 🎯 1. Visión de la Librería
2026-06-29 06:22:15 +00:00
### 1.1 ¿Qué es rony-llm-agent?
2026-06-29 00:25:45 +00:00
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:**
2026-06-29 06:22:15 +00:00
- [`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
2026-06-29 00:25:45 +00:00
### 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
▼
┌─────────────────────────────────────────────────────────────────┐
2026-06-29 06:22:15 +00:00
│ rony-llm-agent (pkg/ — API pública) │
2026-06-29 00:25:45 +00:00
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 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 |
2026-06-29 06:22:15 +00:00
> 🔒 Por eso la librería **requiere Go 1.26+** (ver [README §16.1.3.1](https://github.com/VictorVargas/rony-harness/blob/main/docs/architecture.md#16131)).
2026-06-29 00:25:45 +00:00
### 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 `<untrusted_content>` (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
2026-06-29 06:22:15 +00:00
import "github.com/VictorVargas/rony-llm-agent/pkg/llm/mock"
2026-06-29 00:25:45 +00:00
mockClient := mock.New(mock.Responses{
{Match: "hola", Response: "¡Hola! ¿Cómo estás?"},
{Match: "*", Response: "default"},
})
```
### 6.2 Mock Memory
```go
2026-06-29 06:22:15 +00:00
import "github.com/VictorVargas/rony-llm-agent/pkg/rag/mock"
2026-06-29 00:25:45 +00:00
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 )