From ca65fbe4df42b6c69ad918ce1f4b961ac420a9b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 00:58:02 +0000 Subject: [PATCH 1/4] Replace Trace with ConversationHistory, drop OpenTelemetry dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Curator's input is now ConversationHistory (list[dict[str, Any]]) following the litellm/OpenAI message format convention. This replaces the OTel Trace wrapper — conversation messages are a better source for skill extraction than telemetry spans. skillos-core: - New conversation.py with Message and ConversationHistory type aliases and a TODO noting the need for a cross-framework standard. - Curator ABC: curate(history: ConversationHistory) -> Changelog. - Removed trace.py, test_trace.py, opentelemetry-api/sdk dependencies. skillos-strands: - StrandsCurator.curate now takes ConversationHistory. - _format_history renders both Strands-style (content blocks with toolUse/toolResult) and OpenAI-style (tool_calls at message level) messages into a readable prompt for the curator agent. - Tests cover both message formats, empty history, and end-to-end curator integration. https://claude.ai/code/session_01K9ZQSP244HQWEobYU7euik --- packages/skillos-core/pyproject.toml | 2 - .../skillos-core/src/skillos_core/__init__.py | 5 +- .../src/skillos_core/conversation.py | 17 ++++ .../skillos-core/src/skillos_core/curator.py | 4 +- .../skillos-core/src/skillos_core/trace.py | 12 --- packages/skillos-core/tests/test_trace.py | 23 ------ .../src/skillos_strands/curator.py | 79 +++++++++++++------ .../skillos-strands/tests/test_curator.py | 65 ++++++++++++--- uv.lock | 4 - 9 files changed, 130 insertions(+), 81 deletions(-) create mode 100644 packages/skillos-core/src/skillos_core/conversation.py delete mode 100644 packages/skillos-core/src/skillos_core/trace.py delete mode 100644 packages/skillos-core/tests/test_trace.py diff --git a/packages/skillos-core/pyproject.toml b/packages/skillos-core/pyproject.toml index cd48bca..3fc6e7b 100644 --- a/packages/skillos-core/pyproject.toml +++ b/packages/skillos-core/pyproject.toml @@ -17,8 +17,6 @@ classifiers = [ ] dependencies = [ "fsspec>=2024.6.0", - "opentelemetry-api>=1.20", - "opentelemetry-sdk>=1.20", "pyyaml>=6.0", ] diff --git a/packages/skillos-core/src/skillos_core/__init__.py b/packages/skillos-core/src/skillos_core/__init__.py index b5e7eda..7ad9675 100644 --- a/packages/skillos-core/src/skillos_core/__init__.py +++ b/packages/skillos-core/src/skillos_core/__init__.py @@ -1,15 +1,16 @@ from .changelog import Change, ChangeKind, Changelog +from .conversation import ConversationHistory, Message from .curator import Curator from .repo import License, Skill, SkillRepo -from .trace import Trace __all__ = [ "Change", "ChangeKind", "Changelog", + "ConversationHistory", "Curator", "License", + "Message", "Skill", "SkillRepo", - "Trace", ] diff --git a/packages/skillos-core/src/skillos_core/conversation.py b/packages/skillos-core/src/skillos_core/conversation.py new file mode 100644 index 0000000..0ddbcbd --- /dev/null +++ b/packages/skillos-core/src/skillos_core/conversation.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any + +# TODO: The AI agent ecosystem badly needs a cross-framework standard for +# conversation history. Right now every framework (Strands, LangChain, +# PydanticAI, ADK) has its own message format. We use litellm/OpenAI's +# message shape as the convention here, but framework packages can pass +# their native format and handle formatting themselves. +# +# Candidates to watch: +# - Pydantic AI's ModelMessage types (closest to a typed standard) +# - OpenAI message format via litellm (de facto lingua franca) +# - A2A protocol message types + +Message = dict[str, Any] +ConversationHistory = list[Message] diff --git a/packages/skillos-core/src/skillos_core/curator.py b/packages/skillos-core/src/skillos_core/curator.py index dafc1cf..01cd351 100644 --- a/packages/skillos-core/src/skillos_core/curator.py +++ b/packages/skillos-core/src/skillos_core/curator.py @@ -3,9 +3,9 @@ from abc import ABC, abstractmethod from .changelog import Changelog -from .trace import Trace +from .conversation import ConversationHistory class Curator(ABC): @abstractmethod - async def curate(self, trace: Trace) -> Changelog: ... + async def curate(self, history: ConversationHistory) -> Changelog: ... diff --git a/packages/skillos-core/src/skillos_core/trace.py b/packages/skillos-core/src/skillos_core/trace.py deleted file mode 100644 index 9262008..0000000 --- a/packages/skillos-core/src/skillos_core/trace.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -from collections.abc import Sequence -from dataclasses import dataclass - -from opentelemetry.sdk.trace import ReadableSpan - - -@dataclass -class Trace: - trace_id: str - spans: Sequence[ReadableSpan] diff --git a/packages/skillos-core/tests/test_trace.py b/packages/skillos-core/tests/test_trace.py deleted file mode 100644 index 00d89da..0000000 --- a/packages/skillos-core/tests/test_trace.py +++ /dev/null @@ -1,23 +0,0 @@ -from opentelemetry.sdk.trace import TracerProvider -from skillos_core import Trace - - -def _make_span(name: str): - provider = TracerProvider() - tracer = provider.get_tracer("test") - with tracer.start_as_current_span(name) as span: - pass - return span - - -def test_trace_holds_spans() -> None: - s1 = _make_span("op-a") - s2 = _make_span("op-b") - t = Trace(trace_id="abc123", spans=[s1, s2]) - assert t.trace_id == "abc123" - assert len(t.spans) == 2 - - -def test_trace_empty_spans() -> None: - t = Trace(trace_id="empty", spans=[]) - assert t.spans == [] diff --git a/packages/skillos-strands/src/skillos_strands/curator.py b/packages/skillos-strands/src/skillos_strands/curator.py index e4aa273..4fe010b 100644 --- a/packages/skillos-strands/src/skillos_strands/curator.py +++ b/packages/skillos-strands/src/skillos_strands/curator.py @@ -1,19 +1,20 @@ from __future__ import annotations +import json from typing import Any -from skillos_core import Changelog, Curator, SkillRepo, Trace +from skillos_core import Changelog, ConversationHistory, Curator, SkillRepo from strands import Agent from strands.models import Model from .tools import create_skill_tools SYSTEM_PROMPT = """\ -You are a skill curator for SkillOS. You receive traces of agent execution \ -and use your tools to manage a skill repository. +You are a skill curator for SkillOS. You receive conversation history from \ +an agent's run and use your tools to manage a skill repository. -Analyze the trace and determine what skills should be created, updated, or \ -deleted. Use list_skills and read_skill to understand the current state, then \ +Analyze the conversation and determine what skills should be created, updated, \ +or deleted. Use list_skills and read_skill to understand the current state, then \ insert_skill, update_skill, or delete_skill to make changes. Rules for skills: @@ -24,27 +25,59 @@ """ -def _format_trace(trace: Trace) -> str: - lines = [f"Trace {trace.trace_id}", ""] - for span in trace.spans: - name = span.name if hasattr(span, "name") else str(span) - attrs = dict(span.attributes) if hasattr(span, "attributes") and span.attributes else {} - line = f"- [{name}]" - if attrs: - attr_str = ", ".join(f"{k}={v!r}" for k, v in attrs.items()) - line += f" {{{attr_str}}}" - lines.append(line) - if len(lines) == 2: - lines.append("(empty trace)") - return "\n".join(lines) +def _format_history(history: ConversationHistory) -> str: + lines: list[str] = [] + for msg in history: + role = msg.get("role", "unknown") + content = msg.get("content", "") + + if isinstance(content, str): + lines.append(f"[{role}] {content}") + + elif isinstance(content, list): + for block in content: + if isinstance(block, dict): + if "text" in block: + lines.append(f"[{role}] {block['text']}") + elif "toolUse" in block: + tu = block["toolUse"] + lines.append( + f"[{role}] tool_call: {tu.get('name', '?')}" + f"({json.dumps(tu.get('input', {}), default=str)})" + ) + elif "toolResult" in block: + tr = block["toolResult"] + result_parts = tr.get("content", []) + text = " ".join( + p.get("text", "") for p in result_parts if isinstance(p, dict) + ) + lines.append(f"[{role}] tool_result: {text[:500]}") + elif "tool_calls" in block: + for tc in block.get("tool_calls", []): + fn = tc.get("function", {}) + lines.append( + f"[{role}] tool_call: {fn.get('name', '?')}" + f"({fn.get('arguments', '')})" + ) + else: + lines.append(f"[{role}] {block}") + + if "tool_calls" in msg: + for tc in msg["tool_calls"]: + fn = tc.get("function", {}) + lines.append( + f"[{role}] tool_call: {fn.get('name', '?')}({fn.get('arguments', '')})" + ) + + return "\n".join(lines) if lines else "(empty conversation)" class StrandsCurator(Curator): """Strands Agent-based Curator that uses tools to mutate a SkillRepo. - The Agent receives a formatted trace, reasons about what skills to - create/update/delete, and calls tools to make those changes. The - Changelog records what actually happened. + The Agent receives formatted conversation history, reasons about what + skills to create/update/delete, and calls tools to make those changes. + The Changelog records what actually happened. """ def __init__( @@ -58,10 +91,10 @@ def __init__( self._model = model self._system_prompt = system_prompt - async def curate(self, trace: Trace) -> Changelog: + async def curate(self, history: ConversationHistory) -> Changelog: changelog = Changelog() tools: list[Any] = create_skill_tools(self._repo, changelog=changelog) agent = Agent(model=self._model, tools=tools, system_prompt=self._system_prompt) - prompt = _format_trace(trace) + prompt = _format_history(history) await agent.invoke_async(prompt) return changelog diff --git a/packages/skillos-strands/tests/test_curator.py b/packages/skillos-strands/tests/test_curator.py index fd07aaa..cabb6c9 100644 --- a/packages/skillos-strands/tests/test_curator.py +++ b/packages/skillos-strands/tests/test_curator.py @@ -4,9 +4,9 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from skillos_core import ChangeKind, Changelog, SkillRepo, Trace +from skillos_core import ChangeKind, Changelog, ConversationHistory, SkillRepo from skillos_strands import StrandsCurator, create_skill_tools -from skillos_strands.curator import _format_trace +from skillos_strands.curator import _format_history @pytest.fixture @@ -19,15 +19,54 @@ def mock_model() -> MagicMock: return MagicMock() -def _make_trace() -> Trace: - return Trace(trace_id="test-trace", spans=[]) - - -def test_format_trace_empty() -> None: - t = Trace(trace_id="abc", spans=[]) - text = _format_trace(t) - assert "abc" in text - assert "(empty trace)" in text +def _sample_history() -> ConversationHistory: + return [ + {"role": "user", "content": "Summarize this PDF."}, + { + "role": "assistant", + "content": [ + {"text": "I'll read the PDF."}, + {"toolUse": {"name": "Read", "input": {"path": "doc.pdf"}}}, + ], + }, + { + "role": "user", + "content": [ + {"toolResult": {"toolUseId": "t1", "content": [{"text": "PDF text here"}]}}, + ], + }, + {"role": "assistant", "content": "Here is your summary."}, + ] + + +def test_format_history_renders_messages() -> None: + text = _format_history(_sample_history()) + assert "[user] Summarize this PDF." in text + assert "tool_call: Read" in text + assert "tool_result: PDF text here" in text + assert "[assistant] Here is your summary." in text + + +def test_format_history_openai_style() -> None: + history: ConversationHistory = [ + {"role": "user", "content": "hello"}, + { + "role": "assistant", + "content": "thinking...", + "tool_calls": [ + {"function": {"name": "search", "arguments": '{"q": "test"}'}}, + ], + }, + {"role": "tool", "content": "search result"}, + ] + text = _format_history(history) + assert "[user] hello" in text + assert "tool_call: search" in text + assert "[tool] search result" in text + + +def test_format_history_empty() -> None: + assert _format_history([]) == "(empty conversation)" def test_recording_tools_insert(repo: SkillRepo) -> None: @@ -125,7 +164,7 @@ def fake_invoke(prompt): mock_agent.invoke_async = AsyncMock(side_effect=fake_invoke) curator = StrandsCurator(repo, model=mock_model) - await curator.curate(_make_trace()) + await curator.curate(_sample_history()) mock_agent_cls.assert_called_once() assert mock_agent_cls.call_args.kwargs["model"] is mock_model @@ -152,7 +191,7 @@ def fake_invoke(prompt): mock_agent.invoke_async = AsyncMock(side_effect=fake_invoke) curator = StrandsCurator(repo, model=mock_model) - cl = await curator.curate(_make_trace()) + cl = await curator.curate(_sample_history()) assert len(cl.applied) == 1 assert cl.applied[0].name == "new-skill" diff --git a/uv.lock b/uv.lock index e460733..73d2685 100644 --- a/uv.lock +++ b/uv.lock @@ -2253,8 +2253,6 @@ name = "skillos-core" source = { editable = "packages/skillos-core" } dependencies = [ { name = "fsspec" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, { name = "pyyaml" }, ] @@ -2281,8 +2279,6 @@ requires-dist = [ { name = "fsspec", specifier = ">=2024.6.0" }, { name = "gcsfs", marker = "extra == 'all'", specifier = ">=2024.6.0" }, { name = "gcsfs", marker = "extra == 'gcs'", specifier = ">=2024.6.0" }, - { name = "opentelemetry-api", specifier = ">=1.20" }, - { name = "opentelemetry-sdk", specifier = ">=1.20" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "s3fs", marker = "extra == 'all'", specifier = ">=2024.6.0" }, { name = "s3fs", marker = "extra == 's3'", specifier = ">=2024.6.0" }, From 566c1f3bba3f7d6ce622e21e148752419e567443 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 01:35:19 +0000 Subject: [PATCH 2/4] Remove verbose TODO comment from conversation.py https://claude.ai/code/session_01K9ZQSP244HQWEobYU7euik --- .../skillos-core/src/skillos_core/conversation.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/skillos-core/src/skillos_core/conversation.py b/packages/skillos-core/src/skillos_core/conversation.py index 0ddbcbd..8967062 100644 --- a/packages/skillos-core/src/skillos_core/conversation.py +++ b/packages/skillos-core/src/skillos_core/conversation.py @@ -2,16 +2,5 @@ from typing import Any -# TODO: The AI agent ecosystem badly needs a cross-framework standard for -# conversation history. Right now every framework (Strands, LangChain, -# PydanticAI, ADK) has its own message format. We use litellm/OpenAI's -# message shape as the convention here, but framework packages can pass -# their native format and handle formatting themselves. -# -# Candidates to watch: -# - Pydantic AI's ModelMessage types (closest to a typed standard) -# - OpenAI message format via litellm (de facto lingua franca) -# - A2A protocol message types - Message = dict[str, Any] ConversationHistory = list[Message] From 26bccc0b18f4784f4c8a21b6e574a17805354c3f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 01:46:52 +0000 Subject: [PATCH 3/4] docs: restructure around core / strands / langgraph subcategories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: adds SDK integration table (strands / ADK / langgraph) and a getting-started snippet using ConversationHistory - docs/index.md: split into skillos-core interfaces section and SDK integrations section with per-framework entries - docs/getting-started.md: rewritten as a tabbed "three languages" guide — Strands, ADK, LangGraph — with working Strands examples and placeholder tabs for coming-soon integrations - docs/api/skillos-core.md: extended with Curator, ConversationHistory, Changelog, Change, and ChangeKind added in recent commits - docs/api/skillos-strands.md: new API reference for StrandsCurator and create_skill_tools - packages/skillos-strands/README.md: new package-level README - mkdocs.yml: enables navigation.tabs, content.tabs.link, and pymdownx.tabbed for the tabbed getting-started page https://claude.ai/code/session_018a9BJRxnNgyVy4qchZY2tE --- README.md | 64 +++++++++--- docs/api/skillos-core.md | 90 +++++++++++++++++ docs/api/skillos-strands.md | 96 ++++++++++++++++++ docs/getting-started.md | 156 ++++++++++++++++++++++------- docs/index.md | 69 ++++++++++--- mkdocs.yml | 5 + packages/skillos-strands/README.md | 60 +++++++++++ 7 files changed, 477 insertions(+), 63 deletions(-) create mode 100644 docs/api/skillos-strands.md create mode 100644 packages/skillos-strands/README.md diff --git a/README.md b/README.md index 94bd370..d44cf84 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,69 @@ # skillos -Implementation of Google's [SkillOS](https://arxiv.org/abs/2605.06614). +Implementation of Google's [SkillOS](https://arxiv.org/abs/2605.06614) — a framework for self-evolving agents. -Basically, it is a framework for self-evolving agents. +SkillOS lets agents record what they learn as structured skills and reuse them in future sessions. This repository is organized into a backend-agnostic core and SDK-specific integrations. ## Packages -- [`packages/skillos-core/`](packages/skillos-core/) — core abstractions - (`SkillRepo`, `Skill`, `Curator`, `AsyncCurator`). Backend-agnostic via fsspec. -- [`packages/skillos-strands/`](packages/skillos-strands/) — Strands Agents - analyzer for the Curator (Amazon Bedrock via `strands-agents`). +### Core -## Development +- [`skillos-core`](packages/skillos-core/) — interfaces and shared components: `SkillRepo`, `Skill`, `Curator`, `ConversationHistory`, `Changelog`. Backend-agnostic via [fsspec](https://filesystem-spec.readthedocs.io/). + +### SDK Integrations + +Pick the package that matches your agent framework: + +| Package | Framework | Status | +|---------|-----------|--------| +| [`skillos-strands`](packages/skillos-strands/) | [Strands Agents](https://strandsagents.com) | Available | +| `skillos-adk` | [Google ADK](https://google.github.io/adk-docs/) | Coming soon | +| `skillos-langgraph` | [LangGraph](https://langchain-ai.github.io/langgraph/) | Coming soon | + +## Getting Started + +Choose the package for your agent framework and install it: + +```bash +# Strands Agents (Amazon Bedrock) +pip install skillos-strands + +# Google ADK (coming soon) +pip install skillos-adk + +# LangGraph (coming soon) +pip install skillos-langgraph +``` + +All three share the same `SkillRepo` — the SDK package is just the bridge between your framework and the skill repository. + +**Strands example:** -This project uses [uv](https://docs.astral.sh/uv/) for environment and -dependency management. The Python version is pinned in `.python-version` -and resolved deps are locked in `uv.lock`. +```python +from skillos_core import SkillRepo +from skillos_strands import StrandsCurator +from strands.models import BedrockModel +repo = SkillRepo("./my-skills") +curator = StrandsCurator(repo, model=BedrockModel("us.amazon.nova-pro-v1:0")) + +# After each agent run, pass the conversation history to the curator +changelog = await curator.curate(history) +print(f"{len(changelog.applied)} skill(s) updated") ``` + +## Development + +This project uses [uv](https://docs.astral.sh/uv/) for environment and dependency management. The Python version is pinned in `.python-version` and resolved deps are locked in `uv.lock`. + +```bash uv sync # create .venv and install all workspace packages uv run pytest # run the full test suite ``` -To work on a single package, run pytest scoped to it: +To work on a single package: -``` +```bash uv run pytest packages/skillos-core +uv run pytest packages/skillos-strands ``` diff --git a/docs/api/skillos-core.md b/docs/api/skillos-core.md index b40ddda..a0aeaf0 100644 --- a/docs/api/skillos-core.md +++ b/docs/api/skillos-core.md @@ -106,3 +106,93 @@ Enum of common SPDX license identifiers. Accepts string coercion (`License("MIT" | `MPL_2_0` | `MPL-2.0` | | `AGPL_3_0` | `AGPL-3.0` | | `UNLICENSE` | `Unlicense` | + +--- + +## Curator + +```python +from skillos_core import Curator +``` + +Abstract base class for all curator implementations. SDK-specific packages (`skillos-strands`, `skillos-adk`, `skillos-langgraph`) each provide a concrete subclass. + +### `async curate(history: ConversationHistory) -> Changelog` + +Analyse a conversation history and apply changes to the skill repository. Returns a `Changelog` recording what was inserted, updated, or deleted. + +--- + +## ConversationHistory + +```python +from skillos_core import ConversationHistory, Message +``` + +Type alias for the conversation passed to a curator. + +```python +Message = dict[str, Any] +ConversationHistory = list[Message] +``` + +Each message is a dictionary with at minimum a `"role"` key (`"user"`, `"assistant"`) and a `"content"` key. The content may be a string or a list of content blocks (text, tool calls, tool results) depending on the agent framework. + +--- + +## Changelog + +```python +from skillos_core import Changelog +``` + +Record of mutations produced by a single curator run. + +### Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `changes` | `list[Change]` | All changes attempted (applied and failed). | +| `applied` | `list[Change]` | Changes that succeeded. | +| `failed` | `list[Change]` | Changes that raised an exception. | + +--- + +## Change + +```python +from skillos_core import Change, ChangeKind +``` + +A single mutation record within a `Changelog`. + +### Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `kind` | `ChangeKind` | `INSERT`, `UPDATE`, or `DELETE`. | +| `name` | `str` | Name of the affected skill. | +| `applied` | `bool` | `True` if the operation succeeded. | +| `error` | `str \| None` | Error message if the operation failed. | +| `description` | `str \| None` | New description (insert/update only). | +| `body` | `str \| None` | New body (insert/update only). | +| `license` | `License \| str \| None` | New license (insert/update only). | +| `allowed_tools` | `list[str] \| None` | New allowed tools (insert/update only). | +| `compatibility` | `str \| None` | New compatibility string (insert/update only). | +| `metadata` | `dict \| None` | New metadata (insert/update only). | + +--- + +## ChangeKind + +```python +from skillos_core import ChangeKind +``` + +Enum for the type of skill mutation. + +| Member | Value | +|--------|-------| +| `INSERT` | `"insert"` | +| `UPDATE` | `"update"` | +| `DELETE` | `"delete"` | diff --git a/docs/api/skillos-strands.md b/docs/api/skillos-strands.md new file mode 100644 index 0000000..088d28a --- /dev/null +++ b/docs/api/skillos-strands.md @@ -0,0 +1,96 @@ +# skillos-strands API Reference + +`skillos-strands` implements the `Curator` interface for [Strands Agents](https://strandsagents.com) (Amazon Bedrock). + +```bash +pip install skillos-strands +``` + +--- + +## StrandsCurator + +```python +from skillos_strands import StrandsCurator +``` + +A `Curator` that runs a Strands `Agent` to analyse conversation history and mutate the skill repository. The agent receives a formatted history and calls skill tools (`list_skills`, `read_skill`, `insert_skill`, `update_skill`, `delete_skill`) to decide what to create, update, or delete. + +### `StrandsCurator(repo, *, model, system_prompt=...)` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `repo` | `SkillRepo` | The skill repository to manage. | +| `model` | `strands.models.Model` | Any Strands-compatible model (e.g. `BedrockModel`). | +| `system_prompt` | `str` | Override the default curator system prompt. | + +### `async curate(history: ConversationHistory) -> Changelog` + +Format the conversation history, invoke the Strands agent, and return the resulting `Changelog`. + +**Example:** + +```python +from skillos_core import SkillRepo +from skillos_strands import StrandsCurator +from strands.models import BedrockModel + +repo = SkillRepo("./my-skills") +curator = StrandsCurator( + repo, + model=BedrockModel("us.amazon.nova-pro-v1:0"), +) + +changelog = await curator.curate(history) + +for change in changelog.applied: + print(f"[{change.kind}] {change.name}") + +for change in changelog.failed: + print(f"FAILED [{change.kind}] {change.name}: {change.error}") +``` + +--- + +## create_skill_tools + +```python +from skillos_strands import create_skill_tools +``` + +Build the list of Strands tools for interacting with a `SkillRepo`. Use this directly when you want to embed skill-management tools into your own Strands agent rather than delegating to `StrandsCurator`. + +### `create_skill_tools(repo, *, changelog=None) -> list[DecoratedFunctionTool]` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `repo` | `SkillRepo` | The skill repository to manage. | +| `changelog` | `Changelog \| None` | If provided, every mutation is recorded here. | + +Returns five tools: + +| Tool | Description | +|------|-------------| +| `list_skills` | List all skill names in the repository. | +| `read_skill` | Read a skill's metadata and body by name. | +| `insert_skill` | Create a new skill. | +| `update_skill` | Partially update an existing skill. | +| `delete_skill` | Delete a skill and all its bundled resources. | + +**Example — embedding tools in your own agent:** + +```python +from skillos_core import Changelog, SkillRepo +from skillos_strands import create_skill_tools +from strands import Agent +from strands.models import BedrockModel + +repo = SkillRepo("./my-skills") +changelog = Changelog() +skill_tools = create_skill_tools(repo, changelog=changelog) + +agent = Agent( + model=BedrockModel("us.amazon.nova-pro-v1:0"), + tools=[*your_other_tools, *skill_tools], +) +``` diff --git a/docs/getting-started.md b/docs/getting-started.md index 0f76b1c..1447cfa 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,81 +1,165 @@ # Getting Started -## Installation +SkillOS speaks three languages — **Strands**, **ADK**, and **LangGraph** — one for each major agent framework. All three share the same `SkillRepo` from `skillos-core`; the SDK package is just the bridge between your framework and the skill repository. -```bash -pip install skillos-core -``` +## 1. Install + +=== "Strands" + + ```bash + pip install skillos-strands + ``` + + Requires Python ≥ 3.10 and an AWS account with Amazon Bedrock access. -Or with [uv](https://docs.astral.sh/uv/): +=== "ADK" + + ```bash + pip install skillos-adk # coming soon + ``` + +=== "LangGraph" + + ```bash + pip install skillos-langgraph # coming soon + ``` + +If you prefer [uv](https://docs.astral.sh/uv/): ```bash -uv add skillos-core +uv add skillos-strands ``` -## Create a skill repository +--- + +## 2. Create a skill repository -A skill repository is any directory (or remote path) where each subdirectory contains a `SKILL.md` file. +A skill repository is a directory (or any remote path) where each subdirectory contains a `SKILL.md` file: ``` my-skills/ -├── hello-world/ +├── code-review/ │ └── SKILL.md -└── code-review/ +└── summarize/ ├── SKILL.md └── prompt.txt ``` -## Open and read skills +Open it with `SkillRepo`: + +```python +from skillos_core import SkillRepo + +repo = SkillRepo("./my-skills") # local +repo = SkillRepo("s3://bucket/skills") # S3 +repo = SkillRepo("gs://bucket/skills") # GCS +repo = SkillRepo("memory://test-repo") # in-memory (tests) +``` + +--- + +## 3. Attach a curator + +The curator analyses conversation history from your agent runs and updates the skill repository accordingly. + +=== "Strands" + + ```python + from skillos_core import ConversationHistory, SkillRepo + from skillos_strands import StrandsCurator + from strands.models import BedrockModel + + repo = SkillRepo("./my-skills") + curator = StrandsCurator( + repo, + model=BedrockModel("us.amazon.nova-pro-v1:0"), + ) + + # After each agent run, pass the conversation history: + history: ConversationHistory = agent.messages # list of role/content dicts + changelog = await curator.curate(history) + print(f"{len(changelog.applied)} skill(s) changed") + ``` + +=== "ADK" + + ```python + # Coming soon + from skillos_core import ConversationHistory, SkillRepo + from skillos_adk import ADKCurator + + repo = SkillRepo("./my-skills") + curator = ADKCurator(repo, model=...) + changelog = await curator.curate(history) + ``` + +=== "LangGraph" + + ```python + # Coming soon + from skillos_core import ConversationHistory, SkillRepo + from skillos_langgraph import LangGraphCurator + + repo = SkillRepo("./my-skills") + curator = LangGraphCurator(repo, model=...) + changelog = await curator.curate(history) + ``` + +--- + +## 4. Read and write skills directly + +You can also interact with the repository without a curator — useful for seeding initial skills or inspecting the repo. ```python from skillos_core import SkillRepo repo = SkillRepo("./my-skills") -# list skill names -print(repo.list_skills()) # ['code-review', 'hello-world'] +# List all skill names +print(repo.list_skills()) # ['code-review', 'summarize'] -# read a specific skill -skill = repo.read("hello-world") +# Read a skill +skill = repo.read("code-review") print(skill.description) print(skill.body) -``` -## Write skills +# Iterate +for skill in repo: + print(f"{skill.name}: {skill.description}") -```python +# Insert repo.insert( - name="summarize", - description="Summarize a document into bullet points.", - body="# Summarize\n\nGiven a document, produce a concise summary.", + name="hello-world", + description="Greet the user. Use when the user asks for a greeting.", + body="# Hello World\n\nSay hello to the user.", license="MIT", - allowed_tools=["Read", "Write"], + allowed_tools=["Read", "Bash"], ) -``` -Update an existing skill: +# Update +repo.update("hello-world", description="Greet the user warmly.") -```python -repo.update("summarize", description="Summarize any document concisely.") +# Delete +repo.delete("hello-world") ``` -Delete a skill: +--- -```python -repo.delete("summarize") -``` +## 5. Remote backends -## Remote backends - -Because SkillOS uses fsspec, any supported protocol works: +Because `SkillRepo` uses [fsspec](https://filesystem-spec.readthedocs.io/), any supported protocol works out of the box: ```python -# S3 +# S3 (pip install skillos-core[s3]) repo = SkillRepo("s3://my-bucket/skills", anon=False) -# GCS +# GCS (pip install skillos-core[gcs]) repo = SkillRepo("gs://my-bucket/skills") -# In-memory (useful for tests) +# Azure Blob +repo = SkillRepo("az://my-container/skills") + +# In-memory — great for tests repo = SkillRepo("memory://test-repo") ``` diff --git a/docs/index.md b/docs/index.md index cef6b22..dc1fe65 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,28 +1,67 @@ # SkillOS -SkillOS is a backend-agnostic skill repository format built on [fsspec](https://filesystem-spec.readthedocs.io/). It lets you store, discover, and manage agent skills — locally, on S3, GCS, Azure, or any filesystem fsspec supports. +SkillOS is a framework for self-evolving agents, based on [Google's SkillOS paper](https://arxiv.org/abs/2605.06614). Agents accumulate experience as structured **skills** stored in a repository, and a **curator** analyses conversation history to keep that repository up to date. -## Packages +This implementation is split into a backend-agnostic core and SDK-specific integrations. -| Package | Description | -|---------|-------------| -| [skillos-core](api/skillos-core.md) | Core abstractions: `SkillRepo`, `Skill`, `License`. | +--- + +## skillos-core + +The core package defines the shared interfaces and the skill repository. It has no dependency on any agent framework. + +| Component | Description | +|-----------|-------------| +| [`SkillRepo`](api/skillos-core.md#skillrepo) | Read/write skill repository backed by any [fsspec](https://filesystem-spec.readthedocs.io/) filesystem — local, S3, GCS, Azure, in-memory, and more. | +| [`Skill`](api/skillos-core.md#skill) | A parsed skill: name, frontmatter metadata, markdown body, and bundled resources. | +| [`Curator`](api/skillos-core.md#curator) | Abstract base class. Receives a `ConversationHistory` and returns a `Changelog` of mutations. | +| [`ConversationHistory`](api/skillos-core.md#conversationhistory) | Type alias for a list of conversation messages passed to a curator. | +| [`Changelog`](api/skillos-core.md#changelog) | Record of `Change` objects (insert / update / delete) produced by a curator run. | + +```bash +pip install skillos-core +``` + +--- + +## SDK integrations + +Each integration implements the `Curator` interface for a specific agent framework. Pick the one that matches your stack. + +### skillos-strands + +Curator backed by [Strands Agents](https://strandsagents.com) (Amazon Bedrock). + +| Component | Description | +|-----------|-------------| +| [`StrandsCurator`](api/skillos-strands.md#strandscurator) | Runs a Strands `Agent` that calls skill tools to mutate the repo. | +| [`create_skill_tools`](api/skillos-strands.md#create_skill_tools) | Returns the list of Strands tools (`list_skills`, `read_skill`, `insert_skill`, `update_skill`, `delete_skill`). | + +```bash +pip install skillos-strands +``` + +### skillos-adk *(coming soon)* + +Curator backed by [Google ADK](https://google.github.io/adk-docs/). + +### skillos-langgraph *(coming soon)* + +Curator backed by [LangGraph](https://langchain-ai.github.io/langgraph/). + +--- ## Quick example ```python from skillos_core import SkillRepo +from skillos_strands import StrandsCurator +from strands.models import BedrockModel repo = SkillRepo("./my-skills") +curator = StrandsCurator(repo, model=BedrockModel("us.amazon.nova-pro-v1:0")) -# list all skills -for skill in repo: - print(f"{skill.name}: {skill.description}") - -# create a new skill -repo.insert( - name="hello-world", - description="A minimal example skill.", - body="# Hello World\n\nThis skill does nothing yet.", -) +changelog = await curator.curate(history) +for change in changelog.applied: + print(change.kind, change.name) ``` diff --git a/mkdocs.yml b/mkdocs.yml index 5e8ca73..9dbb5f1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,9 @@ theme: name: Switch to light mode features: - content.code.copy + - content.tabs.link - navigation.sections + - navigation.tabs markdown_extensions: - admonition @@ -28,6 +30,8 @@ markdown_extensions: anchor_linenums: true - pymdownx.superfences - pymdownx.details + - pymdownx.tabbed: + alternate_style: true - toc: permalink: true @@ -36,3 +40,4 @@ nav: - Getting Started: getting-started.md - API Reference: - skillos-core: api/skillos-core.md + - skillos-strands: api/skillos-strands.md diff --git a/packages/skillos-strands/README.md b/packages/skillos-strands/README.md new file mode 100644 index 0000000..0fb7ab4 --- /dev/null +++ b/packages/skillos-strands/README.md @@ -0,0 +1,60 @@ +# skillos-strands + +[Strands Agents](https://strandsagents.com) integration for [SkillOS](https://github.com/sheahawkins/skillos). + +Implements the `Curator` interface from `skillos-core` using a Strands `Agent` backed by Amazon Bedrock. After each agent run, pass the conversation history to the curator and it will automatically create, update, or delete skills in your `SkillRepo`. + +## Installation + +```bash +pip install skillos-strands +``` + +Requires Python ≥ 3.10 and an AWS account with Amazon Bedrock access. + +## Quick start + +```python +from skillos_core import SkillRepo +from skillos_strands import StrandsCurator +from strands.models import BedrockModel + +repo = SkillRepo("./my-skills") +curator = StrandsCurator( + repo, + model=BedrockModel("us.amazon.nova-pro-v1:0"), +) + +# history is a list of role/content message dicts from your agent run +changelog = await curator.curate(history) + +for change in changelog.applied: + print(f"[{change.kind}] {change.name}") +``` + +## Embedding tools in your own agent + +If you want to add skill-management capabilities to an existing Strands agent rather than running a dedicated curator, use `create_skill_tools` directly: + +```python +from skillos_core import Changelog, SkillRepo +from skillos_strands import create_skill_tools +from strands import Agent +from strands.models import BedrockModel + +repo = SkillRepo("./my-skills") +changelog = Changelog() +skill_tools = create_skill_tools(repo, changelog=changelog) + +agent = Agent( + model=BedrockModel("us.amazon.nova-pro-v1:0"), + tools=[*your_other_tools, *skill_tools], +) +``` + +The five tools exposed are: `list_skills`, `read_skill`, `insert_skill`, `update_skill`, `delete_skill`. + +## See also + +- [skillos-core](../skillos-core/) — core abstractions and interfaces +- [Full documentation](https://sheahawkins.github.io/skillos/) From f64cdef887c90d773ffa156db98cbe19126ece9b Mon Sep 17 00:00:00 2001 From: skillos-bot Date: Thu, 28 May 2026 14:07:04 -0600 Subject: [PATCH 4/4] docs: add hook() to strands docs, update getting-started with hook-based integration --- README.md | 14 ++++++++----- docs/api/skillos-strands.md | 28 +++++++++++++++++++++++-- docs/getting-started.md | 29 ++++++++++++++++++++++++++ packages/skillos-strands/README.md | 33 ++++++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d44cf84..51c8a18 100644 --- a/README.md +++ b/README.md @@ -35,21 +35,25 @@ pip install skillos-adk pip install skillos-langgraph ``` -All three share the same `SkillRepo` — the SDK package is just the bridge between your framework and the skill repository. +All three share the same `SkillRepo` from `skillos-core` — the SDK package is just the bridge between your framework and the skill repository. -**Strands example:** +**Strands example — hook-based (automatic):** ```python from skillos_core import SkillRepo from skillos_strands import StrandsCurator +from strands import Agent from strands.models import BedrockModel repo = SkillRepo("./my-skills") curator = StrandsCurator(repo, model=BedrockModel("us.amazon.nova-pro-v1:0")) -# After each agent run, pass the conversation history to the curator -changelog = await curator.curate(history) -print(f"{len(changelog.applied)} skill(s) updated") +# Wire the curator as a hook — skills are updated automatically after every run +agent = Agent( + model=BedrockModel("us.amazon.nova-pro-v1:0"), + hooks=[curator.hook()], +) +await agent.invoke_async("What tools did you use for that task?") ``` ## Development diff --git a/docs/api/skillos-strands.md b/docs/api/skillos-strands.md index 088d28a..cc3aaee 100644 --- a/docs/api/skillos-strands.md +++ b/docs/api/skillos-strands.md @@ -24,9 +24,33 @@ A `Curator` that runs a Strands `Agent` to analyse conversation history and muta | `model` | `strands.models.Model` | Any Strands-compatible model (e.g. `BedrockModel`). | | `system_prompt` | `str` | Override the default curator system prompt. | +### `hook() -> HookProvider` + +Return a Strands [`HookProvider`](https://strandsagents.com/latest/user-guide/concepts/hooks/) that automatically calls `curate()` after every agent invocation. Pass the result to `Agent(hooks=[...])` for zero-touch curation — no extra code in your run loop. + +```python +from skillos_core import SkillRepo +from skillos_strands import StrandsCurator +from strands import Agent +from strands.models import BedrockModel + +repo = SkillRepo("./my-skills") +curator = StrandsCurator(repo, model=BedrockModel("us.amazon.nova-pro-v1:0")) + +agent = Agent( + model=BedrockModel("us.amazon.nova-pro-v1:0"), + hooks=[curator.hook()], +) + +# Curation fires automatically after every invocation +await agent.invoke_async("Extract text from invoice.pdf") +``` + +The hook fires on `AfterInvocationEvent`. If the agent's message list is empty the hook is a no-op. + ### `async curate(history: ConversationHistory) -> Changelog` -Format the conversation history, invoke the Strands agent, and return the resulting `Changelog`. +Format the conversation history, invoke the Strands agent, and return the resulting `Changelog`. Use this for manual control — e.g. when you receive history from an agent you don't own. **Example:** @@ -58,7 +82,7 @@ for change in changelog.failed: from skillos_strands import create_skill_tools ``` -Build the list of Strands tools for interacting with a `SkillRepo`. Use this directly when you want to embed skill-management tools into your own Strands agent rather than delegating to `StrandsCurator`. +Build the list of Strands tools for interacting with a `SkillRepo`. Use this directly when you want to embed skill-management capabilities into your own Strands agent rather than delegating to `StrandsCurator`. ### `create_skill_tools(repo, *, changelog=None) -> list[DecoratedFunctionTool]` diff --git a/docs/getting-started.md b/docs/getting-started.md index 1447cfa..48bc54f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -64,6 +64,35 @@ The curator analyses conversation history from your agent runs and updates the s === "Strands" + **Option A — automatic (recommended): wire as a hook** + + Pass `curator.hook()` to `Agent(hooks=[...])` and curation fires automatically after every invocation — no extra code in your run loop: + + ```python + from skillos_core import SkillRepo + from skillos_strands import StrandsCurator + from strands import Agent + from strands.models import BedrockModel + + repo = SkillRepo("./my-skills") + curator = StrandsCurator( + repo, + model=BedrockModel("us.amazon.nova-pro-v1:0"), + ) + + agent = Agent( + model=BedrockModel("us.amazon.nova-pro-v1:0"), + hooks=[curator.hook()], + ) + + # Curation fires automatically after this call + await agent.invoke_async("Extract text from invoice.pdf") + ``` + + **Option B — manual: call `curate()` yourself** + + Use this when you receive conversation history from an agent you don't own: + ```python from skillos_core import ConversationHistory, SkillRepo from skillos_strands import StrandsCurator diff --git a/packages/skillos-strands/README.md b/packages/skillos-strands/README.md index 0fb7ab4..6455d6f 100644 --- a/packages/skillos-strands/README.md +++ b/packages/skillos-strands/README.md @@ -2,7 +2,7 @@ [Strands Agents](https://strandsagents.com) integration for [SkillOS](https://github.com/sheahawkins/skillos). -Implements the `Curator` interface from `skillos-core` using a Strands `Agent` backed by Amazon Bedrock. After each agent run, pass the conversation history to the curator and it will automatically create, update, or delete skills in your `SkillRepo`. +Implements the `Curator` interface from `skillos-core` using a Strands `Agent` backed by Amazon Bedrock. After each agent run the curator analyses the conversation history and automatically creates, updates, or deletes skills in your `SkillRepo`. ## Installation @@ -14,6 +14,35 @@ Requires Python ≥ 3.10 and an AWS account with Amazon Bedrock access. ## Quick start +### Hook-based integration (recommended) + +Pass `curator.hook()` to `Agent(hooks=[...])` and curation fires automatically after every invocation — no extra code in your run loop: + +```python +from skillos_core import SkillRepo +from skillos_strands import StrandsCurator +from strands import Agent +from strands.models import BedrockModel + +repo = SkillRepo("./my-skills") +curator = StrandsCurator( + repo, + model=BedrockModel("us.amazon.nova-pro-v1:0"), +) + +agent = Agent( + model=BedrockModel("us.amazon.nova-pro-v1:0"), + hooks=[curator.hook()], +) + +# The curator runs automatically after this call +await agent.invoke_async("Extract text from invoice.pdf") +``` + +### Manual integration + +If you receive history from an agent you don't own, call `curate()` directly: + ```python from skillos_core import SkillRepo from skillos_strands import StrandsCurator @@ -25,7 +54,7 @@ curator = StrandsCurator( model=BedrockModel("us.amazon.nova-pro-v1:0"), ) -# history is a list of role/content message dicts from your agent run +# history is a list of role/content message dicts changelog = await curator.curate(history) for change in changelog.applied: