Skip to main content

runtime/pipeline/
exec.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! One `exec` result as the model reads it.
7//!
8//! The command's stdout and stderr become one text, go through
9//! [`package`] under the command's key, and come back as `content`
10//! with the sieve's account beside it. Every other field of the result
11//! (the arm, the exit code, the names the child inherited, what the
12//! backlog handed back) stays where the tool put it. This is the one
13//! door for the shape: the city and citysim both come through it, so a
14//! window the simulator replays is the window the product showed.
15
16use kernel::{AxCode, AxError, ToolCall, ToolOutcome};
17use serde_json::{Map, Value};
18
19use crate::clock::ClockStamp;
20use crate::offload::OffloadSite;
21use crate::sieve::{CommandKey, FilterTable, SieveHistory};
22
23use super::{PackContext, SieveRequest, package};
24
25/// A command result's byte budget in the window. One value for both
26/// callers; the sieve's floor and stages are the real shrink, this is
27/// the ceiling behind them.
28pub const EXEC_CAP_BYTES: u64 = 16_384;
29
30/// Where a sieved result's original goes and what decides the cut:
31/// the tee's site, the run's filter table, and the run's history.
32pub struct SieveSite<'a> {
33    pub offload: OffloadSite<'a>,
34    pub table: &'a FilterTable,
35    pub history: &'a mut SieveHistory,
36}
37
38fn text_field<'a>(map: &'a Map<String, Value>, name: &str) -> &'a str {
39    map.get(name).and_then(Value::as_str).unwrap_or("")
40}
41
42/// Sieves and packages one `exec` outcome.
43///
44/// A result with neither `stdout` nor `stderr` - a command handed to
45/// the background - is returned as it came: there is no text to cut
46/// and pretending there was would put an empty `content` where the
47/// handle is.
48///
49/// # Errors
50/// Propagates a call whose arm does not parse, and whatever the
51/// pipeline reports about the tee or the cut.
52pub fn package_exec(
53    call: &ToolCall,
54    outcome: ToolOutcome,
55    site: SieveSite<'_>,
56    stamp: Option<ClockStamp>,
57) -> Result<ToolOutcome, AxError> {
58    let mut result = outcome.result.as_map().clone();
59    if !result.contains_key("stdout") && !result.contains_key("stderr") {
60        return Ok(outcome);
61    }
62    let key = CommandKey::of(&crate::tools::parse_arm(call.args.as_map())?);
63    let exit_code = super::exit_code_in(&result);
64    let mut text = text_field(&result, "stdout").to_owned();
65    let stderr = text_field(&result, "stderr");
66    if !stderr.is_empty() {
67        if !text.is_empty() {
68            text.push('\n');
69        }
70        text.push_str(stderr);
71    }
72    let packaged = package(
73        text.as_bytes(),
74        PackContext {
75            cap_bytes: EXEC_CAP_BYTES,
76            stamp,
77            net_notice: false,
78            steer: None,
79            reminder: None,
80            offload: Some(site.offload),
81            sieve: Some(SieveRequest {
82                key,
83                exit_code,
84                table: site.table,
85                history: site.history,
86            }),
87            adviser: None,
88        },
89    )?;
90    result.remove("stdout");
91    result.remove("stderr");
92    result.insert("content".to_owned(), Value::String(packaged.content));
93    let accounts = packaged
94        .events
95        .iter()
96        .map(serde_json::to_value)
97        .collect::<Result<Vec<Value>, _>>()
98        .map_err(|err| {
99            AxError::failure(AxCode::InvalidArgs, "encode sieve account", err.to_string())
100                .with_recovery(
101                    "report this against runtime::pipeline::exec: a sieve account holds \
102                     names and counts, and JSON refuses neither",
103                )
104        })?;
105    result.insert(super::EXEC_ACCOUNTS.to_owned(), Value::Array(accounts));
106    Ok(ToolOutcome {
107        result: kernel::Payload::new(result)?,
108        attachments: Vec::new(),
109    })
110}