# "Agents"
_Path: en/framework/agents_
> "Define and run Wippy agents with tools, streaming, delegates, traits, memory, and custom resolution."
## Table of Contents
- Agents
## Content
# Agents
The `wippy/agent` module defines agents declaratively and runs them through a context and runner. Agents can use tools, stream responses, delegate work, apply traits, and recall memory.
This page is an API primer with composable reference snippets, not a standalone tutorial. The snippets assume an existing Wippy project, a registered LLM model and provider, configured provider credentials, and the agent, tool, or resolver entries referenced by each example. Later snippets build on variables such as `ctx`, `runner`, and `conversation` created in earlier sections. For a complete runnable project, follow [Build an LLM Agent](tutorials/llm-agent.md).
## Setup
Add the module to your project:
```bash
wippy add wippy/agent
wippy install
```
The agent module declares its `wippy/llm` dependency itself. Add the agent
dependency to source when it is not already present:
```yaml
version: "1.0"
namespace: app
entries:
- name: dep.agent
kind: ns.dependency
component: wippy/agent
version: "*"
```
## Agent Definitions
Agents are registry entries with `meta.type: agent.gen1`:
```yaml
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
```
### Agent Fields
| Field | Type | Description |
|-------|------|-------------|
| `meta.type` | string | Must be `agent.gen1` |
| `meta.name` | string | Agent identifier |
| `prompt` | string | System prompt |
| `model` | string | Model name or class |
| `max_tokens` | number | Maximum output tokens (default `512`) |
| `temperature` | number | Optional sampling temperature; omitted by default, with range and support determined by the provider |
| `thinking_effort` | number | Forwarded to the model only when `> 0` (provider-defined scale) |
| `tools` | array | Tool registry IDs |
| `traits` | array | Trait references |
| `delegates` | array | Delegate agent references |
| `memory` | array | Static memory items (strings) |
| `memory_contract` | table | Dynamic memory configuration |
## Agent Context
Create an agent context, configure it as needed, and then load an agent:
```yaml
imports:
agent_context: wippy.agent:context
prompt: wippy.llm:prompt
```
```lua
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
```
### Context Methods
| Method | Description |
|--------|-------------|
| `agent_context.new(options?)` | Create new context |
| `:add_tools(specs)` | Add tools at runtime |
| `:add_delegates(specs)` | Add delegate agents |
| `:configure_delegate_tools(config)` | Configure how delegates expose themselves as tools |
| `:set_memory_contract(config)` | Configure dynamic memory |
| `:set_context_merger(fn)` | Provide a function to merge runtime context updates |
| `:update_context(updates)` | Update runtime context |
| `:load_agent(spec_or_id, options?)` | Load and compile agent, returns runner |
| `:switch_to_agent(id, options?)` | Switch to different agent, returns `(boolean, string?)` |
| `:switch_to_model(name)` | Change model on current agent, returns `(boolean, string?)` |
| `:get_current_agent()` | Get current runner |
| `:get_config()` | Return a summary of the context configuration |
### Context Options
```lua
local ctx = agent_context.new({
context = { session_id = "abc", user_id = "u1" },
delegate_tools = { enabled = true },
enable_cache = true,
})
```
| Option | Description |
|--------|-------------|
| `context` | Base runtime context forwarded to tools and delegates |
| `delegate_tools` | Default delegate-tool configuration (overridden by `configure_delegate_tools`) |
| `enable_cache` | Prompt cache marker setting for Claude models. The current implementation always enables markers, including when this option is `false`. |
### Loading by Inline Spec
Load an agent without a registry entry:
```lua
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" },
})
```
## Running Steps
The runner executes one agent step from a prompt-builder conversation:
```lua
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)
```
### Step Options
```lua
local self_pid, pid_err = process.pid()
if pid_err then
error("Failed to get process PID: " .. tostring(pid_err))
end
local response, err = runner:step(conversation, {
context = { session_id = "abc" },
stream_target = { reply_to = self_pid, topic = "stream" },
tool_call = "auto",
})
if err then
error("Agent step failed: " .. tostring(err))
end
```
| Option | Type | Description |
|--------|------|-------------|
| `context` | table | Runtime context merged with agent context |
| `stream_target` | table | Streaming: `{ reply_to, topic }` |
| `tool_call` | string | `"auto"`, `"any"`, `"none"`, or a tool name |
### Step Response
| Field | Type | Description |
|-------|------|-------------|
| `result` | string | Generated text |
| `tokens` | table | Token usage |
| `finish_reason` | string | Stop reason |
| `tool_calls` | table? | Tool calls to execute |
| `delegate_calls` | table? | Delegate invocations |
### Runner Stats
```lua
local stats = runner:get_stats()
-- stats.id, stats.name, stats.total_tokens
```
## Tool Definitions
Tools are `function.lua` entries with `meta.type: tool`. Define them in a separate `_index.yaml`:
```yaml
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
```
```lua
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 }
```
### Tool Metadata
| Field | Type | Description |
|-------|------|-------------|
| `meta.type` | string | Must be `tool` |
| `meta.input_schema` | string/table | JSON Schema for tool arguments |
| `meta.llm_alias` | string | Name exposed to the LLM |
| `meta.llm_description` | string | Description exposed to the LLM |
| `meta.exclusive` | boolean | If true, cancels concurrent tool calls |
### Referencing Tools in Agents
List tool registry IDs in the agent definition:
```yaml
- 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
```
Tools can also be referenced with custom aliases and context:
```yaml
tools:
- id: app.tools:search
alias: web_search
context:
api_key: "${SEARCH_API_KEY}"
```
## Tool Execution
When an agent step returns `tool_calls`, execute the calls and add their results to the conversation:
```lua
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, json.encode(tc.arguments), tc.id)
conversation:add_function_result(tc.name, result_str, tc.id)
end
end
end
```
### Tool Call Fields
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique call identifier |
| `name` | string | Tool name (alias or llm_alias) |
| `arguments` | table | Parsed arguments |
| `registry_id` | string | Full registry ID for `funcs.call()` |
Use funcs.call(tc.registry_id, tc.arguments) to execute tools. The registry_id field maps directly to the tool's entry in the registry.
For how agent tool access and observability are secured, see the [Security Model](concepts/security-model.md).
## Streaming
Stream agent responses through `stream_target`:
```lua
local TOPIC = "agent_stream"
local function stream_step(runner, conversation)
local stream_ch, listen_err = process.listen(TOPIC)
if listen_err then
return nil, nil, listen_err
end
local function finish(text, response, err)
local ok, cleanup_err = process.unlisten(stream_ch)
if not ok then
cleanup_err = cleanup_err or "Failed to remove agent stream listener"
if err then
return text, nil, tostring(err) .. "; cleanup failed: " .. tostring(cleanup_err)
end
return text, nil, cleanup_err
end
if err then
return text, nil, err
end
return text, response, nil
end
local self_pid, pid_err = process.pid()
if pid_err then
return finish("", nil, pid_err)
end
local done_ch = channel.new(1)
coroutine.spawn(function()
local response, err = runner:step(conversation, {
stream_target = {
reply_to = self_pid,
topic = TOPIC,
},
})
done_ch:send({ response = response, err = err })
end)
local full_text = ""
local step_result = nil
local stream_done = false
local stream_err = nil
while true do
local cases = {}
if not stream_done then
table.insert(cases, stream_ch:case_receive())
end
if not step_result then
table.insert(cases, done_ch:case_receive())
end
local result = channel.select(cases)
if not result.ok then
return finish(full_text, nil, "Agent stream closed before completion")
end
if result.channel == done_ch then
step_result = result.value
if step_result.err then
return finish(full_text, nil, step_result.err)
end
if stream_done then
return finish(full_text, step_result.response, stream_err)
end
else
local chunk = result.value
if chunk.type == "chunk" then
local content = chunk.content or ""
print(content)
full_text = full_text .. content
elseif chunk.type == "error" then
stream_done = true
stream_err = chunk.error and chunk.error.message or "Agent stream failed"
elseif chunk.type == "done" then
stream_done = true
end
if stream_done and step_result then
return finish(full_text, step_result.response, stream_err)
end
end
end
end
```
The stream uses the same chunk types as direct LLM streaming: `"chunk"`, `"thinking"`, `"tool_call"`, `"error"`, `"done"`.
Use coroutine.spawn to run runner:step() in a separate coroutine so you can receive stream chunks concurrently. Use channel.select to multiplex the stream and completion channels.
## Delegates
Agents can delegate to other agents. Delegates appear as tools to the parent agent:
```yaml
- 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
```
Delegate calls appear in `response.delegate_calls`:
```lua
local response, err = runner:step(conversation)
if err then
error("Delegate step failed: " .. tostring(err))
end
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
```
Delegates can also be added at runtime:
```lua
ctx:add_delegates({
{ id = "app:specialist", name = "ask_specialist", rule = "for domain questions" },
})
```
## Traits
Traits are reusable definitions that contribute prompts, tools, and behavior to agents:
```yaml
- 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
```
### Built-in Traits
| Trait | Description |
|-------|-------------|
| `time_aware` | Injects current date and time into the prompt |
The `time_aware` trait accepts context options:
```yaml
traits:
- id: time_aware
context:
timezone: America/New_York
time_interval: 15
```
### Custom Traits
Traits are registry entries with `meta.type: agent.trait`. They can contribute:
- **prompt** - static text appended to the system prompt
- **build_func_id** - function called at compile time to contribute tools, prompts, delegates
- **prompt_func_id** - function called at each step to inject dynamic content
- **step_func_id** - function called at each step for side effects
### Static Memory
Static memory items are appended to the system prompt:
```yaml
- 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"
```
### Dynamic Memory Contract
Configure dynamic memory recall through an external implementation:
```yaml
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
```
The memory contract is called during `runner:step()` to recall relevant items based on the conversation context. Results are injected as developer messages.
| Option | Default | Description |
|--------|---------|-------------|
| `max_items` | `3` | Maximum memory items per recall |
| `max_length` | `1000` | Maximum total character length |
| `recall_cooldown` | `1` | Minimum steps between recalls |
| `min_conversation_length` | `2` | Minimum conversation turns before first recall |
## Resolver Contract
When `load_agent()` receives a string identifier, it first tries to resolve it through the `wippy.agent:resolver` contract. If no resolver is bound or the resolver returns nil, it falls back to the registry lookup.
This allows applications to implement custom agent resolution, such as loading agent definitions from a database.
### Binding a Resolver
Define a resolver function and bind it to the contract:
```yaml
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
```
### Resolver Implementation
The resolver receives `{ agent_id = "..." }` and returns an agent spec table or nil:
```lua
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,
}
```
### Resolution Order
1. Try `wippy.agent:resolver` contract (if bound)
2. Try registry lookup by ID
3. Try registry lookup by name
4. Return error if not found
Custom resolution can load agent definitions outside the framework registry, including definitions scoped by user or workspace.
## See Also
- [LLM](framework/llm.md) — Underlying model interface
- [Building an LLM Agent](../tutorials/llm-agent.md) — Build an agent step by step
- [Framework Overview](framework/overview.md) — Install and import framework modules
## Navigation
Previous: "LLM" (framework/llm)
Next: "Test Framework" (framework/testing)