orchestrator: per-node model selection via node.attrs["agent"]
TurnRequest gains an optional `agent` sourced from the graph node's attrs["agent"]. The ZeroClaw executor binds a node to that alias directly when present, falling back to the role→alias map otherwise. This lets a single POST /api/topologies/run specify a different model per role (heterogeneous topologies) entirely in the graph JSON — no server ZEROCLAW_AGENT_MAP change or recreate per configuration, which makes quota-frugal model×role sweeps practical. cm-orchestrator 13 + cm-api topology_exec 4 tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ae517a18c1
commit
99e2fd3ec3
@@ -256,7 +256,15 @@ impl ZeroClawDriveExecutor {
|
|||||||
|
|
||||||
impl TurnExecutor for ZeroClawDriveExecutor {
|
impl TurnExecutor for ZeroClawDriveExecutor {
|
||||||
async fn run_turn(&self, req: TurnRequest) -> Result<TurnOutcome, OrchestratorError> {
|
async fn run_turn(&self, req: TurnRequest) -> Result<TurnOutcome, OrchestratorError> {
|
||||||
let alias = self.alias_for(&req.role);
|
// An explicit per-node agent (graph `node.attrs["agent"]`) wins, so one
|
||||||
|
// request can pin a different model per role; otherwise use the map.
|
||||||
|
let alias = req
|
||||||
|
.agent
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|a| !a.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| self.alias_for(&req.role));
|
||||||
let prompt = Self::build_prompt(&req);
|
let prompt = Self::build_prompt(&req);
|
||||||
self.drive(&alias, &prompt).await
|
self.drive(&alias, &prompt).await
|
||||||
}
|
}
|
||||||
@@ -334,11 +342,30 @@ mod tests {
|
|||||||
TurnRequest {
|
TurnRequest {
|
||||||
node_id: "a".into(),
|
node_id: "a".into(),
|
||||||
role: "researcher".into(),
|
role: "researcher".into(),
|
||||||
|
agent: None,
|
||||||
task: "say hi".into(),
|
task: "say hi".into(),
|
||||||
context: vec![],
|
context: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_node_agent_overrides_role_map() {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("researcher".to_string(), "worker_glm".to_string());
|
||||||
|
let exec = ZeroClawDriveExecutor::new("http://x".into(), "c".into(), map, "scout".into());
|
||||||
|
// role map → worker_glm
|
||||||
|
assert_eq!(exec.alias_for("researcher"), "worker_glm");
|
||||||
|
// but an explicit per-node agent should win in run_turn's selection
|
||||||
|
let agent = Some("worker_kimi".to_string());
|
||||||
|
let chosen = agent
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|a| !a.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| exec.alias_for("researcher"));
|
||||||
|
assert_eq!(chosen, "worker_kimi");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn drives_a_turn_and_accumulates_output_and_tokens() {
|
async fn drives_a_turn_and_accumulates_output_and_tokens() {
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ pub struct TurnRequest {
|
|||||||
pub node_id: String,
|
pub node_id: String,
|
||||||
/// The node's role.
|
/// The node's role.
|
||||||
pub role: String,
|
pub role: String,
|
||||||
|
/// Optional explicit agent/model alias for this node, from the graph
|
||||||
|
/// (`node.attrs["agent"]`). When set, the executor binds this node to this
|
||||||
|
/// alias directly — letting one request pin a different model per role
|
||||||
|
/// (heterogeneous topologies) without reconfiguring the server. Falls back
|
||||||
|
/// to the role→alias map when absent.
|
||||||
|
pub agent: Option<String>,
|
||||||
/// The top-level task.
|
/// The top-level task.
|
||||||
pub task: String,
|
pub task: String,
|
||||||
/// Upstream context (task and/or prior step outputs) for this turn.
|
/// Upstream context (task and/or prior step outputs) for this turn.
|
||||||
@@ -186,6 +192,7 @@ pub async fn execute<E: TurnExecutor>(
|
|||||||
.run_turn(TurnRequest {
|
.run_turn(TurnRequest {
|
||||||
node_id: node.id.clone(),
|
node_id: node.id.clone(),
|
||||||
role: node.role.clone(),
|
role: node.role.clone(),
|
||||||
|
agent: node.attrs.get("agent").cloned(),
|
||||||
task: task.to_string(),
|
task: task.to_string(),
|
||||||
context,
|
context,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user