Spaces:
Running on Zero
Running on Zero
Download main.lua from jeorgexyz/lua-agent-lab: direct link, hf CLI and curl.
- Browser
- Download file 9.81 kB
-
https://huggingface.co/spaces/jeorgexyz/lua-agent-lab/resolve/main/main.lua
- Command line
-
hf download hf://spaces/jeorgexyz/lua-agent-lab/main.lua
-
curl -L -o main.lua https://huggingface.co/spaces/jeorgexyz/lua-agent-lab/resolve/main/main.lua
9.81 kB
| -- main.lua - CLI entry point. | |
| -- | |
| -- lua54 main.lua "<task>" [options] | |
| -- | |
| -- Options: | |
| -- --backend local|ollama|http|replay default local | |
| -- --llama-path <dir> lua-llama checkout, default ../lua_llama | |
| -- --checkpoint <path> default <llama-path>/stories15M.bin | |
| -- --tokenizer <path> default <llama-path>/tokenizer.bin | |
| -- --no-constrain free decoding; the 0% arm of the ablation | |
| -- --tools a,b,c default calc,read_file | |
| -- --mcp "<command>" mount an MCP server's tools over stdio | |
| -- --mcp-url <url> ...or over streamable HTTP (persistent) | |
| -- --mcp-prefix <str> namespace them, default "mcp_" | |
| -- --mcp-trust let MCP tools run without approval | |
| -- --max-steps <n> default 6 | |
| -- --max-tokens <n> context budget; defaults to the model's window | |
| -- --policy <name> drop_oldest|elide_observations|summarize | |
| -- --full-prompt verbose tool catalogue (needs a big window) | |
| -- --temperature <t> default 0.0 | |
| -- --trace <path> record a JSONL trace | |
| -- --replay <path> play a recorded trace back | |
| -- --model <name> http/ollama model name | |
| -- --ollama-host <url> default http://localhost:11434 | |
| -- --ollama-free turn OFF Ollama's schema (free decoding) | |
| -- --yes auto-approve gated tools | |
| -- --quiet suppress per-step output | |
| package.path = "./?.lua;" .. package.path | |
| local Agent = require('agent') | |
| local tools = require('tools') | |
| local protocol = require('protocol') | |
| local FLAGS = { | |
| ["--no-constrain"] = true, ["--yes"] = true, ["--quiet"] = true, | |
| ["--full-prompt"] = true, ["--help"] = true, ["-h"] = true, | |
| ["--ollama-free"] = true, ["--mcp-trust"] = true, | |
| } | |
| local function parse_args(argv) | |
| local o = { task = nil } | |
| local i = 1 | |
| while argv[i] do | |
| local a = argv[i] | |
| if a:sub(1, 2) == "--" or a == "-h" then | |
| local key = a:gsub("^%-%-?", ""):gsub("%-", "_") | |
| if FLAGS[a] then | |
| o[key] = true | |
| i = i + 1 | |
| else | |
| local v = argv[i + 1] | |
| if v == nil then | |
| io.stderr:write("missing value for " .. a .. "\n") | |
| os.exit(2) | |
| end | |
| o[key] = v | |
| i = i + 2 | |
| end | |
| else | |
| o.task = o.task and (o.task .. " " .. a) or a | |
| i = i + 1 | |
| end | |
| end | |
| return o | |
| end | |
| local function build_tools(spec) | |
| local reg = tools.registry() | |
| for name in (spec or "calc,read_file"):gmatch("[^,]+") do | |
| name = name:match("^%s*(.-)%s*$") | |
| local t = tools[name] | |
| if not t then | |
| io.stderr:write("unknown tool: " .. name .. "\n") | |
| os.exit(2) | |
| end | |
| reg:add(t) | |
| end | |
| return reg | |
| end | |
| local function build_backend(o, reg) | |
| if o.replay then | |
| return require('backend.replay').new({ path = o.replay }) | |
| elseif o.backend == "http" then | |
| return require('backend.http').new({ | |
| model = o.model, | |
| temperature = tonumber(o.temperature) or 0, | |
| }) | |
| elseif o.backend == "ollama" then | |
| return require('backend.ollama').new({ | |
| model = o.model, | |
| host = o.ollama_host, | |
| tools = reg, | |
| -- Schema-constrained unless --ollama-free; see backend/ollama.lua. | |
| format = not o.ollama_free, | |
| temperature = tonumber(o.temperature) or 0, | |
| }) | |
| else | |
| return require('backend.local').new({ | |
| llama_path = o.llama_path, | |
| checkpoint = o.checkpoint, | |
| tokenizer = o.tokenizer, | |
| tools = reg, | |
| constrain = not o.no_constrain, | |
| temperature = tonumber(o.temperature) or 0.0, | |
| }) | |
| end | |
| end | |
| -- The approval gate. Read-only tools never reach here; write and exec tools | |
| -- always do. Defaults to no -- an agent that writes to disk because the | |
| -- prompt was ambiguous is the failure this exists to prevent. | |
| local function make_approver(auto) | |
| if auto then | |
| return function(call) | |
| io.write(" [auto-approved: " .. call.tool .. "]\n") | |
| return true | |
| end | |
| end | |
| return function(call) | |
| io.write(string.format("\n APPROVE %s %s? [y/N] ", call.tool, | |
| require('trace').encode(call.args))) | |
| io.flush() | |
| local line = io.read("l") | |
| return line ~= nil and line:lower():sub(1, 1) == "y" | |
| end | |
| end | |
| local function main(argv) | |
| local o = parse_args(argv) | |
| if o.help or o.h or not o.task then | |
| local src = io.open(arg[0] or "main.lua", "r") | |
| if src then | |
| for line in src:lines() do | |
| if line:sub(1, 2) ~= "--" then break end | |
| io.write(line:sub(4) .. "\n") | |
| end | |
| src:close() | |
| end | |
| os.exit(o.task and 0 or 2) | |
| end | |
| local reg = build_tools(o.tools) | |
| -- An MCP server's tools join the registry alongside the built-ins and | |
| -- are indistinguishable to the loop from there: same validation, same | |
| -- approval gate, same grammar. Mounted BEFORE the backend is built, | |
| -- because the grammar and the Ollama schema are compiled from the | |
| -- registry and have to include them. | |
| if o.mcp or o.mcp_url then | |
| local mcp = require('mcp') | |
| local client = mcp.new({ command = o.mcp, url = o.mcp_url }) | |
| local ok, err = pcall(function() | |
| mcp.mount(reg, client, { | |
| prefix = o.mcp_prefix or "mcp_", | |
| trust = o.mcp_trust, | |
| }) | |
| end) | |
| if not ok then | |
| io.stderr:write("MCP: " .. tostring(err) .. "\n") | |
| os.exit(2) | |
| end | |
| io.write(string.format("mcp: %s (%s) -- %d tools over %s\n", | |
| client.server_info and client.server_info.name or "?", | |
| client.negotiated_version or "?", #(client.tools or {}), | |
| o.mcp_url and "http (session)" or "stdio (respawn per call)")) | |
| end | |
| local backend = build_backend(o, reg) | |
| -- Token counting needs a tokenizer, but NOT a model. The local backend | |
| -- already has one loaded; every other backend loads the 433KB | |
| -- tokenizer.bin on its own rather than falling back to guesswork. | |
| local bpe = require('bpe') | |
| local tokenizer = backend.tok | |
| local approximate, tok_note = false, nil | |
| if not tokenizer then | |
| local path = o.tokenizer | |
| or ((o.llama_path or "../lua_llama") .. "/tokenizer.bin") | |
| local loaded, err = bpe.load(path, o.llama_path) | |
| if loaded then | |
| tokenizer = loaded | |
| else | |
| approximate = true | |
| tokenizer = bpe.approximate() | |
| tok_note = err | |
| end | |
| end | |
| -- The context budget belongs to the MODEL, not the tokenizer. This used | |
| -- to key off "do we have a real tokenizer", which happened to be true | |
| -- only for the local backend -- so once the tokenizer could load on its | |
| -- own, a replay run silently inherited stories15M's 256-token window. | |
| local default_budget = 4096 | |
| if backend.cfg and backend.cfg.seq_len then | |
| default_budget = math.floor(backend.cfg.seq_len * 0.75) | |
| end | |
| -- The verbose catalogue costs 194 tokens for two tools; stories15M's | |
| -- whole context is 256. Compact by default, opt out with --full-prompt. | |
| local compact = not o.full_prompt | |
| local real_render = protocol.render_system | |
| if compact then | |
| protocol.render_system = function(t, task) return real_render(t, task, true) end | |
| end | |
| local agent = Agent.new({ | |
| backend = backend, | |
| tools = reg, | |
| tokenizer = tokenizer, | |
| max_steps = tonumber(o.max_steps) or 6, | |
| max_tokens = tonumber(o.max_tokens) or default_budget, | |
| policy = o.policy, | |
| approve = make_approver(o.yes), | |
| trace_path = o.trace, | |
| verbose = not o.quiet, | |
| }) | |
| io.write(string.format("task: %s\nbackend: %s%s\ntools: %s\n", | |
| o.task, | |
| o.replay and ("replay " .. o.replay) or (o.backend or "local"), | |
| (not o.replay and (o.backend == nil or o.backend == "local")) | |
| and (o.no_constrain and " (free decoding)" or " (grammar-constrained)") or "", | |
| table.concat(reg:names(), ", "))) | |
| if approximate then | |
| -- Say what it is and how to fix it, in one line. This shows up in | |
| -- every transcript, so it has to be informative without reading as | |
| -- a defect. | |
| io.write(" token counts estimated -- pass --tokenizer <tokenizer.bin> for exact\n") | |
| if o.verbose_tokenizer then | |
| io.write(" (" .. tostring(tok_note) .. ")\n") | |
| end | |
| end | |
| local started = os.clock() | |
| local answer, reason, steps = agent:run(o.task) | |
| local elapsed = os.clock() - started | |
| io.write("\n" .. string.rep("-", 60) .. "\n") | |
| if answer then | |
| io.write("ANSWER: " .. answer .. "\n") | |
| else | |
| io.write("No answer. Stopped because: " .. reason .. "\n") | |
| end | |
| local parsed = 0 | |
| for _, s in ipairs(steps) do | |
| if s.call then parsed = parsed + 1 end | |
| end | |
| io.write(string.format("%d steps, %d/%d turns parsed as a tool call, %.1fs\n", | |
| #steps, parsed, #steps, elapsed)) | |
| if o.trace then io.write("trace: " .. o.trace .. "\n") end | |
| os.exit(answer and 0 or 1) | |
| end | |
| main(arg) | |