adk-skill 2.1.0

AgentSkills parser, index, and runtime injection helpers for ADK-Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# adk-skill

AgentSkills parser, index, matcher, and runtime injection helpers for ADK-Rust.

[![Crates.io](https://img.shields.io/crates/v/adk-skill.svg)](https://crates.io/crates/adk-skill)
[![Documentation](https://docs.rs/adk-skill/badge.svg)](https://docs.rs/adk-skill)
[![License](https://img.shields.io/crates/l/adk-skill.svg)](https://github.com/zavora-ai/adk-rust/blob/main/LICENSE)

## Overview

`adk-skill` is the engine for **specification-driven agent skills**, implementing the [**agentskills.io**](https://agentskills.io) specification. It provides the core building blocks to discover, parse, and index skill metadata, enabling agents to dynamically configure their behavior based on structured Markdown definitions.

This crate is provider-agnostic and can be used through:

- `adk-agent` (`LlmAgentBuilder::with_skills*`)
- `adk-runner` (`Runner::with_auto_skills`)
- Direct API calls from custom runtimes

## Supported File Conventions

### `agentskills.io` Compliance (`.skills/**/*.md`)

Each skill is a Markdown file with YAML frontmatter following the `agentskills.io` standard. This structure allows for both human-readable instructions and machine-readable governance.

```md
---
name: zenith-voice-receptionist
description: Official voice persona for Mario's Plumbing Co. receptionist.
version: "1.1.0"
license: MIT
compatibility: Gemini Live 2.5 Flash Native Audio
allowed-tools:
  - user_profile
  - knowledge
  - reasoning_engine
references:
  - references/technicians.json
---
You are Zenith, the voice receptionist for Mario's Plumbing...
```

#### Specification Fields

| Field | Required | Description |
| :--- | :--- | :--- |
| `name` | Yes | Unique identifier (lowercase, numbers, hyphens). |
| `description` | Yes | Concise summary of the skill's purpose for agent selection. |
| `version` | No | Semantic version of the skill. |
| `license` | No | License identifier (e.g., MIT, Apache-2.0). |
| `compatibility` | No | Environment or model constraints. |
| `tags` | No | List of discovery and filtering labels. |
| `allowed-tools` | No | List of tools the agent is permitted to use for this skill. |
| `references` | No | External assets (JSON, CSV) required by the skill. |
| `trigger` | No | If true, requires explicit `@name` invocation. |
| `hint` | No | UI guidance for user input. |
| `metadata` | No | Arbitrary key-value map for extensions. |

#### Parsing Strictness

`adk-skill` employs a dual-validation strategy based on the file's location:

*   **Strict Mode (`.skills/**/*.md`)**: 
    - **Mandatory**: `name` and `description` must be present and non-empty.
    - **Validation**: Failing to provide these fields will result in a `SkillError`, and the file will not be indexed.
    - **Optional**: All other fields (version, license, tools, etc.) are optional and will default to empty or `None`.
*   **Permissive Mode (Convention Files)**:
    - Files like `AGENTS.md` or `SOUL.md` treat frontmatter as **entirely optional**.
    - If frontmatter is missing, the parser automatically derives the skill name from the filename and assigns default convention tags.

### Instruction Convention Files

The index loader also discovers and ingests these markdown files:

- `AGENTS.md` and `AGENT.md`
- `CLAUDE.md`
- `GEMINI.md`
- `COPILOT.md`
- `SKILLS.md`
- `SOUL.md` (root-level)

For these files, frontmatter is optional:

- If valid frontmatter is present, it is used.
- Otherwise the file is parsed as plain markdown instructions and converted into a skill document with convention tags (for example `agents-md`, `claude-md`).

## Logic-as-Data

The core philosophy of `adk-skill` is that **Agent behavior should be controlled by configuration, not just code.**

By parsing `allowed-tools` and `references`, the runtime (e.g., the `data_plane`) can dynamically instantiate the appropriate toolset for an agent session. This enables:
- **Role-Based Access Control**: Limit an agent's capabilities based on the active skill.
- **Pluggable Personalities**: Swap personas by simply changing the active skill metadata.
- **Resource Injection**: Automatically load the correct reference data for specific flows.

## What The Crate Does

### 1. Discovery

- Scans `<root>/.skills/` recursively for frontmatter skills.
- Scans `<root>` recursively for supported convention files.
- Skips common heavy directories (`.git`, `target`, `node_modules`, etc.).
- Returns deterministic sorted file paths with de-duplication.

API: `discover_skill_files(root)`
API: `discover_instruction_files(root)`

### Tool Discovery & Validation

While `adk-skill` handles the **declaration** of tools via `allowed-tools`, the **implementation** and **validation** are managed via core ADK traits.

- **`ToolRegistry` (Core)**: In [adk-core](file:///home/michael/src/voice_gateway/zenith/adk-rust/adk-core), use the `ToolRegistry` trait to map string identifiers (e.g., `user_profile`) to concrete `Arc<dyn Tool>` implementations.
- **`ValidationMode` (Core)**: Control whether the framework should strictly enforce tool availability or allow permissive binding.
- **Selective Injection**: Use the `ContextCoordinator` to filter available tools against a skill's `allowed_tools` list, ensuring the agent only sees authorized capabilities.

Example Flow:
1. `adk-skill` parses `allowed-tools: [weather]`.
2. Runtime looks up `"weather"` in its local registry.
3. Runtime injects the `WeatherTool` into the LLM context.

### 2. Parsing + Validation

- Strict path (`.skills/**`): parses required frontmatter as YAML with validation.
- Convention path (`AGENTS.md`, `CLAUDE.md`, etc.): parses plain markdown (or frontmatter if provided).
- Returns actionable errors with file path context for strict frontmatter paths.

API: `parse_skill_markdown(path, content)`
API: `parse_instruction_markdown(path, content)`

### 3. Indexing

- Builds `SkillIndex` from discovered files.
- Computes:
  - content hash (`SHA-256`)
  - `last_modified` (Unix timestamp seconds when available)
  - stable document id: `normalized-name + first-12-hash-chars`
- Sorts documents deterministically by `(name, path)`.

API: `load_skill_index(root)`

### 4. The Context Coordinator (Context Engineering)

The `ContextCoordinator` is the high-level engine that orchestrates the **Context Engineering Pipeline**. It bridges the gap between skill *selection* and agent *execution*, ensuring that any instruction given to the LLM is backed by concrete, validated capabilities.

#### The Role
1. **Orchestration**: It runs the full flow: `Selection` → `Validation` → `Engineering`.
2. **Preventing "Phantom Tools"**: It verifies that every tool listed in a skill's `allowed-tools` metadata exists in the host's `ToolRegistry`. If a tool is missing, it can reject the match (Strict) or omit the tool (Permissive), preventing the LLM from hallucinating an action it cannot perform.
3. **Atomic Delivery**: It emits a `SkillContext`, which encapsulates the final system instruction and the collection of executable `Arc<dyn Tool>` instances as a single unit.

#### Resolution Strategies
The coordinator supports a cascading **Resolution Strategy** pattern, allowing for flexible skill loading:
- **ByName**: Load a specific skill for dedicated flows.
- **ByQuery**: Find the best skill for a user's intent.
- **ByTag**: Find a skill categorised with specific labels (e.g., "emergency", "fallback").

API: `ContextCoordinator::new(index, registry, config)`
API: `coordinator.resolve(&[Strategy::ByName("..."), Strategy::ByQuery("...")])`

### 5. Selection (Scoring & Consumption)

The `select_skills` function implements a deterministic, token-based relevance engine. It calculates a score for each skill based on the query and then consumes that score to rank and filter the results.

#### The Scoring Algorithm

Relevance is determined by weighted lexical overlap. For every token in the query that is found in a skill field, the score increases by a specific weight:

| Field | Weight | Rationale |
| :--- | :--- | :--- |
| **Name** | `+4.0` | Exact name matches are highly intentional. |
| **Description** | `+2.5` | Concise summaries are the primary driver for relevance. |
| **Tags** | `+2.0` | Explicit labels provide strong categorization. |
| **Body** | `+1.0` | Mentions in instructions are relevant but can be noisy. |

**Normalization**: To prevent long-form instruction sets from unfairly drowning out concise skills, the raw score is divided by the square root of the unique tokens in the body: `FinalScore = RawScore / sqrt(unique_tokens)`.

#### Score Significance & Expression

Because the score is expressed as a weighted lexical distance, its significance should be interpreted as **Strength of Intent**:

-   **`0.0 - 0.9` (Trace)**: Incidental token overlap. Likely irrelevant to the user's current goal.
-   **`1.0 - 2.4` (Broad Match)**: Weak relevance. Found in the body or tags, but missing from primary identifiers (Name/Description).
-   **`2.5 - 4.9` (Specific Match)**: Strong relevance. Usually indicates a match in the `description` or multiple tags.
-   **`5.0+` (High Confidence)**: Direct hit. Indicates a match in the `name` or high-density overlap across all fields.

#### The Runtime Lifecycle of a Score

In a running application, the score follows this logical flow:

1.  **Query Arrival**: The user sends a message (e.g., "I need a plumber for a leak").
2.  **Lexical Calculation**: The `select_skills` engine tokenizes the query and performs a weighted lookup across the `SkillIndex`. Initial scores are generated for all candidate skills.
3.  **Policy Enforcement**: The `SelectionPolicy` is applied. 
    -   *Filtering*: If "Plumbing" scores `4.5` but the policy requires `min_score: 5.0`, it is discarded.
    -   *Clipping*: The sorted list is trimmed to `top_k`.
4.  **Action/Injection**:
    -   **Automated**: If a match survives, its `body` is injected into the prompt context using `apply_skill_injection`.
    -   **Manual/UI**: The application inspects the `SkillMatch.score` to decide whether to trigger a tool, log a warning, or ask for user confirmation.

API: `select_skills(index, query, policy)`

The skill engine uses the calculated score in two primary ways:

1.  **Filtering**: Any skill with a score below the `SelectionPolicy.min_score` (default: `1.0`) is immediately discarded. This ensures agents only receive highly relevant instructions.
2.  **Ranking & Top-K**: Resulting matches are sorted by descending score. If scores are tied, the engine applies deterministic **Tie-Breaking**:
    -   Lexicographical sort by **Name**.
    -   Lexicographical sort by **File Path**.

The engine then takes the top `top_k` results (default: `1`) for injection or inspection.

API: `select_skills(index, query, policy)`

`SelectionPolicy` defaults:

- `top_k = 1`
- `min_score = 1.0`
- `include_tags = []`
- `exclude_tags = []`

### 5. The Reliability Contract

What "makes the score well used" in a production app is its **predictability** and **reproducibility**.

-   **Deterministic Selection**: The lexical matching algorithm contains no random seeds or opaque model weights. The same query against the same skill index will *always* yield the same score.
-   **Stable Identification**: Every `SkillMatch` includes a unique `id` derived from the content hash. If the score changes, it's because the instructions (the data) changed.
-   **Policy-Enforced Boundaries**: By using `SelectionPolicy`, the application defines a "Safety Contract" where no agent ever receives instructions below a verified relevance threshold.

This transparency allows you to build **unit tests for your agent's persona**:
```rust,ignore
fn verification_of_emergency_triage() {
    let index = load_skill_index("skills")?;
    let matches = select_skills(&index, "gas leak", &SelectionPolicy::default());
    assert!(matches[0].score > 5.0, "Emergency skill must have high confidence for 'gas leak'");
}
```

### 6. Injection

Injection helpers prepend the selected skill body to user content using:

```text
[skill:<name>]
<skill body>
[/skill]
```

Then original user text follows.

Behavior:

- Injection runs only when `Content.role == "user"`.
- Query text is extracted from text parts and joined with newlines.
- Only the top match is injected.
- Injected body is truncated to `max_injected_chars`.

APIs:

- `select_skill_prompt_block(...)`
- `apply_skill_injection(...)`
- `SkillInjector` / `SkillInjectorConfig`
- `SkillInjector::build_plugin(...)`
- `SkillInjector::build_plugin_manager(...)`

## Quick Start

### Load and Match Skills

```rust
use adk_skill::{SelectionPolicy, load_skill_index, select_skills};

let index = load_skill_index(".")?;
let policy = SelectionPolicy {
    top_k: 1,
    min_score: 0.1,
    include_tags: vec![],
    exclude_tags: vec![],
};

let matches = select_skills(&index, "find TODO markers in code", &policy);
for m in matches {
    println!("{} ({:.2})", m.skill.name, m.score);
}
# Ok::<(), Box<dyn std::error::Error>>(())
```

### Inject Into User Content

```rust
use adk_core::Content;
use adk_skill::{SelectionPolicy, apply_skill_injection, load_skill_index};

let index = load_skill_index(".")?;
let policy = SelectionPolicy { min_score: 0.1, ..SelectionPolicy::default() };
let mut content = Content::new("user").with_text("Search this repository for TODO markers");

let matched = apply_skill_injection(&mut content, &index, &policy, 1500);
if let Some(m) = matched {
    println!("Injected skill: {}", m.skill.name);
}
# Ok::<(), Box<dyn std::error::Error>>(())
```

### Write Skills

`SkillWriter` writes into the `.skills` directory `load_skill_index` discovers. Writes go through
a unique sibling temporary file, are synchronized, and atomically replace the destination on Unix
and Windows, so a crash mid-write cannot leave a half-written skill that breaks the index load.

```rust
use adk_skill::{SkillDraft, SkillWriter, load_skill_index};

let root = tempfile::tempdir().unwrap();
let writer = SkillWriter::new(root.path());

writer.write(
    &SkillDraft::new("disk-triage", "Diagnose low disk space. Use when a disk alert fires.")
        .with_body("1. Check the largest directories.\n2. Report the growth rate.")
        .with_tags(["ops"])
        .with_metadata_entry("incidents", serde_json::json!(["INC-1", "INC-2"])),
)?;

let index = load_skill_index(root.path())?;
assert!(index.find_by_name("disk-triage").is_some());
# Ok::<(), Box<dyn std::error::Error>>(())
```

A skill name becomes a filename, so `validate_skill_name` enforces the specification's
`[a-z0-9-]` rule (1–64 characters, no leading or trailing hyphen). That check is also the
path-safety boundary: `../escape` and `nested/name` are rejected before any file is touched.

Use `with_metadata_entry` to record provenance. A skill an agent derived from experience should
carry the incidents and timestamp behind it, so a change in behaviour is auditable rather than
unexplained.

### Reload After Writing

`SkillInjector` holds an index snapshotted at construction, and `build_plugin` captures it by
handle — a plugin already handed to a runner keeps using the index it was built from. To pick up
a skill written since, call `reloaded` and rebuild the plugin from the result:

```rust,ignore
let injector = injector.reloaded()?;
let plugin = injector.build_plugin("skills");
```

`reloaded` returns an error when the injector was built with `from_index`, since there is no root
to rescan.

### Build A Plugin Manager

```rust
use adk_skill::{SkillInjector, SkillInjectorConfig};

let injector = SkillInjector::from_root(".", SkillInjectorConfig::default())?;
let plugin_manager = injector.build_plugin_manager("skills");
# let _ = plugin_manager;
# Ok::<(), Box<dyn std::error::Error>>(())
```

## Error Model

Main error type: `SkillError`

- `Io`
- `Yaml`
- `InvalidFrontmatter { path, message }`
- `MissingField { path, field }`
- `InvalidSkillsRoot(path)`

Type alias: `SkillResult<T> = Result<T, SkillError>`

## Current Limits

- No embedding/vector retrieval (lexical matching only).
- No incremental file reload API yet.
- No remote catalog (`skills-ref`/MCP) in this crate yet.
- No script/file reference execution layer in this crate (selection + injection only).
- No standard CLI for skill management (use `adk-cli` wrapper if available).

## Application Integration Patterns

To ensure scores are well-consumed and the system is "designed for an app," consider these production-grade patterns:

### 1. The "Confidence Gatekeeper"
Don't always accept the top-1 match. In high-stakes applications (e.g., medical or financial), use a higher `min_score` (e.g., `5.0`) to ensure the agent only acts when it is highly confident in the match. 

### 2. Ambiguity Handling (The "Menu" Pattern)
If the top 3 skills have very close scores (e.g., within 0.5 of each other), instead of injecting one and potentially being wrong, use the scores to present a "Menu" to the user:
> "I found a few skills that could help. Would you like to use the **Account Recovery** skill or the **Security Update** skill?"

### 3. The "Generalist Fallback"
Always maintain a "Generalist" skill with broad tags and no strict `allowed-tools`. If `select_skills` returns an empty list, fall back to this base instruction set to ensure the agent doesn't simply fail or hallucinate a persona.

### 4. Observability & Evaluation
Log the `SkillMatch.score` along with the `SkillDocument.hash` for every interaction. This allows you to perform "Offline Evaluation":
- Re-run queries against newer instructor sets.
- Detect "Score Drift" where changes to a popular skill's description cause it to lose relevance for core queries.

## Best Practices for Skill Authors

### 1. Specification vs. Runtime Split
Maintain two versions of high-stakes skills:
- **`SKILL.md` (Design-Time)**: The comprehensive specification, including rationale, edge cases, and compliance data. Store this in your source-controlled `skills/` directory.
- **`.skills/<name>.md` (Runtime)**: An optimized, "distilled" version of the instructions. Strip out noise and focus on "Loud" instructions that the LLM can follow with minimal latency.

### 2. Explicit Tooling (Logic-as-Data)
Always define `allowed-tools` if your runtime supports dynamic loading. It acts as a safety barrier and reduces "tool hallucination" where the model tries to use a tool it isn't authorized for.

### 3. Descriptive Metadata
The `description` field is the primary driver for selection. If multiple skills are being matched, ensure descriptions are mutually exclusive to avoid "prompt pollution" where two conflicting personas are injected simultaneously.

### 5. Optimizing Selection Score

Because the selection engine uses weighted token matches, you can "steer" discovery by optimizing your metadata:
- **Title Power**: Use specific, unique terms in the `name` field (+4.0 weight).
- **Keyword Density**: Ensure the `description` (+2.5) contains the primary keywords you expect in users' queries.
- **Tag Categorization**: Use `tags` (+2.0) for synonyms or broad-brush categories (e.g., `[plumbing, drainage]`) that might not be in the name.
- **Normalization Strategy**: Keep your instructional `body` concise. A very long body increases the normalization factor (`sqrt(tokens)`), which can slightly penalize the overall score compared to a punchy, focused skill.

## Related Examples

From this repository:

- `examples/skills_llm_minimal`
- `examples/skills_auto_discovery`
- `examples/skills_policy_filters`
- `examples/skills_runner_injector`
- `examples/skills_workflow_minimal`

## Development

```bash
cargo test -p adk-skill
```

## License

Apache-2.0