Skip to content

Commit d9c2502

Browse files
authored
fix(agent): expose runtime catalogs from metadata (#523)
## Summary - Expose `config_options`, `available_modes`, and `available_models` on `/api/agents/management` rows from persisted `agent_metadata` handshake data. - Seed and migrate built-in aionrs runtime mode catalog from `agent_metadata`, keyed by `agent_type='aionrs'` and `agent_source='internal'` instead of a fixed id. - Add regression coverage for management catalog projection and legacy aionrs-id migration behavior. ## Test Plan - `cargo test -p aionui-db seed_rows_include_icon_backfill` - `cargo test -p aionui-db migration_015_populates_aionrs_catalog_by_agent_type` - `cargo test -p aionui-ai-agent management_rows_project_runtime_catalogs_from_agent_metadata` - `cargo test -p aionui-ai-agent management_rows_include_aionrs_builtin_mode_catalog` - `cargo fmt --all -- --check` Co-authored-by: zk <>
1 parent 712cc6b commit d9c2502

8 files changed

Lines changed: 197 additions & 1 deletion

File tree

crates/aionui-ai-agent/src/registry.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ impl AgentRegistry {
280280
.map(|meta| {
281281
let status = derive_management_status(&meta);
282282
let diagnostics = derive_management_diagnostics(&meta, status);
283+
let handshake = meta.handshake;
283284
AgentManagementRow {
284285
id: meta.id,
285286
icon: meta.icon,
@@ -299,6 +300,9 @@ impl AgentRegistry {
299300
native_skills_dirs: meta.native_skills_dirs,
300301
behavior_policy: meta.behavior_policy,
301302
yolo_id: meta.yolo_id,
303+
config_options: handshake.config_options.clone(),
304+
available_modes: handshake.available_modes.clone(),
305+
available_models: handshake.available_models.clone(),
302306
sort_order: meta.sort_order,
303307
team_capable: meta.team_capable,
304308
status,

crates/aionui-ai-agent/src/registry_tests.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,91 @@ async fn management_rows_derive_missing_diagnostics_from_probe_reason() {
223223
Some("definitely-missing-cli")
224224
);
225225
}
226+
227+
#[tokio::test]
228+
async fn management_rows_project_runtime_catalogs_from_agent_metadata() {
229+
let db = init_database_memory().await.unwrap();
230+
let repo: Arc<dyn IAgentMetadataRepository> = Arc::new(SqliteAgentMetadataRepository::new(db.pool().clone()));
231+
232+
repo.upsert(&UpsertAgentMetadataParams {
233+
id: "agent-with-catalog",
234+
icon: None,
235+
name: "Catalog Agent",
236+
name_i18n: None,
237+
description: None,
238+
description_i18n: None,
239+
backend: Some("claude".into()),
240+
agent_type: "acp",
241+
agent_source: "builtin",
242+
agent_source_info: None,
243+
enabled: true,
244+
command: None,
245+
args: Some("[]"),
246+
env: Some("[]"),
247+
native_skills_dirs: None,
248+
behavior_policy: None,
249+
yolo_id: None,
250+
agent_capabilities: None,
251+
auth_methods: None,
252+
config_options: Some(
253+
r#"{"config_options":[{"id":"model","type":"select","category":"model","options":[{"value":"claude-opus","label":"Claude Opus"}],"current_value":"claude-opus"}]}"#,
254+
),
255+
available_modes: Some(
256+
r#"{"current_mode_id":"plan","available_modes":[{"id":"plan","name":"Plan"}]}"#,
257+
),
258+
available_models: Some(
259+
r#"{"current_model_id":"claude-opus","current_model_label":"Claude Opus","available_models":[{"id":"claude-opus","label":"Claude Opus"}]}"#,
260+
),
261+
available_commands: None,
262+
sort_order: 100,
263+
})
264+
.await
265+
.unwrap();
266+
267+
let registry = AgentRegistry::new(repo);
268+
registry.hydrate().await.unwrap();
269+
270+
let row = registry
271+
.list_management_rows()
272+
.await
273+
.into_iter()
274+
.find(|item| item.id == "agent-with-catalog")
275+
.unwrap();
276+
let row_json = serde_json::to_value(&row).unwrap();
277+
278+
assert_eq!(
279+
row_json["available_models"]["current_model_id"].as_str(),
280+
Some("claude-opus")
281+
);
282+
assert_eq!(row_json["available_modes"]["current_mode_id"].as_str(), Some("plan"));
283+
assert_eq!(
284+
row_json["config_options"]["config_options"][0]["current_value"].as_str(),
285+
Some("claude-opus")
286+
);
287+
}
288+
289+
#[tokio::test]
290+
async fn management_rows_include_aionrs_builtin_mode_catalog() {
291+
let db = init_database_memory().await.unwrap();
292+
let repo: Arc<dyn IAgentMetadataRepository> = Arc::new(SqliteAgentMetadataRepository::new(db.pool().clone()));
293+
let registry = AgentRegistry::new(repo);
294+
registry.hydrate().await.unwrap();
295+
296+
let row = registry
297+
.list_management_rows()
298+
.await
299+
.into_iter()
300+
.find(|item| item.agent_type == AgentType::Aionrs)
301+
.unwrap();
302+
let row_json = serde_json::to_value(&row).unwrap();
303+
304+
assert_eq!(row_json["available_modes"]["current_mode_id"].as_str(), Some("default"));
305+
assert_eq!(
306+
row_json["available_modes"]["available_modes"][1]["id"].as_str(),
307+
Some("auto_edit")
308+
);
309+
assert_eq!(
310+
row_json["config_options"]["config_options"][0]["options"][2]["value"].as_str(),
311+
Some("yolo")
312+
);
313+
}

crates/aionui-api-types/src/agent_discovery.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ pub struct AgentManagementRow {
277277
pub behavior_policy: BehaviorPolicy,
278278
#[serde(default, skip_serializing_if = "Option::is_none")]
279279
pub yolo_id: Option<String>,
280+
#[serde(default, skip_serializing_if = "Option::is_none")]
281+
pub config_options: Option<serde_json::Value>,
282+
#[serde(default, skip_serializing_if = "Option::is_none")]
283+
pub available_modes: Option<serde_json::Value>,
284+
#[serde(default, skip_serializing_if = "Option::is_none")]
285+
pub available_models: Option<serde_json::Value>,
280286
pub sort_order: i64,
281287
#[serde(default)]
282288
pub team_capable: bool,

crates/aionui-app/tests/assistants_e2e.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ fn test_agent_row(id: &str, backend: Option<&str>, agent_type: AgentType, name:
9696
..Default::default()
9797
},
9898
yolo_id: None,
99+
config_options: None,
100+
available_modes: None,
101+
available_models: None,
99102
sort_order: 0,
100103
team_capable: true,
101104
status: AgentManagementStatus::Online,

crates/aionui-assistant/src/service.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2639,6 +2639,9 @@ mod tests {
26392639
..Default::default()
26402640
},
26412641
yolo_id: None,
2642+
config_options: None,
2643+
available_modes: None,
2644+
available_models: None,
26422645
sort_order: 3100,
26432646
team_capable: true,
26442647
status,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
-- Migration 015: persist the built-in aionrs mode catalog.
2+
--
3+
-- aionrs is not an ACP backend, so it does not populate agent_metadata through
4+
-- ACP handshake catalog sync. Pre-conversation surfaces such as Guid still read
5+
-- mode options from agent_metadata via /api/agents/management, so seed the
6+
-- stable aionrs runtime mode catalog here.
7+
8+
UPDATE agent_metadata
9+
SET
10+
available_modes = '{
11+
"current_mode_id": "default",
12+
"available_modes": [
13+
{ "id": "default", "name": "Default" },
14+
{ "id": "auto_edit", "name": "Auto Edit" },
15+
{ "id": "yolo", "name": "YOLO" }
16+
]
17+
}',
18+
config_options = '{
19+
"config_options": [
20+
{
21+
"id": "mode",
22+
"name": "Mode",
23+
"category": "mode",
24+
"type": "select",
25+
"current_value": "default",
26+
"options": [
27+
{ "value": "default", "name": "Default" },
28+
{ "value": "auto_edit", "name": "Auto Edit" },
29+
{ "value": "yolo", "name": "YOLO" }
30+
]
31+
}
32+
]
33+
}',
34+
updated_at = unixepoch('now','subsec')*1000
35+
WHERE agent_type = 'aionrs'
36+
AND agent_source = 'internal';

crates/aionui-db/src/repository/sqlite_agent_metadata.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,30 @@ mod tests {
364364
let claude = repo.get("2d23ff1c").await.unwrap().expect("seeded claude row");
365365
assert_eq!(claude.icon.as_deref(), Some("/api/assets/logos/ai-major/claude.svg"));
366366

367-
let aionrs = repo.get("632f31d2").await.unwrap().expect("seeded aion cli row");
367+
let rows = repo.list_all().await.unwrap();
368+
let aionrs = rows
369+
.iter()
370+
.find(|row| row.agent_type == "aionrs" && row.agent_source == "internal")
371+
.expect("seeded aion cli row");
368372
assert_eq!(aionrs.icon.as_deref(), Some("/api/assets/logos/brand/aion.svg"));
373+
let aionrs_modes: serde_json::Value =
374+
serde_json::from_str(aionrs.available_modes.as_deref().expect("aionrs modes catalog")).unwrap();
375+
assert_eq!(aionrs_modes["current_mode_id"].as_str(), Some("default"));
376+
assert_eq!(
377+
aionrs_modes["available_modes"]
378+
.as_array()
379+
.expect("aionrs available modes")
380+
.iter()
381+
.filter_map(|item| item.get("id").and_then(serde_json::Value::as_str))
382+
.collect::<Vec<_>>(),
383+
vec!["default", "auto_edit", "yolo"]
384+
);
385+
let aionrs_config_options: serde_json::Value =
386+
serde_json::from_str(aionrs.config_options.as_deref().expect("aionrs config options")).unwrap();
387+
assert_eq!(
388+
aionrs_config_options["config_options"][0]["options"][1]["value"].as_str(),
389+
Some("auto_edit")
390+
);
369391

370392
let kiro = repo.get("e044000d").await.unwrap().expect("seeded kiro row");
371393
assert!(kiro.icon.is_none());

crates/aionui-db/tests/cron_assistant_first_migration.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,40 @@ async fn seed_legacy_assistant_identity(pool: &sqlx::SqlitePool) {
160160
.unwrap();
161161
}
162162

163+
#[tokio::test]
164+
async fn migration_015_populates_aionrs_catalog_by_agent_type() {
165+
let pool = SqlitePoolOptions::new()
166+
.max_connections(1)
167+
.connect("sqlite::memory:")
168+
.await
169+
.unwrap();
170+
171+
run_migrations_through(&pool, 14).await;
172+
sqlx::query(
173+
"INSERT INTO agent_metadata (
174+
id, name, backend, command, agent_type, enabled, agent_source, sort_order, created_at, updated_at
175+
) VALUES ('agent-aionrs', 'Aion CLI', NULL, '', 'aionrs', 1, 'internal', 100, 1, 1)",
176+
)
177+
.execute(&pool)
178+
.await
179+
.unwrap();
180+
181+
run_migration(&pool, 15).await;
182+
183+
let row = sqlx::query(
184+
"SELECT available_modes, config_options
185+
FROM agent_metadata
186+
WHERE id = 'agent-aionrs'",
187+
)
188+
.fetch_one(&pool)
189+
.await
190+
.unwrap();
191+
let available_modes: String = row.get("available_modes");
192+
let config_options: String = row.get("config_options");
193+
assert!(available_modes.contains("\"auto_edit\""));
194+
assert!(config_options.contains("\"yolo\""));
195+
}
196+
163197
async fn insert_legacy_cron(
164198
pool: &sqlx::SqlitePool,
165199
id: &str,

0 commit comments

Comments
 (0)