Ir al contenido

Agentes

El modulo wippy/agent proporciona un framework para construir agentes de IA con uso de herramientas, streaming, delegacion, traits y memoria. Los agentes se definen declarativamente y se ejecutan a traves de un patron de contexto/runner.

Agrega el modulo a tu proyecto:

Ventana de terminal
wippy add wippy/agent
wippy install

El modulo de agentes requiere wippy/llm y un host de procesos. Declara ambas dependencias:

version: "1.0"
namespace: app
entries:
- name: os_env
kind: env.storage.os
- name: processes
kind: process.host
lifecycle:
auto_start: true
- name: dep.llm
kind: ns.dependency
component: wippy/llm
version: "*"
parameters:
- name: env_storage
value: app:os_env
- name: process_host
value: app:processes
- name: dep.agent
kind: ns.dependency
component: wippy/agent
version: "*"
parameters:
- name: process_host
value: app:processes

Los agentes son entradas de registro con meta.type: agent.gen1:

entries:
- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
title: Assistant
comment: A helpful chat assistant
prompt: |
You are a helpful assistant. Be concise and direct.
Answer questions clearly.
model: gpt-4o
max_tokens: 1024
temperature: 0.7
CampoTipoDescripcion
meta.typestringDebe ser agent.gen1
meta.namestringIdentificador del agente
promptstringPrompt del sistema
modelstringNombre o clase del modelo
max_tokensnumberMaximo de tokens de salida (por defecto 512)
temperaturenumberTemperatura de muestreo (por defecto 0; rango dependiente del proveedor)
thinking_effortnumberSolo se reenvia al modelo cuando > 0 (escala definida por el proveedor)
toolsarrayIDs de registro de herramientas
traitsarrayReferencias a traits
delegatesarrayReferencias a agentes delegados
memoryarrayElementos de memoria estatica (strings)
memory_contracttableConfiguracion de memoria dinamica

El contexto del agente es el punto de entrada principal. Crea un contexto, configuralo opcionalmente y luego carga un agente:

imports:
agent_context: wippy.agent:context
local agent_context = require("agent_context")
local ctx = agent_context.new()
local runner, err = ctx:load_agent("app:assistant")
if err then
error("Failed to load agent: " .. tostring(err))
end
MetodoDescripcion
agent_context.new(options?)Crear nuevo contexto
:add_tools(specs)Agregar herramientas en tiempo de ejecucion
:add_delegates(specs)Agregar agentes delegados
:configure_delegate_tools(config)Configurar como los delegados se exponen como herramientas
:set_memory_contract(config)Configurar memoria dinamica
:set_context_merger(fn)Proporcionar una funcion para fusionar actualizaciones del contexto en tiempo de ejecucion
:update_context(updates)Actualizar contexto en tiempo de ejecucion
:load_agent(spec_or_id, options?)Cargar y compilar agente, retorna runner
:switch_to_agent(id, options?)Cambiar a otro agente, retorna (boolean, string?)
:switch_to_model(name)Cambiar modelo del agente actual, retorna (boolean, string?)
:get_current_agent()Obtener runner actual
:get_config()Retornar un resumen de la configuracion del contexto
local ctx = agent_context.new({
context = { session_id = "abc", user_id = "u1" },
delegate_tools = { enabled = true },
enable_cache = true,
})
OpcionDescripcion
contextContexto base en tiempo de ejecucion reenviado a herramientas y delegados
delegate_toolsConfiguracion predeterminada de delegate-tool (sobrescrita por configure_delegate_tools)
enable_cacheHabilitar marcadores de cache de prompt (modelos Claude). Por defecto es true.

Carga un agente sin una entrada de registro:

local runner, err = ctx:load_agent({
id = "inline-agent",
name = "helper",
prompt = "You are a helpful assistant.",
model = "gpt-4o",
max_tokens = 1024,
tools = { "app.tools:search" },
})

El runner ejecuta un solo paso de razonamiento. Pasa un constructor de prompts con la conversacion:

local prompt = require("prompt")
local conversation = prompt.new()
conversation:add_user("What is the capital of France?")
local response, err = runner:step(conversation)
if err then
error(tostring(err))
end
print(response.result)
local response, err = runner:step(conversation, {
context = { session_id = "abc" },
stream_target = { reply_to = process.pid(), topic = "stream" },
tool_call = "auto",
})
OpcionTipoDescripcion
contexttableContexto en tiempo de ejecucion combinado con el contexto del agente
stream_targettableStreaming: { reply_to, topic }
tool_callstring"auto", "any", "none" o un nombre de herramienta
CampoTipoDescripcion
resultstringTexto generado
tokenstableUso de tokens
finish_reasonstringRazon de detencion
tool_callstable?Llamadas a herramientas para ejecutar
delegate_callstable?Invocaciones de delegados
local stats = runner:get_stats()
-- stats.id, stats.name, stats.total_tokens

Las herramientas son entradas function.lua con meta.type: tool. Definilas en un _index.yaml separado:

version: "1.0"
namespace: app.tools
entries:
- name: calculate
kind: function.lua
meta:
type: tool
title: Calculate
input_schema: |
{
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression to evaluate"
}
},
"required": ["expression"],
"additionalProperties": false
}
llm_alias: calculate
llm_description: Evaluate a mathematical expression.
source: file://calculate.lua
modules: [expr]
method: handler
local expr = require("expr")
local function handler(args)
local result, err = expr.eval(args.expression)
if err then
return { error = tostring(err) }
end
return { result = result }
end
return { handler = handler }
CampoTipoDescripcion
meta.typestringDebe ser tool
meta.input_schemastring/tableJSON Schema para los argumentos de la herramienta
meta.llm_aliasstringNombre expuesto al LLM
meta.llm_descriptionstringDescripcion expuesta al LLM
meta.exclusivebooleanSi es true, cancela llamadas concurrentes a herramientas

Lista los IDs de registro de herramientas en la definicion del agente:

- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
prompt: You are a helpful assistant with tools.
model: gpt-4o
max_tokens: 1024
tools:
- app.tools:calculate
- app.tools:search
- app.tools:* # wildcard: all tools in namespace

Las herramientas tambien pueden referenciarse con alias personalizados y contexto:

tools:
- id: app.tools:search
alias: web_search
context:
api_key: "${SEARCH_API_KEY}"

Cuando un paso del agente retorna tool_calls, ejecutalas y alimenta los resultados de vuelta:

local json = require("json")
local funcs = require("funcs")
local function execute_and_continue(runner, conversation)
while true do
local response, err = runner:step(conversation)
if err then return nil, err end
local tool_calls = response.tool_calls
if not tool_calls or #tool_calls == 0 then
return response.result, nil
end
for _, tc in ipairs(tool_calls) do
local result, call_err = funcs.call(tc.registry_id, tc.arguments)
local result_str
if call_err then
result_str = json.encode({ error = tostring(call_err) })
else
result_str = json.encode(result)
end
conversation:add_function_call(tc.name, tc.arguments, tc.id)
conversation:add_function_result(tc.name, result_str, tc.id)
end
end
end
CampoTipoDescripcion
idstringIdentificador unico de la llamada
namestringNombre de la herramienta (alias o llm_alias)
argumentstableArgumentos parseados
registry_idstringID de registro completo para funcs.call()
Usa funcs.call(tc.registry_id, tc.arguments) para ejecutar herramientas. El campo registry_id mapea directamente a la entrada de la herramienta en el registro.

Transmite respuestas del agente en tiempo real usando stream_target:

local TOPIC = "agent_stream"
local function stream_step(runner, conversation)
local stream_ch = process.listen(TOPIC)
local done_ch = channel.new(1)
coroutine.spawn(function()
local response, err = runner:step(conversation, {
stream_target = {
reply_to = process.pid(),
topic = TOPIC,
},
})
done_ch:send({ response = response, err = err })
end)
local full_text = ""
while true do
local result = channel.select({
stream_ch:case_receive(),
done_ch:case_receive(),
})
if not result.ok then break end
if result.channel == done_ch then
process.unlisten(stream_ch)
local r = result.value
return full_text, r.response, r.err
end
local chunk = result.value
if chunk.type == "chunk" then
io.write(chunk.content or "")
full_text = full_text .. (chunk.content or "")
elseif chunk.type == "done" then
-- wait for the step to complete
local r, ok = done_ch:receive()
process.unlisten(stream_ch)
if ok and r then
return full_text, r.response, r.err
end
return full_text, nil, nil
end
end
process.unlisten(stream_ch)
return full_text, nil, nil
end

El stream usa los mismos tipos de chunk que el streaming directo de LLM: "chunk", "thinking", "tool_call", "error", "done".

Usa coroutine.spawn para ejecutar runner:step() en una corrutina separada para poder recibir chunks del stream de forma concurrente. Usa channel.select para multiplexar los canales de stream y completado.

Los agentes pueden delegar a otros agentes. Los delegados aparecen como herramientas para el agente padre:

- name: coordinator
kind: registry.entry
meta:
type: agent.gen1
name: coordinator
prompt: Route questions to the right specialist.
model: gpt-4o
max_tokens: 1024
delegates:
- id: app:code_agent
name: ask_coder
rule: for programming questions
- id: app:math_agent
name: ask_mathematician
rule: for math problems

Las llamadas a delegados aparecen en response.delegate_calls:

local response = runner:step(conversation)
if response.delegate_calls then
for _, dc in ipairs(response.delegate_calls) do
-- dc.agent_id - target agent registry ID
-- dc.name - delegate tool name
-- dc.arguments - forwarded message
end
end

Los delegados tambien pueden agregarse en tiempo de ejecucion:

ctx:add_delegates({
{ id = "app:specialist", name = "ask_specialist", rule = "for domain questions" },
})

Los traits son capacidades reutilizables que aportan prompts, herramientas y comportamiento a los agentes:

- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
prompt: You are a helpful assistant.
model: gpt-4o
traits:
- time_aware
- id: custom_trait
context:
key: value
TraitDescripcion
time_awareInyecta la fecha y hora actual en el prompt

El trait time_aware acepta opciones de contexto:

traits:
- id: time_aware
context:
timezone: America/New_York
time_interval: 15

Los traits son entradas de registro con meta.type: agent.trait. Pueden aportar:

  • prompt - texto estatico agregado al prompt del sistema
  • build_func_id - funcion llamada en tiempo de compilacion para aportar herramientas, prompts y delegados
  • prompt_func_id - funcion llamada en cada paso para inyectar contenido dinamico
  • step_func_id - funcion llamada en cada paso para efectos secundarios

Elementos de memoria simples agregados al prompt del sistema:

- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
prompt: You are a helpful assistant.
model: gpt-4o
memory:
- "User prefers concise answers"
- "Always cite sources when possible"

Configura la recuperacion de memoria dinamica desde una fuente externa:

memory_contract:
implementation_id: app:memory_store
context:
user_id: "${user_id}"
options:
max_items: 3
max_length: 1000
recall_cooldown: 1
min_conversation_length: 2

El contrato de memoria se invoca durante runner:step() para recuperar elementos relevantes basados en el contexto de la conversacion. Los resultados se inyectan como mensajes de desarrollador.

OpcionPor defectoDescripcion
max_items3Maximo de elementos de memoria por recuperacion
max_length1000Longitud total maxima de caracteres
recall_cooldown1Minimo de pasos entre recuperaciones
min_conversation_length2Minimo de turnos de conversacion antes de la primera recuperacion

Cuando load_agent() recibe un identificador de tipo string, primero intenta resolverlo a traves del contrato wippy.agent:resolver. Si no hay un resolver vinculado o el resolver retorna nil, recurre a la busqueda en el registro.

Esto permite a las aplicaciones implementar resolucion personalizada de agentes, como cargar definiciones de agentes desde una base de datos.

Define una funcion de resolucion y vinculala al contrato:

entries:
- name: agent_resolver.resolve
kind: function.lua
source: file://agent_resolver.lua
method: resolve
modules:
- logger
imports:
agent_registry: wippy.agent.discovery:registry
- name: agent_resolver_binding
kind: contract.binding
contracts:
- contract: wippy.agent:resolver
default: true
methods:
resolve: app:agent_resolver.resolve

El resolver recibe { agent_id = "..." } y retorna una tabla de especificacion del agente o nil:

local agent_registry = require("agent_registry")
local CUSTOM_PREFIX = "custom:"
function resolve(args)
local agent_id = args.agent_id
if not agent_id then
return nil, "agent_id is required"
end
if agent_id:sub(1, #CUSTOM_PREFIX) == CUSTOM_PREFIX then
local id = agent_id:sub(#CUSTOM_PREFIX + 1)
-- load from database, config file, or any other source
return {
id = agent_id,
name = "custom-agent",
prompt = "You are a custom agent.",
model = "class:balanced",
max_tokens = 1024,
tools = {},
}
end
-- fall back to registry
local spec, err = agent_registry.get_by_id(agent_id)
if not spec then
spec, err = agent_registry.get_by_name(agent_id)
end
return spec, err
end
return {
resolve = resolve,
}
  1. Intenta el contrato wippy.agent:resolver (si esta vinculado)
  2. Intenta busqueda en el registro por ID
  3. Intenta busqueda en el registro por nombre
  4. Retorna error si no se encuentra

Este patron habilita aplicaciones multi-tenant donde los agentes se configuran por usuario o por workspace y se almacenan fuera del registro del framework.