feat(agent): orchestration v3 — surface MCP model-agnostic (M0→M4) — §14.3
Expose l'orchestration IdeA comme serveur MCP par-dessus le même OrchestratorService::dispatch, avec repli fichier .ideai/requests pour les CLI sans MCP. v3 réduite à la surface MCP : la messagerie inter-agents et la corrélation requête↔réponse étaient déjà résolues par §17 (send_blocking). - M0 capacité MCP sur le profil (McpCapability/McpConfigStrategy/McpTransport) - M1 injection conf MCP au LaunchAgent + prose adaptée selon la surface - M2 serveur/adapter MCP (JSON-RPC 2.0 maison ; outils idea_*) + ListAgents - M3 câblage par projet (registre mcp_servers jumeau du watcher) - M4 observabilité UI : OrchestratorRequestProcessed.source = file|mcp + badge Trois portes d'entrée (fichier, MCP, UI) → un seul dispatch ; aucun nouveau port applicatif ; MCP confiné à l'adapter infra. Tous lots verts (cycle §3). Cadrage : .ideai/briefs/orchestration-v3-cadrage.md ; ARCHITECTURE.md §14.3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -15,7 +15,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::agent::{AgentManifest, ManifestEntry};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::events::{DomainEvent, OrchestrationSource};
|
||||
use domain::ids::SkillId;
|
||||
use domain::ids::{AgentId, ProfileId, ProjectId};
|
||||
use domain::markdown::MarkdownDoc;
|
||||
@ -37,7 +37,7 @@ use application::{
|
||||
CloseTerminal, CreateAgentFromScratch, CreateSkill, LaunchAgent, ListAgents,
|
||||
OrchestratorService, StructuredSessions, TerminalSessions, UpdateAgentContext,
|
||||
};
|
||||
use infrastructure::{process_request_file, OrchestratorResponse};
|
||||
use infrastructure::{process_request_file, FsOrchestratorWatcher, OrchestratorResponse};
|
||||
|
||||
// --- temp dir (mirror local_fs.rs) ---
|
||||
struct TempDir(PathBuf);
|
||||
@ -720,3 +720,68 @@ async fn failure_response_omits_reply_key() {
|
||||
"reply key must be absent on failure — got {raw}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// M4 — observability: the filesystem watcher tags processed requests source=File.
|
||||
//
|
||||
// Drives the *public* `FsOrchestratorWatcher::start` against a real temp project
|
||||
// root with a capturing events closure (the same sink shape the composition root
|
||||
// wires), then polls (bounded) until the OrchestratorRequestProcessed beacon for
|
||||
// the dropped request lands. Asserts the tag is `OrchestrationSource::File`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn watcher_publishes_processed_event_tagged_file_source() {
|
||||
use std::time::Duration;
|
||||
|
||||
let tmp = TempDir::new();
|
||||
// A project rooted at the temp dir so the watcher scans <tmp>/.ideai/requests/.
|
||||
let project = Project::new(
|
||||
ProjectId::from_uuid(Uuid::from_u128(2000)),
|
||||
"demo",
|
||||
ProjectPath::new(tmp.0.to_str().unwrap()).unwrap(),
|
||||
RemoteRef::local(),
|
||||
1_700_000_000_000,
|
||||
)
|
||||
.unwrap();
|
||||
let service = build_service(FakeContexts::new());
|
||||
|
||||
let captured = Arc::new(Mutex::new(Vec::<DomainEvent>::new()));
|
||||
let sink = captured.clone();
|
||||
let publish: Arc<dyn Fn(DomainEvent) + Send + Sync> =
|
||||
Arc::new(move |e| sink.lock().unwrap().push(e));
|
||||
|
||||
let handle = FsOrchestratorWatcher::start(project, Arc::clone(&service), publish);
|
||||
|
||||
// Drop a valid request under .ideai/requests/<requester>/.
|
||||
let req_dir = tmp.0.join(".ideai").join("requests").join("Main");
|
||||
std::fs::create_dir_all(&req_dir).unwrap();
|
||||
std::fs::write(
|
||||
req_dir.join("req-1.json"),
|
||||
br#"{ "action": "spawn_agent", "name": "dev-backend", "profile": "claude-code" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Poll (bounded) until the processed beacon for our request shows up.
|
||||
let mut found: Option<DomainEvent> = None;
|
||||
for _ in 0..50 {
|
||||
if let Some(e) = captured.lock().unwrap().iter().find(|e| {
|
||||
matches!(e, DomainEvent::OrchestratorRequestProcessed { action, .. } if action == "spawn_agent")
|
||||
}) {
|
||||
found = Some(e.clone());
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
handle.stop();
|
||||
|
||||
let event = found.expect("watcher must publish a processed beacon within the poll budget");
|
||||
match event {
|
||||
DomainEvent::OrchestratorRequestProcessed { source, ok, requester_id, .. } => {
|
||||
assert_eq!(source, OrchestrationSource::File, "fs door must tag File");
|
||||
assert!(ok, "the spawn request succeeded");
|
||||
assert_eq!(requester_id, "Main", "requester id = request subdirectory");
|
||||
}
|
||||
other => panic!("expected OrchestratorRequestProcessed, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user