feat(TASK-008): add shared transient matcher and retry support to agent nodes

Hoist the llm node's transient-error matcher to graph::is_transient_error,
extend it with broken-pipe / connection-error / error-sending-request
signatures, and give AgentNode an opt-in max_attempts retry loop with
llm-parity semantics: contexts applied before the transient check, fresh
wall-clock budget per attempt, peer identity (assignment + registry)
re-armed before each retry, retirement exactly once after the final
attempt, and structured extraction outside the loop.
This commit is contained in:
Dark-Alex-17 committed 2026-09-14 10:50:33 -06:00
1 parent 677bce18b8
commit 1c68331738
8 files changed
+474 -59

No files matched your search

+40 -2
View File
@@ -240,6 +240,9 @@ pub struct AgentNode {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<u64>,
#[serde(default = "default_max_attempts")]
pub max_attempts: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inputs: Option<HashMap<String, String>>,
@@ -321,7 +324,7 @@ pub struct LlmNode {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<String>,
#[serde(default = "default_llm_max_attempts")]
#[serde(default = "default_max_attempts")]
pub max_attempts: u32,
#[serde(default = "default_llm_max_iterations")]
@@ -349,7 +352,7 @@ pub struct LlmNode {
pub skill_instructions: Option<String>,
}
fn default_llm_max_attempts() -> u32 {
fn default_max_attempts() -> u32 {
1
}
@@ -640,6 +643,41 @@ nodes:
}
}
#[test]
fn agent_node_max_attempts_parses_and_defaults_to_one() {
let yaml = r#"
name: g
start: a
nodes:
a:
id: a
type: agent
agent: helper
prompt: hi
max_attempts: 2
next: b
b:
id: b
type: agent
agent: helper
prompt: hi
next: e
e:
id: e
type: end
output: done
"#;
let graph: Graph = serde_yaml::from_str(yaml).unwrap();
match &graph.get_node("a").unwrap().node_type {
NodeType::Agent(n) => assert_eq!(n.max_attempts, 2),
_ => panic!("expected Agent variant"),
}
match &graph.get_node("b").unwrap().node_type {
NodeType::Agent(n) => assert_eq!(n.max_attempts, 1),
_ => panic!("expected Agent variant"),
}
}
#[test]
fn agent_node_inputs_parse_optionally_and_empty_map_is_some() {
let yaml = r#"