diff options
Diffstat (limited to 'warming/warm.lua')
| -rw-r--r-- | warming/warm.lua | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/warming/warm.lua b/warming/warm.lua new file mode 100644 index 0000000..e5e41fc --- /dev/null +++ b/warming/warm.lua @@ -0,0 +1,160 @@ +-- 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 `agent:run_async { prompt, dispatch_tools = false }` on its +-- own pump thread, so the UI thread never blocks. Tools are +-- never dispatched: a misbehaving model cannot run real +-- tools from a keep-alive prompt. The response is drained +-- and dropped. +-- +-- 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 poll or a message. +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 started, job = try(agent.run_async, agent, { + prompt = prompt, + dispatch_tools = false, + }) + if not started then + return nil, "warm turn: " .. tostring(job) + end + if job == nil then + return nil, "the warm turn could not be started" + end + + return { agent = agent, job = job, store = store, conv = conv } +end + +-- poll(handle) -> nil while the pump runs, then a settled +-- { status = "completed"|"failed"|"cancelled", err = ... }. Drains buffered +-- events as it goes so a chatty response cannot grow the queue. The result +-- content is deliberately discarded; `close` joins the (settled) pump. +function M.poll(handle) + local job = handle.job + while true do + local ok, event = pcall(job.next_event, job) + if not ok or event == nil then + break + end + end + local ok, result = pcall(job.result, job) + if not ok then + pcall(job.close, job) + return { status = "failed", err = tostring(result) } + end + if type(result) ~= "table" then + return nil + end + pcall(job.close, job) + return { status = result.status or "failed", err = result.err } +end + +-- abandon(handle): cancel and join the pump, on session teardown. The +-- cancel half shuts the response socket down, so the join returns promptly +-- even mid-request. +function M.abandon(handle) + pcall(handle.job.request_cancel, handle.job) + pcall(handle.job.close, handle.job) +end + +return M |
