summaryrefslogtreecommitdiff
path: root/warming/warm.lua
diff options
context:
space:
mode:
Diffstat (limited to 'warming/warm.lua')
-rw-r--r--warming/warm.lua160
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