feat(agent): conversation par paire + entrée médiée + pivot terminal/MCP
Coeur inter-agents consolidé et surface front réalignée sur la décision "terminal natif PTY, pas d'UI chat" (Option 1). Domaine - nouveaux modules conversation, mailbox, input, fileguard (ports + types) - orchestrator/profile/events étendus (conversation par paire, FIFO) Application / Infrastructure - orchestrator/service + context_guard : sérialisation FIFO par agent, garde RW mémoire/contexte, dispatch ask/reply - adapters in-memory conversation / mailbox / input / fileguard - registry session + lifecycle agent durcis (1 agent = 1 session vivante) - outils MCP idea_* alignés sur le nouveau dispatch Frontend - MediatedInput + useAgentBusy : entrée utilisateur médiée par IdeA, terminal = vue sortie inchangée - suppression de la vue chat structurée (AgentChatView) — abandonnée - adapter input + ports mis à jour Divers - .ideai/ : mémoire projet + briefs de cadrage versionnés ; requests/ runtime ignoré ; agents projet réels (DevBackend/DevFrontend/QA) Tests : Rust (domain/application/infrastructure/app-tauri) + front (346) verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1750,6 +1750,17 @@ pub(crate) fn compose_convention_file(
|
||||
(lister les agents du projet). IdeA lancera ou réattachera l'agent cible \
|
||||
avec son propre AI Profile, son contexte et sa mémoire.\n\n",
|
||||
);
|
||||
// Protocole de délégation (Option 1, B-5) : côté agent SOLLICITÉ. Une tâche
|
||||
// déléguée arrive dans ton terminal préfixée `[IdeA · tâche de … · ticket …]`.
|
||||
// Tu DOIS y répondre via l'outil `idea_reply`, jamais en texte libre — sinon
|
||||
// l'agent qui t'a sollicité reste bloqué (sa réponse ne lui parviendra pas).
|
||||
out.push_str(
|
||||
"Quand tu reçois une tâche déléguée par IdeA (un message préfixé \
|
||||
`[IdeA · tâche de … · ticket …]`), traite-la puis appelle \
|
||||
**impérativement** l'outil `idea_reply(result=…)` pour rendre ton \
|
||||
résultat. Ne réponds **jamais** uniquement en texte : seul `idea_reply` \
|
||||
débloque l'agent qui t'a sollicité.\n\n",
|
||||
);
|
||||
} else {
|
||||
out.push_str(
|
||||
"Pour déléguer une tâche à un autre agent, n'utilise jamais les subagents \
|
||||
@ -1785,6 +1796,7 @@ pub(crate) fn compose_convention_file(
|
||||
out.push_str("- [");
|
||||
out.push_str(&entry.title);
|
||||
out.push_str("](");
|
||||
out.push_str(".ideai/memory/");
|
||||
out.push_str(entry.slug.as_str());
|
||||
out.push_str(".md) — ");
|
||||
out.push_str(&entry.hook);
|
||||
@ -1998,9 +2010,13 @@ mod tests {
|
||||
let section_at = doc.find("# Mémoire projet").unwrap();
|
||||
assert!(persona_at < section_at, "memory comes after the persona");
|
||||
|
||||
// Exact line format: `- [Title](slug.md) — hook (type)`.
|
||||
assert!(doc.contains("- [Alpha](alpha-note.md) — the first hook (user)"));
|
||||
assert!(doc.contains("- [Beta](beta-note.md) — the second hook (reference)"));
|
||||
// Exact line format: `- [Title](.ideai/memory/slug.md) — hook (type)`.
|
||||
assert!(doc.contains("- [Alpha](.ideai/memory/alpha-note.md) — the first hook (user)"));
|
||||
assert!(
|
||||
doc.contains(
|
||||
"- [Beta](.ideai/memory/beta-note.md) — the second hook (reference)"
|
||||
)
|
||||
);
|
||||
|
||||
// Deterministic order: first entry precedes the second.
|
||||
let alpha_at = doc.find("[Alpha]").unwrap();
|
||||
@ -2033,7 +2049,7 @@ mod tests {
|
||||
assert!(doc.contains("# Skills"));
|
||||
assert!(doc.contains("REFAC_BODY"));
|
||||
assert!(doc.contains("# Mémoire projet"));
|
||||
assert!(doc.contains("- [Note](note.md) — a hook (project)"));
|
||||
assert!(doc.contains("- [Note](.ideai/memory/note.md) — a hook (project)"));
|
||||
|
||||
// Skills section precedes the memory section (persona → skills → memory).
|
||||
let skills_at = doc.find("# Skills").unwrap();
|
||||
@ -2066,6 +2082,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_convention_file_mcp_prose_carries_the_idea_reply_delegation_protocol() {
|
||||
// B-5 — the solicited-agent side of the protocol: a delegated task arrives as
|
||||
// `[IdeA · tâche …]` and MUST be answered via `idea_reply`, never plain text.
|
||||
let doc = compose_convention_file("/root", "", "# Persona", &[], &[], true);
|
||||
assert!(
|
||||
doc.contains("idea_reply"),
|
||||
"MCP prose must instruct answering via idea_reply"
|
||||
);
|
||||
assert!(
|
||||
doc.contains("[IdeA · tâche"),
|
||||
"MCP prose must describe the delegated-task prefix it answers to"
|
||||
);
|
||||
// Negative: the non-MCP (file-protocol) prose must NOT carry the idea_reply
|
||||
// instruction (zero regression on the file path).
|
||||
let file_doc = compose_convention_file("/root", "", "# Persona", &[], &[], false);
|
||||
assert!(!file_doc.contains("idea_reply"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_convention_file_non_mcp_prose_is_the_unchanged_file_protocol() {
|
||||
// mcp_enabled = false ⇒ the current `.ideai/requests` wording, unchanged,
|
||||
@ -2087,6 +2122,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_declaration_with_runtime_points_the_bridge_at_the_real_endpoint() {
|
||||
// B-0 — a PTY-launched MCP-capable CLI (Claude/Codex REPL) auto-discovers the
|
||||
// `.mcp.json` in its cwd (the run dir). When the composition root injects the
|
||||
// real `McpRuntime`, the declaration written there must spawn *this* IdeA exe
|
||||
// in `mcp-server` mode and dial the project's exact loopback endpoint — the
|
||||
// same source of truth `ensure_mcp_server` binds — so the CLI can call
|
||||
// `idea_list_agents` and get a reply end-to-end.
|
||||
let rt = McpRuntime {
|
||||
exe: "/opt/idea/idea".to_owned(),
|
||||
endpoint: "/run/user/1000/idea-mcp/proj.sock".to_owned(),
|
||||
project_id: "0123456789abcdef0123456789abcdef".to_owned(),
|
||||
requester: "11112222-3333-4444-5555-666677778888".to_owned(),
|
||||
};
|
||||
let decl = mcp_server_declaration(domain::profile::McpTransport::Stdio, Some(&rt));
|
||||
|
||||
// It is valid JSON with the IdeA server under `mcpServers/idea`.
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&decl).expect("declaration is valid JSON");
|
||||
let idea = &parsed["mcpServers"]["idea"];
|
||||
assert_eq!(idea["command"], "/opt/idea/idea");
|
||||
let args = idea["args"].as_array().expect("args is an array");
|
||||
let args: Vec<&str> = args.iter().filter_map(serde_json::Value::as_str).collect();
|
||||
assert_eq!(args[0], "mcp-server");
|
||||
// The real endpoint / project / requester are all threaded through.
|
||||
assert!(args.contains(&"--endpoint"));
|
||||
assert!(args.contains(&"/run/user/1000/idea-mcp/proj.sock"));
|
||||
assert!(args.contains(&"--project"));
|
||||
assert!(args.contains(&"0123456789abcdef0123456789abcdef"));
|
||||
assert!(args.contains(&"--requester"));
|
||||
assert_eq!(idea["transport"], "stdio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_declaration_without_runtime_is_a_coherent_minimal_fallback() {
|
||||
// Launches issued from inside `application` (orchestrator/hot-swap/tests) have
|
||||
// no OS/runtime facts: the declaration falls back to the bare `idea mcp-server`
|
||||
// command — still a valid, self-consistent `mcpServers/idea` entry.
|
||||
let decl = mcp_server_declaration(domain::profile::McpTransport::Stdio, None);
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&decl).expect("minimal declaration is valid JSON");
|
||||
let idea = &parsed["mcpServers"]["idea"];
|
||||
assert_eq!(idea["command"], "idea");
|
||||
let args: Vec<&str> = idea["args"]
|
||||
.as_array()
|
||||
.expect("args array")
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.collect();
|
||||
assert_eq!(args, vec!["mcp-server"]);
|
||||
// No endpoint/project/requester when no runtime was injected.
|
||||
assert!(!args.contains(&"--endpoint"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_settings_seed_grants_autonomy_and_keeps_guardrails() {
|
||||
let json = claude_settings_seed("/home/me/proj");
|
||||
|
||||
Reference in New Issue
Block a user