///|
/// One request to run a sub-agent: the workflow-side view of a call before
/// any engine specifics (binary path, model, endpoint) attach to it.
/// `kind` names the child mode the engine dispatches on (`explore`,
/// `echo`, later `worker`); `input` is the EXACT JSON the child receives
/// as its input line — the persisted replay identity IS the child input,
/// so an encoder change is an identity change and can never replay stale
/// work. `label` is display metadata: journal replay matches on (kind,
/// input, max_steps, schema, scope), never on the label. By convention a
/// label reads `stage:instance` (`verify:claude:3`, `survey:journal`), so
/// a reader can group the instances of one stage by the prefix before the
/// first colon.
///
/// The workflow BUILDS one and engines READ it; a caller seeding a journal
/// by hand goes through `AgentCall(...)`, so the replay identity can grow
/// a field without invalidating every hand-written call.
pub struct AgentCall {
kind : String
input : Json
label : String
max_steps : Int?
/// Replay namespace: everything OUTSIDE the input that makes two
/// otherwise-identical calls non-interchangeable — model, prompt
/// revision, tenant, workspace. Empty means "unscoped".
scope : String
/// A JSON Schema the report must satisfy, when the caller asked for one.
/// It rides the request so an engine that can constrain the
/// model does, and it is part of the work identity: the same question
/// under a different shape is different work. Absent from the journal
/// line when `None`, so pre-schema journals load unchanged.
schema : Json?
} derive(Eq, ToJson, FromJson)
///|
/// A call from its parts: the work (`kind`, `input`) and how a reader
/// names it (`label`). The rest is optional because the common call has
/// no ceiling, no shape, and no namespace — `scope` defaults to unscoped,
/// which is what `""` means on the field.
pub fn AgentCall::AgentCall(
kind~ : String,
input~ : Json,
label~ : String,
max_steps? : Int,
scope? : String = "",
schema? : Json,
) -> AgentCall {
{ kind, input, label, max_steps, scope, schema, }
}
///|
/// The cost accounting of one attempt to run an agent, captured whether or
/// not the attempt produced a report: a timed-out child spent real tokens,
/// and losing that spend would understate every budget built on top.
///
/// Engines BUILD one through `AgentAttempt(...)`; everyone else READS the
/// fields. That split is what lets a later accounting figure arrive as one
/// more optional argument instead of breaking every engine that ever
/// wrote the record out by hand.
pub struct AgentAttempt {
attempt_id : String
steps_used : Int
prompt_tokens : Int
completion_tokens : Int
/// The engine's own money figure for the attempt, when it reports one
/// (`usage.cost_usd` on the wire, summed). `None` means unknown, never
/// free: an engine that prices nothing leaves it absent. Absent from the
/// journal line when `None`, so earlier journals load unchanged.
cost_usd : Double?
/// Why the counters above may fall short of what the attempt spent:
/// `Some(reason)` when the engine could not give its own account (a child
/// that died without a result, say), so the counters are only what was
/// observed. `None` means they are the engine's account. Absent from the
/// journal line when `None`, like `cost_usd`.
unaccounted : String?
} derive(Eq, ToJson, FromJson)
///|
/// An attempt's accounting from its parts. Everything an engine always
/// knows is named; `cost_usd` is optional because an engine that prices
/// nothing simply leaves it out — which is what `None` means on the field:
/// unknown, never free.
pub fn AgentAttempt::AgentAttempt(
attempt_id~ : String,
steps_used~ : Int,
prompt_tokens~ : Int,
completion_tokens~ : Int,
cost_usd? : Double,
unaccounted? : String,
) -> AgentAttempt {
{
attempt_id,
steps_used,
prompt_tokens,
completion_tokens,
cost_usd,
unaccounted,
}
}
///|
/// Why an agent produced no usable report — the workflow-facing vocabulary
/// a script can meaningfully react to. Transient transport errors never
/// appear here: retrying those is the engine's job, below this seam. What
/// a script retries is a whole AGENT that came back with nothing usable —
/// `retry`, whose default policy is written against this vocabulary.
/// The child contract's result statuses other than `completed` map onto
/// these (`docs/child-contract.md` §4), plus `Skipped` (a human declined
/// the call — replay keeps it declined rather than overriding the
/// decision).
pub(all) enum AgentFailure {
TimedOut
NoReport
MaxSteps
ContextYield
Skipped
Failed(String)
} derive(Eq, ToJson, FromJson)
///|
/// The lossless envelope one agent run resolves to: either a report value
/// with its attempt accounting, or a failure that STILL carries the
/// attempt's cost. `attempt=None` means no launch was ever TRIED — a
/// `Skipped` call, or a refusal before spawn; a child that failed to
/// spawn or died early is an attempt that observed zero cost, not an
/// absent one. This is the unit the journal persists: replaying it must
/// lose nothing the live run knew.
pub(all) enum AgentOutcome {
Finished(value~ : Json, attempt~ : AgentAttempt)
DidNotFinish(failure~ : AgentFailure, attempt~ : AgentAttempt?)
} derive(Eq, ToJson, FromJson)
///|
/// The attempt this outcome accounts for: always there for `Finished`, and
/// for `DidNotFinish` unless no launch was ever tried.
pub fn AgentOutcome::attempt(self : AgentOutcome) -> AgentAttempt? {
match self {
Finished(attempt~, ..) => Some(attempt)
DidNotFinish(attempt~, ..) => attempt
}
}
///|
/// The typed error channel of the workflow layer. Every call site either
/// propagates one of these (via `raise`) or catches it into `attempt`'s
/// `Result` — there is no silent `null` to forget.
pub suberror WorkflowError {
/// The named agent call produced no usable report, and why.
AgentFailed(label~ : String, failure~ : AgentFailure)
/// The workflow's launch allowance was already spent when this call was
/// about to launch — the runaway backstop. Only LAUNCHED agents consume
/// allowance; a call cancelled while queued never counts.
CallBudgetExhausted(label~ : String)
/// A quorum/collect policy over a fan-out did not reach its threshold:
/// `ok` of `of` succeeded, `need` were required.
QuorumNotReached(need~ : Int, ok~ : Int, of~ : Int)
}
///|
pub impl Show for AgentFailure with fn output(self, logger) {
let text = match self {
TimedOut => "timed out"
NoReport => "finished without a report"
MaxSteps => "exhausted its step ceiling"
ContextYield => "yielded at the context ceiling"
Skipped => "was skipped by the user"
Failed(reason) => "failed: \{reason}"
}
logger.write_string(text)
}
///|
pub impl Show for WorkflowError with fn output(self, logger) {
let text = match self {
AgentFailed(label~, failure~) => "agent '\{label}' \{failure}"
CallBudgetExhausted(label~) =>
"launch allowance exhausted before agent '\{label}' could launch"
QuorumNotReached(need~, ok~, of~) =>
"quorum not reached: \{ok} of \{of} succeeded, \{need} required"
}
logger.write_string(text)
}