refactor(#2441): move agent field from DAG onto Node; drop dedup

Agent was a single field on Dag/DagSpec, making a DAG structurally
one-agent — a multi-agent op could only ever be N separate DAGs. Move it
onto Node/NodeSpec (and the NodeView wire type), drop it from Dag/DagSpec
(and DagView): a DAG can now span agents.

- lifecycle lease keys on the node's agent, still globally exclusive per
  agent across all DAGs (Inner.leases unchanged in shape). A DAG holds one
  lease per distinct agent it touches; settle() frees each at DAG-terminal
  (per-agent-subgraph early release is a follow-up, only observable with
  multi-agent DAGs).
- transient guard keyed (dag_id, agent); cancel-revert + Rebuilt events
  walk TerminalDag.agents.
- submit-time dedup removed (a multi-agent DAG has no single agent to key
  on); every submit enqueues a fresh DAG. Whether dedup needs reintroducing
  is tracked in a follow-up sub-issue.
- templates gain a node(agent, kind, deps) helper stamping the agent onto
  every node; meta templates stamp "hyperhive".

Templates stay single-agent in this PR — behaviour is unchanged, only the
representation + wire shape. Multi-agent DAG emission (restart/restart-all/
broad stop+start as one DAG) and the SetWanted-as-a-node change are
follow-ups off #2439.
This commit is contained in:
atlas 2026-07-14 21:42:26 +02:00 committed by mara
commit 2a59f2f5fc
7 changed files with 223 additions and 356 deletions

View file

@ -3,6 +3,11 @@
//! confined to this validation; the runtime store stays the plain
//! `Vec<Node>` + `deps`).
//!
//! Every node carries its own `agent` (there is no DAG-level agent) — the
//! `node` helper stamps the template's agent onto each. Today's templates
//! are single-agent (every node shares one agent); a future multi-agent
//! template would stamp different agents per subgraph.
//!
//! ```text
//! rebuild(a): Prebuild(a) → StopForUpdate(a) → Swap(a) →(any) Reconcile(a)
//! graceful-stop(a): [wanted=Offline] Signal(a) → Drain(a) → Reconcile(a)
@ -29,35 +34,42 @@ fn after_ok(on: u32) -> Vec<Dep> {
}]
}
/// Build one node targeting `agent`. The single place templates stamp a
/// node's agent, so a whole template is single-agent by passing the same
/// `agent` to every call.
fn node(agent: &str, kind: NodeKind, deps: Vec<Dep>) -> NodeSpec {
NodeSpec {
agent: agent.to_owned(),
kind,
deps,
}
}
/// The rebuild node chain. `Reconcile` deps on `Swap` with `AfterAny`:
/// it must run even when the profile swap failed, so a previously-up
/// agent comes back on its old config (today's recovery-start). This
/// is the only `AfterAny` edge in v1.
fn rebuild_nodes(relock: bool, base: u32) -> Vec<NodeSpec> {
fn rebuild_nodes(agent: &str, relock: bool, base: u32) -> Vec<NodeSpec> {
vec![
NodeSpec {
kind: NodeKind::Prebuild { relock },
deps: if base == 0 {
node(
agent,
NodeKind::Prebuild { relock },
if base == 0 {
Vec::new()
} else {
after_ok(base - 1)
},
},
NodeSpec {
kind: NodeKind::StopForUpdate,
deps: after_ok(base),
},
NodeSpec {
kind: NodeKind::Swap,
deps: after_ok(base + 1),
},
NodeSpec {
kind: NodeKind::Reconcile,
deps: vec![Dep {
),
node(agent, NodeKind::StopForUpdate, after_ok(base)),
node(agent, NodeKind::Swap, after_ok(base + 1)),
node(
agent,
NodeKind::Reconcile,
vec![Dep {
on: base + 2,
when: DepWhen::AfterAny,
}],
},
),
]
}
@ -75,7 +87,6 @@ pub fn rebuild(
) -> DagSpec {
DagSpec {
template: Template::Rebuild,
agent: agent.to_owned(),
source,
reason,
parent_id,
@ -83,7 +94,7 @@ pub fn rebuild(
inputs: Vec::new(),
perm_payload: None,
transient: Some(TransientKind::Rebuilding),
nodes: rebuild_nodes(relock, 0),
nodes: rebuild_nodes(agent, relock, 0),
}
}
@ -93,7 +104,6 @@ pub fn rebuild(
pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec {
DagSpec {
template: Template::Rebuild,
agent: agent.to_owned(),
source: Source::Approval,
reason,
parent_id: None,
@ -101,10 +111,7 @@ pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec
inputs: Vec::new(),
perm_payload: None,
transient: Some(TransientKind::Rebuilding),
nodes: vec![NodeSpec {
kind: NodeKind::ApprovalDeploy,
deps: Vec::new(),
}],
nodes: vec![node(agent, NodeKind::ApprovalDeploy, Vec::new())],
}
}
@ -117,7 +124,6 @@ pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec
pub fn graceful_stop(agent: &str, source: Source, reason: String) -> DagSpec {
DagSpec {
template: Template::GracefulStop,
agent: agent.to_owned(),
source,
reason,
parent_id: None,
@ -126,18 +132,9 @@ pub fn graceful_stop(agent: &str, source: Source, reason: String) -> DagSpec {
perm_payload: None,
transient: Some(TransientKind::Stopping),
nodes: vec![
NodeSpec {
kind: NodeKind::Signal,
deps: Vec::new(),
},
NodeSpec {
kind: NodeKind::Drain,
deps: after_ok(0),
},
NodeSpec {
kind: NodeKind::Reconcile,
deps: after_ok(1),
},
node(agent, NodeKind::Signal, Vec::new()),
node(agent, NodeKind::Drain, after_ok(0)),
node(agent, NodeKind::Reconcile, after_ok(1)),
],
}
}
@ -148,7 +145,6 @@ pub fn graceful_stop(agent: &str, source: Source, reason: String) -> DagSpec {
pub fn restart(agent: &str, source: Source, reason: String) -> DagSpec {
DagSpec {
template: Template::Restart,
agent: agent.to_owned(),
source,
reason,
parent_id: None,
@ -157,14 +153,8 @@ pub fn restart(agent: &str, source: Source, reason: String) -> DagSpec {
perm_payload: None,
transient: Some(TransientKind::Restarting),
nodes: vec![
NodeSpec {
kind: NodeKind::StopForUpdate,
deps: Vec::new(),
},
NodeSpec {
kind: NodeKind::Reconcile,
deps: after_ok(0),
},
node(agent, NodeKind::StopForUpdate, Vec::new()),
node(agent, NodeKind::Reconcile, after_ok(0)),
],
}
}
@ -180,7 +170,6 @@ pub fn restart(agent: &str, source: Source, reason: String) -> DagSpec {
pub fn graceful_restart(agent: &str, source: Source, reason: String) -> DagSpec {
DagSpec {
template: Template::GracefulRestart,
agent: agent.to_owned(),
source,
reason,
parent_id: None,
@ -189,22 +178,10 @@ pub fn graceful_restart(agent: &str, source: Source, reason: String) -> DagSpec
perm_payload: None,
transient: Some(TransientKind::Restarting),
nodes: vec![
NodeSpec {
kind: NodeKind::Signal,
deps: Vec::new(),
},
NodeSpec {
kind: NodeKind::Drain,
deps: after_ok(0),
},
NodeSpec {
kind: NodeKind::StopForUpdate,
deps: after_ok(1),
},
NodeSpec {
kind: NodeKind::Reconcile,
deps: after_ok(2),
},
node(agent, NodeKind::Signal, Vec::new()),
node(agent, NodeKind::Drain, after_ok(0)),
node(agent, NodeKind::StopForUpdate, after_ok(1)),
node(agent, NodeKind::Reconcile, after_ok(2)),
],
}
}
@ -220,7 +197,6 @@ pub fn reconcile_only(
) -> DagSpec {
DagSpec {
template,
agent: agent.to_owned(),
source,
reason,
parent_id: None,
@ -228,10 +204,7 @@ pub fn reconcile_only(
inputs: Vec::new(),
perm_payload: None,
transient,
nodes: vec![NodeSpec {
kind: NodeKind::Reconcile,
deps: Vec::new(),
}],
nodes: vec![node(agent, NodeKind::Reconcile, Vec::new())],
}
}
@ -242,7 +215,6 @@ pub fn reconcile_only(
pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
DagSpec {
template: Template::Spawn,
agent: agent.to_owned(),
source: Source::Approval,
reason,
parent_id: None,
@ -251,22 +223,10 @@ pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
perm_payload: None,
transient: Some(TransientKind::Spawning),
nodes: vec![
NodeSpec {
kind: NodeKind::Provision,
deps: Vec::new(),
},
NodeSpec {
kind: NodeKind::Create,
deps: after_ok(0),
},
NodeSpec {
kind: NodeKind::WriteDropin,
deps: after_ok(1),
},
NodeSpec {
kind: NodeKind::Reconcile,
deps: after_ok(2),
},
node(agent, NodeKind::Provision, Vec::new()),
node(agent, NodeKind::Create, after_ok(0)),
node(agent, NodeKind::WriteDropin, after_ok(1)),
node(agent, NodeKind::Reconcile, after_ok(2)),
],
}
}
@ -275,14 +235,10 @@ pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
/// the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes
/// effect in the container.
pub fn perm_change(agent: &str, source: Source, reason: String, payload: PermPayload) -> DagSpec {
let mut nodes = vec![NodeSpec {
kind: NodeKind::WritePermFile,
deps: Vec::new(),
}];
nodes.extend(rebuild_nodes(true, 1));
let mut nodes = vec![node(agent, NodeKind::WritePermFile, Vec::new())];
nodes.extend(rebuild_nodes(agent, true, 1));
DagSpec {
template: Template::PermChange,
agent: agent.to_owned(),
source,
reason,
parent_id: None,
@ -306,7 +262,6 @@ pub fn meta_update(
) -> DagSpec {
DagSpec {
template: Template::MetaUpdate,
agent: "hyperhive".to_owned(),
source,
reason,
parent_id: None,
@ -314,19 +269,17 @@ pub fn meta_update(
inputs,
perm_payload: None,
transient: None,
nodes: vec![NodeSpec {
kind: NodeKind::MetaLock {
nodes: vec![node(
"hyperhive",
NodeKind::MetaLock {
sweep: false,
fanout: None,
},
deps: Vec::new(),
}],
Vec::new(),
)],
}
}
/// Boot-time sweep parent: bump meta's hyperhive input (non-fatal),
/// then fan out `Rebuild` children for the precomputed stale agent
/// list (topology-sorted by the caller).
/// Boot-time root anchor DAG: a single [`NodeKind::Noop`] node that groups
/// this boot's `StartupSweep` + per-agent `Reconcile` child DAGs (linked via
/// `parent_id`) into one tree so the dashboard renders the boot as one entry.
@ -336,7 +289,6 @@ pub fn meta_update(
pub fn boot_root(reason: String) -> DagSpec {
DagSpec {
template: Template::Boot,
agent: "hyperhive".to_owned(),
source: Source::AutoUpdate,
reason,
parent_id: None,
@ -344,17 +296,16 @@ pub fn boot_root(reason: String) -> DagSpec {
inputs: Vec::new(),
perm_payload: None,
transient: None,
nodes: vec![NodeSpec {
kind: NodeKind::Noop,
deps: Vec::new(),
}],
nodes: vec![node("hyperhive", NodeKind::Noop, Vec::new())],
}
}
/// Boot-time sweep parent: bump meta's hyperhive input (non-fatal),
/// then fan out `Rebuild` children for the precomputed stale agent
/// list (topology-sorted by the caller).
pub fn startup_sweep(reason: String, stale_agents: Vec<String>) -> DagSpec {
DagSpec {
template: Template::StartupSweep,
agent: "hyperhive".to_owned(),
source: Source::AutoUpdate,
reason,
parent_id: None,
@ -362,13 +313,14 @@ pub fn startup_sweep(reason: String, stale_agents: Vec<String>) -> DagSpec {
inputs: Vec::new(),
perm_payload: None,
transient: None,
nodes: vec![NodeSpec {
kind: NodeKind::MetaLock {
nodes: vec![node(
"hyperhive",
NodeKind::MetaLock {
sweep: true,
fanout: Some(stale_agents),
},
deps: Vec::new(),
}],
Vec::new(),
)],
}
}