# API
`kcode-chatend` owns Kennedy's provider-independent Chatend model: durable
box and event state, exact context projection, pending-resource metadata,
provider-usage projection, replay, and the mutable adapter that synchronizes
Chatend changes through `kcode-session-log`.
Ordinary application code continues to consume these types through
`kcode_session_history::chatend` and obtains mutable `Session` values from
`kcode_session_history::SessionHistory`. The
`SessionHistoryIntegration` type is the narrow implementation boundary used
by that facade; it is not an alternative application-level session owner.
## Constants and identifiers
```rust
pub const FORMAT_VERSION: u32 = 1;
pub const MAX_OBJECT_BYTES: u64 = 32 * 1024 * 1024 * 1024;
pub const ESTIMATED_BYTES_PER_TOKEN: u64 = 4;
pub struct EventId(pub u64);
pub struct BoxId(pub u64);
pub struct PendingId(/* private String */);
```
`EventId` and `BoxId` are ordered, hashable, serializable transparent
identifiers and implement `Display`. Event identity starts at one. Box
creation derives its box identity from its event identity.
```rust
impl PendingId {
pub fn from_event(id: EventId) -> PendingId;
pub fn parse(value: impl Into<String>) -> anyhow::Result<PendingId>;
pub fn number(&self) -> u64;
}
```
A pending identity is exactly `pending:N` with a nonzero unsigned integer.
`parse` rejects every other form. `PendingId` is ordered, hashable,
serializable, and implements `Display`.
## Session metadata
```rust
pub enum SessionKind {
Conversation,
Telegram,
TelegramGroup,
SelfTime,
AudioIngress,
HistoryIngress,
Other(String),
}
pub struct SessionMetadata {
pub session_id: String,
pub kind: SessionKind,
pub created_at: String,
pub effective_context_tokens: u64,
pub channel: serde_json::Value,
}
```
`SessionKind` uses snake-case Serde names. `SessionMetadata` uses
camel-case field names; absent `channel` data defaults to JSON null.
`session_id` and `created_at` must exactly match the underlying session-log
header during replay.
## Provider accounting
```rust
pub struct ProviderTokenUsage {
pub input_tokens: u64,
pub cached_input_tokens: u64,
pub thinking_tokens: u64,
pub output_tokens: u64,
}
pub enum ProviderMetering {
Tokens(ProviderTokenUsage),
DurationSeconds { seconds: f64 },
Unavailable,
}
pub struct ProviderCostEstimate {
pub usd_nanos: u64,
pub accuracy: serde_json::Value,
pub pricing_version: String,
}
pub type ProviderCostEstimator =
fn(&str, &ProviderMetering) -> Option<ProviderCostEstimate>;
pub struct ProviderCostSummary {
pub estimated_cost_usd_nanos: u64,
pub unpriced_provider_calls: u64,
}
```
The estimator callback receives the provider model and reconstructable
metering for a legacy receipt. Returning `None` leaves that call explicitly
unpriced. Compatibility pricing changes only replayed state and never rewrites
the immutable event stream.
## Boxes and representations
```rust
pub enum BoxOwner {
User,
Kennedy,
Controller,
System,
Tool { tool_instance: String, slot: String },
}
pub struct BoxContent {
pub text: String,
pub objects: Vec<String>,
pub metadata: serde_json::Value,
}
impl BoxContent {
pub fn text(value: impl Into<String>) -> BoxContent;
pub fn use_concise_header(&mut self);
}
```
`BoxContent::text` creates text-only content. Objects render as
`Object provided: ID` lines after the text. `use_concise_header` remains for
source compatibility; all current Chatend box headers omit the internal owner.
```rust
pub enum Representation {
Hydrated { canonical_event: EventId },
Dehydrated { based_on: EventId },
Summarized { based_on: EventId, text: String },
}
pub enum BoxRepresentation {
Hydrated,
Dehydrated,
Summarized(String),
}
pub struct CanonicalRevision {
pub event_id: EventId,
pub content: BoxContent,
}
pub struct BoxState {
pub id: BoxId,
pub name: String,
pub owner: BoxOwner,
pub created_at: EventId,
pub canonical: CanonicalRevision,
pub representation: Representation,
pub occurrence_events: Vec<EventId>,
pub active: bool,
}
impl BoxState {
pub fn stale(&self) -> bool;
}
```
`Representation` is durable state. `BoxRepresentation` is the desired
representation used by batch preview and application APIs. A compact
representation is stale when its recorded base differs from the latest
canonical event. Canonical contents are never destroyed by representation
changes.
## Events
```rust
pub enum PendingKind {
Node,
Object,
}
pub enum EventKind {
SessionConfigured {
effective_context_tokens: u64,
kind: SessionKind,
},
BoxCreated {
box_id: BoxId,
name: String,
owner: BoxOwner,
content: BoxContent,
},
CanonicalUpdated {
box_id: BoxId,
content: BoxContent,
},
BoxRenamed {
box_id: BoxId,
name: String,
},
BoxDehydrated { box_id: BoxId },
BoxSummarized { box_id: BoxId, text: String },
BoxRehydrated { box_id: BoxId },
BoxRetired { box_id: BoxId },
PendingAllocated {
pending_id: PendingId,
resource: PendingKind,
},
ToolInvoked {
tool_instance: String,
tool_name: String,
arguments: serde_json::Value,
invocation_id: Option<String>,
},
ToolCompleted {
tool_instance: String,
tool_name: String,
outcome: serde_json::Value,
invocation_id: Option<String>,
},
ToolLayoutChanged {
tool_instance: String,
box_ids: Vec<BoxId>,
},
InferenceSubmitted {
manifest_hash: String,
estimated_input_tokens: u64,
raw_estimated_input_tokens: Option<u64>,
},
ProviderReceipt {
manifest_hash: String,
input_tokens: Option<u64>,
output_tokens: Option<u64>,
context_bytes: Option<u64>,
raw_context_tokens: Option<u64>,
provider_data: serde_json::Value,
},
CapacityError {
attempted_operation: String,
projected_tokens: u64,
limit_tokens: u64,
},
SourceTerminated { reason: String },
HistoryIngressStarted,
HistoryEventInspected { source_event: EventId },
HistoryEventReleased { source_event: EventId },
KwebPlanChanged { operation: serde_json::Value },
KwebCommitted {
transaction_id: String,
session_object_id: String,
mappings: serde_json::Value,
},
SessionCompleted { session_object_id: String },
Note {
label: String,
value: serde_json::Value,
},
}
pub struct Event {
pub id: EventId,
pub recorded_at: String,
pub kind: EventKind,
}
pub struct Transition {
pub recorded_at: String,
pub events: Vec<Event>,
}
```
Event and transition fields use camel-case Serde names; event variants use a
snake-case `type` tag. Optional invocation and legacy calibration fields
default when absent so accepted history remains replayable. Derived identities
in box-creation and pending-allocation events must agree with their durable
event positions.
## Pending objects and managed tool slots
```rust
pub struct ObjectMetadata {
pub pending_id: PendingId,
pub event_id: EventId,
pub recorded_at: String,
pub media_type: String,
pub file_name: Option<String>,
pub transport: serde_json::Value,
}
pub struct ObjectLocation {
pub metadata: ObjectMetadata,
pub payload_offset: u64,
pub payload_len: u64,
}
pub struct ToolSlot {
pub slot: String,
pub box_id: BoxId,
pub retired: bool,
}
pub struct ToolState {
pub slots: Vec<ToolSlot>,
}
pub struct ToolSlotInput {
pub slot: String,
pub name: String,
pub content: BoxContent,
pub retired: bool,
}
```
Object metadata and tool state use camel-case Serde fields. An
`ObjectLocation` identifies a verified pending payload inside the durable
session. Tool slots provide stable box identities for stateful tools; applying
a new layout revises or retires those stable boxes rather than duplicating
their complete state.
## Chatend state and projection
```rust
pub struct Chatend {
pub metadata: SessionMetadata,
pub next_id: u64,
pub events: Vec<Event>,
pub boxes: std::collections::BTreeMap<BoxId, BoxState>,
pub pending: std::collections::BTreeMap<PendingId, PendingKind>,
pub tools: std::collections::BTreeMap<String, ToolState>,
pub tool_layouts: std::collections::BTreeMap<String, Vec<BoxId>>,
pub source_terminated: bool,
pub history_ingress_started: bool,
pub completed_session_object: Option<String>,
}
impl Chatend {
pub fn event(&self, id: EventId) -> Option<&Event>;
pub fn box_state(&self, id: BoxId) -> Option<&BoxState>;
pub fn active_boxes(&self) -> impl Iterator<Item = &BoxState>;
pub fn live_context_limit(&self) -> u64;
pub fn forced_ingress_context_limit(&self) -> u64;
pub fn ingress_initial_context_limit(&self) -> u64;
pub fn ingress_context_limit(&self) -> u64;
pub fn active_context_limit(&self) -> u64;
pub fn projection_with_new_boxes(
&self,
boxes: &[(String, BoxOwner, BoxContent)],
) -> anyhow::Result<ContextProjection>;
pub fn projection_with_new_boxes_at(
&self,
recorded_at: &str,
boxes: &[(String, BoxOwner, BoxContent)],
) -> anyhow::Result<ContextProjection>;
pub fn projection_with_new_boxes_and_updates(
&self,
boxes: &[(String, BoxOwner, BoxContent)],
updates: &std::collections::BTreeMap<BoxId, BoxContent>,
) -> anyhow::Result<ContextProjection>;
pub fn projection_with_new_boxes_and_updates_at(
&self,
recorded_at: &str,
boxes: &[(String, BoxOwner, BoxContent)],
updates: &std::collections::BTreeMap<BoxId, BoxContent>,
) -> anyhow::Result<ContextProjection>;
pub fn projection_with_box_representations(
&self,
desired: &std::collections::BTreeMap<BoxId, BoxRepresentation>,
) -> anyhow::Result<ContextProjection>;
pub fn projection(&self) -> ContextProjection;
pub fn render(&self) -> String;
}
```
Preview methods clone and validate state without durable mutation. Variants
without an explicit time use `preview` as the proposed event time.
`active_context_limit` selects the ingress or live limit from the session
kind. `projection` refreshes the current-time footer and returns the exact
provider-facing state; `render` returns its UTF-8 string.
```rust
pub struct ProjectionItem {
pub event_id: EventId,
pub box_id: BoxId,
pub marker: bool,
pub stale: bool,
pub approximate_tokens: u64,
pub text: String,
}
pub struct ContextProjection {
pub items: Vec<ProjectionItem>,
pub stale_boxes: Vec<BoxId>,
pub footer: String,
pub estimated_tokens: u64,
pub raw_estimated_tokens: u64,
pub context_bytes: u64,
pub status: SessionStatus,
}
impl ContextProjection {
pub fn render(&self) -> String;
}
pub struct SessionStatus {
pub current_context_tokens: u64,
pub fully_hydrated_context_tokens: u64,
pub context_limit_tokens: u64,
pub current_context_bytes: u64,
pub cached_input_tokens: u64,
pub non_cached_input_tokens: u64,
pub thinking_tokens: u64,
pub output_tokens: u64,
pub estimated_cost_usd_nanos: u64,
pub unpriced_provider_calls: u64,
}
pub fn estimate_tokens(text: &str) -> u64;
```
Projection items preserve durable event and box identity. A marker item is a
generic superseded-position marker such as `[box updated]`. The rendered
projection joins items and the footer with blank lines. The calibrated current
estimate and raw four-bytes-per-token estimate remain separate.
`SessionStatus` distinguishes current occupancy from cumulative provider
usage and cost.
## Mutable durable session
`Session` is opaque. Ordinary callers obtain it from
`kcode_session_history::SessionHistory`. Every successful mutation first
synchronizes its session-log records and only then revises the in-memory
Chatend state.
```rust
pub struct Session { /* private fields */ }
impl Session {
pub fn id(&self) -> &str;
pub fn state(&self) -> &Chatend;
pub fn objects(
&self,
) -> &std::collections::BTreeMap<PendingId, ObjectLocation>;
pub fn archive_bytes(&self) -> anyhow::Result<Vec<u8>>;
pub fn is_sealed(&self) -> bool;
pub fn seal(&mut self) -> anyhow::Result<()>;
pub fn repair_unfinished_tools(
&mut self,
recorded_at: impl Into<String>,
) -> anyhow::Result<Vec<EventId>>;
pub fn mark_completed(&mut self, session_object_id: String);
pub fn configure_context(
&mut self,
kind: SessionKind,
effective_context_tokens: u64,
);
pub fn create_box(
&mut self,
recorded_at: impl Into<String>,
name: impl Into<String>,
owner: BoxOwner,
content: BoxContent,
) -> anyhow::Result<BoxId>;
pub fn update_box(
&mut self,
recorded_at: impl Into<String>,
box_id: BoxId,
content: BoxContent,
) -> anyhow::Result<Option<EventId>>;
pub fn dehydrate_boxes(
&mut self,
recorded_at: impl Into<String>,
box_ids: &[BoxId],
) -> anyhow::Result<Vec<EventId>>;
pub fn summarize_box(
&mut self,
recorded_at: impl Into<String>,
box_id: BoxId,
text: impl Into<String>,
) -> anyhow::Result<EventId>;
pub fn rehydrate_box(
&mut self,
recorded_at: impl Into<String>,
box_id: BoxId,
) -> anyhow::Result<EventId>;
pub fn retire_box(
&mut self,
recorded_at: impl Into<String>,
box_id: BoxId,
) -> anyhow::Result<EventId>;
pub fn allocate_pending_node(
&mut self,
recorded_at: impl Into<String>,
) -> anyhow::Result<PendingId>;
pub fn stage_object(
&mut self,
recorded_at: impl Into<String>,
media_type: impl Into<String>,
file_name: Option<String>,
transport: serde_json::Value,
bytes: &[u8],
) -> anyhow::Result<PendingId>;
pub fn read_object(&mut self, id: &PendingId)
-> anyhow::Result<Vec<u8>>;
pub fn record(
&mut self,
recorded_at: impl Into<String>,
kind: EventKind,
) -> anyhow::Result<EventId>;
pub fn commit_events(
&mut self,
recorded_at: impl Into<String>,
events: Vec<Event>,
) -> anyhow::Result<()>;
pub fn apply_tool_slots(
&mut self,
recorded_at: impl Into<String>,
tool_instance: impl Into<String>,
slots: Vec<ToolSlotInput>,
) -> anyhow::Result<Vec<EventId>>;
pub fn apply_tool_slots_with_layout(
&mut self,
recorded_at: impl Into<String>,
tool_instance: impl Into<String>,
slots: Vec<ToolSlotInput>,
layout_slots: &[String],
) -> anyhow::Result<Vec<EventId>>;
pub fn apply_box_representations(
&mut self,
recorded_at: impl Into<String>,
desired: &std::collections::BTreeMap<BoxId, BoxRepresentation>,
) -> anyhow::Result<Vec<EventId>>;
}
```
`archive_bytes` returns the session-log archive augmented with metadata,
boxes, exact context, and rendered Chatend text. `seal` rejects unfinished
tool calls; `repair_unfinished_tools` closes interrupted invocations durably.
`update_box` returns `None` for an unchanged canonical value. Batch methods
validate the complete requested change before committing it. Pending object
size is bounded both per object and across the session by `MAX_OBJECT_BYTES`.
`mark_completed` and `configure_context` are in-memory compatibility helpers;
durable completion and configuration are recorded through the ordinary event
APIs.
## Session History integration
```rust
pub struct SessionHistoryIntegration;
impl SessionHistoryIntegration {
pub fn create_session(
path: impl AsRef<std::path::Path>,
metadata: SessionMetadata,
) -> anyhow::Result<Session>;
pub fn open_session(
path: impl AsRef<std::path::Path>,
metadata: SessionMetadata,
default_provider_model: Option<&str>,
estimator: Option<ProviderCostEstimator>,
) -> anyhow::Result<Session>;
pub fn replay(
metadata: SessionMetadata,
log: &kcode_session_log::SessionLog,
default_provider_model: Option<&str>,
estimator: Option<ProviderCostEstimator>,
) -> anyhow::Result<Chatend>;
pub fn legacy_provider_cost_summary_for_archive(
archive: &serde_json::Value,
default_provider_model: Option<&str>,
estimator: ProviderCostEstimator,
) -> anyhow::Result<ProviderCostSummary>;
}
```
This zero-sized integration type is the only construction and raw-replay
surface needed by `kcode-session-history`. `create_session` requires a
positive effective context size. `open_session` validates the
`.session-log` path and metadata/header identity, reconstructs pending-object
locations, and optionally projects legacy costs. `replay` performs the same
identity and event validation without opening mutable storage.
`legacy_provider_cost_summary_for_archive` returns only compatible cost
fields and never modifies its input.
All fallible methods report validation, replay, serialization, or durable
storage failures through `anyhow::Error`. There is no HTTP, provider network,
Kweb, credential, lifecycle-control, or completed-catalog boundary in this
crate.