style: Cleaned up all graph agent code

This commit is contained in:
2026-05-18 13:46:52 -06:00
parent fce08140bf
commit f14c006d28
23 changed files with 560 additions and 652 deletions
+34 -23
View File
@@ -1,11 +1,3 @@
//! Script execution for `script`-type graph nodes.
//!
//! Scripts receive graph state via either `GRAPH_STATE` (inline JSON env var)
//! or `GRAPH_STATE_FILE` (path to a file containing the JSON) when state
//! exceeds [`super::MAX_STATE_SIZE_BYTES`]. Scripts MUST print a single JSON
//! object on stdout. The `_next` key (if present) is consumed for routing
//! and removed before the remaining keys are merged into state.
use super::state::{StateManager, StateRepresentation};
use super::types::ScriptNode;
use crate::function::Language;
@@ -18,9 +10,6 @@ use std::time::Duration;
use tokio::process::Command;
use tokio::time::timeout;
/// Executor for script nodes. `base_dir` is the directory script paths are
/// resolved against (typically the owning agent's data directory) and is
/// also used as the child process's working directory.
pub struct ScriptExecutor {
base_dir: PathBuf,
}
@@ -32,10 +21,6 @@ impl ScriptExecutor {
}
}
/// Run the script, merge its JSON output into state (extracting `_next`
/// for routing), and then apply any `state_updates` templates. Returns
/// the routing decision from `_next`, or `None` if the script did not
/// emit one (in which case the executor falls back to `Node.next`).
pub async fn execute(
&self,
node: &ScriptNode,
@@ -153,6 +138,7 @@ fn detect_language(script_path: &Path) -> Result<Language> {
.and_then(|e| e.to_str())
.ok_or_else(|| anyhow!("Script has no file extension: '{}'", script_path.display()))?
.to_string();
match Language::from(&ext) {
Language::Unsupported => bail!(
"Unsupported script extension '.{}' for '{}'",
@@ -171,9 +157,11 @@ fn build_command(language: Language, script_path: &Path) -> Result<Command> {
)
})?;
let mut cmd = Command::new(program);
for arg in prefix_args {
cmd.arg(arg);
}
cmd.arg(script_path);
Ok(cmd)
}
@@ -183,8 +171,10 @@ mod tests {
use super::super::MAX_STATE_SIZE_BYTES;
use super::*;
use crate::utils::temp_file;
use indoc::formatdoc;
use serde_json::json;
use std::collections::HashMap;
use std::env::temp_dir;
use std::fs;
fn cmd_available(name: &str) -> bool {
@@ -226,6 +216,7 @@ echo '{"quality": 0.85, "issues": 3, "_next": "approve"}'
);
let mut state = StateManager::new(HashMap::new());
let executor = ScriptExecutor::new(&dir);
let next = executor
.execute(
&node_for(path.file_name().unwrap().to_str().unwrap(), 5),
@@ -233,6 +224,7 @@ echo '{"quality": 0.85, "issues": 3, "_next": "approve"}'
)
.await
.unwrap();
assert_eq!(next.as_deref(), Some("approve"));
assert_eq!(state.state().get("quality"), Some(&json!(0.85)));
assert_eq!(state.state().get("issues"), Some(&json!(3)));
@@ -257,6 +249,7 @@ printf '{"greeting": "hello %s"}' "$NAME"
initial.insert("name".into(), json!("alice"));
let mut state = StateManager::new(initial);
let executor = ScriptExecutor::new(&dir);
let _ = executor
.execute(
&node_for(path.file_name().unwrap().to_str().unwrap(), 5),
@@ -264,6 +257,7 @@ printf '{"greeting": "hello %s"}' "$NAME"
)
.await
.unwrap();
assert_eq!(state.state().get("greeting"), Some(&json!("hello alice")));
cleanup(&dir);
}
@@ -281,6 +275,7 @@ echo '{"ok": true}'
);
let mut state = StateManager::new(HashMap::new());
let executor = ScriptExecutor::new(&dir);
let next = executor
.execute(
&node_for(path.file_name().unwrap().to_str().unwrap(), 5),
@@ -288,6 +283,7 @@ echo '{"ok": true}'
)
.await
.unwrap();
assert!(next.is_none());
assert_eq!(state.state().get("ok"), Some(&json!(true)));
cleanup(&dir);
@@ -321,12 +317,14 @@ echo '{"raw": "hello"}'
#[tokio::test]
async fn missing_script_file_errors_before_spawning() {
let mut state = StateManager::new(HashMap::new());
let executor = ScriptExecutor::new(std::env::temp_dir());
let executor = ScriptExecutor::new(temp_dir());
let err = executor
.execute(&node_for("__does_not_exist__.sh", 5), &mut state)
.await
.unwrap_err()
.to_string();
assert!(err.contains("Script file not found"), "got: {err}");
}
@@ -338,6 +336,7 @@ echo '{"raw": "hello"}'
let (dir, path) = write_script("#!/bin/bash\n", "sh");
let mut state = StateManager::new(HashMap::new());
let executor = ScriptExecutor::new(&dir);
let err = executor
.execute(
&node_for(path.file_name().unwrap().to_str().unwrap(), 5),
@@ -346,6 +345,7 @@ echo '{"raw": "hello"}'
.await
.unwrap_err()
.to_string();
assert!(err.contains("produced no output"), "got: {err}");
cleanup(&dir);
}
@@ -356,13 +356,15 @@ echo '{"raw": "hello"}'
return;
}
let (dir, path) = write_script(
r#"#!/bin/bash
echo "not json at all"
"#,
&formatdoc! {r#"
#!/bin/bash
echo "not json at all"
"#},
"sh",
);
let mut state = StateManager::new(HashMap::new());
let executor = ScriptExecutor::new(&dir);
let err = executor
.execute(
&node_for(path.file_name().unwrap().to_str().unwrap(), 5),
@@ -371,6 +373,7 @@ echo "not json at all"
.await
.unwrap_err()
.to_string();
assert!(err.contains("merge output"), "got: {err}");
cleanup(&dir);
}
@@ -381,14 +384,16 @@ echo "not json at all"
return;
}
let (dir, path) = write_script(
r#"#!/bin/bash
echo "bad happened" >&2
exit 7
"#,
&formatdoc! {r#"
#!/bin/bash
echo "bad happened" >&2
exit 7
"#},
"sh",
);
let mut state = StateManager::new(HashMap::new());
let executor = ScriptExecutor::new(&dir);
let err = executor
.execute(
&node_for(path.file_name().unwrap().to_str().unwrap(), 5),
@@ -397,6 +402,7 @@ exit 7
.await
.unwrap_err()
.to_string();
assert!(err.contains("exit code"), "got: {err}");
assert!(err.contains("bad happened"), "got: {err}");
cleanup(&dir);
@@ -416,6 +422,7 @@ echo '{"ok":true}'
);
let mut state = StateManager::new(HashMap::new());
let executor = ScriptExecutor::new(&dir);
let err = executor
.execute(
&node_for(path.file_name().unwrap().to_str().unwrap(), 1),
@@ -424,6 +431,7 @@ echo '{"ok":true}'
.await
.unwrap_err()
.to_string();
assert!(err.contains("timed out"), "got: {err}");
cleanup(&dir);
}
@@ -493,6 +501,7 @@ print(json.dumps({
)
.await
.unwrap();
assert_eq!(next.as_deref(), Some("next_node"));
assert_eq!(state.state().get("doubled"), Some(&json!(42)));
cleanup(&dir);
@@ -503,6 +512,7 @@ print(json.dumps({
let (dir, path) = write_script("echo hi", "xyz");
let mut state = StateManager::new(HashMap::new());
let executor = ScriptExecutor::new(&dir);
let err = executor
.execute(
&node_for(path.file_name().unwrap().to_str().unwrap(), 5),
@@ -511,6 +521,7 @@ print(json.dumps({
.await
.unwrap_err()
.to_string();
assert!(
err.contains("Unsupported script extension '.xyz'"),
"got: {err}"