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 builder
  • agent_context — Agent loading and dynamic tool management
  • compress — LLM-based text compression for context management

Tools

Create src/tools/_index.yaml with five tools:

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