mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
fix(workflow): reserve current_node and current.* — not every 'current*' key
CURRENT_PREFIX was the bare string 'current', so is_engine_internal_key swallowed ANY user key starting with 'current' — current_seed_id, current_task, currently_running — hiding it from stage preambles at every fidelity level and from child->parent propagation. Observed in run 01M0NJ3QZ1FK53X9DK3BBAN2ED: the planner emitted current_seed_id/current_seed_brief via context_updates (the documented inter-stage contract), record_result applied them to the shared Context, and the implementer's compact preamble still showed only 'planner: succeeded' — no Context section. The implementer re-derived the seed from the tracker (17 wasted tool calls, re-claimed the in_progress seed: a role violation). The engine reservation is now exactly 'current_node' (singular) plus the dotted 'current.*' namespace, matching graph./internal./thread. discipline. Natural user keys beginning with 'current' are ordinary context values. Tests: key-classification cases for user 'current*' keys; regression test building the real pipeline shape (planner outcome -> context -> compact preamble) asserting the values render.
This commit is contained in:
parent
7200d437e9
commit
af518f2f2c
2 changed files with 68 additions and 1 deletions
|
|
@ -63,7 +63,15 @@ pub mod keys {
|
|||
// --- 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";
|
||||
/// Engine-reserved key names starting with "current": the singular
|
||||
/// `current_node` plus the dotted `current.*` namespace. Deliberately NOT
|
||||
/// the bare prefix "current" — user keys like `current_seed_id` or
|
||||
/// `current_task` are natural names and must not be swallowed by the
|
||||
/// engine-internal filter (observed in run 01M0NJ3QZ1FK53X9DK3BBAN2ED:
|
||||
/// planner-emitted `current_seed_id`/`current_seed_brief` never reached
|
||||
/// the next stage's preamble at any fidelity).
|
||||
pub const CURRENT_NODE_KEY: &str = "current_node";
|
||||
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.";
|
||||
|
|
@ -110,6 +118,7 @@ pub mod keys {
|
|||
key.starts_with(INTERNAL_PREFIX)
|
||||
|| key.starts_with(GRAPH_PREFIX)
|
||||
|| key.starts_with(THREAD_PREFIX)
|
||||
|| key == CURRENT_NODE_KEY
|
||||
|| key.starts_with(CURRENT_PREFIX)
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +158,10 @@ pub mod keys {
|
|||
assert!(is_engine_internal_key("thread.main.current_node"));
|
||||
assert!(is_engine_internal_key("current.preamble"));
|
||||
assert!(is_engine_internal_key("current_node"));
|
||||
// Natural user keys starting with "current" are NOT internal
|
||||
assert!(!is_engine_internal_key("current_seed_id"));
|
||||
assert!(!is_engine_internal_key("current_task"));
|
||||
assert!(!is_engine_internal_key("currently_running"));
|
||||
|
||||
// Keys that are NOT engine-internal (should propagate)
|
||||
assert!(!is_engine_internal_key("response.plan"));
|
||||
|
|
|
|||
|
|
@ -2252,6 +2252,60 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// Regression (fabro-6a78): run 01M0NJ3QZ1FK53X9DK3BBAN2ED — an LLM
|
||||
// stage emitted routing context_updates (current_seed_id/brief) and the
|
||||
// next stage's COMPACT preamble showed '## Completed stages' with the
|
||||
// model line and nothing else: no '## Context' section, no values. The
|
||||
// implementer re-derived the seed blind (17 wasted tool calls, re-claimed
|
||||
// the seed: role violation). context_updates values are the inter-stage
|
||||
// contract; compact must render them.
|
||||
#[test]
|
||||
fn compact_renders_custom_context_updates_values() {
|
||||
let graph = Graph::new("test");
|
||||
let context = Context::new();
|
||||
let completed_nodes = vec!["planner".to_string()];
|
||||
let mut node_outcomes: HashMap<String, Outcome> = HashMap::new();
|
||||
let mut outcome = Outcome::success();
|
||||
outcome.context_updates.insert(
|
||||
"current_seed_id".to_string(),
|
||||
serde_json::json!("fabro-0879"),
|
||||
);
|
||||
outcome.context_updates.insert(
|
||||
"current_seed_brief".to_string(),
|
||||
serde_json::json!("- add -pretty flag\n- both flags combine"),
|
||||
);
|
||||
node_outcomes.insert("planner".to_string(), outcome);
|
||||
|
||||
// Mirror the real pipeline: record_result applies the planner's
|
||||
// context_updates into the shared Context BEFORE the next node's
|
||||
// preamble is built (fabro-core state.rs record_result).
|
||||
let context = Context::new();
|
||||
for (k, v) in &node_outcomes.get("planner").unwrap().context_updates {
|
||||
context.set(k.clone(), v.clone());
|
||||
}
|
||||
|
||||
let preamble = build_preamble(
|
||||
keys::Fidelity::Compact,
|
||||
&context,
|
||||
&graph,
|
||||
&completed_nodes,
|
||||
&node_outcomes,
|
||||
);
|
||||
|
||||
assert!(
|
||||
preamble.contains("current_seed_id"),
|
||||
"compact must name custom context keys, got:\n{preamble}"
|
||||
);
|
||||
assert!(
|
||||
preamble.contains("fabro-0879"),
|
||||
"compact must render the VALUE of custom context keys, got:\n{preamble}"
|
||||
);
|
||||
assert!(
|
||||
preamble.contains("current_seed_brief"),
|
||||
"compact must render brief keys, got:\n{preamble}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_high_command_stage_truncates_long_output() {
|
||||
let mut graph = Graph::new("test");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue