summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--spec/test_luatool.lua34
-rw-r--r--spec/test_progress.lua30
-rw-r--r--spec/test_progress_replay.lua2
-rw-r--r--subagents/progress.lua53
-rw-r--r--subagents/spawn.lua4
-rw-r--r--subagents/workflow.lua3
6 files changed, 117 insertions, 9 deletions
diff --git a/spec/test_luatool.lua b/spec/test_luatool.lua
index 509b804..e7f2294 100644
--- a/spec/test_luatool.lua
+++ b/spec/test_luatool.lua
@@ -4,6 +4,7 @@
local fake = require("spec.fake_ext")
local jobs = require("subagents.jobs")
local luatool = require("subagents.luatool")
+local progress = require("subagents.progress")
local workflow = require("subagents.workflow")
local uv = require("luv")
@@ -115,6 +116,39 @@ return {
end)
end },
+ { "a background workflow keeps its completed tool entry pinned", function()
+ with_host(function(handle, profiles)
+ progress.reset()
+ progress.begin_live_turn()
+ local pins
+ progress.claim({
+ id = "pinned-workflow-call",
+ tool_name = "subagents.lua",
+ set_component = function()
+ pins = {}
+ return {
+ alive = function() return true end,
+ invalidate = function() end,
+ set_pinned = function(_, value) pins[#pins + 1] = value end,
+ }
+ end,
+ })
+ handle.queue_for("alpha", { output = "finished" })
+ local id = luatool.handle({ source = [[
+ return subagents.workflow(function(ctx)
+ return ctx:agent{name="work", agent="alpha", prompt="go"}:await().output
+ end)
+ ]] }, profiles, { tool_call_id = "pinned-workflow-call" })
+ progress.settle({ id = "pinned-workflow-call" })
+ assert(#pins == 1 and pins[1] == true, "the outer result must not unpin the running workflow")
+
+ local record = pump(id)
+ assert(record.status == "completed", tostring(record.error))
+ assert(#pins == 2 and pins[2] == false, "workflow completion restores transcript order")
+ progress.reset()
+ end)
+ end },
+
{ "a later Lua call inspects workflows and records are read-only", function()
with_host(function(_, profiles)
local id = luatool.handle({ source = [[
diff --git a/spec/test_progress.lua b/spec/test_progress.lua
index 5788998..76cd289 100644
--- a/spec/test_progress.lua
+++ b/spec/test_progress.lua
@@ -76,6 +76,36 @@ return {
assert(pins.b[2] == false, "reset safely releases unresolved boards")
end },
+ { "background workflows keep their board pinned after the outer tool settles", function()
+ progress.reset()
+ progress.begin_live_turn()
+ local component, pins
+ progress.claim({
+ id = "background-call",
+ tool_name = "subagents.lua",
+ set_component = function(_, value)
+ component, pins = value, {}
+ return {
+ alive = function() return true end,
+ invalidate = function() end,
+ set_pinned = function(_, value) pins[#pins + 1] = value end,
+ }
+ end,
+ })
+
+ progress.bind({ tool_call_id = "background-call" })
+ progress.card("worker", "child")
+ progress.workflow_started("background-call")
+ progress.settle({ id = "background-call" })
+ progress.reset()
+ assert(#pins == 1 and pins[1] == true, "turn boundaries must not unpin a running workflow")
+ assert(component:render(80)[#component:render(80)] == "", "a running workflow retains its spacing")
+
+ progress.workflow_finished("background-call")
+ assert(#pins == 2 and pins[2] == false, "the board unpins when its final workflow settles")
+ progress.reset()
+ end },
+
{ "a pinned board leaves one blank line before the waiting indicator", function()
progress.reset()
local component, pins
diff --git a/spec/test_progress_replay.lua b/spec/test_progress_replay.lua
index ffe7e5c..ca87657 100644
--- a/spec/test_progress_replay.lua
+++ b/spec/test_progress_replay.lua
@@ -94,7 +94,7 @@ return {
local text = plain(component:render(120))
has(text, "✔ reviewer child-direct")
has(text, "completed")
- has(text, "↳ openai:test")
+ has(text, "↳ openai:test (high)")
has(text, "prompt: inspect the change")
has(text, "checking")
has(text, "std.read [tool-1]")
diff --git a/subagents/progress.lua b/subagents/progress.lua
index d88e239..b39fe00 100644
--- a/subagents/progress.lua
+++ b/subagents/progress.lua
@@ -32,6 +32,7 @@ local GLYPHS = {
local M = {}
local boards = {}
+local background_workflows = {}
local bound = setmetatable({}, { __mode = "k" })
local tools_collapsed = true
local sid_width = 8
@@ -284,6 +285,11 @@ local function set_board_pinned(board, pinned)
end
end
+local function sync_board_pin(board)
+ local workflows = type(board.key) == "string" and background_workflows[board.key] or 0
+ set_board_pinned(board, board.tool_active or (workflows or 0) > 0)
+end
+
local function prune_boards()
for key, board in pairs(boards) do
if board.handle and board.handle.alive then
@@ -293,8 +299,16 @@ local function prune_boards()
end
end
-local function new_board()
- local board = { cards = {}, handle = nil, collapsed = tools_collapsed, next_sequence = 0, pinned = false }
+local function new_board(key)
+ local board = {
+ key = key,
+ cards = {},
+ handle = nil,
+ collapsed = tools_collapsed,
+ next_sequence = 0,
+ pinned = false,
+ tool_active = false,
+ }
board.component = { render = function(_, width) return render(board, width) end }
return board
end
@@ -305,7 +319,8 @@ function M.claim(event)
if not PROGRESS_TOOLS[name] then return end
if type(event.set_component) ~= "function" then return end
if type(event.collapsed) == "boolean" then tools_collapsed = event.collapsed end
- local key, board = event.id or event.tool_call_id or name, new_board()
+ local key = event.id or event.tool_call_id or name
+ local board = new_board(key)
local attached, handle = pcall(event.set_component, event, board.component)
if not attached then return end
board.handle = handle
@@ -317,20 +332,41 @@ function M.claim(event)
if replaying then
replay_board(board, type(key) == "string" and key or nil)
else
- set_board_pinned(board, true)
+ board.tool_active = true
end
+ sync_board_pin(board)
end
-- A result settles the outer tool invocation. Keep the board attached to its
--- transcript entry, but return it to that entry's original ordering.
+-- transcript entry, but leave it pinned while a background workflow it started
+-- is still running.
function M.settle(event)
local key = type(event) == "table" and (event.id or event.tool_call_id) or nil
local board = key and boards[key]
- if board then set_board_pinned(board, false) end
+ if board then
+ board.tool_active = false
+ sync_board_pin(board)
+ end
local co = coroutine.running()
if co and bound[co] == key then bound[co] = nil end
end
+function M.workflow_started(tool_call_id)
+ if type(tool_call_id) ~= "string" or tool_call_id == "" then return end
+ background_workflows[tool_call_id] = (background_workflows[tool_call_id] or 0) + 1
+ local board = boards[tool_call_id]
+ if board then sync_board_pin(board) end
+end
+
+function M.workflow_finished(tool_call_id)
+ if type(tool_call_id) ~= "string" or tool_call_id == "" then return end
+ local count = background_workflows[tool_call_id]
+ if count == nil then return end
+ if count <= 1 then background_workflows[tool_call_id] = nil else background_workflows[tool_call_id] = count - 1 end
+ local board = boards[tool_call_id]
+ if board then sync_board_pin(board) end
+end
+
function M.collapse(event)
if type(event.collapsed) ~= "boolean" then return end
prune_boards()
@@ -510,6 +546,8 @@ local function replay_turn(board, conv, messages, start, finish, info, agent, sy
local id = nonempty(info and info.id)
local model = nonempty(metadata and metadata.model) or nonempty(info and info.model)
+ local reasoning = nonempty(metadata and metadata.reasoning) or nonempty(info and info.reasoning)
+ if model and reasoning then model = model .. " (" .. reasoning .. ")" end
local card = new_card(board, agent or "subagent", id, model, {
prompt = prompt,
system_prompt = system_prompt,
@@ -727,7 +765,8 @@ end
function M.reset()
for _, board in pairs(boards) do
- set_board_pinned(board, false)
+ board.tool_active = false
+ sync_board_pin(board)
end
prune_boards()
-- `bound` has weak coroutine keys. Foreground handlers clear themselves in
diff --git a/subagents/spawn.lua b/subagents/spawn.lua
index 9117fdd..d90631d 100644
--- a/subagents/spawn.lua
+++ b/subagents/spawn.lua
@@ -434,7 +434,9 @@ function M.spawn(spec)
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, {
+ local card_detail = model_label
+ if card_detail and reasoning_label then card_detail = card_detail .. " (" .. reasoning_label .. ")" end
+ local card = progress.card(spec.label or "subagent", id, card_detail, {
prompt = spec.prompt,
system_prompt = spec.presentation_system_prompt,
})
diff --git a/subagents/workflow.lua b/subagents/workflow.lua
index 97dd858..671890e 100644
--- a/subagents/workflow.lua
+++ b/subagents/workflow.lua
@@ -767,6 +767,7 @@ local function finish_workflow(record, status, result, err)
record.error = err
record.coroutine = nil
active_workflows[record.id] = nil
+ progress.workflow_finished(record.tool_call_id)
notify(record)
end
@@ -785,6 +786,7 @@ function M.start(wf, opts)
workflow_sequence = workflow_sequence + 1
local id = "workflow-" .. workflow_sequence
local record = make_workflow_record(id)
+ record.tool_call_id = type(opts.tool_call_id) == "string" and opts.tool_call_id ~= "" and opts.tool_call_id or nil
workflow_records[id] = record
active_workflows[id] = record
@@ -823,6 +825,7 @@ function M.start(wf, opts)
finish_workflow(record, record.cancel_requested and "cancelled" or "failed", nil, tostring(err))
end
end)
+ progress.workflow_started(record.tool_call_id)
return id
end