///|
/// A conversation session. `messages` is the linear transcript this thread
/// owns; `metadata` is a free-form product-owned bag (used for lineage via
/// `parent_thread_id`, product-specific flags, anything posoco does not
/// interpret).
///
/// R3 M3.7: `messages` carries canonical `@kernel.Message` values directly.
/// There is no longer a legacy `@types.Message` form — the kernel ADT is the
/// single protocol.
pub(all) struct Session {
messages : Array[@kernel.Message]
metadata : Map[String, Json]
} derive(Eq, Debug)
///|
/// Metadata key under which a session's parent thread id is stored. Absent
/// for threads created via the `new` command (fresh, no parent). Present
/// when this session was forked from another thread (user-initiated
/// `/fork-here`) or compacted from another thread (modelport-driven compact
/// via `ModelPort::compact` returning `CompactMode::NewThread`).
///
/// Stored in `metadata` rather than a dedicated field so existing Session
/// construction sites continue to compile unchanged. Product code reads it
/// via `Session::parent_thread_id(session)`.
pub const PARENT_THREAD_ID_KEY = "posoco.parent_thread_id"
///|
pub const CONTEXT_STATE_METADATA_KEY = "posoco.context_state"
///|
pub(all) enum ContextReadingSource {
Measured
Estimated
Unknown
} derive(Eq, Debug)
///|
pub impl Show for ContextReadingSource with fn to_string(
self : ContextReadingSource,
) -> String {
match self {
Measured => "measured"
Estimated => "estimated"
Unknown => "unknown"
}
}
///|
pub(all) struct ContextState {
session_id : String
model_id : String
context_version : Int
last_measured_tokens : Int?
estimated_added_tokens : Int
window_tokens : Int?
compact_threshold : Double
source : ContextReadingSource
reading_message_count : Int
} derive(Eq, Debug)
///|
pub impl Show for ContextState with fn to_string(self : ContextState) -> String {
let measured = match self.last_measured_tokens {
Some(n) => n.to_string()
None => "unknown"
}
"ContextState(session=\{self.session_id}, model=\{self.model_id}, version=\{self.context_version}, measured=\{measured}, estimated_added=\{self.estimated_added_tokens}, window=\{match self.window_tokens { Some(w) => w.to_string(); None => "unknown" }}, threshold=\{self.compact_threshold}, source=\{self.source.to_string()})"
}
///|
pub fn ContextState::to_metadata_json(self : ContextState) -> Json {
Json::object(
Map::from_array([
("session_id", Json::string(self.session_id)),
("model_id", Json::string(self.model_id)),
("context_version", Json::number(self.context_version.to_double())),
(
"last_measured_tokens",
match self.last_measured_tokens {
Some(n) => Json::number(n.to_double())
None => Json::null()
},
),
(
"estimated_added_tokens",
Json::number(self.estimated_added_tokens.to_double()),
),
(
"window_tokens",
match self.window_tokens {
Some(n) => Json::number(n.to_double())
None => Json::null()
},
),
("compact_threshold", Json::number(self.compact_threshold)),
("source", Json::string(self.source.to_string())),
(
"reading_message_count",
Json::number(self.reading_message_count.to_double()),
),
]),
)
}
///|
pub fn ContextState::from_metadata(
metadata : Map[String, Json],
) -> ContextState? {
match metadata.get(CONTEXT_STATE_METADATA_KEY) {
Some(Object(fields)) => {
let session_id = match fields.get("session_id") {
Some(String(s)) => s
_ => return None
}
let model_id = match fields.get("model_id") {
Some(String(s)) => s
_ => return None
}
let context_version = match fields.get("context_version") {
Some(Number(n, ..)) => n.to_int()
_ => return None
}
let last_measured : Int? = match fields.get("last_measured_tokens") {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
}
let estimated_added = match fields.get("estimated_added_tokens") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
let window : Int? = match fields.get("window_tokens") {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
}
let threshold = match fields.get("compact_threshold") {
Some(Number(n, ..)) => n
_ => return None
}
let source : ContextReadingSource = match fields.get("source") {
Some(String(s)) =>
match s {
"measured" => Measured
"estimated" => Estimated
_ => Unknown
}
_ => Unknown
}
let reading_count = match fields.get("reading_message_count") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
Some({
session_id,
model_id,
context_version,
last_measured_tokens: last_measured,
estimated_added_tokens: estimated_added,
window_tokens: window,
compact_threshold: threshold,
source,
reading_message_count: reading_count,
})
}
_ => None
}
}
///|
pub(all) struct CompactOutcome {
final_session_id : String
mode : @kernel.CompactMode
messages_after : Int
} derive(Eq)
///|
/// Read the parent thread id from a session's metadata. Returns `None` when
/// the key is absent or not a string.
pub fn Session::parent_thread_id(self : Session) -> String? {
match self.metadata.get(PARENT_THREAD_ID_KEY) {
Some(String(s)) => Some(s)
_ => None
}
}
///|
/// Construct a Session with the given parent thread id recorded in its
/// metadata. Used by fork/compact handlers.
pub fn Session::with_parent_thread_id(
self : Session,
parent_thread_id : String,
) -> Session {
let new_metadata : Map[String, Json] = self.metadata.copy()
new_metadata[PARENT_THREAD_ID_KEY] = Json::string(parent_thread_id)
{ messages: self.messages, metadata: new_metadata, }
}
///|
/// Attribution identity of one emitted `TurnEvent` / `HookStage`: which
/// session and run the event belongs to. Projected from the committed event
/// envelope at the Agent boundary — never reconstructed by scanning a
/// transcript. Scope answers "which run does this belong to"; it does not
/// carry effect identity (`ToolCall.call_id` already correlates tool
/// events).
pub(all) struct EventScope {
session_id : @kernel.SessionId
run_id : @kernel.RunId
turn_id : @kernel.TurnId
} derive(Eq, Debug)
///|
/// Turn lifecycle events observed via `Observer::on_event`. R3 M3.7: payload
/// types are now canonical kernel types (`ToolCall`, `ToolOutcome`,
/// `Message`, `Usage`). `is_error` on `ToolCallResult` is preserved for
/// legacy observer compatibility — it is derived from the `ToolOutcome`
/// variant (`is_failure`).
pub(all) enum TurnEvent {
TurnStarted
ToolCallPending(@kernel.ToolCall)
ToolCallResult(
call~ : @kernel.ToolCall,
result~ : @kernel.ToolOutcome,
is_error~ : Bool
)
ModelResponseReceived(message~ : @kernel.Message, usage~ : @kernel.Usage?)
SessionRedirect(
from~ : String,
to~ : String,
messages_before~ : Int,
messages_after~ : Int
)
TurnCompleted
TurnFailed(String)
ToolCallDeferred(call~ : @kernel.ToolCall, reason~ : String)
StreamChunkReceived(chunk~ : StreamChunk)
/// Emitted when the buffered chunk queue overflowed and oldest chunks were
/// dropped. `count` is the number of dropped `StreamChunkReceived` events.
/// This event is synthesized by the chunk dispatcher and inserted before
/// the next delivered chunk or terminal event so observers can detect loss.
StreamChunksDropped(count~ : Int)
/// Configuration warning emitted by an extension or product configuration
/// flow when a requested setting cannot be applied. This event lets
/// subscribers surface the warning without coupling to the configuration
/// source.
///
/// `field` identifies the configuration field, `value` is the requested
/// value, and `reason` explains why it could not be applied.
ConfigWarning(field~ : String, value~ : String, reason~ : String)
/// Configuration change notification emitted by an extension or product
/// configuration flow. Persistence, UI, and other subscribers use it to
/// stay in sync without polling or coupling to the configuration source.
///
/// `old_value` is empty when the field was previously unset.
/// `new_value` is empty when the field was reset to `None`.
ConfigChanged(field~ : String, old_value~ : String, new_value~ : String)
ContextStateUpdated(state~ : ContextState)
CompactStarted(trigger~ : String)
CompactFinished(
trigger~ : String,
mode~ : String,
final_session_id~ : String,
messages_after~ : Int
)
OperationFinalized(operation~ : String, outcome~ : String, detail~ : String)
Custom(source~ : String, label~ : String, data~ : Json)
} derive(Eq, Debug)
///|
pub impl Show for TurnEvent with fn to_string(self : TurnEvent) -> String {
match self {
TurnStarted => "TurnStarted"
ToolCallPending(call) => "ToolCallPending(\{call.call_id.to_string()})"
ToolCallResult(call~, is_error~, ..) =>
"ToolCallResult(\{call.call_id.to_string()}, is_error=\{is_error})"
ModelResponseReceived(..) => "ModelResponseReceived"
SessionRedirect(from~, to~, ..) => "SessionRedirect(from:\{from}, to:\{to})"
TurnCompleted => "TurnCompleted"
TurnFailed(msg) => "TurnFailed(\{msg})"
ToolCallDeferred(call~, reason~) =>
"ToolCallDeferred(\{call.call_id.to_string()}, \{reason})"
StreamChunkReceived(..) => "StreamChunkReceived"
StreamChunksDropped(count~) => "StreamChunksDropped(count=\{count})"
ConfigWarning(field~, value~, reason~) =>
"ConfigWarning(field=\{field}, value=\{value}, reason=\{reason})"
ConfigChanged(field~, old_value~, new_value~) =>
"ConfigChanged(field=\{field}, \{old_value} → \{new_value})"
ContextStateUpdated(state~) => "ContextStateUpdated(\{state.to_string()})"
CompactStarted(trigger~) => "CompactStarted(trigger=\{trigger})"
CompactFinished(trigger~, mode~, final_session_id~, messages_after~) =>
"CompactFinished(trigger=\{trigger}, mode=\{mode}, session=\{final_session_id}, msgs=\{messages_after})"
OperationFinalized(operation~, outcome~, detail~) =>
"OperationFinalized(op=\{operation}, outcome=\{outcome}, detail=\{detail})"
Custom(source~, label~, ..) => "Custom(source:\{source}, label:\{label})"
}
}
///|
/// Return value of `Agent::run_turn`. R3 M3.7: payload types are canonical.
pub(all) struct TurnResult {
message : @kernel.Message
tool_results : Array[@kernel.ToolOutcome]
final_session_id : String
} derive(Eq, Debug)