kcode_k1_codex_shim/
lib.rs1use std::{collections::VecDeque, future::Future, pin::Pin};
7
8pub use kcode_k1_codex_runtime::{
9 Adapter, Config, DynamicTool, Error, ErrorKind, Event, ToolCall, ToolResult, Turn,
10};
11
12pub const ASYNC_TOOL_ACKNOWLEDGEMENT: &str = "The tool was launched asynchronously. Its result will not be available during this turn. Do not wait for or poll this call; continue the turn without its result. The result will be provided in a subsequent turn when it becomes available.";
14
15pub type ToolLaunchFuture<'a> = Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
17
18pub trait ToolCallLauncher<B>: Send {
20 fn launch_stage<'a>(&'a mut self, text: String, boxes: Vec<B>) -> ToolLaunchFuture<'a>;
22}
23
24pub trait BoxCodec {
26 type Box: Clone;
28
29 fn tool_call_box(&mut self, call: &ToolCall) -> Self::Box;
33
34 fn box_text<'a>(&self, box_: &'a Self::Box) -> &'a str;
36}
37
38#[derive(Clone, Debug, PartialEq)]
40pub enum ShimItem<B> {
41 Text(String),
43 Box(B),
45}
46
47#[derive(Clone, Debug, PartialEq)]
49pub struct ShimOutput<B> {
50 pub items: Vec<ShimItem<B>>,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55enum Health {
56 Ready,
57 Unusable,
58}
59
60pub struct Shim<C: BoxCodec> {
64 adapter: Adapter,
65 conversation_key: String,
66 codec: C,
67 launcher: Box<dyn ToolCallLauncher<C::Box>>,
68 health: Health,
69 pending_boxes: VecDeque<C::Box>,
70}
71
72impl<C: BoxCodec> Shim<C> {
73 pub fn new(
75 adapter: Adapter,
76 conversation_key: impl Into<String>,
77 codec: C,
78 launcher: Box<dyn ToolCallLauncher<C::Box>>,
79 ) -> Self {
80 Self {
81 adapter,
82 conversation_key: conversation_key.into(),
83 codec,
84 launcher,
85 health: Health::Ready,
86 pending_boxes: VecDeque::new(),
87 }
88 }
89
90 pub fn record_box(&mut self, box_: C::Box) {
92 self.pending_boxes.push_back(box_);
93 }
94
95 pub fn record_boxes(&mut self, boxes: impl IntoIterator<Item = C::Box>) {
97 self.pending_boxes.extend(boxes);
98 }
99
100 pub fn pending_box_count(&self) -> usize {
102 self.pending_boxes.len()
103 }
104
105 pub async fn close_conversation(&mut self) -> Result<(), Error> {
109 if self.health == Health::Unusable {
110 return Err(self.unusable());
111 }
112 self.adapter
113 .close_conversation(self.conversation_key.clone())
114 .await
115 }
116
117 pub async fn infer(&mut self, input: impl Into<String>) -> Result<ShimOutput<C::Box>, Error> {
122 if self.health == Health::Unusable {
123 return Err(self.unusable());
124 }
125
126 let submitted_box_count = self.pending_boxes.len();
127 let input = append_section(self.render_pending_boxes(), &input.into());
128
129 self.health = Health::Unusable;
130 let mut turn = match self
131 .adapter
132 .start_turn(self.conversation_key.clone(), input)
133 .await
134 {
135 Ok(turn) => turn,
136 Err(error) => {
137 self.health = Health::Ready;
138 return Err(error);
139 }
140 };
141
142 for _ in 0..submitted_box_count {
143 debug_assert!(self.pending_boxes.pop_front().is_some());
144 }
145
146 let mut text = String::new();
147 let mut lookahead = None;
148 loop {
149 let event = match lookahead.take() {
150 Some(event) => Some(event),
151 None => turn.next_event().await,
152 };
153 match event {
154 Some(Event::TextDelta(delta)) => text.push_str(&delta),
155 Some(Event::ToolCall(first)) => {
156 let first_box = self.codec.tool_call_box(&first);
157 let mut calls = vec![first];
158 let mut boxes = vec![first_box];
159 let mut drain_error = None;
160
161 loop {
162 match turn.try_next_event() {
163 Ok(Some(Event::ToolCall(call))) => {
164 let box_ = self.codec.tool_call_box(&call);
165 calls.push(call);
166 boxes.push(box_);
167 }
168 Ok(Some(event)) => {
169 lookahead = Some(event);
170 break;
171 }
172 Ok(None) => break,
173 Err(error) => {
174 drain_error = Some(error);
175 break;
176 }
177 }
178 }
179
180 if let Err(message) = self
181 .launcher
182 .launch_stage(std::mem::take(&mut text), boxes)
183 .await
184 {
185 return Err(Error {
186 kind: ErrorKind::LaunchRejected,
187 message,
188 diagnostics: self.adapter.diagnostics(),
189 });
190 }
191
192 for call in calls {
193 turn.respond(
194 call.call_id,
195 ToolResult {
196 success: true,
197 output: ASYNC_TOOL_ACKNOWLEDGEMENT.to_owned(),
198 },
199 )
200 .await?;
201 }
202
203 if let Some(error) = drain_error {
204 return Err(error);
205 }
206 }
207 Some(Event::Done) => {
208 self.health = Health::Ready;
209 let items = if text.is_empty() {
210 Vec::new()
211 } else {
212 vec![ShimItem::Text(text)]
213 };
214 return Ok(ShimOutput { items });
215 }
216 Some(Event::Error(error)) => return Err(error),
217 None => {
218 return Err(
219 self.error("Codex app-server closed before the active turn completed")
220 );
221 }
222 }
223 }
224 }
225
226 fn render_pending_boxes(&self) -> String {
227 let mut output = String::new();
228 for box_ in &self.pending_boxes {
229 output = append_section(output, self.codec.box_text(box_));
230 }
231 output
232 }
233
234 fn unusable(&self) -> Error {
235 self.error("Codex shim cannot be reused after an active turn failed or was cancelled")
236 }
237
238 fn error(&self, message: impl Into<String>) -> Error {
239 Error {
240 kind: ErrorKind::Unavailable,
241 message: message.into(),
242 diagnostics: self.adapter.diagnostics(),
243 }
244 }
245}
246
247fn append_section(mut output: String, section: &str) -> String {
248 if section.is_empty() {
249 return output;
250 }
251 if !output.is_empty() && !output.ends_with('\n') {
252 output.push('\n');
253 }
254 output.push_str(section);
255 output
256}