kcode-agent-runtime 0.3.0

Provider-neutral agent loops over kcode-intelligence-router
Documentation
# kcode-agent-runtime 0.3.0

`kcode-agent-runtime` owns provider-neutral agent loops over
`kcode-intelligence-router`. It has two entry points: `run_session` drives a
primary application session whose host prepares each complete round, while
`run` drives one fresh-context native subagent turn with replaceable projected
tool state.
Both paths use one native `call_ktool` bridge and preserve router accounting.

## Complete public API

```rust
pub type HostFuture<'a, T> =
    Pin<Box<dyn Future<Output = anyhow::Result<T>> + Send + 'a>>;

pub const DEFAULT_ROUND_LIMIT: u64 = 250;

pub struct ToolCall {
    pub name: String,
    pub arguments: serde_json::Value,
}

pub enum AuditEvent {
    Started {
        parent_operation_id: Uuid,
        model: String,
        provider_model: String,
        provider: kcode_intelligence_router::AgentProvider,
        context_window_tokens: u64,
        max_input_tokens: u64,
        context: Vec<String>,
        task: String,
        host: serde_json::Value,
    },
    InferenceSubmitted {
        parent_operation_id: Uuid,
        round: u64,
        manifest_hash: String,
        estimated_input_tokens: u64,
    },
    ToolCall {
        parent_operation_id: Uuid,
        name: String,
        arguments: serde_json::Value,
    },
    ToolResult {
        parent_operation_id: Uuid,
        name: String,
        ok: bool,
        projection_accepted: bool,
        result: String,
    },
    ProviderReceipt {
        parent_operation_id: Uuid,
        round: u64,
        manifest_hash: String,
        usage: Option<kcode_codex_runtime_v2::TokenUsage>,
        receipt: Box<kcode_intelligence_router::UsageReceipt>,
    },
    Completed {
        parent_operation_id: Uuid,
        model: String,
        response: String,
    },
}

pub struct StateUpdate {
    pub key: String,
    pub text: Option<String>,
}

pub struct ToolOutcome {
    pub text: String,
    pub ok: bool,
    pub state_updates: Vec<StateUpdate>,
    pub displayed_state_keys: Vec<String>,
    pub capture: Option<serde_json::Value>,
}

impl ToolOutcome {
    pub fn success(text: impl Into<String>) -> Self;
    pub fn failure(text: impl Into<String>) -> Self;
}

pub struct ContextBudget { /* private projection */ }

impl ContextBudget {
    pub fn estimated_tokens(&self) -> u64;
    pub fn max_input_tokens(&self) -> u64;
    pub fn fits_state(
        &self,
        key: impl Into<String>,
        text: impl Into<String>,
    ) -> bool;
}

pub trait Host: Send {
    fn render_tool_call(&mut self, call: &ToolCall) -> anyhow::Result<String>;
    fn execute_tool<'a>(
        &'a mut self,
        call: ToolCall,
        operation_id: Uuid,
        budget: ContextBudget,
    ) -> HostFuture<'a, ToolOutcome>;
    fn complete_capture<'a>(
        &'a mut self,
        capture: serde_json::Value,
        contents: String,
        budget: ContextBudget,
    ) -> HostFuture<'a, ToolOutcome>;
    fn record(&mut self, event: AuditEvent) -> anyhow::Result<()>;
}

pub struct SessionRunRequest {
    pub user_id: String,
    pub operation_id: Uuid,
    pub rounds_used: u64,
    pub round_limit: u64,
}

pub struct PreparedRound {
    pub input: String,
    pub provider_input: String,
    pub continuation: Option<kcode_intelligence_router::AgentContinuation>,
    pub model: String,
    pub reasoning_effort: String,
    pub tool_description: String,
    pub timeout: Option<Duration>,
}

pub enum RoundPreparation {
    Run(PreparedRound),
    Complete(Option<String>),
}

pub enum SessionEvent {
    InferenceSubmitted {
        round: u64,
        manifest_hash: String,
        model: String,
    },
    ProviderInput {
        round: u64,
        context: kcode_codex_runtime_v2::ModelContext,
    },
    UsageUpdated {
        round: u64,
        usage: kcode_codex_runtime_v2::TokenUsage,
    },
    ProviderReceipt {
        round: u64,
        usage: Option<kcode_codex_runtime_v2::TokenUsage>,
        receipt: Box<kcode_intelligence_router::UsageReceipt>,
        continuation: Option<kcode_intelligence_router::AgentContinuation>,
    },
}

pub struct SessionToolOutcome {
    pub text: String,
    pub ok: bool,
    pub capture: Option<serde_json::Value>,
    pub stop: bool,
    pub finish_after_round: bool,
    pub emitted_response: bool,
}

pub enum ProviderResume {
    Continue(SessionToolOutcome),
    Complete(Option<String>),
    RestartFresh,
}

impl SessionToolOutcome {
    pub fn success(text: impl Into<String>) -> Self;
    pub fn failure(text: impl Into<String>) -> Self;
}

pub struct RoundCompletion {
    pub answer: String,
    pub used_tool: bool,
    pub finish_requested: bool,
    pub emitted_response: bool,
}

pub enum SessionControl {
    Continue,
    Complete(Option<String>),
}

pub trait SessionHost: Send {
    fn prepare_round<'a>(
        &'a mut self,
        round: u64,
    ) -> HostFuture<'a, RoundPreparation>;
    fn record<'a>(&'a mut self, event: SessionEvent) -> HostFuture<'a, ()>;
    fn execute_tool<'a>(
        &'a mut self,
        call: anyhow::Result<ToolCall>,
        provider_operation_id: Uuid,
    ) -> HostFuture<'a, SessionToolOutcome>;
    fn prepare_provider_resume<'a>(
        &'a mut self,
        outcome: SessionToolOutcome,
    ) -> HostFuture<'a, ProviderResume>;
    fn complete_capture<'a>(
        &'a mut self,
        capture: serde_json::Value,
        contents: String,
    ) -> HostFuture<'a, SessionControl>;
    fn complete_round<'a>(
        &'a mut self,
        completion: RoundCompletion,
    ) -> HostFuture<'a, SessionControl>;
}

pub struct SessionRoundLimitError { /* private limit */ }
pub fn is_session_round_limit(error: &anyhow::Error) -> bool;

pub struct RunRequest {
    pub user_id: String,
    pub parent_operation_id: Uuid,
    pub model: String,
    pub reasoning_effort: String,
    pub context: Vec<String>,
    pub task: String,
    pub timeout: Option<Duration>,
    pub start_metadata: serde_json::Value,
}

pub struct RunResult {
    pub answer: String,
    pub model: kcode_intelligence_router::ResolvedAgentModel,
}

#[derive(Clone)]
pub struct AgentRuntime { /* private router */ }

impl AgentRuntime {
    pub fn new(intelligence: kcode_intelligence_router::Intelligence) -> Self;
    pub async fn resolve_model(
        &self,
        requested: &str,
    ) -> anyhow::Result<kcode_intelligence_router::ResolvedAgentModel>;
    pub async fn run_session<H: SessionHost>(
        &self,
        request: SessionRunRequest,
        host: &mut H,
    ) -> anyhow::Result<Option<String>>;
    pub async fn run<H: Host>(
        &self,
        request: RunRequest,
        host: &mut H,
    ) -> anyhow::Result<RunResult>;
}
```

## Primary-session loop

`run_session` resumes at `rounds_used`, rejects a zero limit or a restored
count above the limit, and calls `SessionHost::prepare_round` before every
provider call. `PreparedRound::input` is the complete logical projection used
for audit identity, while `provider_input` is the new context delta transported
to a typed native continuation (or the same full projection for a fresh
thread). The runtime registers exactly one `call_ktool` definition using
`tool_description` and applies the selected model, reasoning effort, and
optional per-round timeout.

The normalized `ModelContextSubmitted` event is delivered as
`SessionEvent::ProviderInput`; raw provider transport records are intentionally
not passed through this semantic session boundary. Every usage event and the
router's canonical receipt is also delivered to `SessionHost::record` on
completed and interrupted calls. Parsed and invalid tool calls both pass
through `execute_tool`; host errors abort the run, while
`SessionToolOutcome::ok` controls the native success/failure result sent to the
provider.

Immediately before every native tool response resumes the provider,
`prepare_provider_resume` selects one of three boundary actions. `Continue`
keeps the append-only behavior: its possibly amended result is sent through
native `turn.respond`, and the current provider round continues. `Complete`
keeps the existing behavior: the current call is accounted as interrupted and
the logical session returns without another inference.

`RestartFresh` is for a successful tool whose effect and result were already
durably applied by the host. The runtime does not call native `turn.respond`,
does not invoke `complete_round` for the interrupted round, and does not retain
or replay the tool outcome. It accounts for the provider call through the same
interrupted `ProviderReceipt` path, then selects the next cumulative outer
round. The host that requested the restart can return no continuation from the
next `prepare_round` to start a fresh native thread. If interrupted-call
accounting fails, `run_session` returns that failure instead of selecting the
next round, so it cannot replay an ambiguous effect.

The default resume implementation returns `Continue` unchanged, preserving
compatibility for hosts without boundary policy. A host error from tool
execution or resume preparation records the interrupted provider receipt
before escaping. `capture` directs the terminal answer to `complete_capture`.
Otherwise `complete_round` receives the answer plus accumulated tool and
delivery facts. `stop` immediately returns `Ok(None)` after a resumed response
unless the host completes at the pre-resume boundary. `SessionControl` selects
another affine round or the final optional answer.

Exhausting the cumulative limit returns `SessionRoundLimitError`. Use
`is_session_round_limit` instead of matching its text.

## Single-turn subagent loop

`run` resolves one exact model for the entire run and renders, in order, the
immutable context sections, task, exact call/result history, and latest
replaceable state. The provider input must fit the resolved model's maximum
input capacity including the runtime's protocol reserve.

`StateUpdate.key` is a stable identity: a later update replaces the prior
projected value, and `None` removes it. `ContextBudget::fits_state` predicts the
complete projection after one replacement. A host may identify complete state
values displayed by a tool result through `ToolOutcome::displayed_state_keys`.
When a later successful update uses the same key, the old projected result is
replaced in place with `[Tool output was displayed here, but has since been
updated and now appears elsewhere in the context]`, and only the latest complete
value is rendered after history. Removing that state uses a truthful removal
marker. Exact audited results are never rewritten.

A successful tool result whose exact result and updates would exceed the budget
is audited with `projection_accepted = false` and is returned to the provider as
a failure without changing projected state. Successful results are retained
exactly in provider context; the resolved model's input capacity is the size
boundary.

A delegated run starts exactly one fresh ephemeral native provider turn.
Multiple dynamic tools, including successive state updates, execute
sequentially inside that turn. A non-`None` `ToolOutcome::capture` designates
the same turn's nonempty terminal answer as the complete captured contents and
passes it to `Host::complete_capture`; an empty terminal capture is an explicit
failure. `run` returns only a nonempty terminal result and the resolved model.

## Ownership boundary

The crate owns provider-round sequencing, native bridge parsing and responses,
fresh rendering, capacity estimation, state replacement, capture sequencing,
cancellation lineage, receipt surfacing, and round limits. Hosts own prompts,
application tool authorization and effects, durable audit/checkpoint storage,
Kmap and object access, session lifecycle, and interpretation of opaque JSON
metadata or capture tokens. The intelligence router remains the sole provider,
credential, model-catalog, cancellation, and canonical receipt owner.