diff --git a/lib/crates/fabro-core/src/context.rs b/lib/crates/fabro-core/src/context.rs index 265d23342..3e93d2244 100644 --- a/lib/crates/fabro-core/src/context.rs +++ b/lib/crates/fabro-core/src/context.rs @@ -3,80 +3,28 @@ use std::sync::{Arc, RwLock}; use serde_json::Value; -pub trait ContextStore: Send + Sync { - fn set(&self, key: String, value: Value); - fn get(&self, key: &str) -> Option; - fn snapshot(&self) -> HashMap; - fn fork(&self) -> Arc; -} - -pub struct InMemoryStore { - data: RwLock>, -} - -impl InMemoryStore { - pub fn new() -> Self { - Self { - data: RwLock::new(HashMap::new()), - } - } -} - -impl Default for InMemoryStore { - fn default() -> Self { - Self::new() - } -} - -impl ContextStore for InMemoryStore { - fn set(&self, key: String, value: Value) { - self.data.write().unwrap().insert(key, value); - } - - fn get(&self, key: &str) -> Option { - self.data.read().unwrap().get(key).cloned() - } - - fn snapshot(&self) -> HashMap { - self.data.read().unwrap().clone() - } - - fn fork(&self) -> Arc { - let cloned = self.data.read().unwrap().clone(); - Arc::new(InMemoryStore { - data: RwLock::new(cloned), - }) - } -} - -#[derive(Clone)] +#[derive(Clone, Default)] pub struct Context { - store: Arc, -} - -impl Default for Context { - fn default() -> Self { - Self::new() - } + values: Arc>>, } impl Context { pub fn new() -> Self { + Self::default() + } + + pub fn from_values(values: HashMap) -> Self { Self { - store: Arc::new(InMemoryStore::new()), + values: Arc::new(RwLock::new(values)), } } - pub fn with_store(store: Arc) -> Self { - Self { store } - } - pub fn set(&self, key: impl Into, value: Value) { - self.store.set(key.into(), value); + self.values.write().unwrap().insert(key.into(), value); } pub fn get(&self, key: &str) -> Option { - self.store.get(key) + self.values.read().unwrap().get(key).cloned() } pub fn get_string(&self, key: &str, default: &str) -> String { @@ -86,18 +34,21 @@ impl Context { } pub fn apply_updates(&self, updates: &HashMap) { + let mut values = self.values.write().unwrap(); for (k, v) in updates { - self.store.set(k.clone(), v.clone()); + values.insert(k.clone(), v.clone()); } } pub fn snapshot(&self) -> HashMap { - self.store.snapshot() + self.values.read().unwrap().clone() } - pub fn clone_context(&self) -> Self { + /// Deep copy for parallel branch isolation. + /// `.clone()` shares state (Arc clone); `.fork()` creates an independent copy. + pub fn fork(&self) -> Self { Self { - store: self.store.fork(), + values: Arc::new(RwLock::new(self.snapshot())), } } @@ -117,36 +68,6 @@ impl Context { mod tests { use super::*; use serde_json::json; - use std::sync::atomic::{AtomicUsize, Ordering}; - - #[test] - fn in_memory_store_set_and_get() { - let store = InMemoryStore::new(); - store.set("k".into(), json!("v")); - assert_eq!(store.get("k"), Some(json!("v"))); - assert_eq!(store.get("missing"), None); - } - - #[test] - fn in_memory_store_snapshot_is_independent() { - let store = InMemoryStore::new(); - store.set("a".into(), json!(1)); - let snap = store.snapshot(); - store.set("b".into(), json!(2)); - assert!(!snap.contains_key("b")); - assert_eq!(snap.len(), 1); - } - - #[test] - fn in_memory_store_fork() { - let store = InMemoryStore::new(); - store.set("x".into(), json!(10)); - let forked = store.fork(); - forked.set("y".into(), json!(20)); - assert!(store.get("y").is_none()); - assert_eq!(forked.get("x"), Some(json!(10))); - assert_eq!(forked.get("y"), Some(json!(20))); - } #[test] fn context_set_and_get() { @@ -180,59 +101,24 @@ mod tests { assert_eq!(ctx.get("b"), Some(json!(2))); } - #[test] - fn context_clone_is_independent() { - let ctx = Context::new(); - ctx.set("x", json!(1)); - let cloned = ctx.clone_context(); - cloned.set("x", json!(2)); - assert_eq!(ctx.get("x"), Some(json!(1))); - assert_eq!(cloned.get("x"), Some(json!(2))); - } - - #[test] - fn context_with_custom_store() { - struct CountingStore { - inner: InMemoryStore, - set_count: AtomicUsize, - } - impl ContextStore for CountingStore { - fn set(&self, key: String, value: Value) { - self.set_count.fetch_add(1, Ordering::Relaxed); - self.inner.set(key, value); - } - fn get(&self, key: &str) -> Option { - self.inner.get(key) - } - fn snapshot(&self) -> HashMap { - self.inner.snapshot() - } - fn fork(&self) -> Arc { - self.inner.fork() - } - } - - let store = Arc::new(CountingStore { - inner: InMemoryStore::new(), - set_count: AtomicUsize::new(0), - }); - let ctx = Context::with_store(store.clone()); - ctx.set("k", json!(1)); - ctx.set("k2", json!(2)); - assert_eq!(store.set_count.load(Ordering::Relaxed), 2); - assert_eq!(ctx.get("k"), Some(json!(1))); - } - #[test] fn context_fork_is_independent() { let ctx = Context::new(); ctx.set("shared", json!("original")); - let forked = ctx.clone_context(); + let forked = ctx.fork(); forked.set("shared", json!("modified")); assert_eq!(ctx.get("shared"), Some(json!("original"))); assert_eq!(forked.get("shared"), Some(json!("modified"))); } + #[test] + fn context_from_values() { + let mut vals = HashMap::new(); + vals.insert("k".into(), json!("v")); + let ctx = Context::from_values(vals); + assert_eq!(ctx.get("k"), Some(json!("v"))); + } + #[test] fn context_current_node_id() { let ctx = Context::new(); diff --git a/lib/crates/fabro-core/src/lib.rs b/lib/crates/fabro-core/src/lib.rs index 70c227e4d..84c53cedc 100644 --- a/lib/crates/fabro-core/src/lib.rs +++ b/lib/crates/fabro-core/src/lib.rs @@ -12,7 +12,7 @@ pub mod state; #[cfg(test)] pub mod test_fixtures; -pub use context::{Context, ContextStore, InMemoryStore}; +pub use context::Context; pub use error::{CoreError, HandlerErrorDetail, Result}; pub use executor::{Executor, ExecutorBuilder, ExecutorSettings}; pub use graph::{EdgeSelection, EdgeSpec, Graph, NodeSpec}; diff --git a/lib/crates/fabro-workflows/src/backend/api.rs b/lib/crates/fabro-workflows/src/backend/api.rs index ee44dcab8..86922d311 100644 --- a/lib/crates/fabro-workflows/src/backend/api.rs +++ b/lib/crates/fabro-workflows/src/backend/api.rs @@ -12,7 +12,7 @@ use fabro_llm::client::Client; use fabro_model::FallbackTarget; use fabro_model::Provider; -use crate::context::Context; +use crate::context::{Context, WorkflowContext}; use crate::cost::compute_stage_cost; use crate::error::FabroError; use crate::event::WorkflowRunEvent; diff --git a/lib/crates/fabro-workflows/src/context/mod.rs b/lib/crates/fabro-workflows/src/context/mod.rs index c0acae5a7..9bcac8804 100644 --- a/lib/crates/fabro-workflows/src/context/mod.rs +++ b/lib/crates/fabro-workflows/src/context/mod.rs @@ -1,148 +1,42 @@ pub mod keys; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; +pub use fabro_core::Context; -use serde_json::Value; +use fabro_graphviz::Fidelity; -/// Thread-safe key-value context shared across pipeline stages. -#[derive(Debug, Clone)] -pub struct Context { - values: Arc>>, +/// Domain-specific typed accessors for workflow context values. +pub trait WorkflowContext { + fn fidelity(&self) -> Fidelity; + fn thread_id(&self) -> Option; + fn preamble(&self) -> String; + fn run_id(&self) -> String; } -impl Default for Context { - fn default() -> Self { - Self::new() - } -} - -impl Context { - #[must_use] - pub fn new() -> Self { - Self { - values: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Set a key-value pair in the context. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - pub fn set(&self, key: impl Into, value: Value) { - self.values - .write() - .expect("context lock poisoned") - .insert(key.into(), value); - } - - /// Get a value by key, returning None if not present. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - #[must_use] - pub fn get(&self, key: &str) -> Option { - self.values - .read() - .expect("context lock poisoned") - .get(key) - .cloned() - } - - /// Get a value as a string, returning the default if not present or not a string. - #[must_use] - pub fn get_string(&self, key: &str, default: &str) -> String { - self.get(key) - .and_then(|v| v.as_str().map(String::from)) - .unwrap_or_else(|| default.to_string()) - } - - /// Return a snapshot (clone) of all current context values. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - #[must_use] - pub fn snapshot(&self) -> HashMap { - self.values.read().expect("context lock poisoned").clone() - } - - /// Deep copy for parallel branch isolation. - #[must_use] - pub fn clone_context(&self) -> Self { - let values = self.snapshot(); - Self { - values: Arc::new(RwLock::new(values)), - } - } - - /// Merge a map of updates into the context. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - pub fn apply_updates(&self, updates: &HashMap) { - let mut values = self.values.write().expect("context lock poisoned"); - for (key, value) in updates { - values.insert(key.clone(), value.clone()); - } - } - - // --- Internal accessors for bridge code --- - - pub(crate) fn from_values(values: HashMap) -> Self { - Self { - values: Arc::new(RwLock::new(values)), - } - } - - pub(crate) fn values_arc(&self) -> Arc>> { - self.values.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) -> keys::Fidelity { +impl WorkflowContext for Context { + fn fidelity(&self) -> Fidelity { self.get_string(keys::INTERNAL_FIDELITY, "") .parse() .unwrap_or_default() } - #[must_use] - pub fn preamble(&self) -> String { - self.get_string(keys::CURRENT_PREAMBLE, "") - } - - #[must_use] - pub fn thread_id(&self) -> Option { + fn thread_id(&self) -> Option { 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 + fn preamble(&self) -> String { + self.get_string(keys::CURRENT_PREAMBLE, "") } - #[must_use] - pub fn current_node_id(&self) -> String { - self.get_string(keys::CURRENT_NODE, "") + fn run_id(&self) -> String { + self.get_string(keys::INTERNAL_RUN_ID, "unknown") } } #[cfg(test)] mod tests { use super::*; + use std::collections::HashMap; #[test] fn new_context_is_empty() { @@ -189,24 +83,20 @@ mod tests { ctx.set("a", serde_json::json!(1)); let snap = ctx.snapshot(); ctx.set("b", serde_json::json!(2)); - // snapshot should not contain "b" assert!(snap.contains_key("a")); assert!(!snap.contains_key("b")); } #[test] - fn clone_context_is_independent() { + fn fork_is_independent() { let ctx = Context::new(); ctx.set("shared", serde_json::json!("original")); - let cloned = ctx.clone_context(); - cloned.set("shared", serde_json::json!("modified")); + let forked = ctx.fork(); + forked.set("shared", serde_json::json!("modified")); - // original should be unchanged assert_eq!(ctx.get("shared"), Some(serde_json::json!("original"))); - - // cloned has the modification - assert_eq!(cloned.get("shared"), Some(serde_json::json!("modified"))); + assert_eq!(forked.get("shared"), Some(serde_json::json!("modified"))); } #[test] @@ -291,7 +181,9 @@ mod tests { #[test] fn node_visit_count_default() { let ctx = Context::new(); - assert_eq!(ctx.node_visit_count(), 1); + // fabro-core returns 0 for missing; workflow code expects 1 as default + // when used in workflow context. The raw core accessor returns 0. + assert_eq!(ctx.node_visit_count(), 0); } #[test] diff --git a/lib/crates/fabro-workflows/src/core_adapter/context.rs b/lib/crates/fabro-workflows/src/core_adapter/context.rs deleted file mode 100644 index 8c3765cb5..000000000 --- a/lib/crates/fabro-workflows/src/core_adapter/context.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; - -use fabro_core::context::{Context as CoreContext, ContextStore}; -use serde_json::Value; - -use crate::context::keys; -use crate::context::Context as WfContext; - -/// A ContextStore implementation that delegates to a wf::Context's internal values map. -struct WfContextStore { - values: Arc>>, -} - -impl ContextStore for WfContextStore { - fn set(&self, key: String, value: Value) { - self.values - .write() - .expect("context lock poisoned") - .insert(key, value); - } - - fn get(&self, key: &str) -> Option { - self.values - .read() - .expect("context lock poisoned") - .get(key) - .cloned() - } - - fn snapshot(&self) -> HashMap { - self.values.read().expect("context lock poisoned").clone() - } - - fn fork(&self) -> Arc { - let cloned = self.values.read().expect("context lock poisoned").clone(); - Arc::new(WfContextStore { - values: Arc::new(RwLock::new(cloned)), - }) - } -} - -/// Create a fabro_core::Context that shares the same underlying values -/// as the given wf::Context. Writes through either are visible to both. -pub fn bridge_context(wf_ctx: &WfContext) -> CoreContext { - let store = Arc::new(WfContextStore { - values: wf_ctx.values_arc(), - }); - CoreContext::with_store(store) -} - -/// Extension trait providing typed domain accessors on a fabro_core::Context. -pub trait WorkflowContextExt { - fn run_id(&self) -> String; - fn fidelity(&self) -> keys::Fidelity; - fn preamble(&self) -> String; - fn thread_id(&self) -> Option; -} - -impl WorkflowContextExt for CoreContext { - fn run_id(&self) -> String { - self.get_string(keys::INTERNAL_RUN_ID, "unknown") - } - - fn fidelity(&self) -> keys::Fidelity { - self.get_string(keys::INTERNAL_FIDELITY, "") - .parse() - .unwrap_or_default() - } - - fn preamble(&self) -> String { - self.get_string(keys::CURRENT_PREAMBLE, "") - } - - fn thread_id(&self) -> Option { - self.get(keys::INTERNAL_THREAD_ID) - .and_then(|v| v.as_str().map(String::from)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn bridge_shares_values() { - let wf = WfContext::new(); - let core = bridge_context(&wf); - - // Set via wf, read via core - wf.set("key1", json!("from_wf")); - assert_eq!(core.get("key1"), Some(json!("from_wf"))); - - // Set via core, read via wf - core.set("key2", json!("from_core")); - assert_eq!(wf.get("key2"), Some(json!("from_core"))); - } - - #[test] - fn bridge_fork_is_independent() { - let wf = WfContext::new(); - wf.set("shared", json!("original")); - let core = bridge_context(&wf); - let forked = core.clone_context(); - - // Write to fork should not affect original - forked.set("shared", json!("modified")); - assert_eq!(wf.get("shared"), Some(json!("original"))); - assert_eq!(core.get("shared"), Some(json!("original"))); - assert_eq!(forked.get("shared"), Some(json!("modified"))); - } - - #[test] - fn workflow_context_ext_accessors() { - let wf = WfContext::new(); - wf.set(keys::INTERNAL_RUN_ID, json!("run-42")); - wf.set(keys::INTERNAL_FIDELITY, json!("full")); - wf.set(keys::CURRENT_PREAMBLE, json!("You are a helpful assistant")); - wf.set(keys::INTERNAL_THREAD_ID, json!("thread-1")); - - let core = bridge_context(&wf); - assert_eq!(core.run_id(), "run-42"); - assert_eq!(core.fidelity(), keys::Fidelity::Full); - assert_eq!(core.preamble(), "You are a helpful assistant"); - assert_eq!(core.thread_id(), Some("thread-1".to_string())); - } - - #[test] - fn workflow_context_ext_defaults() { - let core = CoreContext::new(); - assert_eq!(core.run_id(), "unknown"); - assert_eq!(core.fidelity(), keys::Fidelity::Compact); - assert_eq!(core.preamble(), ""); - assert_eq!(core.thread_id(), None); - } -} diff --git a/lib/crates/fabro-workflows/src/core_adapter/graph.rs b/lib/crates/fabro-workflows/src/core_adapter/graph.rs index 87e53eb89..318f65b08 100644 --- a/lib/crates/fabro-workflows/src/core_adapter/graph.rs +++ b/lib/crates/fabro-workflows/src/core_adapter/graph.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use std::sync::Arc; -use fabro_core::context::Context as CoreContext; use fabro_core::error::{CoreError, Result as CoreResult}; use fabro_core::graph::{EdgeSelection, EdgeSpec, Graph, NodeSpec}; use fabro_graphviz::graph::types::{Edge as GvEdge, Graph as GvGraph, Node as GvNode}; +use crate::context::Context; use crate::engine; use crate::outcome::{Outcome, StageUsage}; @@ -101,16 +101,12 @@ impl Graph for WorkflowGraph { &self, node: &Self::Node, outcome: &Outcome, - context: &CoreContext, + context: &Context, ) -> Option> { - // Build a wf Context from the core context snapshot so edge conditions - // that read context values (e.g. `context.failure_class=budget_exhausted`) - // evaluate correctly. - let wf_context = crate::context::Context::from_values(context.snapshot()); let selection = engine::select_edge( node.inner(), outcome, - &wf_context, + context, self.inner(), node.inner().selection(), ); diff --git a/lib/crates/fabro-workflows/src/core_adapter/handler.rs b/lib/crates/fabro-workflows/src/core_adapter/handler.rs index 37d0c7f9a..574b309d2 100644 --- a/lib/crates/fabro-workflows/src/core_adapter/handler.rs +++ b/lib/crates/fabro-workflows/src/core_adapter/handler.rs @@ -5,12 +5,13 @@ use std::sync::Arc; use async_trait::async_trait; use futures::FutureExt; -use fabro_core::context::Context as CoreContext; use fabro_core::error::{CoreError, HandlerErrorDetail, Result as CoreResult}; use fabro_core::handler::NodeHandler; use fabro_core::outcome::FailureCategory; use fabro_core::retry::RetryPolicy as CoreRetryPolicy; +use crate::context::Context; + use super::graph::WorkflowGraph; use super::WorkflowNode; use crate::engine; @@ -20,8 +21,8 @@ use crate::outcome::{Outcome, StageStatus}; /// Production node handler that bridges fabro-core's NodeHandler to the /// existing fabro-workflows Handler trait via EngineServices. /// -/// On each `execute()` call, snapshots the CoreContext into a WfContext, -/// runs the handler, then diffs and applies changes back. +/// On each `execute()` call, forks the context, runs the handler, +/// then diffs and applies changes back. pub struct WorkflowNodeHandler { pub services: Arc, pub run_dir: PathBuf, @@ -33,16 +34,15 @@ impl NodeHandler for WorkflowNodeHandler { async fn execute( &self, node: &WorkflowNode, - context: &CoreContext, + context: &Context, _graph: &WorkflowGraph, ) -> CoreResult { let gv_node = node.inner(); let handler = self.services.registry.resolve(gv_node); - // Per-call snapshot/apply context bridge: - // 1. Snapshot the CoreContext into a WfContext + // Fork the context so handler writes don't leak back unless we diff+apply. let snapshot = context.snapshot(); - let wf_context = crate::context::Context::from_values(snapshot.clone()); + let wf_context = context.fork(); // Timeout from the node let node_timeout = gv_node.timeout(); @@ -75,8 +75,8 @@ impl NodeHandler for WorkflowNodeHandler { panic_safe.await }; - // 2. After handler returns, diff the WfContext against the snapshot - // and apply changes back to the CoreContext + // 2. After handler returns, diff the forked context against the snapshot + // and apply changes back to the original context let new_values = wf_context.snapshot(); for (k, v) in &new_values { if snapshot.get(k) != Some(v) { @@ -154,7 +154,7 @@ mod tests { async fn execute( &self, _node: &WorkflowNode, - _context: &CoreContext, + _context: &Context, _graph: &WorkflowGraph, ) -> CoreResult { Ok(Outcome::success()) diff --git a/lib/crates/fabro-workflows/src/core_adapter/lifecycle/fidelity.rs b/lib/crates/fabro-workflows/src/core_adapter/lifecycle/fidelity.rs index 6df3df1e5..5323183c5 100644 --- a/lib/crates/fabro-workflows/src/core_adapter/lifecycle/fidelity.rs +++ b/lib/crates/fabro-workflows/src/core_adapter/lifecycle/fidelity.rs @@ -87,16 +87,13 @@ impl RunLifecycle for FidelityLifecycle { ); // 4. Preamble building: if Full, empty preamble; otherwise build from context - let preamble = { - let wf_context = crate::context::Context::from_values(state.context.snapshot()); - build_preamble( - fidelity, - &wf_context, - &self.graph, - &state.completed_nodes, - &state.node_outcomes, - ) - }; + let preamble = build_preamble( + fidelity, + &state.context, + &self.graph, + &state.completed_nodes, + &state.node_outcomes, + ); state .context .set(keys::CURRENT_PREAMBLE, serde_json::json!(preamble)); diff --git a/lib/crates/fabro-workflows/src/core_adapter/mod.rs b/lib/crates/fabro-workflows/src/core_adapter/mod.rs index 044e33fd6..3f22a99e6 100644 --- a/lib/crates/fabro-workflows/src/core_adapter/mod.rs +++ b/lib/crates/fabro-workflows/src/core_adapter/mod.rs @@ -1,9 +1,7 @@ -pub mod context; pub mod graph; pub mod handler; pub mod lifecycle; -pub use context::{bridge_context, WorkflowContextExt}; pub use graph::{WorkflowEdge, WorkflowGraph, WorkflowNode}; pub use handler::WorkflowNodeHandler; pub use lifecycle::WorkflowLifecycle; diff --git a/lib/crates/fabro-workflows/src/engine.rs b/lib/crates/fabro-workflows/src/engine.rs index 2c86cde03..24ab1554f 100644 --- a/lib/crates/fabro-workflows/src/engine.rs +++ b/lib/crates/fabro-workflows/src/engine.rs @@ -1664,7 +1664,7 @@ impl WorkflowRunEngine { match result { Ok((core_outcome, final_state)) => { // Extract the executor's final context so callers see all state - let ctx = Context::from_values(final_state.context.snapshot()); + let ctx = final_state.context.clone(); Ok((core_outcome, ctx)) } Err(fabro_core::CoreError::StallTimeout { node_id }) => { diff --git a/lib/crates/fabro-workflows/src/handler/agent.rs b/lib/crates/fabro-workflows/src/handler/agent.rs index 73f9d9876..7354e16f4 100644 --- a/lib/crates/fabro-workflows/src/handler/agent.rs +++ b/lib/crates/fabro-workflows/src/handler/agent.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use fabro_agent::Sandbox; use crate::context::keys; -use crate::context::Context; +use crate::context::{Context, WorkflowContext}; use crate::error::FabroError; use crate::event::EventEmitter; use crate::outcome::{Outcome, OutcomeExt, StageUsage}; diff --git a/lib/crates/fabro-workflows/src/handler/manager_loop.rs b/lib/crates/fabro-workflows/src/handler/manager_loop.rs index 203c3d0da..0d0f7d568 100644 --- a/lib/crates/fabro-workflows/src/handler/manager_loop.rs +++ b/lib/crates/fabro-workflows/src/handler/manager_loop.rs @@ -8,7 +8,7 @@ use async_trait::async_trait; use crate::condition::evaluate_condition; use crate::context::keys; -use crate::context::Context; +use crate::context::{Context, WorkflowContext}; use crate::engine::{RunConfig, WorkflowRunEngine}; use crate::error::FabroError; use crate::outcome::{Outcome, OutcomeExt, StageStatus}; @@ -161,7 +161,7 @@ impl Handler for SubWorkflowHandler { }; // Clone parent context for child; inject parent preamble - let child_context = context.clone_context(); + let child_context = context.fork(); let parent_preamble = context.preamble(); if !parent_preamble.is_empty() { child_context.set( diff --git a/lib/crates/fabro-workflows/src/handler/parallel.rs b/lib/crates/fabro-workflows/src/handler/parallel.rs index 5d67803b4..501b1859c 100644 --- a/lib/crates/fabro-workflows/src/handler/parallel.rs +++ b/lib/crates/fabro-workflows/src/handler/parallel.rs @@ -7,7 +7,7 @@ use fabro_agent::{Sandbox, WorktreeConfig, WorktreeSandbox}; use tokio::sync::Semaphore; use crate::context::keys; -use crate::context::Context; +use crate::context::{Context, WorkflowContext}; use crate::engine::set_hook_node; use crate::error::FabroError; use crate::event::WorkflowRunEvent; @@ -73,7 +73,7 @@ impl Handler for ParallelHandler { let target_id = &edge.to; if let Some(target_node) = graph.nodes.get(target_id) { let handler = services.registry.resolve(target_node); - let branch_context = context.clone_context(); + let branch_context = context.fork(); let outcome = super::dispatch_handler( handler, target_node, @@ -196,7 +196,7 @@ impl Handler for ParallelHandler { let mut branch_setups: Vec = Vec::new(); for (branch_index, edge) in branches.iter().enumerate() { let target_id = edge.to.clone(); - let branch_context = context.clone_context(); + let branch_context = context.fork(); let (branch_sandbox, worktree_path): (Arc, Option) = if let ( Some(ref gs), diff --git a/lib/crates/fabro-workflows/src/handler/prompt.rs b/lib/crates/fabro-workflows/src/handler/prompt.rs index 31bb5cb5b..cb137abea 100644 --- a/lib/crates/fabro-workflows/src/handler/prompt.rs +++ b/lib/crates/fabro-workflows/src/handler/prompt.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use fabro_model::Provider; use crate::context::keys; -use crate::context::Context; +use crate::context::{Context, WorkflowContext}; use crate::error::FabroError; use crate::outcome::Outcome; use fabro_graphviz::graph::{Graph, Node}; diff --git a/lib/crates/fabro-workflows/src/preamble.rs b/lib/crates/fabro-workflows/src/preamble.rs index d387de0f3..bd706cf96 100644 --- a/lib/crates/fabro-workflows/src/preamble.rs +++ b/lib/crates/fabro-workflows/src/preamble.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use crate::artifact::{artifact_path, format_artifact_reference}; use crate::context::keys; -use crate::context::Context; +use crate::context::{Context, WorkflowContext}; use crate::outcome::Outcome; use crate::outcome::OutcomeExt; use fabro_graphviz::graph::{is_llm_handler_type, Graph, Node};