okami.mom/albedo

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

dawnloop: pair every tool_use with its tool_result at the request boundaryc6501f5

main
3.6 KiB122 linesraw
1//// A request pairs every tool_use with its tool_result: a late result moves
2//// back beside its call, a call no result answers gets `unpaired_result`,
3//// and a result whose call the fold dropped stays out of the request. Turn
4//// starts recover pending calls before any request is built, so no e2e
5//// request reaches the builder with the inputs that broke pairing here; the
6//// fold damage this repairs is only visible at the request boundary.
7
8import albedo/daemon/message_content
9import albedo/daemon/tool_results
10import albedo/openai_api/replay
11import albedo/openai_api/types
12import gleam/json
13import gleam/list
14import gleam/option.{None}
15import gleam/string
16import gleeunit/should
17
18fn calls(ids: List(String)) -> types.Input {
19  let assert Ok(item) =
20    json.parse(
21      json.to_string(replay.message(
22        json.string(""),
23        "",
24        None,
25        list.map(ids, fn(id) { types.ToolCall(id, "python", "{}") }),
26      )),
27      types.replay_decoder(types.ChatCompletions),
28    )
29  types.Replay(item)
30}
31
32fn note(text: String) -> types.Input {
33  types.User(text)
34}
35
36/// Each input as what a request sees it as: the calls a provider item makes,
37/// a tool result's id, or a plain row.
38fn shape(inputs: List(types.Input)) -> List(#(String, String)) {
39  list.map(inputs, fn(input) {
40    case input {
41      types.ToolOutput(id, _, _) -> #("result", id)
42      types.Replay(_) -> {
43        let ids =
44          message_content.calls(input)
45          |> list.map(fn(call) { call.id })
46          |> string.join(",")
47        #("calls", ids)
48      }
49      types.User(text) -> #("user", text)
50      types.UserImage(text, _) -> #("user", text)
51      types.Assistant(text) -> #("assistant", text)
52    }
53  })
54}
55
56pub fn a_call_nothing_answers_gets_a_result_test() -> Nil {
57  let inputs = [calls(["toolu_a"]), note("a note saved while the call ran")]
58  shape(tool_results.pair(inputs))
59  |> should.equal([
60    #("calls", "toolu_a"),
61    #("result", "toolu_a"),
62    #("user", "a note saved while the call ran"),
63  ])
64  let assert [_, types.ToolOutput(_, text, []), _] = tool_results.pair(inputs)
65  text |> should.equal(tool_results.unpaired_result)
66}
67
68pub fn a_result_separated_from_its_call_moves_beside_it_test() -> Nil {
69  let inputs = [
70    calls(["toolu_a"]),
71    note("a note saved while the call ran"),
72    types.ToolOutput("toolu_a", "ran", []),
73  ]
74  shape(tool_results.pair(inputs))
75  |> should.equal([
76    #("calls", "toolu_a"),
77    #("result", "toolu_a"),
78    #("user", "a note saved while the call ran"),
79  ])
80}
81
82pub fn a_result_the_fold_dropped_leaves_its_call_a_placeholder_test() -> Nil {
83  let inputs = [
84    calls(["toolu_a", "toolu_b"]),
85    types.ToolOutput("toolu_a", "ran", []),
86    note("later"),
87  ]
88  shape(tool_results.pair(inputs))
89  |> should.equal([
90    #("calls", "toolu_a,toolu_b"),
91    #("result", "toolu_a"),
92    #("result", "toolu_b"),
93    #("user", "later"),
94  ])
95}
96
97pub fn a_result_whose_call_the_fold_dropped_stays_out_test() -> Nil {
98  let inputs = [
99    note("the call was replaced by a summary"),
100    types.ToolOutput("toolu_gone", "ran", []),
101  ]
102  shape(tool_results.pair(inputs))
103  |> should.equal([#("user", "the call was replaced by a summary")])
104}
105
106pub fn pairing_is_idempotent_test() -> Nil {
107  let inputs = [
108    calls(["toolu_a", "toolu_b"]),
109    note("between"),
110    types.ToolOutput("toolu_b", "ran", []),
111    types.ToolOutput("toolu_b", "ran twice", []),
112  ]
113  let paired = tool_results.pair(inputs)
114  tool_results.pair(paired) |> should.equal(paired)
115  shape(paired)
116  |> should.equal([
117    #("calls", "toolu_a,toolu_b"),
118    #("result", "toolu_b"),
119    #("result", "toolu_a"),
120    #("user", "between"),
121  ])
122}