Micro AGI
Micro AGI
Section titled “Micro AGI”Build a self-modifying agent that creates its own tools at runtime — reading docs, writing Lua, registering entries in the registry, and loading them into the active session.
What We’re Building
Section titled “What We’re Building”A terminal agent that:
- Answers questions using an LLM with streaming
- Searches Wippy documentation to learn APIs
- Inspects the registry to discover existing capabilities
- Builds new tools on the fly when it lacks a capability
- Manages its own context window via compression
flowchart LR User -->|prompt| Agent Agent -->|step| LLM[GPT-5.1] LLM -->|tool_calls| Agent Agent -->|funcs.call| Tools Tools -->|result| Agent Agent -->|text| User
subgraph Tools doc_search registry_list registry_read create_tool load_tool endArchitecture
Section titled “Architecture”The agent runs as a Wippy process with access to the registry. When the LLM decides it needs a capability it doesn’t have, it uses the self-modification loop:
sequenceDiagram participant U as User participant A as Agent participant L as LLM participant R as Registry
U->>A: "what time is it?" A->>L: step(conversation) L->>A: tool_call: doc_search("lua/core/time") A->>A: execute doc_search A->>L: step(conversation + tool result) L->>A: tool_call: create_tool(name, source, schema) A->>R: evaluate deny policies + changeset create R->>A: ok A->>L: step(conversation + tool result) L->>A: tool_call: load_tool("app.generated:current_time") A->>A: ctx:add_tools() + reload agent A->>L: step(conversation + tool result) L->>A: tool_call: current_time() A->>A: execute new tool A->>L: step(conversation + tool result) L->>A: text: "The current time is..." A->>U: stream responseThe key insight: tools are registry entries. Creating a tool is just writing a function.lua entry with inline Lua source in data.source. The agent runtime compiles and loads it like any other entry.
Project Structure
Section titled “Project Structure”micro-agi/├── .wippy.yaml├── wippy.yaml└── src/ ├── _index.yaml ├── README.md ├── agent.lua └── tools/ ├── _index.yaml ├── doc_search.lua ├── registry_list.lua ├── registry_read.lua ├── create_tool.lua └── load_tool.luaInfrastructure
Section titled “Infrastructure”Create .wippy.yaml:
version: "1.0"
logger: encoding: consoleEntry Definitions
Section titled “Entry Definitions”Create src/_index.yaml with infrastructure, security policies, models, agent, and process:
version: "1.0"namespace: app
entries: - name: definition kind: ns.definition readme: file://README.md meta: title: Micro AGI description: Self-modifying development agent that builds its own tools at runtime depends_on: [wippy/llm, wippy/agent]
- 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:processesSecurity Policies
Section titled “Security Policies”Two security.policy entries restrict which namespaces the agent can write to:
- name: deny_core_ns kind: security.policy policy: actions: "*" resources: "app:*" effect: deny groups: - agent_security
- name: deny_tools_ns kind: security.policy policy: actions: "*" resources: "app.tools:*" effect: deny groups: - agent_securityThese policies are loaded as a named scope (app:agent_security) by create_tool and evaluated before any registry write. The agent can write to app.generated:* (no deny policy matches), but cannot write to app:* (core entries, models, agent definition) or app.tools:* (built-in tools).
See Security Model for details on policy evaluation.
Models
Section titled “Models”Two models serve different purposes:
- name: gpt-5.1 kind: registry.entry meta: name: gpt-5.1 type: llm.model title: GPT-5.1 comment: Reasoning model capabilities: [generate, tool_use, structured_output, vision, thinking] class: [reasoning] priority: 210 max_tokens: 128000 output_tokens: 32768 pricing: input: 2.5 output: 10 providers: - id: wippy.llm.openai:provider options: reasoning_model_request: true provider_model: gpt-5.1 thinking_effort: 10
- name: gpt-4.1-nano kind: registry.entry meta: name: gpt-4.1-nano type: llm.model title: GPT-4.1 Nano comment: Compression model capabilities: [generate, tool_use, structured_output] class: [fast] priority: 100 max_tokens: 1047576 output_tokens: 32768 pricing: input: 0.1 output: 0.4 providers: - id: wippy.llm.openai:provider provider_model: gpt-4.1-nanoGPT-5.1 handles reasoning and tool use. GPT-4.1 Nano handles context compression at 25x lower cost.
Agent Definition
Section titled “Agent Definition” - name: dev_assistant kind: registry.entry meta: type: agent.gen1 name: dev_assistant title: Dev Assistant comment: Wippy development assistant prompt: | Self-modifying Wippy development agent. You run inside Wippy runtime with access to docs, registry, and dynamic tool creation.
Rules: - NEVER fabricate, guess, or hallucinate facts. If you need real data, use or build a tool to get it. Only state what a tool actually returned. - Maximum 2-3 sentences per response. No bullet lists. No disclaimers. - Never say "I can't" or "I don't have". Build the tool and do it. - Act first, explain only if asked.
To gain new capabilities: doc_search the API, create_tool with Lua source, load_tool, call it. All in one turn. model: gpt-5.1 max_tokens: 2048 tools: - "app.tools:*"The prompt is deliberately terse. Key rules:
- No hallucination — the agent must use tools for real data
- Self-modification — build tools instead of refusing
- Action over explanation — do first, explain if asked
Process
Section titled “Process” - name: agent kind: process.lua meta: command: name: agent short: Start dev assistant source: file://agent.lua method: main modules: [io, json, process, funcs, registry, time, security] imports: prompt: wippy.llm:prompt agent_context: wippy.agent:context compress: wippy.llm.util:compressThe process runs as a terminal command. Security enforcement happens inside create_tool which loads the agent_security policy group and evaluates it before writing.
Imports:
prompt— conversation builderagent_context— agent loading and dynamic tool managementcompress— LLM-based text compression for context management
Create src/tools/_index.yaml with five tools:
doc_search
Section titled “doc_search”Fetches Wippy documentation via the wippy.ai/llm API. Supports two modes: fetch a page by path, or search by query.
local http_client = require("http_client")local json = require("json")
local BASE_URL = "https://wippy.ai/llm"local MAX_CHARS = 8000
local function fetch_page(path) local url = BASE_URL .. "/path/en/" .. path local resp, err = http_client.get(url, { headers = { ["User-Agent"] = "wippy-agent/1.0" }, }) if err then return nil, tostring(err) end if resp.status_code ~= 200 then return nil, "HTTP " .. resp.status_code end
local body = resp.body or "" if #body > MAX_CHARS then body = body:sub(1, MAX_CHARS) .. "\n... (truncated)" end return body, nilend
local function search_docs(query) local url = BASE_URL .. "/search?q=" .. query local resp, err = http_client.get(url, { headers = { ["User-Agent"] = "wippy-agent/1.0" }, }) if err then return { error = tostring(err) } end if resp.status_code ~= 200 then return { error = "HTTP " .. resp.status_code } end
local body = resp.body or "" if #body > MAX_CHARS then body = body:sub(1, MAX_CHARS) .. "\n... (truncated)" end
return { results = body }end
local function handler(input) if input.path then local content, err = fetch_page(input.path) if err then return { error = err } end return { path = input.path, content = content } end
if input.query then return search_docs(input.query) end
return { error = "provide either 'path' or 'query'" }end
return { handler = handler }create_tool
Section titled “create_tool”The core of self-modification. Evaluates namespace deny policies and creates a function.lua entry in the registry with inline Lua source.
The modules field on the generated entry controls what the tool can access. Modules not listed simply do not exist for that entry — there is nothing to block or scan for.
local registry = require("registry")local json = require("json")local security = require("security")
local NAMESPACE = "app.generated"local MAX_SOURCE_LEN = 16000local MAX_NAME_LEN = 64
local ALLOWED_MODULES = { time = true, json = true, http_client = true, expr = true, text = true, base64 = true, yaml = true, crypto = true, hash = true, uuid = true,}Policy evaluation — create_tool loads the agent_security named scope and evaluates the deny policies against the target entry ID. Writes to app:* or app.tools:* are denied; writes to app.generated:* pass (no matching deny policy):
local actor = security.new_actor("service:agent", { role = "agent" })local scope, scope_err = security.named_scope("app:agent_security")if scope_err then return { error = "failed to load security scope: " .. tostring(scope_err) }end
local result = scope:evaluate(actor, action, id)if result == "deny" then return { error = "policy denied: " .. action .. " on " .. id }endRegistry write — the entry is written with source in data.source and only the allowed modules:
local entry = { id = id, kind = "function.lua", meta = { type = "tool", title = input.name, comment = input.description, input_schema = schema, llm_alias = input.name, llm_description = input.description, }, data = { source = input.source, modules = modules, method = "handler", },}
local snap = registry.snapshot()local changes = snap:changes()if existing then changes:update(entry)else changes:create(entry)endchanges:apply()No files on disk. The tool lives entirely in the registry.
load_tool
Section titled “load_tool”Validates the entry is a tool and signals the agent loop to reload:
local function handler(input) local entry, err = registry.get(input.id) if err then return { error = tostring(err) } end if not entry then return { error = "not found: " .. input.id } end if not entry.meta or entry.meta.type ~= "tool" then return { error = "not a tool (meta.type != 'tool'): " .. input.id } end
return { loaded = true, id = entry.id, alias = entry.meta.llm_alias or input.id, description = entry.meta.llm_description or "", }endThe agent loop detects loaded = true in the result and calls ctx:add_tools(id) followed by ctx:load_agent() to recompile the agent with the new tool.
Agent Loop
Section titled “Agent Loop”The agent loop in src/agent.lua handles streaming, tool execution, dynamic loading, and context compression.
Streaming
Section titled “Streaming”Uses the same coroutine + channel pattern from the LLM Agent tutorial:
coroutine.spawn(function() local response, err = session.runner:step(session.conversation, { stream_target = { reply_to = process.pid(), topic = STREAM_TOPIC, }, }) done_ch:send({ response = response, err = err })end)Tool Execution
Section titled “Tool Execution”Tools are called via funcs.call() with pcall for safety:
local ok, result = pcall(funcs.call, tc.registry_id, args)Dynamic Tool Loading
Section titled “Dynamic Tool Loading”When load_tool returns loaded = true, the agent reloads itself:
flowchart TD A[load_tool returns loaded=true] --> B[ctx:add_tools id] B --> C[ctx:load_agent] C --> D[New runner with added tool] D --> E[Conversation preserved] E --> F[Next LLM step sees new tool]local function handle_tool_loading(tool_calls, results) local reload_needed = false for _, tc in ipairs(tool_calls) do if tc.name == "load_tool" then local result = results[tc.id] if result and result.loaded then session.ctx:add_tools(result.id) reload_needed = true end end end if reload_needed then reload_agent() endendThe conversation is preserved across reloads because it lives in the prompt builder, not in the runner.
Context Compression
Section titled “Context Compression”When prompt tokens exceed 96K (75% of the 128K context window), the conversation is compressed using GPT-4.1 Nano:
if response.tokens and response.tokens.prompt_tokens and response.tokens.prompt_tokens > PROMPT_TOKEN_LIMIT then try_compress()endCompression extracts message content, calls compress.to_size() targeting 4000 characters, and replaces the conversation with a summary:
local summary = compress.to_size(COMPRESS_MODEL, full_text, COMPRESS_TARGET)session.conversation = prompt.new()session.conversation:add_system("Conversation summary:\n\n" .. summary)Security Model
Section titled “Security Model”The agent is secured through namespace deny policies and module-level access control.
flowchart TD LLM[LLM generates tool] --> P{Namespace Deny Policies} P -->|scope:evaluate| Check{Target namespace?} Check -->|app.generated:*| OK[No deny match] Check -->|app:* or app.tools:*| Deny[Policy Denied]
OK --> M{Module Allowlist} M -->|only granted modules| R[Registry write] M -->|unknown module requested| Err[Rejected]Namespace Deny Policies
Section titled “Namespace Deny Policies”| Policy | Resources | Effect |
|---|---|---|
deny_core_ns | app:* | deny |
deny_tools_ns | app.tools:* | deny |
create_tool loads the agent_security policy group and evaluates against the target entry ID. Since deny policies only match app:* and app.tools:*, writes to app.generated:* pass through (result is undefined, meaning “not denied”).
This prevents the agent from:
- Modifying its own prompt or agent definition (
app:dev_assistant) - Overwriting its built-in tools (
app.tools:*) - Changing infrastructure entries (
app:processes, etc.)
Module Access Control
Section titled “Module Access Control”Generated tools declare their modules in data.modules. Only modules from the ALLOWED_MODULES set are permitted. The Wippy runtime enforces this at the module level — if a module is not listed on the entry, require() returns an error. There is no source code scanning because there is nothing to scan for: modules that are not granted do not exist in the execution context.
Run directly from hub:
wippy run wippy/micro-agi agentOr clone and run locally:
cd micro-agiwippy init && wippy updatewippy run agentdev assistant (quit to exit)
> what time is it? [doc_search] ok [create_tool] ok [load_tool] ok [+] app.generated:current_time_utc [current_time_utc] okThe current UTC time is 2026-02-13T03:13:41Z.
> fetch https://httpbin.org/get and show my ip [create_tool] ok [load_tool] ok [+] app.generated:http_get [http_get] okYour IP is 203.0.113.42.Next Steps
Section titled “Next Steps”- LLM Agent — Build a basic agent from scratch
- Agent Module — Agent framework reference
- Registry — How the registry works
- Security Model — Declarative security policies
- Entry Kinds — Available entry types