Skip to main content

agentwerk (Python)

A minimal agentic loop for building efficient harnesses.
Coordinate agent fleets across complex tasks, with shared knowledge and detailed observability.

Beta: The API might introduce breaking changes before 0.2.0.

“Werk” is German for both a factory and a work of art.

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")
)

See Provider and Model.

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:

  1. Agent handover API: Agent.handover creates a configured child task when the agent finishes.
  2. Result hook: on_result creates follow-up tasks from completed work.
  3. Knowledge: the knowledge tool shares durable pages between agents.
  4. Task tool: the task tool reads any finished task's result by ID.
  5. Read result file: the read_file tool opens a task's result.json in 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:

  1. A task ID such as t-3 means task = t-3.
  2. A built-in event name such as tool_call_failed means event = tool_call_failed.
  3. Any other value such as scan means label = 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.

See Event and Werk.

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:

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)

Source distribution for agentwerk 0.1.29
File Size Uploaded
agentwerk-0.1.29.tar.gz 453.8 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for agentwerk 0.1.29
File Interpreter ABI Platform
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 log

Release 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 log

Release 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 log

Release 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

Release history Release notifications | RSS feed

0.1.33

4 release files

0.1.32

4 release files

0.1.31

4 release files

This release

0.1.29 This release

4 release files

0.1.27

4 release files

0.1.26

4 release files

0.1.25

4 release files

0.1.24

4 release files

0.1.21

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page