fix(agent): rendre explicite le routage structuré vs PTY dans LaunchAgent (#33)
Un profil structuré rencontrant une factory de session non câblée retombait
silencieusement sur pty.spawn (fallthrough du `if let`), masquant une erreur de
configuration au lieu de la signaler.
Remplace ce fallthrough par une intention explicite :
- nouvel enum StructuredRoutingMode { HumanPtyFallback, RequireStructured } sur
LaunchAgent, posé via le builder with_structured_routing_mode ;
- le `if let` devient un `match` explicite (agent/lifecycle.rs) ; en mode
RequireStructured, un profil structuré sans factory câblée retourne
AppError::Process("structured profile requires structured session factory")
au lieu de tomber sur pty.spawn.
Composition root (app-tauri/src/state.rs) : launcher humain = HumanPtyFallback,
launcher orchestrateur = RequireStructured, wake background rebranché sur
orchestrator_launch_agent.
Tests : agent_lifecycle.rs (4 branches de routage) + non-régression
agent_wake/structured_launch_d3. Crate application verte.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -36,10 +36,10 @@ use application::{
|
|||||||
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, RotateConversationLog,
|
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, RotateConversationLog,
|
||||||
SaveEmbedderProfile, SaveModelServer, SaveProfile, SessionLimitService, SetActiveLayout,
|
SaveEmbedderProfile, SaveModelServer, SaveProfile, SessionLimitService, SetActiveLayout,
|
||||||
SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent,
|
SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent,
|
||||||
StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions,
|
StructuredRoutingMode, StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate,
|
||||||
UnassignSkillFromAgent, UnassignTicketFromSprint, UnlinkIssues, UpdateAgentContext,
|
TerminalSessions, UnassignSkillFromAgent, UnassignTicketFromSprint, UnlinkIssues,
|
||||||
UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory,
|
UpdateAgentContext, UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState,
|
||||||
UpdateProjectContext, UpdateProjectPermissions, UpdateSkill, UpdateTemplate,
|
UpdateMemory, UpdateProjectContext, UpdateProjectPermissions, UpdateSkill, UpdateTemplate,
|
||||||
WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@ -1568,6 +1568,7 @@ impl AppState {
|
|||||||
Arc::clone(&memory_recall_port),
|
Arc::clone(&memory_recall_port),
|
||||||
Some(Arc::clone(&check_embedder_suggestion)),
|
Some(Arc::clone(&check_embedder_suggestion)),
|
||||||
)
|
)
|
||||||
|
.with_structured_routing_mode(StructuredRoutingMode::HumanPtyFallback)
|
||||||
.with_permission_store(Arc::clone(&permission_store_port))
|
.with_permission_store(Arc::clone(&permission_store_port))
|
||||||
// Reprise conversationnelle (lot P7) : à chaque (re)lancement, si la cellule
|
// Reprise conversationnelle (lot P7) : à chaque (re)lancement, si la cellule
|
||||||
// porte une conversation et qu'un handoff existe (`<root>/.ideai/conversations/`),
|
// porte une conversation et qu'un handoff existe (`<root>/.ideai/conversations/`),
|
||||||
@ -1613,6 +1614,7 @@ impl AppState {
|
|||||||
Arc::clone(&memory_recall_port),
|
Arc::clone(&memory_recall_port),
|
||||||
Some(Arc::clone(&check_embedder_suggestion)),
|
Some(Arc::clone(&check_embedder_suggestion)),
|
||||||
)
|
)
|
||||||
|
.with_structured_routing_mode(StructuredRoutingMode::RequireStructured)
|
||||||
.with_permission_store(Arc::clone(&permission_store_port))
|
.with_permission_store(Arc::clone(&permission_store_port))
|
||||||
.with_handoff_provider(
|
.with_handoff_provider(
|
||||||
Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>
|
Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>
|
||||||
@ -1910,7 +1912,7 @@ impl AppState {
|
|||||||
Arc::clone(&mailbox),
|
Arc::clone(&mailbox),
|
||||||
Arc::clone(&background_tasks_port),
|
Arc::clone(&background_tasks_port),
|
||||||
Arc::new(AppWakeSessionProvider {
|
Arc::new(AppWakeSessionProvider {
|
||||||
launch_agent: Arc::clone(&launch_agent),
|
launch_agent: Arc::clone(&orchestrator_launch_agent),
|
||||||
structured_sessions: Arc::clone(&structured_sessions),
|
structured_sessions: Arc::clone(&structured_sessions),
|
||||||
}),
|
}),
|
||||||
Some(Arc::clone(&events_port)),
|
Some(Arc::clone(&events_port)),
|
||||||
|
|||||||
@ -998,6 +998,15 @@ pub struct LaunchAgentOutput {
|
|||||||
pub profile: Option<AgentProfile>,
|
pub profile: Option<AgentProfile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Explicit routing intent for profiles that declare a structured adapter.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum StructuredRoutingMode {
|
||||||
|
/// Human-facing cells intentionally keep the native CLI/TUI in a PTY.
|
||||||
|
HumanPtyFallback,
|
||||||
|
/// Headless/orchestrated launchers must route structured profiles via AgentSession.
|
||||||
|
RequireStructured,
|
||||||
|
}
|
||||||
|
|
||||||
/// Registry mapping each [`ProjectorKey`] to its concrete
|
/// Registry mapping each [`ProjectorKey`] to its concrete
|
||||||
/// [`PermissionProjector`] (lot LP3-3). Injected into [`LaunchAgent`] via the
|
/// [`PermissionProjector`] (lot LP3-3). Injected into [`LaunchAgent`] via the
|
||||||
/// optional builder [`LaunchAgent::with_permission_projectors`]; the concrete
|
/// optional builder [`LaunchAgent::with_permission_projectors`]; the concrete
|
||||||
@ -1141,6 +1150,8 @@ pub struct LaunchAgent {
|
|||||||
/// `opencode.json` (B35). Optional for legacy tests/wiring; required at runtime
|
/// `opencode.json` (B35). Optional for legacy tests/wiring; required at runtime
|
||||||
/// when `OpenCodeConfig.localModelServerId` is set.
|
/// when `OpenCodeConfig.localModelServerId` is set.
|
||||||
local_model_server: Option<Arc<EnsureLocalModelServer>>,
|
local_model_server: Option<Arc<EnsureLocalModelServer>>,
|
||||||
|
/// Explicit intent for structured profiles when structured ports are absent.
|
||||||
|
structured_routing_mode: StructuredRoutingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LaunchAgent {
|
impl LaunchAgent {
|
||||||
@ -1180,9 +1191,17 @@ impl LaunchAgent {
|
|||||||
projectors: None,
|
projectors: None,
|
||||||
live_state_lean: None,
|
live_state_lean: None,
|
||||||
local_model_server: None,
|
local_model_server: None,
|
||||||
|
structured_routing_mode: StructuredRoutingMode::HumanPtyFallback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the explicit routing intent for profiles carrying a structured adapter.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn with_structured_routing_mode(mut self, mode: StructuredRoutingMode) -> Self {
|
||||||
|
self.structured_routing_mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Injects the local model-server ensure use case used by OpenCode profiles.
|
/// Injects the local model-server ensure use case used by OpenCode profiles.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_local_model_server(mut self, ensure: Arc<EnsureLocalModelServer>) -> Self {
|
pub fn with_local_model_server(mut self, ensure: Arc<EnsureLocalModelServer>) -> Self {
|
||||||
@ -1712,51 +1731,60 @@ impl LaunchAgent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 5b. ── POINT DE ROUTAGE §17.4 : IA structuré vs terminal brut ──
|
// 5b. ── POINT DE ROUTAGE §17.4 : IA structuré vs terminal brut ──
|
||||||
// Le convention file (CLAUDE.md / AGENTS.md) vient d'être écrit dans le
|
// L'intention est explicite sur le launcher : les cellules humaines peuvent
|
||||||
// run dir (étape 5) ; la CLI structurée le lira à chaque tour
|
// garder une CLI/TUI native en PTY, les launchers headless/orchestrés doivent
|
||||||
// (incarnation « un run par tour »). Si le profil porte un
|
// échouer fort si le routage structuré est mal câblé.
|
||||||
// `structured_adapter` ET que la fabrique structurée est câblée, on
|
match (
|
||||||
// démarre une `AgentSession` via le port — **pas** de `pty.spawn` — et on
|
profile.structured_adapter.is_some(),
|
||||||
// l'enregistre dans `StructuredSessions`. Sinon : chemin PTY inchangé.
|
|
||||||
if let (Some(factory), Some(structured), true) = (
|
|
||||||
self.session_factory.as_ref(),
|
self.session_factory.as_ref(),
|
||||||
self.structured.as_ref(),
|
self.structured.as_ref(),
|
||||||
profile.structured_adapter.is_some(),
|
self.structured_routing_mode,
|
||||||
) {
|
) {
|
||||||
// ── Clé **logique** de la cellule = id de paire IdeA (ARCHITECTURE §19.7,
|
(false, _, _, _) => {}
|
||||||
// lot P8a) ──
|
(true, Some(factory), Some(structured), _) => {
|
||||||
// - cellule porteuse d'un `conversation_id` (resume, ou lancement délégué
|
// ── Clé **logique** de la cellule = id de paire IdeA (ARCHITECTURE §19.7,
|
||||||
// qui a déjà injecté l'id de paire A↔B) ⇒ c'est **déjà** l'id de paire,
|
// lot P8a) ──
|
||||||
// on le conserve tel quel ;
|
// - cellule porteuse d'un `conversation_id` (resume, ou lancement délégué
|
||||||
// - cellule structurée **neuve** (lancement direct utilisateur, aucun
|
// qui a déjà injecté l'id de paire A↔B) ⇒ c'est **déjà** l'id de paire,
|
||||||
// requester) ⇒ on **dérive** `pair(User, agent)` via la fonction domaine
|
// on le conserve tel quel ;
|
||||||
// pure partagée avec `resolve_conversation` (aucun couplage à
|
// - cellule structurée **neuve** (lancement direct utilisateur, aucun
|
||||||
// l'orchestrateur). C'est cet id — et **non** l'id de session moteur —
|
// requester) ⇒ on **dérive** `pair(User, agent)` via la fonction domaine
|
||||||
// qui retrouve log + handoff au (re)lancement (P7) et survit au swap.
|
// pure partagée avec `resolve_conversation` (aucun couplage à
|
||||||
let pair_conversation_id = input.conversation_id.clone().unwrap_or_else(|| {
|
// l'orchestrateur). C'est cet id — et **non** l'id de session moteur —
|
||||||
ConversationId::for_pair(
|
// qui retrouve log + handoff au (re)lancement (P7) et survit au swap.
|
||||||
ConversationParty::User,
|
let pair_conversation_id = input.conversation_id.clone().unwrap_or_else(|| {
|
||||||
ConversationParty::agent(agent.id),
|
ConversationId::for_pair(
|
||||||
)
|
ConversationParty::User,
|
||||||
.to_string()
|
ConversationParty::agent(agent.id),
|
||||||
});
|
)
|
||||||
return self
|
.to_string()
|
||||||
.launch_structured(
|
});
|
||||||
factory.as_ref(),
|
return self
|
||||||
structured,
|
.launch_structured(
|
||||||
&agent,
|
factory.as_ref(),
|
||||||
&profile,
|
structured,
|
||||||
&prepared,
|
&agent,
|
||||||
&run_dir,
|
&profile,
|
||||||
&session_plan,
|
&prepared,
|
||||||
pair_conversation_id,
|
&run_dir,
|
||||||
&input.project.root,
|
&session_plan,
|
||||||
input.node_id,
|
pair_conversation_id,
|
||||||
size,
|
&input.project.root,
|
||||||
&spec.env,
|
input.node_id,
|
||||||
spec.sandbox.as_ref(),
|
size,
|
||||||
)
|
&spec.env,
|
||||||
.await;
|
spec.sandbox.as_ref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
(true, _, _, StructuredRoutingMode::HumanPtyFallback) => {
|
||||||
|
// Fallback humain intentionnel : cellule interactive native en PTY.
|
||||||
|
}
|
||||||
|
(true, _, _, StructuredRoutingMode::RequireStructured) => {
|
||||||
|
return Err(AppError::Process(
|
||||||
|
"structured profile requires structured session factory".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Spawn the PTY at the resolved cwd; adopt its session id everywhere.
|
// 6. Spawn the PTY at the resolved cwd; adopt its session id everywhere.
|
||||||
|
|||||||
@ -34,8 +34,8 @@ pub use lifecycle::{
|
|||||||
InjectedLiveRow, LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
InjectedLiveRow, LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
||||||
ListAgentsOutput, LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry,
|
ListAgentsOutput, LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry,
|
||||||
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
||||||
StructuredSessionDescriptor, UpdateAgentContext, UpdateAgentContextInput,
|
StructuredRoutingMode, StructuredSessionDescriptor, UpdateAgentContext,
|
||||||
AGENT_MEMORY_RECALL_BUDGET, LIVE_STATE_INJECT_MAX,
|
UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET, LIVE_STATE_INJECT_MAX,
|
||||||
};
|
};
|
||||||
pub use resume::{
|
pub use resume::{
|
||||||
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
|
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
|
||||||
|
|||||||
@ -52,8 +52,9 @@ pub use agent::{
|
|||||||
PermissionProjectorRegistry, ProfileAvailability, ProviderSessionProvider, ReadAgentContext,
|
PermissionProjectorRegistry, ProfileAvailability, ProviderSessionProvider, ReadAgentContext,
|
||||||
ReadAgentContextInput, ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput,
|
ReadAgentContextInput, ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput,
|
||||||
ResumableAgent, SaveProfile, SaveProfileInput, SaveProfileOutput, SessionLimitService,
|
ResumableAgent, SaveProfile, SaveProfileInput, SaveProfileOutput, SessionLimitService,
|
||||||
StructuredSessionDescriptor, TurnOutcome, UpdateAgentContext, UpdateAgentContextInput,
|
StructuredRoutingMode, StructuredSessionDescriptor, TurnOutcome, UpdateAgentContext,
|
||||||
AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS, LIVE_STATE_INJECT_MAX, RESUME_PROMPT,
|
UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS,
|
||||||
|
LIVE_STATE_INJECT_MAX, RESUME_PROMPT,
|
||||||
};
|
};
|
||||||
pub use background::{
|
pub use background::{
|
||||||
BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput,
|
BackgroundCommandArchive, CancelBackgroundTask, CancelBackgroundTaskOutput,
|
||||||
|
|||||||
@ -28,10 +28,11 @@ use domain::permission::{
|
|||||||
ProjectionContext, ProjectorKey,
|
ProjectionContext, ProjectorKey,
|
||||||
};
|
};
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
AgentContextStore, AgentRuntime, ContextInjectionPlan, DirEntry, EventBus, EventStream,
|
AgentContextStore, AgentRuntime, AgentSession, AgentSessionError, AgentSessionFactory,
|
||||||
ExitStatus, FileSystem, FsError, IdGenerator, MemoryError, MemoryQuery, MemoryRecall,
|
ContextInjectionPlan, DirEntry, EventBus, EventStream, ExitStatus, FileSystem, FsError,
|
||||||
OutputStream, PermissionStore, PreparedContext, ProfileStore, PtyError, PtyHandle, PtyPort,
|
IdGenerator, MemoryError, MemoryQuery, MemoryRecall, OutputStream, PermissionStore,
|
||||||
RemotePath, RuntimeError, SessionPlan, SkillStore, SpawnSpec, StoreError,
|
PreparedContext, ProfileStore, PtyError, PtyHandle, PtyPort, RemotePath, ReplyStream,
|
||||||
|
RuntimeError, SessionPlan, SkillStore, SpawnSpec, StoreError,
|
||||||
};
|
};
|
||||||
use domain::profile::{
|
use domain::profile::{
|
||||||
AgentProfile, ContextInjection, McpCapability, McpConfigStrategy, McpTransport, OpenCodeConfig,
|
AgentProfile, ContextInjection, McpCapability, McpConfigStrategy, McpTransport, OpenCodeConfig,
|
||||||
@ -48,7 +49,8 @@ use uuid::Uuid;
|
|||||||
use application::{
|
use application::{
|
||||||
CreateAgentFromScratch, CreateAgentInput, DeleteAgent, DeleteAgentInput, LaunchAgent,
|
CreateAgentFromScratch, CreateAgentInput, DeleteAgent, DeleteAgentInput, LaunchAgent,
|
||||||
LaunchAgentInput, ListAgents, ListAgentsInput, PermissionProjectorRegistry, ReadAgentContext,
|
LaunchAgentInput, ListAgents, ListAgentsInput, PermissionProjectorRegistry, ReadAgentContext,
|
||||||
ReadAgentContextInput, TerminalSessions, UpdateAgentContext, UpdateAgentContextInput,
|
ReadAgentContextInput, StructuredRoutingMode, StructuredSessions, TerminalSessions,
|
||||||
|
UpdateAgentContext, UpdateAgentContextInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -557,6 +559,83 @@ impl PtyPort for FakePty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FakeStructuredFactory (AgentSessionFactory) — records structured starts
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FakeSession {
|
||||||
|
id: SessionId,
|
||||||
|
conversation_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AgentSession for FakeSession {
|
||||||
|
fn id(&self) -> SessionId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn conversation_id(&self) -> Option<String> {
|
||||||
|
self.conversation_id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send(&self, _prompt: &str) -> Result<ReplyStream, AgentSessionError> {
|
||||||
|
Ok(Box::new(std::iter::empty()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown(&self) -> Result<(), AgentSessionError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FakeStructuredFactory {
|
||||||
|
trace: Trace,
|
||||||
|
starts: Arc<Mutex<Vec<ProfileId>>>,
|
||||||
|
next_session: SessionId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeStructuredFactory {
|
||||||
|
fn new(trace: Trace, next_session: SessionId) -> Self {
|
||||||
|
Self {
|
||||||
|
trace,
|
||||||
|
starts: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
next_session,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn starts(&self) -> Vec<ProfileId> {
|
||||||
|
self.starts.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AgentSessionFactory for FakeStructuredFactory {
|
||||||
|
fn supports(&self, profile: &AgentProfile) -> bool {
|
||||||
|
profile.structured_adapter.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start(
|
||||||
|
&self,
|
||||||
|
profile: &AgentProfile,
|
||||||
|
_ctx: &PreparedContext,
|
||||||
|
_cwd: &ProjectPath,
|
||||||
|
_session: &SessionPlan,
|
||||||
|
_env: &[(String, String)],
|
||||||
|
_sandbox: Option<&domain::sandbox::SandboxPlan>,
|
||||||
|
) -> Result<Arc<dyn AgentSession>, AgentSessionError> {
|
||||||
|
self.trace
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push("structured.start".to_owned());
|
||||||
|
self.starts.lock().unwrap().push(profile.id);
|
||||||
|
Ok(Arc::new(FakeSession {
|
||||||
|
id: self.next_session,
|
||||||
|
conversation_id: Some("engine-session-1".to_owned()),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// SpyBus + SeqIds
|
// SpyBus + SeqIds
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -941,6 +1020,108 @@ async fn launch_orders_prepare_then_injection_then_spawn() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn structured_profile_with_factory_routes_to_structured_session_without_pty_spawn() {
|
||||||
|
let profile = profile(
|
||||||
|
pid(9),
|
||||||
|
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||||
|
)
|
||||||
|
.with_structured_adapter(StructuredAdapter::Claude);
|
||||||
|
let (launch, agent, _fs, pty, _bus, _sessions, tr, _session) = launch_fixture_with_profile(
|
||||||
|
profile,
|
||||||
|
Some(ContextInjectionPlan::File {
|
||||||
|
target: "CLAUDE.md".to_owned(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let factory = FakeStructuredFactory::new(Arc::clone(&tr), sid(888));
|
||||||
|
let factory_port: Arc<dyn AgentSessionFactory> = Arc::new(factory.clone());
|
||||||
|
let structured = Arc::new(StructuredSessions::new());
|
||||||
|
let launch = launch
|
||||||
|
.with_structured_routing_mode(StructuredRoutingMode::RequireStructured)
|
||||||
|
.with_structured(factory_port, Arc::clone(&structured));
|
||||||
|
|
||||||
|
let out = launch.execute(launch_input(agent.id)).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(factory.starts(), vec![pid(9)]);
|
||||||
|
assert!(
|
||||||
|
pty.spawns().is_empty(),
|
||||||
|
"structured route must not spawn PTY"
|
||||||
|
);
|
||||||
|
assert!(out.structured.is_some());
|
||||||
|
assert!(structured.session(&sid(888)).is_some());
|
||||||
|
assert_eq!(
|
||||||
|
*tr.lock().unwrap(),
|
||||||
|
vec![
|
||||||
|
"prepare".to_owned(),
|
||||||
|
"fs.write".to_owned(),
|
||||||
|
"structured.start".to_owned()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn structured_profile_without_factory_require_structured_errors_without_pty_spawn() {
|
||||||
|
let profile = profile(
|
||||||
|
pid(9),
|
||||||
|
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||||
|
)
|
||||||
|
.with_structured_adapter(StructuredAdapter::Claude);
|
||||||
|
let (launch, agent, _fs, pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||||
|
profile,
|
||||||
|
Some(ContextInjectionPlan::File {
|
||||||
|
target: "CLAUDE.md".to_owned(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let launch = launch.with_structured_routing_mode(StructuredRoutingMode::RequireStructured);
|
||||||
|
|
||||||
|
let err = launch.execute(launch_input(agent.id)).await.unwrap_err();
|
||||||
|
|
||||||
|
assert!(err
|
||||||
|
.to_string()
|
||||||
|
.contains("structured profile requires structured session factory"));
|
||||||
|
assert!(
|
||||||
|
pty.spawns().is_empty(),
|
||||||
|
"miswired structured route must not spawn PTY"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn structured_profile_without_factory_human_fallback_spawns_pty() {
|
||||||
|
let profile = profile(
|
||||||
|
pid(9),
|
||||||
|
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||||
|
)
|
||||||
|
.with_structured_adapter(StructuredAdapter::Claude);
|
||||||
|
let (launch, agent, _fs, pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||||
|
profile,
|
||||||
|
Some(ContextInjectionPlan::File {
|
||||||
|
target: "CLAUDE.md".to_owned(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let launch = launch.with_structured_routing_mode(StructuredRoutingMode::HumanPtyFallback);
|
||||||
|
|
||||||
|
let out = launch.execute(launch_input(agent.id)).await.unwrap();
|
||||||
|
|
||||||
|
assert!(out.structured.is_none());
|
||||||
|
assert_eq!(pty.spawns().len(), 1, "human fallback keeps native CLI PTY");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn non_structured_profile_uses_pty_even_when_structured_is_required() {
|
||||||
|
let (launch, agent, _fs, pty, _bus, _sessions, _tr, _session) = launch_fixture(
|
||||||
|
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||||
|
Some(ContextInjectionPlan::File {
|
||||||
|
target: "CLAUDE.md".to_owned(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let launch = launch.with_structured_routing_mode(StructuredRoutingMode::RequireStructured);
|
||||||
|
|
||||||
|
let out = launch.execute(launch_input(agent.id)).await.unwrap();
|
||||||
|
|
||||||
|
assert!(out.structured.is_none());
|
||||||
|
assert_eq!(pty.spawns().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn launch_injects_shared_project_context_from_ideai_context_md() {
|
async fn launch_injects_shared_project_context_from_ideai_context_md() {
|
||||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture(
|
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture(
|
||||||
|
|||||||
Reference in New Issue
Block a user