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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
|
-- subagents/run.lua and subagents/spawn.lua: what a child is built out of, and
-- what the model is told afterwards.
--
-- Every case installs a fresh fake host, so `handle.spawns` holds exactly the
-- children this case produced: one record per child agent, carrying the
-- resolve_model arguments, the store directory, the system messages it was
-- seeded with, the tool declarations it was given and the run_async options.
-- Profile sets are built inline rather than discovered: precedence, not
-- discovery, is what these cases are about, and an explicit set also keeps the
-- machine's real ~/.config out of the run.
local fake = require("spec.fake_ext")
local run = require("subagents.run")
local spawn = require("subagents.spawn")
local function has(text, needle)
assert(type(text) == "string", "expected a string result, got " .. type(text))
assert(text:find(needle, 1, true), "expected to find " .. needle .. " in:\n" .. tostring(text))
end
local function profile_set()
local reviewer = {
name = "reviewer",
description = "Reviews changes",
model = "anthropic:sonnet",
reasoning = "high",
body = "You are a reviewer.\n",
}
local scout = { name = "scout", description = "", body = "" }
return {
list = { reviewer, scout },
by_name = { reviewer = reviewer, scout = scout },
warnings = {},
}
end
-- with_host(fn, opts): install the fake, run fn(handle, profiles), restore.
local function with_host(fn, opts)
local handle = fake.install(opts)
local ok, err = pcall(fn, handle, profile_set())
handle.restore()
if not ok then
error(err, 0)
end
end
-- The child conversation a resumed reviewer loads: the manifest on its profile
-- system message, and the model/reasoning it last ran with on its last turn.
local function stored_reviewer()
return {
{ role = "system", text = spawn.CHILD_ROLE },
{
role = "system",
text = "You are a reviewer.\n",
metadata = { subagents = { owner = "0198-primary", agent = "reviewer" } },
},
{
role = "user",
text = "the first turn",
metadata = { subagents = { model = "openai:gpt-5.6", reasoning = "xhigh" } },
},
{ role = "assistant", text = "first answer" },
}
end
return {
{ "neither agent nor id is refused before anything is spawned", function()
with_host(function(handle, profiles)
local text = run.handle({ prompt = "do the thing" }, profiles)
has(text, "Error:")
has(text, "exactly one of `agent`")
assert(#handle.spawns == 0, "nothing may be spawned by a rejected call")
assert(not text:find("id:", 1, true), "a pre-allocation failure has no id")
end)
end },
{ "both agent and id is refused", function()
with_host(function(handle, profiles)
local text = run.handle({ agent = "reviewer", id = "0198-x", prompt = "go" }, profiles)
has(text, "Error:")
has(text, "not both")
assert(#handle.spawns == 0)
end)
end },
{ "an empty or missing prompt is refused", function()
with_host(function(handle, profiles)
has(run.handle({ agent = "reviewer", prompt = "" }, profiles), "`prompt` must be a non-empty string")
has(run.handle({ agent = "reviewer", prompt = " " }, profiles), "`prompt` must be a non-empty string")
has(run.handle({ agent = "reviewer" }, profiles), "`prompt` must be a non-empty string")
assert(#handle.spawns == 0)
end)
end },
{ "an unknown agent names the known profiles", function()
with_host(function(handle, profiles)
local text = run.handle({ agent = "ghost", prompt = "go" }, profiles)
has(text, "unknown agent 'ghost'")
has(text, "reviewer, scout")
assert(#handle.spawns == 0)
end)
end },
{ "a new child is seeded with the child-role and profile system messages", function()
with_host(function(handle, profiles)
run.handle({ agent = "reviewer", prompt = "Review the auth change." }, profiles)
assert(#handle.spawns == 1, "expected exactly one child")
local child = handle.spawns[1]
assert(child.store_dir == handle.session.session_dir .. "/subagents/" .. handle.session.session_id,
"wrong child store dir: " .. tostring(child.store_dir))
assert(child.session_id == nil, "a new child must not name a session")
assert(child.prompt == "Review the auth change.", tostring(child.prompt))
local messages = child.system_messages
assert(type(messages) == "table" and #messages == 2, "expected role + profile messages, saw " .. #messages)
assert(messages[1].text == spawn.CHILD_ROLE, "the first message is the fixed child role")
assert(messages[1].metadata == nil, "the role message carries no manifest")
assert(messages[2].text == "You are a reviewer.\n", "the profile body is the second message")
local manifest = messages[2].metadata.subagents
assert(manifest.owner == handle.session.session_id, tostring(manifest.owner))
assert(manifest.agent == "reviewer", tostring(manifest.agent))
end)
end },
{ "the primary's system context comes first, then the role, then the profile", function()
with_host(function(handle, profiles)
run.handle({ agent = "reviewer", prompt = "go" }, profiles)
local messages = handle.spawns[1].system_messages
assert(#messages == 4, "expected two copied messages plus role and profile, saw " .. #messages)
assert(messages[1].text == "Project context.", tostring(messages[1].text))
assert(messages[2].text == "House style.", tostring(messages[2].text))
assert(messages[3].text == spawn.CHILD_ROLE, "the child role follows the primary's context")
assert(messages[4].text == "You are a reviewer.\n", "the profile body is last")
assert(messages[1].metadata == nil, "copied context carries no manifest")
end, {
primary_messages = {
{ role = "system", text = "Project context." },
{ role = "user", text = "the parent dialogue is never copied" },
{ role = "assistant", text = "nor this" },
{ role = "system", text = "House style." },
},
})
end },
{ "the child store directory is created before the store is opened", function()
with_host(function(handle, profiles)
run.handle({ agent = "reviewer", prompt = "go" }, profiles)
local dir = handle.session.session_dir .. "/subagents/" .. handle.session.session_id
assert(handle.made_dir(dir), "the child catalog is created, not assumed: " ..
table.concat(handle.mkdirs, ", "))
assert(handle.stores[1] == dir, "the store opens on that directory: " .. tostring(handle.stores[1]))
end)
end },
{ "a child inherits the primary's tools except the subagents ones", function()
with_host(function(handle, profiles)
run.handle({ agent = "reviewer", prompt = "go" }, profiles)
local child = handle.spawns[1]
assert(table.concat(child.tools, ",") == "bash,read_file",
"expected only the non-subagents tools, saw " .. table.concat(child.tools, ","))
for _, decl in ipairs(child.tool_decls) do
assert(decl._source == fake.SOURCE, "the re-registration tag must be passed through untouched")
end
end)
end },
{ "the resolved model and reasoning are recorded on the turn", function()
with_host(function(handle, profiles)
run.handle({ agent = "reviewer", prompt = "go" }, profiles)
local metadata = handle.spawns[1].run.metadata
assert(type(metadata) == "table" and type(metadata.subagents) == "table",
"every child turn records what it ran on")
assert(metadata.subagents.model == "anthropic:sonnet", tostring(metadata.subagents.model))
assert(metadata.subagents.reasoning == "high", tostring(metadata.subagents.reasoning))
assert(handle.spawns[1].run.dispatch_tools ~= false, "an ordinary child dispatches its tools")
end)
end },
{ "the child-role instruction states the contract the design fixes", function()
local role = spawn.CHILD_ROLE
has(role, "You are a subagent")
has(role, "self-contained report")
has(role, "verbatim")
has(role, "cannot ask the user questions")
end },
{ "model and reasoning resolve tool over profile over inherited", function()
with_host(function(handle, profiles)
run.handle({ agent = "reviewer", prompt = "a" }, profiles)
assert(handle.spawns[1].model == "anthropic:sonnet", "the profile model applies")
assert(handle.spawns[1].reasoning == "high", "the profile reasoning applies")
run.handle({ agent = "reviewer", prompt = "b", model = "openai:gpt-5.6", reasoning = "xhigh" }, profiles)
assert(handle.spawns[2].model == "openai:gpt-5.6", "the call overrides the profile")
assert(handle.spawns[2].reasoning == "xhigh", "the call overrides the profile")
run.handle({ agent = "reviewer", prompt = "d", reasoning = "low" }, profiles)
assert(handle.spawns[3].model == "anthropic:sonnet", "model and reasoning resolve independently")
assert(handle.spawns[3].reasoning == "low")
end)
end },
{ "a profile that names neither inherits the primary's pair", function()
with_host(function(handle, profiles)
run.handle({ agent = "scout", prompt = "look around" }, profiles)
assert(handle.spawns[1].model == "openai:gpt-5.6", tostring(handle.spawns[1].model))
assert(handle.spawns[1].reasoning == "low", tostring(handle.spawns[1].reasoning))
end, { session = { model = "openai:gpt-5.6", reasoning = "low" } })
end },
{ "a profile with an empty body contributes no system message", function()
with_host(function(handle, profiles)
run.handle({ agent = "scout", prompt = "look around" }, profiles)
local messages = handle.spawns[1].system_messages
assert(#messages == 1, "expected only the child-role message, saw " .. #messages)
assert(messages[1].text == spawn.CHILD_ROLE)
end)
end },
{ "a resume loads the stored conversation and seeds nothing", function()
with_host(function(handle, profiles)
handle.add_session("0198-child", stored_reviewer())
handle.queue({ output = "second turn" })
local text = run.handle({ id = "0198-child", prompt = "now the tests" }, profiles)
local child = handle.spawns[1]
assert(child.session_id == "0198-child", tostring(child.session_id))
assert(child.resumed, "the agent is built on the resolved session")
assert(#child.system_messages == 0, "the stored conversation is canonical on resume")
assert(child.store_dir == handle.session.session_dir .. "/subagents/" .. handle.session.session_id)
has(text, "id: 0198-child")
has(text, "second turn")
end)
end },
{ "a resumed child keeps the model and reasoning of its last turn", function()
with_host(function(handle, profiles)
handle.add_session("0198-child", stored_reviewer())
handle.queue({})
run.handle({ id = "0198-child", prompt = "carry on" }, profiles)
assert(handle.spawns[1].model == "openai:gpt-5.6", tostring(handle.spawns[1].model))
assert(handle.spawns[1].reasoning == "xhigh", tostring(handle.spawns[1].reasoning))
end)
end },
{ "a per-turn override beats the stored default and only where it is given", function()
with_host(function(handle, profiles)
handle.add_session("0198-child", stored_reviewer())
handle.queue({})
run.handle({ id = "0198-child", prompt = "carry on", reasoning = "high" }, profiles)
assert(handle.spawns[1].reasoning == "high", "the call overrides the stored default")
assert(handle.spawns[1].model == "openai:gpt-5.6", "the model keeps its last effective value")
end)
end },
{ "a resumed child takes its agent name from the manifest", function()
with_host(function(handle, profiles)
handle.add_session("0198-child", stored_reviewer())
handle.queue({ output = "done" })
local text = run.handle({ id = "0198-child", prompt = "carry on" }, profiles)
has(text, "agent: reviewer")
end)
end },
{ "an id this session never started is refused before anything is built", function()
with_host(function(handle, profiles)
local text = run.handle({ id = "0198-nope", prompt = "carry on" }, profiles)
has(text, "Error:")
has(text, "unknown subagent id '0198-nope'")
assert(#handle.spawns == 0, "no child is built for an id that does not resolve")
assert(#handle.runs == 0, "and no turn is started")
end)
end },
{ "a completed result renders every field", function()
with_host(function(handle, profiles)
handle.queue({ id = "0198-abc", status = "completed", output = "Found two issues." })
local text = run.handle({ agent = "reviewer", prompt = "review" }, profiles)
assert(text == table.concat({
"id: 0198-abc",
"agent: reviewer",
"status: completed",
"resumable: true",
"--- output ---",
"Found two issues.",
}, "\n"), "unexpected block:\n" .. text)
end)
end },
{ "a failed first turn reports the error and is not resumable", function()
with_host(function(handle, profiles)
handle.queue({ id = "0198-def", status = "failed", error = "provider refused", resumable = false })
local text = run.handle({ agent = "reviewer", prompt = "review" }, profiles)
has(text, "status: failed")
has(text, "resumable: false")
has(text, "provider refused")
end)
end },
{ "resumability is read back from the store after the turn settles", function()
with_host(function(handle, profiles)
handle.queue({ id = "0198-ghi", output = "wrote something" })
has(run.handle({ agent = "reviewer", prompt = "review" }, profiles), "resumable: true")
assert(handle.sessions["0198-ghi"], "a durable child leaves a session behind")
handle.queue({ id = "0198-jkl", output = "died young", resumable = false })
has(run.handle({ agent = "reviewer", prompt = "review" }, profiles), "resumable: false")
assert(handle.sessions["0198-jkl"] == nil, "no file, no continuation")
end)
end },
{ "a cancelled child settles as cancelled", function()
with_host(function(handle, profiles)
handle.queue({ id = "0198-ghi", status = "cancelled", error = "cancelled by the user" })
local text = run.handle({ agent = "reviewer", prompt = "review" }, profiles)
has(text, "status: cancelled")
has(text, "cancelled by the user")
end)
end },
{ "a model the host cannot resolve is reported and starts nothing", function()
with_host(function(handle, profiles)
local text = run.handle({ agent = "reviewer", prompt = "review", model = "openai:ghost" }, profiles)
assert(text == "Error: resolve_model: unknown model 'openai:ghost'", text)
assert(#handle.resolves == 1, "the host decides what a model reference means, not the rock")
assert(handle.resolves[1].model == "openai:ghost", tostring(handle.resolves[1].model))
assert(#handle.spawns == 0, "the child is never built")
assert(#handle.runs == 0, "a rejected child is never started")
end, { unknown_models = { ["openai:ghost"] = true } })
end },
}
|