///|
/// What a background job is when it is a subagent: a child agent the engine
/// launched, carried on its `JobRecord` so a host can show it as an agent
/// rather than as a command.
///
/// The job's own `state` stays the fact about the PROCESS; `outcome` is what
/// the child achieved. `outcome`, `usage` and `report` are absent while the
/// child is live and settled once it has ended.
pub(all) struct SubagentInfo {
/// The preset the child runs: `explore`, `review`, ...
kind : String
/// A short human label for the child's task.
label : String
/// The child's own durable session, `-sr-N`.
session : String
/// Who started the child, which is also who owns it.
launch : SubagentLaunch
/// How the child ended; `None` while it is live.
outcome : SubagentOutcome?
/// What the child spent; `None` while it is live.
usage : SubagentUsage?
/// Where its report is; `None` unless it ended `Captured`.
report : SubagentReport?
/// The checkout the child works in.
workspace : SubagentWorkspace
} derive(Eq, Debug)
///|
/// Who started a subagent. The starter is its owner.
pub(all) enum SubagentLaunch {
/// The model's own tool call. The child belongs to the session.
Direct
/// A script's host-tool call: the host's id for that script's program,
/// and which of that program's subagent starts this is (from 0).
Script(program~ : String, call~ : Int32)
} derive(Eq, Debug)
///|
/// How a subagent ended.
pub(all) enum SubagentOutcome {
/// It submitted a report that passed its kind's validation.
Captured
/// It finished its turn without submitting a report.
NoReport
/// Its step budget ran out first.
MaxSteps
/// It filled its context window first.
ContextYield
/// Its launcher's wall deadline ran out first.
TimedOut
/// It was stopped: by a job stop, its owner closing, or shutdown.
Stopped
/// Anything else, with the reason.
Failed(String)
} derive(Eq, Debug)
///|
/// What a subagent spent, once it has ended.
pub(all) enum SubagentUsage {
/// The counts, from the child's own account or reconstructed. `steps`
/// is `None` when the account did not say.
Known(
steps~ : Int32?,
prompt_tokens~ : Int32,
completion_tokens~ : Int32,
cost_usd~ : Double?,
source~ : SubagentUsageSource,
unaccounted~ : String?
)
/// Nothing could be read; why.
Unavailable(String)
} derive(Eq, Debug)
///|
/// Where a subagent's usage counts came from.
pub(all) enum SubagentUsageSource {
/// The child's own result file: exact for the requests it made.
Result
/// Summed from the child's transcript because it left no result: a lower
/// bound when `unaccounted` says so.
Transcript
} derive(Eq, Debug)
///|
/// Where a captured report is.
pub(all) enum SubagentReport {
/// In `report.json` in the child's session directory.
Stored
/// Storing it failed, with the reason. The engine that captured it still
/// holds it in memory.
StoreFailed(String)
} derive(Eq, Debug)
///|
/// The checkout a subagent works in.
pub(all) enum SubagentWorkspace {
/// The session's own checkout, the one its launcher works in.
InPlace
/// A git worktree the engine provisioned for it, as of the record.
Worktree(Worktree)
/// A directory below the session's checkout, `path` (canonical), that the
/// worker had to itself while it ran: it changed files there in place, and
/// what it changed is left there, uncommitted, for the launching session.
Directory(path~ : String)
} derive(Eq, Debug)
///|
/// A worker's git worktree: a branch forked from the launching checkout's
/// HEAD, on which the engine commits the worker's changes once they pass
/// validation. When the worker ends, the engine hands it over: what is left
/// (the branch, or a worktree kept for inspection) is the launching
/// session's to merge or remove, and the engine tracks it no further.
pub(all) struct Worktree {
/// The slice's name, unique among the repository's active worktrees.
name : String
/// The worktree's root directory: where the worker ran, and where its
/// files are kept when they could not be committed.
path : String
branch : String
/// The commit it was forked from.
base : String
/// The repository-relative path prefixes the worker may change.
allowed_paths : Array[String]
state : WorktreeState
} derive(Eq, Debug)
///|
/// What a worker's worktree holds when it is handed over.
pub(all) enum WorktreeState {
/// The worker is running in it.
Working
/// Git itself failed to capture it, for these reasons; its files are kept
/// at the worktree's path as the worker left them.
CaptureFailed(reasons~ : Array[String])
/// Its changes passed validation and are this commit on the branch; the
/// checkout was removed.
Committed(commit~ : String)
/// It changed nothing; its worktree and branch were removed.
NoChanges
/// Its changes failed validation, for these reasons; its files are kept at
/// the worktree's path for inspection.
Unmergeable(defects~ : Array[String])
} derive(Eq, Debug)
///|
fn subagent_count(
fields : Map[String, Json],
name : String,
path : @json.JsonPath,
) -> Int32 raise @json.JsonDecodeError {
let value = job_uint(fields, name, path)
guard value <= 2147483647L else {
raise JsonDecodeError((path, "subagent count out of range: " + name))
}
value.to_int()
}
///|
/// A session id the session store accepts: not empty, not `.` or `..`, and
/// without path separators or NUL. Broader than a job artifact name: a
/// session may be named with spaces or any Unicode.
fn valid_session_id(id : String) -> Bool {
!(id is ("" | "." | "..")) &&
!id.contains("/") &&
!id.contains("\\") &&
!id.contains("\u{0}")
}
///|
pub impl ToJson for SubagentLaunch with fn to_json(self) {
match self {
Direct => { "kind": "direct" }
Script(program~, call~) =>
{ "kind": "script", "program": program, "call": call }
}
}
///|
pub impl FromJson for SubagentLaunch with fn from_json(json, path) {
match json {
{ "kind": "direct", .. } => Direct
Object({ "kind": "script", "program": String(program), .. } as fields) if !program.is_empty() =>
Script(program~, call=subagent_count(fields, "call", path))
_ => raise JsonDecodeError((path, "invalid subagent launch"))
}
}
///|
pub impl ToJson for SubagentOutcome with fn to_json(self) {
match self {
Captured => { "kind": "captured" }
NoReport => { "kind": "no_report" }
MaxSteps => { "kind": "max_steps" }
ContextYield => { "kind": "context_yield" }
TimedOut => { "kind": "timed_out" }
Stopped => { "kind": "stopped" }
Failed(reason) => { "kind": "failed", "reason": reason }
}
}
///|
pub impl FromJson for SubagentOutcome with fn from_json(json, path) {
match json {
{ "kind": "captured", .. } => Captured
{ "kind": "no_report", .. } => NoReport
{ "kind": "max_steps", .. } => MaxSteps
{ "kind": "context_yield", .. } => ContextYield
{ "kind": "timed_out", .. } => TimedOut
{ "kind": "stopped", .. } => Stopped
{ "kind": "failed", "reason": String(reason), .. } if !reason.is_empty() =>
Failed(reason)
_ => raise JsonDecodeError((path, "invalid subagent outcome"))
}
}
///|
pub impl ToJson for SubagentUsage with fn to_json(self) {
match self {
Known(
steps~,
prompt_tokens~,
completion_tokens~,
cost_usd~,
source~,
unaccounted~
) => {
let fields : Map[String, Json] = {
"kind": "known",
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"source": match source {
Result => "result"
Transcript => "transcript"
},
}
if steps is Some(steps) {
fields["steps"] = Json(steps)
}
if cost_usd is Some(cost) {
fields["cost_usd"] = Json(cost)
}
if unaccounted is Some(reason) {
fields["unaccounted"] = Json(reason)
}
Json::object(fields)
}
Unavailable(reason) => { "kind": "unavailable", "reason": reason }
}
}
///|
pub impl FromJson for SubagentUsage with fn from_json(json, path) {
match json {
Object({ "kind": "known", "source": String(source), .. } as fields) => {
let source : SubagentUsageSource = match source {
"result" => Result
"transcript" => Transcript
_ => raise JsonDecodeError((path, "invalid subagent usage source"))
}
let cost_usd : Double? = match fields.get("cost_usd") {
None | Some(Null) => None
Some(Number(cost, ..)) if cost >= 0.0 => Some(cost)
Some(_) => raise JsonDecodeError((path, "invalid subagent cost"))
}
let steps = match fields.get("steps") {
None | Some(Null) => None
Some(_) => Some(subagent_count(fields, "steps", path))
}
Known(
steps~,
prompt_tokens=subagent_count(fields, "prompt_tokens", path),
completion_tokens=subagent_count(fields, "completion_tokens", path),
cost_usd~,
source~,
unaccounted=job_optional(fields, "unaccounted", path),
)
}
{ "kind": "unavailable", "reason": String(reason), .. } if !reason.is_empty() =>
Unavailable(reason)
_ => raise JsonDecodeError((path, "invalid subagent usage"))
}
}
///|
pub impl ToJson for SubagentReport with fn to_json(self) {
match self {
Stored => { "kind": "stored" }
StoreFailed(reason) => { "kind": "store_failed", "reason": reason }
}
}
///|
pub impl FromJson for SubagentReport with fn from_json(json, path) {
match json {
{ "kind": "stored", .. } => Stored
{ "kind": "store_failed", "reason": String(reason), .. } if !reason.is_empty() =>
StoreFailed(reason)
_ => raise JsonDecodeError((path, "invalid subagent report"))
}
}
///|
pub impl ToJson for SubagentWorkspace with fn to_json(self) {
match self {
InPlace => { "kind": "in_place" }
Worktree(worktree) => { "kind": "worktree", "worktree": worktree.to_json() }
Directory(path~) => { "kind": "directory", "path": path }
}
}
///|
pub impl FromJson for SubagentWorkspace with fn from_json(json, path) {
match json {
{ "kind": "in_place", .. } => InPlace
{ "kind": "worktree", "worktree": worktree, .. } =>
Worktree(Worktree::from_json(worktree, path))
{ "kind": "directory", "path": String(directory), .. } =>
Directory(path=directory)
_ => raise JsonDecodeError((path, "invalid subagent workspace"))
}
}
///|
pub impl ToJson for Worktree with fn to_json(self) {
{
"name": self.name,
"path": self.path,
"branch": self.branch,
"base": self.base,
"allowed_paths": self.allowed_paths,
"state": self.state.to_json(),
}
}
///|
pub impl FromJson for Worktree with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "expected a worktree object"))
}
let name : String = job_required(fields, "name", path)
let root : String = job_required(fields, "path", path)
guard !name.is_empty() && !root.is_empty() else {
raise JsonDecodeError((path, "invalid worktree name or path"))
}
{
name,
path: root,
branch: job_required(fields, "branch", path),
base: job_required(fields, "base", path),
allowed_paths: job_required(fields, "allowed_paths", path),
state: job_required(fields, "state", path),
}
}
///|
pub impl ToJson for WorktreeState with fn to_json(self) {
match self {
Working => { "kind": "working" }
CaptureFailed(reasons~) => { "kind": "capture_failed", "reasons": reasons }
Committed(commit~) => { "kind": "committed", "commit": commit }
NoChanges => { "kind": "no_changes" }
Unmergeable(defects~) => { "kind": "unmergeable", "defects": defects }
}
}
///|
pub impl FromJson for WorktreeState with fn from_json(json, path) {
match json {
{ "kind": "working", .. } => Working
{ "kind": "capture_failed", "reasons": reasons, .. } =>
CaptureFailed(reasons=@json.from_json(reasons, path~))
{ "kind": "committed", "commit": String(commit), .. } if !commit.is_empty() =>
Committed(commit~)
{ "kind": "no_changes", .. } => NoChanges
{ "kind": "unmergeable", "defects": defects, .. } =>
Unmergeable(defects=@json.from_json(defects, path~))
_ => raise JsonDecodeError((path, "invalid worktree state"))
}
}
///|
pub impl ToJson for SubagentInfo with fn to_json(self) {
let fields : Map[String, Json] = {
"kind": self.kind,
"label": self.label,
"session": self.session,
"launch": self.launch.to_json(),
"workspace": self.workspace.to_json(),
}
if self.outcome is Some(outcome) {
fields["outcome"] = outcome.to_json()
}
if self.usage is Some(usage) {
fields["usage"] = usage.to_json()
}
if self.report is Some(report) {
fields["report"] = report.to_json()
}
Json::object(fields)
}
///|
pub impl FromJson for SubagentInfo with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "expected a subagent object"))
}
let kind : String = job_required(fields, "kind", path)
let session : String = job_required(fields, "session", path)
guard !kind.is_empty() && valid_session_id(session) else {
raise JsonDecodeError((path, "invalid subagent kind or session"))
}
{
kind,
label: job_required(fields, "label", path),
session,
launch: job_required(fields, "launch", path),
outcome: job_optional(fields, "outcome", path),
usage: job_optional(fields, "usage", path),
report: job_optional(fields, "report", path),
workspace: job_required(fields, "workspace", path),
}
}
///|
pub extend SubagentInfo with Eq::{equal, not_equal}
///|
pub extend SubagentInfo with Debug::{to_repr}
///|
pub extend SubagentInfo with ToJson::{to_json}
///|
pub extend SubagentInfo with FromJson::{from_json}
///|
pub extend SubagentLaunch with Eq::{equal, not_equal}
///|
pub extend SubagentLaunch with Debug::{to_repr}
///|
pub extend SubagentOutcome with Eq::{equal, not_equal}
///|
pub extend SubagentOutcome with Debug::{to_repr}
///|
pub extend SubagentUsage with Eq::{equal, not_equal}
///|
pub extend SubagentUsage with Debug::{to_repr}
///|
pub extend SubagentUsageSource with Eq::{equal, not_equal}
///|
pub extend SubagentUsageSource with Debug::{to_repr}
///|
pub extend SubagentReport with Eq::{equal, not_equal}
///|
pub extend SubagentReport with Debug::{to_repr}
///|
pub extend SubagentWorkspace with Eq::{equal, not_equal}
///|
pub extend SubagentWorkspace with Debug::{to_repr}
///|
pub extend SubagentLaunch with ToJson::{to_json}
///|
pub extend SubagentLaunch with FromJson::{from_json}
///|
pub extend SubagentOutcome with ToJson::{to_json}
///|
pub extend SubagentOutcome with FromJson::{from_json}
///|
pub extend SubagentUsage with ToJson::{to_json}
///|
pub extend SubagentUsage with FromJson::{from_json}
///|
pub extend SubagentReport with ToJson::{to_json}
///|
pub extend SubagentReport with FromJson::{from_json}
///|
pub extend SubagentWorkspace with ToJson::{to_json}
///|
pub extend SubagentWorkspace with FromJson::{from_json}
///|
pub extend Worktree with Eq::{equal, not_equal}
///|
pub extend Worktree with Debug::{to_repr}
///|
pub extend Worktree with ToJson::{to_json}
///|
pub extend Worktree with FromJson::{from_json}
///|
pub extend WorktreeState with Eq::{equal, not_equal}
///|
pub extend WorktreeState with Debug::{to_repr}
///|
pub extend WorktreeState with ToJson::{to_json}
///|
pub extend WorktreeState with FromJson::{from_json}