Skip to main content

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 Prompt objects composed from sections such as MarkdownSection, 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. Session acts 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:

  1. 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.
  2. 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.
  3. Integrated, hash-based prompt overrides. PromptDescriptor content hashes, tool contracts, and chapter descriptors ensure overrides only apply to the intended section version while describing the declared chapter layout. LocalPromptOverridesStore keeps the JSON artifacts in version control so teams can collaborate without risking stale edits.
  4. 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.
  5. 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-version for development)
  • uv CLI

Install

uv add weakincentives
# optional tool extras
uv add "weakincentives[asteval]"
# optional provider adapters
uv add "weakincentives[openai]"
uv add "weakincentives[litellm]"
# optional CLI extras (FastAPI debug UI)
uv add "weakincentives[wink]"
# cloning the repo? use: uv sync --extra asteval --extra openai --extra litellm --extra wink

Debugging snapshots with wink

The wink CLI ships a debug subcommand that serves a FastAPI-based UI for exploring session snapshot JSON files. Install the extra and start the server:

uv run --extra wink wink debug snapshots/5de6bba7-d699-4229-9747-d68664d8f91e.json \
  --host 127.0.0.1 --port 8000

The UI is tuned for quick inspection of captured runs:

  • Snapshot metadata (path, created timestamp, schema version) is pinned to the header so you always know what file is being viewed.
  • A slice sidebar lists every slice type with item counts; selecting one streams the items into a JSON viewer with copy-to-clipboard and collapse controls.
  • A reload action re-reads the snapshot from disk so you can iterate on reproducible runs without restarting the server, and a raw download button fetches the full JSON for archival or diffing.

Snapshot Explorer UI (1902x1572)

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:

  1. Returns structured, typed data.
  2. Interacts with files in a sandboxed environment.
  3. Creates and follows plans to solve complex tasks.
  4. Whose every action is recorded and can be inspected.
  5. 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

  1. Install Python 3.12 (for example with pyenv install 3.12.0).

  2. Install uv, then bootstrap the environment and hooks:

    uv sync
    ./install-hooks.sh
    
  3. Run checks with uv run so everything shares the managed virtualenv:

    • make format / make format-check
    • make lint / make lint-fix
    • make typecheck (Ty + Pyright, warnings fail the build)
    • make test (pytest via build/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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

weakincentives-0.11.0.tar.gz (2.8 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

weakincentives-0.11.0-py3-none-any.whl (197.9 kB view details)

Uploaded Python 3

File details

Details for the file weakincentives-0.11.0.tar.gz.

File metadata

  • Download URL: weakincentives-0.11.0.tar.gz
  • Upload date:
  • Size: 2.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for weakincentives-0.11.0.tar.gz
Algorithm Hash digest
SHA256 a790a149be5f17904479a4a08db732d2150cbc3025ef257f3481b08b0fc1ebe6
MD5 9c074de9b1a0965871f277550cff7023
BLAKE2b-256 accf3817bf6f6cc71f33ec98510770913f5e9c918dfd58647cba3374a3764bc6

See more details on using hashes here.

Provenance

The following attestation bundles were made for weakincentives-0.11.0.tar.gz:

Publisher: release.yml on weakincentives/weakincentives

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file weakincentives-0.11.0-py3-none-any.whl.

File metadata

  • Download URL: weakincentives-0.11.0-py3-none-any.whl
  • Upload date:
  • Size: 197.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for weakincentives-0.11.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dab416b78b3f66502cfb954100a4e5420be577041874148801f87bbe653830f4
MD5 72592ee71c92d114c55932c2b4871f4e
BLAKE2b-256 14a87b8a95b3ef7a18596abea5afcd11c15a5fdaa7204cfed986fe3ec8a13dc3

See more details on using hashes here.

Provenance

The following attestation bundles were made for weakincentives-0.11.0-py3-none-any.whl:

Publisher: release.yml on weakincentives/weakincentives

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page