# kcode-agent-runtime 0.2.2
`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 a fresh-context subagent 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 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 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 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,
input: String,
},
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>,
},
}
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,
}
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 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 and subagent round limit */ }
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 fresh
provider call. `PreparedRound::input` is the complete provider-visible input;
the runtime hashes it, registers exactly one `call_ktool` definition using
`tool_description`, disables provider thread continuation, and applies the
selected model, reasoning effort, and optional per-round timeout.
Every protocol event is delivered to `SessionHost::record`, including the
router's canonical receipt 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. `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)`. `SessionControl`
selects another fresh round or the final optional answer.
Exhausting the cumulative limit returns `SessionRoundLimitError`. Use
`is_session_round_limit` instead of matching its text.
## Fresh-context subagent loop
`run` resolves one exact model for the entire run and renders, in order, the
immutable context sections, task, compact 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 successful tool result whose
updates would exceed the budget is audited with `projection_accepted = false`
and is returned to the provider as a failure without changing projected state.
Large historical tool results are compacted in provider context while their
exact text remains in `AuditEvent::ToolResult`.
A non-`None` `ToolOutcome::capture` requests the next complete tool-free model
answer and passes it, with the opaque token, to `Host::complete_capture`.
Successful capture state is projected like any other tool state. `run` returns
only a nonempty terminal assistant answer and the resolved model. It enforces
the runtime's fixed 100-round subagent safety limit.
## 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.