///|
/// A complete invocation of a JSONL-emitting agent CLI.
///
/// `command` and `arguments` select the executable and its arguments.
/// `environment` adds or replaces variables, and `inherit_environment` controls
/// whether the child receives the parent environment. `input` is written to the
/// child's standard input before events are consumed.
pub struct Invocation {
command : String
arguments : Array[String]
environment : Map[String, String]
inherit_environment : Bool
input : String
}
///|
/// Construct an agent CLI invocation with optional environment configuration.
///
/// The default inherits the parent environment and applies no extra variables.
/// Use `inherit_environment=false` with an explicit `PATH` when the child must
/// run with a deterministic environment.
pub fn Invocation::new(
command~ : String,
arguments~ : Array[String],
input~ : String,
environment? : Map[String, String] = Map([]),
inherit_environment? : Bool = true,
) -> Invocation {
{ command, arguments, environment, inherit_environment, input }
}
///|
/// The process-level outcome of an agent CLI invocation.
///
/// `Completed` means the child exited with status zero, `Stopped` means the
/// callback returned `false`, and `Failed` contains a nonzero exit code and the
/// captured standard error text.
pub enum RunResult {
Completed
Stopped
Failed(code~ : Int, stderr~ : String)
} derive(Debug, Eq)
///|
/// Errors raised while reading or decoding the JSONL protocol.
pub suberror AgentCliError {
InvalidJson(line~ : String, cause~ : String)
} derive(Debug, Eq)
///|
priv enum ChildExit {
Exited(Int)
CancelledForStop
}
///|
/// Run an agent CLI, write its stdin, and stream decoded JSONL events in order.
///
/// Returning `false` from `on_event` terminates the child and returns `Stopped`.
/// A nonzero child exit is returned as `Failed`; process and callback errors are
/// raised after all child resources have been cleaned up.
///
/// A typical call is `run(Invocation::new(command="agent", arguments=["run"],
/// input="Hello"), async fn(event : Event) { handle(event); true })` where
/// `Event` derives `FromJson`.
pub async fn[T : @json.FromJson] run(
invocation : Invocation,
on_event : async (T) -> Bool,
) -> RunResult {
let stop_requested = Ref(false)
@async.with_task_group(async fn(group) {
let (child_input, input_writer) = @process.write_to_process()
let (output_reader, child_output) = @process.read_from_process()
let (error_reader, child_error) = @process.read_from_process()
group.spawn_bg(async fn() {
defer input_writer.close()
input_writer.write(invocation.input)
})
let stderr_task = group.spawn(async fn() {
defer error_reader.close()
error_reader.read_all().text()
})
let child = @process.spawn(
group,
invocation.command,
invocation.arguments,
extra_env=invocation.environment,
inherit_env=invocation.inherit_environment,
stdin=child_input,
stdout=child_output,
stderr=child_error,
cancel_handler=@process.hard_cancel(),
)
let stdout_task = group.spawn(async fn() {
defer output_reader.close()
while output_reader.read_until("\n") is Some(line) {
let json = @json.parse(line) catch {
error => raise AgentCliError::InvalidJson(line~, cause="\{error}")
}
let event : T = @json.from_json(json) catch {
error => raise AgentCliError::InvalidJson(line~, cause="\{error}")
}
if !on_event(event) {
stop_requested.val = true
child.cancel()
break
}
}
})
let child_exit = try {
let code = child.wait()
if stop_requested.val {
CancelledForStop
} else {
Exited(code)
}
} catch {
error if stop_requested.val && @async.is_cancellation_error(error) =>
CancelledForStop
error => raise error
}
let exited_with_failure = match child_exit {
Exited(code) => code != 0
CancelledForStop => false
}
if exited_with_failure {
stdout_task.cancel()
stdout_task.wait() catch {
error => if !@async.is_cancellation_error(error) { raise error }
}
} else {
stdout_task.wait()
}
let stderr = if stop_requested.val {
stderr_task.cancel()
try stderr_task.wait() |> ignore catch {
error => if !@async.is_cancellation_error(error) { raise error }
}
""
} else {
stderr_task.wait()
}
match child_exit {
Exited(0) => Completed
Exited(code) => Failed(code~, stderr~)
CancelledForStop => Stopped
}
})
}