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:
Bryan Helmkamp 2026-03-06 11:55:31 -05:00
parent af6e091ef3
commit b4159243ac
13 changed files with 378 additions and 158 deletions

View file

@ -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 {

View file

@ -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,

View 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");
}
}

View file

@ -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");
}
}

View file

@ -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(&current_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|"),

View file

@ -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");

View file

@ -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"),

View file

@ -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"))
);
}

View file

@ -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"))
);
}

View file

@ -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);

View file

@ -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

View file

@ -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();

View file

@ -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 ---