okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petfeat(usage): time provider calls and show tok/s on the turn signoff0a3eddd

main
9.9 KiB369 linesraw
1//// Worker call timeouts and helper cleanup require acknowledgment races and
2//// process monitors that the external E2E API cannot control.
3
4import albedo/daemon/conversation
5import albedo/daemon/events
6import albedo/daemon/session_run
7import albedo/daemon/turn
8import albedo/daemon/usage
9import albedo/harness/extension
10import albedo/openai_api/types
11import gleam/erlang/process.{type Subject}
12import gleam/list
13import gleam/option.{None}
14import gleam/otp/actor
15import gleeunit/should
16
17fn server(mode: fn(message) -> actor.Next(Nil, message)) -> Subject(message) {
18  let assert Ok(started) =
19    actor.new(Nil)
20    |> actor.on_message(fn(_, message) { mode(message) })
21    |> actor.start
22  started.data
23}
24
25/// A stand-in for the session actor's protocol.
26type Owner {
27  Publish(reply: Subject(Bool))
28  Progress(reply: Subject(Bool))
29  Commit(reply: Subject(Result(#(Int, option.Option(Int)), String)))
30  Commits(reply: Subject(Int))
31  Unused
32}
33
34fn messages() -> session_run.Messages(Owner) {
35  session_run.Messages(
36    publish: fn(_, _, reply) { Publish(reply) },
37    tool_progress_delta: fn(_, _, _, _, _, _, reply) { Progress(reply) },
38    tool_progress_running: fn(_, _, _, _, _, _, reply) { Progress(reply) },
39    tool_progress_reset: fn(_, _, _) { Unused },
40    tool_progress_finish: fn(_, _, _) { Unused },
41    commit: fn(_, _, _, _, reply) { Commit(reply) },
42    fits: fn(_, _, _) { Unused },
43    context: fn(_, _, _, _) { Unused },
44    usage: fn(_, _, _) { Unused },
45    drain: fn(_, _) { Unused },
46    pin: fn(_, _, _) { Unused },
47    sent: fn(_, _, _) { Unused },
48    finished: fn(_, _) { Unused },
49    collect: Unused,
50  )
51}
52
53fn publish(owner: Subject(Owner)) -> Bool {
54  publish_after(owner, turn.latch())
55}
56
57fn publish_after(owner: Subject(Owner), stop: turn.Latch) -> Bool {
58  session_run.publish_fn(owner, "run", messages(), stop, waiting: 20)(
59    events.Checkpoint,
60  )
61}
62
63pub fn publish_passes_the_owner_answer_through_test() -> Nil {
64  let answering = fn(keep_going) {
65    server(fn(message) {
66      case message {
67        Publish(reply) -> process.send(reply, keep_going)
68        _ -> Nil
69      }
70      actor.continue(Nil)
71    })
72  }
73  list.each([True, False], fn(keep_going) {
74    session_run.publish_fn(
75      answering(keep_going),
76      "run",
77      messages(),
78      turn.latch(),
79      waiting: 2000,
80    )(events.Checkpoint)
81    |> should.equal(keep_going)
82  })
83}
84
85pub fn publish_keeps_streaming_through_a_stalled_owner_test() -> Nil {
86  server(fn(_) { actor.continue(Nil) })
87  |> publish
88  |> should.be_true
89}
90
91/// A cancelled session actor that stalls must not let a tool gate open: the
92/// kernel interrupt cannot stop a tool outside the kernel.
93pub fn publish_fails_closed_on_a_stall_once_cancelled_test() -> Nil {
94  let pid = process.spawn(fn() { Nil })
95  let assert turn.Running(run) =
96    turn.Running(turn.Run(
97      "run",
98      pid,
99      process.monitor(pid),
100      False,
101      turn.latch(),
102      turn.Turn(None),
103    ))
104    |> turn.cancel
105  server(fn(_) { actor.continue(Nil) })
106  |> publish_after(run.stop)
107  |> should.be_false
108}
109
110pub fn publish_stops_the_stream_when_the_owner_dies_test() -> Nil {
111  server(fn(_) { actor.stop() })
112  |> publish
113  |> should.be_false
114}
115
116fn commit(owner: Subject(Owner)) -> Result(#(Int, option.Option(Int)), String) {
117  session_run.commit_fn(owner, "run", messages(), waiting: 20)(
118    [],
119    conversation.Tool,
120    None,
121  )
122}
123
124pub fn a_dead_owner_ends_the_turn_instead_of_the_worker_test() -> Nil {
125  let dies = fn() { server(fn(_) { actor.stop() }) }
126  commit(dies())
127  |> should.equal(Error("commit unconfirmed: the session stopped"))
128  session_run.usage_fn(dies(), "run", messages(), waiting: 20)(usage.Metadata(
129    "model",
130    0,
131    None,
132    None,
133    None,
134  ))
135  |> should.equal(Error("usage unconfirmed: the session stopped"))
136  session_run.drain_fn(dies(), "run", messages(), waiting: 20)()
137  |> should.equal(Error("steering unconfirmed: the session stopped"))
138}
139
140/// A stalled commit may still land, so it must be sent exactly once.
141pub fn a_stalled_commit_ends_the_turn_without_a_retry_test() -> Nil {
142  let assert Ok(started) =
143    actor.new(0)
144    |> actor.on_message(fn(commits, message) {
145      case message {
146        Commit(_) -> actor.continue(commits + 1)
147        Commits(reply) -> {
148          process.send(reply, commits)
149          actor.continue(commits)
150        }
151        _ -> actor.continue(commits)
152      }
153    })
154    |> actor.start
155  commit(started.data)
156  |> should.equal(Error(
157    "commit unconfirmed after a session stall; it may still be saved",
158  ))
159  actor.call(started.data, 1000, Commits) |> should.equal(1)
160}
161
162fn cancel(stop: turn.Latch) -> Nil {
163  let pid = process.self()
164  let monitor = process.monitor(pid)
165  let _ =
166    turn.cancel(
167      turn.Running(turn.Run("run", pid, monitor, False, stop, turn.Turn(None))),
168    )
169  process.demonitor_process(monitor)
170}
171
172pub fn progress_timeout_stops_delivery_at_the_first_fragment_test() -> Nil {
173  let assert Ok(started) =
174    actor.new(0)
175    |> actor.on_message(fn(count, message) {
176      case message {
177        Progress(_) -> actor.continue(count + 1)
178        Commits(reply) -> {
179          process.send(reply, count)
180          actor.continue(count)
181        }
182        _ -> actor.continue(count)
183      }
184    })
185    |> actor.start
186  let progress =
187    session_run.tool_progress_delta_fn(
188      started.data,
189      "run",
190      messages(),
191      turn.latch(),
192      waiting: 20,
193    )
194  // This is the transport's continuation rule: a refusal stops delivery.
195  let continued = case progress(0, 1, 0, "python", "first") {
196    True -> progress(0, 1, 0, "python", "second")
197    False -> False
198  }
199  continued |> should.be_false
200  actor.call(started.data, 1000, Commits) |> should.equal(1)
201}
202
203pub fn running_timeout_refuses_continuation_test() -> Nil {
204  let owner = server(fn(_) { actor.continue(Nil) })
205  session_run.tool_progress_running_fn(
206    owner,
207    "run",
208    messages(),
209    turn.latch(),
210    waiting: 20,
211  )(0, 1, 0, "tool", "python")
212  |> should.be_false
213}
214
215pub fn cancelled_progress_sends_no_request_test() -> Nil {
216  let assert Ok(started) =
217    actor.new(0)
218    |> actor.on_message(fn(count, message) {
219      case message {
220        Progress(reply) -> {
221          process.send(reply, True)
222          actor.continue(count + 1)
223        }
224        Commits(reply) -> {
225          process.send(reply, count)
226          actor.continue(count)
227        }
228        _ -> actor.continue(count)
229      }
230    })
231    |> actor.start
232  let owner = started.data
233  let stop = turn.latch()
234  cancel(stop)
235  session_run.tool_progress_delta_fn(
236    owner,
237    "run",
238    messages(),
239    stop,
240    waiting: 20,
241  )(0, 1, 0, "python", "fragment")
242  |> should.be_false
243  session_run.tool_progress_running_fn(
244    owner,
245    "run",
246    messages(),
247    stop,
248    waiting: 20,
249  )(0, 1, 0, "tool", "python")
250  |> should.be_false
251  // The reply follows every request already sent by this process.
252  actor.call(owner, 1000, Commits) |> should.equal(0)
253}
254
255pub fn cancellation_before_progress_acknowledgment_refuses_continuation_test() -> Nil {
256  let stop = turn.latch()
257  let owner =
258    server(fn(message) {
259      case message {
260        Progress(reply) -> {
261          cancel(stop)
262          process.send(reply, True)
263        }
264        _ -> Nil
265      }
266      actor.continue(Nil)
267    })
268  session_run.tool_progress_delta_fn(
269    owner,
270    "run",
271    messages(),
272    stop,
273    waiting: 1000,
274  )(0, 1, 0, "python", "fragment")
275  |> should.be_false
276  let other_stop = turn.latch()
277  let other_owner =
278    server(fn(message) {
279      case message {
280        Progress(reply) -> {
281          cancel(other_stop)
282          process.send(reply, True)
283        }
284        _ -> Nil
285      }
286      actor.continue(Nil)
287    })
288  session_run.tool_progress_running_fn(
289    other_owner,
290    "run",
291    messages(),
292    other_stop,
293    waiting: 1000,
294  )(0, 1, 0, "tool", "python")
295  |> should.be_false
296}
297
298fn idle_provider(started: Subject(process.Pid)) -> extension.Upstream {
299  extension.Upstream(
300    endpoint: "test",
301    protocol: types.Responses,
302    stream: fn(_, _) {
303      let never = process.new_subject()
304      process.send(started, process.self())
305      let _: Nil = process.receive_forever(never)
306      Error(types.Cancelled)
307    },
308    explain: fn(_) { None },
309    account: fn() { None },
310    cache_marks: fn(_) { [] },
311    images: types.any_images,
312  )
313}
314
315fn idle_provider_cleanup(owner_dies: Bool) -> Nil {
316  let owner = server(fn(_) { actor.continue(Nil) })
317  let started = process.new_subject()
318  let finished = process.new_subject()
319  let stop = turn.latch()
320  let upstream = session_run.stoppable(idle_provider(started), owner, stop)
321  let worker =
322    process.spawn_unlinked(fn() {
323      process.send(
324        finished,
325        upstream.stream(
326          types.Request("test", None, [], [], None, types.defaults),
327          fn(_) { types.Continue },
328        ),
329      )
330    })
331  let worker_monitor = process.monitor(worker)
332  let assert Ok(helper) = process.receive(started, 1000)
333  let helper_monitor = process.monitor(helper)
334  case owner_dies {
335    True -> {
336      let assert Ok(owner_pid) = process.subject_owner(owner)
337      process.unlink(owner_pid)
338      process.kill(owner_pid)
339    }
340    False -> cancel(stop)
341  }
342  process.receive(finished, 1000) |> should.equal(Ok(Error(types.Cancelled)))
343  process.new_selector()
344  |> process.select_specific_monitor(helper_monitor, fn(_) { Nil })
345  |> process.selector_receive(1000)
346  |> should.equal(Ok(Nil))
347  process.new_selector()
348  |> process.select_specific_monitor(worker_monitor, fn(_) { Nil })
349  |> process.selector_receive(1000)
350  |> should.equal(Ok(Nil))
351  process.demonitor_process(helper_monitor)
352  process.demonitor_process(worker_monitor)
353  case owner_dies {
354    True -> Nil
355    False -> {
356      let assert Ok(owner_pid) = process.subject_owner(owner)
357      process.unlink(owner_pid)
358      process.kill(owner_pid)
359    }
360  }
361}
362
363pub fn owner_death_cleans_up_an_idle_provider_helper_test() -> Nil {
364  idle_provider_cleanup(True)
365}
366
367pub fn cancellation_cleans_up_an_idle_provider_helper_test() -> Nil {
368  idle_provider_cleanup(False)
369}