diff options
| author | t <t@tjp.lol> | 2026-08-18 14:18:14 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-08-18 14:20:28 -0600 |
| commit | cc69a2e431779528578211a5cb3385bb23e076bf (patch) | |
| tree | 8833171e25bb3dfb71fc4a2e0b8fe583782da764 /subagents/spawn.lua | |
| parent | 372ef8ff40991644ec2654c61328f31779f4ad21 (diff) | |
Persist and replay subagent progress across sessions
Record durable child-turn metadata, restore bounded progress cards during
startup replay, and keep settled cards attached to transcript entries. Add
workflow-local Lua profiles and expose discovered agents and workflows in the
session header.
Diffstat (limited to 'subagents/spawn.lua')
| -rw-r--r-- | subagents/spawn.lua | 59 |
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 |
