agent-block
Single-purpose agent building block with built-in mesh communication.
What is agent-block?
A headless agent runtime. Each agent runs as a single process, executes its task, then exits. No rich interactive TUI, no sub-agent orchestration — orchestration belongs to the caller (shell, A2A, CI, etc.).
agent-block handles the infrastructure that individual agents shouldn't have to — mesh connectivity (A2A), MCP server management, LLM API access — so that Lua code focuses purely on domain logic.
Think of it like Envoy for agents: the process itself is simple, but the communication layer is fully capable.
Design Decisions
- Single run — One process, one task, one exit. Orchestration belongs to the caller (shell, A2A, CI, etc.), not inside the agent
- Headless — No terminal UI. Agents are composed via A2A/mesh protocols, not interactive prompts
- Runtime owns the protocol — Mesh, MCP, and HTTP are provided by the runtime. Lua code never deals with connection management or wire formats
- Lua for logic, Rust for plumbing — Domain logic in Lua. VM, networking, and protocol handling in Rust
Architecture
┌─────────────────────────────────────────────┐
│ agent-block (binary) │
│ │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ │
│ │ mlua-isle│ │ mesh-sdk │ │ llm-client│ │
│ │ (Lua VM) │ │ (relay) │ │ (API) │ │
│ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ ─────┴─────────────┴──────────────┴─────── │
│ Lua Stdlib Bridge │
│ mesh.send / mesh.on / llm.chat / fs.read │
│ tool.register / tool.call / log.* / env.* │
│ mcp.connect / mcp.call / mcp.list_tools │
└─────────────────────────────────────────────┘
↕ WebSocket ↕ stdio
┌─────────────────┐ ┌──────────────────┐
│ agent-mesh │ │ MCP Servers │
│ relay │ │ (outline-mcp) │
└─────────────────┘ └──────────────────┘
Usage
# Basic
# With project context
# With mesh
ANTHROPIC_API_KEY=...
# Pass a prompt and system context from the CLI
CLI flags --prompt and -c / --context inject the _PROMPT and _CONTEXT Lua globals
into the script. Use them with agent.run:
-- my_agent.lua
local agent = require
local result = agent.
print
Both flags also accept environment variables as fallback:
| Flag | Env var |
|---|---|
--prompt |
AGENT_BLOCK_PROMPT |
-c / --context |
AGENT_BLOCK_CONTEXT |
MCP Echo Harness
A self-contained reference MCP server for smoke-testing the agent-block MCP client bridge. Exposes tools, resources, prompts, logging, and sampling over stdio or HTTP.
# stdio (default) — connect via mcp.connect("echo", "target/debug/examples/echo_mcp_server", {})
# HTTP on an ephemeral port — prints ECHO_MCP_URL=http://127.0.0.1:<port>/mcp
# Also emit 5 log notifications (1-second intervals) and attempt a sampling round-trip
Verify from Lua (requires the server to be running with --transport http):
local url = os.
mcp.
print -- 2 tools: echo, slow_echo
print -- 2 resources: text://hello, text://note
print -- 1 prompt: greet
-- call slow_echo to exercise progress notifications
mcp.
print
See examples/verify_echo_harness.lua for the full verification script.
MCP Resource Subscribe Smoke Server
A standalone binary example for shell-level smoke-testing the Resource Subscribe API
(mcp.subscribe_resource / mcp.on_resource_update). Starts an HTTP MCP server with
resources.subscribe capability enabled and fires at least one notify_resource_updated
event after each subscribe call.
# Ephemeral port — prints SUBSCRIBE_TEST_SERVER_URL=http://127.0.0.1:<port>/mcp
# Fixed port
# Periodic notify every 500 ms (instead of single fire on subscribe)
Shell smoke (requires the server URL printed above):
# Expect: SUBSCRIBE_OK, RESOURCE_UPDATE_EV_OK, UPDATE_HITS=1, FIXTURE_DONE
See docs/runbooks/e2e-mcp-resource-subscribe.md for the full positive/negative verification
procedure (Step 2 = shell positive, Step 3 = negative against a server without subscribe
capability).
Lua API
llm.*
llm.chat(messages, opts)— LLM call (Anthropic Messages API)
tool.*
tool.register(name, schema, handler [, meta])— Register a tool. Optionalmeta = { group = "..." }assigns the tool to a named group for use withagent.run({ tool_groups = {...} }).tool.call(name, input)— Call a registered tooltool.list()— List registered tool namestool.schema()— Anthropic tools-format schema array (includesgroupfield when set)
mcp.*
Support status, capability matrix, and the tool-grouping design rationale
live in docs/architecture/mcp-support.md.
mcp.connect(name, command, args)— Spawn MCP server over stdio + initialize handshakemcp.connect_http(name, url, opts)— Connect to an MCP server over HTTP transport.opts.transport = "sse" | "http"(default"http"= Streamable HTTP;"sse"= SSE).opts.headerstable is forwarded as request headers.mcp.call(name, tool_name, arguments)— Call an MCP toolmcp.list_tools(name)— List available toolsmcp.list_resources(name)— List resources exposed by the server. Returns{ ok=true, resources=[{uri, name, description, mimeType, ...}] }.mcp.list_resource_templates(name)— List resource URI templates exposed by the server. Returns{ ok=true, resource_templates=[{uriTemplate, name, ...}] }.mcp.read_resource(name, uri)— Read a resource by URI. Returns{ ok=true, contents=[{uri, mimeType, text|blob}] }.mcp.list_prompts(name)— List prompt templates exposed by the server. Returns{ ok=true, prompts=[{name, description, arguments}] }.mcp.get_prompt(name, prompt_name, args)— Retrieve a rendered prompt template. Returns{ ok=true, description, messages=[{role, content}] }.mcp.complete(name, ref, arg_name, arg_value)— Request completion suggestions (MCP Completion typeahead, Phase 3).refis{type="ref/prompt", name=...}or{type="ref/resource", uri=...}. Returns{ ok=true, values=[...], total=number?, has_more=bool? }or{ ok=false, error=str }.mcp.on_progress(name, handler)— Register a per-server progress notification callback.handler(token, progress, total, message)is called for eachnotifications/progressevent from the named server. Handler must be a pure Lua function.mcp.on_log(name, handler)— Register a per-server log notification callback.handler(level, logger, data)is called for eachnotifications/messageevent from the named server. When no handler is registered the notification is forwarded to the Rusttracingtarget"lua"at the corresponding level (debug/info/notice/warning/ error/critical/alert/emergency). Handler must be a pure Lua function.mcp.cancel(name, request_id)— Send anotifications/cancellednotification to the named server for the givenrequest_id. Also fired automatically whenmcp.calltimes out. Explicit use is only needed for manual cancellation flows.mcp.set_sampling_handler(server_name, handler)— Register a per-server Lua function to respond tosampling/createMessagerequests from the MCP server.handler(params)receives theCreateMessageRequesttable and must return a table matchingCreateMessageResult({ model, stop_reason, role, content }). When no handler is registered the server receivesmethod_not_found.mcp.set_elicitation_handler(server_name, fn)— Register a per-server Lua function to respond toelicitation/createrequests originating from the MCP server (server→client, Form variant only).fn(server_name, message, schema_json)must return a table withaction = "accept"|"decline"|"cancel"and (for accept) acontenttable conforming to the schema. Url-variant elicitation requests are always declined without reaching the callback. Handler must be a pure Lua function.mcp.set_roots_handler(server_name, fn)— Register a per-server Lua function to respond toroots/listrequests originating from the MCP server (server→client direction).fn(server_name)must return a Lua array of root tables, each with at least aurifield and an optionalnamefield (e.g.{ { uri="file:///home/user", name="home" } }). When no handler is registered the server receivesmethod_not_found. Handler must be a pure Lua function; C functions and Rust-bound callbacks are not supported.mcp.notify_roots_list_changed(name)— Send anotifications/roots/list_changednotification to the named server (client→server, fire-and-forget). Use this whenever the client's set of filesystem roots changes so the server can re-request the updated list viaroots/list. Failures are logged atwarnlevel and silently discarded.mcp.server_info(name)— Return the server'sInitializeResultas a Lua table. Returns{ ok=true, server_info={serverInfo, capabilities, ...} }on success. Useful for inspecting which MCP capability groups (resources, prompts, tools, etc.) a server declares. Returns{ ok=false, error="..." }if the server is not connected.mcp.ping(name)— Send apingkeepalive request to the named server and measure round-trip latency. Returns{ ok=true, latency_ms=N }on success or{ ok=false, error="..." }on failure (unknown server, timeout, or RPC error).mcp.subscribe_resource(server, uri)— Send aresources/subscribeRPC for the given resource URI. Returns{ ok=true }on success or{ ok=false, error="..." }on failure. Requires the server to declare theresources.subscribecapability.mcp.unsubscribe_resource(server, uri)— Send aresources/unsubscribeRPC to stop receiving change notifications for the given URI. Same return shape assubscribe_resource.mcp.on_resource_update(server, callback)— Register a per-server callback fornotifications/resources/updatedevents.callback(ev)whereev = { type="resource_update", server, uri }. Handler must be a pure Lua function.mcp.on_resources_list_changed(server, callback)— Register a per-server callback fornotifications/resources/list_changedevents.callback(ev)whereev = { type="resources_list_changed", server }.mcp.on_tools_list_changed(server, callback)— Register a per-server callback fornotifications/tools/list_changedevents.callback(ev)whereev = { type="tools_list_changed", server }.mcp.on_prompts_list_changed(server, callback)— Register a per-server callback fornotifications/prompts/list_changedevents.callback(ev)whereev = { type="prompts_list_changed", server }.mcp.disconnect(name)— Disconnect server
mesh.*
mesh.send(agent_id, payload)— Synchronous send (raises Lua error on failure)mesh.request(agent_id, payload)— Request-responsemesh.agent_id()— Own AgentId
std.fs.* (mlua-batteries)
std.fs.read(path),std.fs.write(path, content),std.fs.glob(pattern),std.fs.exists(path)std.fs.walk(dir),std.fs.copy(src, dst),std.fs.mkdir(path),std.fs.remove(path)std.fs.is_file(path),std.fs.is_dir(path),std.fs.read_binary(path),std.fs.write_binary(path, bytes)
sh.*
sh.exec(cmd, opts)— Execute a shell command
std.json.* (mlua-batteries)
std.json.encode(value),std.json.decode(str),std.json.encode_pretty(value)
std.env.* (mlua-batteries + agent-block extensions)
std.env.get(key),std.env.set(key, value),std.env.get_or(key, default),std.env.home()std.env.agent_id(),std.env.project_root()— agent-block specific
std.path.* / std.time.* (mlua-batteries)
std.path.join(...),std.path.basename(path),std.path.dirname(path)std.time.now(),std.time.sleep(secs),std.time.measure(fn)
std.kv.* (mlua-batteries, SQLite-backed)
std.kv.get(ns, key)— retrieve a value by namespace + key; returnsnilif absentstd.kv.set(ns, key, value)— store a value (any Lua value, JSON-encoded internally)std.kv.delete(ns, key)— delete a key; returnstrueif it existed,falseotherwisestd.kv.list(ns, prefix?)— list keys in a namespace, optionally filtered by prefixstd.kv.register_tools()— registerkv_get,kv_set,kv_delete,kv_listas LLM-callable tools
Storage: AGENT_BLOCK_HOME/kv.sqlite (override via AGENT_BLOCK_KV_PATH; :memory: supported).
std.sql.* (mlua-batteries, SQLite-backed)
std.sql.execute(sql, params?)— execute a DML statement; returns{ affected = N }std.sql.query(sql, params?)— execute a query; returns an array of row tablesstd.sql.register_tools()— registersql_execute,sql_queryas LLM-callable tools
Storage: AGENT_BLOCK_HOME/sql.sqlite (override via AGENT_BLOCK_SQL_PATH; :memory: supported).
std.ts.* (agent-block, SQLite-backed TSDB)
std.ts.append(series, value, tags?, at?)— append a data point;valueis a Lua number or table (JSON-encoded, losslessly decoded on read);tagsis an optional{key=value}table;atis an optional Unix timestamp in milliseconds (default: now)std.ts.query(series, opts)— range query;optsfields:from,to(integer ms) — time range (default: full range)tags(table) — AND-filter; each key-value pair uses SQLitejson_extractagg(string) —"count"|"sum"|"avg"|"last"(optional)bucket_ms(integer) — bucket width; requiresagg; produces time-bucketed rowslimit,offset(integer) — pagination
std.ts.last(series, tags?)— most-recent data point; same tag AND-filter asquerystd.ts.register_tools()— registerts_append,ts_query,ts_lastas LLM-callable tools
Ordering guarantee: raw-path results (query without agg) are ordered by (ts ASC, rowid ASC);
last and query with agg="last" resolve same-millisecond ties by (ts DESC, rowid DESC) so
the last-appended row always wins. This is a deterministic SQLite rowid tie-breaker — no DDL or
index change is required.
Storage: AGENT_BLOCK_HOME/ts.sqlite (override via AGENT_BLOCK_TS_PATH; :memory: supported).
agent (StdPkg — require("agent"))
Built-in ReAct loop module. Available without any path configuration after cargo install.
local agent = require
local result = agent.
if result.
-- result fields: ok, content, usage{input_tokens,output_tokens,total_tokens}, num_turns, error, messages
Provider Switching
By default agent.run uses the Anthropic Messages API. Pass provider = "openai" to route to any OpenAI-compatible endpoint (vLLM, llama.cpp, OpenRouter, RunPod, etc.):
-- Anthropic (default) — requires ANTHROPIC_API_KEY
local result = agent.
-- OpenAI — requires OPENAI_API_KEY (or opts.api_key)
local result = agent.
-- Local vLLM / llama.cpp / RunPod — custom base_url
local result = agent.
Environment variables used per provider:
| provider | default key env | override via |
|---|---|---|
anthropic |
ANTHROPIC_API_KEY |
opts.api_key / opts.api_key_env |
openai |
OPENAI_API_KEY |
opts.api_key / opts.api_key_env |
opts.base_url overrides the endpoint root. Default for openai is https://api.openai.com/v1.
cache_control, context_management, and context_management_config are Anthropic-only: they are operative when provider="anthropic" (or unset) and emit a warn-level log message then are ignored when provider="openai".
Key behaviours:
- MCP servers listed in
mcp_serversare connected automatically and disconnected on exit (even on error). - Each entry may use the stdio form
{ name, command, args }or the HTTP form{ name, url, transport_opts }. Both forms can coexist in the same list. - Pass
sampling = fninagent.runopts to register a single Lua function as thesampling/createMessagehandler for every connected MCP server (mcp.set_sampling_handleris called per server automatically). - Pass
enable_resources = trueinagent.runopts to automatically register{server}__mcp_list_resourcesand{server}__mcp_read_resourceas LLM-callable tools for each connected server that declares theresourcescapability. Defaultfalse. If a server does not declareresources, the opt-in is silently skipped (logged atinfo). - Pass
enable_prompts = trueinagent.runopts to automatically register{server}__mcp_list_promptsand{server}__mcp_get_promptas LLM-callable tools for each connected server that declares thepromptscapability. Defaultfalse. Capability check and silent skip apply the same way asenable_resources. - Pass
on_progress = fn(ev)inagent.runopts to receive progress notifications from all connected MCP servers. The callback is called with an envelope table{ type="progress", server, token, progress, total, message }. No capability gate — all servers are registered. User callback errors are swallowed and logged atwarn. - Pass
progress_to_log = trueinagent.runopts to bridge progress notifications tolog.infoautomatically. Ignored whenon_progressis also set (callback takes priority). Defaultfalse. - Pass
on_log = fn(ev)inagent.runopts to receive log notifications from servers that declare theloggingcapability. The callback is called with an envelope table{ type="log", server, level, logger, data }. Servers without logging capability are silently skipped (logged atinfo). User callback errors are swallowed and logged atwarn. - Pass
log_to_stderr = trueinagent.runopts to bridge server log notifications tolog.debug|info|warn|errorautomatically. Ignored whenon_logis also set (callback takes priority). Logging capability gate applies the same way ason_log. Defaultfalse. - MCP tool names are namespaced as
server_name__tool_nameto avoid collisions. - MCP tools are automatically assigned to a group for use with
tool_groups. Group resolution follows this priority: (1) the tool's_meta.groupfield (string, non-empty) declared by the server takes precedence — rmcp serialisesTool.metaas_metavia#[serde(rename = "_meta")]; (2) fallback to the server name. Passtool_groups = { "outline" }(for example) toagent.runto include only tools from that MCP server. This aligns with the MCP SEP-986 tool-name prefix grouping guidance and themcp__<server>__*convention used by Claude Code. Tools without an explicit group (e.g. plain registered Lua tools) fall into the"default"group. - Tool dispatch: MCP tools via
mcp.call(), registered Lua tools viatool.call(). - Never throws — all errors returned as
{ ok=false, error="..." }. - Context editing is on by default: once the conversation crosses ~80K input tokens, Anthropic evicts all but the most recent 3 tool-use / tool-result pairs server-side so the loop can keep running. Works on Sonnet 4 / Sonnet 4.5 / Haiku 4.5 / Opus 4 / 4.1 / 4.5. Pass
context_management = falseto disable, orcontext_management_config = { edits = { ... } }to replace the default entirely (the whole table is forwarded asbody.context_management; no partial merge). on_turn(info)gains an additiveinfo.context_managementfield that forwards the rawresponse.context_managementfrom Anthropic ({ applied_edits = { { type, cleared_tool_uses, cleared_input_tokens }, ... } }). The field is absent on turns where the server did not fire any edit — nil-guard before indexing.- The
blocks/directory is embedded in the binary; place a localblocks/agent/init.luain the project root to override. - LLM dump logging is safe-by-default and ENV-driven:
AGENT_BLOCK_LLM_DUMP=off|meta|full(defaultoff)- when unset,
RUST_LOGcontainingdebugortraceenablesmeta fullis downgraded tometawhenAGENT_BLOCK_ENV=prod|productionunlessAGENT_BLOCK_LLM_DUMP_ALLOW_PROD=true- request auth headers (
x-api-key/authorization) are always redacted in dump logs - log lines use fixed-order
key=valueformat with a unique marker (prefix=ab.obs component=llm); legacyprefix=ab.llmlines are also emitted for compatibility metaincludes call correlation and runtime signals (call,turn,iter,latency_ms,stop_reason,tool_uses, token usage, context edit count)- optional
agent.run({ log_meta = { trace_id, agent_id, agent_name, run_id } })appends external context to dump lines (same keys can also come fromAGENT_BLOCK_TRACE_ID,AGENT_BLOCK_AGENT_ID,AGENT_BLOCK_AGENT_NAME,AGENT_BLOCK_RUN_ID)
compile_loop (Filesystem block — require("compile_loop"))
Tool factory for the autonomous compile-and-fix loop. The primary surface is
compile_loop.make(conf), which returns a tool_def consumable directly by agent.run.
Place blocks/compile_loop/init.lua in the project root (resolved via the filesystem
blocks/ path; no EMBEDDED_BLOCKS entry is required).
local compile_loop = require
local agent = require
-- Define a caller-supplied runner function
local
-- Build a tool_def and pass it to the parent agent
local td = compile_loop.
local result = agent.
compile_loop.make(conf) returns { name, schema, handler }. As a side-effect
tool.register(name, schema, handler) is called, so the registry and tool_def.handler
share the same function identity. The tool name defaults to "compile_loop"; pass
conf.name to override (useful when registering multiple instances).
Multi-file mode: pass target_files = {pathA, pathB, ...} together with edit_mode = "diff" to edit several files in a single loop. The runner signature changes to function(paths) (list). Multi-file lazy-load (the read_file tool dispatch loop, sliding window K=3, stderr trim) works on both the "anthropic" and "openai" provider paths. See blocks/compile_loop/README.md §"Multi-file mode" and the examples/test_anthropic_compile_loop_multi*.lua / examples/test_openai_compile_loop_multi_lazy_load.lua smoke scripts.
Read-and-distill for large files: in multi-file lazy-load mode, read_file now inspects
file size before returning content. Files at or below READ_FILE_FULL_THRESHOLD (default
10 000 chars) are returned verbatim as before. Files that exceed the threshold are split into
line-based chunks and summarised by the child LLM (provider-agnostic, same call path as the
outer loop), and the tool returns a digest string plus a line-index ("L1-50: ...\nL51-180: ...").
The digest cache (mf_state.file_digest[path]) survives per-iteration resets; only file-mtime
changes or file_digest_refresh = "always" trigger re-distillation.
read_file_range tool: after receiving a digest the LLM can call read_file_range(path, line_start, line_end) to retrieve the verbatim lines from that range. The handler reads
directly from disk without passing through distillation, regardless of file size. Guards:
target_files allowlist, 1-indexed inclusive range, max READ_FILE_RANGE_MAX_LINES
lines (default 500) per call.
New optional conf fields for large-file distillation:
| field | type | default | description |
|---|---|---|---|
conf.target_func |
string | nil | nil |
Function name to prioritise in chunk ordering. Chunks containing this name are ranked second (after last_err-overlap chunks). Existing callers that omit this field are unaffected. |
conf.distill_threshold |
number | nil | 10 000 | Override READ_FILE_FULL_THRESHOLD per-instance. |
conf.distill_chunk_lines |
number | nil | 200 | Lines per distill chunk. |
conf.distill_max_tokens |
number | nil | 4 000 | Max chars for the packed digest returned to the LLM. |
Tool input (supplied by the LLM at call time): spec (string, required),
target_file (absolute path, required), lang (string, optional).
edit_mode (opt-in diff mode): pass edit_mode = "diff" to compile_loop.make to
switch the child LLM to Aider-style SEARCH/REPLACE patch output instead of emitting the
whole file on every iteration. This is the preferred mode for large existing files where
minimal-edit is critical (e.g. fixing a single function in a 500-line file).
local td = compile_loop.
The child LLM must output one or more SEARCH/REPLACE blocks in this exact format:
<<<<<<< SEARCH
<existing text to replace, character-exact>
=======
<replacement text>
>>>>>>> REPLACE
compile_loop applies each block in order using a two-stage match (exact → whitespace-
normalized). Blocks whose SEARCH text does not match the current file content are reported
back to the child LLM with the full file content attached, triggering a retry.
When target_file is absent or empty at loop entry, edit_mode = "diff" automatically
falls back to "full" with a warn-level log line (diff requires a base file to patch).
target_file dual role: when target_file already exists at loop entry, its content is
embedded in the initial user message as === Current file content === so the child LLM can
build on it rather than generating from scratch. In full mode the file is overwritten on
every iteration; in diff mode only the matched regions are replaced. When the file is absent
or empty, the message contains spec only — preserving the original synthesis behaviour
(backward-compatible).
Target model class: the full-file output strategy is designed for Qwen3 / Haiku-grade mid-weight models. Emitting the whole file on each iteration avoids the apply-failure cost of diff/Edit-tool workflows and keeps the feedback loop simple and fast. For the latest Sonnet/Opus with native edit-tool support, a diff-based block is a future consideration (separate issue; out of scope here).
Tool output JSON (never contains code or history — Counter WF-A defence):
{ ok, iters, summary, failure_reason?, last_error?, artifact_path }
failure_reason values: "llm_call" | "open_target_file" | "stagnation" | "max_iters".
LLM inheritance (Crux #2): when conf.llm is omitted (or individual fields are absent),
compile_loop resolves provider, base_url, api_key, api_key_env, and model from
the parent agent.run call context at tool-dispatch time. No hardcoded provider default;
no error for missing credentials at make() time.
Stagnation detection: when 3 consecutive iterations produce identical runner stderr
the loop gives up immediately, independent of the remaining iteration budget.
failure_reason = "stagnation".
Observability (ab.obs events): compile_loop emits structured ab.obs log events on
each iteration, gated by AGENT_BLOCK_LLM_DUMP (same env var as the agent block). Set
AGENT_BLOCK_LLM_DUMP=meta to activate. Each line uses the key=value format with
prefix=ab.obs component=compile_loop.
| event | when emitted | fields |
|---|---|---|
iter_start |
start of each iteration | iter, target_file |
iter_result |
after runner executes | iter, ok, exit_code, stderr_len |
converged |
before PASS return | iters |
stagnation |
before stagnation give-up | iters |
max_iters_reached |
before max_iters give-up | iters |
Provider support: "anthropic" and "openai"-compatible endpoints (vLLM, llama.cpp,
OpenRouter, RunPod, etc.) are both fully implemented in conf.llm.
conf.llm.provider |
Default key env | Override via |
|---|---|---|
"anthropic" |
ANTHROPIC_API_KEY |
conf.llm.api_key / api_key_env |
"openai" |
OPENAI_API_KEY |
conf.llm.api_key / api_key_env |
External runner examples
| Example | Runner | Provider |
|---|---|---|
examples/test_anthropic_compile_loop.lua |
inline lua | Anthropic |
examples/test_qwen_compile_loop.lua |
inline lua | Qwen (OpenAI-compat) |
examples/test_qwen_compile_loop_rust.lua |
inline cargo | Qwen (OpenAI-compat) |
examples/test_qwen_compile_loop_lust.lua |
mlua-probe MCP | Qwen (OpenAI-compat) |
examples/test_compile_loop_parent.lua |
inline lua | Anthropic parent + Qwen child |
examples/test_anthropic_compile_loop_pytest.lua |
inline pytest | Anthropic |
examples/test_anthropic_compile_loop_multi_lazy_load.lua |
inline lua (multi-file) | Anthropic |
examples/test_openai_compile_loop_multi_lazy_load.lua |
inline lua (multi-file) | Qwen (OpenAI-compat) |
tests/fixtures/compile_loop_distill_mock.lua |
shared e2e fixture (distill, multi-file) | Anthropic / OpenAI-compat |
tests/fixtures/compile_loop_distill_range_mock.lua |
e2e fixture (read_file_range verbatim) | Anthropic |
coding_agent (Filesystem block — require("coding_agent"), thin facade)
Backward-compatible facade over compile_loop. Prefer the compile_loop.make() API for
new code. coding_agent is retained for existing callers.
Place blocks/coding_agent/init.lua in the project root.
coding_agent.run(opts) — run the loop directly from Lua (facade over compile_loop).
local coding = require
local res = coding.
-- res fields:
-- ok boolean
-- artifact_path string absolute path of the target file
-- iters int
-- summary string "PASS in N iters" or "give-up: <reason>"
-- failure_reason string? "llm_call"|"open_target_file"|"stagnation"|"max_iters"
-- last_error string? last runner stderr (trimmed to 800 chars) on failure
--
-- NOTE: "code" and "history" fields are no longer returned (removed in this release).
coding_agent.register_tool(opts) — register the compile_loop tool with the host
tool registry so a parent LLM can invoke it via tool.call. Returns the registered tool name.
local coding = require
-- Register once (typically at agent startup)
coding.
-- The parent LLM can now call the "compile_loop" tool with:
-- { spec = "...", target_file = "/abs/path/to/file.lua", lang = "lua" }
-- The tool response JSON contains: ok, artifact_path, iters, summary,
-- failure_reason?, last_error? (code and history are excluded).
Built-in runner_kind values (resolved in the coding_agent facade; compile_loop itself
accepts only a runner function):
runner_kind |
Behaviour |
|---|---|
"lua" |
Runs lua <file> and passes on exit 0 + ALL_PASS in stdout |
"cargo" |
Runs cargo test --offline in the file's directory; passes on "test result: ok" |
| function | Called as runner(file_path) — must return { ok, stdout, stderr, exit_code } |
lshape (Vendored package — require("lshape"))
lshape is vendored under blocks/lshape/ so scripts can use schema validation
and LuaCATS generation without external installation.
local lshape = require
local T = lshape.
local User = T.
local ok, why = lshape..
assert
log.*
log.info/warn/error/debug(msg)
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.