mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Add context key constants and typed accessors to eliminate string literal typo risk
Introduces context/keys.rs with 24 static constants, 4 prefix constants, and 4 helper functions for dynamic keys. Adds 6 typed accessor methods on Context (run_id, fidelity, preamble, thread_id, node_visit_count, current_node_id). Replaces all bare string literals across 13 files with constants/accessors. Fixes bug in manager_loop.rs where "internal.node_visit" was read instead of "internal.node_visit_count". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
af6e091ef3
commit
b4159243ac
13 changed files with 378 additions and 158 deletions
|
|
@ -342,7 +342,7 @@ impl CodergenBackend for AgentApiBackend {
|
|||
stage_dir: &std::path::Path,
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
) -> Result<CodergenResult, ArcError> {
|
||||
let fidelity = context.get_string("internal.fidelity", "");
|
||||
let fidelity = context.fidelity();
|
||||
let reuse_key = if fidelity == "full" {
|
||||
thread_id.map(String::from)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
/// Op ::= '=' | '!=' | '>' | '<' | '>=' | '<='
|
||||
/// | 'contains' | 'matches'
|
||||
/// ```
|
||||
use crate::context::keys;
|
||||
use crate::context::Context;
|
||||
use crate::error::ArcError;
|
||||
use crate::outcome::Outcome;
|
||||
|
|
@ -312,10 +313,10 @@ pub fn parse_condition(expr: &str) -> Result<(), ArcError> {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn resolve_key(key: &str, outcome: &Outcome, context: &Context) -> String {
|
||||
if key == "outcome" {
|
||||
if key == keys::OUTCOME {
|
||||
return outcome.status.to_string();
|
||||
}
|
||||
if key == "preferred_label" {
|
||||
if key == keys::PREFERRED_LABEL {
|
||||
return outcome.preferred_label.as_deref().unwrap_or("").to_string();
|
||||
}
|
||||
if let Some(path) = key.strip_prefix("context.") {
|
||||
|
|
@ -337,10 +338,10 @@ fn resolve_key_value(
|
|||
outcome: &Outcome,
|
||||
context: &Context,
|
||||
) -> serde_json::Value {
|
||||
if key == "outcome" {
|
||||
if key == keys::OUTCOME {
|
||||
return serde_json::Value::String(outcome.status.to_string());
|
||||
}
|
||||
if key == "preferred_label" {
|
||||
if key == keys::PREFERRED_LABEL {
|
||||
return outcome
|
||||
.preferred_label
|
||||
.as_deref()
|
||||
|
|
@ -635,7 +636,7 @@ mod tests {
|
|||
fn context_failure_class_matches_when_set() {
|
||||
let outcome = make_outcome(StageStatus::Fail);
|
||||
let context = Context::new();
|
||||
context.set("failure_class", serde_json::json!("budget_exhausted"));
|
||||
context.set(keys::FAILURE_CLASS, serde_json::json!("budget_exhausted"));
|
||||
assert!(evaluate_condition(
|
||||
"context.failure_class=budget_exhausted",
|
||||
&outcome,
|
||||
|
|
@ -647,7 +648,7 @@ mod tests {
|
|||
fn context_failure_class_not_equals_on_success() {
|
||||
let outcome = make_outcome(StageStatus::Success);
|
||||
let context = Context::new();
|
||||
context.set("failure_class", serde_json::json!(""));
|
||||
context.set(keys::FAILURE_CLASS, serde_json::json!(""));
|
||||
assert!(evaluate_condition(
|
||||
"context.failure_class!=transient_infra",
|
||||
&outcome,
|
||||
|
|
@ -659,7 +660,7 @@ mod tests {
|
|||
fn context_failure_class_combined_with_outcome() {
|
||||
let outcome = make_outcome(StageStatus::Fail);
|
||||
let context = Context::new();
|
||||
context.set("failure_class", serde_json::json!("transient_infra"));
|
||||
context.set(keys::FAILURE_CLASS, serde_json::json!("transient_infra"));
|
||||
assert!(evaluate_condition(
|
||||
"outcome=fail && context.failure_class=transient_infra",
|
||||
&outcome,
|
||||
|
|
|
|||
100
crates/arc-workflows/src/context/keys.rs
Normal file
100
crates/arc-workflows/src/context/keys.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/// Static context key constants and helper functions for dynamic keys.
|
||||
///
|
||||
/// All context keys used across the engine, handlers, and preamble are
|
||||
/// defined here to prevent typos and improve discoverability.
|
||||
|
||||
// --- Top-level keys ---
|
||||
pub const CURRENT_NODE: &str = "current_node";
|
||||
pub const OUTCOME: &str = "outcome";
|
||||
pub const FAILURE_CLASS: &str = "failure_class";
|
||||
pub const FAILURE_SIGNATURE: &str = "failure_signature";
|
||||
pub const PREFERRED_LABEL: &str = "preferred_label";
|
||||
pub const LAST_STAGE: &str = "last_stage";
|
||||
pub const LAST_RESPONSE: &str = "last_response";
|
||||
|
||||
// --- graph.* keys ---
|
||||
pub const GRAPH_GOAL: &str = "graph.goal";
|
||||
|
||||
// --- internal.* keys ---
|
||||
pub const INTERNAL_RUN_ID: &str = "internal.run_id";
|
||||
pub const INTERNAL_WORK_DIR: &str = "internal.work_dir";
|
||||
pub const INTERNAL_FIDELITY: &str = "internal.fidelity";
|
||||
pub const INTERNAL_THREAD_ID: &str = "internal.thread_id";
|
||||
pub const INTERNAL_NODE_VISIT_COUNT: &str = "internal.node_visit_count";
|
||||
|
||||
// --- current.* keys ---
|
||||
pub const CURRENT_PREAMBLE: &str = "current.preamble";
|
||||
|
||||
// --- command.* keys ---
|
||||
pub const COMMAND_OUTPUT: &str = "command.output";
|
||||
pub const COMMAND_STDERR: &str = "command.stderr";
|
||||
|
||||
// --- human.gate.* keys ---
|
||||
pub const HUMAN_GATE_SELECTED: &str = "human.gate.selected";
|
||||
pub const HUMAN_GATE_LABEL: &str = "human.gate.label";
|
||||
pub const HUMAN_GATE_TEXT: &str = "human.gate.text";
|
||||
|
||||
// --- parallel.* keys ---
|
||||
pub const PARALLEL_RESULTS: &str = "parallel.results";
|
||||
pub const PARALLEL_BRANCH_COUNT: &str = "parallel.branch_count";
|
||||
pub const PARALLEL_FAN_IN_BEST_ID: &str = "parallel.fan_in.best_id";
|
||||
pub const PARALLEL_FAN_IN_BEST_OUTCOME: &str = "parallel.fan_in.best_outcome";
|
||||
pub const PARALLEL_FAN_IN_BEST_HEAD_SHA: &str = "parallel.fan_in.best_head_sha";
|
||||
|
||||
// --- Prefix constants (for filtering and dynamic keys) ---
|
||||
pub const GRAPH_PREFIX: &str = "graph.";
|
||||
pub const INTERNAL_PREFIX: &str = "internal.";
|
||||
pub const CURRENT_PREFIX: &str = "current";
|
||||
pub const THREAD_PREFIX: &str = "thread.";
|
||||
pub const RESPONSE_PREFIX: &str = "response.";
|
||||
pub const INTERNAL_RETRY_COUNT_PREFIX: &str = "internal.retry_count.";
|
||||
|
||||
// --- Helper functions for dynamic keys ---
|
||||
|
||||
#[must_use]
|
||||
pub fn response_key(node_id: &str) -> String {
|
||||
format!("{RESPONSE_PREFIX}{node_id}")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn thread_current_node_key(thread_id: &str) -> String {
|
||||
format!("{THREAD_PREFIX}{thread_id}.current_node")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn graph_attr_key(attr: &str) -> String {
|
||||
format!("{GRAPH_PREFIX}{attr}")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn retry_count_key(node_id: &str) -> String {
|
||||
format!("{INTERNAL_RETRY_COUNT_PREFIX}{node_id}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn response_key_formats_correctly() {
|
||||
assert_eq!(response_key("plan"), "response.plan");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_current_node_key_formats_correctly() {
|
||||
assert_eq!(
|
||||
thread_current_node_key("main"),
|
||||
"thread.main.current_node"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_attr_key_formats_correctly() {
|
||||
assert_eq!(graph_attr_key("goal"), "graph.goal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_count_key_formats_correctly() {
|
||||
assert_eq!(retry_count_key("plan"), "internal.retry_count.plan");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
pub mod keys;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
|
|
@ -113,6 +115,41 @@ impl Context {
|
|||
values.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Typed accessors ---
|
||||
|
||||
#[must_use]
|
||||
pub fn run_id(&self) -> String {
|
||||
self.get_string(keys::INTERNAL_RUN_ID, "unknown")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fidelity(&self) -> String {
|
||||
self.get_string(keys::INTERNAL_FIDELITY, "")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn preamble(&self) -> String {
|
||||
self.get_string(keys::CURRENT_PREAMBLE, "")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn thread_id(&self) -> Option<String> {
|
||||
self.get(keys::INTERNAL_THREAD_ID)
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn node_visit_count(&self) -> usize {
|
||||
self.get(keys::INTERNAL_NODE_VISIT_COUNT)
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(1) as usize
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_node_id(&self) -> String {
|
||||
self.get_string(keys::CURRENT_NODE, "")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -219,4 +256,89 @@ mod tests {
|
|||
let ctx = Context::default();
|
||||
assert!(ctx.snapshot().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_id_default() {
|
||||
let ctx = Context::new();
|
||||
assert_eq!(ctx.run_id(), "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_id_set() {
|
||||
let ctx = Context::new();
|
||||
ctx.set(keys::INTERNAL_RUN_ID, serde_json::json!("abc-123"));
|
||||
assert_eq!(ctx.run_id(), "abc-123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fidelity_default() {
|
||||
let ctx = Context::new();
|
||||
assert_eq!(ctx.fidelity(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fidelity_set() {
|
||||
let ctx = Context::new();
|
||||
ctx.set(keys::INTERNAL_FIDELITY, serde_json::json!("compact"));
|
||||
assert_eq!(ctx.fidelity(), "compact");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preamble_default() {
|
||||
let ctx = Context::new();
|
||||
assert_eq!(ctx.preamble(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preamble_set() {
|
||||
let ctx = Context::new();
|
||||
ctx.set(keys::CURRENT_PREAMBLE, serde_json::json!("hello"));
|
||||
assert_eq!(ctx.preamble(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_id_default() {
|
||||
let ctx = Context::new();
|
||||
assert_eq!(ctx.thread_id(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_id_null() {
|
||||
let ctx = Context::new();
|
||||
ctx.set(keys::INTERNAL_THREAD_ID, serde_json::Value::Null);
|
||||
assert_eq!(ctx.thread_id(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_id_set() {
|
||||
let ctx = Context::new();
|
||||
ctx.set(keys::INTERNAL_THREAD_ID, serde_json::json!("main"));
|
||||
assert_eq!(ctx.thread_id(), Some("main".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_visit_count_default() {
|
||||
let ctx = Context::new();
|
||||
assert_eq!(ctx.node_visit_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_visit_count_set() {
|
||||
let ctx = Context::new();
|
||||
ctx.set(keys::INTERNAL_NODE_VISIT_COUNT, serde_json::json!(3));
|
||||
assert_eq!(ctx.node_visit_count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_node_id_default() {
|
||||
let ctx = Context::new();
|
||||
assert_eq!(ctx.current_node_id(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_node_id_set() {
|
||||
let ctx = Context::new();
|
||||
ctx.set(keys::CURRENT_NODE, serde_json::json!("plan"));
|
||||
assert_eq!(ctx.current_node_id(), "plan");
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ use crate::artifact::{offload_large_values, sync_artifacts_to_env, ArtifactStore
|
|||
use crate::asset_snapshot;
|
||||
use crate::checkpoint::Checkpoint;
|
||||
use crate::condition::evaluate_condition;
|
||||
use crate::context;
|
||||
use crate::context::Context;
|
||||
use crate::error::{ArcError, FailureClass, FailureSignature, Result};
|
||||
use crate::event::{EventEmitter, WorkflowRunEvent};
|
||||
|
|
@ -311,10 +312,7 @@ pub fn node_dir(logs_root: &Path, node_id: &str, visit: usize) -> PathBuf {
|
|||
|
||||
/// Read the visit count from context, defaulting to 1 if not set.
|
||||
pub fn visit_from_context(context: &Context) -> usize {
|
||||
context
|
||||
.get("internal.node_visit_count")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(1) as usize
|
||||
context.node_visit_count()
|
||||
}
|
||||
|
||||
/// Write status.json for a completed node into {`logs_root}/nodes/{node_id}/status.json`.
|
||||
|
|
@ -820,11 +818,11 @@ impl WorkflowRunEngine {
|
|||
/// Mirror graph-level attributes into the context.
|
||||
fn mirror_graph_attributes(graph: &Graph, context: &Context) {
|
||||
if !graph.goal().is_empty() {
|
||||
context.set("graph.goal", serde_json::json!(graph.goal()));
|
||||
context.set(context::keys::GRAPH_GOAL, serde_json::json!(graph.goal()));
|
||||
}
|
||||
for (key, val) in &graph.attrs {
|
||||
context.set(
|
||||
format!("graph.{key}"),
|
||||
context::keys::graph_attr_key(key),
|
||||
serde_json::json!(val.to_string_value()),
|
||||
);
|
||||
}
|
||||
|
|
@ -1210,7 +1208,7 @@ impl WorkflowRunEngine {
|
|||
}
|
||||
}
|
||||
// Gap #6: Check if the checkpointed node used full fidelity
|
||||
if cp.context_values.get("internal.fidelity") == Some(&serde_json::json!("full")) {
|
||||
if cp.context_values.get(context::keys::INTERNAL_FIDELITY) == Some(&serde_json::json!("full")) {
|
||||
degrade_fidelity_on_resume = true;
|
||||
}
|
||||
} else if let Some(start) = start_at {
|
||||
|
|
@ -1232,10 +1230,10 @@ impl WorkflowRunEngine {
|
|||
}
|
||||
|
||||
// Store run_id and work_dir in context for handlers
|
||||
context.set("internal.run_id", serde_json::json!(run_id));
|
||||
context.set(context::keys::INTERNAL_RUN_ID, serde_json::json!(run_id));
|
||||
if let Some(GitCheckpointMode::Host(ref wd)) = config.git_checkpoint {
|
||||
context.set(
|
||||
"internal.work_dir",
|
||||
context::keys::INTERNAL_WORK_DIR,
|
||||
serde_json::json!(wd.to_string_lossy().as_ref()),
|
||||
);
|
||||
}
|
||||
|
|
@ -1384,16 +1382,16 @@ impl WorkflowRunEngine {
|
|||
fidelity = "summary:high".to_string();
|
||||
}
|
||||
degrade_fidelity_on_resume = false;
|
||||
context.set("internal.fidelity", serde_json::json!(&fidelity));
|
||||
context.set(context::keys::INTERNAL_FIDELITY, serde_json::json!(&fidelity));
|
||||
|
||||
// Preamble injection at execution time (spec 5.4 / 8.3): synthesize a
|
||||
// fidelity-appropriate preamble from runtime data for handlers to read
|
||||
if fidelity == "full" {
|
||||
context.set("current.preamble", serde_json::json!(""));
|
||||
context.set(context::keys::CURRENT_PREAMBLE, serde_json::json!(""));
|
||||
} else {
|
||||
let preamble =
|
||||
build_preamble(&fidelity, &context, graph, &completed_nodes, &node_outcomes);
|
||||
context.set("current.preamble", serde_json::json!(preamble));
|
||||
context.set(context::keys::CURRENT_PREAMBLE, serde_json::json!(preamble));
|
||||
}
|
||||
|
||||
// Thread context sharing: resolve thread ID and store in context for handlers
|
||||
|
|
@ -1401,18 +1399,18 @@ impl WorkflowRunEngine {
|
|||
resolve_thread_id(incoming_edge, node, graph, previous_node_id.as_deref());
|
||||
if let Some(ref tid) = resolved_thread_id {
|
||||
context.set(
|
||||
format!("thread.{tid}.current_node"),
|
||||
context::keys::thread_current_node_key(tid),
|
||||
serde_json::json!(&node.id),
|
||||
);
|
||||
context.set("internal.thread_id", serde_json::json!(tid));
|
||||
context.set(context::keys::INTERNAL_THREAD_ID, serde_json::json!(tid));
|
||||
} else {
|
||||
context.set("internal.thread_id", serde_json::Value::Null);
|
||||
context.set(context::keys::INTERNAL_THREAD_ID, serde_json::Value::Null);
|
||||
}
|
||||
|
||||
// Step 2: Execute node handler with retry policy
|
||||
let visit = *loop_state.node_visits.get(¤t_node_id).unwrap_or(&1);
|
||||
context.set("internal.node_visit_count", serde_json::json!(visit));
|
||||
context.set("current_node", serde_json::json!(&node.id));
|
||||
context.set(context::keys::INTERNAL_NODE_VISIT_COUNT, serde_json::json!(visit));
|
||||
context.set(context::keys::CURRENT_NODE, serde_json::json!(&node.id));
|
||||
let retry_policy = build_retry_policy(node, graph);
|
||||
|
||||
self.services.emitter.emit(&WorkflowRunEvent::StageStarted {
|
||||
|
|
@ -1505,7 +1503,7 @@ impl WorkflowRunEngine {
|
|||
// Gap #5: Track retry count per node
|
||||
node_retries.insert(node.id.clone(), attempts_used);
|
||||
context.set(
|
||||
format!("internal.retry_count.{}", node.id),
|
||||
context::keys::retry_count_key(&node.id),
|
||||
serde_json::json!(attempts_used),
|
||||
);
|
||||
|
||||
|
|
@ -1637,19 +1635,19 @@ impl WorkflowRunEngine {
|
|||
|
||||
// Step 4: Apply context updates from outcome
|
||||
context.apply_updates(&outcome.context_updates);
|
||||
context.set("outcome", serde_json::json!(outcome.status.to_string()));
|
||||
context.set(context::keys::OUTCOME, serde_json::json!(outcome.status.to_string()));
|
||||
context.set(
|
||||
"failure_class",
|
||||
context::keys::FAILURE_CLASS,
|
||||
serde_json::json!(outcome_failure_class.map_or(String::new(), |fc| fc.to_string())),
|
||||
);
|
||||
context.set(
|
||||
"failure_signature",
|
||||
context::keys::FAILURE_SIGNATURE,
|
||||
serde_json::json!(failure_sig
|
||||
.as_ref()
|
||||
.map_or(String::new(), |s| s.to_string())),
|
||||
);
|
||||
if let Some(ref pref) = outcome.preferred_label {
|
||||
context.set("preferred_label", serde_json::json!(pref));
|
||||
context.set(context::keys::PREFERRED_LABEL, serde_json::json!(pref));
|
||||
}
|
||||
|
||||
// Step 5: Select next edge (done before checkpoint so we can store next_node_id)
|
||||
|
|
@ -2762,7 +2760,7 @@ mod tests {
|
|||
// Verify checkpoint has graph.goal mirrored
|
||||
let cp = Checkpoint::load(&dir.path().join("checkpoint.json")).unwrap();
|
||||
assert_eq!(
|
||||
cp.context_values.get("graph.goal"),
|
||||
cp.context_values.get(context::keys::GRAPH_GOAL),
|
||||
Some(&serde_json::json!("Run tests"))
|
||||
);
|
||||
}
|
||||
|
|
@ -3035,7 +3033,7 @@ mod tests {
|
|||
// The checkpoint context should contain internal.fidelity
|
||||
let cp = Checkpoint::load(&dir.path().join("checkpoint.json")).unwrap();
|
||||
assert_eq!(
|
||||
cp.context_values.get("internal.fidelity"),
|
||||
cp.context_values.get(context::keys::INTERNAL_FIDELITY),
|
||||
Some(&serde_json::json!("compact"))
|
||||
);
|
||||
}
|
||||
|
|
@ -4543,7 +4541,7 @@ mod tests {
|
|||
// Check the checkpoint for the failure_signature context value
|
||||
let checkpoint_path = dir.path().join("checkpoint.json");
|
||||
let cp = Checkpoint::load(&checkpoint_path).unwrap();
|
||||
let sig_value = cp.context_values.get("failure_signature").unwrap();
|
||||
let sig_value = cp.context_values.get(context::keys::FAILURE_SIGNATURE).unwrap();
|
||||
let sig_str = sig_value.as_str().unwrap();
|
||||
assert!(
|
||||
sig_str.contains("work|deterministic|"),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::sync::Arc;
|
|||
use arc_agent::Sandbox;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::context::keys;
|
||||
use crate::context::Context;
|
||||
use crate::error::ArcError;
|
||||
use crate::event::EventEmitter;
|
||||
|
|
@ -188,7 +189,7 @@ impl Handler for AgentHandler {
|
|||
.filter(|p| !p.is_empty())
|
||||
.unwrap_or_else(|| node.label());
|
||||
let expanded = expand_variables(raw_prompt, graph)?;
|
||||
let preamble = context.get_string("current.preamble", "");
|
||||
let preamble = context.preamble();
|
||||
let prompt = if preamble.is_empty() {
|
||||
expanded
|
||||
} else {
|
||||
|
|
@ -202,9 +203,7 @@ impl Handler for AgentHandler {
|
|||
tokio::fs::write(stage_dir.join("prompt.md"), &prompt).await?;
|
||||
|
||||
// 3. Call LLM backend (agent loop)
|
||||
let thread_id = context
|
||||
.get("internal.thread_id")
|
||||
.and_then(|v| v.as_str().map(String::from));
|
||||
let thread_id = context.thread_id();
|
||||
let (response_text, stage_usage, backend_files_touched) =
|
||||
if let Some(backend) = &self.backend {
|
||||
let result = backend
|
||||
|
|
@ -253,13 +252,13 @@ impl Handler for AgentHandler {
|
|||
outcome.notes = Some(format!("Stage completed: {}", node.id));
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("last_stage".to_string(), serde_json::json!(node.id));
|
||||
.insert(keys::LAST_STAGE.to_string(), serde_json::json!(node.id));
|
||||
outcome.context_updates.insert(
|
||||
"last_response".to_string(),
|
||||
keys::LAST_RESPONSE.to_string(),
|
||||
serde_json::json!(truncate(&response_text, 200)),
|
||||
);
|
||||
outcome.context_updates.insert(
|
||||
format!("response.{}", node.id),
|
||||
keys::response_key(&node.id),
|
||||
serde_json::json!(&response_text),
|
||||
);
|
||||
|
||||
|
|
@ -395,12 +394,12 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("last_stage"),
|
||||
outcome.context_updates.get(keys::LAST_STAGE),
|
||||
Some(&serde_json::json!("step"))
|
||||
);
|
||||
assert!(outcome.context_updates.contains_key("last_response"));
|
||||
assert!(outcome.context_updates.contains_key(keys::LAST_RESPONSE));
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("response.step"),
|
||||
outcome.context_updates.get(&keys::response_key("step")),
|
||||
Some(&serde_json::json!("[Simulated] Response for stage: step"))
|
||||
);
|
||||
}
|
||||
|
|
@ -489,7 +488,7 @@ mod tests {
|
|||
let node = Node::new("work");
|
||||
let context = Context::new();
|
||||
// Simulate what the engine stores in internal.thread_id
|
||||
context.set("internal.thread_id", serde_json::json!("main"));
|
||||
context.set(keys::INTERNAL_THREAD_ID, serde_json::json!("main"));
|
||||
let graph = Graph::new("test");
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
|
|
@ -744,7 +743,7 @@ Some text in between.
|
|||
);
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
"current.preamble",
|
||||
keys::CURRENT_PREAMBLE,
|
||||
serde_json::json!("## Test Output\n10 passed, 0 failed"),
|
||||
);
|
||||
let graph = Graph::new("test");
|
||||
|
|
@ -834,7 +833,7 @@ Some text in between.
|
|||
);
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
"current.preamble",
|
||||
keys::CURRENT_PREAMBLE,
|
||||
serde_json::json!("## Script Output\nAll tests passed"),
|
||||
);
|
||||
let graph = Graph::new("test");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::path::Path;
|
|||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::context::keys;
|
||||
use crate::context::Context;
|
||||
use crate::error::ArcError;
|
||||
use crate::graph::{Graph, Node};
|
||||
|
|
@ -129,10 +130,10 @@ impl Handler for CommandHandler {
|
|||
let mut outcome = Outcome::success();
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("command.output".to_string(), serde_json::json!(stdout));
|
||||
.insert(keys::COMMAND_OUTPUT.to_string(), serde_json::json!(stdout));
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("command.stderr".to_string(), serde_json::json!(stderr));
|
||||
.insert(keys::COMMAND_STDERR.to_string(), serde_json::json!(stderr));
|
||||
outcome.notes = Some(format!("Script completed: {script}"));
|
||||
Ok(outcome)
|
||||
} else {
|
||||
|
|
@ -151,10 +152,10 @@ impl Handler for CommandHandler {
|
|||
let mut outcome = Outcome::fail_classify(reason);
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("command.output".to_string(), serde_json::json!(stdout));
|
||||
.insert(keys::COMMAND_OUTPUT.to_string(), serde_json::json!(stdout));
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("command.stderr".to_string(), serde_json::json!(stderr));
|
||||
.insert(keys::COMMAND_STDERR.to_string(), serde_json::json!(stderr));
|
||||
Ok(outcome)
|
||||
}
|
||||
}
|
||||
|
|
@ -219,9 +220,9 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
assert!(outcome.notes.as_deref().unwrap().contains("echo hello"));
|
||||
let command_output = outcome.context_updates.get("command.output").unwrap();
|
||||
let command_output = outcome.context_updates.get(keys::COMMAND_OUTPUT).unwrap();
|
||||
assert!(command_output.as_str().unwrap().contains("hello"));
|
||||
let command_stderr = outcome.context_updates.get("command.stderr").unwrap();
|
||||
let command_stderr = outcome.context_updates.get(keys::COMMAND_STDERR).unwrap();
|
||||
assert_eq!(command_stderr.as_str().unwrap(), "");
|
||||
}
|
||||
|
||||
|
|
@ -486,7 +487,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
let command_output = outcome.context_updates.get("command.output").unwrap();
|
||||
let command_output = outcome.context_updates.get(keys::COMMAND_OUTPUT).unwrap();
|
||||
assert!(command_output
|
||||
.as_str()
|
||||
.unwrap()
|
||||
|
|
@ -560,7 +561,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
let command_output = outcome.context_updates.get("command.output").unwrap();
|
||||
let command_output = outcome.context_updates.get(keys::COMMAND_OUTPUT).unwrap();
|
||||
assert!(command_output.as_str().unwrap().contains("legacy"));
|
||||
}
|
||||
|
||||
|
|
@ -581,7 +582,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
let command_stderr = outcome.context_updates.get("command.stderr").unwrap();
|
||||
let command_stderr = outcome.context_updates.get(keys::COMMAND_STDERR).unwrap();
|
||||
assert!(
|
||||
command_stderr.as_str().unwrap().contains("err"),
|
||||
"command.stderr should contain 'err', got: {:?}",
|
||||
|
|
@ -606,7 +607,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
assert!(outcome.context_updates.contains_key("command.output"));
|
||||
assert!(outcome.context_updates.contains_key(keys::COMMAND_OUTPUT));
|
||||
assert!(
|
||||
!outcome.context_updates.contains_key("tool.output"),
|
||||
"tool.output should not be emitted"
|
||||
|
|
@ -679,7 +680,7 @@ mod tests {
|
|||
assert_eq!(outcome.status, StageStatus::Fail);
|
||||
let command_output = outcome
|
||||
.context_updates
|
||||
.get("command.output")
|
||||
.get(keys::COMMAND_OUTPUT)
|
||||
.expect("command.output should be set on failure");
|
||||
assert!(
|
||||
command_output.as_str().unwrap().contains("build output"),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||
use arc_agent::Sandbox;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::context::keys;
|
||||
use crate::context::Context;
|
||||
use crate::error::ArcError;
|
||||
use crate::event::EventEmitter;
|
||||
|
|
@ -35,7 +36,7 @@ impl Handler for FanInHandler {
|
|||
logs_root: &Path,
|
||||
services: &EngineServices,
|
||||
) -> Result<Outcome, ArcError> {
|
||||
let results = context.get("parallel.results");
|
||||
let results = context.get(keys::PARALLEL_RESULTS);
|
||||
let Some(results) = results else {
|
||||
return Ok(Outcome::fail_deterministic(
|
||||
"No parallel results to evaluate",
|
||||
|
|
@ -99,16 +100,16 @@ impl Handler for FanInHandler {
|
|||
|
||||
let mut outcome = Outcome::success();
|
||||
outcome.context_updates.insert(
|
||||
"parallel.fan_in.best_id".to_string(),
|
||||
keys::PARALLEL_FAN_IN_BEST_ID.to_string(),
|
||||
serde_json::json!(best.id),
|
||||
);
|
||||
outcome.context_updates.insert(
|
||||
"parallel.fan_in.best_outcome".to_string(),
|
||||
keys::PARALLEL_FAN_IN_BEST_OUTCOME.to_string(),
|
||||
serde_json::json!(best.status),
|
||||
);
|
||||
if let Some(ref sha) = best_head_sha {
|
||||
outcome.context_updates.insert(
|
||||
"parallel.fan_in.best_head_sha".to_string(),
|
||||
keys::PARALLEL_FAN_IN_BEST_HEAD_SHA.to_string(),
|
||||
serde_json::json!(sha),
|
||||
);
|
||||
}
|
||||
|
|
@ -234,7 +235,7 @@ async fn llm_evaluate(
|
|||
// If the backend returned a full Outcome, extract best_id from context_updates
|
||||
let best_id = outcome
|
||||
.context_updates
|
||||
.get("parallel.fan_in.best_id")
|
||||
.get(keys::PARALLEL_FAN_IN_BEST_ID)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.or_else(|| outcome.notes.clone())
|
||||
|
|
@ -330,7 +331,7 @@ mod tests {
|
|||
let node = Node::new("fan_in");
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
"parallel.results",
|
||||
keys::PARALLEL_RESULTS,
|
||||
serde_json::json!([
|
||||
{"id": "branch_a", "status": "fail"},
|
||||
{"id": "branch_b", "status": "success"},
|
||||
|
|
@ -345,7 +346,7 @@ mod tests {
|
|||
.unwrap();
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("parallel.fan_in.best_id"),
|
||||
outcome.context_updates.get(keys::PARALLEL_FAN_IN_BEST_ID),
|
||||
Some(&serde_json::json!("branch_b"))
|
||||
);
|
||||
}
|
||||
|
|
@ -356,7 +357,7 @@ mod tests {
|
|||
let node = Node::new("fan_in");
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
"parallel.results",
|
||||
keys::PARALLEL_RESULTS,
|
||||
serde_json::json!([
|
||||
{"id": "c", "status": "success"},
|
||||
{"id": "a", "status": "success"},
|
||||
|
|
@ -371,7 +372,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("parallel.fan_in.best_id"),
|
||||
outcome.context_updates.get(keys::PARALLEL_FAN_IN_BEST_ID),
|
||||
Some(&serde_json::json!("a"))
|
||||
);
|
||||
}
|
||||
|
|
@ -394,7 +395,7 @@ mod tests {
|
|||
);
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
"parallel.results",
|
||||
keys::PARALLEL_RESULTS,
|
||||
serde_json::json!([
|
||||
{"id": "branch_a", "status": "success"},
|
||||
{"id": "branch_b", "status": "fail"},
|
||||
|
|
@ -410,7 +411,7 @@ mod tests {
|
|||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
// Should still pick branch_a via heuristic (success beats fail)
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("parallel.fan_in.best_id"),
|
||||
outcome.context_updates.get(keys::PARALLEL_FAN_IN_BEST_ID),
|
||||
Some(&serde_json::json!("branch_a"))
|
||||
);
|
||||
}
|
||||
|
|
@ -451,7 +452,7 @@ mod tests {
|
|||
);
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
"parallel.results",
|
||||
keys::PARALLEL_RESULTS,
|
||||
serde_json::json!([
|
||||
{"id": "branch_a", "status": "success"},
|
||||
{"id": "branch_b", "status": "success"},
|
||||
|
|
@ -467,7 +468,7 @@ mod tests {
|
|||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
// LLM chose branch_b
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("parallel.fan_in.best_id"),
|
||||
outcome.context_updates.get(keys::PARALLEL_FAN_IN_BEST_ID),
|
||||
Some(&serde_json::json!("branch_b"))
|
||||
);
|
||||
|
||||
|
|
@ -489,7 +490,7 @@ mod tests {
|
|||
let node = Node::new("fan_in");
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
"parallel.results",
|
||||
keys::PARALLEL_RESULTS,
|
||||
serde_json::json!([
|
||||
{"id": "branch_a", "status": "fail"},
|
||||
{"id": "branch_b", "status": "fail"},
|
||||
|
|
@ -516,7 +517,7 @@ mod tests {
|
|||
let node = Node::new("fan_in");
|
||||
let context = Context::new();
|
||||
context.set(
|
||||
"parallel.results",
|
||||
keys::PARALLEL_RESULTS,
|
||||
serde_json::json!([
|
||||
{"id": "branch_a", "status": "success", "score": 0.5},
|
||||
{"id": "branch_b", "status": "success", "score": 0.9},
|
||||
|
|
@ -533,7 +534,7 @@ mod tests {
|
|||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
// branch_b has highest score
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("parallel.fan_in.best_id"),
|
||||
outcome.context_updates.get(keys::PARALLEL_FAN_IN_BEST_ID),
|
||||
Some(&serde_json::json!("branch_b"))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::time::Instant;
|
|||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::context::keys;
|
||||
use crate::context::Context;
|
||||
use crate::error::ArcError;
|
||||
use crate::event::{EventEmitter, WorkflowRunEvent};
|
||||
|
|
@ -204,15 +205,15 @@ impl Handler for HumanHandler {
|
|||
let mut outcome = Outcome::success();
|
||||
outcome.suggested_next_ids = vec![freeform_to.clone()];
|
||||
outcome.context_updates.insert(
|
||||
"human.gate.selected".to_string(),
|
||||
keys::HUMAN_GATE_SELECTED.to_string(),
|
||||
serde_json::json!("freeform"),
|
||||
);
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("human.gate.label".to_string(), serde_json::json!(text));
|
||||
.insert(keys::HUMAN_GATE_LABEL.to_string(), serde_json::json!(text));
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("human.gate.text".to_string(), serde_json::json!(text));
|
||||
.insert(keys::HUMAN_GATE_TEXT.to_string(), serde_json::json!(text));
|
||||
return Ok(outcome);
|
||||
}
|
||||
|
||||
|
|
@ -231,10 +232,10 @@ fn make_choice_outcome(key: &str, label: &str, to: &str) -> Outcome {
|
|||
outcome.suggested_next_ids = vec![to.to_string()];
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("human.gate.selected".to_string(), serde_json::json!(key));
|
||||
.insert(keys::HUMAN_GATE_SELECTED.to_string(), serde_json::json!(key));
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("human.gate.label".to_string(), serde_json::json!(label));
|
||||
.insert(keys::HUMAN_GATE_LABEL.to_string(), serde_json::json!(label));
|
||||
outcome
|
||||
}
|
||||
|
||||
|
|
@ -362,7 +363,7 @@ mod tests {
|
|||
assert_eq!(outcome.status, crate::outcome::StageStatus::Success);
|
||||
// Auto-approve picks first option key "A"
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("human.gate.selected"),
|
||||
outcome.context_updates.get(keys::HUMAN_GATE_SELECTED),
|
||||
Some(&serde_json::json!("A"))
|
||||
);
|
||||
assert_eq!(outcome.suggested_next_ids, vec!["approve"]);
|
||||
|
|
@ -418,7 +419,7 @@ mod tests {
|
|||
assert_eq!(outcome.status, crate::outcome::StageStatus::Success);
|
||||
assert_eq!(outcome.suggested_next_ids, vec!["freeform_target"]);
|
||||
assert_eq!(
|
||||
outcome.context_updates.get("human.gate.text"),
|
||||
outcome.context_updates.get(keys::HUMAN_GATE_TEXT),
|
||||
Some(&serde_json::json!("custom input"))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,17 +128,11 @@ impl Handler for SubWorkflowHandler {
|
|||
};
|
||||
|
||||
// Build child RunConfig
|
||||
let visit = context
|
||||
.get("internal.node_visit")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(1);
|
||||
let visit = context.node_visit_count() as u64;
|
||||
let child_logs = logs_root.join(format!("nodes/{}_{visit}/child", node.id));
|
||||
let _ = std::fs::create_dir_all(&child_logs);
|
||||
|
||||
let parent_run_id = context
|
||||
.get("internal.run_id")
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default();
|
||||
let parent_run_id = context.run_id();
|
||||
let cancel_token = Arc::new(AtomicBool::new(false));
|
||||
let child_cancel = Arc::clone(&cancel_token);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use arc_agent::Sandbox;
|
|||
use async_trait::async_trait;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
use crate::context::keys;
|
||||
use crate::context::Context;
|
||||
use crate::engine::GitCheckpointMode;
|
||||
use crate::error::ArcError;
|
||||
|
|
@ -317,7 +318,7 @@ impl Handler for ParallelHandler {
|
|||
ArcError::handler(format!("worktree setup join error: {e}"))
|
||||
})??;
|
||||
branch_context.set(
|
||||
"internal.work_dir",
|
||||
keys::INTERNAL_WORK_DIR,
|
||||
serde_json::json!(wt_path.to_string_lossy().as_ref()),
|
||||
);
|
||||
let env: Arc<dyn Sandbox> =
|
||||
|
|
@ -366,7 +367,7 @@ impl Handler for ParallelHandler {
|
|||
"failed to reset remote worktree {wt_path_str}"
|
||||
)));
|
||||
}
|
||||
branch_context.set("internal.work_dir", serde_json::json!(&wt_path_str));
|
||||
branch_context.set(keys::INTERNAL_WORK_DIR, serde_json::json!(&wt_path_str));
|
||||
let env: Arc<dyn Sandbox> = Arc::new(WorktreeSandbox {
|
||||
inner: Arc::clone(&services.sandbox),
|
||||
worktree_dir: wt_path_str.clone(),
|
||||
|
|
@ -637,8 +638,8 @@ impl Handler for ParallelHandler {
|
|||
entry
|
||||
})
|
||||
.collect();
|
||||
context.set("parallel.results", serde_json::json!(results_json));
|
||||
context.set("parallel.branch_count", serde_json::json!(total));
|
||||
context.set(keys::PARALLEL_RESULTS, serde_json::json!(results_json));
|
||||
context.set(keys::PARALLEL_BRANCH_COUNT, serde_json::json!(total));
|
||||
|
||||
let visit = crate::engine::visit_from_context(context);
|
||||
let node_dir = crate::engine::node_dir(logs_root, &node.id, visit);
|
||||
|
|
@ -815,7 +816,7 @@ mod tests {
|
|||
assert!(outcome.notes.as_deref().unwrap().contains("2 branches"));
|
||||
|
||||
// Check context was set
|
||||
let results = context.get("parallel.results");
|
||||
let results = context.get(keys::PARALLEL_RESULTS);
|
||||
assert!(results.is_some());
|
||||
|
||||
// Check parallel_results.json was written
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::path::Path;
|
|||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::context::keys;
|
||||
use crate::context::Context;
|
||||
use crate::error::ArcError;
|
||||
use crate::graph::{Graph, Node};
|
||||
|
|
@ -40,7 +41,7 @@ impl Handler for PromptHandler {
|
|||
.filter(|p| !p.is_empty())
|
||||
.unwrap_or_else(|| node.label());
|
||||
let expanded = expand_variables(raw_prompt, graph)?;
|
||||
let preamble = context.get_string("current.preamble", "");
|
||||
let preamble = context.preamble();
|
||||
let prompt = if preamble.is_empty() {
|
||||
expanded
|
||||
} else {
|
||||
|
|
@ -92,13 +93,13 @@ impl Handler for PromptHandler {
|
|||
outcome.notes = Some(format!("Stage completed: {}", node.id));
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("last_stage".to_string(), serde_json::json!(node.id));
|
||||
.insert(keys::LAST_STAGE.to_string(), serde_json::json!(node.id));
|
||||
outcome.context_updates.insert(
|
||||
"last_response".to_string(),
|
||||
keys::LAST_RESPONSE.to_string(),
|
||||
serde_json::json!(truncate(&response_text, 200)),
|
||||
);
|
||||
outcome.context_updates.insert(
|
||||
format!("response.{}", node.id),
|
||||
keys::response_key(&node.id),
|
||||
serde_json::json!(&response_text),
|
||||
);
|
||||
|
||||
|
|
@ -277,7 +278,7 @@ mod tests {
|
|||
AttrValue::String("Classify this".to_string()),
|
||||
);
|
||||
let context = Context::new();
|
||||
context.set("current.preamble", serde_json::json!("Prior output here"));
|
||||
context.set(keys::CURRENT_PREAMBLE, serde_json::json!("Prior output here"));
|
||||
let graph = Graph::new("test");
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::artifact::{artifact_path, format_artifact_reference};
|
||||
use crate::context::keys;
|
||||
use crate::context::Context;
|
||||
use crate::graph::{is_llm_handler_type, Graph, Node};
|
||||
use crate::outcome::Outcome;
|
||||
|
|
@ -23,7 +24,7 @@ pub fn build_preamble(
|
|||
node_outcomes: &HashMap<String, Outcome>,
|
||||
) -> String {
|
||||
let goal = graph.goal();
|
||||
let run_id = context.get_string("internal.run_id", "unknown");
|
||||
let run_id = context.run_id();
|
||||
|
||||
match fidelity {
|
||||
"truncate" => {
|
||||
|
|
@ -69,15 +70,15 @@ pub fn build_preamble(
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn is_context_key_excluded(key: &str) -> bool {
|
||||
key.starts_with("internal.")
|
||||
|| key.starts_with("current")
|
||||
|| key.starts_with("graph.")
|
||||
|| key.starts_with("thread.")
|
||||
|| key.starts_with("response.")
|
||||
|| key == "outcome"
|
||||
|| key == "last_stage"
|
||||
|| key == "last_response"
|
||||
|| key == "preferred_label"
|
||||
key.starts_with(keys::INTERNAL_PREFIX)
|
||||
|| key.starts_with(keys::CURRENT_PREFIX)
|
||||
|| key.starts_with(keys::GRAPH_PREFIX)
|
||||
|| key.starts_with(keys::THREAD_PREFIX)
|
||||
|| key.starts_with(keys::RESPONSE_PREFIX)
|
||||
|| key == keys::OUTCOME
|
||||
|| key == keys::LAST_STAGE
|
||||
|| key == keys::LAST_RESPONSE
|
||||
|| key == keys::PREFERRED_LABEL
|
||||
}
|
||||
|
||||
fn format_value(val: &serde_json::Value) -> String {
|
||||
|
|
@ -101,11 +102,11 @@ fn format_token_count(tokens: i64) -> String {
|
|||
/// handler-specific details, so they can be skipped in the trailing context section.
|
||||
fn stage_rendered_keys(node_id: &str, outcome: &Outcome) -> HashSet<String> {
|
||||
let candidates = [
|
||||
"command.output".to_string(),
|
||||
"command.stderr".to_string(),
|
||||
"last_stage".to_string(),
|
||||
"last_response".to_string(),
|
||||
format!("response.{node_id}"),
|
||||
keys::COMMAND_OUTPUT.to_string(),
|
||||
keys::COMMAND_STDERR.to_string(),
|
||||
keys::LAST_STAGE.to_string(),
|
||||
keys::LAST_RESPONSE.to_string(),
|
||||
keys::response_key(node_id),
|
||||
];
|
||||
candidates
|
||||
.into_iter()
|
||||
|
|
@ -133,7 +134,7 @@ fn render_compact_stage_details(
|
|||
lines.push(format!(" - Script: `{cmd}`"));
|
||||
}
|
||||
}
|
||||
if let Some(stdout_val) = outcome.context_updates.get("command.output") {
|
||||
if let Some(stdout_val) = outcome.context_updates.get(keys::COMMAND_OUTPUT) {
|
||||
let stdout = format_value(stdout_val);
|
||||
if stdout.trim().is_empty() {
|
||||
lines.push(" - Stdout: (empty)".to_string());
|
||||
|
|
@ -144,7 +145,7 @@ fn render_compact_stage_details(
|
|||
lines.push(" ```".to_string());
|
||||
}
|
||||
}
|
||||
if let Some(stderr_val) = outcome.context_updates.get("command.stderr") {
|
||||
if let Some(stderr_val) = outcome.context_updates.get(keys::COMMAND_STDERR) {
|
||||
let stderr = format_value(stderr_val);
|
||||
if stderr.trim().is_empty() {
|
||||
lines.push(" - Stderr: (empty)".to_string());
|
||||
|
|
@ -203,7 +204,7 @@ fn render_summary_high_stage_section(
|
|||
lines.push(format!("- Script: `{cmd}`"));
|
||||
}
|
||||
}
|
||||
if let Some(stdout_val) = outcome.context_updates.get("command.output") {
|
||||
if let Some(stdout_val) = outcome.context_updates.get(keys::COMMAND_OUTPUT) {
|
||||
if let Some(path) = artifact_path(stdout_val) {
|
||||
lines.push(format!("- Stdout: {}", format_artifact_reference(path)));
|
||||
} else {
|
||||
|
|
@ -218,7 +219,7 @@ fn render_summary_high_stage_section(
|
|||
}
|
||||
}
|
||||
}
|
||||
if let Some(stderr_val) = outcome.context_updates.get("command.stderr") {
|
||||
if let Some(stderr_val) = outcome.context_updates.get(keys::COMMAND_STDERR) {
|
||||
if let Some(path) = artifact_path(stderr_val) {
|
||||
lines.push(format!("- Stderr: {}", format_artifact_reference(path)));
|
||||
} else {
|
||||
|
|
@ -250,7 +251,7 @@ fn render_summary_high_stage_section(
|
|||
));
|
||||
}
|
||||
// Include full response from context_updates (or artifact pointer)
|
||||
if let Some(resp_val) = outcome.context_updates.get(&format!("response.{node_id}")) {
|
||||
if let Some(resp_val) = outcome.context_updates.get(&keys::response_key(node_id)) {
|
||||
if let Some(path) = artifact_path(resp_val) {
|
||||
lines.push(format!("- Response: {}", format_artifact_reference(path)));
|
||||
} else {
|
||||
|
|
@ -532,7 +533,7 @@ mod tests {
|
|||
AttrValue::String("Fix the login bug".to_string()),
|
||||
);
|
||||
let context = Context::new();
|
||||
context.set("internal.run_id", serde_json::json!("abc-123"));
|
||||
context.set(keys::INTERNAL_RUN_ID, serde_json::json!("abc-123"));
|
||||
let completed_nodes: Vec<String> = Vec::new();
|
||||
let node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
|
||||
|
|
@ -596,7 +597,7 @@ mod tests {
|
|||
AttrValue::String("Deploy app".to_string()),
|
||||
);
|
||||
let context = Context::new();
|
||||
context.set("internal.run_id", serde_json::json!("run-456"));
|
||||
context.set(keys::INTERNAL_RUN_ID, serde_json::json!("run-456"));
|
||||
let completed_nodes = vec!["plan".to_string(), "code".to_string()];
|
||||
let mut node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
node_outcomes.insert("plan".to_string(), Outcome::success());
|
||||
|
|
@ -637,7 +638,7 @@ mod tests {
|
|||
fn build_preamble_compact_includes_context_values() {
|
||||
let graph = Graph::new("test");
|
||||
let context = Context::new();
|
||||
context.set("graph.goal", serde_json::json!("Build it"));
|
||||
context.set(keys::GRAPH_GOAL, serde_json::json!("Build it"));
|
||||
context.set("user.name", serde_json::json!("alice"));
|
||||
let completed_nodes: Vec<String> = Vec::new();
|
||||
let node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
|
|
@ -665,15 +666,15 @@ mod tests {
|
|||
fn build_preamble_compact_excludes_internal_keys() {
|
||||
let graph = Graph::new("test");
|
||||
let context = Context::new();
|
||||
context.set("internal.fidelity", serde_json::json!("compact"));
|
||||
context.set("internal.retry_count.plan", serde_json::json!(1));
|
||||
context.set("current_node", serde_json::json!("work"));
|
||||
context.set("graph.default_fidelity", serde_json::json!("compact"));
|
||||
context.set(keys::INTERNAL_FIDELITY, serde_json::json!("compact"));
|
||||
context.set(&keys::retry_count_key("plan"), serde_json::json!(1));
|
||||
context.set(keys::CURRENT_NODE, serde_json::json!("work"));
|
||||
context.set(&keys::graph_attr_key("default_fidelity"), serde_json::json!("compact"));
|
||||
context.set("thread.main.current_node", serde_json::json!("work"));
|
||||
context.set("response.plan", serde_json::json!("some response"));
|
||||
context.set("last_stage", serde_json::json!("plan"));
|
||||
context.set("last_response", serde_json::json!("resp"));
|
||||
context.set("preferred_label", serde_json::json!("success"));
|
||||
context.set(&keys::response_key("plan"), serde_json::json!("some response"));
|
||||
context.set(keys::LAST_STAGE, serde_json::json!("plan"));
|
||||
context.set(keys::LAST_RESPONSE, serde_json::json!("resp"));
|
||||
context.set(keys::PREFERRED_LABEL, serde_json::json!("success"));
|
||||
context.set("user.name", serde_json::json!("bob"));
|
||||
let completed_nodes: Vec<String> = Vec::new();
|
||||
let node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
|
|
@ -777,12 +778,12 @@ mod tests {
|
|||
let mut node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
let mut outcome = Outcome::success();
|
||||
outcome.context_updates.insert(
|
||||
"command.output".to_string(),
|
||||
keys::COMMAND_OUTPUT.to_string(),
|
||||
serde_json::json!("10 passed\n"),
|
||||
);
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("command.stderr".to_string(), serde_json::json!(""));
|
||||
.insert(keys::COMMAND_STDERR.to_string(), serde_json::json!(""));
|
||||
node_outcomes.insert("run_tests".to_string(), outcome);
|
||||
|
||||
let preamble = build_preamble(
|
||||
|
|
@ -856,10 +857,10 @@ mod tests {
|
|||
fn compact_context_excludes_engine_keys() {
|
||||
let graph = Graph::new("test");
|
||||
let context = Context::new();
|
||||
context.set("graph.default_fidelity", serde_json::json!("compact"));
|
||||
context.set(&keys::graph_attr_key("default_fidelity"), serde_json::json!("compact"));
|
||||
context.set("thread.main.current_node", serde_json::json!("work"));
|
||||
context.set("response.plan", serde_json::json!("some LLM response"));
|
||||
context.set("last_stage", serde_json::json!("plan"));
|
||||
context.set(&keys::response_key("plan"), serde_json::json!("some LLM response"));
|
||||
context.set(keys::LAST_STAGE, serde_json::json!("plan"));
|
||||
context.set("user.preference", serde_json::json!("dark"));
|
||||
let completed_nodes: Vec<String> = Vec::new();
|
||||
let node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
|
|
@ -910,17 +911,17 @@ mod tests {
|
|||
|
||||
let context = Context::new();
|
||||
// command.output is set in context (the engine copies context_updates to context)
|
||||
context.set("command.output", serde_json::json!("hi\n"));
|
||||
context.set("command.stderr", serde_json::json!(""));
|
||||
context.set(keys::COMMAND_OUTPUT, serde_json::json!("hi\n"));
|
||||
context.set(keys::COMMAND_STDERR, serde_json::json!(""));
|
||||
let completed_nodes = vec!["step".to_string()];
|
||||
let mut node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
let mut outcome = Outcome::success();
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("command.output".to_string(), serde_json::json!("hi\n"));
|
||||
.insert(keys::COMMAND_OUTPUT.to_string(), serde_json::json!("hi\n"));
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("command.stderr".to_string(), serde_json::json!(""));
|
||||
.insert(keys::COMMAND_STDERR.to_string(), serde_json::json!(""));
|
||||
node_outcomes.insert("step".to_string(), outcome);
|
||||
|
||||
let preamble = build_preamble(
|
||||
|
|
@ -1037,7 +1038,7 @@ mod tests {
|
|||
let mut node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
let mut outcome = Outcome::fail_classify("exit code 1");
|
||||
outcome.context_updates.insert(
|
||||
"command.output".to_string(),
|
||||
keys::COMMAND_OUTPUT.to_string(),
|
||||
serde_json::json!("test failed"),
|
||||
);
|
||||
node_outcomes.insert("run_tests".to_string(), outcome);
|
||||
|
|
@ -1214,12 +1215,12 @@ mod tests {
|
|||
let mut node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
let mut outcome = Outcome::success();
|
||||
outcome.context_updates.insert(
|
||||
"command.output".to_string(),
|
||||
keys::COMMAND_OUTPUT.to_string(),
|
||||
serde_json::json!("All tests passed\n"),
|
||||
);
|
||||
outcome
|
||||
.context_updates
|
||||
.insert("command.stderr".to_string(), serde_json::json!(""));
|
||||
.insert(keys::COMMAND_STDERR.to_string(), serde_json::json!(""));
|
||||
node_outcomes.insert("run_tests".to_string(), outcome);
|
||||
|
||||
let preamble = build_preamble(
|
||||
|
|
@ -1355,7 +1356,7 @@ mod tests {
|
|||
fn build_preamble_summary_high_includes_context_values() {
|
||||
let graph = Graph::new("test");
|
||||
let context = Context::new();
|
||||
context.set("graph.goal", serde_json::json!("Build"));
|
||||
context.set(keys::GRAPH_GOAL, serde_json::json!("Build"));
|
||||
context.set("user.name", serde_json::json!("alice"));
|
||||
let completed_nodes: Vec<String> = Vec::new();
|
||||
let node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
|
|
@ -1427,11 +1428,11 @@ mod tests {
|
|||
let mut node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
let mut outcome = Outcome::success();
|
||||
outcome.context_updates.insert(
|
||||
"command.output".to_string(),
|
||||
keys::COMMAND_OUTPUT.to_string(),
|
||||
serde_json::json!("All tests passed\n"),
|
||||
);
|
||||
outcome.context_updates.insert(
|
||||
"command.stderr".to_string(),
|
||||
keys::COMMAND_STDERR.to_string(),
|
||||
serde_json::json!("warning: unused var\n"),
|
||||
);
|
||||
node_outcomes.insert("run_tests".to_string(), outcome);
|
||||
|
|
@ -1487,7 +1488,7 @@ mod tests {
|
|||
});
|
||||
outcome.files_touched = vec!["src/lib.rs".to_string()];
|
||||
outcome.context_updates.insert(
|
||||
"response.report".to_string(),
|
||||
keys::response_key("report"),
|
||||
serde_json::json!("The tests all pass successfully."),
|
||||
);
|
||||
node_outcomes.insert("report".to_string(), outcome);
|
||||
|
|
@ -1605,21 +1606,21 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn is_context_key_excluded_checks() {
|
||||
assert!(is_context_key_excluded("internal.fidelity"));
|
||||
assert!(is_context_key_excluded("internal.retry_count.plan"));
|
||||
assert!(is_context_key_excluded("current_node"));
|
||||
assert!(is_context_key_excluded("current.preamble"));
|
||||
assert!(is_context_key_excluded("graph.default_fidelity"));
|
||||
assert!(is_context_key_excluded("graph.goal"));
|
||||
assert!(is_context_key_excluded("thread.main.current_node"));
|
||||
assert!(is_context_key_excluded("response.plan"));
|
||||
assert!(is_context_key_excluded("outcome"));
|
||||
assert!(is_context_key_excluded("last_stage"));
|
||||
assert!(is_context_key_excluded("last_response"));
|
||||
assert!(is_context_key_excluded("preferred_label"));
|
||||
assert!(is_context_key_excluded(keys::INTERNAL_FIDELITY));
|
||||
assert!(is_context_key_excluded(&keys::retry_count_key("plan")));
|
||||
assert!(is_context_key_excluded(keys::CURRENT_NODE));
|
||||
assert!(is_context_key_excluded(keys::CURRENT_PREAMBLE));
|
||||
assert!(is_context_key_excluded(&keys::graph_attr_key("default_fidelity")));
|
||||
assert!(is_context_key_excluded(keys::GRAPH_GOAL));
|
||||
assert!(is_context_key_excluded(&keys::thread_current_node_key("main")));
|
||||
assert!(is_context_key_excluded(&keys::response_key("plan")));
|
||||
assert!(is_context_key_excluded(keys::OUTCOME));
|
||||
assert!(is_context_key_excluded(keys::LAST_STAGE));
|
||||
assert!(is_context_key_excluded(keys::LAST_RESPONSE));
|
||||
assert!(is_context_key_excluded(keys::PREFERRED_LABEL));
|
||||
assert!(!is_context_key_excluded("user.name"));
|
||||
assert!(!is_context_key_excluded("custom.key"));
|
||||
assert!(!is_context_key_excluded("command.output"));
|
||||
assert!(!is_context_key_excluded(keys::COMMAND_OUTPUT));
|
||||
}
|
||||
|
||||
// --- unknown fidelity mode ---
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue