summaryrefslogtreecommitdiff
path: root/warming/warm.lua
blob: c8b3af3195ce5df6125e2356afda9028af30e0de (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
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
-- One transient warming request: the session's current prompt, a minimal
-- extra user message, one model response -- and none of it kept.
--
-- The request runs on a throwaway agent so the live session is never
-- touched. The pieces:
--
--   transcript  `panto.file_system_jsonl_store` over the session's own
--               store dir, then `load` -- the same reconstruction `/resume`
--               uses, so system blocks, tool inputs, and thinking
--               signatures come back verbatim and the request shares the
--               session's prompt prefix byte for byte.
--   config      `panto.ext.resolve_model` for the session's current
--               model/reasoning label, which inherits the live credentialed
--               transport when it resolves the session's own model.
--   tools       the session agent's declarations, copied with `set_tools`
--               so the tools array matches the session's own requests (see
--               the README's fidelity notes on schema re-encoding).
--   store       `panto.null_store()` -- the warm turn's user message and
--               response are appended to the throwaway conversation and
--               persisted nowhere.
--   turn        callback-form `agent:run_async(request, callback)`, started
--               by the timer. Progress is ignored; only termination is cached.
--
-- Nothing here raises: the binding reports failures by raising, and every
-- call goes through `try` so callers get one `nil, message` shape.

local M = {}

local function try(fn, ...)
    local ok, value = pcall(fn, ...)
    if not ok then
        return false, tostring(value)
    end
    return true, value
end

-- start(prompt) -> handle | nil, err
--
-- Everything that can fail before the turn starts fails here: a caller
-- either gets a handle to inspect or a message. The callback retains the
-- throwaway Lua owners until termination, and never retains event payloads.
function M.start(prompt)
    local panto = require("panto")
    local ext = panto.ext

    local ok, info = try(ext.session_info)
    if not ok then
        return nil, "session_info: " .. tostring(info)
    end
    if type(info) ~= "table" or type(info.session_dir) ~= "string" or type(info.session_id) ~= "string" then
        return nil, "no session information available"
    end

    local opened, store = try(panto.file_system_jsonl_store, { dir = info.session_dir })
    if not opened then
        return nil, "session store: " .. tostring(store)
    end
    if store == nil then
        return nil, "the session store could not be opened"
    end

    local loaded, conv = try(store.load, store, info.session_id)
    if not loaded then
        return nil, "session store: " .. tostring(conv)
    end
    if conv == nil then
        return nil, "the session transcript could not be loaded"
    end

    local asked, cfg, resolve_err = pcall(ext.resolve_model, {
        model = info.model,
        reasoning = info.reasoning,
    })
    if not asked then
        return nil, tostring(cfg)
    end
    if cfg == nil then
        return nil, resolve_err and tostring(resolve_err) or "the session model could not be resolved"
    end

    local made, no_store = try(panto.null_store)
    if not made then
        return nil, tostring(no_store)
    end

    local built, agent = try(panto.agent, {
        config = cfg,
        store = no_store,
        conversation = conv,
    })
    if not built then
        return nil, "warm agent: " .. tostring(agent)
    end
    if agent == nil then
        return nil, "the warm agent could not be created"
    end

    local primary = ext.agent
    if primary ~= nil then
        local got, decls = try(primary.tools, primary)
        if not got then
            return nil, "tools: " .. tostring(decls)
        end
        if type(decls) == "table" and #decls > 0 then
            local armed, tools_err = try(agent.set_tools, agent, decls)
            if not armed then
                return nil, "warm tools: " .. tostring(tools_err)
            end
        end
    end

    local handle = { agent = agent, store = store, conv = conv }
    local function on_delivery(delivery)
        if handle.abandoned or handle.result or type(delivery) ~= "table" or delivery.kind ~= "termination" then
            return
        end
        local status = delivery.status
        local err = type(delivery.error) == "table" and delivery.error.message or nil
        handle.result = { status = status, err = err }
    end

    local started, stream = try(agent.run_async, agent, {
        prompt = prompt,
        dispatch_tools = false,
    }, on_delivery)
    if not started then
        return nil, "warm turn: " .. tostring(stream)
    end
    if stream == nil then
        return nil, "the warm turn could not be started"
    end
    handle.stream = stream
    return handle
end

-- poll(handle) returns only the cached terminal state. It never calls into
-- the native stream; callbacks own terminal observation and discard events.
function M.poll(handle)
    return handle.result
end

-- Abandon without joining. close() suppresses future callbacks and requests
-- cancellation; the binding manager retains native dependencies until worker
-- retirement. The caller keeps this handle through state teardown.
function M.abandon(handle)
    if handle.abandoned then
        return
    end
    handle.abandoned = true
    handle.result = nil
    if handle.stream then
        pcall(handle.stream.close, handle.stream)
    end
end

return M