// session.mbt — upstream `src/session.ts` mirror: `LodySessionMeta` (the
// `_meta.lody` envelope value on standard ACP session updates and
// tool_call/tool_call_update messages), every sub-object schema, and the
// `LodySubagentTask` row used by `_lody/subagents/list`.
//
// `LodySessionMeta` itself has no `version` field upstream; `version: 1`
// lives inside `forkAtTurn` / `activity` / `task`. The elicitation feature
// is a separate envelope (see elicitation.mbt).
///|
/// `LodySteerPromptMeta` — `{ id: string }`, no version. Associates a message
/// chunk with the `_lody/session/steer` request that produced it.
pub(all) struct LodySteerPromptMeta {
id : String
} derive(Eq, Debug)
///|
pub fn LodySteerPromptMeta::to_json(self : LodySteerPromptMeta) -> Json {
Json::object(Map::from_array([("id", Json::string(self.id))]))
}
///|
pub fn LodySteerPromptMeta::from_json(
raw : Json,
) -> Result[LodySteerPromptMeta, String] {
try {
let ctx = "lody.steer"
let fields = object_fields(raw, ctx)
Ok(LodySteerPromptMeta::{ id: req_string(fields, ctx, "id"), })
} catch {
DecodeError::Msg(message) => Err(message)
}
}
///|
/// `LodyForkAtTurn` — source-turn pointer carried by `session/fork`.
pub(all) struct LodyForkAtTurn {
version : Int
turn_id : String?
} derive(Eq, Debug)
///|
pub fn LodyForkAtTurn::to_json(self : LodyForkAtTurn) -> Json {
let pairs : Array[(String, Json)] = [
("version", Json::number(self.version.to_double())),
]
match self.turn_id {
Some(turn_id) => pairs.push(("turnId", Json::string(turn_id)))
None => ()
}
Json::object(Map::from_array(pairs))
}
///|
pub fn LodyForkAtTurn::from_json(raw : Json) -> Result[LodyForkAtTurn, String] {
try {
let ctx = "lody.forkAtTurn"
let fields = object_fields(raw, ctx)
let version = req_int(fields, ctx, "version")
guard version == 1 else {
raise DecodeError::Msg(
"\{ctx}: unsupported version \{version} (expected 1)",
)
}
Ok(LodyForkAtTurn::{ version, turn_id: opt_string(fields, ctx, "turnId"), })
} catch {
DecodeError::Msg(message) => Err(message)
}
}
///|
/// `LodyActivityMeta` — compaction/retry lifecycle on tool_call envelopes.
pub(all) struct LodyActivityMeta {
version : Int
kind : LodyActivityKind
automatic : Bool?
used_tokens_before : Int?
used_tokens_after : Int?
duration_ms : Int64?
failure_reason : String?
} derive(Eq, Debug)
///|
pub fn LodyActivityMeta::to_json(self : LodyActivityMeta) -> Json {
let pairs : Array[(String, Json)] = [
("version", Json::number(self.version.to_double())),
("kind", Json::string(self.kind.to_wire())),
]
match self.automatic {
Some(v) => pairs.push(("automatic", Json::boolean(v)))
None => ()
}
match self.used_tokens_before {
Some(v) => pairs.push(("usedTokensBefore", Json::number(v.to_double())))
None => ()
}
match self.used_tokens_after {
Some(v) => pairs.push(("usedTokensAfter", Json::number(v.to_double())))
None => ()
}
match self.duration_ms {
Some(v) => pairs.push(("durationMs", Json::number(v.to_double())))
None => ()
}
match self.failure_reason {
Some(v) => pairs.push(("failureReason", Json::string(v)))
None => ()
}
Json::object(Map::from_array(pairs))
}
///|
pub fn LodyActivityMeta::from_json(
raw : Json,
) -> Result[LodyActivityMeta, String] {
try {
let ctx = "lody.activity"
let fields = object_fields(raw, ctx)
let version = req_int(fields, ctx, "version")
guard version == 1 else {
raise DecodeError::Msg(
"\{ctx}: unsupported version \{version} (expected 1)",
)
}
Ok(LodyActivityMeta::{
version,
kind: wire_of(
ctx,
"kind",
LodyActivityKind::of_wire(req_string(fields, ctx, "kind")),
),
automatic: opt_bool(fields, ctx, "automatic"),
used_tokens_before: opt_int(fields, ctx, "usedTokensBefore"),
used_tokens_after: opt_int(fields, ctx, "usedTokensAfter"),
duration_ms: opt_int64(fields, ctx, "durationMs"),
failure_reason: opt_string(fields, ctx, "failureReason"),
})
} catch {
DecodeError::Msg(message) => Err(message)
}
}
///|
/// `LodyTaskMeta.usage` — totals for one delegated task.
pub(all) struct LodyTaskUsage {
total_tokens : Int?
tool_uses : Int?
duration_ms : Int64?
} derive(Eq, Debug)
///|
pub fn LodyTaskUsage::to_json(self : LodyTaskUsage) -> Json {
let pairs : Array[(String, Json)] = []
match self.total_tokens {
Some(v) => pairs.push(("totalTokens", Json::number(v.to_double())))
None => ()
}
match self.tool_uses {
Some(v) => pairs.push(("toolUses", Json::number(v.to_double())))
None => ()
}
match self.duration_ms {
Some(v) => pairs.push(("durationMs", Json::number(v.to_double())))
None => ()
}
Json::object(Map::from_array(pairs))
}
///|
pub fn LodyTaskUsage::from_json(raw : Json) -> Result[LodyTaskUsage, String] {
try {
let ctx = "lody.task.usage"
let fields = object_fields(raw, ctx)
Ok(LodyTaskUsage::{
total_tokens: opt_int(fields, ctx, "totalTokens"),
tool_uses: opt_int(fields, ctx, "toolUses"),
duration_ms: opt_int64(fields, ctx, "durationMs"),
})
} catch {
DecodeError::Msg(message) => Err(message)
}
}
///|
/// `LodyTaskMeta` — subagent / background / scheduled task lifecycle on
/// tool_call envelopes. The task track: status vocabulary is
/// pending/in_progress/completed/failed (disjoint from the subagent-list
/// vocabulary; see `LodySubagentStatus`).
pub(all) struct LodyTaskMeta {
version : Int
task_id : String
kind : LodyTaskKind
status : LodyTaskStatus
description : String?
actor : String?
parent_task_id : String?
parent_tool_call_id : String?
model_id : String?
started_at_epoch_seconds : Int64?
ended_at_epoch_seconds : Int64?
summary : String?
error : String?
last_tool_name : String?
usage : LodyTaskUsage?
skip_transcript : Bool?
} derive(Eq, Debug)
///|
pub fn LodyTaskMeta::to_json(self : LodyTaskMeta) -> Json {
let pairs : Array[(String, Json)] = [
("version", Json::number(self.version.to_double())),
("taskId", Json::string(self.task_id)),
("kind", Json::string(self.kind.to_wire())),
("status", Json::string(self.status.to_wire())),
]
match self.description {
Some(v) => pairs.push(("description", Json::string(v)))
None => ()
}
match self.actor {
Some(v) => pairs.push(("actor", Json::string(v)))
None => ()
}
match self.parent_task_id {
Some(v) => pairs.push(("parentTaskId", Json::string(v)))
None => ()
}
match self.parent_tool_call_id {
Some(v) => pairs.push(("parentToolCallId", Json::string(v)))
None => ()
}
match self.model_id {
Some(v) => pairs.push(("modelId", Json::string(v)))
None => ()
}
match self.started_at_epoch_seconds {
Some(v) =>
pairs.push(("startedAtEpochSeconds", Json::number(v.to_double())))
None => ()
}
match self.ended_at_epoch_seconds {
Some(v) => pairs.push(("endedAtEpochSeconds", Json::number(v.to_double())))
None => ()
}
match self.summary {
Some(v) => pairs.push(("summary", Json::string(v)))
None => ()
}
match self.error {
Some(v) => pairs.push(("error", Json::string(v)))
None => ()
}
match self.last_tool_name {
Some(v) => pairs.push(("lastToolName", Json::string(v)))
None => ()
}
match self.usage {
Some(v) => pairs.push(("usage", v.to_json()))
None => ()
}
match self.skip_transcript {
Some(v) => pairs.push(("skipTranscript", Json::boolean(v)))
None => ()
}
Json::object(Map::from_array(pairs))
}
///|
pub fn LodyTaskMeta::from_json(raw : Json) -> Result[LodyTaskMeta, String] {
try {
let ctx = "lody.task"
let fields = object_fields(raw, ctx)
let version = req_int(fields, ctx, "version")
guard version == 1 else {
raise DecodeError::Msg(
"\{ctx}: unsupported version \{version} (expected 1)",
)
}
Ok(LodyTaskMeta::{
version,
task_id: req_string(fields, ctx, "taskId"),
kind: wire_of(
ctx,
"kind",
LodyTaskKind::of_wire(req_string(fields, ctx, "kind")),
),
status: wire_of(
ctx,
"status",
LodyTaskStatus::of_wire(req_string(fields, ctx, "status")),
),
description: opt_string(fields, ctx, "description"),
actor: opt_string(fields, ctx, "actor"),
parent_task_id: opt_string(fields, ctx, "parentTaskId"),
parent_tool_call_id: opt_string(fields, ctx, "parentToolCallId"),
model_id: opt_string(fields, ctx, "modelId"),
started_at_epoch_seconds: opt_int64(fields, ctx, "startedAtEpochSeconds"),
ended_at_epoch_seconds: opt_int64(fields, ctx, "endedAtEpochSeconds"),
summary: opt_string(fields, ctx, "summary"),
error: opt_string(fields, ctx, "error"),
last_tool_name: opt_string(fields, ctx, "lastToolName"),
usage: opt_sub(fields, ctx, "usage", LodyTaskUsage::from_json),
skip_transcript: opt_bool(fields, ctx, "skipTranscript"),
})
} catch {
DecodeError::Msg(message) => Err(message)
}
}
///|
/// `LodyGoalSnapshot` — one goal-management snapshot on session updates.
/// `lastReason` / `tokenBudget` are `string | null` / `number | null`
/// upstream without a distinct absent meaning, so both null and absent
/// decode to `None`.
pub(all) struct LodyGoalSnapshot {
objective : String
status : LodyGoalStatus
iterations : Int?
last_reason : String?
created_at_epoch_seconds : Int64?
updated_at_epoch_seconds : Int64?
token_budget : Int?
tokens_used : Int?
time_used_seconds : Int64?
} derive(Eq, Debug)
///|
pub fn LodyGoalSnapshot::to_json(self : LodyGoalSnapshot) -> Json {
let pairs : Array[(String, Json)] = [
("objective", Json::string(self.objective)),
("status", Json::string(self.status.to_wire())),
]
match self.iterations {
Some(v) => pairs.push(("iterations", Json::number(v.to_double())))
None => ()
}
match self.last_reason {
Some(v) => pairs.push(("lastReason", Json::string(v)))
None => ()
}
match self.created_at_epoch_seconds {
Some(v) =>
pairs.push(("createdAtEpochSeconds", Json::number(v.to_double())))
None => ()
}
match self.updated_at_epoch_seconds {
Some(v) =>
pairs.push(("updatedAtEpochSeconds", Json::number(v.to_double())))
None => ()
}
match self.token_budget {
Some(v) => pairs.push(("tokenBudget", Json::number(v.to_double())))
None => ()
}
match self.tokens_used {
Some(v) => pairs.push(("tokensUsed", Json::number(v.to_double())))
None => ()
}
match self.time_used_seconds {
Some(v) => pairs.push(("timeUsedSeconds", Json::number(v.to_double())))
None => ()
}
Json::object(Map::from_array(pairs))
}
///|
pub fn LodyGoalSnapshot::from_json(
raw : Json,
) -> Result[LodyGoalSnapshot, String] {
try {
let ctx = "lody.goal"
let fields = object_fields(raw, ctx)
Ok(LodyGoalSnapshot::{
objective: req_string(fields, ctx, "objective"),
status: wire_of(
ctx,
"status",
LodyGoalStatus::of_wire(req_string(fields, ctx, "status")),
),
iterations: opt_int(fields, ctx, "iterations"),
last_reason: opt_string(fields, ctx, "lastReason"),
created_at_epoch_seconds: opt_int64(fields, ctx, "createdAtEpochSeconds"),
updated_at_epoch_seconds: opt_int64(fields, ctx, "updatedAtEpochSeconds"),
token_budget: opt_int(fields, ctx, "tokenBudget"),
tokens_used: opt_int(fields, ctx, "tokensUsed"),
time_used_seconds: opt_int64(fields, ctx, "timeUsedSeconds"),
})
} catch {
DecodeError::Msg(message) => Err(message)
}
}
///|
/// `LodyNotice` — one notification on session updates.
pub(all) struct LodyNotice {
level : LodyNoticeLevel
message : String
source : String?
} derive(Eq, Debug)
///|
pub fn LodyNotice::to_json(self : LodyNotice) -> Json {
let pairs : Array[(String, Json)] = [
("level", Json::string(self.level.to_wire())),
("message", Json::string(self.message)),
]
match self.source {
Some(v) => pairs.push(("source", Json::string(v)))
None => ()
}
Json::object(Map::from_array(pairs))
}
///|
pub fn LodyNotice::from_json(raw : Json) -> Result[LodyNotice, String] {
try {
let ctx = "lody.notice"
let fields = object_fields(raw, ctx)
Ok(LodyNotice::{
level: wire_of(
ctx,
"level",
LodyNoticeLevel::of_wire(req_string(fields, ctx, "level")),
),
message: req_string(fields, ctx, "message"),
source: opt_string(fields, ctx, "source"),
})
} catch {
DecodeError::Msg(message) => Err(message)
}
}
///|
/// Tri-state for `LodySessionMeta.goal` (`LodyGoalSnapshot | null`, or the
/// field absent): absent means "no goal information", explicit null means
/// "the goal was cleared".
pub(all) enum LodyGoalPresence {
GoalAbsent
GoalCleared
GoalSet(LodyGoalSnapshot)
} derive(Eq, Debug)
///|
pub fn LodyGoalPresence::to_json(self : LodyGoalPresence) -> (String, Json)? {
match self {
GoalAbsent => None
GoalCleared => Some(("goal", Json::null()))
GoalSet(snapshot) => Some(("goal", snapshot.to_json()))
}
}
///|
pub fn LodyGoalPresence::from_json(
raw : Json?,
) -> Result[LodyGoalPresence, String] {
match raw {
None => Ok(GoalAbsent)
Some(Json::Null) => Ok(GoalCleared)
Some(value) =>
match LodyGoalSnapshot::from_json(value) {
Ok(snapshot) => Ok(GoalSet(snapshot))
Err(message) => Err(message)
}
}
}
///|
/// `LodySessionMeta` — the full `_meta.lody` envelope value on standard ACP
/// session updates and tool_call/tool_call_update messages. No `version`
/// field: versions live inside `forkAtTurn` / `activity` / `task`.
pub(all) struct LodySessionMeta {
turn_id : String?
fork_at_turn : LodyForkAtTurn?
steer : LodySteerPromptMeta?
tool_name : String?
activity : LodyActivityMeta?
task : LodyTaskMeta?
goal : LodyGoalPresence
notice : LodyNotice?
title_source : LodyTitleSource?
message_phase : LodyMessagePhase?
} derive(Eq, Debug)
///|
pub fn LodySessionMeta::to_json(self : LodySessionMeta) -> Json {
let pairs : Array[(String, Json)] = []
match self.turn_id {
Some(v) => pairs.push(("turnId", Json::string(v)))
None => ()
}
match self.fork_at_turn {
Some(v) => pairs.push(("forkAtTurn", v.to_json()))
None => ()
}
match self.steer {
Some(v) => pairs.push(("steer", v.to_json()))
None => ()
}
match self.tool_name {
Some(v) => pairs.push(("toolName", Json::string(v)))
None => ()
}
match self.activity {
Some(v) => pairs.push(("activity", v.to_json()))
None => ()
}
match self.task {
Some(v) => pairs.push(("task", v.to_json()))
None => ()
}
match self.goal.to_json() {
Some(pair) => pairs.push(pair)
None => ()
}
match self.notice {
Some(v) => pairs.push(("notice", v.to_json()))
None => ()
}
match self.title_source {
Some(v) => pairs.push(("titleSource", Json::string(v.to_wire())))
None => ()
}
match self.message_phase {
Some(v) => pairs.push(("messagePhase", Json::string(v.to_wire())))
None => ()
}
Json::object(Map::from_array(pairs))
}
///|
pub fn LodySessionMeta::from_json(
raw : Json,
) -> Result[LodySessionMeta, String] {
try {
let ctx = "lody.sessionMeta"
let fields = object_fields(raw, ctx)
let turn_id = opt_string(fields, ctx, "turnId")
let fork_at_turn = opt_sub(
fields,
ctx,
"forkAtTurn",
LodyForkAtTurn::from_json,
)
let steer = opt_sub(fields, ctx, "steer", LodySteerPromptMeta::from_json)
let tool_name = opt_string(fields, ctx, "toolName")
let activity = opt_sub(fields, ctx, "activity", LodyActivityMeta::from_json)
let task = opt_sub(fields, ctx, "task", LodyTaskMeta::from_json)
let goal : LodyGoalPresence = match
LodyGoalPresence::from_json(fields.get("goal")) {
Ok(g) => g
Err(message) => raise DecodeError::Msg(message)
}
let notice = opt_sub(fields, ctx, "notice", LodyNotice::from_json)
let title_source = opt_wire(
ctx,
"titleSource",
opt_string(fields, ctx, "titleSource"),
LodyTitleSource::of_wire,
)
let message_phase = opt_wire(
ctx,
"messagePhase",
opt_string(fields, ctx, "messagePhase"),
LodyMessagePhase::of_wire,
)
Ok(LodySessionMeta::{
turn_id,
fork_at_turn,
steer,
tool_name,
activity,
task,
goal,
notice,
title_source,
message_phase,
})
} catch {
DecodeError::Msg(message) => Err(message)
}
}
///|
/// `LodySubagentTask` — one row of `_lody/subagents/list`. Status vocabulary
/// (running/completed/failed/timed_out/killed/lost) is disjoint from
/// `LodyTaskStatus`. `endedAtEpochSeconds` is required but nullable upstream,
/// so `to_json` always emits the key (null until the task ends).
pub(all) struct LodySubagentTask {
task_id : String
description : String
status : LodySubagentStatus
agent_id : String?
subagent_type : String?
model_id : String?
thinking_effort : String?
started_at_epoch_seconds : Int64
ended_at_epoch_seconds : Int64?
stop_reason : String?
} derive(Eq, Debug)
///|
pub fn LodySubagentTask::to_json(self : LodySubagentTask) -> Json {
let pairs : Array[(String, Json)] = [
("taskId", Json::string(self.task_id)),
("description", Json::string(self.description)),
("status", Json::string(self.status.to_wire())),
]
match self.agent_id {
Some(v) => pairs.push(("agentId", Json::string(v)))
None => ()
}
match self.subagent_type {
Some(v) => pairs.push(("subagentType", Json::string(v)))
None => ()
}
match self.model_id {
Some(v) => pairs.push(("modelId", Json::string(v)))
None => ()
}
match self.thinking_effort {
Some(v) => pairs.push(("thinkingEffort", Json::string(v)))
None => ()
}
pairs.push(
(
"startedAtEpochSeconds",
Json::number(self.started_at_epoch_seconds.to_double()),
),
)
match self.ended_at_epoch_seconds {
Some(v) => pairs.push(("endedAtEpochSeconds", Json::number(v.to_double())))
None => pairs.push(("endedAtEpochSeconds", Json::null()))
}
match self.stop_reason {
Some(v) => pairs.push(("stopReason", Json::string(v)))
None => ()
}
Json::object(Map::from_array(pairs))
}
///|
pub fn LodySubagentTask::from_json(
raw : Json,
) -> Result[LodySubagentTask, String] {
try {
let ctx = "lody.subagents.task"
let fields = object_fields(raw, ctx)
Ok(LodySubagentTask::{
task_id: req_string(fields, ctx, "taskId"),
description: req_string(fields, ctx, "description"),
status: wire_of(
ctx,
"status",
LodySubagentStatus::of_wire(req_string(fields, ctx, "status")),
),
agent_id: opt_string(fields, ctx, "agentId"),
subagent_type: opt_string(fields, ctx, "subagentType"),
model_id: opt_string(fields, ctx, "modelId"),
thinking_effort: opt_string(fields, ctx, "thinkingEffort"),
started_at_epoch_seconds: req_int64(fields, ctx, "startedAtEpochSeconds"),
ended_at_epoch_seconds: req_int64_nullable(
fields, ctx, "endedAtEpochSeconds",
),
stop_reason: opt_string(fields, ctx, "stopReason"),
})
} catch {
DecodeError::Msg(message) => Err(message)
}
}