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
157
158
159
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
|