Skip to content

Commit 6c15bb7

Browse files
authored
fix: resolve ACP backends from metadata (#559)
## Summary - Remove the hardcoded ACP vendor label list and resolve ACP backends from agent_metadata rows instead. - Use metadata yolo_id for ACP session mode when creating or rebuilding team agent runtime config. - Add a regression test for a metadata-defined ACP backend. ## Test Plan - cargo fmt --all -- --check - cargo clippy -p aionui-team -- -D warnings - cargo test -p aionui-team --quiet - cargo test -p aionui-team --test e2e_smoke --quiet - cargo test -p aionui-team --test e2e_team_flow --quiet - cargo test -p aionui-team --test session_service_integration provisioning_resolves_acp_backend_from_agent_metadata --quiet Co-authored-by: zk <>
1 parent 155c278 commit 6c15bb7

3 files changed

Lines changed: 142 additions & 72 deletions

File tree

crates/aionui-team/src/provisioning.rs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::sync::Arc;
33
use aionui_ai_agent::IWorkerTaskManager;
44
use aionui_api_types::{AddAgentRequest, TeamAgentInput};
55
use aionui_common::{AgentKillReason, AgentType, ProviderWithModel, generate_id};
6-
use aionui_db::models::TeamRow;
6+
use aionui_db::models::{AgentMetadataRow, TeamRow};
77
use aionui_db::{
88
IAgentMetadataRepository, IAssistantDefinitionRepository, IAssistantOverlayRepository, IProviderRepository,
99
ITeamRepository, UpdateTeamParams,
@@ -14,7 +14,9 @@ use tracing::{info, warn};
1414
use crate::error::TeamError;
1515
use crate::mcp::TeamMcpStdioConfig;
1616
use crate::service::inherit_team_workspace;
17-
use crate::service::spawn_support::{parse_agent_type, resolve_full_auto_mode, resolve_runtime_backend};
17+
use crate::service::spawn_support::{
18+
acp_backend_metadata, parse_agent_type, resolve_runtime_backend, session_mode_for_backend,
19+
};
1820
use crate::types::{Team, TeamAgent, TeammateRole};
1921
use crate::workspace::TeamWorkspaceResolver;
2022

@@ -358,9 +360,16 @@ impl TeamAgentProvisioner {
358360
agent: &TeamAgent,
359361
mcp_stdio_cfg: TeamMcpStdioConfig,
360362
) -> Result<(), TeamError> {
363+
let acp_metadata = acp_backend_metadata(&self.agent_metadata_repo, &agent.backend).await?;
364+
let agent_type = if acp_metadata.is_some() {
365+
AgentType::Acp
366+
} else {
367+
parse_agent_type(&agent.backend)?
368+
};
369+
let session_mode = session_mode_for_backend(&agent.backend, agent_type, acp_metadata.as_ref());
361370
let patch = serde_json::json!({
362371
"team_mcp_stdio_config": mcp_stdio_cfg,
363-
"session_mode": resolve_full_auto_mode(&agent.backend),
372+
"session_mode": session_mode,
364373
});
365374
self.conversation_port
366375
.patch_runtime_config(&agent.conversation_id, patch)
@@ -430,11 +439,23 @@ impl TeamAgentProvisioner {
430439
assistant_id: Option<&str>,
431440
workspace: Option<&str>,
432441
) -> Result<ProvisionedConversation, TeamError> {
433-
let extra = self
434-
.build_team_extra(team_id, slot_id, role, backend, model, assistant_id, workspace)
435-
.await?;
436-
437-
let agent_type = parse_agent_type(backend)?;
442+
let acp_metadata = acp_backend_metadata(&self.agent_metadata_repo, backend).await?;
443+
let agent_type = if acp_metadata.is_some() {
444+
AgentType::Acp
445+
} else {
446+
parse_agent_type(backend)?
447+
};
448+
let extra = self.build_team_extra(
449+
team_id,
450+
slot_id,
451+
role,
452+
backend,
453+
model,
454+
assistant_id,
455+
workspace,
456+
agent_type,
457+
acp_metadata.as_ref(),
458+
);
438459
let provider_id = if agent_type == AgentType::Aionrs {
439460
self.resolve_provider_for_model(model)
440461
.await
@@ -524,7 +545,7 @@ impl TeamAgentProvisioner {
524545
}
525546

526547
#[allow(clippy::too_many_arguments)]
527-
async fn build_team_extra(
548+
fn build_team_extra(
528549
&self,
529550
team_id: &str,
530551
slot_id: &str,
@@ -533,15 +554,18 @@ impl TeamAgentProvisioner {
533554
model: &str,
534555
assistant_id: Option<&str>,
535556
workspace: Option<&str>,
536-
) -> Result<serde_json::Value, TeamError> {
557+
agent_type: AgentType,
558+
acp_metadata: Option<&AgentMetadataRow>,
559+
) -> serde_json::Value {
560+
let session_mode = session_mode_for_backend(backend, agent_type, acp_metadata);
537561
let mut extra = serde_json::json!({
538562
"teamId": team_id,
539563
"slot_id": slot_id,
540564
"role": role.to_string(),
541565
"backend": backend,
542-
"session_mode": resolve_full_auto_mode(backend),
566+
"session_mode": session_mode,
543567
});
544-
if parse_agent_type(backend)? != AgentType::Aionrs {
568+
if agent_type != AgentType::Aionrs {
545569
extra["current_model_id"] = serde_json::Value::String(model.to_owned());
546570
}
547571
if let Some(assistant_id) = assistant_id {
@@ -550,7 +574,7 @@ impl TeamAgentProvisioner {
550574
if let Some(workspace) = workspace {
551575
inherit_team_workspace(&mut extra, workspace);
552576
}
553-
Ok(extra)
577+
extra
554578
}
555579

556580
async fn persist_agents(&self, team_id: &str, agents: &[TeamAgent]) -> Result<(), TeamError> {

crates/aionui-team/src/service/spawn_support.rs

Lines changed: 28 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::*;
22
use aionui_api_types::BehaviorPolicy;
33
use aionui_common::AgentType;
44
use aionui_common::constants::{TEAM_CAPABLE_BACKENDS, has_mcp_capability};
5-
use aionui_db::models::AssistantOverlayRow;
5+
use aionui_db::models::{AgentMetadataRow, AssistantOverlayRow};
66
use aionui_db::{IAgentMetadataRepository, resolve_agent_binding_from_rows};
77
use std::collections::HashMap;
88
use std::sync::Arc;
@@ -11,39 +11,9 @@ use crate::prompts::AvailableAssistant;
1111

1212
use crate::provisioning::PersistSpawnedAgentRequest;
1313

14-
/// Known ACP vendor labels. Kept in lockstep with the `agent_metadata`
15-
/// seed in `005_agent_metadata.sql` — a caller hitting an unknown
16-
/// vendor should trigger a schema drift discussion, not silently fall
17-
/// through.
18-
const ACP_VENDOR_LABELS: &[&str] = &[
19-
"claude",
20-
"codex",
21-
"gemini",
22-
"qwen",
23-
"codebuddy",
24-
"droid",
25-
"goose",
26-
"auggie",
27-
"kimi",
28-
"opencode",
29-
"copilot",
30-
"qoder",
31-
"vibe",
32-
"cursor",
33-
"kiro",
34-
"hermes",
35-
"snow",
36-
];
37-
3814
const DEPRECATED_AGENT_TYPE_MESSAGE: &str = "This agent type is no longer supported for new conversations.";
3915

4016
pub(crate) fn parse_agent_type(backend: &str) -> Result<AgentType, TeamError> {
41-
// Any registered ACP vendor label collapses to `AgentType::Acp`.
42-
if ACP_VENDOR_LABELS.contains(&backend) {
43-
return Ok(AgentType::Acp);
44-
}
45-
// Otherwise interpret as a top-level `AgentType` (e.g. "acp",
46-
// "nanobot", "aionrs", "remote", "openclaw-gateway").
4717
let quoted = format!("\"{backend}\"");
4818
if let Ok(agent_type) = serde_json::from_str::<AgentType>(&quoted) {
4919
if agent_type.is_deprecated_runtime() {
@@ -54,16 +24,31 @@ pub(crate) fn parse_agent_type(backend: &str) -> Result<AgentType, TeamError> {
5424
Err(TeamError::InvalidRequest(format!("unsupported backend: {backend}")))
5525
}
5626

57-
/// Resolve the most permissive session mode for a given backend string.
58-
/// Reuses `AgentType::full_auto_mode_id` from aionui-common.
59-
pub(crate) fn resolve_full_auto_mode(backend: &str) -> &'static str {
60-
let agent_type = if ACP_VENDOR_LABELS.contains(&backend) {
61-
AgentType::Acp
62-
} else {
63-
let quoted = format!("\"{backend}\"");
64-
serde_json::from_str::<AgentType>(&quoted).unwrap_or(AgentType::Acp)
65-
};
66-
agent_type.full_auto_mode_id(Some(backend))
27+
fn find_acp_backend_metadata(rows: &[AgentMetadataRow], backend: &str) -> Option<AgentMetadataRow> {
28+
rows.iter()
29+
.find(|row| row.agent_type == AgentType::Acp.serde_name() && row.backend.as_deref() == Some(backend))
30+
.cloned()
31+
}
32+
33+
pub(crate) async fn acp_backend_metadata(
34+
agent_metadata_repo: &Arc<dyn IAgentMetadataRepository>,
35+
backend: &str,
36+
) -> Result<Option<AgentMetadataRow>, TeamError> {
37+
let rows = agent_metadata_repo.list_all().await?;
38+
Ok(find_acp_backend_metadata(&rows, backend))
39+
}
40+
41+
pub(crate) fn session_mode_for_backend(
42+
backend: &str,
43+
agent_type: AgentType,
44+
acp_metadata: Option<&AgentMetadataRow>,
45+
) -> String {
46+
if let Some(row) = acp_metadata
47+
&& let Some(yolo_id) = row.yolo_id.as_deref().map(str::trim).filter(|value| !value.is_empty())
48+
{
49+
return yolo_id.to_owned();
50+
}
51+
agent_type.full_auto_mode_id(Some(backend)).to_owned()
6752
}
6853

6954
pub(crate) async fn resolve_runtime_backend(
@@ -486,15 +471,14 @@ mod tests {
486471
}
487472

488473
#[test]
489-
fn parse_agent_type_known_backends() {
474+
fn parse_agent_type_accepts_top_level_supported_runtimes() {
490475
assert_eq!(parse_agent_type("acp").unwrap(), AgentType::Acp);
491-
assert_eq!(parse_agent_type("gemini").unwrap(), AgentType::Acp);
492476
assert_eq!(parse_agent_type("aionrs").unwrap(), AgentType::Aionrs);
493477
}
494478

495479
#[test]
496480
fn parse_agent_type_rejects_deprecated_runtime_types() {
497-
for backend in ["nanobot", "remote", "openclaw-gateway"] {
481+
for backend in ["codex", "gemini", "nanobot", "remote", "openclaw-gateway"] {
498482
let err = parse_agent_type(backend).unwrap_err();
499483
assert!(matches!(err, TeamError::InvalidRequest(_)));
500484
assert!(
@@ -511,11 +495,6 @@ mod tests {
511495
assert!(matches!(err, TeamError::InvalidRequest(_)));
512496
}
513497

514-
#[test]
515-
fn resolve_full_auto_mode_keeps_hermes_on_default() {
516-
assert_eq!(resolve_full_auto_mode("hermes"), "default");
517-
}
518-
519498
#[tokio::test]
520499
async fn persist_spawned_agent_uses_team_workspace_resolver() {
521500
let (svc, team_repo, _, conv_repo) = setup_with_factory_metadata_team_repo_and_conversation_repo();

crates/aionui-team/tests/session_service_integration.rs

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,20 @@ fn make_agent_metadata_row(id: &str, backend: &str, icon: &str) -> AgentMetadata
16591659
}
16601660
}
16611661

1662+
fn acp_agent_metadata_row(id: &str, backend: &str, yolo_id: Option<&str>) -> AgentMetadataRow {
1663+
let mut row = make_agent_metadata_row(id, backend, "");
1664+
row.yolo_id = yolo_id.map(str::to_owned);
1665+
row
1666+
}
1667+
1668+
fn seeded_agent_metadata_repo() -> Arc<dyn IAgentMetadataRepository> {
1669+
Arc::new(StubAgentMetadataRepo::with_rows(vec![
1670+
acp_agent_metadata_row("claude-id", "claude", Some("bypassPermissions")),
1671+
acp_agent_metadata_row("codex-id", "codex", Some("full-access")),
1672+
acp_agent_metadata_row("gemini-id", "gemini", Some("yolo")),
1673+
]))
1674+
}
1675+
16621676
fn word_creator_definition() -> AssistantDefinitionRow {
16631677
AssistantDefinitionRow {
16641678
id: "def-word-creator".into(),
@@ -1941,7 +1955,7 @@ async fn tc_create_team_prefers_assistant_avatar_over_backend_logo() {
19411955

19421956
#[tokio::test]
19431957
async fn tc_create_team_carries_assistant_identity_into_lead_conversation_extra() {
1944-
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = Arc::new(StubAgentMetadataRepo::empty());
1958+
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = seeded_agent_metadata_repo();
19451959
let definition_repo: Arc<dyn IAssistantDefinitionRepository> = Arc::new(SingleAssistantDefinitionRepo {
19461960
row: AssistantDefinitionRow {
19471961
id: "def-team-lead".into(),
@@ -2017,7 +2031,7 @@ async fn tc_create_team_carries_assistant_identity_into_lead_conversation_extra(
20172031

20182032
#[tokio::test]
20192033
async fn tc_create_team_derives_backend_from_assistant_when_backend_missing() {
2020-
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = Arc::new(StubAgentMetadataRepo::empty());
2034+
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = seeded_agent_metadata_repo();
20212035
let definition_repo: Arc<dyn IAssistantDefinitionRepository> = Arc::new(SingleAssistantDefinitionRepo {
20222036
row: AssistantDefinitionRow {
20232037
id: "def-team-lead".into(),
@@ -2104,7 +2118,7 @@ async fn tc_create_team_derives_backend_from_assistant_when_backend_missing() {
21042118

21052119
#[tokio::test]
21062120
async fn tc_create_team_ignores_requested_backend_when_assistant_id_present() {
2107-
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = Arc::new(StubAgentMetadataRepo::empty());
2121+
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = seeded_agent_metadata_repo();
21082122
let definition_repo: Arc<dyn IAssistantDefinitionRepository> = Arc::new(SingleAssistantDefinitionRepo {
21092123
row: AssistantDefinitionRow {
21102124
id: "def-team-lead".into(),
@@ -2208,7 +2222,7 @@ async fn team_preset_assistant_snapshot_is_frozen() {
22082222
});
22092223
let (svc, _team_repo, conversation_ports, conv_repo) = setup_with_ports_metadata_assistants_and_conversation_repo(
22102224
success_factory(),
2211-
Arc::new(StubAgentMetadataRepo::empty()),
2225+
seeded_agent_metadata_repo(),
22122226
definition_repo,
22132227
Arc::new(EmptyAssistantOverlayRepo),
22142228
);
@@ -2255,7 +2269,7 @@ async fn spawned_preset_assistant_snapshot_is_frozen() {
22552269
});
22562270
let (svc, _team_repo, conversation_ports, conv_repo) = setup_with_ports_metadata_assistants_and_conversation_repo(
22572271
success_factory(),
2258-
Arc::new(StubAgentMetadataRepo::empty()),
2272+
seeded_agent_metadata_repo(),
22592273
definition_repo,
22602274
Arc::new(EmptyAssistantOverlayRepo),
22612275
);
@@ -2400,7 +2414,7 @@ async fn ta_add_agent_derives_backend_from_assistant_when_backend_missing() {
24002414
updated_at: 0,
24012415
},
24022416
});
2403-
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = Arc::new(StubAgentMetadataRepo::empty());
2417+
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = seeded_agent_metadata_repo();
24042418
let (svc, _team_repo, _task_manager, _conv_repo) = setup_with_factory_metadata_assistants_and_conversation_repo(
24052419
success_factory(),
24062420
agent_metadata_repo,
@@ -2495,7 +2509,7 @@ async fn ta_add_agent_ignores_requested_backend_when_assistant_id_present() {
24952509
updated_at: 0,
24962510
},
24972511
});
2498-
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = Arc::new(StubAgentMetadataRepo::empty());
2512+
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = seeded_agent_metadata_repo();
24992513
let (svc, _team_repo, _task_manager, _conv_repo) = setup_with_factory_metadata_assistants_and_conversation_repo(
25002514
success_factory(),
25012515
agent_metadata_repo,
@@ -3267,6 +3281,59 @@ async fn provisioning_writes_typed_team_binding_for_create_and_add_agent() {
32673281
);
32683282
}
32693283

3284+
#[tokio::test]
3285+
async fn provisioning_resolves_acp_backend_from_agent_metadata() {
3286+
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> =
3287+
Arc::new(StubAgentMetadataRepo::with_rows(vec![acp_agent_metadata_row(
3288+
"future-acp-id",
3289+
"future-acp",
3290+
Some("turbo"),
3291+
)]));
3292+
let (svc, _, conv_repo) =
3293+
setup_with_factory_and_metadata_and_conversation_repo(success_factory(), agent_metadata_repo);
3294+
3295+
let created = svc
3296+
.create_team(
3297+
"user1",
3298+
CreateTeamRequest {
3299+
name: "Metadata ACP".into(),
3300+
agents: vec![TeamAgentInput {
3301+
name: "Lead".into(),
3302+
role: "lead".into(),
3303+
backend: Some("future-acp".into()),
3304+
model: "model-x".into(),
3305+
assistant_id: None,
3306+
conversation_id: None,
3307+
}],
3308+
workspace: None,
3309+
},
3310+
)
3311+
.await
3312+
.unwrap();
3313+
3314+
let row = conv_repo
3315+
.get(&created.assistants[0].conversation_id)
3316+
.await
3317+
.unwrap()
3318+
.expect("conversation row");
3319+
assert_eq!(row.r#type, "acp");
3320+
let extra = conv_repo
3321+
.get_extra(&created.assistants[0].conversation_id)
3322+
.expect("conversation extra");
3323+
assert_eq!(
3324+
extra.get("backend").and_then(serde_json::Value::as_str),
3325+
Some("future-acp")
3326+
);
3327+
assert_eq!(
3328+
extra.get("session_mode").and_then(serde_json::Value::as_str),
3329+
Some("turbo")
3330+
);
3331+
assert_eq!(
3332+
extra.get("provider_id").and_then(serde_json::Value::as_str),
3333+
Some("future-acp")
3334+
);
3335+
}
3336+
32703337
#[tokio::test]
32713338
async fn aa4_add_agent_to_nonexistent_team() {
32723339
let svc = setup();
@@ -3443,7 +3510,7 @@ async fn spawn_agent_in_session_succeeds_without_active_team_run() {
34433510
updated_at: 0,
34443511
},
34453512
});
3446-
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = Arc::new(StubAgentMetadataRepo::empty());
3513+
let agent_metadata_repo: Arc<dyn IAgentMetadataRepository> = seeded_agent_metadata_repo();
34473514
let (svc, _team_repo, _task_manager, _conv_repo) = setup_with_factory_metadata_assistants_and_conversation_repo(
34483515
success_factory(),
34493516
agent_metadata_repo,
@@ -3539,7 +3606,7 @@ async fn lead_send_agent_message_in_session_requires_active_team_run() {
35393606
async fn spawn_agent_in_session_aborts_lease_when_persistence_fails() {
35403607
let (svc, team_repo, _, _) = setup_with_factory_metadata_assistants_and_conversation_repo(
35413608
success_factory(),
3542-
Arc::new(StubAgentMetadataRepo::empty()),
3609+
seeded_agent_metadata_repo(),
35433610
Arc::new(SingleAssistantDefinitionRepo {
35443611
row: word_creator_definition(),
35453612
}),
@@ -3586,7 +3653,7 @@ async fn spawn_agent_in_session_aborts_lease_when_persistence_fails() {
35863653
async fn spawn_agent_in_session_compensates_when_welcome_mailbox_write_fails() {
35873654
let (svc, team_repo, _, _) = setup_with_factory_metadata_assistants_and_conversation_repo(
35883655
success_factory(),
3589-
Arc::new(StubAgentMetadataRepo::empty()),
3656+
seeded_agent_metadata_repo(),
35903657
Arc::new(SingleAssistantDefinitionRepo {
35913658
row: word_creator_definition(),
35923659
}),

0 commit comments

Comments
 (0)