agentwerk (Python)
0.2.0.Why use agentwerk?
- Simple interface: create agents with a few lines of code.
- Efficient harness: optimized for small and fast LLMs with low memory footprint.
- Complex interactions: let agents collaborate through a shared Werk, event hooks, and knowledge.
- Deep observability: inspect every request, tool call, and failure.
- Facilitate training: store trajectories based on granular events for fine-tuning models.
Installation
Install the Python package from PyPI or follow the separate guide for the Rust crate.
Python
pip install agentwerk
Quick Start
This example gives one agent read-only tools to search Rust source files, then waits for one result.
import asyncio
from agentwerk import Agent, GrepTool, ReadFileTool
async def main():
agent = (
Agent.from_env()
.role("Explore Rust source files and answer the question.")
.tool(ReadFileTool())
.tool(GrepTool())
)
task = agent.add_task(
"Find every `pub trait` defined under src/ and explain each in one sentence."
)
result = await agent.finish_task(task)
print(result)
asyncio.run(main())
API
agentwerk has six core concepts. An Agent uses Tools to complete a Task and create a JSON result. A Werk coordinates agents and tasks, Events record activity, and Knowledge provides durable shared memory.
| Section | Covers |
|---|---|
| Agents | Roles, behavior, and Providers. |
| Tasks | Templates, JSONPath, Schemas, and Directives. |
| Werk | Queries, Execution, Result sharing, Configuration, Compaction, and Sessions. |
| Tools | Controlled capabilities for agents. |
| Events | Observability and hooks. |
| Knowledge | Durable shared memory. |
Agents
An Agent uses a language model and the tools you provide to complete tasks.
from agentwerk import Agent, ReadFileTool
agent = (
Agent.from_env()
.role("You are a release manager who prepares release notes.")
.tool(ReadFileTool())
)
agent.add_task("Read CHANGELOG.md and summarize the entries added since the last release.")
results = await agent.finish()
Agent reference
| Method | Description | |
|---|---|---|
| Configure | role(role) |
Define who the agent is and how it should work. |
tool(tool) |
Register a tool the agent may call. | |
tools(tools) |
Register several tools the agent may call. | |
label(label) |
Restrict the agent to tasks carrying this label. | |
dir(dir) |
Set the directory the agent has access to. | |
template(key, value) |
Set a shared template. | |
templates(variables) |
Set several shared templates together. | |
directive(key, template) |
Set a directive template by key. | |
directives(overrides) |
Set more than one directive template. | |
knowledge(store) |
Share a knowledge store and register its KnowledgeTool. |
|
interactive() |
Let the agent wait for new instructions to keep a task in-progress. | |
| Work | add_task(task) |
Submit a task, or a Task carrying a label or schema, and return its task ID. |
start() |
Keep processing tasks in the background. | |
finish_task(query) |
Wait for all matches and return the first result in query order. | |
finish_tasks(query) |
Wait for matching tasks and get their results. | |
finish() |
Run tasks and return their results. | |
get_id() |
Get the unique identifier of an agent. |
Use {{ context }} in a prompt to include the current task and execution limits:
- Task: t-7
- Date: 2026-05-06
- Working directory: /Users/caro
- Platform: darwin 25.1.0
- Turns remaining: 8
- Input tokens remaining: 95000
- Output tokens remaining: 12000
- Time remaining: 240s
Each context field is also available separately: {{ task_id }}, {{ date }}, {{ dir }}, {{ platform }}, {{ os_version }}, {{ turns_remaining }}, {{ input_tokens_remaining }}, {{ output_tokens_remaining }}, and {{ time_remaining }}.
Interactive
An interactive agent keeps a task open across replies. It has no completion tool by default.
def show(werk, task, result):
print(f"{task.get_id()}: {result}")
agent = Agent.from_env().interactive()
id = agent.add_task("Where does the configuration get loaded?")
werk = agent.start()
werk.on_result(show)
await werk.finish()
werk.add_reply(id, "And which environment variables override it?")
await werk.finish()
werk.set_task_finished(id, "answered")
Replies pause the task in in_progress, and completion methods return when it pauses. Use add_reply(id, content) to resume and set_task_finished(id, result) to end the conversation. Intermediate replies arrive as events. on_result receives the final result.
See Agent.
Providers
Connect agents to Anthropic, OpenAI, Mistral, or a LiteLLM proxy.
from agentwerk import Agent, Anthropic
agent = (
Agent()
.provider(Anthropic(key))
.model("claude-sonnet-4-20250514")
)
Provider and model reference
| Method | Description |
|---|---|
provider(provider) |
Define the LLM provider. |
model(model) |
Set the model. |
Agent.from_env() |
Read the provider and the model from environment variables. |
verify(model) |
Verify that the provider can answer with a model. |
Anthropic(key, base_url=..., timeout=...) |
Configure an Anthropic endpoint. OpenAi, Mistral, and LiteLlm accept the same options. |
You can also read the model or provider individually: .provider(Provider.from_env()) or .model(Model.from_env()).
| Variable | Description |
|---|---|
LITELLM_PROVIDER |
Choose anthropic, mistral, openai, or litellm outright, ahead of the keys below. |
LITELLM_API_KEY, MISTRAL_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY |
Authenticate with that vendor. The first one set picks the LLM provider, in this order. |
LITELLM_BASE_URL, MISTRAL_BASE_URL, ANTHROPIC_BASE_URL, OPENAI_BASE_URL |
Set a different API address for that vendor. |
SSL_CERT_FILE, SSL_CERT_DIR |
Trust these CA certificates instead of the built-in root store. |
Set a model's context window or reasoning level when the defaults do not fit. Claude, GPT, Mistral, and Qwen families have built-in settings.
| Method | Description |
|---|---|
context_window(size) |
Set the context window size for a model. |
get_context_window() |
Get the configured window size. |
reasoning_effort(effort) |
Set the reasoning level. |
get_reasoning_effort() |
Get the configured effort. |
| Variable | Description |
|---|---|
MODEL |
Set the model name. |
ANTHROPIC_MODEL, OPENAI_MODEL, MISTRAL_MODEL, LITELLM_MODEL |
Set the model for the detected provider when MODEL is unset. |
MODEL_CONTEXT_WINDOW |
Set the context window size in tokens. |
Configure a custom model:
from agentwerk import Agent, Model
agent = Agent().model(
Model("my-local-model").context_window(128_000).reasoning_effort("high")
)
Tasks
Roles and tasks can interpolate shared values and selected runtime data. The prompt skill provides a compact template for writing agent roles.
Template values
Define an agent with placeholders, then set the template values before adding its task:
from agentwerk import Agent, Task
werk.add_agent(
Agent.from_env()
.label("report")
.role(
"Write for {{ company }} using:\n"
"{{ results: research }}"
)
)
werk.set_template("company", "Canvas Computing")
await werk.finish_tasks("research")
werk.add_task(Task("Write the board report.", label="report"))
agentwerk fills placeholders in the role and task just before each task's first model request. Newly added tasks use the latest template values and results.
Template reference
| Expression | Output |
|---|---|
{{ name }} |
The value assigned to name. |
{{ name | JSONPath }} |
A value selected from the template variable after parsing it as JSON. |
{{ result: AQL }} |
The first matching result. Strings appear as text and other values as compact JSON. |
{{ results: AQL }} |
Matching results as a compact JSON array. |
{{ result: AQL | JSONPath }} |
A value selected from the first result. |
{{ results: AQL | JSONPath }} |
A value selected from the array of matching results. |
{{ task: AQL }} |
The first matching task as compact JSON. |
{{ tasks: AQL }} |
Matching tasks as a compact JSON array. |
{{ task: AQL | JSONPath }} |
A value selected from the first matching task. |
{{ tasks: AQL | JSONPath }} |
A value selected from the array of matching tasks. |
{{ event: AQL }} |
The first matching event as compact JSON. |
{{ events: AQL }} |
Matching events as a compact JSON array. |
{{ event: AQL | JSONPath }} |
A value selected from the first matching event. |
{{ events: AQL | JSONPath }} |
A value selected from the array of matching events. |
null, empty arrays, and unmatched selectors render to an empty string.
| Record | JSONPath properties |
|---|---|
| Task | task, label, schema, id, status, reporter, assignee, created_at, started_at, finished_at, failed_at |
| Event | name, directive, data, task_id, agent_id, label, created_at |
task is the original JSON input. Unset label, schema, assignee, and directive properties are omitted. Task results, errors, replies, and cancellation state are not serialized.
For example, given this research result:
{
"company": {"name": "Canvas Computing"},
"findings": [{"summary": "one"}, {"summary": "two"}]
}
This template:
{{ result: research | company.name }}
Renders as:
Canvas Computing
Select each task input or event's data:
{{ tasks: research | [*].task }}
{{ events: event.name = tool_call_failed | [*].data }}
research is shorthand for task.label = research.
| Path | Selects |
|---|---|
company.name |
A nested field. |
metadata."build-id" |
A field that requires JSON quoting. |
findings[0], findings[-1] |
An array element. |
findings[1:4], findings[::-1] |
An array slice. |
findings[*].summary |
The summary field from each array element. |
authors.*.name |
The name field from each object value, in unspecified order. |
groups[].members |
The members field after flattening one array level. |
Missing fields, incompatible types, and out-of-range indexes produce null. Wildcards, slices, and flattening omit null values when more path steps follow. Filters, comparisons, logical expressions, literals, multi-selects, functions, and additional pipes are not supported.
{ name } stays unchanged. To output the literal text {{ name }}, write {{{{ name }}}}.
Schemas
A Schema constrains a task result.
from agentwerk import Schema, Task
schema = Schema(
{
"type": "object",
"properties": {"title": {"type": "string"}},
"required": ["title"],
}
)
werk.add_task(Task("Write a report.", schema=schema))
Schema reference
agentwerk corrects common result-formatting mistakes, such as a quoted number or a nested object encoded as JSON text. Schema-bound results must be objects. Remaining schema violations trigger a retry, subject to max_schema_retries. Without a schema, a task may return any JSON value.
Use shallow, focused schemas for small models. Split complex work into tasks with separate schemas.
| Method | Description | |
|---|---|---|
| Schema | Schema(document) |
Create a schema. |
validate(value) |
Return the validated value and JSON pointers to repaired values, or report violations. |
See Schema.
Directives
Directives tell the model how to recover from failures. Override their wording for your model or environment.
from agentwerk import Agent
agent = (
Agent.from_env()
.directive("grep_failed", "The search did not run. Narrow `path`.")
.directives(
{
"tool_timed_out": "Reduce the command scope.",
"cache_miss": "No cache entry exists for {{ path }}.",
}
)
)
Directive reference
Built-in keys override recovery text. Keys without overrides retain their defaults. Templates accept runtime values such as {{ detail }}, {{ attempt }}, and {{ path }}. Placeholders without a value remain unchanged.
See prompts/directives for the built-in text.
Werk
A Werk stores tasks, assigns them to matching agents, and records their results.
from agentwerk import Agent, Task, Werk
analyst = (
Agent.from_env()
.label("analysis")
)
writer = (
Agent.from_env()
.label("report")
)
werk = Werk()
werk.add_agent(analyst).add_agent(writer)
werk.add_task(Task("Rank all products by value.", label="analysis"))
werk.add_task(Task("Write up the ranking.", label="report"))
Werk reference
| Method | Description | |
|---|---|---|
| Configure | set_policy(policy) |
Set execution limits and retry settings. |
get_policy() |
Get the policy in force. | |
set_dir(dir) |
Define where a session is stored. | |
get_dir() |
Get the session directory. | |
add_agent(agent) |
Add an agent to this Werk. | |
add_condition(condition) |
Add a runtime AQL condition and return its ID. | |
| Submit and interact | add_task(task) |
Submit a task and return its task ID. |
add_reply(id, content) |
Add a reply to a task. | |
edit_replies(id, editor) |
Rewrite a task's replies now. | |
set_task_finished(id, result) |
Finish a task with a result. | |
set_task_failed(id) |
Fail a task. | |
| Observe | on_event(handler) |
Read every event as it is emitted. |
on_event_async(handler) |
Read every event in an async hook. | |
on_result(handler) |
Read every finished task together with its result. | |
on_result_async(handler) |
Read every finished task and result in an async hook. | |
on_failure(handler) |
Read every failure together with its task. | |
on_failure_async(handler) |
Read every failure and task in an async hook. | |
on_task(handler) |
Read task state changes. | |
on_task_async(handler) |
Read task state changes in an async hook. | |
| Run | start() |
Keep processing tasks in the background. |
finish_task(query) |
Wait for all matches and return the first result in query order. | |
finish_tasks(query) |
Wait for matching tasks and get their results. | |
finish() |
Run tasks and return their results. | |
| Cancel | cancel_tasks(query) |
Stop work on matching tasks. |
cancel() |
Stop work on every task. | |
| Inspect tasks | get_task(id) |
Get one task by ID. |
get_tasks() |
Get every task in creation order. | |
find_task(query) |
Get the first matching task. | |
find_tasks(query) |
Get every matching task. | |
| Inspect results and events | get_results() |
Get every finished task result in creation order. |
find_result(query) |
Get the first result in query order. | |
find_results(query) |
Get every result in query order. | |
find_event(query) |
Get the first recorded event in query order. | |
find_events(query) |
Get every recorded event in query order. | |
| Inspect execution | get_finish_reason() |
Get why the last execution ended. |
get_model_for_agent(agent_id) |
Get the model used by an agent. | |
get_input_tokens() |
Get input tokens across finished requests. | |
get_output_tokens() |
Get output tokens across finished requests. | |
get_duration() |
Get the elapsed execution duration. |
See Werk.
Queries
Agent Query Language (AQL) filters tasks and events. Pass an AQL string
directly, or compile it with Query to reuse it.
# Find tasks labeled "scan".
werk.find_tasks("scan")
# Find failed tool calls from scan tasks.
werk.find_events("scan AND event.name = tool_call_failed")
# Find the result produced by task "t-3".
werk.find_results("t-3")
Query reference
Terms
| Syntax | Meaning | |
|---|---|---|
| Match | field = value, field != value |
Include or exclude one exact value. |
field IN (a, b), field NOT IN (a, b) |
Include or exclude a list. | |
| Presence | field IS EMPTY, field IS NOT EMPTY |
Test whether an optional field has a value. |
| Search | field ~ text, field !~ text |
Include or exclude case-insensitive text. |
| Compare | field > value, >=, <, <= |
Compare a time field. |
| Combine | A AND B, A OR B, NOT A, (A OR B) |
Combine or group conditions. |
| Task label | scan, "needs review" |
Short for task.label = scan. Quote labels containing spaces or query words. |
| Task ID | t-3 |
Short for task.id = t-3. IDs take precedence over labels. |
| Sort | ORDER BY field DESC |
Sort matches. ASC is the default. |
Fields
| Origin | Fields |
|---|---|
| Task | task.id, task.label, task.status, task.pending, task.cancelled, task.assignee, task.input, task.result, task.errors, task.created, task.started, task.finished, task.failed |
| Event | event.name, event.agent_id, event.task_id, event.label, event.created, event.data |
Queries using both namespaces match events with their referenced tasks. Events without an existing task do not match. Joined matches default to event-log order. ORDER BY accepts task or event fields.
Result finders return raw results where task.result is present. They select finished tasks unless the query specifies another status.
Completion methods and cancel_tasks also accept AQL. Event and joined queries snapshot matching task IDs when the operation starts. Task-only cancellation also applies to later matching tasks.
Rules
Missing values do not match !=. Include unlabeled tasks with task.label IS EMPTY OR task.label != scan.
Qualify fields in full expressions. Use parentheses when mixing AND and OR. NOT applies to the next condition or group. Query keywords ignore case. Labels and IDs do not.
Times accept UTC dates such as 2026-08-30, epoch milliseconds, or offsets such as -30m, -2h, -7d, and -1w. Offsets are resolved when a query is compiled. Reusing a compiled query keeps its original cutoff.
Missing sort values come last in either direction. Tasks and results selected through events follow matching event order, with each task returned once. Events selected through tasks follow task order, then log order within each task.
An invalid query string raises ValueError. Query(query) checks a query without running it and raises the same error.
Examples
# Find tasks referenced by failed tool-call events.
werk.find_tasks("event.name = tool_call_failed")
# Find events attached to tasks labeled "scan".
werk.find_events("scan")
# Find results produced by tasks with finished events.
werk.find_results("event.name = task_finished")
werk.find_results("report AND task.result ~ risk")
werk.find_tasks("task.errors ~ tool_call_failed")
werk.find_tasks("task.status = todo AND task.assignee IS EMPTY")
werk.find_tasks("task.failed > -1h ORDER BY task.failed DESC")
werk.find_tasks(lambda t: len(t.get_replies()) > 4) # a callable, for what no field carries
Execution
start() keeps processing tasks in the background. finish() runs tasks and waits for results.
task = werk.add_task("Write a report.")
answer = await werk.finish_task(task)
if answer is not None:
print(answer)
Execution and task reference
| Method | Description | |
|---|---|---|
| Run | start() |
Keep processing tasks in the background. |
finish_task(query) |
Wait for all matches and return the first result in query order. | |
finish_tasks(query) |
Wait for matching tasks and get their results. | |
finish() |
Run tasks and return their results. | |
| Cancel | cancel_tasks(query) |
Stop work on matching tasks. |
cancel() |
Stop work on every task. |
Cancellation affects only the current execution, not persisted task status. Starting a new run with start() clears cancellation. Inspect it with task.is_cancelled(), or query execution state with task.cancelled = true and task.pending = true.
Task members:
| Member | Description | |
|---|---|---|
| Identity | get_id() |
Get the task ID, of the form t-N. |
get_task() |
Get the work the agent is asked to do. | |
get_label() |
Get the label carried by the task. | |
get_reporter() |
Get the ID of the agent that created the task. | |
get_assignee() |
Get the ID of the agent that claimed the task. | |
| Outcome | get_status() |
Get the task's current status. |
is_todo() |
Check whether the task is waiting to be claimed. | |
is_in_progress() |
Check whether an agent is working on the task. | |
is_finished() |
Check whether the task finished. | |
is_failed() |
Check whether the task failed. | |
is_pending() |
Check whether the task has work in this run. | |
is_cancelled() |
Check whether this run has excluded the task from scheduling. | |
get_result() |
Get the result the agent produced. | |
get_errors() |
Get failures recorded against the task as events. | |
get_replies() |
Get messages exchanged with the model. | |
get_schema() |
Get the optional schema the result must satisfy. | |
| Timestamps | get_created_at() |
Get the creation time in milliseconds. |
get_started_at() |
Get the claim time in milliseconds. | |
get_finished_at() |
Get the finish time in milliseconds. | |
get_failed_at() |
Get the failure time in milliseconds. |
See Task.
Sharing results
Agents can pass work and results in five ways:
- Follow-up routing:
on_resultor a runtime condition creates follow-up tasks. - Task templates: interpolate shared values, results, tasks, and events.
- KnowledgeTool: shares durable pages between agents.
- TaskTool: reads any finished task's result by ID.
- ReadFileTool: opens a task's
result.jsonin the session directory.
Other result-sharing examples
Result hook
Use hooks to create new tasks when certain results arrive:
def hand_to_report(werk, done, result):
if done.get_label() == "research":
werk.add_task(Task(result, label="report"))
werk.on_result(hand_to_report)
Runtime condition
Use an AQL query to create follow-up tasks or add agents based on conditions:
from agentwerk import Condition
werk.add_condition(
Condition("task.label = research AND task.status = finished")
.id("report-after-research")
.add_agent(Agent.from_env().label("report"))
.add_task(
Task(
"Write {{ result: task.label = research AND task.status = finished }}",
label="report",
)
)
)
KnowledgeTool
Hand both agents one store, and either can write a page the other reads:
store = Knowledge.load(".agentwerk/knowledge")
analyst = Agent.from_env().label("analysis").knowledge(store)
writer = Agent.from_env().label("report").knowledge(store)
analyst.add_task("Rank the products by value, then save the ranking to your knowledge.")
TaskTool
Give the writer TaskTool, and it reads what any finished task produced, by ID:
writer = Agent.from_env().label("report").tool(TaskTool())
writer.add_task("Read the result of t-1, then write the board report.")
ReadFileTool
Give the writer ReadFileTool instead, and it opens the result file named at the end of its task:
writer = Agent.from_env().label("report").tool(ReadFileTool())
writer.add_task("Read .agentwerk/tasks/t-1/result.json, then write the board report.")
Results live in the session directory, one result.json per task.
Configuration
A Policy sets limits for turns, tokens, elapsed time, retries, and compaction.
werk.set_policy(Policy(max_turns=40, max_time=300.0))
Configuration reference
| Field | Description |
|---|---|
max_turns |
Limit the total number of turns. |
max_time |
Limit the total elapsed duration. |
max_input_tokens |
Limit the total input tokens. |
max_output_tokens |
Limit the total output tokens. |
max_request_tokens |
Limit the output tokens of a single request. |
max_schema_retries |
Limit consecutive failed tool calls or silent replies. A successful call resets the count. |
max_request_retries |
Limit how often a failing request is retried. |
request_retry_delay |
Set the base delay for exponential backoff between request retries. |
compaction_threshold |
Compact once the next request would fill this share of the window. |
set_policy(policy) replaces the whole configuration, and get_policy() reads it back. A violated limit emits Event.POLICY_VIOLATED. compaction_threshold is the exception, see Compaction.
Compaction
Compaction summarizes older messages as a task approaches the model's context limit or after the provider reports an overflow.
werk.set_policy(Policy(compaction_threshold=0.7))
Compaction reference
compaction_threshold is a fraction of the model's context window, 0.85 by default. Reaching it summarizes the older messages and the agent continues its task.
Compaction also runs after the LLM provider reports that the window was exceeded. compaction_started, compaction_progress, compaction_finished, and compaction_failed report each step, see Events.
def watch(werk, event):
if event.get_name() == Event.COMPACTION_FINISHED:
print(f"[{event.get_task_id()}] compacted {event.get_data()['trigger']}")
werk.on_event(watch)
Each compaction event carries the trigger: proactive before a context-window error or reactive after one. A failure also carries a stable kind and human-readable message.
Sessions
A Werk saves tasks, replies, and recorded events so you can resume a session.
The session directory is ./.agentwerk by default.
werk = Werk.load(".agentwerk")
werk.add_agent(my_agent)
werk.start()
Session files
.agentwerk/
├── events.jsonl recorded events, one per line
├── tasks/
│ └── t-1/
│ ├── task.json task metadata and input
│ ├── result.json task result
│ ├── replies.jsonl model messages, one per line
│ └── outputs/<tool_use_id>.txt full tool outputs
└── knowledge/
├── pages/<slug>.md knowledge pages
└── index.md knowledge index
Tools
Tools give agents controlled access to files, commands, URLs, tasks, and shared knowledge.
from agentwerk import Agent, CommandTool, GrepTool, ReadFileTool
agent = (
Agent()
.tool(ReadFileTool())
.tool(GrepTool())
.tool(CommandTool("git").allow("git *"))
)
Tool reference
FinishTool
Agents use FinishTool to end their task and share their outcomes:
{
"answer": "The configuration is loaded in src/config.rs.",
"confidence": 0.9
}
To return a result, the agent must call FinishTool. If the task has a result schema, the tool validates the object against it. For a non-interactive task without a schema, the agent can instead finish by responding with plain text.
Interactive agents are the exception: they have no FinishTool unless you add one explicitly with .tool(FinishTool()).
| Tool | Description | |
|---|---|---|
| File | ReadFileTool |
Read a file with line numbers, offset, and limit. |
WriteFileTool |
Create or overwrite a file. | |
EditFileTool |
Replace text in a file. | |
| Search | GlobTool |
Find files by pattern. |
GrepTool |
Search file contents by regular expression, or by code shape with syntax: "code". |
|
ListDirectoryTool |
List files and directories. | |
| Command | CommandTool |
Give access to specific commands. |
| Web | FetchTool |
Fetch a URL and read its body. |
| Events | EventTool |
Publish an event. task_finished also completes the current task. |
| Tasks | FinishTool |
Write the result for the current task and mark it finished. |
TaskTool |
Read the Werk and create or edit tasks. | |
| Knowledge | KnowledgeTool |
Write, read, remove, or list pages in a knowledge store. |
Timeouts
Override a tool's limit with timeout(seconds). Zero disables it.
from agentwerk import FetchTool
quick_fetch = FetchTool().timeout(15)
patient_fetch = FetchTool().timeout(0)
| Tool | Default timeout |
|---|---|
FetchTool |
60 seconds |
GrepTool |
180 seconds |
CommandTool |
The call's timeout_ms, or 120 seconds if omitted |
| All other tools | None |
When a Python tool times out, the agent stops waiting, but its worker thread may continue in the background.
EventTool
Give an agent EventTool to let it publish custom events:
from agentwerk import EventTool
agent = Agent().tool(EventTool())
The model supplies a name and optional JSON data:
{
"name": "...",
"data": {}
}
Events carry the current task and agent context. See Events for hooks and queries. Names are unrestricted. Lowercase snake case is conventional.
Only task_finished completes the current task. Its data is the result dictionary:
{
"name": "task_finished",
"data": { "answer": "..." }
}
Use Directives to customize the acknowledgement sent to the model.
CommandTool
The CommandTool lets you specify exactly which commands and flags are allowed or denied.
git = (
CommandTool("git")
.allow("git status")
.allow("git log *")
.deny("git push*")
.deny_flag("--force")
)
With an allow_flag set, a command carrying any other flag is refused:
cargo = CommandTool("cargo").allow("cargo test*").allow_flag("--all-features")
FetchTool
The FetchTool fetches a URL and returns its text with the user agent agentwerk/<version>. impersonate() uses the headers and HTTP/2 settings of a browser.
web = FetchTool().impersonate()
Custom Tools
Use concurrent=True when a custom tool has no side effects and may run in parallel with other calls.
agentwerk uses type annotations to tell the model which arguments it can pass.
Arguments without default values are required. It understands lists,
dictionaries, tuples, Literal, Optional, and unions. Use schema= when
annotations are not enough.
from agentwerk import tool
@tool(concurrent=True, timeout=5)
def greet(name: str) -> str:
"""Say hello."""
return f"Hello, {name}!"
See Tool.
Events
Events provide detailed observability into agent behavior during execution. Register hooks to react to every event, finished result, failure, or task state change:
werk.on_event(lambda _, event: print(f"event: {event.get_name()}"))
werk.on_result(lambda _, task, result: print(f"{task.get_id()}: {result}"))
werk.on_failure(lambda _, event, task: print(f"{task.get_id()}: {event.get_name()}"))
werk.on_task(lambda _, event, task: print(f"{task.get_id()}: {event.get_name()}"))
Event and hook reference
Event names
| Name | Description | |
|---|---|---|
| Run | run_started |
Execution began. |
run_finished |
Execution ended, carrying its outcome. | |
policy_violated |
A limit was breached and execution stopped. | |
| Task | task_started |
An agent claimed a task. |
task_created |
A task was added to the Werk. | |
task_finished |
A task finished successfully, carrying its result when it has one. | |
task_failed |
A task failed. | |
turn_started |
The agent began another turn on its task. | |
schema_retried |
A tool call or result the model created was invalid. | |
| LLM provider | request_started |
A request went out to the model. |
request_finished |
A request finished and reported its token usage. | |
request_failed |
A request failed and was not retried. | |
prompt_render_failed |
A role or task expression could not render before its request. | |
request_retried |
A temporary LLM provider error triggered a retry. | |
text_chunk_received |
Part of the reply arrived. | |
| Tool | tool_call_declined |
A tool call proposed by the model was declined. |
tool_call_repaired |
A tool call or value the model created was invalid and was corrected. | |
tool_call_started |
A tool invocation began, carrying its registered name, call ID, and raw input. | |
tool_call_finished |
A tool invocation finished. | |
tool_call_failed |
A tool invocation failed but the task continues. | |
| Knowledge | knowledge_written |
A page was written. |
knowledge_read |
A page was read. | |
knowledge_removed |
A page was removed. | |
knowledge_listed |
The pages were listed. | |
knowledge_failed |
An action against the store did not go through. | |
| Compaction | compaction_started |
Compaction is about to rewrite the older messages. |
compaction_progress |
Compaction finished part of the work. | |
compaction_finished |
Compaction replaced the older messages. | |
compaction_failed |
Compaction could not finish. | |
| Custom | name chosen by your application | An event published with emit_event. |
Publish events
Publish custom events through the Werk. Add agent or task context when relevant:
from agentwerk import Event
werk.emit_event(
Event("document_indexed")
.data({"documents": 42})
.task_id("t-1")
.agent_id("indexer-1")
)
werk.emit_event(Event("index_refreshed"))
Werk.emit_event() does not change task status. Use EventTool for model-driven completion through task_finished.
Read events
Events are saved to .agentwerk/events.jsonl, except text_chunk_received.
| Method | Description |
|---|---|
emit_event(event) |
Publish an event for querying and observation. |
find_event(query) |
Get the first event selected directly or through a matching task. |
find_events(query) |
Get events selected directly or through matching tasks, in query order. |
get_input_tokens() |
Get input tokens across the run's requests. |
get_output_tokens() |
Get output tokens across the run's requests. |
get_duration() |
Get the elapsed execution duration. |
| Event method | Description |
|---|---|
get_name() |
Read the event name. |
get_data() |
Read the event payload. |
get_task_id() |
Read the associated task ID. |
get_agent_id() |
Read the associated agent ID. |
get_label() |
Read the associated task's label. |
get_created_at() |
Read the timestamp in epoch milliseconds. |
directive(value) |
Set directive metadata. This does not send an instruction to the model. |
get_directive() |
Read the directive metadata. |
The default logger runs when no event hook is installed.
Query events
Query events with AQL or a predicate. See Queries for shared syntax.
werk.find_events("event.name = tool_call_failed")
werk.find_events("event.name = request_finished AND event.agent_id = research-1")
werk.find_events("event.task_id = t-3 ORDER BY event.created DESC")
werk.find_events("event.data ~ timeout AND event.created > -1h")
| Field | Description | |
|---|---|---|
| Match | event.name |
The event name, such as run_started or tool_call_failed. |
event.agent_id |
The attributed agent ID, when the event has agent context. | |
event.task_id |
The attributed task ID, when the event has task context. | |
event.label |
The attributed task's label, when the task is known and labelled. | |
| Search | event.data |
Search only the serialized raw event data as text. |
| Compare | event.created |
When the event was recorded. |
Use IS EMPTY to find events without agent, task, or label context. event.data searches only the payload, not the name. Events default to oldest-first order.
Hooks
| Method | Description | |
|---|---|---|
| Observe | on_event(handler) |
Read every event as it is emitted. |
on_event_async(handler) |
Read every event in an async hook. | |
on_result(handler) |
Read every finished task together with its result. | |
on_result_async(handler) |
Read every finished task and result in an async hook. | |
on_failure(handler) |
Read every failure together with its task. | |
on_failure_async(handler) |
Read every failure and task in an async hook. | |
on_task(handler) |
Read task state changes. | |
on_task_async(handler) |
Read task state changes in an async hook. |
on_result runs synchronously on the agent. Keep it brief. Use on_result_async for work that needs to await.
Async hooks run while a completion method is waiting, and finish before it returns. start() alone does not run them. Do not call finish, finish_task, or finish_tasks inside an async hook: it can deadlock. Python hooks use async def and run on the caller's event loop.
Knowledge
Knowledge provides durable memory that agents share across tasks and with other agents.
from agentwerk import Agent, Knowledge
store = Knowledge.load("./notes")
alice = Agent().knowledge(store)
bob = Agent().knowledge(store)
Calling .knowledge(store) registers a KnowledgeTool bound to that store, so the agent can read and update its shared pages.
Knowledge reference
Pages use the Open Knowledge Format (OKF) and are stored at ./notes/pages/<slug>.md. Each has an entry in ./notes/index.md, which is included in the prompts of agents sharing the store.
| Method | Description |
|---|---|
get_index() |
Get the index, which is injected into the agent prompt. |
set_index_char_limit(count) |
Limit how much of the index is injected into the prompt. |
get_index_char_limit() |
Get the index size limit in force. |
get_pages() |
Get the page collection for reading and writing pages. |
get_pages().get_all() |
Get every page in the store. |
clear() |
Remove every page from the store. |
By default, prompts include up to 12,000 characters of the index. Agents can read the rest from index.md. Pages are always saved in full.
Create entries in code:
from agentwerk import Page
store.get_pages().save(
Page(
"build-command",
"How the project is built.",
"Run `make` to compile.",
tags=["build"],
)
)
page = store.get_pages().get_page("build-command")
store.get_pages().remove("build-command")
See Knowledge.
Use Cases
Example projects built with agentwerk:
- Hello World: basic example, also available as a Python example
- Terminal REPL: minimal multi-turn terminal chat
- Editorial Review: route a draft through an editor with a result hook and AQL, also available as a Python example
- Deep Research: research across several sources (requires
BRAVE_API_KEY) - Malware Scanner: find signs of malware in a software package
- Apparat Fabrik: simulate agents inspecting and assembling factory parts
Configure an LLM provider first (see Environment).
python examples/editorial_review.py "Draft a short release announcement."
Security
Report a vulnerability to security@canvascomputing.org, not in a public issue. See SECURITY.md.
Development
See DEVELOPMENT.md for build, test, and release instructions.
Metadata
Release files for agentwerk 0.1.32
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agentwerk-0.1.32.tar.gz | 482.0 kB | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agentwerk-0.1.32-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.8 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| agentwerk-0.1.32-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.8 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| agentwerk-0.1.32-cp38-abi3-macosx_11_0_arm64.whl | CPython 3.8 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 15.1 MB
Release files / agentwerk-0.1.32.tar.gz
| Download URL | agentwerk-0.1.32.tar.gz |
|---|---|
| Size | 482.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9091ee88f1b5fdc783040d64c937035a5a7a37f3bc8002c96a9296868c164529
|
|
BLAKE2b-256 checksum How to use checksums |
25bafc67e1276dccace02aaf73764314092cf28412875fc61d6922213b4ddd26
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency logRelease files / agentwerk-0.1.32-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | agentwerk-0.1.32-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 4.9 MB |
| Tags | CPython 3.8 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
4d3b5640a871578cba3d8fbf2f5fb8160a7d7db369959eb6aaef8b24920619a8
|
|
BLAKE2b-256 checksum How to use checksums |
0c53c24050fd8e6150df7261cdab23d085e9c4d8393128837fc80dafed36e5a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency logRelease files / agentwerk-0.1.32-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | agentwerk-0.1.32-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 5.1 MB |
| Tags | CPython 3.8 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
7a7fc40f34f0227c05eaa73c56ef75f874c3f8c2bb6a4a63b5bb686fd739dcaf
|
|
BLAKE2b-256 checksum How to use checksums |
84dcd7bf716ed2d1913e295dccafb6f4d7975579da214240184705078f7acd71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency logRelease files / agentwerk-0.1.32-cp38-abi3-macosx_11_0_arm64.whl
| Download URL | agentwerk-0.1.32-cp38-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 4.5 MB |
| Tags | CPython 3.8 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
445a479e067af99d40b8d8f738e6eefb4aee1a96f8c6964e0bdb514c9460086a
|
|
BLAKE2b-256 checksum How to use checksums |
8b23121d5a5e1c7c282467a72cb8c7cf143c5a46533aa7caa9ba6d5329e5ef91
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency log