summaryrefslogtreecommitdiff
path: root/init.lua
diff options
context:
space:
mode:
Diffstat (limited to 'init.lua')
-rw-r--r--init.lua213
1 files changed, 213 insertions, 0 deletions
diff --git a/init.lua b/init.lua
new file mode 100644
index 0000000..5f28308
--- /dev/null
+++ b/init.lua
@@ -0,0 +1,213 @@
+-- panto-subagents: the `subagents` extension entry point.
+--
+-- Activation happens once per panto session (a new session, `/new`, or
+-- `/resume` rebuilds the whole Lua state and runs this again). It discovers
+-- the agent profiles once, registers the four model-facing tools, and
+-- registers a `/workflow:<name>` command for every valid TOML workflow.
+--
+-- Discovery order is deterministic and side-effect free apart from
+-- registration, because pantograph evaluates every candidate extension file
+-- but only activates the ones its allow/deny policy permits.
+--
+-- The discovered profile set is captured here and threaded into the run and
+-- models handlers, so both tools describe and resolve exactly the profiles
+-- named in the tool description the model was shown. Profile warnings (a
+-- foreign `model` spelling, a duplicate name in one layer) have no logging
+-- channel in an extension, so a bounded number of them ride along at the end
+-- of the run description — the one place the user and the model both see the
+-- profile list.
+--
+-- Activation also subscribes to the turn lifecycle: an interrupted turn asks
+-- every live child to stop, and the end of a turn joins them and drops the
+-- progress cards, which are presentation state that must not survive the turn
+-- that produced it.
+--
+-- If the host predates the model-resolution seam, activation fails loudly
+-- instead of registering tools that cannot work.
+
+local jobs = require("subagents.jobs")
+local luatool = require("subagents.luatool")
+local models = require("subagents.models")
+local progress = require("subagents.progress")
+local run = require("subagents.run")
+local spawn = require("subagents.spawn")
+local toml_workflows = require("subagents.toml_workflows")
+
+local MAX_SHOWN_WARNINGS = 5
+
+local function host()
+ return require("panto").ext
+end
+
+-- Events are optional: a host without the bus, or a print-mode session with no
+-- components, simply never calls back and every child still runs.
+local function subscribe(ext, name, handler)
+ if type(ext.on) == "function" then
+ pcall(ext.on, name, handler)
+ end
+end
+
+local function run_description(profiles)
+ local lines = {
+ "Delegate a task to a subagent and wait for its report. Start a new child from an agent profile, or continue a child you started earlier in this session.",
+ "",
+ "Agent profiles:",
+ }
+ if #profiles.list == 0 then
+ lines[#lines + 1] = " (none found; define them in .panto/agents/*.md)"
+ else
+ for _, profile in ipairs(profiles.list) do
+ if profile.description ~= "" then
+ lines[#lines + 1] = string.format(" %s — %s", profile.name, profile.description)
+ else
+ lines[#lines + 1] = " " .. profile.name
+ end
+ end
+ end
+
+ lines[#lines + 1] = ""
+ lines[#lines + 1] = "Rules:"
+ lines[#lines + 1] = "- Pass exactly one of `agent` (start a new child from that profile) or `id` (continue a child from an earlier result). `prompt` is always required and must be non-empty."
+ lines[#lines + 1] = "- Omit `model` and `reasoning` normally; a child inherits yours. Call subagents.models before choosing an unfamiliar model or reasoning level."
+ lines[#lines + 1] = "- A child shares your workspace and tools but not your conversation, and cannot ask the user questions. Put every piece of task context it needs into `prompt`."
+ lines[#lines + 1] = "- To delegate in parallel, emit several subagents.run calls in one tool batch; they run concurrently and one failure does not discard the others."
+ lines[#lines + 1] = "- Every started child reports an id. Pass that id back to continue the same conversation."
+
+ if #profiles.warnings > 0 then
+ lines[#lines + 1] = ""
+ lines[#lines + 1] = "Profile warnings:"
+ for index, warning in ipairs(profiles.warnings) do
+ if index > MAX_SHOWN_WARNINGS then
+ lines[#lines + 1] = string.format(" (%d more)", #profiles.warnings - MAX_SHOWN_WARNINGS)
+ break
+ end
+ lines[#lines + 1] = " " .. warning
+ end
+ end
+
+ return table.concat(lines, "\n")
+end
+
+local function activate()
+ local ext = host()
+ if type(ext.resolve_model) ~= "function" or type(require("panto").agent) ~= "function" then
+ error("panto-subagents: this pantograph is too old for subagents (panto.ext.resolve_model is missing)")
+ end
+
+ -- Discover through spawn so the workflow lanes, which resolve profiles
+ -- lazily, share the exact set this tool description advertises.
+ local profiles = spawn.profiles()
+
+ -- Escape reaches the children through the turn, not through a tool: the
+ -- primary is parked inside a tool call when they are running.
+ subscribe(ext, "turn_interrupt", function()
+ jobs.cancel_all()
+ end)
+ subscribe(ext, "turn_end", function()
+ jobs.close_all()
+ progress.reset()
+ end)
+ -- The entry for a delegation call is where that call's children render.
+ subscribe(ext, "tool_call_complete", function(event)
+ progress.claim(event)
+ end)
+
+ ext.register_tool {
+ name = "subagents.run",
+ description = run_description(profiles),
+ schema = {
+ type = "object",
+ properties = {
+ agent = { type = "string", description = "Profile name for a new child. Mutually exclusive with `id`." },
+ id = { type = "string", description = "Id of a child started earlier in this session, to continue it. Mutually exclusive with `agent`." },
+ prompt = { type = "string", description = "The complete task for the child. It sees none of this conversation." },
+ model = { type = "string", description = "Optional `provider:model` override. Omit to inherit." },
+ reasoning = { type = "string", description = "Optional reasoning level override. Omit to inherit." },
+ },
+ required = { "prompt" },
+ },
+ handler = function(input, context)
+ progress.bind(context)
+ return run.handle(input, profiles)
+ end,
+ }
+
+ ext.register_tool {
+ name = "subagents.models",
+ description = "Query the configured model catalog: no arguments for the inherited model plus provider counts, `model` for an exact lookup including valid reasoning levels, `provider`/`query` for a bounded search, or `agent` for what a profile will actually run on.",
+ schema = {
+ type = "object",
+ properties = {
+ provider = { type = "string", description = "Restrict a search to one provider." },
+ query = { type = "string", description = "Substring to search model names for." },
+ limit = { type = "integer", description = "Maximum matches to return (1-50, default 10).", minimum = 1, maximum = 50 },
+ model = { type = "string", description = "Exact `provider:model` to look up." },
+ agent = { type = "string", description = "Agent profile whose effective model to report." },
+ },
+ },
+ handler = function(input)
+ return models.handle(input, profiles)
+ end,
+ }
+
+ ext.register_tool {
+ name = "subagents.lua",
+ description = "Run a one-off Lua workflow that fans several subagents out and combines their results. `source` must return subagents.workflow(function(ctx, input) ... end) and runs in a restricted environment with no filesystem, process, or module access.",
+ schema = {
+ type = "object",
+ properties = {
+ prompt = { type = "string", description = "The workflow input, passed to the callback as its second argument." },
+ source = { type = "string", description = "Lua source returning subagents.workflow(function(ctx, input) ... end)." },
+ },
+ required = { "prompt", "source" },
+ },
+ handler = function(input, context)
+ progress.bind(context)
+ return luatool.handle(input, profiles)
+ end,
+ }
+
+ ext.register_tool {
+ name = "subagents.workflow",
+ description = "Run a fixed dependency graph of subagents: `name` runs a discovered TOML workflow, or `steps` defines one inline. Every step receives `prompt` as the workflow input, and a step with `needs` also receives those steps' outputs.",
+ schema = {
+ type = "object",
+ properties = {
+ name = { type = "string", description = "Name of a discovered workflow. Mutually exclusive with `steps`." },
+ prompt = { type = "string", description = "The workflow input, given to every step." },
+ steps = {
+ type = "array",
+ description = "Inline workflow definition. Mutually exclusive with `name`.",
+ items = {
+ type = "object",
+ properties = {
+ id = { type = "string", description = "Unique step id." },
+ agent = { type = "string", description = "Agent profile to run this step." },
+ prompt = { type = "string", description = "Step instruction." },
+ model = { type = "string", description = "Optional `provider:model` override." },
+ reasoning = { type = "string", description = "Optional reasoning level override." },
+ needs = {
+ type = "array",
+ description = "Ids of steps whose output this step receives.",
+ items = { type = "string" },
+ },
+ },
+ required = { "id", "agent", "prompt" },
+ },
+ },
+ },
+ required = { "prompt" },
+ },
+ handler = function(input, context)
+ progress.bind(context)
+ return toml_workflows.handle(input, profiles)
+ end,
+ }
+
+ toml_workflows.discover_and_register(profiles)
+end
+
+return {
+ name = "subagents",
+ activate = activate,
+}