summaryrefslogtreecommitdiff
path: root/subagents/run.lua
blob: 7fbed67871d419487132eea6676dc67f849af48f (plain)
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
-- The `subagents.run` tool: start one child agent, or continue one, and wait.
--
-- One call handles all cases. `agent` starts a new child from that profile;
-- `system_prompt` starts one without a saved profile; `id` continues a child
-- this primary session started earlier. Exactly one is required, and `prompt`
-- is always required — a child cannot see the parent dialogue, so the prompt
-- is the only task context it gets.
--
-- The call blocks until the child settles. Parallelism is ordinary tool
-- batching: several subagents.run calls emitted in one batch run concurrently
-- under the session-wide bound in subagents/jobs.lua, and one failure does not
-- disturb its siblings.
--
-- Failures are values, not exceptions. Everything the model could plausibly
-- have caused — a missing prompt, both selectors at once, an unknown profile,
-- an unresumable id, a child that errored or was cancelled — comes back as
-- readable text. Validation that fails before a child is allocated has
-- no id to report, and a new child that dies before its first assistant
-- message has no durable file, so it reports `resumable: false` rather than
-- promising a continuation that would not resolve.
--
-- The result block is plain `key: value` lines rather than JSON: it is read by
-- a model, and the field names match the design's result shape (id, agent,
-- status, resumable, then the output or the error message).

local jobs = require("subagents.jobs")
local spawn = require("subagents.spawn")

local M = {}

-- A resumed child has no profile in hand; its identity comes back from the
-- manifest metadata stored on its first profile system message.
local function manifest_agent(result)
    local manifest = result.manifest
    if type(manifest) ~= "table" then
        return nil
    end
    local mine = manifest.subagents
    if type(mine) ~= "table" or type(mine.agent) ~= "string" then
        return nil
    end
    return mine.agent
end

-- format_result(result, agent_name) -> the model-visible block.
function M.format_result(result, agent_name)
    local body = result.output
    if body == nil or body == "" then
        body = result.error or ""
    end
    return table.concat({
        "id: " .. (result.id or "(none)"),
        "agent: " .. (agent_name or manifest_agent(result) or "?"),
        "status: " .. (result.status or "unknown"),
        "resumable: " .. tostring(result.resumable == true),
        "--- output ---",
        tostring(body),
    }, "\n")
end

function M.handle(input, profiles)
    local spec, err = spawn.build_spec(input, profiles)
    if not spec then
        return "Error: " .. tostring(err)
    end

    local handle, spawn_err = spawn.spawn(spec)
    if not handle then
        return "Error: " .. tostring(spawn_err)
    end

    local results = jobs.await({ handle }, "all")
    local result = results and results[1]
    if type(result) ~= "table" then
        return "Error: the subagent produced no result."
    end

    return M.format_result(result, spec.label)
end

return M