feat(orchestrator): modèle de désignation d'orchestrateur + sink de diagnostic
Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).
Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -45,6 +45,7 @@ fn ctx() -> PreparedContext {
|
||||
PreparedContext {
|
||||
content: MarkdownDoc::new("# hi"),
|
||||
relative_path: ".ideai/agent.md".to_owned(),
|
||||
project_root: "/repo".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -151,3 +151,63 @@ async fn manifest_file_is_camelcase_json_under_ideai() {
|
||||
);
|
||||
assert!(entry.get("md_path").is_none(), "no snake_case leak");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_manifest_without_orchestrator_loads_as_default() {
|
||||
// A pre-feature `agents.json` has no `orchestrator` key. It must deserialise with
|
||||
// `orchestrator == None` (serde default), so `effective_orchestrator()` falls back
|
||||
// to the oldest entry (free backward compatibility).
|
||||
let tmp = TempDir::new();
|
||||
let p = project(&tmp.root());
|
||||
let fs = LocalFileSystem::new();
|
||||
fs.create_dir_all(&tmp.child(".ideai")).await.unwrap();
|
||||
let legacy = r#"{
|
||||
"version": 1,
|
||||
"agents": [
|
||||
{ "agentId": "00000000-0000-0000-0000-000000000001", "name": "Oldest",
|
||||
"mdPath": "agents/oldest.md", "profileId": "00000000-0000-0000-0000-0000000000aa",
|
||||
"synchronized": false },
|
||||
{ "agentId": "00000000-0000-0000-0000-000000000002", "name": "Newer",
|
||||
"mdPath": "agents/newer.md", "profileId": "00000000-0000-0000-0000-0000000000aa",
|
||||
"synchronized": false }
|
||||
]
|
||||
}"#;
|
||||
fs.write(&tmp.child(".ideai/agents.json"), legacy.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let manifest = store().load_manifest(&p).await.unwrap();
|
||||
assert_eq!(manifest.orchestrator, None);
|
||||
assert_eq!(manifest.effective_orchestrator(), Some(aid(1)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manifest_with_orchestrator_roundtrips_and_emits_camelcase_key() {
|
||||
let tmp = TempDir::new();
|
||||
let store = store();
|
||||
let p = project(&tmp.root());
|
||||
|
||||
let a = agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
let b = agent(aid(2), "Frontend", "agents/frontend.md", pid(9));
|
||||
let mut manifest = AgentManifest::new(
|
||||
1,
|
||||
vec![ManifestEntry::from_agent(&a), ManifestEntry::from_agent(&b)],
|
||||
)
|
||||
.unwrap();
|
||||
manifest.designate(aid(2)).unwrap();
|
||||
store.save_manifest(&p, &manifest).await.unwrap();
|
||||
|
||||
// Round-trips byte-for-byte through the store…
|
||||
let back = store.load_manifest(&p).await.unwrap();
|
||||
assert_eq!(back, manifest);
|
||||
assert_eq!(back.effective_orchestrator(), Some(aid(2)));
|
||||
|
||||
// …and the on-disk JSON carries the camelCase `orchestrator` key.
|
||||
let fs = LocalFileSystem::new();
|
||||
let bytes = fs.read(&tmp.child(".ideai/agents.json")).await.unwrap();
|
||||
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(
|
||||
json.get("orchestrator").and_then(|v| v.as_str()),
|
||||
Some("00000000-0000-0000-0000-000000000002")
|
||||
);
|
||||
}
|
||||
|
||||
@ -78,6 +78,7 @@ impl FakeContexts {
|
||||
manifest: AgentManifest {
|
||||
version: 1,
|
||||
entries: Vec::new(),
|
||||
orchestrator: None,
|
||||
},
|
||||
contents: HashMap::new(),
|
||||
})))
|
||||
@ -1227,21 +1228,18 @@ async fn ask_agent_rendezvous_times_out_with_a_jsonrpc_error() {
|
||||
agent_id,
|
||||
SessionId::from_uuid(Uuid::from_u128(910)),
|
||||
);
|
||||
let server = server(service)
|
||||
.with_ask_rendezvous_timeout(std::time::Duration::from_millis(50));
|
||||
let server = server(service).with_ask_rendezvous_timeout(std::time::Duration::from_millis(50));
|
||||
|
||||
let raw = tools_call(
|
||||
1,
|
||||
"idea_ask_agent",
|
||||
json!({ "target": "architect", "task": "never answered" }),
|
||||
);
|
||||
let response = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
server.handle_raw(&raw),
|
||||
)
|
||||
.await
|
||||
.expect("must not hang past the injected timeout")
|
||||
.expect("reply owed");
|
||||
let response =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), server.handle_raw(&raw))
|
||||
.await
|
||||
.expect("must not hang past the injected timeout")
|
||||
.expect("reply owed");
|
||||
|
||||
let error = response.error.expect("a JSON-RPC error on timeout");
|
||||
assert_eq!(error.code, error_codes::INTERNAL_ERROR, "got {error:?}");
|
||||
|
||||
@ -74,6 +74,7 @@ impl FakeContexts {
|
||||
manifest: AgentManifest {
|
||||
version: 1,
|
||||
entries: Vec::new(),
|
||||
orchestrator: None,
|
||||
},
|
||||
contents: HashMap::new(),
|
||||
})))
|
||||
|
||||
Reference in New Issue
Block a user