Skip to main content

cookie_agent_tools/
skill.rs

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}