feat(maestro): select ACP adapter by command

This commit is contained in:
Miguel Palhas
2026-09-06 18:11:04 +01:00
parent 0c46fb993a
commit bbf622ed9b
4 changed files with 241 additions and 71 deletions
+66 -30
View File
@@ -15,7 +15,8 @@ use std::task::{Context, Poll};
use std::time::Duration;
use chrono::Utc;
use maestro_acp::{AcpSession, PermissionRequest, SpawnConfig};
use maestro_acp::{AcpAdapter, AcpSession, PermissionRequest, SpawnConfig};
use maestro_protocol::display::command_tool;
use maestro_protocol::{
ActivityState, CatalogEvent, CreateSessionRequest, ErrorInfo, NameSource, Session,
SessionStatus, SessionView, StructuredReplayEvent, TranscriptDelta, TranscriptRow,
@@ -59,6 +60,16 @@ pub const DEFAULT_BLOCKED_WORK_INTERVAL: Duration = Duration::from_secs(15);
/// actually go.
const SUBMIT_DELAY: Duration = Duration::from_millis(150);
fn structured_adapter(command: &str) -> Result<AcpAdapter, String> {
match command_tool(command) {
Some("claude") => Ok(AcpAdapter::ClaudeCode),
Some("codex") => Ok(AcpAdapter::Codex),
_ => Err(format!(
"structured view supports Claude Code or Codex commands; cannot select an ACP adapter for {command:?}"
)),
}
}
/// Per-session daemon-local bookkeeping that complements the public `Session`.
#[derive(Debug)]
pub struct SessionRecord {
@@ -1250,14 +1261,18 @@ impl SessionManager {
// up still yields a session — Dead, with the reason on it — so the
// operator sees what to fix instead of a silently missing session.
let (acp, acp_failure) = match req.view {
SessionView::Structured => {
match maestro_acp::spawn(SpawnConfig::new(
&effective_cwd,
session_env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
))
SessionView::Structured => match structured_adapter(&req.command) {
Err(error) => (None, Some(error)),
Ok(adapter) => match maestro_acp::spawn(
SpawnConfig::new(
&effective_cwd,
session_env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
)
.adapter(adapter),
)
.await
{
Ok(acp) => (Some(Arc::new(acp)), None),
@@ -1265,8 +1280,8 @@ impl SessionManager {
tracing::warn!(%id, error = %e, "structured session: adapter failed to start");
(None, Some(e.to_string()))
}
}
}
},
},
SessionView::Terminal => {
let spawned = {
let tmux = self.inner.tmux.clone();
@@ -1674,23 +1689,30 @@ impl SessionManager {
let (acp, failure) = if recoverable {
match session.acp_session_id.clone() {
Some(acp_session_id) => {
let config = SpawnConfig::new(
&session.cwd,
session_env
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect(),
)
.load_session(acp_session_id);
match maestro_acp::spawn(config).await {
Ok(acp) => (Some(Arc::new(acp)), None),
Err(error) => {
tracing::warn!(%id, %error, "structured session: adapter failed to resume");
(None, Some(error.to_string()))
Some(acp_session_id) => match structured_adapter(&session.command) {
Err(error) => {
tracing::warn!(%id, %error, "structured session: unsupported command during recovery");
(None, Some(error))
}
Ok(adapter) => {
let config = SpawnConfig::new(
&session.cwd,
session_env
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect(),
)
.load_session(acp_session_id)
.adapter(adapter);
match maestro_acp::spawn(config).await {
Ok(acp) => (Some(Arc::new(acp)), None),
Err(error) => {
tracing::warn!(%id, %error, "structured session: adapter failed to resume");
(None, Some(error.to_string()))
}
}
}
}
},
None => (
None,
Some("structured session has no stored ACP session id".into()),
@@ -2969,15 +2991,18 @@ mod tests {
use std::task::Poll;
use std::time::Duration;
use maestro_acp::schema::{
PermissionOption, PermissionOptionKind, RequestPermissionOutcome, RequestPermissionRequest,
ToolCallUpdate, ToolCallUpdateFields,
use maestro_acp::{
schema::{
PermissionOption, PermissionOptionKind, RequestPermissionOutcome,
RequestPermissionRequest, ToolCallUpdate, ToolCallUpdateFields,
},
AcpAdapter,
};
use maestro_protocol::{ActivityState, CatalogEvent, CreateSessionRequest, SessionView};
use super::{
apply_structured_activity, cancel_pending_approvals, permission_outcome,
record_permission_request, run_structured_activity_pass,
record_permission_request, run_structured_activity_pass, structured_adapter,
sync_structured_activity_from_stream, AcpDispatchState, AcpEventStream, ApprovalDecision,
RecordOutcome, SessionManager,
};
@@ -3001,6 +3026,17 @@ mod tests {
)
}
#[test]
fn structured_adapter_uses_the_shared_command_resolution() {
assert_eq!(structured_adapter("claude"), Ok(AcpAdapter::ClaudeCode));
assert_eq!(
structured_adapter("sudo -u agent -- /opt/bin/codex"),
Ok(AcpAdapter::Codex)
);
assert!(structured_adapter("opencode").is_err());
assert!(structured_adapter("claude && codex").is_err());
}
fn stream(log: EventLog) -> AcpEventStream {
let (live, _live_rx) = tokio::sync::broadcast::channel(16);
let (projected_live, _projected_rx) = tokio::sync::broadcast::channel(16);