Tools for developing and optimizing side effect free background agents
Project description
Weak Incentives (Is All You Need)
Weak Incentives (WINK) is a Python library for building "background agents" (automated AI systems). It provides lean, typed, and composable building blocks that keep determinism, testability, and safe execution front and center without relying on heavy dependencies or hosted services.
The core philosophy treats agent development as a structured engineering discipline rather than an exercise in ad-hoc scripting. The library ships a set of focused abstractions that reinforce that rigor:
- Prompts as code. Prompts are typed
Promptobjects composed from sections such asMarkdownSection, versioned deterministically, and scoped with chapters so long-form directives can stay dormant until policies open them. - Structured I/O. Declaring a dataclass output type for a
Prompt[OutputT]automatically builds a JSON schema, instructs the provider to obey it, and parses the reply back into a typed Python object. - Stateful, replayable sessions.
Sessionacts as a Redux-like state store, letting pure reducers respond to events (for example,ToolInvoked) so every change is observable, replayable, and snapshot-friendly. - Typed and sandboxed tools. Tools are typed callables (
Tool[ParamsT, ResultT]) with explicit contracts for inputs and outputs, plus built-in suites for planning, a secure in-memory VFS, and sandboxed Python evaluation. - Provider-agnostic adapters. Adapters connect the framework to providers like OpenAI or LiteLLM by handling API calls, tool negotiation, and response parsing while keeping the agent logic model-agnostic.
- Configuration and optimization hooks. Structured logging via
structlog, enforced deadlines, and a powerful prompt overrides system let teams A/B test and iterate on prompts through JSON files without touching the application source.
What's novel?
While other agent frameworks provide a toolbox of loose components, WINK offers an opinionated chassis that emphasizes determinism, type contracts, and observable workflows:
- Redux-like state management with reducers. Every state change is a traceable consequence of a published event processed by a pure reducer. This thread of causality delivers replayability and visibility far beyond free-form dictionaries or mutable object properties.
- Composable prompt blueprints with typed contracts. Prompts are built from reusable sections and chapters backed by dataclasses, so composition and parameter binding feel like standard software engineering instead of string concatenation.
- Integrated, hash-based prompt overrides.
PromptDescriptorcontent hashes, tool contracts, and chapter descriptors ensure overrides only apply to the intended section version while describing the declared chapter layout.LocalPromptOverridesStorekeeps the JSON artifacts in version control so teams can collaborate without risking stale edits. - First-class in-memory virtual filesystem. The sandboxed VFS ships as a core tool, giving agents a secure workspace whose state is tracked like any other session slice and avoiding accidental host access.
- Lean dependency surface. Avoiding heavyweight stacks such as Pydantic keeps the core lightweight. Custom serde modules provide the needed functionality without saddling users with sprawling dependency trees.
In short, WINK favors software-engineering discipline—determinism, type safety, testability, and clear state management—over maximizing the number of exposed knobs.
The specs below dive into each area when you need exact contracts and deeper context:
- Observable session state with reducer hooks. A Redux-like session ledger and in-process event bus keep every tool call and prompt render replayable. Built-in planning, virtual filesystem, and Python-evaluation sections ship with reducers that enforce domain rules while emitting structured telemetry. See Session State, Prompt Event Emission, Planning Tools, Virtual Filesystem Tools, and Asteval Integration.
- Composable prompt blueprints with strict contracts. Dataclass-backed sections compose into reusable blueprints that render validated Markdown and expose tool contracts automatically. Specs: Prompt Overview, Prompt Composition, and Structured Output.
- Chapter-driven visibility controls. Chapters gate when prompt regions enter the model context, defaulting to closed until runtime policies open them. Expansion strategies and lifecycle guidance live in Chapters Specification.
- Override-friendly workflows that scale into optimization. Prompt definitions ship with hash-based descriptors and on-disk overrides that stay in sync through schema validation and Git-root discovery, laying the groundwork for iterative optimization. Review Prompt Overrides for the full contract.
- Provider adapters standardize tool negotiation. Shared conversation loops negotiate tool calls, apply JSON-schema response formats, and normalize structured payloads so the runtime stays model-agnostic. See Adapter Specification and provider-specific docs such as LiteLLM Adapter.
- Local-first, deterministic execution. Everything runs locally without hosted dependencies, and prompt renders stay diff-friendly so version control captures intent instead of churn. The code-review example ties it together with override-aware prompts, session telemetry, and replayable tooling.
Requirements
- Python 3.12+ (the repository pins 3.12 in
.python-versionfor development) uvCLI
Install
uv add weakincentives
# optional tool extras
uv add "weakincentives[asteval]"
# optional provider adapters
uv add "weakincentives[openai]"
uv add "weakincentives[litellm]"
# cloning the repo? use: uv sync --extra asteval --extra openai --extra litellm
Tutorial: An Interactive Code Review Assistant
Let's build a simple, interactive code review assistant. This agent will be able to browse a codebase, answer questions about it, and create plans for more complex reviews. We'll see how WINK helps build this in a structured, observable, and safe way.
The full source for this example is in
code_reviewer_example.py,
and a high-level walkthrough of the architecture lives in
specs/code_reviewer_example.md.
1. Define the Agent's Task with Typed Dataclasses
Instead of dealing with messy string outputs from the LLM, we'll define our expected output using a Python dataclass. The library ensures the model's response is parsed into this structure.
from dataclasses import dataclass
@dataclass(slots=True, frozen=True)
class ReviewResponse:
"""The structured response we expect from our agent."""
summary: str
issues: list[str]
next_steps: list[str]
This ReviewResponse class is our contract with the agent. We're telling it exactly what we need: a summary, a list of issues, and next steps.
2. Compose a "Blueprint" for the Agent's Brain
In WINK, prompts are not just f-strings; they are composable, versioned objects. We build a Prompt from Sections, which are like building blocks for the agent's reasoning process.
Here, we create a main prompt that includes:
guidance_section: General instructions for the agent.planning_section: Gives the agent the ability to create and manage plans.workspace_section: Provides tools for interacting with a virtual filesystem.user_turn_section: A placeholder for the user's interactive request.
from weakincentives import MarkdownSection, Prompt
from weakincentives.tools.planning import PlanningToolsSection
from weakincentives.tools.vfs import VfsToolsSection
# Sections are reusable components for building prompts.
guidance_section = MarkdownSection(...)
planning_section = PlanningToolsSection(session=session)
workspace_section = VfsToolsSection(session=session, mounts=...)
user_turn_section = MarkdownSection[ReviewTurnParams](...) # Takes user input
# The Prompt object is the blueprint for the agent.
review_prompt = Prompt[ReviewResponse](
ns="examples/code-review",
key="code-review-session",
name="sunfish_code_review_agent",
sections=(
guidance_section,
planning_section,
workspace_section,
user_turn_section,
),
)
This "prompt as code" approach makes our agent's logic modular, reusable, and easier to test.
3. Provide a Safe Workspace with a Virtual Filesystem
To let the agent review code, we need to give it access to the files. But we don't want it to have unrestricted access to the host machine. The VfsToolsSection provides a sandboxed in-memory filesystem. We can mount a real directory into this virtual workspace.
from weakincentives.tools.vfs import HostMount, VfsPath, VfsToolsSection
# Mount the 'sunfish' test repository into the agent's virtual workspace.
# The agent will see it at the path 'sunfish/'.
mounts = (
HostMount(
host_path="sunfish",
mount_path=VfsPath(("sunfish",)),
),
)
vfs_section = VfsToolsSection(
session=session,
mounts=mounts,
allowed_host_roots=(TEST_REPOSITORIES_ROOT,), # Limit host access
)
Now the agent can use tools like vfs_list_files and vfs_read_file to explore the code inside its sandbox, without any risk to the host system.
4. Run the Agent and Get a Structured Result
With the prompt defined, we need a Session to track state and an Adapter to communicate with an LLM provider (like OpenAI).
The Session is a central state store. All events, like tool calls and prompt evaluations, are recorded in the session. This makes the agent's execution fully observable and replayable.
from weakincentives.runtime.session import Session
from weakincentives.runtime.events import InProcessEventBus
from weakincentives.adapters.openai import OpenAIAdapter
# The event bus allows us to listen to events from the session.
bus = InProcessEventBus()
# The session tracks all state changes.
session = Session(bus=bus)
# The adapter connects to the LLM provider.
adapter = OpenAIAdapter(model="gpt-4o-mini")
# This is the main evaluation loop.
response = adapter.evaluate(
review_prompt,
ReviewTurnParams(request="Are there any obvious bugs in sunfish.py?"),
bus=bus,
session=session,
)
# The output is a typed dataclass object, not a raw string.
review: ReviewResponse = response.output
print(review.summary)
If the model's output doesn't match our ReviewResponse dataclass, the adapter will raise an error, preventing corrupted data from flowing through the system.
5. Observe the Agent's Thought Process
Because every action is tracked in the Session, we can inspect the agent's state at any time. For example, we can retrieve the final plan the agent came up with.
from weakincentives.runtime.session import select_latest
from weakincentives.tools.planning import Plan
# Select the latest plan from the session state.
latest_plan = select_latest(session, Plan)
if latest_plan:
print(f"Plan objective: {latest_plan.objective}")
for step in latest_plan.steps:
print(f"- [{step.status}] {step.title}")
This observability is crucial for debugging and understanding the agent's behavior. You can see exactly what tools it ran, what files it read, and what conclusions it drew at each step.
6. Evolve Prompts without Changing Code
What if you want to tweak the agent's instructions? Instead of editing the Python code, you can use Prompt Overrides. WINK can load modified prompt sections from external JSON files.
This allows you to iterate on prompts, A/B test different instructions, and tune the agent's behavior without redeploying your application.
# When rendering, specify a tag to look for overrides.
rendered = review_prompt.render(
...,
overrides_store=LocalPromptOverridesStore(),
tag="assertive-feedback",
)
The LocalPromptOverridesStore will look for a JSON file in .weakincentives/prompts/overrides/ that matches the prompt's namespace, key, and the "assertive-feedback" tag. This makes prompt engineering a data-driven process, separate from application logic.
You've built a reviewer!
That's it. You now have a deterministic, observable, and safe code review assistant that:
- Returns structured, typed data.
- Interacts with files in a sandboxed environment.
- Creates and follows plans to solve complex tasks.
- Whose every action is recorded and can be inspected.
- Can be easily tweaked and improved via external configuration.
This approach turns agent development from a scripting exercise into a structured engineering discipline.
Logging
WINK ships a structured logging adapter so hosts can add contextual
metadata to every record without manual dictionary plumbing. Call
configure_logging() during startup to install the default handler and then
bind logger instances wherever you need telemetry:
from weakincentives.runtime.logging import configure_logging, get_logger
configure_logging(json_mode=True)
logger = get_logger("demo").bind(component="cli")
logger.info("boot", event="demo.start", context={"attempt": 1})
The helper respects any existing root handlers—omit force=True if your
application already configures logging and you only want WINK to
honor the selected level. When you do want to take over the pipeline, call
configure_logging(..., force=True) and then customize the root handler list
with additional sinks (for example, forwarding records to Cloud Logging or a
structured log shipper). Each emitted record contains an event field plus a
context mapping, so downstream processors can make routing decisions without
parsing raw message strings.
Development Setup
-
Install Python 3.12 (for example with
pyenv install 3.12.0). -
Install
uv, then bootstrap the environment and hooks:uv sync ./install-hooks.sh -
Run checks with
uv runso everything shares the managed virtualenv:make format/make format-checkmake lint/make lint-fixmake typecheck(Ty + Pyright, warnings fail the build)make test(pytest viabuild/run_pytest.py, 100% coverage enforced)make check(aggregates the quiet checks above plus Bandit, Deptry, pip-audit, and markdown linting)
Integration tests
Provider integrations require live credentials, so the suite stays opt-in. Export the
necessary OpenAI configuration and then run the dedicated make target, which disables
coverage enforcement automatically:
export OPENAI_API_KEY="sk-your-key"
# Optionally override the default model (`gpt-4.1`).
export OPENAI_TEST_MODEL="gpt-4.1-mini"
make integration-tests
make integration-tests forwards --no-cov to pytest so you can exercise the adapter
scenarios without tripping the 100% coverage gate configured for the unit test suite. The
tests remain skipped when OPENAI_API_KEY is not present.
Documentation
AGENTS.md— operational handbook and contributor workflow.specs/— design docs for prompts, planning tools, and adapters.ROADMAP.md— upcoming feature sketches.docs/api/— API reference material.
License
Apache 2.0 • Status: Alpha (APIs may change between releases)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file weakincentives-0.10.0.tar.gz.
File metadata
- Download URL: weakincentives-0.10.0.tar.gz
- Upload date:
- Size: 2.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3ed289b02fd82af4bf7a0de15cde29eeecfe97a664a3d6c24f6d905df42a329
|
|
| MD5 |
308dce68ee97415d3743f15eeddcf139
|
|
| BLAKE2b-256 |
988b0a55559dcd4076d78f0bf98617e4f0af3645c4cb86d0128924ffcf041672
|
Provenance
The following attestation bundles were made for weakincentives-0.10.0.tar.gz:
Publisher:
release.yml on weakincentives/weakincentives
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
weakincentives-0.10.0.tar.gz -
Subject digest:
c3ed289b02fd82af4bf7a0de15cde29eeecfe97a664a3d6c24f6d905df42a329 - Sigstore transparency entry: 716827418
- Sigstore integration time:
-
Permalink:
weakincentives/weakincentives@06df091873cb743ff95b1959542e31e06a0bc899 -
Branch / Tag:
refs/tags/v0.10.0 - Owner: https://github.com/weakincentives
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@06df091873cb743ff95b1959542e31e06a0bc899 -
Trigger Event:
release
-
Statement type:
File details
Details for the file weakincentives-0.10.0-py3-none-any.whl.
File metadata
- Download URL: weakincentives-0.10.0-py3-none-any.whl
- Upload date:
- Size: 179.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c74f7af53d01bee92b9207fb76989448d4bf3c37023214dde24a25a0046481d1
|
|
| MD5 |
eea768fa3bd303a3aab6a8d9b053923f
|
|
| BLAKE2b-256 |
493568e5edc6dcc929636a908b74b2b6c3f086be09fcf74677023ee849ea6cdd
|
Provenance
The following attestation bundles were made for weakincentives-0.10.0-py3-none-any.whl:
Publisher:
release.yml on weakincentives/weakincentives
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
weakincentives-0.10.0-py3-none-any.whl -
Subject digest:
c74f7af53d01bee92b9207fb76989448d4bf3c37023214dde24a25a0046481d1 - Sigstore transparency entry: 716827429
- Sigstore integration time:
-
Permalink:
weakincentives/weakincentives@06df091873cb743ff95b1959542e31e06a0bc899 -
Branch / Tag:
refs/tags/v0.10.0 - Owner: https://github.com/weakincentives
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@06df091873cb743ff95b1959542e31e06a0bc899 -
Trigger Event:
release
-
Statement type: