summaryrefslogtreecommitdiff
path: root/subagents/spawn.lua
diff options
context:
space:
mode:
Diffstat (limited to 'subagents/spawn.lua')
-rw-r--r--subagents/spawn.lua59
1 files changed, 52 insertions, 7 deletions
diff --git a/subagents/spawn.lua b/subagents/spawn.lua
index b355d9a..caf65aa 100644
--- a/subagents/spawn.lua
+++ b/subagents/spawn.lua
@@ -18,9 +18,13 @@
-- context, then the fixed child-role instruction, then — when the profile has
-- a body — the profile prompt as a further system message. That profile
-- message carries the immutable manifest metadata (owning primary session id +
--- profile name), which is how a resumed child re-identifies itself. The parent
--- dialogue is never copied, which is why the child-role text tells the child
--- its final message is the whole of what the delegator sees.
+-- profile name); workflow-local profiles also carry an inline marker so the
+-- progress replay path can expose only prompts the caller explicitly supplied.
+-- The per-turn user metadata records the effective model/reasoning, the
+-- per-outer-call card sequence, terminal presentation status, and — when the
+-- spawn happened inside a bound extension tool — that outer tool call id.
+-- The parent dialogue is never copied, which is why the child-role text tells
+-- the child its final message is the whole of what the delegator sees.
--
-- Edge cases: a profile with an empty body contributes no system message and
-- therefore no manifest, so a resumed child started from a body-less profile
@@ -206,12 +210,17 @@ function M.build_spec(input, profiles)
local system_messages = { { text = M.CHILD_ROLE } }
if profile.body and profile.body:match("%S") then
+ local manifest = { owner = info_or_err.session_id, agent = profile.name }
+ if profile.layer == "workflow" then manifest.inline = true end
system_messages[#system_messages + 1] = {
text = profile.body,
- metadata = { subagents = { owner = info_or_err.session_id, agent = profile.name } },
+ metadata = { subagents = manifest },
}
end
spec.system_messages = system_messages
+ if profile.layer == "workflow" then
+ spec.presentation_system_prompt = profile.body
+ end
return spec
end
@@ -447,10 +456,31 @@ function M.spawn(spec)
local named, session_id = try(agent.session_id, agent)
local id = (not one_shot) and named and nonempty(session_id) or nil
- local card = progress.card(spec.label or "subagent", id, model_label)
+ local turn_index
+ local got_conv, child_conv = try(agent.conversation, agent)
+ if got_conv and child_conv and type(child_conv.len) == "function" then
+ local got_len, length = try(child_conv.len, child_conv)
+ if got_len and type(length) == "number" then turn_index = length + 1 end
+ end
+
+ local card = progress.card(spec.label or "subagent", id, model_label, {
+ prompt = spec.prompt,
+ system_prompt = spec.presentation_system_prompt,
+ })
+
+ local owner_tool_call_id = progress.tool_call_id()
+ local turn_metadata = {
+ subagents = { model = model_label, reasoning = reasoning_label },
+ }
+ local sequence = card.sequence
+ if type(sequence) == "number" then turn_metadata.subagents.sequence = sequence end
+ if type(owner_tool_call_id) == "string" and owner_tool_call_id ~= "" then
+ turn_metadata.subagents.tool_call_id = owner_tool_call_id
+ end
-- The settled run_async result becomes the result table every caller
-- already reads: run.lua's block and the workflow API's shape_result.
+ local child_handle
local function shape(raw)
raw = type(raw) == "table" and raw or {}
local result = {
@@ -483,16 +513,30 @@ function M.spawn(spec)
local ok, found = try(store.resolve, store, id)
result.resumable = ok and found ~= nil
end
+ turn_metadata.subagents.status = result.status
+ -- `agent:set_message_metadata` is deliberately refused while a pump
+ -- is live. The result has already been copied out, so joining here is
+ -- safe and releases the agent's mutation guard before the annotation.
+ if child_handle and child_handle.job then
+ pcall(child_handle.job.close, child_handle.job)
+ end
+ if turn_index and type(agent.set_message_metadata) == "function" then
+ -- The binding's annotation seam updates the already-written user
+ -- record, so cancellation has a durable presentation status even
+ -- though the stream correctly rolls its assistant messages back.
+ pcall(agent.set_message_metadata, agent, turn_index, turn_metadata)
+ end
card:done(result.status, result.error)
return result
end
- local handle, start_err = jobs.start {
+ local start_err
+ child_handle, start_err = jobs.start {
id = id,
build = function(wake_fd)
local job, err = agent:run_async {
prompt = spec.prompt,
- metadata = { subagents = { model = model_label, reasoning = reasoning_label } },
+ metadata = turn_metadata,
dispatch_tools = not one_shot,
wake_fd = wake_fd,
}
@@ -506,6 +550,7 @@ function M.spawn(spec)
end,
shape = shape,
}
+ local handle = child_handle
if not handle then
card:done("failed", start_err)
return nil, start_err