tala-weave

The adaptive replay engine. Takes a narrative graph (or subgraph) and produces a topologically sorted replay plan, applies environment variable substitutions, checks idempotency against completed steps, and orchestrates execution through a caller-supplied executor closure.

Key Types

TypeDescription
ReplayEngineOrchestrates the full replay pipeline: plan, transform, idempotency, execute
ReplayConfigConfiguration for a replay run: variables, completed set, dry-run flag
ExecutorType alias for the executor closure: Box<dyn FnMut(&str) -> Outcome>

Key Functions

FunctionDescription
build_plan(graph, intent_ids, commands)Topological sort via Kahn's algorithm
substitute_vars(input, vars)Replace ${VAR} patterns in a command string
filter_completed(steps, completed)Remove already-completed steps from a plan

build_plan

Produces a topologically sorted replay plan from a set of intent IDs and the edges between them in the narrative graph. Uses Kahn's algorithm. Returns TalaError::CycleDetected if the subgraph contains a cycle.

#![allow(unused)]
fn main() {
/// Build a topologically sorted replay plan.
///
/// Only considers edges between nodes in `intent_ids`. Each step's `deps`
/// field lists its predecessors within the subgraph.
pub fn build_plan(
    graph: &NarrativeGraph,
    intent_ids: &[IntentId],
    commands: &HashMap<IntentId, String>,
) -> Result<Vec<ReplayStep>, TalaError>;
}

The returned Vec<ReplayStep> is ordered such that no step appears before its dependencies. Each ReplayStep contains the intent ID, the command string, and a list of dependency IDs.

Example

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use tala_core::{IntentId, RelationType};
use tala_graph::NarrativeGraph;
use tala_weave::build_plan;

let mut graph = NarrativeGraph::new();
let a = IntentId::random();
let b = IntentId::random();
let c = IntentId::random();

graph.insert_node(a, 1, 1.0);
graph.insert_node(b, 2, 1.0);
graph.insert_node(c, 3, 1.0);
graph.add_edge(a, b, RelationType::Causal, 1.0);
graph.add_edge(b, c, RelationType::Causal, 1.0);

let mut commands = HashMap::new();
commands.insert(a, "echo A".to_string());
commands.insert(b, "echo B".to_string());
commands.insert(c, "echo C".to_string());

let plan = build_plan(&graph, &[a, b, c], &commands).unwrap();
assert_eq!(plan.len(), 3);
// A appears before B, B before C
let pos: HashMap<_, _> = plan.iter().enumerate()
    .map(|(i, s)| (s.intent_id, i)).collect();
assert!(pos[&a] < pos[&b]);
assert!(pos[&b] < pos[&c]);
}

Cycle Detection

#![allow(unused)]
fn main() {
use tala_core::{IntentId, RelationType, TalaError};
use tala_graph::NarrativeGraph;

let mut graph = NarrativeGraph::new();
let a = IntentId::random();
let b = IntentId::random();
graph.insert_node(a, 1, 1.0);
graph.insert_node(b, 2, 1.0);
graph.add_edge(a, b, RelationType::Causal, 1.0);
graph.add_edge(b, a, RelationType::Causal, 1.0); // cycle

let result = build_plan(&graph, &[a, b], &HashMap::new());
assert!(matches!(result, Err(TalaError::CycleDetected)));
}

substitute_vars

Replaces ${VAR} patterns in a string with values from a lookup map. Unknown variables are left as-is. Handles edge cases: adjacent variables, unclosed braces, empty variable names.

#![allow(unused)]
fn main() {
/// Replace `${VAR}` patterns in `input` with values from `vars`.
/// Unknown variables are left intact.
pub fn substitute_vars(input: &str, vars: &HashMap<String, String>) -> String;
}

Examples

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use tala_weave::substitute_vars;

let mut vars = HashMap::new();
vars.insert("HOME".into(), "/home/user".into());
vars.insert("ENV".into(), "prod".into());

assert_eq!(
    substitute_vars("cd ${HOME} && deploy --env=${ENV}", &vars),
    "cd /home/user && deploy --env=prod"
);

// Unknown variables are left as-is
assert_eq!(
    substitute_vars("echo ${UNKNOWN}", &HashMap::new()),
    "echo ${UNKNOWN}"
);

// Adjacent variables
vars.insert("A".into(), "foo".into());
vars.insert("B".into(), "bar".into());
assert_eq!(substitute_vars("${A}${B}", &vars), "foobar");
}

filter_completed

Removes steps whose intent_id appears in the completed set. Used for idempotent replay: if a step has already been executed successfully, it is skipped.

#![allow(unused)]
fn main() {
/// Filter out steps whose `intent_id` appears in `completed`.
pub fn filter_completed(
    steps: Vec<ReplayStep>,
    completed: &HashSet<IntentId>,
) -> Vec<ReplayStep>;
}

Executor

The type alias for executor closures. An executor receives a transformed command string and returns an Outcome.

#![allow(unused)]
fn main() {
pub type Executor = Box<dyn FnMut(&str) -> Outcome>;
}

ReplayConfig

Configuration for a replay run.

#![allow(unused)]
fn main() {
pub struct ReplayConfig {
    /// Environment variable substitutions to apply to commands.
    pub vars: HashMap<String, String>,
    /// Set of intent IDs already completed (for idempotency).
    pub completed: HashSet<IntentId>,
    /// If true, skip execution and return the plan with Pending outcomes.
    pub dry_run: bool,
}

impl Default for ReplayConfig {
    fn default() -> Self {
        Self {
            vars: HashMap::new(),
            completed: HashSet::new(),
            dry_run: true,
        }
    }
}
}

ReplayEngine

The replay orchestrator. Combines plan building, variable substitution, idempotency checking, and execution into a single engine.

#![allow(unused)]
fn main() {
pub struct ReplayEngine { /* private */ }

impl ReplayEngine {
    /// Create a new replay engine with the given configuration.
    pub fn new(config: ReplayConfig) -> Self;

    /// Set the executor closure for live replay. Consumes and returns self
    /// for builder-style chaining.
    pub fn with_executor(self, executor: Executor) -> Self;

    /// Produce a dry-run plan: ordered, transformed steps with completed
    /// steps removed. Does not execute anything.
    pub fn dry_run(
        &self,
        graph: &NarrativeGraph,
        intent_ids: &[IntentId],
        commands: &HashMap<IntentId, String>,
    ) -> Result<Vec<ReplayStep>, TalaError>;

    /// Execute the replay. Returns a `ReplayResult` for every step in the
    /// plan (including skipped ones).
    ///
    /// Behavior per step:
    /// - If the step's ID is in `completed`: skipped=true, Status::Success, latency=0
    /// - If `dry_run` is set: skipped=false, Status::Pending, latency=0
    /// - Otherwise: applies variable substitution, calls the executor, returns outcome
    pub fn execute(
        &mut self,
        graph: &NarrativeGraph,
        intent_ids: &[IntentId],
        commands: &HashMap<IntentId, String>,
    ) -> Result<Vec<ReplayResult>, TalaError>;
}
}

Example: Dry Run

#![allow(unused)]
fn main() {
use std::collections::{HashMap, HashSet};
use tala_core::{IntentId, RelationType, Status};
use tala_graph::NarrativeGraph;
use tala_weave::{ReplayConfig, ReplayEngine};

let mut graph = NarrativeGraph::new();
let a = IntentId::random();
graph.insert_node(a, 1, 1.0);

let mut commands = HashMap::new();
commands.insert(a, "deploy --env=${ENV}".to_string());

let mut vars = HashMap::new();
vars.insert("ENV".to_string(), "staging".to_string());

let config = ReplayConfig {
    vars,
    completed: HashSet::new(),
    dry_run: true,
};

let engine = ReplayEngine::new(config);
let plan = engine.dry_run(&graph, &[a], &commands).unwrap();

assert_eq!(plan.len(), 1);
assert_eq!(plan[0].command, "deploy --env=staging");
}

Example: Live Execution

#![allow(unused)]
fn main() {
use std::collections::{HashMap, HashSet};
use tala_core::{IntentId, Outcome, RelationType, Status};
use tala_graph::NarrativeGraph;
use tala_weave::{Executor, ReplayConfig, ReplayEngine};

let mut graph = NarrativeGraph::new();
let a = IntentId::random();
let b = IntentId::random();
graph.insert_node(a, 1, 1.0);
graph.insert_node(b, 2, 1.0);
graph.add_edge(a, b, RelationType::Causal, 1.0);

let mut commands = HashMap::new();
commands.insert(a, "echo A".to_string());
commands.insert(b, "echo B".to_string());

let config = ReplayConfig {
    vars: HashMap::new(),
    completed: HashSet::new(),
    dry_run: false,
};

let executor: Executor = Box::new(|_cmd| Outcome {
    status: Status::Success,
    latency_ns: 100,
    exit_code: 0,
});

let mut engine = ReplayEngine::new(config).with_executor(executor);
let results = engine.execute(&graph, &[a, b], &commands).unwrap();

assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| !r.skipped));
assert!(results.iter().all(|r| r.outcome.status == Status::Success));
}