agent conversation fix
This commit is contained in:
@ -28,14 +28,13 @@ use application::{
|
||||
OrchestratorService, PermissionProjectorRegistry, ProposeContext, ReadAgentContext,
|
||||
ReadContext, ReadConversationPage, ReadMemory, ReadMemoryIndex, ReadProjectContext, ReadSkill,
|
||||
RecallMemory, ReconcileLayouts, ReconcileLiveState, ReconcileLiveStateInput, RecordTurn,
|
||||
RecordTurnProvider, ReferenceProfiles,
|
||||
RenameLayout, ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks,
|
||||
RotateConversationLog, SaveEmbedderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
|
||||
SnapshotRunningAgents, StopLiveAgent, StructuredSessions, SuggestedThisSession,
|
||||
SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent, UpdateAgentContext,
|
||||
UpdateAgentPermissions, UpdateLiveState, UpdateMemory, UpdateProjectContext,
|
||||
UpdateProjectPermissions, UpdateSkill, UpdateTemplate, WriteMemory, WriteToTerminal,
|
||||
AGENT_MEMORY_RECALL_BUDGET,
|
||||
RecordTurnProvider, ReferenceProfiles, RenameLayout, ResizeTerminal, ResolveAgentPermissions,
|
||||
ResolveMemoryLinks, RotateConversationLog, SaveEmbedderProfile, SaveProfile,
|
||||
SessionLimitService, SetActiveLayout, SnapshotRunningAgents, StopLiveAgent, StructuredSessions,
|
||||
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent,
|
||||
UpdateAgentContext, UpdateAgentPermissions, UpdateLiveState, UpdateMemory,
|
||||
UpdateProjectContext, UpdateProjectPermissions, UpdateSkill, UpdateTemplate, WriteMemory,
|
||||
WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||
};
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, AgentSessionFactory, Clock, Embedder, EmbedderEnvInspector,
|
||||
@ -59,8 +58,7 @@ use infrastructure::{
|
||||
FsProjectStore, FsProviderSessionStore, FsSkillStore, FsTemplateStore, Git2Repository,
|
||||
HeuristicHandoffSummarizer, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
|
||||
LocalFileSystem, LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall,
|
||||
OrchestratorWatchHandle, PortablePtyAdapter, RwFileGuard,
|
||||
StructuredSessionFactory,
|
||||
OrchestratorWatchHandle, PortablePtyAdapter, RwFileGuard, StructuredSessionFactory,
|
||||
SystemClock, SystemMillisClock, TokioBroadcastEventBus, TokioScheduler, UuidGenerator,
|
||||
VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS,
|
||||
VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||
@ -235,10 +233,7 @@ impl AppReconcileLiveState {
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError`] si le projet est inconnu (registre) ou si le store échoue.
|
||||
pub(crate) async fn execute(
|
||||
&self,
|
||||
input: ReconcileLiveStateInput,
|
||||
) -> Result<(), AppError> {
|
||||
pub(crate) async fn execute(&self, input: ReconcileLiveStateInput) -> Result<(), AppError> {
|
||||
let project = self.projects.load_project(input.project_id).await?;
|
||||
let store = Arc::new(FsLiveStateStore::new(&project.root));
|
||||
let uc = ReconcileLiveState::new(
|
||||
@ -928,15 +923,9 @@ impl AppState {
|
||||
// LaunchAgent shares the SAME pty_port and terminal_sessions as the terminal
|
||||
// use cases — indispensable for the PtyBridge to work correctly.
|
||||
//
|
||||
// Option 1 « Terminal + MCP » (lot B-2) : on **ne câble plus** la fabrique
|
||||
// structurée. La vue humaine d'un agent est désormais le **terminal brut
|
||||
// natif** (PTY interactif) — réflexion live + Échap natifs CLI, zéro parsing —
|
||||
// et la délégation inter-agents passe par les outils MCP (`idea_ask_agent` /
|
||||
// `idea_reply`), pas par une `AgentChatView`. Sans `with_structured`, le point
|
||||
// de routage §17.4 de `LaunchAgent::execute` retombe **toujours** sur le chemin
|
||||
// PTH : tout profil (Claude/Codex inclus) ouvre une cellule terminal. Le code
|
||||
// `launch_structured` reste en place (mort-code retiré au lot de nettoyage B-6).
|
||||
let _session_factory = session_factory; // décâblé en B-2 (nettoyage B-6)
|
||||
// The human-facing launcher intentionally stays PTY-only: when the user opens an
|
||||
// agent cell, they keep the native Claude/Codex CLI and its commands. Inter-agent
|
||||
// delegation gets its own launcher below, wired to structured/headless sessions.
|
||||
|
||||
// --- Permission projectors (lot LP3-5) ---
|
||||
// UN seul registre, source unique de vérité, injecté à l'identique dans
|
||||
@ -989,6 +978,40 @@ impl AppState {
|
||||
}) as Arc<dyn LiveStateLeanProvider>),
|
||||
);
|
||||
|
||||
// Inter-agent launcher: same context, memory, permissions and live-state
|
||||
// injection as the human launcher, but with the structured factory wired. It is
|
||||
// used only by OrchestratorService::ask_agent when a delegated target must be
|
||||
// started headlessly; UI launches still go through `launch_agent` above.
|
||||
let orchestrator_launch_agent = Arc::new(
|
||||
LaunchAgent::new(
|
||||
Arc::clone(&contexts_port),
|
||||
Arc::clone(&profile_store_port),
|
||||
Arc::clone(&runtime_port),
|
||||
Arc::clone(&fs_port),
|
||||
Arc::clone(&pty_port),
|
||||
Arc::clone(&skill_store_port),
|
||||
Arc::clone(&terminal_sessions),
|
||||
Arc::clone(&events_port),
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
Arc::clone(&memory_recall_port),
|
||||
Some(Arc::clone(&check_embedder_suggestion)),
|
||||
)
|
||||
.with_permission_store(Arc::clone(&permission_store_port))
|
||||
.with_handoff_provider(
|
||||
Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>
|
||||
)
|
||||
.with_provider_session_provider(Arc::new(AppProviderSessionProvider)
|
||||
as Arc<dyn application::ProviderSessionProvider>)
|
||||
.with_permission_projectors(Arc::clone(&permission_projectors))
|
||||
.with_live_state_lean(Arc::new(AppLiveStateLeanProvider {
|
||||
clock: Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
}) as Arc<dyn LiveStateLeanProvider>)
|
||||
.with_structured(
|
||||
Arc::clone(&session_factory),
|
||||
Arc::clone(&structured_sessions),
|
||||
),
|
||||
);
|
||||
|
||||
// Hot-swap an agent's runtime profile (§15.1). Reuses the shared context/
|
||||
// profile/project/fs stores, the live-session registry and PTY port, and
|
||||
// *composes* the launcher above for the in-place relaunch (no duplication).
|
||||
@ -1034,9 +1057,11 @@ impl AppState {
|
||||
// Backstop no-reply : observateur de fin de tour transcript (Claude `turn_duration`)
|
||||
// — lit le même `<home>/.claude/projects/<encoded-run-dir>/` que l'inspecteur, via
|
||||
// le même `FileSystem`. Armé par agent supporté au lancement (cf. `arm_turn_watch`).
|
||||
let turn_watcher: Arc<dyn domain::ports::TurnWatcher> = Arc::new(
|
||||
infrastructure::ClaudeTranscriptTurnWatcher::new(Arc::clone(&fs_port), home_dir.clone()),
|
||||
);
|
||||
let turn_watcher: Arc<dyn domain::ports::TurnWatcher> =
|
||||
Arc::new(infrastructure::ClaudeTranscriptTurnWatcher::new(
|
||||
Arc::clone(&fs_port),
|
||||
home_dir.clone(),
|
||||
));
|
||||
let inspect_conversation = Arc::new(InspectConversation::new(
|
||||
Arc::clone(&contexts_port),
|
||||
Arc::clone(&profile_store_port),
|
||||
@ -1259,10 +1284,6 @@ impl AppState {
|
||||
// (rebind de la cellule-vue, zéro spawn) et stop (kill PTY via la primitive
|
||||
// `CloseTerminal` existante / shutdown structuré). Aucune création de session.
|
||||
let attach_live_agent = Arc::new(AttachLiveAgent::new(Arc::clone(&live_sessions)));
|
||||
let stop_live_agent = Arc::new(StopLiveAgent::new(
|
||||
Arc::clone(&live_sessions),
|
||||
Arc::clone(&close_terminal),
|
||||
));
|
||||
|
||||
// --- Limites de session des agents (ARCHITECTURE §21, LS7) ---
|
||||
// Service pur-ports « détecter → planifier → reprendre » câblé sur l'existant :
|
||||
@ -1334,7 +1355,7 @@ impl AppState {
|
||||
let orchestrator_service = Arc::new(
|
||||
OrchestratorService::new(
|
||||
Arc::clone(&create_agent),
|
||||
Arc::clone(&launch_agent),
|
||||
Arc::clone(&orchestrator_launch_agent),
|
||||
Arc::clone(&list_agents),
|
||||
Arc::clone(&close_terminal),
|
||||
Arc::clone(&update_agent_context),
|
||||
@ -1412,7 +1433,8 @@ impl AppState {
|
||||
);
|
||||
let cwd = domain::project::ProjectPath::new(run_dir).ok()?;
|
||||
infrastructure::transcript_activity_token(fs.as_ref(), &home, &cwd).await
|
||||
}) as std::pin::Pin<Box<dyn std::future::Future<Output = Option<u64>> + Send>>
|
||||
})
|
||||
as std::pin::Pin<Box<dyn std::future::Future<Output = Option<u64>> + Send>>
|
||||
}) as application::AskLivenessProbe
|
||||
})
|
||||
// Plafond absolu du rendez-vous délégué (réglage projet via
|
||||
@ -1422,19 +1444,20 @@ impl AppState {
|
||||
std::env::var("IDEA_ASK_RENDEZVOUS_CEILING_MS")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse::<u32>().ok()),
|
||||
)),
|
||||
// NB (régression corrigée) : on ne câble PAS `.with_structured(...)` ici.
|
||||
// Décision produit lot B-2 (« Option 1 Terminal + MCP », cf. construction
|
||||
// de `LaunchAgent` plus haut) : la fabrique structurée est décâblée, donc
|
||||
// AUCUN agent n'a de session structurée vivante — tous tournent en PTY brut
|
||||
// et la délégation passe par les outils MCP (`idea_ask_agent`/`idea_reply`).
|
||||
// Si l'orchestrateur recevait `with_structured`, `ask_agent` emprunterait la
|
||||
// branche structurée (`ensure_structured_session`) qui ne peut jamais aboutir
|
||||
// (le launcher ne crée plus de session structurée) ⇒ erreur systématique
|
||||
// « aucune session structurée vivante après lancement ». On laisse donc
|
||||
// `self.structured = None` pour que `ask_agent` retombe sur le chemin PTY+MCP
|
||||
// fonctionnel. `drain_with_readiness` (readiness/heartbeat lot 1) reste dormant
|
||||
// tant que la voie structurée n'est pas réactivée au composition root.
|
||||
))
|
||||
// Conversation inter-agent headless : le service voit le même registre que le
|
||||
// launcher orchestrateur ci-dessus. Une cible à `structured_adapter` est donc
|
||||
// démarrée/drainée via `AgentSession::send` et son `Final`, sans dépendre de
|
||||
// `idea_reply`; les autres outils MCP restent câblés par ailleurs.
|
||||
.with_structured(Arc::clone(&structured_sessions)),
|
||||
);
|
||||
|
||||
let stop_live_agent = Arc::new(
|
||||
StopLiveAgent::new(Arc::clone(&live_sessions), Arc::clone(&close_terminal))
|
||||
.with_cascade(
|
||||
Arc::clone(&input_mediator),
|
||||
Arc::clone(&orchestrator_service),
|
||||
),
|
||||
);
|
||||
|
||||
// --- Windows (L10) ---
|
||||
@ -1627,59 +1650,10 @@ impl AppState {
|
||||
service_for_ready.release_agent_cold_start(AgentId::from_uuid(uuid));
|
||||
}
|
||||
});
|
||||
// Borne du rendez-vous `idea_ask_agent` (filet serveur, plancher universel FINI) :
|
||||
// réglage projet optionnel via `IDEA_ASK_RENDEZVOUS_TIMEOUT_MS`. Absent / invalide /
|
||||
// `0` ⇒ défaut fini (600 s) — jamais (quasi-)infini, sinon une cible silencieuse
|
||||
// wedge l'appelant.
|
||||
let ask_timeout = infrastructure::resolve_ask_rendezvous_timeout(
|
||||
std::env::var("IDEA_ASK_RENDEZVOUS_TIMEOUT_MS")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse::<u32>().ok()),
|
||||
);
|
||||
// Plafond absolu du rendez-vous (réglage projet optionnel via
|
||||
// `IDEA_ASK_RENDEZVOUS_CEILING_MS`, défaut 4 h) : borne dure que la fenêtre
|
||||
// d'inactivité réarmée ne dépasse jamais, même contre une cible perpétuellement active.
|
||||
let ask_ceiling = infrastructure::resolve_ask_rendezvous_ceiling(
|
||||
std::env::var("IDEA_ASK_RENDEZVOUS_CEILING_MS")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse::<u32>().ok()),
|
||||
);
|
||||
// Sonde d'activité de la cible (signe de vie du rendez-vous) : l'McpServer (infra) ne
|
||||
// connaît la cible que par son NOM ; la composition root est la seule à savoir
|
||||
// résoudre nom→AgentId puis dériver le run-dir transcript Claude. La sonde renvoie un
|
||||
// jeton monotone = octets cumulés des `.jsonl` de la cible (croît même pendant un seul
|
||||
// long tour sans `turn_duration`). `None` ⇒ cible/transcript introuvable ⇒ traité comme
|
||||
// « pas de progrès » par le watchdog.
|
||||
let service_for_probe = Arc::clone(&self.orchestrator_service);
|
||||
let fs_for_probe = Arc::clone(&self.fs_port);
|
||||
let home_for_probe = self.home_dir.clone();
|
||||
let project_for_probe = project.clone();
|
||||
let activity_probe: infrastructure::AskActivityProbe = Arc::new(move |target: String| {
|
||||
let service = Arc::clone(&service_for_probe);
|
||||
let fs = Arc::clone(&fs_for_probe);
|
||||
let home = home_for_probe.clone();
|
||||
let project = project_for_probe.clone();
|
||||
Box::pin(async move {
|
||||
let agent_id = service
|
||||
.resolve_agent_id_by_name(&project, &target)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let run_dir = format!(
|
||||
"{}/.ideai/run/{agent_id}",
|
||||
project.root.as_str().trim_end_matches(['/', '\\'])
|
||||
);
|
||||
let cwd = domain::project::ProjectPath::new(run_dir).ok()?;
|
||||
infrastructure::transcript_activity_token(fs.as_ref(), &home, &cwd).await
|
||||
})
|
||||
});
|
||||
let handle = McpServerHandle::start(
|
||||
McpServer::new(Arc::clone(&self.orchestrator_service), project.clone())
|
||||
.with_events(events)
|
||||
.with_ready_sink(ready_sink)
|
||||
.with_ask_rendezvous_timeout(ask_timeout)
|
||||
.with_ask_rendezvous_ceiling(ask_ceiling)
|
||||
.with_activity_probe(activity_probe),
|
||||
.with_ready_sink(ready_sink),
|
||||
endpoint,
|
||||
listener,
|
||||
project_id,
|
||||
@ -3368,8 +3342,6 @@ mod mcp_serve_peer_tests {
|
||||
let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
||||
for expected in [
|
||||
"idea_list_agents",
|
||||
"idea_ask_agent",
|
||||
"idea_reply",
|
||||
"idea_launch_agent",
|
||||
"idea_stop_agent",
|
||||
"idea_update_context",
|
||||
@ -3390,10 +3362,12 @@ mod mcp_serve_peer_tests {
|
||||
"missing tool {expected}; got {names:?}"
|
||||
);
|
||||
}
|
||||
assert!(!names.contains(&"idea_ask_agent"));
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
14,
|
||||
"exactly the fourteen idea_* tools (7 base + 4 FileGuard C7 + idea_skill_read + 2 live-state LS4); got {names:?}"
|
||||
12,
|
||||
"exactly the twelve non-conversation idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
drop(client); // EOF ⇒ serve loop ends
|
||||
|
||||
Reference in New Issue
Block a user