///|
/// The result-file format version this module writes and reads.
pub const RunResultVersion : Int = 1
///|
/// How a one-shot `openseek run` ended. Only `Completed` means the task was
/// done; every other status is a run that stopped short, whatever it printed.
///
/// The statuses mirror how a turn can end in the session log, plus runs that
/// failed before their turn started (a missing API key, an unreachable
/// workspace), which are `Failed` too.
pub(all) enum RunStatus {
/// The run finished. `output` is what it produced: `{"answer": String}`,
/// the model's final message, for a general run; the preset's typed report
/// for a `--kind` run.
Completed(output~ : Json)
/// A `--kind` run's turn finished without submitting the preset's report.
/// The run did not fail, but it produced nothing the caller asked for.
NoReport
/// The turn filled the model's context window and was checkpointed. The work
/// is not necessarily done; a new turn on the same session continues it.
ContextYield(reason~ : String)
/// The step budget ran out before the model finished.
MaxStepsExhausted
/// The agent stopped on purpose before finishing, e.g. a tool asked to abort.
Aborted(reason~ : String)
/// The run was cancelled from outside.
Interrupted(reason~ : String)
/// An error ended the run.
Failed(reason~ : String)
} derive(Eq, Debug)
///|
/// What `openseek run --result-file PATH` writes to `PATH`: the one
/// authoritative account of how the run ended. See `docs/run-result.md` for
/// the file's lifecycle and the exact JSON.
pub(all) struct RunResult {
/// The request's `request_id`, echoed; `None` when it carried none.
request_id : String?
/// The preset that ran; `None` for a general run.
kind : String?
status : RunStatus
/// The durable session the run recorded to; `None` for `--no-session`.
session : String?
/// Token usage summed over the model responses of this run's turn; `None`
/// when the run never reached its turn.
usage : Usage?
/// How many model responses this run's turn recorded; `None` when the run
/// never reached its turn.
steps : Int?
} derive(Eq, Debug)
///|
/// Why a JSON value is not a result this module can read.
pub(all) suberror RunResultError {
/// A result from a newer (or unknown) format version.
UnsupportedVersion(Int)
/// A required field is missing or mistyped; names the field.
Malformed(String)
} derive(Eq, Debug)
///|
pub impl ToJson for RunResult with fn to_json(self) {
let fields : Map[String, Json] = { "version": RunResultVersion.to_json() }
if self.request_id is Some(id) {
fields["request_id"] = id.to_json()
}
if self.kind is Some(kind) {
fields["kind"] = kind.to_json()
}
match self.status {
Completed(output~) => {
fields["status"] = "completed"
fields["output"] = output
}
ContextYield(reason~) => {
fields["status"] = "context_yield"
fields["reason"] = reason.to_json()
}
NoReport => fields["status"] = "no_report"
MaxStepsExhausted => fields["status"] = "max_steps_exhausted"
Aborted(reason~) => {
fields["status"] = "aborted"
fields["reason"] = reason.to_json()
}
Interrupted(reason~) => {
fields["status"] = "interrupted"
fields["reason"] = reason.to_json()
}
Failed(reason~) => {
fields["status"] = "failed"
fields["reason"] = reason.to_json()
}
}
if self.session is Some(session) {
fields["session"] = session.to_json()
}
if self.usage is Some(usage) {
fields["usage"] = usage.to_json()
}
if self.steps is Some(steps) {
fields["steps"] = steps.to_json()
}
Json::object(fields)
}
///|
/// Read a result file's JSON back into a `RunResult`: the inverse of
/// `to_json`. Unknown fields are ignored, so a newer engine can add optional
/// fields without breaking this reader; an unknown `status` is `Malformed`,
/// because a reader that cannot classify a run must not guess.
pub fn parse_run_result(json : Json) -> RunResult raise RunResultError {
guard json is Object(fields) else { raise Malformed("result") }
guard int(fields, "version") is Some(version) else {
raise Malformed("version")
}
guard version == RunResultVersion else { raise UnsupportedVersion(version) }
fn required(key) raise RunResultError {
guard text(fields, key) is Some(value) else { raise Malformed(key) }
value
}
let status : RunStatus = match text(fields, "status") {
Some("completed") =>
match fields.get("output") {
Some(output) => Completed(output~)
None => raise Malformed("output")
}
Some("context_yield") => ContextYield(reason=required("reason"))
Some("no_report") => NoReport
Some("max_steps_exhausted") => MaxStepsExhausted
Some("aborted") => Aborted(reason=required("reason"))
Some("interrupted") => Interrupted(reason=required("reason"))
Some("failed") => Failed(reason=required("reason"))
_ => raise Malformed("status")
}
fn optional_text(key) raise RunResultError {
match fields.get(key) {
None => None
Some(String(value)) => Some(value)
Some(_) => raise Malformed(key)
}
}
let request_id = optional_text("request_id")
let kind = optional_text("kind")
let session = match fields.get("session") {
None => None
Some(String(session)) => Some(session)
Some(_) => raise Malformed("session")
}
let usage = match fields.get("usage") {
None => None
Some(Object(usage)) =>
match usage_of(usage) {
Some(usage) => Some(usage)
None => raise Malformed("usage")
}
Some(_) => raise Malformed("usage")
}
let steps = match fields.get("steps") {
None => None
Some(_) =>
match int(fields, "steps") {
Some(steps) if steps >= 0 => Some(steps)
_ => raise Malformed("steps")
}
}
{ request_id, kind, status, session, usage, steps, }
}
///|
pub extend RunStatus with Debug::{to_repr}
///|
pub extend RunStatus with Eq::{equal, not_equal}
///|
pub extend RunResult with Debug::{to_repr}
///|
pub extend RunResult with Eq::{equal, not_equal}
///|
pub extend RunResult with ToJson::{to_json}
///|
pub extend RunResultError with Debug::{to_repr}
///|
pub extend RunResultError with Eq::{equal, not_equal}