Add /graph G3: dynamic replan from DISCOVERED work items
Workers, verifiers, and serial node goals can now surface out-of-scope work
as line-anchored 'DISCOVERED: <text>' markers (fence-stripped and
placeholder-filtered — the templates' own examples are fenced so verbatim
echoes never parse; worker summaries embedded in verifier prompts get the
marker neutralized alongside NODE_RESULT/NODE_VERDICT). Discoveries queue on
the orchestration as persisted state and fold into the graph at dispatch
boundaries: a replanner subagent produces a strictly APPEND-ONLY appendix,
validated against the live graph (existing-id deps allowed; edges onto
gn-final rejected — they would cycle the moment the final-gating extension
lands; Blocks deps on Failed/Blocked nodes rejected as DeadDep so the
attempt-2 feedback loop repairs the artifact). Installing an appendix bumps
plan_version, freezes an immutable graph.baseline.v{N}.json next to the
prior versions, extends gn-final's gate (demoting a Ready final back to
Waiting), and recomputes readiness.
DiscoveredFrom edges are audit metadata, never scheduling gates: an origin
is always terminal at replan time, so gating on it is either a no-op or a
permanent wedge — and a failed node's discoveries are still real work.
Replanning is bounded by KIGI_GRAPH_REPLAN_CAP (default 3; 0 disables it
quietly): past the cap, after the final node has achieved, or on replan
failure, discoveries drain to history only — a working graph is never
paused for a failed enhancement pass, and it always converges. The budget
gate now precedes the replan boundary (a budget-dead graph keeps its
discoveries queued for a later --budget top-up instead of spending two
replanner runs first), and both planner runners delete stale artifacts
before spawning so a child that responds without writing can never get a
previous pass's file validated as its own output.
Tests: validate_replan unit coverage (existing-id resolution,
DiscoveredFrom dedup, collisions, dead deps vs dead origins, terminal-node
edges, combined-graph cycles), tracker appendix/regate/audit-edge tests,
and two e2e flows — discovery → replan → appended node runs to Achieved
with both baselines frozen, and cap-0 draining to history while the graph
still converges. kigi-shell 4935 lib tests green; workspace clippy clean.
This commit is contained in:
@@ -49,7 +49,10 @@ pub(super) fn node_goal_objective(
|
||||
{spec}\n\n\
|
||||
This node is one unit of a larger graph objective:\n\
|
||||
{graph_objective}\n\n\
|
||||
Complete ONLY this node's scope; other nodes cover the rest.",
|
||||
Complete ONLY this node's scope; other nodes cover the rest. If you \
|
||||
find NECESSARY work outside this node's scope, do NOT do it — list \
|
||||
each item on its own line as `DISCOVERED: <description>` in your \
|
||||
final summary; the harness turns these into new graph nodes.",
|
||||
title = node.title,
|
||||
spec = node.spec,
|
||||
)
|
||||
@@ -339,7 +342,7 @@ impl SessionActor {
|
||||
/// Write the immutable plan baseline for the current version.
|
||||
/// `create_new` guarantees a frozen baseline is never overwritten —
|
||||
/// an existing file is the infra failure it looks like.
|
||||
async fn write_graph_baseline(&self, nodes: &[GraphNode]) -> std::io::Result<()> {
|
||||
pub(super) async fn write_graph_baseline(&self, nodes: &[GraphNode]) -> std::io::Result<()> {
|
||||
let path = {
|
||||
let tracker = self.graph_tracker.lock();
|
||||
let version = tracker.snapshot().map(|s| s.plan_version).unwrap_or(1);
|
||||
@@ -543,6 +546,22 @@ impl SessionActor {
|
||||
.snapshot()
|
||||
.map(|o| o.total_worker_rounds)
|
||||
.unwrap_or(0);
|
||||
// Serial-path discovery capture: the node's final assistant text
|
||||
// may carry `DISCOVERED:` items (same contract as the parallel
|
||||
// workers, zero extra tool surface).
|
||||
if let Some(text) = self.chat_state_handle.get_last_assistant_text().await {
|
||||
let found = super::graph_workers::parse_discovered_lines(&text);
|
||||
if !found.is_empty() {
|
||||
let ds: Vec<super::super::graph_tracker::Discovery> = found
|
||||
.into_iter()
|
||||
.map(|description| super::super::graph_tracker::Discovery {
|
||||
from_node: node_id.clone(),
|
||||
description,
|
||||
})
|
||||
.collect();
|
||||
self.graph_tracker.lock().queue_discoveries(ds);
|
||||
}
|
||||
}
|
||||
self.archive_node_artifacts(&node_id).await;
|
||||
tracing::info!(%node_id, rounds, node_tokens, "graph: node achieved");
|
||||
self.graph_tracker
|
||||
@@ -565,6 +584,9 @@ impl SessionActor {
|
||||
// serial-launch failure — the pauser already messaged.
|
||||
return None;
|
||||
}
|
||||
// Replan boundary: fold queued discoveries into the graph
|
||||
// (bounded; failure degrades to history-only).
|
||||
self.maybe_replan_graph().await;
|
||||
if self.graph_tracker.lock().remaining_budget() == Some(0) {
|
||||
tracing::warn!("graph: budget exhausted at dispatch");
|
||||
self.graph_tracker.lock().budget_limit();
|
||||
|
||||
Reference in New Issue
Block a user