Skip to content

Syntax Reference

agents/my-agent/agent.lua
local nudges = require("ghost.nudges") -- optional
local template = require("ghost.template")
return {
-- Required
name = "my-agent",
description = "What this agent does",
-- Model settings (all optional)
model = nil, -- nil = default, or "fast", "strong"
reasoning_effort = nil, -- nil, "low", "medium", "high"
max_iterations = 30, -- max tool loop iterations
-- Tools and skills
tools = { "web_search", "web_fetch", "file_read", "todo" },
skills = { "note-writer" },
-- Hooks (all optional except build)
build = function(ctx, args) ... end, -- required
pre_turn = nudges.compose(...), -- before each turn
on_end_turn = nudges.progress_gate(...), -- gate final response
post_completion = function(ctx) end, -- after agent finishes
}

Agents declare which tools they can use via the tools list:

ToolDescription
web_searchSearch the web
web_fetchFetch and extract web page content
file_readRead files from the workspace
file_writeWrite files to the workspace
file_editEdit files in place
shellExecute shell commands
knowledge_searchSearch the knowledge base
note_writeCreate knowledge notes
todoManage a TODO checklist
agentStart, stop, and check agents

If an agent declares skills, file_read is automatically added.

Define agent-specific tools directly in agent.lua:

custom_tools = {
{
name = "my_tool",
description = "What the tool does",
parameters = {
{ name = "input", type = "string",
description = "The input", required = true },
},
terminal = false, -- if true, result ends the session
handler = function(ctx, args)
return "tool result"
end,
},
},
HookWhenReturn
buildBefore first turn{ system_prompt, messages }
pre_turnBefore each model callNudge message or nil
on_end_turnAfter model’s final responseRejection message or nil
post_completionAfter agent finishes— (may call ctx:spawn_agent)

Required. Returns the system prompt and initial messages:

build = function(ctx, args)
return {
system_prompt = template.render(read_file("prompt.md"), {
date = os.date("%Y-%m-%d"),
}),
messages = {
{ role = "user",
content = args.prompt or "Begin." },
},
}
end,

The args table comes from the caller — for dispatch agents it contains { prompt = "..." }, for spawned agents it contains whatever the parent passed to ctx:spawn_agent().

Runs after the agent finishes. Can perform cleanup or spawn child agents:

post_completion = function(ctx)
ctx:curate_web_cache()
end,

For spawning child agents with structured data, prefer terminal custom tool handlers over post_completion — this lets the agent decide what data to pass:

custom_tools = {
{
name = "report_findings",
description = "Submit your final report",
parameters = {
{ name = "report", type = "string", required = true },
{ name = "sources", type = "string", required = true },
},
terminal = true,
handler = function(ctx, args)
ctx:spawn_agent("deep-research-reflection", {
report = args.report,
sources = args.sources,
})
return "Reflection spawned."
end,
},
},

The nudge library provides composable functions for pre_turn and on_end_turn hooks.

Combines multiple nudge functions into one. Each function is called in order; non-nil results are collected and wrapped in a <system-reminder> block.

Injects the current TODO list into the agent’s context.

Count down remaining iterations:

nudges.iteration_countdown({
{ remaining = 10,
message = "{remaining} iterations left. Prioritize." },
{ remaining = 5,
message = "Only {remaining} left. Stop new work." },
{ remaining = 2,
message = "FINAL: {remaining} left. Write your report." },
})

Fire after a wall-clock duration with escalating messages:

nudges.temporal({
after_seconds = 300,
messages = {
"Working for {minutes} minutes. Start wrapping up.",
"STOP new work. Write your report NOW.",
},
})

Fire when context window usage exceeds a threshold:

nudges.context_pressure({
threshold_pct = 0.80,
message = "Context over 80% full. Finish with what you have.",
})

Block end-of-turn until TODO items are completed:

nudges.progress_gate({
no_todo = "REJECTED — create a TODO plan first.",
incomplete = "REJECTED — {incomplete} items remain.",
})

Nudge when a tool hasn’t been called enough:

nudges.tool_count({
tool = "web_fetch",
min = 7,
nudge = "Need {min} {tool} calls (have {count}).",
})

Nudge when a tool hasn’t been used recently:

nudges.recency({
tool = "web_fetch",
window = 3,
message = "You haven't fetched any pages recently.",
})

Write a focused system prompt using {{variable}} interpolation:

# Agent Name — Purpose
You are in autonomous mode. Today is {{date}}.
**A text-only response (no tool calls) ends your session.**
## Workflow
1. First step
2. Second step
3. Handoff (text-only final message)