agentmux_srv/server/agent_handlers/
skills.rs1use std::sync::Arc;
6
7
8use crate::backend::rpc::engine::WshRpcEngine;
9use crate::backend::rpc_types::{
10 COMMAND_LIST_AGENT_SKILLS, COMMAND_CREATE_AGENT_SKILL, COMMAND_UPDATE_AGENT_SKILL,
11 COMMAND_DELETE_AGENT_SKILL,
12 CommandListAgentSkillsData, CommandCreateAgentSkillData, CommandUpdateAgentSkillData,
13 CommandDeleteAgentSkillData,
14};
15use crate::backend::storage::AgentSkill;
16
17use super::super::AppState;
18
19pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
20 let mstore_lfs = state.mstore.clone();
33 let identity_store_lfs = state.identity_store.clone();
34 engine.register_typed(
35 COMMAND_LIST_AGENT_SKILLS,
36 move |cmd: CommandListAgentSkillsData, _ctx| {
37 let mstore = mstore_lfs.clone();
38 let identity_store = identity_store_lfs.clone();
39 async move {
40 let skills = mstore.effective_skills(&identity_store, &cmd.agent_id);
41 Ok(skills)
42 }
43 },
44 );
45
46 let mstore_cfs = state.mstore.clone();
48 let broker_cfs = state.broker.clone();
49 engine.register_typed(
50 COMMAND_CREATE_AGENT_SKILL,
51 move |cmd: CommandCreateAgentSkillData, _ctx| {
52 let mstore = mstore_cfs.clone();
53 let broker = broker_cfs.clone();
54 async move {
55 let now = agentmux_common::time::now_ms();
56 let skill = AgentSkill {
57 id: uuid::Uuid::new_v4().to_string(),
58 agent_id: cmd.agent_id,
59 name: cmd.name,
60 trigger: cmd.trigger,
61 skill_type: cmd.skill_type,
62 description: cmd.description,
63 content: cmd.content,
64 created_at: now,
65 };
66 mstore.agent_skill_insert(&skill).map_err(|e| format!("createagentskill: {e}"))?;
67 broker.publish_event::<crate::backend::mps_events::AgentSkillsChanged>(&(), vec![], 0);
68 Ok(skill)
69 }
70 },
71 );
72
73 let mstore_ufs = state.mstore.clone();
75 let broker_ufs = state.broker.clone();
76 engine.register_typed(
77 COMMAND_UPDATE_AGENT_SKILL,
78 move |cmd: CommandUpdateAgentSkillData, _ctx| {
79 let mstore = mstore_ufs.clone();
80 let broker = broker_ufs.clone();
81 async move {
82 let existing = mstore.agent_skill_get(&cmd.id)
83 .map_err(|e| format!("updateagentskill: {e}"))?
84 .ok_or_else(|| format!("updateagentskill: skill {} not found", cmd.id))?;
85 let skill = AgentSkill {
86 id: cmd.id,
87 agent_id: existing.agent_id,
88 name: cmd.name,
89 trigger: cmd.trigger,
90 skill_type: cmd.skill_type,
91 description: cmd.description,
92 content: cmd.content,
93 created_at: existing.created_at,
94 };
95 let found = mstore.agent_skill_update(&skill).map_err(|e| format!("updateagentskill: {e}"))?;
96 if !found {
97 return Err(format!("updateagentskill: skill {} not found", skill.id));
98 }
99 broker.publish_event::<crate::backend::mps_events::AgentSkillsChanged>(&(), vec![], 0);
100 Ok(skill)
101 }
102 },
103 );
104
105 let mstore_dfs = state.mstore.clone();
107 let broker_dfs = state.broker.clone();
108 engine.register_typed(
109 COMMAND_DELETE_AGENT_SKILL,
110 move |cmd: CommandDeleteAgentSkillData, _ctx| {
111 let mstore = mstore_dfs.clone();
112 let broker = broker_dfs.clone();
113 async move {
114 mstore.agent_skill_delete(&cmd.id).map_err(|e| format!("deleteagentskill: {e}"))?;
115 broker.publish_event::<crate::backend::mps_events::AgentSkillsChanged>(&(), vec![], 0);
116 Ok(())
117 }
118 },
119 );
120
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::backend::rpc_types::{
127 CommandCreateAgentSkillData, CommandUpdateAgentSkillData,
128 };
129
130 #[tokio::test]
132 async fn register_typed_records_every_skill_command() {
133 let state = crate::server::tests::test_state();
134 let (engine, _rx) = WshRpcEngine::new();
135 register(&engine, &state);
136 let schema = engine.schema_json();
137 let rows = schema.as_array().unwrap();
138 let find = |cmd: &str| {
139 rows.iter()
140 .find(|r| r["command"] == cmd)
141 .unwrap_or_else(|| panic!("{cmd} missing from the schema"))
142 };
143
144 for (cmd, req, resp) in [
145 (COMMAND_CREATE_AGENT_SKILL, "CommandCreateAgentSkillData", "AgentSkill"),
146 (COMMAND_UPDATE_AGENT_SKILL, "CommandUpdateAgentSkillData", "AgentSkill"),
147 (COMMAND_DELETE_AGENT_SKILL, "CommandDeleteAgentSkillData", "()"),
148 ] {
149 let row = find(cmd);
150 assert_eq!(row["requestName"], req, "{cmd} request");
151 assert_eq!(row["responseName"], resp, "{cmd} response");
152 }
153
154 let list = find(COMMAND_LIST_AGENT_SKILLS);
155 assert_eq!(list["requestName"], "CommandListAgentSkillsData");
156 let list_resp = list["responseName"].as_str().unwrap();
157 assert!(
158 list_resp.starts_with("alloc::vec::Vec<") && list_resp.contains("AgentSkill"),
159 "listagentskills should answer Vec<AgentSkill>, got {list_resp:?}",
160 );
161 }
162
163 #[test]
177 fn create_and_update_disagree_about_the_default_skill_type() {
178 let created: CommandCreateAgentSkillData = serde_json::from_value(serde_json::json!({
179 "agent_id": "a1",
180 "name": "review",
181 }))
182 .unwrap();
183 assert_eq!(created.skill_type, "prompt", "createagentskill defaults to prompt");
184
185 let updated: CommandUpdateAgentSkillData = serde_json::from_value(serde_json::json!({
186 "id": "s1",
187 "name": "review",
188 }))
189 .unwrap();
190 assert_eq!(
191 updated.skill_type, "",
192 "updateagentskill defaults to empty -- if this ever becomes \"prompt\", \
193 the two are consistent and the warning on CommandUpdateAgentSkillData \
194 should be removed with it",
195 );
196
197 for (c, u) in [
199 (created.trigger.as_str(), updated.trigger.as_str()),
200 (created.description.as_str(), updated.description.as_str()),
201 (created.content.as_str(), updated.content.as_str()),
202 ] {
203 assert_eq!(c, "");
204 assert_eq!(u, "");
205 }
206 }
207}