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("You are a Rust developer who explores source files to answer questions.")
.tool(ReadFileTool())
.tool(GrepTool())
)
task = agent.add_task(
"Find every `pub trait` defined under src/ and explain each in one sentence."
)
result = await agent.start().finish_task(task)
print(result)
asyncio.run(main())
API
- Agents: Set agent roles, behavior, and tasks.
- Werk: Assign work and collect results across agents.
- Tools: Give agents controlled ways to act.
- Events: Inspect requests, tool calls, and failures.
- Knowledge: Share durable memory across agents and tasks.
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.")
agent.start()
The prompt skill provides a compact template for writing agent roles.
All agent methods
| Method | Description | |
|---|---|---|
| Configure | role(role) |
Define who the agent is and how it should work. |
tool(tool) / tools(tools) |
Register a tool the agent may call. | |
label(label) |
Restrict the agent to tasks carrying this label. | |
handover(task) |
Set the task this agent creates when it finishes. | |
dir(dir) |
Set the directory the agent has access to. | |
template(key, value) |
Inject data into prompts with template strings. | |
templates(variables) |
Inject more than one entry into prompts. | |
directive(key, template) |
Set a directive template by key. | |
directives(overrides) |
Set more than one directive template. | |
knowledge(store) |
Share a knowledge store with the agent. | |
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() |
Begin processing tasks. | |
get_id() |
Get the unique identifier of an agent. |
You can use the {context} variable to inject contextual information:
- 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
Every value is a variable of its own: {task_id}, {date}, {dir}, {platform}, {os_version}, {turns_remaining}, {input_tokens_remaining}, {output_tokens_remaining}, and {time_remaining}.
Interactive
An interactive agent holds one task open across many turns, so a conversation spans a whole session.
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_all_tasks()
werk.add_reply(id, "And which environment variables override it?")
await werk.finish_all_tasks()
werk.set_task_finished(id, "answered")
An interactive agent never finishes its own task, because that would end the conversation. Every answer pauses the task instead: it stays in_progress with its agent, and each await werk.finish_all_tasks() returns on the answer it waited for. add_reply(id, content) supplies the next message, and set_task_finished(id, result) ends the conversation with the result reported to the hook. The answers in between arrive as events.
See more: Agent.
Providers
An LLM provider connects agents to Anthropic, OpenAI, Mistral, or a LiteLLM proxy.
from agentwerk import Agent, Anthropic
agent = (
Agent()
.provider(Anthropic(key))
.model("claude-sonnet-4-20250514")
)
Set a tool's timeout with timeout(seconds); zero disables it. Defaults are no
timeout for custom tools, 60 seconds for FetchTool(), 180 seconds for
GrepTool(), and the call's timeout_ms, falling back to 120 seconds, for
CommandTool(). When a Python tool times out, the agent stops waiting, but its
worker thread may continue in the background.
quick_grep = GrepTool().timeout(15)
patient_fetch = FetchTool().timeout(0)
All provider and model settings
| 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. |
await provider.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")
)
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"))
All Werk methods
| 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. | |
| 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 handler. | |
on_result(handler) |
Read every finished task together with its result. | |
on_result_async(handler) |
Read every finished task and result in an async handler. | |
on_failure(handler) |
Read every failure together with its task. | |
on_failure_async(handler) |
Read every failure and task in an async handler. | |
on_task(handler) |
Read task state changes. | |
on_task_async(handler) |
Read task state changes in an async handler. | |
| Run | start() |
Begin processing tasks. |
finish_task(query) |
Wait for matching tasks and get the first result in query order. | |
finish_tasks(query) |
Wait for matching tasks and get their results. | |
finish_all_tasks() |
Wait for every task and get every result. | |
| Cancel | cancel_tasks(query) |
Stop work on matching tasks. |
cancel_all_tasks() |
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
Use queries to choose tasks by label, ID, status, or time.
werk.find_tasks("scan") # Tasks labeled scan.
werk.find_results("t-3") # The result of task t-3.
werk.find_results("scan ORDER BY finished DESC") # Scan results, newest first.
All query terms
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. |
| Shorthand | scan, t-3 |
Short for label = scan and id = t-3. |
| Sort | ORDER BY field DESC |
Sort matches; ASC is the default. |
Fields
| Kind | Fields | Meaning |
|---|---|---|
| Identity | id, label, status |
Task identity and current state. |
| Run state | pending, cancelled |
Whether this run may schedule the task. |
| Relationship | agent, parent |
Claiming agent and handover parent. |
| Text | task, result, errors |
Task body, result, and recorded failures. |
| Time | created, started, finished, failed |
Creation, start, finish, and failure times. |
Rules
Write the field, followed by what it must match:
- Exact value:
label = scan - Contains text:
result ~ timeout - Time range:
failed > -1h - Has no value:
agent IS EMPTY
Missing values do not match !=. For example, label != scan leaves out tasks with no label. To include them, use label IS EMPTY OR label != scan.
Quote values containing spaces: label = "needs review". Put lists in parentheses: label IN (scan, report).
Use parentheses when mixing AND and OR to make the order clear. NOT applies to the condition or parenthesized group after it. Query words such as AND ignore case; labels and IDs do not.
Relative times such as -30m, -2h, -7d, and -1w are measured when the query runs.
ORDER BY field sorts from lowest to highest. Add DESC for the reverse. Tasks with no value for that field come last. Without ORDER BY, tasks remain in creation order.
Examples
werk.find_results("report AND result ~ risk") # reports that mention risk
werk.find_tasks("errors ~ tool_call_failed") # saw a tool call fail
werk.find_tasks("status = todo AND agent IS EMPTY") # waiting, never claimed
werk.find_tasks("failed > -1h ORDER BY failed DESC") # the last hour's failures
werk.find_tasks(lambda t: len(t.get_replies()) > 4) # a callable, for what no field carries
Execution
The Werk schedules the work of your agents and returns their results.
task = werk.add_task("Write a report.")
answer = await werk.finish_task(task)
if answer is not None:
print(answer)
All execution methods
| Method | Description | |
|---|---|---|
| Run | start() |
Begin processing tasks. |
await finish_task(query) |
Wait for matching tasks and get the first result in query order. | |
await finish_tasks(query) |
Wait for matching tasks and get their results. | |
await finish_all_tasks() |
Wait for every task and get every result. | |
| Cancel | cancel_tasks(query) |
Stop work on matching tasks. |
cancel_all_tasks() |
Stop work on every task. |
Cancellation applies only to the current execution: it does not change status or remain attached to the task. start() clears cancellation so unfinished tasks can resume. Use task.is_cancelled() for a task you hold, or cancelled = true and pending = true to select by execution state.
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_parent() |
Get the parent task ID after a handover. | |
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.
Handover
Agents can pass work and results in five ways:
- Agent handover API:
Agent.handovercreates a configured child task when the agent finishes. - Result hook:
on_resultcreates follow-up tasks from completed work. - Knowledge: the
knowledgetool shares durable pages between agents. - Task tool: the
tasktool reads any finished task's result by ID. - Read result file: the
read_filetool opens a task'sresult.jsonin the session directory.
All ways agents pass data
1. Agent handover API
Set the follow-up task on the first agent. The model can then finish with only its result:
analyst = (
Agent.from_env()
.label("analysis")
.role("You are a product analyst.")
.handover(Task("Write the board report from {parent_result}.", label="report"))
)
writer = (
Agent.from_env()
.label("report")
.role("You write concise board reports.")
)
werk = Werk()
werk.add_agent(analyst).add_agent(writer)
werk.add_task(Task("Rank all products by value.", label="analysis"))
Finishing the analysis creates the report task and links it to its parent. The task may use {parent_id}, {parent_result}, and {parent_result_path}. Calling handover again replaces it. Bound object results keep this handover host-owned; the legacy envelope for scalar or unbound results may override its task or schema, but not its label.
2. 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)
3. Knowledge
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.")
4. Task tool
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.")
5. Read result file
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.
Schemas
A Schema defines the required shape of a task result. agentwerk fixes simple formatting errors such as quoted numbers. For other violations, it asks the model to retry up to max_schema_retries.
from agentwerk import Schema, Task
schema = Schema(
{
"type": "object",
"properties": {"title": {"type": "string"}},
"required": ["title"],
}
)
werk.add_task(Task("Write a report.", schema=schema))
All schema methods
For small models, use shallow, focused schemas with few required fields, clear names, and short lists of allowed values. Split large results into labeled tasks with separate schemas, then combine them in a later task. Deep nesting, long property lists, and large anyOf or oneOf branches use more context and trigger retries.
| Method | Description | |
|---|---|---|
| Schema | Schema(document) |
Create a schema. |
validate(value) |
Validate content. | |
A schema for a handover child belongs on its configured Task. This lets a small model create the child with a result-only finish call: |
report_schema = Schema(
{
"type": "object",
"properties": {"title": {"type": "string"}},
"required": ["title"],
}
)
analyst = Agent.from_env().handover(
Task("Write the report from {parent_result}", label="report", schema=report_schema)
)
See Schema.
Configuration
A Policy sets limits for turns, tokens, elapsed time, retries, and compaction.
werk.set_policy(Policy(max_turns=40, max_time=300.0))
All configuration fields
| 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 the consecutive turns without a valid tool call. |
max_request_retries |
Limit how often a failing request is retried. |
request_retry_delay |
Wait this long between 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 a task's older messages once they no longer fit the model's context window.
werk.set_policy(Policy(compaction_threshold=0.7))
When compaction runs and what it reports
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.
Directives
A directive tells the model how to recover: call a tool, match a schema, correct a path, narrow a search, or choose an allowed command. You can replace the built-in 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}.",
}
)
)
Use a built-in directive key such as grep_failed to replace recovery text. To replace the message returned after EventTool publishes an event, use that event's name as the key. Built-ins without a replacement keep their default wording. Templates may use runtime values such as {detail}, {attempt}, and {path}; placeholders without a value remain unchanged. Recovery text should state what failed and what the model should do next.
See prompts/directives for the built-in text.
Sessions
A Werk saves every task, reply, and event so you can continue the same session later.
The working directory is ./.agentwerk by default.
werk = Werk.load(".agentwerk")
werk.add_agent(my_agent)
werk.start()
All session files
.agentwerk/
├── events.jsonl every event (one per line)
├── tasks/
│ └── t-1/
│ ├── task.json the task without its messages (id, status, label, timestamps)
│ ├── result.json the result the agent produced
│ ├── replies.jsonl every message exchanged with the model, one per line
│ └── outputs/<tool_use_id>.txt full tool outputs spilled out of the messages
└── 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 *"))
)
All built-in and custom tools
| 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(name) |
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(store) |
Write, read, remove, or list pages in a knowledge store. |
FinishTool and KnowledgeTool
FinishTool() and KnowledgeTool(store) are registered automatically on every agent. Agents use them to finish queued tasks or work with shared knowledge. An interactive agent gets no FinishTool() by default, since finishing its task would end the conversation. FinishTool() is the compatibility wrapper around EventTool()'s task_finished event; EventTool() remains opt-in.
When a task carries an object schema, its displayed fields are the finish call: pass them directly. Its configured handover runs automatically. Scalar and unbound tasks retain the explicit result envelope, which also supports an inline handover:
{
"result": "...",
"handover": {
"label": "...",
"task": "..."
}
}
EventTool
Give an agent EventTool() to let it publish application events:
from agentwerk import EventTool
agent = Agent().tool(EventTool())
The model supplies a name and optional JSON data:
{
"name": "...",
"data": {}
}
Agentwerk records the current task and agent on every event. Werk handlers can react as events arrive, and queries can retrieve them later:
werk.on_event(lambda _, event: print(event.get_name()))
events = werk.find_events("event = event_name")
Event names are open-ended; lowercase snake case is conventional. Publishing an event does not change the task's status. The exception is the built-in task_finished event, which has the same result and handover behavior as FinishTool():
{
"name": "task_finished",
"data": { "result": "..." }
}
After publishing a non-terminal event, EventTool normally returns Event <name> published to the model. A directive with the event's name replaces this message. Its template can reference {data} for the complete JSON payload or any top-level field by name. The published event and tool result both record the directive key.
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 record what agents, tools, and LLM providers do during execution.
def log(werk, event):
if event.get_name() == Event.TASK_FINISHED:
print(event.get_data().get("result"))
werk.on_event(log)
All event names and readers
| Kind | 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. | |
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. | |
| Application | 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"))
The first event's name, data, and context are stored as:
{"name":"document_indexed","data":{"documents":42},"task_id":"t-1","agent_id":"indexer-1"}
emit_event adds the timestamp and a known task's label. Lowercase snake case is conventional, but other names are accepted; quote names containing spaces or punctuation in AQL. Built-in events have named constructors, such as Event.task_finished() and Event.request_started("model"); their names are also available as constants. Publishing one runs its hooks and updates its statistics. It is saved according to the event's normal rules, but it does not change task or execution state.
Werk.emit_event() never changes a task's status. To let a model finish its current task through an event, register EventTool() on its agent. When the model emits task_finished, the tool validates and stores data.result, optionally creates a handover task, and marks the current task finished. All other names only publish an event.
Events are saved to .agentwerk/events.jsonl. text_chunk_received events are not saved. Read events through the Werk with these methods:
| Method | Description |
|---|---|
emit_event(event) |
Publish an event for querying and observation. |
find_event(query) |
Get the first matching event in query order. Without ORDER BY, this is the earliest one. |
find_events(query) |
Get every matching event in query order. Without ORDER BY, this is oldest first. |
get_input_tokens() / get_output_tokens() |
Get token counts across the run's requests. |
get_duration() |
Get the elapsed execution duration. |
An event handler usually checks get_name() and then reads its payload with get_data(). Use get_task_id(), get_agent_id(), and get_label() to trace where it came from, and get_created_at() to read its timestamp. Application events can attach a model-facing instruction with directive(value) and read it back with get_directive().
All event query fields and rules
You can query events with AQL or a condition you supply.
werk.find_events("tool_call_failed")
werk.find_events("event = request_finished AND agent = research-1")
werk.find_events("task = t-3 ORDER BY created DESC")
werk.find_events("payload ~ timeout AND created > -1h")
| Field | Description | |
|---|---|---|
| Match | event |
The event name, such as run_started or tool_call_failed. |
agent |
The attributed agent ID, when the event has agent context. | |
task |
The attributed task ID, when the event has task context. | |
label |
The attributed task's label, when the task is known and labelled. | |
| Search | payload |
Search the event name and data together as text. |
| Compare | created |
When the event was recorded. |
Event queries follow the same rules as task queries. Match event, agent, task, and label exactly; use ~ to search payload; and use < or > with created. Some events have no agent, task, or label. Find them with IS EMPTY. Without ORDER BY, events remain oldest first.
You can also search with one word. Agentwerk reads it as:
- A task ID such as
t-3meanstask = t-3. - A built-in event name such as
tool_call_failedmeansevent = tool_call_failed. - Any other value such as
scanmeanslabel = scan.
For your own event names, write event = document_indexed. If a label has the same name as a built-in event, write label = knowledge_read.
An invalid query string raises ValueError. Query(query) checks a query without running it and raises the same error.
Hooks
A hook runs your function when an event, result, failure, or task state change occurs.
def triage(werk, event, failed):
if failed.get_label() == "scan":
werk.add_task(Task(failed.get_task(), label="triage"))
werk.on_failure(triage)
All hooks
| Method | Description | |
|---|---|---|
| Observe | on_event(handler) |
Read every event as it is emitted. |
on_event_async(handler) |
Read every event in an async handler. | |
on_result(handler) |
Read every finished task together with its result. | |
on_result_async(handler) |
Read every finished task with its result, in an async handler. | |
on_failure(handler) |
Read every failure together with the task it happened in. | |
on_failure_async(handler) |
Read every failure with its task, in an async handler. | |
on_task(handler) |
Read a task as it starts, finishes, or fails. | |
on_task_async(handler) |
Read a task state change in an async handler. |
Save replies of every finished task as a training example:
def capture(werk, event, task):
if event.get_name() == Event.TASK_FINISHED:
model = werk.get_model_for_agent(event.get_agent_id())
Trajectory.from_task(event.get_agent_id(), model, task).save("datasets")
werk.on_task(capture)
Async handlers
on_result pauses the agent until the hook finishes. Use on_result_async for slower work such as storing results in a database, posting them to an HTTP API, or uploading them to object storage. It takes an async def that runs on the same event loop waiting for results.
async def store(werk, task, result):
await database.insert(task.get_id(), result)
werk.on_result_async(store)
See Werk.
Knowledge
Knowledge provides durable memory that agents share across tasks and with other agents.
Pages use the Open Knowledge Format (OKF).
from agentwerk import Agent, Knowledge
store = Knowledge.load("./notes")
alice = Agent().knowledge(store)
bob = Agent().knowledge(store)
Knowledge details
Each page is written to ./notes/pages/<slug>.md, and every page gets one line in ./notes/index.md. That list is injected into the prompt of every agent sharing the store, so each of them knows which pages it can read.
| 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. |
The prompt includes up to 12 000 characters of the knowledge index by default. If the index exceeds the configured limit, the agent reads the remainder 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: interactive terminal chat
- Divide and Conquer: split an arithmetic problem across agents, 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/divide_and_conquer.py 200 4 2
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.29
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.29.tar.gz | 453.8 kB | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agentwerk-0.1.29-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.29-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.8 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| agentwerk-0.1.29-cp38-abi3-macosx_11_0_arm64.whl | CPython 3.8 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 14.8 MB
Release files / agentwerk-0.1.29.tar.gz
| Download URL | agentwerk-0.1.29.tar.gz |
|---|---|
| Size | 453.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bd19667fabacbd29874bb57c11de0ad24884c17db746eab6d54bec0750570408
|
|
BLAKE2b-256 checksum How to use checksums |
052058a8159618995c27e383904adb7f581c47e0d9db250b4c9144a3792bfdbe
|
| 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 2, 2026.
Transparency logRelease files / agentwerk-0.1.29-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | agentwerk-0.1.29-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 4.8 MB |
| Tags | CPython 3.8 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
824950aed825ae3e841d58f75ce0c3bfb4bb04a2e878a4f6478efacbf27f27b3
|
|
BLAKE2b-256 checksum How to use checksums |
19cc486211cccb5341f6ec780a346a4e0e8ea76ff9067297d41b45a7202d1d72
|
| 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 2, 2026.
Transparency logRelease files / agentwerk-0.1.29-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | agentwerk-0.1.29-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 5.0 MB |
| Tags | CPython 3.8 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
141b0518e22263a91cb5dfcc63ef98c76dd8c20bb18d68b3a49306ae81884f8a
|
|
BLAKE2b-256 checksum How to use checksums |
fef5209337a84796876332524d194a1e77be6088a18c77bbea8afa38654b0166
|
| 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 2, 2026.
Transparency logRelease files / agentwerk-0.1.29-cp38-abi3-macosx_11_0_arm64.whl
| Download URL | agentwerk-0.1.29-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 |
d7984c4a36d1a854c694f50a2c86ece4fa86d7e335e4e0583193b9384f3a2dbf
|
|
BLAKE2b-256 checksum How to use checksums |
1bd29b6bab5e658a3e9a9ca48a4543b0bbc1fb9e8d2fa9e123188994d292a5e9
|
| 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 2, 2026.
Transparency log