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_stage<'a>(&'a mut self, text: String, boxes: Vec<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 = append_section(self.render_pending_boxes(), &input.into());
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 {
debug_assert!(self.pending_boxes.pop_front().is_some());
}
let mut text = String::new();
let mut lookahead = None;
loop {
let event = match lookahead.take() {
Some(event) => Some(event),
None => turn.next_event().await,
};
match event {
Some(Event::TextDelta(delta)) => text.push_str(&delta),
Some(Event::ToolCall(first)) => {
let first_box = self.codec.tool_call_box(&first);
let mut calls = vec![first];
let mut boxes = vec![first_box];
let mut drain_error = None;
loop {
match turn.try_next_event() {
Ok(Some(Event::ToolCall(call))) => {
let box_ = self.codec.tool_call_box(&call);
calls.push(call);
boxes.push(box_);
}
Ok(Some(event)) => {
lookahead = Some(event);
break;
}
Ok(None) => break,
Err(error) => {
drain_error = Some(error);
break;
}
}
}
if let Err(message) = self
.launcher
.launch_stage(std::mem::take(&mut text), boxes)
.await
{
return Err(Error {
kind: ErrorKind::LaunchRejected,
message,
diagnostics: self.adapter.diagnostics(),
});
}
for call in calls {
turn.respond(
call.call_id,
ToolResult {
success: true,
output: ASYNC_TOOL_ACKNOWLEDGEMENT.to_owned(),
},
)
.await?;
}
if let Some(error) = drain_error {
return Err(error);
}
}
Some(Event::Done) => {
self.health = Health::Ready;
let items = if text.is_empty() {
Vec::new()
} else {
vec![ShimItem::Text(text)]
};
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 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
}