Files
cleveragents-core/docs/reference/skill_refresh.md
T
HAL9000 18d00c04c4
CI / lint (pull_request) Failing after 1m15s
CI / quality (pull_request) Successful in 1m21s
CI / typecheck (pull_request) Successful in 1m34s
CI / security (pull_request) Successful in 1m37s
CI / coverage (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 1m37s
CI / docker (pull_request) Has been skipped
CI / build (pull_request) Successful in 33s
CI / helm (pull_request) Successful in 26s
CI / push-validation (pull_request) Successful in 19s
CI / e2e_tests (pull_request) Successful in 3m20s
CI / integration_tests (pull_request) Successful in 4m32s
CI / status-check (pull_request) Failing after 3s
fix(skills): implement multi-scope agent skill discovery for global, project, and local tiers
Implements AgentSkillDiscovery class to support discovering Agent Skills from
multiple configured directories across three scopes (global, project, local).
Handles name collisions with precedence: local > project > global.

Adds comprehensive BDD test coverage for multi-scope discovery scenarios including:
- Global-only, project-only, and local-only discovery
- Combined discovery from all scopes
- Name collision resolution with proper precedence
- Non-existent and empty scope directory handling
- Multiple skills in same scope discovery

ISSUES CLOSED: #9369
2026-05-06 19:55:22 +00:00

8.3 KiB
Raw Blame History

Skill Refresh Hooks

This document covers the M4 skill-registry refresh feature: dynamic recomputation of skill tool sets triggered by notifications/tools/list_changed events from MCP servers.

Module: cleveragents.skills.refresh, cleveragents.skills.registry, cleveragents.mcp.refresh_hook


Overview

Component Purpose
SkillRefreshResult Immutable summary of a refresh operation
SkillRegistry.refresh(name) Recompute a single skill's tool set
SkillRegistry.refresh_all() Recompute all registered skills
MCPRefreshHook Wire MCP notifications to refresh_all() with debouncing

When an MCP server's tool list changes, the adapter broadcasts a notifications/tools/list_changed notification. MCPRefreshHook listens for this event and triggers SkillRegistry.refresh_all() after a configurable debounce window, ensuring rapid successive notifications collapse into a single refresh.


SkillRefreshResult

Module: cleveragents.skills.refresh
Export: cleveragents.skills.SkillRefreshResult

An immutable (frozen=True) dataclass summarising the outcome of one or more refresh operations.

Fields

Field Type Description
refreshed list[str] Skill names successfully recomputed
failed dict[str, str] Mapping of skill name → error message for each failure
skipped list[str] Skill names skipped (tool registry unavailable)

Properties

Property Type Description
total_refreshed int len(refreshed)
total_failed int len(failed)
total_skipped int len(skipped)

Methods

to_summary() → str

Returns a human-readable one-liner suitable for CLI output or log messages.

"refreshed: 3, failed: 1, skipped: 0"

merge(other: SkillRefreshResult) → SkillRefreshResult

Combines two results into one by concatenating the refreshed and skipped lists and merging the failed dicts. Used internally by refresh_all() to aggregate per-skill results.

Example

from cleveragents.skills import SkillRefreshResult

r1 = SkillRefreshResult(refreshed=["ns/skill-a"], failed={}, skipped=[])
r2 = SkillRefreshResult(refreshed=[], failed={"ns/skill-b": "tool missing"}, skipped=[])
merged = r1.merge(r2)
print(merged.to_summary())  # "refreshed: 1, failed: 1, skipped: 0"

SkillRegistry Refresh Methods

Module: cleveragents.skills.registry

refresh(name: str) → SkillRefreshResult

Recomputes the flattened tool set for a single registered skill and validates tool references against the ToolRegistry (if one is configured on the registry).

Parameters

Parameter Type Description
name str Namespaced skill name to refresh

Returns: SkillRefreshResult — skill appears in refreshed, failed, or skipped depending on the outcome.

Raises: SkillExecutionError with SKILL_NOT_FOUND if name is not registered.

Behaviour

Condition Outcome
Resolution succeeds, tool registry present, all refs found refreshed = [name]
Resolution succeeds, tool registry present, refs missing failed = {name: "<missing refs>"}
Resolution succeeds, no tool registry refreshed = [name], warning logged
Resolution fails (cycle, missing include) failed = {name: "<error>"}

When no ToolRegistry is configured, a WARNING-level log message is emitted with recovery instructions (pass a ToolRegistry to SkillRegistry.__init__). The skill is still counted as refreshed so callers are not permanently blocked.

refresh_all() → SkillRefreshResult

Iterates over all registered skills, calls refresh(name) for each, and returns an aggregated SkillRefreshResult.

Returns: Aggregated SkillRefreshResult across all skills.

Example

from cleveragents.skills.registry import SkillRegistry
from cleveragents.tool.registry import ToolRegistry

tool_reg = ToolRegistry()
# ... register tools ...

skill_reg = SkillRegistry(tool_registry=tool_reg)
# ... register skills ...

result = skill_reg.refresh_all()
print(result.to_summary())  # e.g. "refreshed: 5, failed: 0, skipped: 0"

MCPRefreshHook

Module: cleveragents.mcp.refresh_hook
Export: cleveragents.mcp.MCPRefreshHook

Connects an MCPToolAdapter to a SkillRegistry via the adapter's notification listener mechanism. On receiving notifications/tools/list_changed, it schedules a debounced call to skill_registry.refresh_all().

Constructor

MCPRefreshHook(
    adapter: MCPToolAdapter,
    skill_registry: SkillRegistry,
    debounce_seconds: float = 0.5,
)
Parameter Type Default Description
adapter MCPToolAdapter Adapter whose notifications trigger refresh
skill_registry SkillRegistry Registry to refresh on notification
debounce_seconds float 0.5 Seconds to wait after the last notification before triggering refresh

Raises: ValueError if debounce_seconds < 0.

Debounce Behaviour

Rapid successive notifications/tools/list_changed events within the debounce window are coalesced: each new notification resets the countdown timer, so only one refresh_all() fires once the storm subsides.

notification → reset timer (0.5 s)
notification → reset timer (0.5 s)   ← only this one fires
notification → reset timer (0.5 s)
                                     0.5 s later → refresh_all() ×1

Properties

Property Type Description
refresh_count int Number of times refresh_all() has been triggered (thread-safe)

Methods

cancel() → None

Cancels any pending debounced refresh timer. Safe to call multiple times and from any thread. Does not un-register the notification listener from the adapter.

Example

from cleveragents.mcp import MCPRefreshHook
from cleveragents.mcp.adapter import MCPServerConfig, MCPToolAdapter
from cleveragents.skills.registry import SkillRegistry

adapter = MCPToolAdapter(config=MCPServerConfig(name="my-server", transport="stdio", command="my-mcp"))
skill_registry = SkillRegistry()

hook = MCPRefreshHook(
    adapter=adapter,
    skill_registry=skill_registry,
    debounce_seconds=0.5,
)

# ... adapter receives notifications automatically during normal operation ...

# Clean up when done
hook.cancel()

Thread Safety

MCPRefreshHook uses threading.Lock internally. The _on_notification callback, _do_refresh, refresh_count, and cancel are all safe to call from multiple threads concurrently.


MCPToolAdapter Notification API

Module: cleveragents.mcp.adapter

MCPToolAdapter exposes a generic notification listener API used by MCPRefreshHook.

add_notification_listener(callback)

Registers a callable to receive all notifications dispatched by this adapter.

def callback(method: str, params: dict[str, Any]) -> None: ...

dispatch_notification(method, params=None)

Dispatches a notification to all registered listeners. Used internally when the underlying MCP server sends a notification, and available for testing.


Notification Flow

MCP Server
  │  notifications/tools/list_changed
  ▼
MCPToolAdapter.dispatch_notification()
  │
  ▼  (fan-out to all listeners)
MCPRefreshHook._on_notification()
  │  debounce_seconds timer
  ▼
SkillRegistry.refresh_all()
  │  per-skill
  ▼
SkillRegistry.refresh(name)
  │  validate via ToolRegistry
  ▼
SkillRefreshResult  →  logged at INFO

Exports

cleveragents.skills

from cleveragents.skills import SkillRefreshResult

cleveragents.mcp

from cleveragents.mcp import MCPRefreshHook

Error Reference

Situation Result field Details
Skill not registered SkillExecutionError raised SKILL_NOT_FOUND error type
Tool ref missing from registry failed[name] Lists missing ref names
Resolution cycle / missing include failed[name] Exception message
No ToolRegistry configured refreshed[name] WARNING log emitted
debounce_seconds < 0 ValueError raised Constructor validation