1use async_trait::async_trait;
2use cookie_agent_engine::{
3 Engine, PreparedExecutor, PreparedTool, SessionToolContext, ToolCall, ToolError,
4 ToolExecutionContext, ToolPreparationContext, ToolProvider, ToolSpec,
5};
6use cookie_agent_protocol::{
7 ApprovalResourceSource, PermissionAction, PersistedToolResult as ToolResult,
8 PreparedBindingLifetime,
9};
10use serde::{Deserialize, Serialize};
11
12use crate::{parse_args, prepared_operation, prepared_resource, safe_title, schema, tool_error};
13
14pub struct SkillTool {
15 engine: Engine,
16}
17
18impl SkillTool {
19 #[must_use]
20 pub fn new(engine: Engine) -> Self {
21 Self { engine }
22 }
23}
24
25#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
26#[serde(deny_unknown_fields)]
27struct SkillArgs {
28 name: String,
29 #[serde(default)]
30 args: String,
31}
32
33struct SkillExecutor {
34 engine: Engine,
35 call_id: cookie_agent_protocol::ToolCallId,
36 args: SkillArgs,
37}
38
39#[async_trait]
40impl PreparedExecutor for SkillExecutor {
41 async fn revalidate(&self) -> Result<(), ToolError> {
42 Ok(())
43 }
44
45 async fn execute(
46 self: Box<Self>,
47 context: ToolExecutionContext,
48 ) -> Result<cookie_agent_engine::ToolCompletion, ToolError> {
49 let result: Result<ToolResult, ToolError> = async move {
50 if self
51 .engine
52 .skill_invocation_context(&self.args.name)
53 .map_err(tool_error)?
54 == Some(cookie_agent_config::SkillContext::Fork)
55 {
56 return self
57 .engine
58 .execute_skill_fork(
59 context.session,
60 context.run,
61 self.call_id,
62 &self.args.name,
63 &self.args.args,
64 )
65 .await
66 .map_err(tool_error);
67 }
68 let invocation = self
69 .engine
70 .invoke_skill(
71 context.session,
72 Some(context.run),
73 &self.args.name,
74 &self.args.args,
75 true,
76 )
77 .await
78 .map_err(tool_error)?;
79 Ok(ToolResult {
80 display: None,
81 retained_output: None,
82 title: safe_title(format!("Loaded skill {}", invocation.name)),
83 output: invocation.rendered,
84 metadata: serde_json::json!({"skill": invocation.name}),
85 truncation: None,
86 attachments: Vec::new(),
87 additional_messages: Vec::new(),
88 })
89 }
90 .await;
91 result.map(cookie_agent_engine::ToolCompletion::single)
92 }
93}
94
95#[async_trait]
96impl ToolProvider for SkillTool {
97 fn provider_id(&self) -> &'static str {
98 "builtin.skill"
99 }
100
101 fn tools_for_session(&self, ctx: &SessionToolContext) -> Result<Vec<ToolSpec>, ToolError> {
102 if !self
103 .engine
104 .skill_tool_available(ctx.session)
105 .map_err(tool_error)?
106 {
107 return Ok(Vec::new());
108 }
109 Ok(vec![ToolSpec {
110 output: Default::default(),
111 concurrency: Default::default(),
112 result_truncation: Default::default(),
113 name: "skill".into(),
114 permission_name: "skill".into(),
115 description: "Load an available skill by name with optional arguments.".into(),
116 parameters: schema::<SkillArgs>(),
117 }])
118 }
119
120 fn get_permission_name(tool_name: &str) -> Result<&'static str, ToolError> {
121 if tool_name == "skill" {
122 Ok("skill")
123 } else {
124 Err(ToolError::execution("skill provider received another tool"))
125 }
126 }
127
128 fn permission_for_unlisted_tool(
129 &self,
130 tool_name: &str,
131 ) -> Result<Option<&'static str>, ToolError> {
132 Ok((tool_name == "skill").then_some("skill"))
133 }
134
135 fn get_permission_resource(
136 &self,
137 name: &str,
138 arguments: &serde_json::Value,
139 ) -> Result<(&'static str, Option<String>), ToolError> {
140 Self::get_permission_name(name)?;
141 let args: SkillArgs = serde_json::from_value(arguments.clone()).map_err(tool_error)?;
142 Ok(("skill", Some(args.name)))
143 }
144
145 fn get_display_argument(
146 &self,
147 name: &str,
148 arguments: &serde_json::Value,
149 ) -> Result<String, ToolError> {
150 Self::get_permission_name(name)?;
151 Ok(serde_json::from_value::<SkillArgs>(arguments.clone())
152 .map_err(tool_error)?
153 .name)
154 }
155
156 async fn prepare(
157 &self,
158 ctx: ToolPreparationContext,
159 call: ToolCall,
160 ) -> Result<PreparedTool, ToolError> {
161 Self::get_permission_name(&call.name)?;
162 let args: SkillArgs = parse_args("skill", call.arguments)?;
163 if self.engine.is_direct_skill_call(call.id) {
164 self.engine
165 .get_user_skill(ctx.session, &args.name, &args.args)
166 .map_err(tool_error)?;
167 } else {
168 self.engine
169 .get_model_skill(ctx.session, &args.name, &args.args)
170 .map_err(tool_error)?;
171 }
172 let resource = prepared_resource(
173 PermissionAction::Skill,
174 "skill",
175 args.name.as_bytes(),
176 args.name.as_bytes(),
177 PreparedBindingLifetime::RestartStable,
178 ApprovalResourceSource::PrimaryOperation,
179 )?;
180 let operation = prepared_operation(
181 "skill",
182 &args,
183 vec![(PermissionAction::Skill, "load")],
184 vec![resource],
185 ctx.cwd.to_string_lossy().as_bytes(),
186 )?;
187 let normalized = serde_json::to_value(&args).map_err(tool_error)?;
188 PreparedTool::new(
189 operation,
190 normalized,
191 None,
192 Box::new(SkillExecutor {
193 engine: self.engine.clone(),
194 call_id: call.id,
195 args,
196 }),
197 )
198 }
199}