chore(wip): checkpoint P8/C avant chantier Codex inter-agents
Sauvegarde de l'arbre de travail en cours (persistance P8, conversations C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le support de la délégation inter-agents pour les profils Codex. Le round-trip inter-agent question/réponse est couvert sans tokens par les tests loopback existants (state::mcp_e2e_loopback_tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -27,9 +27,9 @@ use std::sync::{Arc, Mutex};
|
||||
use async_trait::async_trait;
|
||||
use domain::agent::{AgentManifest, ManifestEntry};
|
||||
use domain::events::{DomainEvent, OrchestrationSource};
|
||||
use domain::ids::NodeId;
|
||||
use domain::ids::SkillId;
|
||||
use domain::ids::{AgentId, ProfileId, ProjectId};
|
||||
use domain::ids::NodeId;
|
||||
use domain::markdown::MarkdownDoc;
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, ContextInjectionPlan, DirEntry, EventBus, EventStream,
|
||||
@ -52,11 +52,11 @@ use application::{
|
||||
CloseTerminal, CreateAgentFromScratch, CreateSkill, LaunchAgent, ListAgents,
|
||||
OrchestratorService, TerminalSessions, UpdateAgentContext,
|
||||
};
|
||||
use infrastructure::orchestrator::mcp::jsonrpc::error_codes;
|
||||
use infrastructure::{
|
||||
InMemoryConversationRegistry, InMemoryMailbox, McpServer, MediatedInbox, MemoryTransport,
|
||||
SystemMillisClock,
|
||||
};
|
||||
use infrastructure::orchestrator::mcp::jsonrpc::error_codes;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@ -296,7 +296,6 @@ impl PtyPort for FakePty {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct NoopBus;
|
||||
impl EventBus for NoopBus {
|
||||
@ -468,7 +467,9 @@ fn tools_call(id: i64, tool: &str, arguments: Value) -> Vec<u8> {
|
||||
|
||||
/// Extracts the single text content block of a successful `tools/call` result.
|
||||
fn result_text(result: &Value) -> &str {
|
||||
result["content"][0]["text"].as_str().expect("text content block")
|
||||
result["content"][0]["text"]
|
||||
.as_str()
|
||||
.expect("text content block")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -485,15 +486,15 @@ async fn tools_list_advertises_the_seven_idea_tools_with_schemas() {
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let response = server.handle_raw(&raw).await.expect("tools/list owes a reply");
|
||||
let response = server
|
||||
.handle_raw(&raw)
|
||||
.await
|
||||
.expect("tools/list owes a reply");
|
||||
assert!(response.error.is_none(), "got error: {:?}", response.error);
|
||||
let result = response.result.expect("result");
|
||||
|
||||
let tools = result["tools"].as_array().expect("tools array");
|
||||
let names: Vec<&str> = tools
|
||||
.iter()
|
||||
.map(|t| t["name"].as_str().unwrap())
|
||||
.collect();
|
||||
let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
||||
|
||||
for expected in [
|
||||
"idea_list_agents",
|
||||
@ -509,7 +510,10 @@ async fn tools_list_advertises_the_seven_idea_tools_with_schemas() {
|
||||
"idea_memory_read",
|
||||
"idea_memory_write",
|
||||
] {
|
||||
assert!(names.contains(&expected), "missing tool {expected}; got {names:?}");
|
||||
assert!(
|
||||
names.contains(&expected),
|
||||
"missing tool {expected}; got {names:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
@ -520,7 +524,8 @@ async fn tools_list_advertises_the_seven_idea_tools_with_schemas() {
|
||||
// Every tool advertises an object input schema.
|
||||
for t in tools {
|
||||
assert_eq!(
|
||||
t["inputSchema"]["type"], json!("object"),
|
||||
t["inputSchema"]["type"],
|
||||
json!("object"),
|
||||
"tool {} must declare an object input schema",
|
||||
t["name"]
|
||||
);
|
||||
@ -544,7 +549,11 @@ async fn launch_agent_call_creates_and_launches_the_agent() {
|
||||
json!({ "target": "dev-backend", "profile": "claude-code" }),
|
||||
);
|
||||
let response = server.handle_raw(&raw).await.expect("reply owed");
|
||||
assert!(response.error.is_none(), "transport error: {:?}", response.error);
|
||||
assert!(
|
||||
response.error.is_none(),
|
||||
"transport error: {:?}",
|
||||
response.error
|
||||
);
|
||||
let result = response.result.expect("result");
|
||||
assert_eq!(result["isError"], json!(false), "got {result}");
|
||||
|
||||
@ -630,7 +639,11 @@ async fn ask_agent_returns_target_reply_inline() {
|
||||
let contexts = FakeContexts::new();
|
||||
let agent_id = contexts.seed_agent("architect");
|
||||
let (service, mailbox, sessions) = build_service_with_mailbox(contexts);
|
||||
seed_live_pty(&sessions, agent_id, SessionId::from_uuid(Uuid::from_u128(4242)));
|
||||
seed_live_pty(
|
||||
&sessions,
|
||||
agent_id,
|
||||
SessionId::from_uuid(Uuid::from_u128(4242)),
|
||||
);
|
||||
|
||||
let ask_server = server(Arc::clone(&service));
|
||||
let ask = tokio::spawn(async move {
|
||||
@ -655,7 +668,10 @@ async fn ask_agent_returns_target_reply_inline() {
|
||||
// requester, which `for_requester` injects as the `from` of the Reply command.
|
||||
let reply_server = server(Arc::clone(&service)).for_requester(agent_id.to_string());
|
||||
let reply_raw = tools_call(8, "idea_reply", json!({ "result": "the answer is 42" }));
|
||||
let reply_resp = reply_server.handle_raw(&reply_raw).await.expect("reply owed");
|
||||
let reply_resp = reply_server
|
||||
.handle_raw(&reply_raw)
|
||||
.await
|
||||
.expect("reply owed");
|
||||
assert_eq!(reply_resp.result.expect("result")["isError"], json!(false));
|
||||
|
||||
let response = tokio::time::timeout(std::time::Duration::from_secs(10), ask)
|
||||
@ -663,7 +679,11 @@ async fn ask_agent_returns_target_reply_inline() {
|
||||
.expect("ask completes after reply")
|
||||
.expect("join ok");
|
||||
assert_eq!(response.id, json!(7), "id must be echoed");
|
||||
assert!(response.error.is_none(), "transport error: {:?}", response.error);
|
||||
assert!(
|
||||
response.error.is_none(),
|
||||
"transport error: {:?}",
|
||||
response.error
|
||||
);
|
||||
let result = response.result.expect("result");
|
||||
assert_eq!(result["isError"], json!(false), "got {result}");
|
||||
assert_eq!(
|
||||
@ -751,7 +771,10 @@ async fn failed_command_is_tool_error_not_transport_error_and_server_survives()
|
||||
}))
|
||||
.unwrap();
|
||||
let again = server.handle_raw(&raw).await.expect("server still serves");
|
||||
assert!(again.result.is_some(), "server must survive a failed command");
|
||||
assert!(
|
||||
again.result.is_some(),
|
||||
"server must survive a failed command"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -898,7 +921,10 @@ async fn scripted_session_over_memory_transport_round_trips() {
|
||||
// Exactly two responses (init + list); the notification produced none.
|
||||
let first = rx.recv().await.expect("init response");
|
||||
let second = rx.recv().await.expect("list response");
|
||||
assert!(rx.recv().await.is_none(), "notification must not be answered");
|
||||
assert!(
|
||||
rx.recv().await.is_none(),
|
||||
"notification must not be answered"
|
||||
);
|
||||
|
||||
let first: Value = serde_json::from_slice(&first).unwrap();
|
||||
let second: Value = serde_json::from_slice(&second).unwrap();
|
||||
@ -927,7 +953,11 @@ async fn processed_tools_call_publishes_event_tagged_mcp_source() {
|
||||
json!({ "target": "dev-backend", "profile": "claude-code" }),
|
||||
);
|
||||
let response = server.handle_raw(&raw).await.expect("reply owed");
|
||||
assert!(response.error.is_none(), "transport error: {:?}", response.error);
|
||||
assert!(
|
||||
response.error.is_none(),
|
||||
"transport error: {:?}",
|
||||
response.error
|
||||
);
|
||||
assert_eq!(response.result.expect("result")["isError"], json!(false));
|
||||
|
||||
// Exactly one OrchestratorRequestProcessed event, tagged as the MCP door.
|
||||
@ -951,7 +981,10 @@ async fn processed_tools_call_publishes_event_tagged_mcp_source() {
|
||||
assert_eq!(*source, OrchestrationSource::Mcp, "MCP door must tag Mcp");
|
||||
assert_eq!(action, "idea_launch_agent", "action mirrors the tool name");
|
||||
assert!(*ok, "the launch succeeded");
|
||||
assert_eq!(requester_id, "mcp", "stable mcp label until per-session bind");
|
||||
assert_eq!(
|
||||
requester_id, "mcp",
|
||||
"stable mcp label until per-session bind"
|
||||
);
|
||||
}
|
||||
other => panic!("expected OrchestratorRequestProcessed, got {other:?}"),
|
||||
}
|
||||
@ -976,9 +1009,15 @@ async fn failed_tools_call_still_publishes_event_with_ok_false_and_mcp_source()
|
||||
.iter()
|
||||
.filter(|e| matches!(e, DomainEvent::OrchestratorRequestProcessed { .. }))
|
||||
.collect();
|
||||
assert_eq!(processed.len(), 1, "a failed call still beacons; got {events:?}");
|
||||
assert_eq!(
|
||||
processed.len(),
|
||||
1,
|
||||
"a failed call still beacons; got {events:?}"
|
||||
);
|
||||
match processed[0] {
|
||||
DomainEvent::OrchestratorRequestProcessed { ok, source, action, .. } => {
|
||||
DomainEvent::OrchestratorRequestProcessed {
|
||||
ok, source, action, ..
|
||||
} => {
|
||||
assert!(!*ok, "the dispatch failed → ok = false");
|
||||
assert_eq!(*source, OrchestrationSource::Mcp);
|
||||
assert_eq!(action, "idea_stop_agent");
|
||||
@ -1001,7 +1040,11 @@ async fn server_without_event_sink_emits_nothing_and_does_not_panic() {
|
||||
json!({ "target": "dev-backend", "profile": "claude-code" }),
|
||||
);
|
||||
let response = server.handle_raw(&raw).await.expect("reply owed");
|
||||
assert!(response.error.is_none(), "transport error: {:?}", response.error);
|
||||
assert!(
|
||||
response.error.is_none(),
|
||||
"transport error: {:?}",
|
||||
response.error
|
||||
);
|
||||
assert_eq!(response.result.expect("result")["isError"], json!(false));
|
||||
// The command really ran (agent created) — proof the no-op sink path is live.
|
||||
assert_eq!(contexts.entries().len(), 1);
|
||||
|
||||
Reference in New Issue
Block a user