diff --git a/_primitives/_rust/kei-agent-runtime/src/prepare.rs b/_primitives/_rust/kei-agent-runtime/src/prepare.rs index 75bbf6e..8c7fe1f 100644 --- a/_primitives/_rust/kei-agent-runtime/src/prepare.rs +++ b/_primitives/_rust/kei-agent-runtime/src/prepare.rs @@ -18,6 +18,7 @@ use crate::role::resolve_role; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; /// Everything the orchestrator needs to hand the Claude `Agent` tool. #[derive(Debug, Clone, Serialize)] @@ -41,11 +42,14 @@ pub fn prepare(task: &TaskSpec, kit_root: &Path) -> Result { if task.task.role.is_empty() { return Err(anyhow!("task.role is empty")); } - if task.task.agent_id.is_empty() { - return Err(anyhow!( - "task.agent-id is empty — orchestrator must allocate via kei-ledger" - )); - } + // Auto-generate agent-id if absent. Format: `ag---<4hex-rand>` + // Orchestrator can still pre-allocate via `kei-ledger fork` and write explicit + // agent-id into task.toml for deterministic id; auto-gen is the ergonomic default. + let agent_id = if task.task.agent_id.is_empty() { + autogen_agent_id(&task.task.role) + } else { + task.task.agent_id.clone() + }; let role_file = load_role_meta(kit_root, &task.task.role)?; if !role_file.role.spawnable { return Err(anyhow!( @@ -62,12 +66,16 @@ pub fn prepare(task: &TaskSpec, kit_root: &Path) -> Result { .clone() .unwrap_or_else(|| default_subagent_type(&task.task.role)); let isolation = default_isolation(&task.task.role); - let description = build_description(&task.task.role, &task.task.agent_id); - let verify_command = build_verify_command(&task.task.agent_id); - let ledger_row = build_ledger_row(task); - let dna = Dna::compose(task, &resolved).render(); + let description = build_description(&task.task.role, &agent_id); + let verify_command = build_verify_command(&agent_id); + let ledger_row = build_ledger_row_with_id(task, &agent_id); + // DNA uses effective agent-id; if auto-generated we inject it into a clone + // of TaskSpec so Dna::compose sees the resolved id. + let mut task_for_dna = task.clone(); + task_for_dna.task.agent_id = agent_id.clone(); + let dna = Dna::compose(&task_for_dna, &resolved).render(); Ok(AgentInvocation { - agent_id: task.task.agent_id.clone(), + agent_id, role: task.task.role.clone(), prompt, subagent_type, @@ -79,6 +87,15 @@ pub fn prepare(task: &TaskSpec, kit_root: &Path) -> Result { }) } +fn autogen_agent_id(role: &str) -> String { + let ts_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + let rand_hex = format!("{:04x}", rand::random::()); + format!("ag-{}-{:x}-{}", role, ts_ms, rand_hex) +} + /// Human-readable block — copy into Claude Code's Agent-tool dialog. pub fn render_human(inv: &AgentInvocation) -> String { let iso = inv.isolation.as_deref().unwrap_or(""); @@ -141,6 +158,10 @@ fn build_verify_command(agent_id: &str) -> String { } fn build_ledger_row(task: &TaskSpec) -> String { + build_ledger_row_with_id(task, &task.task.agent_id) +} + +fn build_ledger_row_with_id(task: &TaskSpec, agent_id: &str) -> String { let parent = task .task .parent_agent @@ -149,7 +170,7 @@ fn build_ledger_row(task: &TaskSpec) -> String { .unwrap_or("none"); format!( "running agent-id={} role={} parent={}", - task.task.agent_id, task.task.role, parent + agent_id, task.task.role, parent ) }