Add /graph G6: plan-boundary topology optimizer

A restricted optimizer pass now reviews the graph at plan boundaries —
right after initial planning and piggybacked on each replan version
boundary, never mid-execution. An optimizer subagent may emit four ops
over Waiting/Ready nodes only: remove_dep (delete a false dependency,
restoring parallelism — the highest-value edit), reorder (pending
priority for the serial scheduler), merge (fold two tiny nodes; specs
concatenate, deps union, dependents re-point, absorbed self-deps drop),
and split (2-3 focused replacements inheriting the original's deps and
dependents). The optimizer changes graph DATA only; the executor stays
pure deterministic Rust. KIGI_GRAPH_OPTIMIZER=0 disables it entirely.

apply_optimization enforces the contract twice: per-op checks
(pending-only targets, known ids, terminal node untouchable, dead-node
deps rejected as DeadDep, merge/split targets with non-pending
dependents rejected with the true reason instead of tripping the
immutable invariant later), then FINAL invariants — every non-pending
node byte-identical in the result, the gn-final gate rebuilt over all
survivors, node cap, whole-graph acyclicity, and a BIDIRECTIONAL status
re-derivation for pending nodes (adversarial review caught the critical
hole: a merge grafting unsatisfied deps onto a Ready node would
otherwise dispatch it ahead of its new prerequisites, since
recompute_ready is promote-only). Applied passes bump plan_version,
freeze an immutable baseline, and consume a slot of the SHARED replan
cap; an explicit {"ops": []} is a respected free no-op; any failure
degrades to keeping the current plan. Plumbing reuses a new shared
artifact-pass runner (stale-artifact delete, size cap, missing-file
fail-closed) extracted from the replanner.

Tests: remove_dep parallelism restore + loud no-such-dep, immutable and
terminal-node rejections across all four ops, merge/split dependent
rewiring incl. final-gate rebuild and intra-split dep resolution,
result-cycle rejection, dead-dep splits, Ready-demote-on-merge, and
three e2e flows — false-dep removal proven ACTUALLY parallel by the
held-reply fan-out gate, OPTIMIZER=0 spawning zero passes, and the
shared-cap guard. kigi-shell 4959 lib tests green; clippy clean.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 20:21:49 -04:00
1 parent db05ed0751
commit 02cf5deebd
19 files changed
+1123

No files matched your search

@@ -388,6 +388,8 @@ impl SessionActor {
self.graph_tracker.lock().install_nodes(nodes);
self.persist_graph_state();
tracing::info!(total, "graph: DAG installed, launching first node");
// Plan-boundary optimizer pass ① (post-initial-planning).
self.maybe_optimize_graph().await;
match self.drive_graph().await {
Some(reminder) => GraphSetupOutcome::Inference {
@@ -0,0 +1,171 @@
//! Topology optimizer (G6): a plan-boundary review pass that may issue
//! a RESTRICTED set of graph edits — remove false deps (restoring
//! parallelism), reorder pending priority, merge tiny nodes, split
//! oversized ones — over Waiting/Ready nodes only.
//!
//! The optimizer changes GRAPH DATA only; the executor stays pure
//! deterministic Rust. It fires ① right after initial planning and
//! ② at each replan boundary (piggybacked), never mid-execution.
//! Applied (non-empty) passes bump `plan_version`, freeze a baseline,
//! and consume a slot of the SHARED replan cap; an explicit `[]` is a
//! respected no-op consuming nothing. Failure degrades — the current
//! graph keeps running. `KIGI_GRAPH_OPTIMIZER=0` disables entirely.
use std::sync::Arc;
use super::super::goal_planner::{ChannelSpawner, GoalPlannerSpawner};
use super::super::graph_plan;
use super::super::graph_planner::{ArtifactPassSpec, run_graph_artifact_pass};
use super::SessionActor;
const OPTIMIZER_PROMPT_TEMPLATE: &str = include_str!("../templates/graph_optimizer_prompt.md");
impl SessionActor {
/// One optimizer pass at a plan boundary. No-op when disabled, when
/// the shared cap is exhausted, or when the graph is not Active.
pub(super) async fn maybe_optimize_graph(&self) {
if !self.graph_optimizer_enabled {
return;
}
let (replan_runs, current_graph, history_text, graph_file, next_version) = {
let tracker = self.graph_tracker.lock();
let Some(state) = tracker.snapshot() else {
return;
};
if state.status != crate::session::goal_tracker::GoalStatus::Active {
return;
}
let compact: Vec<serde_json::Value> = state
.nodes
.iter()
.map(|n| {
serde_json::json!({
"id": n.id,
"title": n.title,
"spec": n.spec,
"status": format!("{:?}", n.status),
"deps": n.deps.iter().map(|d| d.on.clone()).collect::<Vec<_>>(),
})
})
.collect();
let history_text = state
.history
.iter()
.rev()
.take(12)
.map(|h| {
format!(
"- {:?} {} {}",
h.event,
h.node_id.as_deref().unwrap_or("-"),
h.detail.as_deref().unwrap_or("")
)
})
.collect::<Vec<_>>()
.join("\n");
(
state.replan_runs,
serde_json::to_string(&compact).unwrap_or_default(),
history_text,
tracker
.artifacts_dir()
.join(format!("optimize.v{}.json", state.plan_version + 1)),
state.plan_version + 1,
)
};
if self.graph_replan_cap == 0 || replan_runs >= self.graph_replan_cap {
tracing::info!(
replan_runs,
cap = self.graph_replan_cap,
"graph optimizer: shared cap exhausted; skipping pass"
);
return;
}
let Some(event_tx) = self.tool_context.subagent_event_tx.clone() else {
return;
};
let objective = self
.graph_tracker
.lock()
.objective()
.map(str::to_owned)
.unwrap_or_default();
let parent_prompt_id = self
.current_prompt_id
.lock()
.expect("current_prompt_id mutex poisoned")
.clone();
let spawner: Arc<dyn GoalPlannerSpawner> = Arc::new(ChannelSpawner {
event_tx,
parent_session_id: self.session_id_string(),
parent_prompt_id,
cwd: Some(self.tool_context.cwd.as_str().to_owned()),
role_override: Default::default(),
events: Some(self.events.writer()),
});
let tool_names = self.resolve_inherit_role_tool_names().await;
let sections = format!(
"OBJECTIVE:\n{objective}\n\nCURRENT GRAPH:\n{current_graph}\n\n\
EXECUTION HISTORY:\n{history_text}\n"
);
tracing::info!(next_version, "graph optimizer: firing");
let json = match run_graph_artifact_pass(
spawner,
ArtifactPassSpec {
template: OPTIMIZER_PROMPT_TEMPLATE,
sections: &sections,
graph_file: &graph_file,
tool_names: &tool_names,
role: "graph optimizer",
},
)
.await
{
Ok(json) => json,
Err(reason) => {
// Degrade: an enhancement pass never blocks the graph.
tracing::warn!(%reason, "graph optimizer: pass failed; keeping current plan");
return;
}
};
let existing = self
.graph_tracker
.lock()
.snapshot()
.map(|s| s.nodes.clone())
.unwrap_or_default();
match graph_plan::apply_optimization(&existing, &json) {
Ok(None) => {
tracing::info!("graph optimizer: no ops (already good)");
}
Ok(Some(optimized)) => {
let n_before = existing.len();
let n_after = optimized.len();
self.graph_tracker.lock().install_optimized_nodes(optimized);
let all_nodes = self
.graph_tracker
.lock()
.snapshot()
.map(|s| s.nodes.clone())
.unwrap_or_default();
if let Err(err) = self.write_graph_baseline(&all_nodes).await {
tracing::warn!(%err, "graph optimizer: baseline write failed (audit gap only)");
}
self.persist_graph_state();
tracing::info!(
next_version,
n_before,
n_after,
"graph optimizer: plan optimized"
);
self.send_slash_command_output(&format!(
"Graph optimized (v{next_version}): {n_before} → {n_after} node(s)."
))
.await;
}
Err(err) => {
tracing::warn!(%err, "graph optimizer: ops rejected; keeping current plan");
}
}
}
}
@@ -198,6 +198,9 @@ impl SessionActor {
discovered work."
))
.await;
// Plan-boundary optimizer pass ② (piggybacked on the
// replan version boundary).
self.maybe_optimize_graph().await;
return;
}
GraphPlannerOutcome::Invalid { reason } if attempt == 1 => {
@@ -1104,6 +1104,7 @@ pub(crate) async fn spawn_session_actor(
graph_concurrency: effective_config.resolve_graph_concurrency(),
graph_node_rounds: effective_config.resolve_graph_node_rounds(),
graph_replan_cap: effective_config.resolve_graph_replan_cap(),
graph_optimizer_enabled: effective_config.resolve_graph_optimizer_enabled(),
graph_project_dir,
graph_project_lock: std::cell::RefCell::new(None),
goal_turn_task_ids: parking_lot::Mutex::new(std::collections::HashSet::new()),