use std::{collections::VecDeque, future::Future, pin::Pin};
pub use kcode_k1_codex_runtime::{
Adapter, Config, DynamicTool, Error, ErrorKind, Event, ToolCall, ToolResult, Turn,
};
pub 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.";
pub type ToolLaunchFuture<'a> = Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
pub trait ToolCallLauncher<B>: Send {
fn launch<'a>(&'a mut self, box_: &'a B) -> ToolLaunchFuture<'a>;
}
pub trait BoxCodec {
type Box: Clone;
fn tool_call_box(&mut self, call: &ToolCall) -> Self::Box;
fn box_text<'a>(&self, box_: &'a Self::Box) -> &'a str;
}
#[derive(Clone, Debug, PartialEq)]
pub enum ShimItem<B> {
Text(String),
Box(B),
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShimOutput<B> {
pub items: Vec<ShimItem<B>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Health {
Ready,
Unusable,
}
pub struct Shim<C: BoxCodec> {
adapter: Adapter,
conversation_key: String,
codec: C,
launcher: Box<dyn ToolCallLauncher<C::Box>>,
health: Health,
pending_boxes: VecDeque<C::Box>,
}
impl<C: BoxCodec> Shim<C> {
pub fn new(
adapter: Adapter,
conversation_key: impl Into<String>,
codec: C,
launcher: Box<dyn ToolCallLauncher<C::Box>>,
) -> Self {
Self {
adapter,
conversation_key: conversation_key.into(),
codec,
launcher,
health: Health::Ready,
pending_boxes: VecDeque::new(),
}
}
pub fn record_box(&mut self, box_: C::Box) {
self.pending_boxes.push_back(box_);
}
pub fn record_boxes(&mut self, boxes: impl IntoIterator<Item = C::Box>) {
self.pending_boxes.extend(boxes);
}
pub fn pending_box_count(&self) -> usize {
self.pending_boxes.len()
}
pub async fn close_conversation(&mut self) -> Result<(), Error> {
if self.health == Health::Unusable {
return Err(self.unusable());
}
self.adapter
.close_conversation(self.conversation_key.clone())
.await
}
pub async fn infer(&mut self, input: impl Into<String>) -> Result<ShimOutput<C::Box>, Error> {
if self.health == Health::Unusable {
return Err(self.unusable());
}
let submitted_box_count = self.pending_boxes.len();
let input = input.into();
let input = append_section(self.render_pending_boxes(), &input);
self.health = Health::Unusable;
let mut turn = match self
.adapter
.start_turn(self.conversation_key.clone(), input)
.await
{
Ok(turn) => turn,
Err(error) => {
self.health = Health::Ready;
return Err(error);
}
};
for _ in 0..submitted_box_count {
let removed = self.pending_boxes.pop_front();
debug_assert!(removed.is_some());
}
let mut items = Vec::new();
loop {
match turn.next_event().await {
Some(Event::TextDelta(delta)) => push_text(&mut items, delta),
Some(Event::ToolCall(call)) => {
let box_ = self.codec.tool_call_box(&call);
if let Err(message) = self.launcher.launch(&box_).await {
return Err(Error {
kind: ErrorKind::LaunchRejected,
message,
diagnostics: self.adapter.diagnostics(),
});
}
turn.respond(
call.call_id,
ToolResult {
success: true,
output: ASYNC_TOOL_ACKNOWLEDGEMENT.to_owned(),
},
)
.await?;
items.push(ShimItem::Box(box_));
}
Some(Event::Done) => {
self.health = Health::Ready;
return Ok(ShimOutput { items });
}
Some(Event::Error(error)) => return Err(error),
None => {
return Err(
self.error("Codex app-server closed before the active turn completed")
);
}
}
}
}
fn render_pending_boxes(&self) -> String {
let mut output = String::new();
for box_ in &self.pending_boxes {
output = append_section(output, self.codec.box_text(box_));
}
output
}
fn unusable(&self) -> Error {
self.error("Codex shim cannot be reused after an active turn failed or was cancelled")
}
fn error(&self, message: impl Into<String>) -> Error {
Error {
kind: ErrorKind::Unavailable,
message: message.into(),
diagnostics: self.adapter.diagnostics(),
}
}
}
fn push_text<B>(items: &mut Vec<ShimItem<B>>, delta: String) {
match items.last_mut() {
Some(ShimItem::Text(text)) => text.push_str(&delta),
_ => items.push(ShimItem::Text(delta)),
}
}
fn append_section(mut output: String, section: &str) -> String {
if section.is_empty() {
return output;
}
if !output.is_empty() && !output.ends_with('\n') {
output.push('\n');
}
output.push_str(section);
output
}