lua

Embeds Lua 5.4. Drop init.lua in ~/.bone-rust/ to run code at startup. Tools, commands, and sub-agents are registered with the bone global.

the bone global

fieldtypedescription
bone.versionstringbone version, e.g. "2.2.4"
bone.cwdstringworking directory
bone.config_dirstring~/.bone-rust
bone.agent_depthnumber0 at top level, >0 inside a sub-agent
bone.headlessbooltrue when no TUI is attached
bone.log.info("msg")    -- also: .warn / .error / .debug
bone.register_tool({...})
bone.register_command("name", function(args, ctx) ... end)
bone.register_subagent({...})
bone.on("event", function(event, ctx) ... end)

bone.api.emit("custom_event", { foo = 1 })  -- fire custom events
cjson.encode(table); cjson.decode(string)

register a tool

bone.register_tool({
    name = "word_count",
    description = "Count words in a file",
    parameters = {
        type = "object",
        properties = {
            path = { type = "string", description = "File path" },
        },
        required = { "path" },
    },
    safety = "read_only",          -- "read_only" or "danger" (default)
    execute = function(params, ctx)
        local content = ctx.read_file(params.path)
        local n = select(2, content:gsub("%S+", ""))
        return params.path .. ": " .. n .. " words"
    end,
})
fieldrequireddescription
nameyesunique tool id
descriptionyesshown to the LLM for selection
parametersyesJSON Schema object describing arguments
safetyno"read_only" auto-approves; "danger" prompts (default)
displaynoTUI pane config — { show, show_result, args, template, eager }
statefulnobatch consecutive calls; thread prior state back in
executeyesfunction(params, ctx) → string | table | nil

A tool execute returns a plain string, nil, or a table with { content, state, pane, images } for structured output.

register a command

bone.register_command("greet", {
    description = "Greet someone",
    handler = function(args, ctx)
        -- args: string of text typed after the command
        -- ctx:  the context table (see below)
        return "hello, " .. (args or "world")
    end,
})

Handler receives (args, ctx). Return a string (submitted as a user message), or a table:

return fieldeffect
displaytext to show
submit = falsedisplay only, don't submit to the agent
display_role = "assistant"render output as markdown assistant text
action = "conversation.replace"replace the transcript — sibling messages = { ... }
action = "conversation.load"load a past conversation — sibling messages, optional conversation_id
action = "config.apply" / "config.reload_tools" / "config.switch_provider"config actions; the last needs sibling provider = "id"

register a sub-agent

bone.register_subagent({
    name = "researcher",
    description = "Searches the web and summarizes findings",
    system_prompt = "You are a researcher. Be concise.",
    provider = "openai",        -- optional, inherits parent
    model = "gpt-4o",           -- optional, inherits parent
    approval = "auto",          -- optional: auto | approve | reject
})

Then dispatch from a tool or command via ctx.agent.run / ctx.agent.spawn.

events & hooks

bone.on(event, function(event, ctx) ... end) registers a handler. tool_call and before_turn can return an action table to alter behavior.

eventpayloadcan block?
session_start / session_end{}no
message{}no
turn_start / turn_end{}no
token_usage{}no
tool_call{ name, call_id, arguments, safety }yes — return { block = true, reason = "..." }
tool_result{ name, call_id, is_error }no
before_turnctx has full historyyes — return a LuaReturnAction (below)

before_turn runs before each LLM request. Return a table to: conversation_replace (swap transcript), system_prompt_append (inject text), or tool_filter (allowlist tool names for this turn). Used by auto-compaction and custom prompt injection. Any custom string also works as an event name — fire it with bone.api.emit.

the ctx table

Passed to every tool execute and command handler as the second argument. Scalars: ctx.config_dir, ctx.cwd, ctx.call_id.

apisignaturedescription
ctx.log.*(msg)debug / info / warn / error → bone.log
ctx.fs.exists / is_file / is_dir(path) → boolsynchronous path checks
ctx.fs.read_dir(path) → entries{ name, path, kind } per entry
ctx.fs.metadata(path) → table{ path, kind, len, readonly }
ctx.read_file(path) → stringread entire file (raises on error)
ctx.write_file(path, content) → truewrite a new file (rejects if exists)
ctx.shell(cmd, opts?) → { stdout, stderr, exit_code }run via bash, through approval gate
ctx.shell_streaming(cmd, cb, opts?) → tablecalls cb(line) per stdout line
ctx.ui.notify(msg, level?)transient frontend notification
ctx.ui.notice(msg)persistent notice in transcript
ctx.ui.status(msg)live status line (e.g. progress)
ctx.ui.pane(table) → truepush a live pane update (tools)
ctx.ui.width() → numberterminal column count (0 if unknown)
ctx.ui.key() → KeyEventblock for one keypress (tools)
ctx.tools.call(name, args, opts?) → { ok, name, call_id, content, is_error }invoke another tool (depth ≤ 4)
ctx.tools.definitions() → defsall registered tool definitions
ctx.agent.run(prompt, opts?) → { ok, content, error }blocking sub-agent (see below)
ctx.agent.spawn(prompt, opts?) → { ok, id, error }non-blocking background job
ctx.agent.wait(ids?, opts?) → { ok, jobs, pending, timed_out, cancelled }block until jobs finish
ctx.agent.cancel(id) → { ok }cancel a running job
ctx.agent.jobs() → snapshotsJSON array of all running/finished jobs
ctx.state.get / set / clear(key[, value])process-wide shared KV store
ctx.config.get(section, key) → valueread config/<section>.yaml
ctx.config.get_table(section) → tablewhole YAML section as a Lua table
ctx.config.set_value(ns, key, value)persist a config value
ctx.config.list_providers() → providers{ id, label, model, base_url, ... }
ctx.session.current() → { id, provider?, model? } | nilthe active conversation
ctx.session.list({ limit? }) → convospast conversations from the DB
ctx.session.messages(id, { limit? }) → messagesmessages for a conversation
ctx.conversation.history() → messagesin-memory current transcript
ctx.usage.snapshot() → tabletoken/cost stats for this session
ctx.db.query(sql, params?) → rowsread-only SQL on the session DB

Sub-agent options (ctx.agent.run / spawn): provider, model, system_prompt, approval, tools (allowlist), timeout_ms, max_tokens, wall_timeout_ms. spawn also takes agent (a registered name), title, max_concurrency.

plugins

bone.plugin.load("my_plugin")      -- ~/.bone-rust/lua/plugins/my_plugin/init.lua
bone.plugin.install("user/repo")   -- git clone from GitHub
bone.plugin.list()                 -- { [name] = { has_init = bool } }
bone.plugin.remove("my_plugin")
bone.plugin.update("my_plugin")    -- git pull