Micro AGI
Study an agent that reads documentation, generates Lua tools, registers them at runtime, and loads them into its active session.
Classification: reference implementation walkthrough. The snippets explain the
published wippy/micro-agi module but are intentionally not a complete source tree.
Run the Hub module to exercise the implementation; use the LLM Agent tutorial when
you need a self-contained build.
What the Package Demonstrates
A terminal agent that:
- Streams answers from an LLM.
- Searches Wippy documentation for APIs.
- Inspects the registry for existing capabilities.
- Creates and loads tools when a capability is missing.
- Compresses conversation history when it approaches the context limit.
flowchart LR
User -->|prompt| Agent
Agent -->|step| LLM[Configured model]
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
end
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: apply namespace denylist + 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 response
Tools are registry entries. To create one, the agent writes a function.lua entry with inline Lua source in data.source; the runtime then compiles and loads that entry.
Published Package Structure
The package owns all of these files. This page reproduces doc_search.lua and the
contracts that matter to the architecture, but abbreviates the registry helpers,
changeset plumbing, dynamic-loader helpers, and the agent loop. In particular, the
create_tool, load_tool, and agent.lua sections are excerpts, not files that can
be copied verbatim. The complete registry definitions for registry_list and
registry_read also remain in the published module.
micro-agi/
├── .wippy.yaml
├── wippy.lock
└── src/
├── _index.yaml
├── README.md
├── agent.lua
└── tools/
├── _index.yaml
├── doc_search.lua
├── registry_list.lua
├── registry_read.lua
├── create_tool.lua
└── load_tool.lua
Infrastructure
The package uses this .wippy.yaml configuration:
version: "1.0"
logger:
encoding: console
Entry Definitions
The following selected src/_index.yaml entries show the 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:processes
- name: __dep.security
kind: ns.dependency
component: wippy/security
version: "*"
wippy/security provides the wippy.security:process policy group that the LLM module's background services run under; without it they fail to start.
Security Policies
Two security.policy entries form an application-level namespace denylist:
- 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_security
These policies are loaded as a named scope (app:agent_security) by
create_tool. The helper rejects an explicit deny for app:* (core entries,
models, and the agent definition) or app.tools:* (built-in tools), but treats
the unmatched undefined result for app.generated:* as passing its bespoke
filter. This is not Wippy runtime authorization: guarded operations require an
explicit allow from the execution context, including the security-module
operations shown below and registry.apply inside changes:apply().
A third policy grants the process itself access to the registry. A process launched without a security context is denied every registry read, so the agent command carries this policy as its own scope:
- name: agent_policy
kind: security.policy
policy:
actions: "*"
resources: "*"
effect: allow
See Security Model for details on policy evaluation.
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: 400000
output_tokens: 128000
pricing:
input: 1.25
output: 10
providers:
- id: wippy.llm.openai:provider
options:
reasoning_model_request: true
provider_model: gpt-5.1
- 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-nano
GPT-5.1 handles reasoning and tool use. GPT-4.1 Nano handles context compression.
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
thinking_effort: 10
max_tokens: 2048
tools:
- "app.tools:*"
The prompt gives the agent three operating rules:
- Use retrieved data — use tools for external facts.
- Create missing capabilities — build a tool when an allowed capability is absent.
- Prioritize actions — perform the requested operation before explaining it.
Process
- name: agent
kind: process.lua
meta:
command:
name: agent
short: Start dev assistant
security:
actor:
id: app:agent
policies:
- app:agent_policy
source: file://agent.lua
method: main
modules: [io, json, funcs, registry, time, security]
imports:
prompt: wippy.llm:prompt
agent_context: wippy.agent:context
compress: wippy.llm.util:compress
The process runs as a terminal command. meta.command.security gives it the actor and scope it runs under — without it registry.get fails with not allowed to access entry and the agent never loads. Security enforcement for writes 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
Tools
Create src/tools/_index.yaml with five tools:
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, nil
end
local function search_docs(query)
local url = BASE_URL .. "/search?q=" .. http_client.encode_uri(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
This tool evaluates the package's namespace denylist and creates a
function.lua registry entry with inline Lua source.
The modules field on the generated entry controls which non-ambient runtime modules the
tool can require. The process module is ambient for every executable Lua entry, so
omitting it is not a security boundary; process operations still rely on runtime security
policies.
local registry = require("registry")
local json = require("json")
local security = require("security")
local NAMESPACE = "app.generated"
local MAX_SOURCE_LEN = 16000
local 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,
}
Denylist evaluation — create_tool loads the agent_security named scope.
Writes to app:* or app.tools:* are rejected when the scope returns deny;
an unmatched app.generated:* target returns undefined and passes this
application filter:
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 }
end
This check does not authorize the registry mutation. The current command also
needs a runtime actor and scope that explicitly allow the security-module calls
and registry.apply.
Registry 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)
end
local _, apply_err = changes:apply()
if apply_err then
return { error = "failed to apply registry change: " .. tostring(apply_err) }
end
The generated tool is stored in the registry rather than written to a source file.
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 "",
}
end
The 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
The agent loop in src/agent.lua handles streaming, tool execution, dynamic loading, and context compression.
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
Tools are called via funcs.call(). pcall catches raised Lua errors, while the normal
second return from funcs.call() carries invocation errors:
local ok, result, call_err = pcall(funcs.call, tc.registry_id, args)
if not ok then
results[tc.id] = { error = tostring(result) }
elseif call_err then
results[tc.id] = { error = tostring(call_err) }
else
results[tc.id] = result
end
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()
end
end
The conversation is preserved across reloads because it lives in the prompt builder, not in the runner.
Context Compression
When prompt tokens exceed 300K (75% of the 400K 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()
end
Compression extracts message content, calls compress.to_size() targeting 4000 characters, and replaces the conversation with a summary:
local summary, compress_err = compress.to_size(COMPRESS_MODEL, full_text, COMPRESS_TARGET)
if compress_err then
return nil, compress_err
end
session.conversation = prompt.new()
session.conversation:add_system("Conversation summary:\n\n" .. summary)
Security Model
An application denylist and module-level access controls constrain generated tools, but they do not replace runtime authorization.
flowchart TD
LLM[LLM generates tool] --> P{Application Namespace Denylist}
P -->|scope:evaluate| Check{Target namespace?}
Check -->|app.generated:*| OK[No deny match]
Check -->|app:* or app.tools:*| Deny[Policy Denied]
OK --> M{Non-ambient Module Allowlist}
M -->|only listed non-ambient modules| R[Registry write]
M -->|unknown module requested| Err[Rejected]
R --> A[Ambient process API remains available]
Namespace Denylist
| Policy | Resources | Effect |
|---|---|---|
deny_core_ns |
app:* |
deny |
deny_tools_ns |
app.tools:* |
deny |
create_tool loads the agent_security policy group and evaluates the target
entry ID. It deliberately treats undefined as "not denied" for this
application-level filter. Wippy's guarded authorization does not: it permits an
operation only on explicit allow. The context that runs this code must still
carry the required runtime permissions.
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
Generated tools declare non-ambient capabilities in data.modules, and create_tool
accepts only names from ALLOWED_MODULES. An undeclared non-ambient module cannot be
required. The runtime still injects process into every executable Lua entry, including a
generated tool, so process operations must be constrained with security policies rather
than by omitting process from data.modules.
This tutorial does not define policies for process.spawn or process.exec. Its generated
tools are therefore not a complete sandbox: add runtime policies for ambient process
operations before allowing untrusted tool source.
Run and Current Package Limitation
The published artifact is the Hub module. Start in a fresh empty directory that
does not contain wippy.lock; Hub bootstrap rejects an unrelated or multi-root
lock. The first run creates the deployment lock, and later runs from the same
directory reuse that matching lock.
mkdir micro-agi-deploy
cd micro-agi-deploy
wippy run wippy/micro-agi agent
The command downloads the selected module version, resolves its declared
dependencies, and invokes its agent command.
It still requires the provider credentials and model configuration expected by that module, plus registry/network access for Hub download and documentation search. This page does not provide a local clone or lockfile, so it does not claim a reproducible source build.
At the reviewed release, wippy/micro-agi v0.3.1 declares no
meta.command.security context for agent. With default strict mode, the
guarded tool paths—including funcs.call, registry reads and writes, and the
documentation search HTTP request—do not receive the explicit allows they
require. The tool and self-modification flows above are therefore reference
designs, not successful default-strict-mode runs. Do not disable strict mode to
make an untrusted code generator work; the package should first add a least-
privilege command scope for its required actions.
Next Steps
- LLM Agent — Build a basic agent from scratch
- Agent Module — Agent framework reference
- Registry — Registry concepts
- Security Model — Declarative security policies
- Entry Kinds — Available entry types