1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
|
-- 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 clears the
-- coroutine bindings. Settled progress cards remain attached to their
-- transcript entries until the host destroys those components.
--
-- 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 DIM, RESET = "\27[2m", "\27[0m"
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 wrap_plain(text, width)
width = math.max(1, width or 1)
local lines, from = {}, 1
while #text - from + 1 > width do
local window = text:sub(from, from + width - 1)
local cut = window:match("^.*() ") or width
lines[#lines + 1] = text:sub(from, from + cut - 1):gsub("%s+$", "")
from = from + cut
while from <= #text and text:sub(from, from) == " " do
from = from + 1
end
end
lines[#lines + 1] = text:sub(from)
return lines
end
local function install_header(ext, profiles, workflows)
local profile_names, workflow_names = {}, {}
for _, profile in ipairs(profiles.list) do
profile_names[#profile_names + 1] = profile.name
end
for _, workflow in ipairs(workflows.list) do
if workflow.definition then workflow_names[#workflow_names + 1] = workflow.name end
end
table.sort(profile_names)
table.sort(workflow_names)
subscribe(ext, "session_start", function(event)
local inner = event:get_component()
event:set_component({
render = function(_, width)
local lines = inner:render(width)
local extras = {}
for _, inventory in ipairs({
{ "subagents", profile_names },
{ "workflows", workflow_names },
}) do
local value = #inventory[2] == 0 and "(none)" or table.concat(inventory[2], ", ")
for _, line in ipairs(wrap_plain(" " .. inventory[1] .. ": " .. value, width)) do
extras[#extras + 1] = DIM .. line .. RESET
end
end
local at = (#lines > 0 and lines[#lines] == "") and #lines or (#lines + 1)
for index = #extras, 1, -1 do
table.insert(lines, at, extras[index])
end
return lines
end,
})
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.
progress.begin_replay()
-- Startup session replay fires tool lifecycle events before the first
-- live turn. After that boundary, progress.claim must stay presentation-
-- only and never rescan the child catalog for ordinary calls.
subscribe(ext, "turn_start", function()
progress.begin_live_turn()
end)
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)
subscribe(ext, "tool_result", function(event)
progress.settle(event)
end)
subscribe(ext, "tool_collapse", function(event)
progress.collapse(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)
local result = run.handle(input, profiles)
progress.settle(context)
return result
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. Optional `agents` define profiles available only to this workflow.",
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)." },
agents = {
type = "array",
description = "Agent profiles available only to this workflow. Inline profiles shadow discovered profiles with the same name.",
items = {
type = "object",
properties = {
name = { type = "string", description = "Profile name used by ctx:agent." },
description = { type = "string", description = "Optional human-readable purpose." },
system_prompt = { type = "string", description = "System prompt for this profile." },
},
required = { "name", "system_prompt" },
},
},
},
required = { "prompt", "source" },
},
handler = function(input, context)
progress.bind(context)
local result = luatool.handle(input, profiles)
progress.settle(context)
return result
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)
local result = toml_workflows.handle(input, profiles)
progress.settle(context)
return result
end,
}
local workflows = toml_workflows.discover_and_register(profiles)
install_header(ext, profiles, workflows)
end
return {
name = "subagents",
activate = activate,
}
|