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
| field | type | description |
|---|---|---|
| bone.version | string | bone version, e.g. "2.2.4" |
| bone.cwd | string | working directory |
| bone.config_dir | string | ~/.bone-rust |
| bone.agent_depth | number | 0 at top level, >0 inside a sub-agent |
| bone.headless | bool | true 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,
})
| field | required | description |
|---|---|---|
| name | yes | unique tool id |
| description | yes | shown to the LLM for selection |
| parameters | yes | JSON Schema object describing arguments |
| safety | no | "read_only" auto-approves; "danger" prompts (default) |
| display | no | TUI pane config — { show, show_result, args, template, eager } |
| stateful | no | batch consecutive calls; thread prior state back in |
| execute | yes | function(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 field | effect |
|---|---|
| display | text to show |
| submit = false | display 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.
| event | payload | can 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_turn | ctx has full history | yes — 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.
| api | signature | description |
|---|---|---|
| ctx.log.* | (msg) | debug / info / warn / error → bone.log |
| ctx.fs.exists / is_file / is_dir | (path) → bool | synchronous 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) → string | read entire file (raises on error) |
| ctx.write_file | (path, content) → true | write 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?) → table | calls 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) → true | push a live pane update (tools) |
| ctx.ui.width | () → number | terminal column count (0 if unknown) |
| ctx.ui.key | () → KeyEvent | block 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 | () → defs | all 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 | () → snapshots | JSON array of all running/finished jobs |
| ctx.state.get / set / clear | (key[, value]) | process-wide shared KV store |
| ctx.config.get | (section, key) → value | read config/<section>.yaml |
| ctx.config.get_table | (section) → table | whole 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? } | nil | the active conversation |
| ctx.session.list | ({ limit? }) → convos | past conversations from the DB |
| ctx.session.messages | (id, { limit? }) → messages | messages for a conversation |
| ctx.conversation.history | () → messages | in-memory current transcript |
| ctx.usage.snapshot | () → table | token/cost stats for this session |
| ctx.db.query | (sql, params?) → rows | read-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