Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
15 changes: 15 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,18 @@ agents:
- "final_answer_tool"
- "my_custom_tool"
- "my_other_tool"

# Optional: Skills (Anthropic Agent Skills model)
# Each skill is a directory with a SKILL.md (frontmatter: name, description).
# Skill name + description are auto-registered into the system prompt so the
# agent can invoke a skill on its own via the `use_skill` tool; skills are
# also exposed as commands over ACP (available_commands).
# By default skills are read from ./.agent/skills (CWD) and ~/.agent/skills;
# missing/empty folders are fine. Use the block below to customize.
# skills:
# enabled: true
# paths: # overrides the default roots (relative to CWD)
# - "./.agent/skills"
# include: null # optional allowlist of skill names to activate
# exclude: null # optional denylist of skill names
# The prompt listing budget is global: execution.max_skill_desc_chars (500).
206 changes: 206 additions & 0 deletions docs/design/skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Skills subsystem — design & specification

Status: draft (feat/skills)
Author: feat/skills work
Layered per `.cursor/rules/architecture.mdc` (bottom-up, one class at a time, TDD).

## 1. Motivation

Add first-class **Agent Skills** to SGR Agent Core, modeled on the Anthropic
"Agent Skills" pattern (a `SKILL.md` file with YAML frontmatter + markdown body,
progressive disclosure). Two capabilities are required:

1. **Autonomous invocation** — the agent automatically *registers* available
skills into its system prompt (name + description only, "progressive
disclosure" level 1) so the LLM can decide, on its own, to invoke a skill.
Invoking a skill loads its full body (level 2) into the conversation.
2. **Skills as commands** — skills are surfaced as user-visible **commands**
through the protocol layers: ACP `available_commands` (slash commands in ACP
clients such as Zed) and an optional MCP prompts server. This mirrors how
codex / Claude Code / Cursor expose reusable prompts/skills as commands.

## 2. What a skill is (Anthropic model)

A skill is a directory containing a `SKILL.md`:

```
skills/
pdf-processing/
SKILL.md # required: frontmatter + body
scripts/ # optional bundled resources (level 3)
references/
```

`SKILL.md` frontmatter (YAML). The portable open standard requires exactly two
fields — `name` and `description`; the rest are optional extensions.

| field | required | validation / notes |
|-----------------|----------|--------------------------------------------------------------------|
| `name` | yes | ≤ 64 chars; only lowercase letters, digits, hyphens; no XML tags; not "anthropic"/"claude" |
| `description` | yes | non-empty; ≤ 1024 chars; no XML tags; third person; *what it does* + *when to use* |
| `license` | no | SPDX id or text |
| `allowed-tools` | no | advisory allowlist of tool names the skill uses (soft, documented) |
| `metadata` | no | free-form dict (version, author, ...) |

Validation is enforced at parse time (Anthropic spec rules). Body: markdown
instructions (level-2 content, keep under ~500 lines / ~5k tokens); push long
material into reference files loaded on demand.

### Progressive disclosure (three levels)

- **Level 1 — metadata**: `name` + `description` always injected into the system
prompt so the model knows the skill exists and when to reach for it.
- **Level 2 — body**: the `SKILL.md` markdown body, loaded into context only when
the skill is invoked.
- **Level 3 — bundled files**: scripts/resources under the skill dir, referenced
from the body and read/run only as needed.

## 3. Discovery

Skill roots are scanned (in order; later roots override same-named skills):

1. Packaged/builtin skills dir (optional, shipped with an example).
2. User dir: `~/.sgr/skills` (personal).
3. Project dir: `./.sgr/skills` relative to `config_dir`.
4. Explicit dirs from config (`skills.paths`) and per-agent `skills:` selection.

A skill dir is any immediate subdirectory that contains a `SKILL.md`.

## 4. Architecture (layered)

### Layer 1 — data model + loader (no deps)

- `sgr_agent_core/skills/models.py`
- `SkillMetadata(BaseModel)`: `name`, `description`, `license: str | None`,
`allowed_tools: list[str]`, `metadata: dict`.
- `Skill(BaseModel)`: `metadata: SkillMetadata`, `body: str`, `path: Path`.
Convenience props: `name`, `description`.
- `sgr_agent_core/skills/loader.py`
- `SkillLoader.load_skill(dir: Path) -> Skill` — parse `SKILL.md`
(frontmatter via a tiny YAML front-matter splitter; no new dependency —
PyYAML already present). Validate required fields; raise `SkillError`
on malformed/missing.
- `SkillLoader.discover(root: Path) -> list[Skill]` — scan subdirs.

### Layer 2 — registry + config

- `sgr_agent_core/skills/registry.py`
- `SkillRegistry` — runtime catalog (dict name -> Skill). Unlike ToolRegistry
it is populated by scanning dirs (skills are data, not subclasses).
Methods: `register`, `get`, `list_items`, `load_from_paths(paths)`,
`clear`.
- Config: `SkillsConfig(BaseModel)` added to `AgentConfig`
(`skills: SkillsConfig | None`), with:
- `paths: list[str]` — extra skill roots.
- `enabled: bool = True`.
- `include: list[str] | None` / `exclude: list[str] | None` — filter by name.
Per-agent selection via `AgentDefinition.skills: list[str]` (names) — resolved
in a validator mirroring `agent_level_tools_validator`.

### Layer 3 — prompt injection + invocation tool

- `PromptLoader.get_system_prompt(..., available_skills=...)` — render a
`{available_skills}` block (numbered `name: description`). Default
`system_prompt.txt` gains an `<AVAILABLE_SKILLS>` section. Always pass the
kwarg (empty string when no skills) so custom templates using the placeholder
never KeyError, and templates without it are unaffected (`str.format` ignores
extra kwargs).
- `sgr_agent_core/tools/skill_tool.py`
- `SkillTool(SystemBaseTool)` — `tool_name = "use_skill"`. Field:
`skill_name: str`. `__call__` looks up the skill (from
`context.available_skills` / SkillRegistry), returns the level-2 body as a
string (which the agent appends to conversation). Unknown skill -> helpful
error string listing available skills. This is the autonomous-invocation
vehicle and flows through both function-calling and SGR union paths via the
registry — same pattern as `SearchToolsTool`.

### Layer 4 — agent + factory wiring

- `AgentContext` gains `available_skills: list[Skill]` (default empty) so tools
can resolve skills without global state.
- `BaseAgent._prepare_context()` passes
`available_skills=self.available_skills` to `PromptLoader.get_system_prompt`.
`BaseAgent.__init__` accepts `skills: list[Skill] | None`.
- `AgentFactory.create` resolves skills (from `SkillRegistry` +
`agent_def.skills` + config paths), injects `SkillTool` into the toolkit when
skills are present, and threads the skill list into the agent + context.

### Layer 5 — protocol command surfaces

- **ACP**: `SGRACPBridge` sends an `AvailableCommandsUpdate` session update
after `new_session` (and on agent switch), advertising each skill as an
`AvailableCommand(name, description, input=UnstructuredCommandInput(...))`.
`/skill-name` references are NOT handled in the bridge; they are expanded
centrally in `AgentFactory.create` (via `inject_referenced_skills`) for every
mode (ACP/CLI/server), detecting a reference anywhere in the last user message.
- **MCP**: considered (skills as MCP prompts) but **dropped** — the ACP
`available_commands` surface already covers "skills as commands", and MCP
prompts added a parallel server for little extra value.
- **CLI**: `sgrsh --list-skills` prints discovered skills; `/skill` handling in
chat loop (nice-to-have).
- **Server**: `GET /v1/skills` lists discovered skills (OpenAI-adjacent).

## 5. Testing plan (TDD, red first)

- `tests/test_skill_loader.py` — frontmatter parsing, required-field validation,
discovery, malformed handling.
- `tests/test_skill_registry.py` — register/get/list, path loading, override.
- `tests/test_skills_config.py` — config parse, per-agent selection validator.
- `tests/test_prompts.py` (extend) — `{available_skills}` rendering.
- `tests/test_skill_tool.py` — `use_skill` returns body / errors.
- `tests/test_agent_factory.py` (extend) — factory injects SkillTool + skills.
- `tests/test_acp_bridge.py` (extend) — available_commands advertised; `/name`
routes to skill.
- `tests/test_skills_e2e.py` (`@pytest.mark.e2e`) — end-to-end autonomous
invocation with a mocked LLM choosing `use_skill`.
- Mode checks: ACP, CLI, OpenAI server.

## 5a. Listing budget & invocation semantics

- **Listing budget (level 1).** Injecting many skills must not blow the system
prompt. Each rendered entry is `name: description` with the description
truncated to a per-entry cap (`SkillsConfig.max_desc_chars`, default 500).
This mirrors Claude Code's skill-listing budget (~1% context, ~1536-char
per-entry cap). The name is always shown in full.
- **Invocation = prompt injection.** When `use_skill` runs, the level-2 body is
returned as the tool result and appended to the conversation, where it
persists for the rest of the run (matching Anthropic semantics). No isolated
subagent in v1.
- **ACP push mechanism.** The bridge advertises commands via
`client.session_update(session_id, AvailableCommandsUpdate(available_commands=[
AvailableCommand(name, description, input=UnstructuredCommandInput(hint=...))]))`
— same `session_update` channel the streaming generator already uses.

## 5b. Prior art (codex / Claude Code / Cursor / MCP)

Autonomous invocation across all these tools = inject a rendered catalog block
of `name + description (+ when-to-use)` into the system/developer context, under
a budget, plus a "how to use" instruction telling the model to load the full
body on match. Concretely:

- **Codex** `render_available_skills_body` emits a `## Skills` section:
`- {name}: {description} (file: {path})` followed by a "How to use skills"
block with trigger rules ("if the task matches a skill's description, use it";
read `SKILL.md` fully first). Budget = 2% of context.
- **Claude Code** injects a skill listing (name + truncated description),
budget ~1% of context, per-entry cap ~1536 chars; loads the body on trigger.
- **Cursor** "Apply Intelligently" rules present the description to the agent to
decide relevance.
- **MCP `prompts`** primitive (`prompts/list` / `prompts/get` /
`notifications/prompts/list_changed`; Prompt = `name`/`title`/`description`/
`arguments`) is **user-controlled by spec** → renders as slash commands. This
is the transport for the *command* surface; autonomous invocation still needs
the description in the model catalog (our system-prompt block + `use_skill`).

Our `{available_skills}` block mirrors the codex format (name + description) and
adds an explicit instruction to call `use_skill` to load a skill's body, giving
the same autonomous-invocation behavior. `model_invocable` / `user_invocable`
flags (mirroring codex/Claude `disable-model-invocation` / `user-invocable`)
gate whether a skill appears in the model catalog vs the command menu.

## 6. Non-goals / constraints

- No new runtime dependency (reuse PyYAML, fastmcp, acp already present).
- Backward compatible: agents without skills behave exactly as before
(`{available_skills}` optional, SkillTool only added when skills exist).
- English comments; 120-char lines; ruff clean.
129 changes: 129 additions & 0 deletions docs/en/framework/skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Skills

Skills are reusable, on-demand instruction packages (the Anthropic *Agent
Skills* model). Each skill is a directory with a `SKILL.md` file — YAML
frontmatter plus a markdown body. SGR Agent Core auto-registers skill
`name` + `description` into the agent system prompt so the agent can invoke a
skill autonomously, and also surfaces skills as **commands** over ACP, the CLI,
and the HTTP server.

## What a skill looks like

```
skills/
citation-style/
SKILL.md
concise-answer/
SKILL.md
```

`SKILL.md`:

```markdown
---
name: citation-style
description: Formats research citations in a consistent numbered style. Use when writing reports that reference web sources.
---

# Citation style

1. Number every source in order: [1], [2], ...
2. Collect all sources under a `## Sources` heading.
```

### Frontmatter

| field | required | notes |
|-----------------|----------|--------------------------------------------------------------------|
| `name` | yes | ≤ 64 chars; lowercase letters, digits, hyphens; no `anthropic`/`claude` |
| `description` | yes | non-empty, ≤ 1024 chars; write in third person, say *what* and *when* |
| `license` | no | SPDX id or text |
| `allowed-tools` | no | advisory list of tool names the skill uses |
| `metadata` | no | free-form mapping (version, author, ...) |
| `disable-model-invocation` | no | `true` hides the skill from the model catalog (user command only) |
| `user-invocable` | no | `false` hides the skill from command menus (model-only) |

If `name` is omitted it defaults to the directory name.

## Progressive disclosure

1. **Level 1 — metadata**: `name` + `description` are always injected into the
system prompt (a compact catalog block), so the model knows the skill exists.
2. **Level 2 — body**: the `SKILL.md` body is loaded only when the skill is
invoked (the `use_skill` tool returns it into the conversation).
3. **Level 3 — resources**: any bundled files are read only when needed.

## Enabling skills

Skills work out of the box: **by default** the agent scans

1. `./.agent/skills` (project, relative to the current working directory)
2. `~/.agent/skills` (personal)

If those folders are missing or empty, nothing breaks — the agent runs normally
with no skills. To customize, add a `skills` block to an agent (or globally, at
the top level) in `config.yaml`:

```yaml
agents:
sgr_agent:
base_class: SGRToolCallingAgent
tools:
- reasoningtool
skills:
enabled: true # default true; set false to disable skills
paths: # override the default roots (relative to CWD)
- ./my-skills
include: null # optional allowlist of skill names to activate
exclude: null # optional denylist of skill names
```

`skills.paths`, when set, **replaces** the default roots. Later roots override
earlier ones by name. The per-entry description budget in the prompt listing is
a global setting: `execution.max_skill_desc_chars` (default 500).

When any skill is available, the agent's toolkit automatically gains the
`use_skill` tool.

## Invoking a skill

There are two ways a skill is invoked:

1. **Autonomously (the model decides).** The system prompt gains an
`AVAILABLE_SKILLS` block listing each model-invocable skill and instructing
the agent to call `use_skill` when a task matches. Calling
`use_skill <name>` returns that skill's body into the conversation
(progressive disclosure level 2).
2. **Explicitly (the user references it).** If a user message mentions a skill
by name with a leading slash — `/citation-style` — that skill's body is
injected into the prompt. This works in **every mode** (ACP, CLI, and the
OpenAI-compatible server), because the reference is expanded centrally in
`AgentFactory.create`. The reference may appear anywhere in the message
(`please use /concise-answer`); slashes inside URLs/paths are ignored.

## Discovering skills as commands

- **ACP** (`sgracp`): user-invocable skills are advertised to the client as
`available_commands`, so they appear in the client's slash-command menu.
- **CLI**: `sgrsh --list-skills -c config.yaml` prints the available skills.
- **HTTP server**: `GET /v1/skills` (optional `?model=<agent>`) lists skills per
agent.

## Security & trust

Skills are **trusted content**, on the same level as `config.yaml` and custom
tool code: a skill body is injected verbatim into the model context when
invoked. The default roots `./.agent/skills` and `~/.agent/skills` are scanned
automatically, so opening a repository that ships a `.agent/skills/` directory
will auto-load its authors' instructions. Only run skills from sources you
trust, and review third-party `SKILL.md` files before use. The loader caps each
`SKILL.md` at 1 MiB and skips unreadable files.

## Authoring tips

- Write the `description` in third person; include both *what it does* and *when
to use it* — this is what the model matches against.
- Keep `SKILL.md` focused (under ~500 lines); move long material into separate
reference files.
- Use gerund or noun-phrase names (`processing-pdfs`, `citation-style`); avoid
vague names like `helper` or `utils`.
Loading
Loading