# Agents _Path: en/framework/agents_ ## Table of Contents - Agents ## Content # Agents The `wippy/agent` module provides a framework for building AI agents with tool use, streaming, delegation, traits, and memory. Agents are defined declaratively and executed through a context/runner pattern. ## Setup Add the module to your project: ```bash wippy add wippy/agent wippy install ``` The agent module requires `wippy/llm` and a process host. Declare both dependencies: ```yaml 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 ``` ## 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 | | `temperature` | number | Randomness control, 0-1 | | `thinking_effort` | number | Thinking depth 0-100 | | `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 The agent context is the main entry point. Create a context, optionally configure it, then load an agent: ```yaml imports: agent_context: wippy.agent:context ``` ```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 | | `:set_memory_contract(config)` | Configure dynamic memory | | `: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 | ### Context Options ```lua local ctx = agent_context.new({ context = { session_id = "abc", user_id = "u1" }, delegate_tools = { enabled = true }, }) ``` ### 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 a single reasoning step. Pass a prompt builder with the 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 response, err = runner:step(conversation, { context = { session_id = "abc" }, stream_target = { reply_to = process.pid(), topic = "stream" }, tool_call = "auto", }) ``` | Option | Type | Description | |--------|------|-------------| | `context` | table | Runtime context merged with agent context | | `stream_target` | table | Streaming: `{ reply_to, topic }` | | `tool_call` | string | `"auto"`, `"required"`, `"none"` | ### 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 them and feed results back: ```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, 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. ## Streaming Stream agent responses in real-time using `stream_target`: ```lua 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 ``` 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 = 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 ``` 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 capabilities 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 Simple memory items 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 from an external source: ```yaml memory_contract: implementation_id: app:memory_store context: user_id: "${user_id}" options: max_items: 5 max_length: 2000 recall_cooldown: 2 min_conversation_length: 3 ``` The memory contract is called during `runner:step()` to recall relevant items based on the conversation context. Results are injected as developer messages. | Option | Description | |--------|-------------| | `max_items` | Maximum memory items per recall | | `max_length` | Maximum total character length | | `recall_cooldown` | Minimum steps between recalls | | `min_conversation_length` | 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 This pattern enables multi-tenant applications where agents are configured per-user or per-workspace and stored outside the framework's registry. ## See Also - [LLM](llm.md) - Underlying LLM module - [Building an LLM Agent](../tutorials/llm-agent.md) - Step-by-step tutorial - [Framework Overview](overview.md) - Framework module usage ## Navigation Previous: LLM (framework/llm) Next: Testing (framework/testing)