kcode-k1-codex-adapter 0.2.0

Concrete multiplexed Codex app-server adapter and per-conversation K1 shim
Documentation
# kcode-k1-codex-adapter

## Adapter API

    use serde_json::Value;
    use std::path::PathBuf;

    #[derive(Clone, Debug)]
    pub struct Config {
        pub executable: PathBuf,
        pub working_directory: String,
        pub model: String,
        pub reasoning_effort: Option<String>,
        pub base_instructions: String,
        pub tools: Vec<DynamicTool>,
    }

    #[derive(Clone, Debug, PartialEq)]
    pub struct DynamicTool {
        pub name: String,
        pub description: String,
        pub input_schema: Value,
    }

    #[derive(Clone, Debug, PartialEq)]
    pub struct ToolCall {
        pub call_id: String,
        pub name: String,
        pub arguments: Value,
    }

    #[derive(Clone, Debug, PartialEq, Eq)]
    pub struct ToolResult {
        pub success: bool,
        pub output: String,
    }

    #[derive(Clone, Debug, PartialEq)]
    pub enum Event {
        TextDelta(String),
        ToolCall(ToolCall),
        Done,
        Error(Error),
    }

    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    pub enum ErrorKind {
        Busy,
        Interrupted,
        InvalidToolResult,
        LaunchRejected,
        Protocol,
        Server,
        Unavailable,
    }

    #[derive(Clone, Debug, PartialEq, Eq)]
    pub struct Error {
        pub kind: ErrorKind,
        pub message: String,
        pub diagnostics: Vec<u8>,
    }

    impl std::fmt::Display for Error {}
    impl std::error::Error for Error {}

    #[derive(Clone)]
    pub struct Adapter { /* private fields */ }

    impl Adapter {
        pub async fn open(config: Config) -> Result<Self, Error>;
        pub async fn start_turn(
            &self,
            conversation_key: impl Into<String>,
            input: impl Into<String>,
        ) -> Result<Turn, Error>;
        pub fn diagnostics(&self) -> Vec<u8>;
    }

    pub struct Turn { /* private fields */ }

    impl std::fmt::Debug for Turn {}

    impl Turn {
        pub async fn next_event(&mut self) -> Option<Event>;
        pub async fn respond(
            &self,
            call_id: impl Into<String>,
            result: ToolResult,
        ) -> Result<(), Error>;
    }

`open` starts and initializes the child. A conversation key has at most one active turn; an overlapping `start_turn` returns `ErrorKind::Busy`. `next_event` yields provider events in order and ends in `Done` or `Error`; dropping a nonterminal turn abandons it. `respond` accepts only the exact pending call ID. `diagnostics` returns captured child stderr bytes. Dropping all adapter handles closes and reaps the child.

For ordinary conversation-sized requests, configuration validation and diagnostics copying are linear in their input size; async operations wait for the child and its app-server responses.

## Shim API

    use std::{future::Future, pin::Pin};

    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 the next user turn.";

    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>>,
    }

    pub struct Shim<C: BoxCodec> { /* private fields */ }

    impl<C: BoxCodec> Shim<C> {
        pub fn new(
            adapter: Adapter,
            conversation_key: impl Into<String>,
            codec: C,
            launcher: Box<dyn ToolCallLauncher<C::Box>>,
        ) -> Self;
        pub fn record_box(&mut self, box_: C::Box);
        pub fn record_boxes(&mut self, boxes: impl IntoIterator<Item = C::Box>);
        pub fn pending_box_count(&self) -> usize;
        pub async fn infer(
            &mut self,
            input: impl Into<String>,
        ) -> Result<ShimOutput<C::Box>, Error>;
    }

Use one `Shim` sequentially for one conversation; shims may share cloned adapters. `BoxCodec::tool_call_box` converts every dynamic call exactly once, and `box_text` supplies that box’s complete Codex-visible text.

For every dynamic call, `infer` first awaits `ToolCallLauncher::launch` with the converted canonical box. `Ok(())` means K1 accepted and started or queued the asynchronous tool, not that it completed. The launcher must return promptly. The shim then immediately answers that exact native call with `ASYNC_TOOL_ACKNOWLEDGEMENT` and continues the same Codex turn. Calls are launched sequentially in provider event order without a count limit, timer, batching heuristic, extra turn, or current-turn tool result.

A launcher `Err(String)` certifies that the tool was not launched and returns `ErrorKind::LaunchRejected`. A successful launch is not rolled back if acknowledgement or later inference fails; the consumer must preserve every accepted call and must not blindly retry or duplicate its effect.

Each `infer` drains one native turn through `Done` and returns only terminal, atomic `ShimOutput`. No partial output or cross-round suspension is exposed. `ShimOutput::items` preserves text and box event order; adjacent text deltas are coalesced. Failure or cancellation after accepted start poisons reuse, while a start rejection leaves the shim ready.

`record_box` and `record_boxes` append externally recorded boxes in order. Pending boxes prefix the next fresh turn before ordinary input and are removed only after start acceptance. Dynamic-call boxes are output only and are not queued automatically.

For ordinary conversation-sized inputs, rendering and `infer` bookkeeping are linear in emitted text, boxes, and calls; `infer` and each launch wait for their respective provider or consumer completion.