feat(maestro): select ACP adapter by command
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user