agentlings 0.2.3__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentlings-0.2.3 → agentlings-0.4.0}/PKG-INFO +112 -6
- {agentlings-0.2.3 → agentlings-0.4.0}/README.md +111 -5
- agentlings-0.4.0/media/skills.png +0 -0
- agentlings-0.4.0/media/tools.png +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/pyproject.toml +1 -1
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/cli/init.py +4 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/config.py +12 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/loop.py +3 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/prompt.py +18 -5
- agentlings-0.4.0/src/agentlings/core/skills.py +156 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/task.py +7 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/server.py +13 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/templates/default/.env.example +5 -0
- agentlings-0.4.0/src/agentlings/tools/__init__.py +10 -0
- agentlings-0.4.0/src/agentlings/tools/decorator.py +218 -0
- agentlings-0.4.0/src/agentlings/tools/examples/__init__.py +22 -0
- agentlings-0.4.0/src/agentlings/tools/examples/echo.py +14 -0
- agentlings-0.4.0/src/agentlings/tools/examples/geocode.py +57 -0
- agentlings-0.4.0/src/agentlings/tools/examples/http_get.py +29 -0
- agentlings-0.4.0/src/agentlings/tools/examples/set_severity.py +25 -0
- agentlings-0.4.0/src/agentlings/tools/loader.py +126 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/tools/registry.py +33 -1
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_cli_init.py +29 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_config.py +29 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_prompt.py +34 -1
- agentlings-0.4.0/tests/unit/test_skills.py +467 -0
- agentlings-0.4.0/tests/unit/test_tool_decorator.py +383 -0
- agentlings-0.4.0/tests/unit/test_tool_loader.py +291 -0
- agentlings-0.2.3/DESIGN-memory-sleep.md +0 -505
- agentlings-0.2.3/src/agentlings/tools/__init__.py +0 -1
- {agentlings-0.2.3 → agentlings-0.4.0}/.env.example +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/.github/workflows/ci.yml +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/.github/workflows/publish.yml +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/.gitignore +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/CLAUDE.md +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/Dockerfile +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/LICENSE +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/agent.example.yaml +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/docker-compose.test.yml +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0/media}/logo.png +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0/media}/sleep.png +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/scripts/release.sh +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/__main__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/cli/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/cli/_migrations.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/cli/_templates.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/cli/_version.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/cli/upgrade.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/completion.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/llm.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/memory_models.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/memory_store.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/models.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/scheduler.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/sleep.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/store.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/core/telemetry.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/log.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/migrations/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/migrations/m0001_seed.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/protocol/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/protocol/a2a.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/protocol/a2a_task_store.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/protocol/agent_card.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/protocol/mcp.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/templates/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/templates/default/agent.yaml +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/tools/builtins.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/src/agentlings/tools/memory.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/Dockerfile +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/agent.test.yaml +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/a2a_client.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/conftest.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/mcp_client.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/test_a2a.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/test_agent_card.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/test_mcp.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/test_ollama.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/integration/test_task_flow.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/__init__.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/conftest.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_a2a_executor.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_a2a_task_store.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_agent_card.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_cli_upgrade.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_completion.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_live_api.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_llm.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_logging.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_loop.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_mcp_handler.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_memory_models.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_memory_store.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_memory_tool.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_models.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_scheduler.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_sleep.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_store.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_task.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_telemetry.py +0 -0
- {agentlings-0.2.3 → agentlings-0.4.0}/tests/unit/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentlings
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Lightweight A2A + MCP single-process agent framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/andyjmorgan/DonkeyWork-Agentlings
|
|
6
6
|
Project-URL: Repository, https://github.com/andyjmorgan/DonkeyWork-Agentlings
|
|
@@ -37,7 +37,7 @@ Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
|
|
39
39
|
<p align="center">
|
|
40
|
-
<img src="logo.png" alt="Agentlings" width="256">
|
|
40
|
+
<img src="media/logo.png" alt="Agentlings" width="256">
|
|
41
41
|
</p>
|
|
42
42
|
|
|
43
43
|
<h1 align="center">Agentlings</h1>
|
|
@@ -82,8 +82,10 @@ my-agent/
|
|
|
82
82
|
├── .env # AGENT_API_KEY auto-generated; ANTHROPIC_API_KEY blank for you
|
|
83
83
|
├── .env.example # checked into source control as a template
|
|
84
84
|
├── .framework-version # the framework version that scaffolded this dir
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
├── data/ # journals, memory, conversations
|
|
86
|
+
│ └── .migrations # applied-migrations log
|
|
87
|
+
├── skills/ # drop SKILL.md bundles here; uncomment AGENT_SKILLS_DIR to enable
|
|
88
|
+
└── tools/ # drop @tool-decorated .py files here; uncomment AGENT_TOOLS_DIR to enable
|
|
87
89
|
```
|
|
88
90
|
|
|
89
91
|
`agentling run` reads `agent.yaml`, `.env`, and `data/` from the current directory. To operate on a different dir without `cd`-ing in: `agentling run --dir /path/to/agent`.
|
|
@@ -95,6 +97,8 @@ The running agent serves:
|
|
|
95
97
|
- `POST /a2a` — A2A JSON-RPC endpoint
|
|
96
98
|
- `POST /mcp` — MCP Streamable HTTP endpoint
|
|
97
99
|
|
|
100
|
+
Both protocols are task-aware. Each request becomes a task; the HTTP handler awaits up to `AGENT_TASK_AWAIT_SECONDS` (default 60) and either returns the final answer inline or yields a task handle the caller polls. A2A clients can opt out of the wait per-request via `configuration.return_immediately = true` on `message/send` (A2A v1.0 JSON name `returnImmediately`) — the handler then enqueues a `Task` object immediately and the caller polls via `tasks/get`.
|
|
101
|
+
|
|
98
102
|
## CLI
|
|
99
103
|
|
|
100
104
|
| Command | Purpose |
|
|
@@ -222,7 +226,106 @@ Point to it with `AGENT_CONFIG=./agent.yaml`.
|
|
|
222
226
|
| `filesystem` | `read_file`, `write_file`, `edit_file`, `list_directory`, `search_files` | File operations with offset/limit, find-and-replace, glob search |
|
|
223
227
|
| `memory` | `memory_edit` | Read and write the agent's persistent long-term memory |
|
|
224
228
|
|
|
225
|
-
Tools are off by default. Run `agentling
|
|
229
|
+
Tools are off by default. Run `agentling list-tools` for details.
|
|
230
|
+
|
|
231
|
+
## Custom tools
|
|
232
|
+
|
|
233
|
+
<p align="center">
|
|
234
|
+
<img src="media/tools.png" alt="Custom tools" width="256">
|
|
235
|
+
</p>
|
|
236
|
+
|
|
237
|
+
Beyond the built-ins, you can author your own tools as plain typed Python functions. Decorate them with `@tool`, drop the file in a directory, and point `AGENT_TOOLS_DIR` at it — the agentling scans the directory at startup and registers every `Tool` it finds.
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
# tools/weather.py
|
|
241
|
+
import os
|
|
242
|
+
from typing import Annotated, Literal
|
|
243
|
+
from pydantic import Field
|
|
244
|
+
from agentlings.tools import tool
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@tool
|
|
248
|
+
async def weather(
|
|
249
|
+
city: Annotated[str, Field(description="City name, e.g. 'Dublin'.")],
|
|
250
|
+
units: Literal["metric", "imperial"] = "metric",
|
|
251
|
+
) -> str:
|
|
252
|
+
"""Look up current weather for a city."""
|
|
253
|
+
api_key = os.environ["WEATHER_API_KEY"]
|
|
254
|
+
# ...fetch and return a string the LLM can read...
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Then run with `AGENT_TOOLS_DIR=./tools agentling run`. No registration step, no schema dict — the JSON Schema the LLM sees is derived from the function signature via Pydantic.
|
|
258
|
+
|
|
259
|
+
### How discovery works
|
|
260
|
+
|
|
261
|
+
- The loader scans the top level of `AGENT_TOOLS_DIR` for `.py` files (no recursion).
|
|
262
|
+
- Files whose name begins with `_` are skipped (use them for shared helpers).
|
|
263
|
+
- Each file is imported in isolation — the directory is never added to `sys.path`, so a file named `json.py` cannot shadow the stdlib.
|
|
264
|
+
- Every module-level `Tool` instance (i.e. anything you decorated with `@tool`) is registered.
|
|
265
|
+
- An import or registration failure on one file is logged and the scan continues — one broken tool cannot brick the agent.
|
|
266
|
+
|
|
267
|
+
### Authoring contract
|
|
268
|
+
|
|
269
|
+
| Concept | How to express it |
|
|
270
|
+
|---|---|
|
|
271
|
+
| Tool name | `func.__name__` (or `@tool(name="...")`) |
|
|
272
|
+
| Tool description | The function's docstring (or `@tool(description="...")`) |
|
|
273
|
+
| Parameter description / constraints | `Annotated[T, Field(description="...", ge=..., le=...)]` |
|
|
274
|
+
| Allowed values | `Literal["a", "b"]` or a `str`/`int` `Enum` |
|
|
275
|
+
| Optional / defaults | A normal Python default (`x: int = 30`) |
|
|
276
|
+
| Async I/O | `async def` — sync functions are fine too; both are awaited uniformly |
|
|
277
|
+
| Per-tool secrets | Read your own env vars inside the function (the framework stays out of secret plumbing) |
|
|
278
|
+
|
|
279
|
+
Untyped parameters, `*args`, `**kwargs`, and positional-only parameters are rejected at decoration time — `@tool` raises `ToolDefinitionError` so misuse fails loudly at startup, not in production.
|
|
280
|
+
|
|
281
|
+
Reference tools showcasing each pattern live in `agentlings.tools.examples` (`echo`, `http_get`, `set_severity`, `geocode`).
|
|
282
|
+
|
|
283
|
+
## Skills
|
|
284
|
+
|
|
285
|
+
<p align="center">
|
|
286
|
+
<img src="media/skills.png" alt="Skills" width="256">
|
|
287
|
+
</p>
|
|
288
|
+
|
|
289
|
+
Skills are bundled instructions the agent activates on demand. Each skill is a directory containing a `SKILL.md` whose YAML frontmatter (`name`, `description`) is loaded into the system prompt at startup; the body — and any sibling `scripts/`, `references/`, or `assets/` — stays on disk until the agent decides the task needs it. This is the **progressive disclosure** model from the [Open Skills specification](https://agentskills.io/specification): metadata is cheap, instructions are loaded on activation, resources are loaded on demand.
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
skills/
|
|
293
|
+
├── pdf-processing/
|
|
294
|
+
│ ├── SKILL.md
|
|
295
|
+
│ ├── scripts/extract.py
|
|
296
|
+
│ └── references/FORMS.md
|
|
297
|
+
└── data-analysis/
|
|
298
|
+
└── SKILL.md
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
A minimal `SKILL.md`:
|
|
302
|
+
|
|
303
|
+
```markdown
|
|
304
|
+
---
|
|
305
|
+
name: pdf-processing
|
|
306
|
+
description: Extract text and tables from PDFs, fill PDF forms, merge files. Use when the user mentions PDFs, forms, or document extraction.
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
Step-by-step instructions for the agent go below the frontmatter.
|
|
310
|
+
Reference companion files with relative paths, e.g. `scripts/extract.py`.
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Skills are opt-in: set `AGENT_SKILLS_DIR=./skills` (or any path) and drop skill directories there. On startup the agentling discovers them and prepends a single block to the system prompt explaining progressive disclosure and listing each skill's name, absolute path, and description. The agent reads `SKILL.md` itself when a task calls for the skill.
|
|
314
|
+
|
|
315
|
+
### Frontmatter constraints
|
|
316
|
+
|
|
317
|
+
Per the Open Skills spec:
|
|
318
|
+
|
|
319
|
+
| Field | Required | Constraint |
|
|
320
|
+
|---|---|---|
|
|
321
|
+
| `name` | Yes | 1–64 chars, lowercase `a-z`, digits, hyphens; no leading/trailing/consecutive hyphens; must match the parent directory name |
|
|
322
|
+
| `description` | Yes | 1–1024 chars, non-empty |
|
|
323
|
+
|
|
324
|
+
Optional fields (`license`, `compatibility`, `metadata`, `allowed-tools`) are accepted but currently ignored at the runtime layer. Malformed skills (missing fields, invalid names, broken YAML) are logged at `WARNING` and skipped — one bad skill does not prevent the agent from booting.
|
|
325
|
+
|
|
326
|
+
Discovery is strictly read-only — the agentling never writes to, deletes from, or modifies anything under `AGENT_SKILLS_DIR`. `AGENT_SKILLS_DIR` and `AGENT_TOOLS_DIR` share the same opt-in semantics: unset means "don't scan." `AGENT_TOOLS_DIR` additionally never adds the user-tools directory to `sys.path`, so a file named `json.py` cannot shadow the stdlib.
|
|
327
|
+
|
|
328
|
+
> **Naming note:** the `skills:` array in `agent.yaml` is unrelated — those are A2A Agent Card capabilities advertised on the wire. Runtime skills (this section) live on disk under `AGENT_SKILLS_DIR`.
|
|
226
329
|
|
|
227
330
|
## Docker
|
|
228
331
|
|
|
@@ -260,6 +363,9 @@ Secrets and runtime settings stay in env vars or, more commonly, the `.env` file
|
|
|
260
363
|
| `AGENT_HOST` | `0.0.0.0` | Bind address |
|
|
261
364
|
| `AGENT_PORT` | `8420` | Bind port |
|
|
262
365
|
| `AGENT_DATA_DIR` | `./data` | JSONL journal storage directory |
|
|
366
|
+
| `AGENT_TOOLS_DIR` | — | Directory of `@tool`-decorated `.py` files to load at startup |
|
|
367
|
+
| `AGENT_SKILLS_DIR` | — | Directory of Open Skills `SKILL.md` bundles to advertise to the agent |
|
|
368
|
+
| `AGENT_TASK_AWAIT_SECONDS` | `60` | How long the HTTP handler blocks for task completion before returning a working task handle |
|
|
263
369
|
| `AGENT_LOG_LEVEL` | `INFO` | Log level |
|
|
264
370
|
| `AGENT_LLM_BACKEND` | `anthropic` | `anthropic` or `mock` |
|
|
265
371
|
| `AGENT_EXTERNAL_URL` | — | Public URL for Agent Card (needed in Docker/k8s) |
|
|
@@ -320,7 +426,7 @@ memory:
|
|
|
320
426
|
## Sleep cycle
|
|
321
427
|
|
|
322
428
|
<p align="center">
|
|
323
|
-
<img src="sleep.png" alt="Sleep Cycle" width="256">
|
|
429
|
+
<img src="media/sleep.png" alt="Sleep Cycle" width="256">
|
|
324
430
|
</p>
|
|
325
431
|
|
|
326
432
|
The sleep cycle is a nightly process that journals the day's activity, consolidates new knowledge into memory, prunes stale entries, and cleans up old files. It maps to biological sleep phases.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="logo.png" alt="Agentlings" width="256">
|
|
2
|
+
<img src="media/logo.png" alt="Agentlings" width="256">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Agentlings</h1>
|
|
@@ -44,8 +44,10 @@ my-agent/
|
|
|
44
44
|
├── .env # AGENT_API_KEY auto-generated; ANTHROPIC_API_KEY blank for you
|
|
45
45
|
├── .env.example # checked into source control as a template
|
|
46
46
|
├── .framework-version # the framework version that scaffolded this dir
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
├── data/ # journals, memory, conversations
|
|
48
|
+
│ └── .migrations # applied-migrations log
|
|
49
|
+
├── skills/ # drop SKILL.md bundles here; uncomment AGENT_SKILLS_DIR to enable
|
|
50
|
+
└── tools/ # drop @tool-decorated .py files here; uncomment AGENT_TOOLS_DIR to enable
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
`agentling run` reads `agent.yaml`, `.env`, and `data/` from the current directory. To operate on a different dir without `cd`-ing in: `agentling run --dir /path/to/agent`.
|
|
@@ -57,6 +59,8 @@ The running agent serves:
|
|
|
57
59
|
- `POST /a2a` — A2A JSON-RPC endpoint
|
|
58
60
|
- `POST /mcp` — MCP Streamable HTTP endpoint
|
|
59
61
|
|
|
62
|
+
Both protocols are task-aware. Each request becomes a task; the HTTP handler awaits up to `AGENT_TASK_AWAIT_SECONDS` (default 60) and either returns the final answer inline or yields a task handle the caller polls. A2A clients can opt out of the wait per-request via `configuration.return_immediately = true` on `message/send` (A2A v1.0 JSON name `returnImmediately`) — the handler then enqueues a `Task` object immediately and the caller polls via `tasks/get`.
|
|
63
|
+
|
|
60
64
|
## CLI
|
|
61
65
|
|
|
62
66
|
| Command | Purpose |
|
|
@@ -184,7 +188,106 @@ Point to it with `AGENT_CONFIG=./agent.yaml`.
|
|
|
184
188
|
| `filesystem` | `read_file`, `write_file`, `edit_file`, `list_directory`, `search_files` | File operations with offset/limit, find-and-replace, glob search |
|
|
185
189
|
| `memory` | `memory_edit` | Read and write the agent's persistent long-term memory |
|
|
186
190
|
|
|
187
|
-
Tools are off by default. Run `agentling
|
|
191
|
+
Tools are off by default. Run `agentling list-tools` for details.
|
|
192
|
+
|
|
193
|
+
## Custom tools
|
|
194
|
+
|
|
195
|
+
<p align="center">
|
|
196
|
+
<img src="media/tools.png" alt="Custom tools" width="256">
|
|
197
|
+
</p>
|
|
198
|
+
|
|
199
|
+
Beyond the built-ins, you can author your own tools as plain typed Python functions. Decorate them with `@tool`, drop the file in a directory, and point `AGENT_TOOLS_DIR` at it — the agentling scans the directory at startup and registers every `Tool` it finds.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# tools/weather.py
|
|
203
|
+
import os
|
|
204
|
+
from typing import Annotated, Literal
|
|
205
|
+
from pydantic import Field
|
|
206
|
+
from agentlings.tools import tool
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@tool
|
|
210
|
+
async def weather(
|
|
211
|
+
city: Annotated[str, Field(description="City name, e.g. 'Dublin'.")],
|
|
212
|
+
units: Literal["metric", "imperial"] = "metric",
|
|
213
|
+
) -> str:
|
|
214
|
+
"""Look up current weather for a city."""
|
|
215
|
+
api_key = os.environ["WEATHER_API_KEY"]
|
|
216
|
+
# ...fetch and return a string the LLM can read...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Then run with `AGENT_TOOLS_DIR=./tools agentling run`. No registration step, no schema dict — the JSON Schema the LLM sees is derived from the function signature via Pydantic.
|
|
220
|
+
|
|
221
|
+
### How discovery works
|
|
222
|
+
|
|
223
|
+
- The loader scans the top level of `AGENT_TOOLS_DIR` for `.py` files (no recursion).
|
|
224
|
+
- Files whose name begins with `_` are skipped (use them for shared helpers).
|
|
225
|
+
- Each file is imported in isolation — the directory is never added to `sys.path`, so a file named `json.py` cannot shadow the stdlib.
|
|
226
|
+
- Every module-level `Tool` instance (i.e. anything you decorated with `@tool`) is registered.
|
|
227
|
+
- An import or registration failure on one file is logged and the scan continues — one broken tool cannot brick the agent.
|
|
228
|
+
|
|
229
|
+
### Authoring contract
|
|
230
|
+
|
|
231
|
+
| Concept | How to express it |
|
|
232
|
+
|---|---|
|
|
233
|
+
| Tool name | `func.__name__` (or `@tool(name="...")`) |
|
|
234
|
+
| Tool description | The function's docstring (or `@tool(description="...")`) |
|
|
235
|
+
| Parameter description / constraints | `Annotated[T, Field(description="...", ge=..., le=...)]` |
|
|
236
|
+
| Allowed values | `Literal["a", "b"]` or a `str`/`int` `Enum` |
|
|
237
|
+
| Optional / defaults | A normal Python default (`x: int = 30`) |
|
|
238
|
+
| Async I/O | `async def` — sync functions are fine too; both are awaited uniformly |
|
|
239
|
+
| Per-tool secrets | Read your own env vars inside the function (the framework stays out of secret plumbing) |
|
|
240
|
+
|
|
241
|
+
Untyped parameters, `*args`, `**kwargs`, and positional-only parameters are rejected at decoration time — `@tool` raises `ToolDefinitionError` so misuse fails loudly at startup, not in production.
|
|
242
|
+
|
|
243
|
+
Reference tools showcasing each pattern live in `agentlings.tools.examples` (`echo`, `http_get`, `set_severity`, `geocode`).
|
|
244
|
+
|
|
245
|
+
## Skills
|
|
246
|
+
|
|
247
|
+
<p align="center">
|
|
248
|
+
<img src="media/skills.png" alt="Skills" width="256">
|
|
249
|
+
</p>
|
|
250
|
+
|
|
251
|
+
Skills are bundled instructions the agent activates on demand. Each skill is a directory containing a `SKILL.md` whose YAML frontmatter (`name`, `description`) is loaded into the system prompt at startup; the body — and any sibling `scripts/`, `references/`, or `assets/` — stays on disk until the agent decides the task needs it. This is the **progressive disclosure** model from the [Open Skills specification](https://agentskills.io/specification): metadata is cheap, instructions are loaded on activation, resources are loaded on demand.
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
skills/
|
|
255
|
+
├── pdf-processing/
|
|
256
|
+
│ ├── SKILL.md
|
|
257
|
+
│ ├── scripts/extract.py
|
|
258
|
+
│ └── references/FORMS.md
|
|
259
|
+
└── data-analysis/
|
|
260
|
+
└── SKILL.md
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A minimal `SKILL.md`:
|
|
264
|
+
|
|
265
|
+
```markdown
|
|
266
|
+
---
|
|
267
|
+
name: pdf-processing
|
|
268
|
+
description: Extract text and tables from PDFs, fill PDF forms, merge files. Use when the user mentions PDFs, forms, or document extraction.
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
Step-by-step instructions for the agent go below the frontmatter.
|
|
272
|
+
Reference companion files with relative paths, e.g. `scripts/extract.py`.
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Skills are opt-in: set `AGENT_SKILLS_DIR=./skills` (or any path) and drop skill directories there. On startup the agentling discovers them and prepends a single block to the system prompt explaining progressive disclosure and listing each skill's name, absolute path, and description. The agent reads `SKILL.md` itself when a task calls for the skill.
|
|
276
|
+
|
|
277
|
+
### Frontmatter constraints
|
|
278
|
+
|
|
279
|
+
Per the Open Skills spec:
|
|
280
|
+
|
|
281
|
+
| Field | Required | Constraint |
|
|
282
|
+
|---|---|---|
|
|
283
|
+
| `name` | Yes | 1–64 chars, lowercase `a-z`, digits, hyphens; no leading/trailing/consecutive hyphens; must match the parent directory name |
|
|
284
|
+
| `description` | Yes | 1–1024 chars, non-empty |
|
|
285
|
+
|
|
286
|
+
Optional fields (`license`, `compatibility`, `metadata`, `allowed-tools`) are accepted but currently ignored at the runtime layer. Malformed skills (missing fields, invalid names, broken YAML) are logged at `WARNING` and skipped — one bad skill does not prevent the agent from booting.
|
|
287
|
+
|
|
288
|
+
Discovery is strictly read-only — the agentling never writes to, deletes from, or modifies anything under `AGENT_SKILLS_DIR`. `AGENT_SKILLS_DIR` and `AGENT_TOOLS_DIR` share the same opt-in semantics: unset means "don't scan." `AGENT_TOOLS_DIR` additionally never adds the user-tools directory to `sys.path`, so a file named `json.py` cannot shadow the stdlib.
|
|
289
|
+
|
|
290
|
+
> **Naming note:** the `skills:` array in `agent.yaml` is unrelated — those are A2A Agent Card capabilities advertised on the wire. Runtime skills (this section) live on disk under `AGENT_SKILLS_DIR`.
|
|
188
291
|
|
|
189
292
|
## Docker
|
|
190
293
|
|
|
@@ -222,6 +325,9 @@ Secrets and runtime settings stay in env vars or, more commonly, the `.env` file
|
|
|
222
325
|
| `AGENT_HOST` | `0.0.0.0` | Bind address |
|
|
223
326
|
| `AGENT_PORT` | `8420` | Bind port |
|
|
224
327
|
| `AGENT_DATA_DIR` | `./data` | JSONL journal storage directory |
|
|
328
|
+
| `AGENT_TOOLS_DIR` | — | Directory of `@tool`-decorated `.py` files to load at startup |
|
|
329
|
+
| `AGENT_SKILLS_DIR` | — | Directory of Open Skills `SKILL.md` bundles to advertise to the agent |
|
|
330
|
+
| `AGENT_TASK_AWAIT_SECONDS` | `60` | How long the HTTP handler blocks for task completion before returning a working task handle |
|
|
225
331
|
| `AGENT_LOG_LEVEL` | `INFO` | Log level |
|
|
226
332
|
| `AGENT_LLM_BACKEND` | `anthropic` | `anthropic` or `mock` |
|
|
227
333
|
| `AGENT_EXTERNAL_URL` | — | Public URL for Agent Card (needed in Docker/k8s) |
|
|
@@ -282,7 +388,7 @@ memory:
|
|
|
282
388
|
## Sleep cycle
|
|
283
389
|
|
|
284
390
|
<p align="center">
|
|
285
|
-
<img src="sleep.png" alt="Sleep Cycle" width="256">
|
|
391
|
+
<img src="media/sleep.png" alt="Sleep Cycle" width="256">
|
|
286
392
|
</p>
|
|
287
393
|
|
|
288
394
|
The sleep cycle is a nightly process that journals the day's activity, consolidates new knowledge into memory, prunes stale entries, and cleans up old files. It maps to biological sleep phases.
|
|
Binary file
|
|
Binary file
|
|
@@ -18,6 +18,8 @@ from agentlings.cli import _migrations, _templates, _version
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
DATA_DIRNAME = "data"
|
|
21
|
+
SKILLS_DIRNAME = "skills"
|
|
22
|
+
TOOLS_DIRNAME = "tools"
|
|
21
23
|
ENV_FILENAME = ".env"
|
|
22
24
|
ENV_EXAMPLE_FILENAME = ".env.example"
|
|
23
25
|
YAML_FILENAME = "agent.yaml"
|
|
@@ -64,6 +66,8 @@ def init_agent(
|
|
|
64
66
|
|
|
65
67
|
target.mkdir(parents=True, exist_ok=True)
|
|
66
68
|
(target / DATA_DIRNAME).mkdir(exist_ok=True)
|
|
69
|
+
(target / SKILLS_DIRNAME).mkdir(exist_ok=True)
|
|
70
|
+
(target / TOOLS_DIRNAME).mkdir(exist_ok=True)
|
|
67
71
|
|
|
68
72
|
yaml_path = target / YAML_FILENAME
|
|
69
73
|
if not yaml_path.exists() or force:
|
|
@@ -153,6 +153,7 @@ class AgentConfig(BaseSettings):
|
|
|
153
153
|
agent_host: str = "0.0.0.0"
|
|
154
154
|
agent_port: int = 8420
|
|
155
155
|
agent_data_dir: Path = Path("./data")
|
|
156
|
+
agent_skills_dir: Path | None = None
|
|
156
157
|
agent_log_level: str = "INFO"
|
|
157
158
|
agent_llm_backend: Literal["anthropic", "mock"] = "anthropic"
|
|
158
159
|
agent_external_url: str | None = None
|
|
@@ -162,6 +163,7 @@ class AgentConfig(BaseSettings):
|
|
|
162
163
|
agent_otel_insecure: bool = True
|
|
163
164
|
agent_otel_headers: str = ""
|
|
164
165
|
agent_task_await_seconds: int = 60
|
|
166
|
+
agent_tools_dir: Path | None = None
|
|
165
167
|
|
|
166
168
|
_definition: AgentDefinition = AgentDefinition()
|
|
167
169
|
|
|
@@ -202,6 +204,16 @@ class AgentConfig(BaseSettings):
|
|
|
202
204
|
"""Skills to advertise from the YAML definition."""
|
|
203
205
|
return self._definition.skills
|
|
204
206
|
|
|
207
|
+
@property
|
|
208
|
+
def skills_dir(self) -> Path | None:
|
|
209
|
+
"""Filesystem root for runtime instruction-skills (Open Skills spec).
|
|
210
|
+
|
|
211
|
+
Opt-in: when ``AGENT_SKILLS_DIR`` is unset, the agent does not scan
|
|
212
|
+
anywhere. This matches ``AGENT_TOOLS_DIR`` so the two folder-scan
|
|
213
|
+
env vars share one mental model.
|
|
214
|
+
"""
|
|
215
|
+
return self.agent_skills_dir
|
|
216
|
+
|
|
205
217
|
@property
|
|
206
218
|
def memory_config(self) -> MemoryConfig | None:
|
|
207
219
|
"""Memory configuration from the YAML definition."""
|
|
@@ -17,6 +17,7 @@ from typing import Any
|
|
|
17
17
|
from agentlings.config import AgentConfig
|
|
18
18
|
from agentlings.core.llm import BaseLLMClient
|
|
19
19
|
from agentlings.core.memory_store import MemoryFileStore
|
|
20
|
+
from agentlings.core.skills import SkillRef
|
|
20
21
|
from agentlings.core.store import JournalStore
|
|
21
22
|
from agentlings.core.task import (
|
|
22
23
|
TaskEngine,
|
|
@@ -79,6 +80,7 @@ class MessageLoop:
|
|
|
79
80
|
llm: BaseLLMClient,
|
|
80
81
|
tools: ToolRegistry,
|
|
81
82
|
memory_store: MemoryFileStore | None = None,
|
|
83
|
+
skills: list[SkillRef] | None = None,
|
|
82
84
|
) -> None:
|
|
83
85
|
self._config = config
|
|
84
86
|
self._store = store
|
|
@@ -88,6 +90,7 @@ class MessageLoop:
|
|
|
88
90
|
llm=llm,
|
|
89
91
|
tools=tools,
|
|
90
92
|
memory_store=memory_store,
|
|
93
|
+
skills=skills,
|
|
91
94
|
)
|
|
92
95
|
|
|
93
96
|
@property
|
|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
from agentlings.config import AgentConfig
|
|
10
10
|
from agentlings.core.memory_models import MemoryStore
|
|
11
|
+
from agentlings.core.skills import SkillRef, format_skills_block
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
@@ -40,6 +41,7 @@ def build_system_prompt(
|
|
|
40
41
|
data_dir: Path | None = None,
|
|
41
42
|
injection_prompt: str | None = None,
|
|
42
43
|
token_budget: int = 2000,
|
|
44
|
+
skills: list[SkillRef] | None = None,
|
|
43
45
|
) -> list[dict[str, Any]]:
|
|
44
46
|
"""Build the system prompt blocks for the Anthropic Messages API.
|
|
45
47
|
|
|
@@ -49,6 +51,9 @@ def build_system_prompt(
|
|
|
49
51
|
data_dir: Path to the agent's data directory for the awareness block.
|
|
50
52
|
injection_prompt: Override for the memory injection template.
|
|
51
53
|
Uses ``DEFAULT_MEMORY_INJECTION`` if not provided.
|
|
54
|
+
skills: Discovered runtime skills (Open Skills spec). Prepended ahead
|
|
55
|
+
of the identity block when non-empty so the agent sees them
|
|
56
|
+
before any operator-defined instructions.
|
|
52
57
|
|
|
53
58
|
Returns:
|
|
54
59
|
A list of text blocks with ``cache_control`` set to ephemeral.
|
|
@@ -58,13 +63,21 @@ def build_system_prompt(
|
|
|
58
63
|
else:
|
|
59
64
|
text = _default_prompt(config)
|
|
60
65
|
|
|
61
|
-
blocks = [
|
|
62
|
-
|
|
66
|
+
blocks: list[dict[str, Any]] = []
|
|
67
|
+
|
|
68
|
+
skills_block = format_skills_block(skills or [])
|
|
69
|
+
if skills_block is not None:
|
|
70
|
+
blocks.append({
|
|
63
71
|
"type": "text",
|
|
64
|
-
"text":
|
|
72
|
+
"text": skills_block,
|
|
65
73
|
"cache_control": {"type": "ephemeral"},
|
|
66
|
-
}
|
|
67
|
-
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
blocks.append({
|
|
77
|
+
"type": "text",
|
|
78
|
+
"text": text,
|
|
79
|
+
"cache_control": {"type": "ephemeral"},
|
|
80
|
+
})
|
|
68
81
|
|
|
69
82
|
if memory and memory.entries:
|
|
70
83
|
template = injection_prompt or DEFAULT_MEMORY_INJECTION
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Skill discovery and prompt rendering.
|
|
2
|
+
|
|
3
|
+
A skill is a directory under the configured skills root containing a
|
|
4
|
+
``SKILL.md`` file with YAML frontmatter (``name``, ``description``) followed by
|
|
5
|
+
a Markdown body. Only the metadata is loaded at startup and injected into the
|
|
6
|
+
system prompt; the body and any sibling resources (``scripts/``, ``references/``,
|
|
7
|
+
``assets/``) are loaded by the agent on demand — the *progressive disclosure*
|
|
8
|
+
pattern from the Open Skills specification (https://agentskills.io/specification).
|
|
9
|
+
|
|
10
|
+
Discovery is intentionally lenient: malformed entries are logged and skipped so
|
|
11
|
+
one broken skill does not prevent the agent from booting. It is also strictly
|
|
12
|
+
read-only — no mkdir, no writes, no imports, no ``sys.path`` mutation — so an
|
|
13
|
+
existing skills root in the user's filesystem (e.g. from an unrelated project)
|
|
14
|
+
cannot be clobbered by the agent. Discovery itself only runs when the operator
|
|
15
|
+
sets ``AGENT_SKILLS_DIR``; an unset value is a no-op, matching the opt-in
|
|
16
|
+
semantics of ``AGENT_TOOLS_DIR``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
NAME_MAX_CHARS = 64
|
|
31
|
+
DESCRIPTION_MAX_CHARS = 1024
|
|
32
|
+
|
|
33
|
+
_FRONTMATTER_RE = re.compile(
|
|
34
|
+
r"\A---\s*\n(?P<body>.*?)\n---\s*(?:\n|\Z)", re.DOTALL
|
|
35
|
+
)
|
|
36
|
+
_NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
37
|
+
|
|
38
|
+
SKILL_FILENAME = "SKILL.md"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class SkillRef:
|
|
43
|
+
"""Metadata for one discovered skill.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
name: Validated skill name (matches ``[a-z0-9]+(?:-[a-z0-9]+)*``,
|
|
47
|
+
capped at ``NAME_MAX_CHARS``). Equal to the parent directory name.
|
|
48
|
+
description: Free-form description, capped at ``DESCRIPTION_MAX_CHARS``.
|
|
49
|
+
path: Absolute path to the skill's ``SKILL.md``.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
path: Path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def discover_skills(skills_dir: Path) -> list[SkillRef]:
|
|
58
|
+
"""Walk ``skills_dir`` and return one ``SkillRef`` per valid skill.
|
|
59
|
+
|
|
60
|
+
Returns an empty list if the directory does not exist or contains no
|
|
61
|
+
valid skills. Skills are returned sorted by name for deterministic
|
|
62
|
+
prompt ordering across restarts.
|
|
63
|
+
"""
|
|
64
|
+
if not skills_dir.exists() or not skills_dir.is_dir():
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
refs: list[SkillRef] = []
|
|
68
|
+
for entry in sorted(skills_dir.iterdir()):
|
|
69
|
+
if not entry.is_dir():
|
|
70
|
+
continue
|
|
71
|
+
skill_md = entry / SKILL_FILENAME
|
|
72
|
+
if not skill_md.is_file():
|
|
73
|
+
logger.debug("skipping %s: no %s", entry, SKILL_FILENAME)
|
|
74
|
+
continue
|
|
75
|
+
ref = _parse_skill(skill_md)
|
|
76
|
+
if ref is not None:
|
|
77
|
+
refs.append(ref)
|
|
78
|
+
return refs
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_skill(skill_md: Path) -> SkillRef | None:
|
|
82
|
+
try:
|
|
83
|
+
text = skill_md.read_text(encoding="utf-8")
|
|
84
|
+
except OSError as exc:
|
|
85
|
+
logger.warning("skill unreadable: %s (%s)", skill_md, exc)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
match = _FRONTMATTER_RE.match(text)
|
|
89
|
+
if match is None:
|
|
90
|
+
logger.warning("skill missing YAML frontmatter: %s", skill_md)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
meta = yaml.safe_load(match.group("body")) or {}
|
|
95
|
+
except yaml.YAMLError as exc:
|
|
96
|
+
logger.warning("skill frontmatter not valid YAML: %s (%s)", skill_md, exc)
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
if not isinstance(meta, dict):
|
|
100
|
+
logger.warning("skill frontmatter is not a mapping: %s", skill_md)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
raw_name = meta.get("name")
|
|
104
|
+
raw_desc = meta.get("description")
|
|
105
|
+
if not isinstance(raw_name, str) or not raw_name.strip():
|
|
106
|
+
logger.warning("skill missing required 'name': %s", skill_md)
|
|
107
|
+
return None
|
|
108
|
+
if not isinstance(raw_desc, str) or not raw_desc.strip():
|
|
109
|
+
logger.warning("skill missing required 'description': %s", skill_md)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
name = raw_name.strip()[:NAME_MAX_CHARS]
|
|
113
|
+
description = raw_desc.strip()[:DESCRIPTION_MAX_CHARS]
|
|
114
|
+
|
|
115
|
+
if not _NAME_RE.match(name):
|
|
116
|
+
logger.warning(
|
|
117
|
+
"skill name %r is not spec-compliant (a-z, 0-9, hyphens; no leading/"
|
|
118
|
+
"trailing/consecutive hyphens): %s",
|
|
119
|
+
name, skill_md,
|
|
120
|
+
)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
parent = skill_md.parent.name
|
|
124
|
+
if name != parent:
|
|
125
|
+
logger.warning(
|
|
126
|
+
"skill name %r does not match parent directory %r: %s",
|
|
127
|
+
name, parent, skill_md,
|
|
128
|
+
)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
return SkillRef(name=name, description=description, path=skill_md)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_PROGRESSIVE_DISCLOSURE_PREAMBLE = (
|
|
135
|
+
"## Skills\n"
|
|
136
|
+
"\n"
|
|
137
|
+
"You have access to specialized skills below. Each skill is a self-contained "
|
|
138
|
+
"capability stored on disk. Skills follow **progressive disclosure**: only "
|
|
139
|
+
"the name and description are loaded into this prompt. To activate a skill, "
|
|
140
|
+
"read its `SKILL.md` at the listed path — that loads the full instructions "
|
|
141
|
+
"into context. Skills may also bundle `scripts/`, `references/`, or "
|
|
142
|
+
"`assets/` alongside `SKILL.md`; load those on demand only when the task "
|
|
143
|
+
"requires them.\n"
|
|
144
|
+
"\n"
|
|
145
|
+
"Available skills:"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def format_skills_block(skills: list[SkillRef]) -> str | None:
|
|
150
|
+
"""Render the system-prompt skills block, or ``None`` when no skills exist."""
|
|
151
|
+
if not skills:
|
|
152
|
+
return None
|
|
153
|
+
lines = [_PROGRESSIVE_DISCLOSURE_PREAMBLE]
|
|
154
|
+
for s in skills:
|
|
155
|
+
lines.append(f"- **{s.name}** (`{s.path}`): {s.description}")
|
|
156
|
+
return "\n".join(lines)
|
|
@@ -42,6 +42,7 @@ from agentlings.core.models import (
|
|
|
42
42
|
TaskStarted,
|
|
43
43
|
)
|
|
44
44
|
from agentlings.core.prompt import build_system_prompt
|
|
45
|
+
from agentlings.core.skills import SkillRef
|
|
45
46
|
from agentlings.core.store import JournalStore, TaskJournal
|
|
46
47
|
from agentlings.core.telemetry import get_meter, otel_span
|
|
47
48
|
from agentlings.tools.registry import ToolRegistry
|
|
@@ -351,6 +352,7 @@ class TaskWorker:
|
|
|
351
352
|
registry: TaskRegistry,
|
|
352
353
|
context_lock: asyncio.Lock,
|
|
353
354
|
memory_store: MemoryFileStore | None = None,
|
|
355
|
+
skills: list[SkillRef] | None = None,
|
|
354
356
|
) -> None:
|
|
355
357
|
self._record = record
|
|
356
358
|
self._config = config
|
|
@@ -361,6 +363,7 @@ class TaskWorker:
|
|
|
361
363
|
self._registry = registry
|
|
362
364
|
self._context_lock = context_lock
|
|
363
365
|
self._memory_store = memory_store
|
|
366
|
+
self._skills = skills or []
|
|
364
367
|
|
|
365
368
|
async def run(self) -> None:
|
|
366
369
|
"""Drive the task to a terminal state.
|
|
@@ -461,6 +464,7 @@ class TaskWorker:
|
|
|
461
464
|
data_dir=self._config.agent_data_dir,
|
|
462
465
|
injection_prompt=memory_config.injection_prompt if memory_config else None,
|
|
463
466
|
token_budget=memory_config.token_budget if memory_config else 2000,
|
|
467
|
+
skills=self._skills,
|
|
464
468
|
)
|
|
465
469
|
|
|
466
470
|
async def _on_turn(turn: Any) -> None:
|
|
@@ -573,12 +577,14 @@ class TaskEngine:
|
|
|
573
577
|
llm: BaseLLMClient,
|
|
574
578
|
tools: ToolRegistry,
|
|
575
579
|
memory_store: MemoryFileStore | None = None,
|
|
580
|
+
skills: list[SkillRef] | None = None,
|
|
576
581
|
) -> None:
|
|
577
582
|
self._config = config
|
|
578
583
|
self._store = store
|
|
579
584
|
self._llm = llm
|
|
580
585
|
self._tools = tools
|
|
581
586
|
self._memory_store = memory_store
|
|
587
|
+
self._skills = skills or []
|
|
582
588
|
self._registry = TaskRegistry()
|
|
583
589
|
self._context_locks: dict[str, asyncio.Lock] = {}
|
|
584
590
|
self._context_locks_guard = asyncio.Lock()
|
|
@@ -689,6 +695,7 @@ class TaskEngine:
|
|
|
689
695
|
registry=self._registry,
|
|
690
696
|
context_lock=ctx_lock,
|
|
691
697
|
memory_store=self._memory_store,
|
|
698
|
+
skills=self._skills,
|
|
692
699
|
)
|
|
693
700
|
asyncio_task = asyncio.create_task(worker.run(), name=f"task:{task_id}")
|
|
694
701
|
self._workers[task_id] = asyncio_task
|