Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 108 additions & 19 deletions crates/goose/src/agents/code_execution_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,41 @@ fn extract_type_from_schema(schema: &Value) -> Option<String> {

// type field (string or array)
match schema.get("type") {
Some(Value::String(s)) if s == "array" => {
let item_type = schema
.get("items")
.and_then(extract_type_from_schema)
.unwrap_or_else(|| "any".to_string());
Some(if item_type == "any" {
"array".into()
} else {
format!("{item_type}[]")
})
}
Some(Value::String(s)) if s == "object" => {
let Some(props) = schema.get("properties").and_then(|p| p.as_object()) else {
return Some("object".to_string());
};
let required: Vec<_> = schema
.get("required")
.and_then(|r| r.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
let mut fields: Vec<_> = props
.iter()
.map(|(name, schema)| {
let ty = extract_type_from_schema(schema).unwrap_or_else(|| "any".into());
let opt = if required.contains(&name.as_str()) {
""
} else {
"?"
};
format!("{name}{opt}: {ty}")
})
.collect();
fields.sort();
Some(format!("{{ {} }}", fields.join(", ")))
}
Some(Value::String(s)) => Some(s.clone()),
Some(Value::Array(arr)) => {
let non_null: Vec<_> = arr
Expand Down Expand Up @@ -248,6 +283,13 @@ fn create_server_module(server_tools: &[&ToolInfo], ctx: &mut Context) -> Module
)
}

fn parse_result_to_js(result: &str, ctx: &mut Context) -> JsValue {
serde_json::from_str::<serde_json::Value>(result)
.ok()
.and_then(|v| JsValue::from_json(&v, ctx).ok())
.unwrap_or_else(|| JsValue::from(js_string!(result)))
}

fn create_tool_function(full_tool_name: String) -> NativeFunction {
NativeFunction::from_copy_closure_with_captures(
|_this, args, full_name: &String, ctx| {
Expand All @@ -274,7 +316,7 @@ fn create_tool_function(full_tool_name: String) -> NativeFunction {
rx.blocking_recv()
.map_err(|e| e.to_string())
.and_then(|r| r)
.map(|result| JsValue::from(js_string!(result.as_str())))
.map(|result| parse_result_to_js(&result, ctx))
.map_err(|e| JsNativeError::error().with_message(e).into())
},
full_tool_name,
Expand Down Expand Up @@ -616,15 +658,19 @@ impl CodeExecutionClient {
.await
{
Ok(dispatch_result) => match dispatch_result.result.await {
Ok(result) => Ok(result
.content
.iter()
.filter_map(|c| match &c.raw {
RawContent::Text(t) => Some(t.text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")),
Ok(result) => Ok(if let Some(sc) = &result.structured_content {
serde_json::to_string(sc).unwrap_or_default()
} else {
result
.content
.iter()
.filter_map(|c| match &c.raw {
RawContent::Text(t) => Some(t.text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}),
Err(e) => Err(format!("Tool error: {}", e.message)),
},
Err(e) => Err(format!("Dispatch error: {e}")),
Expand Down Expand Up @@ -1021,18 +1067,18 @@ mod tests {
"no params, no output schema"
)]
#[test_case(
"filesystem__read_file",
serde_json::json!({"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}),
Some(serde_json::json!({"type": "object"})),
"read_file({ path: string }): object - Read the complete contents of a file";
"string param, object output"
"filesystem__read_text_file",
serde_json::json!({"type": "object", "properties": {"path": {"type": "string"}, "tail": {"type": "number"}, "head": {"type": "number"}}, "required": ["path"]}),
Some(serde_json::json!({"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]})),
"read_text_file({ head?: number, path: string, tail?: number }): { content: string } - Read the complete contents of a file";
"optional number params, object output"
)]
#[test_case(
"memory__create_entities",
serde_json::json!({"type": "object", "properties": {"entities": {"type": "array"}}, "required": ["entities"]}),
Some(serde_json::json!({"type": "object"})),
"create_entities({ entities: array }): object - Create multiple new entities";
"array param, object output"
serde_json::json!({"type": "object", "properties": {"entities": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "entityType": {"type": "string"}, "observations": {"type": "array", "items": {"type": "string"}}}, "required": ["name", "entityType", "observations"]}}}, "required": ["entities"]}),
Some(serde_json::json!({"type": "object", "properties": {"entities": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "entityType": {"type": "string"}, "observations": {"type": "array", "items": {"type": "string"}}}, "required": ["name", "entityType", "observations"]}}}, "required": ["entities"]})),
"create_entities({ entities: { entityType: string, name: string, observations: string[] }[] }): { entities: { entityType: string, name: string, observations: string[] }[] } - Create multiple new entities";
"nested object array with typed props"
)]
#[test_case(
"github__dismiss_notification",
Expand Down Expand Up @@ -1081,4 +1127,47 @@ mod tests {
let info = ToolInfo::from_mcp_tool(&tool).unwrap();
assert_eq!(info.to_signature(), expected);
}

#[test_case(serde_json::json!({"type": "string"}), "string"; "string")]
#[test_case(serde_json::json!({"type": "number"}), "number"; "number")]
#[test_case(serde_json::json!({"type": "boolean"}), "boolean"; "boolean")]
#[test_case(serde_json::json!({"type": "array"}), "array"; "array bare")]
#[test_case(serde_json::json!({"type": "array", "items": {"type": "string"}}), "string[]"; "array with items")]
#[test_case(serde_json::json!({"type": "object"}), "object"; "object bare")]
#[test_case(serde_json::json!({"type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"]}), "{ a: string }"; "object with prop")]
#[test_case(serde_json::json!({"type": "object", "properties": {"a": {"type": "string"}}}), "{ a?: string }"; "object optional prop")]
#[test_case(serde_json::json!({"type": "object", "properties": {"a": {"type": "array", "items": {"type": "string"}}}, "required": ["a"]}), "{ a: string[] }"; "object with array prop")]
#[test_case(serde_json::json!({"enum": ["a", "b"]}), "\"a\" | \"b\""; "enum array")]
#[test_case(serde_json::json!({"oneOf": [{"const": "x"}, {"const": "y"}]}), "\"x\" | \"y\""; "oneOf const")]
fn test_extract_type_from_schema(schema: serde_json::Value, expected: &str) {
assert_eq!(
extract_type_from_schema(&schema),
Some(expected.to_string())
);
}

fn eval_with_tools(code: &str, tools: &[(&str, &str)]) -> String {
let mut ctx = Context::default();
for &(name, response) in tools {
let resp = response.to_string();
let func = NativeFunction::from_copy_closure_with_captures(
|_this, _args, resp: &String, ctx| Ok(parse_result_to_js(resp, ctx)),
resp,
);
ctx.register_global_callable(js_string!(name), 0, func)
.unwrap();
}
ctx.eval(Source::from_bytes(code))
.unwrap()
.display()
.to_string()
}

#[test_case("2 + 2", &[], "4"; "pure_js")]
#[test_case("get_data({}).content", &[("get_data", r#"{"content":"hello"}"#)], "\"hello\""; "structured_property_access")]
#[test_case("typeof shell({})", &[("shell", "plain text")], "\"string\""; "plain_text_is_string")]
#[test_case("shell({}).content", &[("shell", "plain text")], "undefined"; "plain_text_no_property")]
fn test_tool_result(code: &str, tools: &[(&str, &str)], expected: &str) {
assert_eq!(eval_with_tools(code, tools), expected);
}
}
28 changes: 12 additions & 16 deletions documentation/docs/guides/context-engineering/using-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,29 @@ You can also ask goose what skills are available.
- [recipes](/docs/guides/recipes/session-recipes): Shareable configurations that package instructions, prompts, and settings together
:::

## Claude Compatibility
## Skill Locations
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I don't think we should remove the mention that this is compatible with Claude Code. This is a pull for a lot of people to use skills with goose and helps with association..like "ohhhh, I could use this here too"

I'm okay if we update this heading to say Skills location but I think we should add a call out or admonition that says something like

::: note Claude Code Compatibility
goose Skills are compatible with Claude Code and other agents that support Agent Skills

Describe the paths .claude/skills or whatever the new path is
:::

obviously, don't write this verbatim, but that may be a possible direction we want to go cc: @dianed-square

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diane will approve to merge this and we will add the note above as a fast follow


goose skills use the same format as Claude Desktop skills. goose discovers skills from both `.claude/skills/` and `.goose/skills/` directories, so you can share skills between both tools or create tool-specific versions as needed.
Skills can be stored globally and/or per-project. goose checks all of these directories in order and combines what it finds. If the same skill name exists in multiple directories, later directories take priority:

When the same skill name exists in multiple directories, goose follows the priority order listed in [Skill Locations](#skill-locations). Later directories override earlier ones regardless of whether they're `.claude` or `.goose` directories.
1. `~/.claude/skills/` — Global, shared with Claude Desktop
2. `~/.config/agents/skills/` — Global, portable across AI coding agents
3. `~/.config/goose/skills/` — Global, goose-specific
4. `./.claude/skills/` — Project-level, shared with Claude Desktop
5. `./.goose/skills/` — Project-level, goose-specific
6. `./.agents/skills/` — Project-level, portable across AI coding agents

Use global skills for workflows you use across projects. Use project-level skills for procedures unique to a codebase.

## Creating a Skill

Create a skill when you have a repeatable workflow that involves multiple steps, specialized knowledge, or supporting files.

### Skill Locations

Skills can be stored globally and/or per-project. goose checks all of these directories in order and combines what it finds. If the same skill name exists in multiple directories, the latest directory takes priority:

1. `~/.claude/skills/` — Global, shared with Claude Desktop
2. `~/.config/goose/skills/` — Global, goose-specific
3. `./.claude/skills/` — Current directory, shared with Claude Desktop
4. `./.goose/skills/` — Current directory, goose-specific (highest priority)

Use global skills for workflows you use across projects. Use project-specific skills for procedures unique to a codebase.

### Skill File Structure

Each skill lives in its own directory with a `SKILL.md` file:

```
~/.config/goose/skills/
~/.config/agents/skills/
└── code-review/
└── SKILL.md
```
Expand Down Expand Up @@ -93,7 +89,7 @@ When reviewing code, check each of these areas:
Skills can include supporting files like scripts, templates, or configuration files. Place them in the skill directory:

```
~/.config/goose/skills/
~/.config/agents/skills/
└── api-setup/
├── SKILL.md
├── setup.sh
Expand Down
6 changes: 3 additions & 3 deletions documentation/docs/mcp/skills-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import GooseBuiltinInstaller from '@site/src/components/GooseBuiltinInstaller';

The Skills extension loads *skills* &mdash; reusable sets of instructions that teach goose how to perform specific tasks or follow particular workflows.

goose automatically discovers skills at startup and uses them when relevant to your request. goose skills are compatible with Claude Desktop's skill format, so skills you create for one tool work with both. To learn about creating skills and how goose uses them, see [Using Skills](/docs/guides/context-engineering/using-skills).
goose automatically discovers skills at startup and uses them when relevant to your request. goose loads skills from `.agents/skills/` in your project directory and `~/.config/agents/skills/` globally, making skills portable across different AI coding agents. To learn about creating skills and how goose uses them, see [Using Skills](/docs/guides/context-engineering/using-skills).
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence only mentions 2 of the 6 skill directories that goose actually checks. While it highlights the new agent-agnostic paths, it could mislead users into thinking these are the only directories supported. Consider mentioning that goose checks multiple directories for compatibility, or add "including" before the specific paths to clarify these are examples.

Suggested change
goose automatically discovers skills at startup and uses them when relevant to your request. goose loads skills from `.agents/skills/` in your project directory and `~/.config/agents/skills/` globally, making skills portable across different AI coding agents. To learn about creating skills and how goose uses them, see [Using Skills](/docs/guides/context-engineering/using-skills).
goose automatically discovers skills at startup and uses them when relevant to your request. For compatibility with different AI coding agents, goose checks multiple standard locations for skills, including `.agents/skills/` in your project directory and `~/.config/agents/skills/` globally, making skills portable across different AI coding agents. To learn about creating skills and how goose uses them, see [Using Skills](/docs/guides/context-engineering/using-skills).

Copilot uses AI. Check for mistakes.

## Configuration

Expand All @@ -20,7 +20,7 @@ goose automatically discovers skills at startup and uses them when relevant to y
<TabItem value="ui" label="goose Desktop" default>
<GooseBuiltinInstaller
extensionName="Skills"
description="Load and use skills from the .claude/skills or .goose/skills directories"
description="Load and use skills from the .agents/skills directory"
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description changed from mentioning multiple directories (".claude/skills or .goose/skills") to only ".agents/skills directory" (singular). This is less accurate since goose actually loads skills from 6 different directories. Consider using "Load and use skills from .agents/skills, .goose/skills, and .claude/skills directories" to better reflect the actual behavior.

Suggested change
description="Load and use skills from the .agents/skills directory"
description="Load and use skills from .agents/skills, .goose/skills, and .claude/skills directories"

Copilot uses AI. Check for mistakes.
/>
</TabItem>
<TabItem value="cli" label="goose CLI">
Expand Down Expand Up @@ -48,7 +48,7 @@ goose automatically discovers skills at startup and uses them when relevant to y

## Example Usage

Let's say you have a skill that goose discovers on startup in `~/.config/goose/skills/deploy/SKILL.md`:
Let's say you have a skill that goose discovers on startup in `~/.config/agents/skills/deploy/SKILL.md`:

```markdown
---
Expand Down
Loading