Skip to main content

wayfinder-agents

LangGraph-orchestrated AI agent framework for multi-domain entity management.

wayfinder-agents is a generic, production-grade Python framework for building LLM-powered workflows that create, modify, validate, and publish any structured entity type via a REST API. Built on LangGraph and LangChain.

Upgrading from 0.1.x? 0.2.0 contains breaking changes. Human approval before a write is now the default, so an app that does not handle the approval gate will keep running but stop writing. See Upgrading from 0.1.x and the changelog.


Quick Start

pip install wayfinder-agents
from wayfinder import WayfinderApp
from wayfinder.decorators import domain, Field

@domain("product", api="/api/products")
class Product:
    """An e-commerce product."""
    name: str = Field(required=True, description="Product name")
    description: str = Field(required=True, description="Full description")
    price_cents: int = Field(type="int", required=True, description="Price in cents")
    category: str = Field(enum=["electronics", "clothing", "books"])

app = WayfinderApp(
    name="ShopBot",
    api_base_url="http://localhost:3000",
)
app.discover_domains("myapp.domains")
app.serve(port=8001)

What It Does

The framework provides a 16-node LangGraph workflow out of the box:

Node Role
context_resolver Detects topic changes between conversation turns
coordinator Routes requests to the right specialist (LLM-based + deterministic fast-paths)
confirm Human approval gate before write actions (on by default, see below)
plan Decomposes multi-step requests into sub-tasks
research Fetches external data (Wikipedia, geocoding, images)
compose Creates a new entity draft from research notes
modify Edits existing entities
review Quality validation before publishing
publish Saves via REST API
propose Suggests changes without immediately modifying
remediate Fixes API validation errors and retries
delete / duplicate Entity lifecycle operations
query Read-only Q&A about existing entities
summarize Formats the final response

Each node can be overridden per-domain.

Human approval gate

Saving an entity pauses the workflow until a person approves. This is the default (PreviewPolicy(mode="before_publish")); a domain that wants unattended writes must opt in with PreviewPolicy(mode="after_publish"). Deleting an entity that has dependents pauses the same way.

The /stream endpoint ends with a confirm SSE event describing the pending action (summary, preview, actions, entity_id). Resume the same thread_id via /confirm with proceed, cancel, or edit (inline corrections applied before the save). Approval is a per-turn grant: every new request re-arms the gate. A confirmation that times out is cancelled, never auto-approved; there is deliberately no on_timeout="proceed" option.

The gate shows a field-level diff, any findings the automated reviewer waived, and any rewrites an LLM made on its own. ConfirmationPolicy can add friction (min_decision_seconds, require_rationale_for, require_distinct_approver), and GET /api/agents/framework/oversight reports whether the human loop is actually working (decision latency, override and fast-approval rates, canary results). See docs/oversight.md for the full mapping to AI Agents Push Humans Out of the Loop (arXiv:2608.23642).

Least-privilege grants

An approval mints a session grant scoped to the requester, the exact change, and the permissions the write needs. Plug in your RBAC with set_authz_provider; the requester covers the scopes they hold, and any missing scope requires a distinct approver who holds it. The grant is bound to the approved diff, so a draft that grows after approval re-gates, and it travels on the write as signed headers your API can verify - with an Express middleware and a Python helper included, and optional RFC 8693 token exchange for a genuinely downscoped OAuth token. Map your existing roles with RoleMappedProvider or HttpAuthorizationProvider, and share grants across workers with RedisGrantStore. See docs/least-privilege.md.


Upgrading from 0.1.x

0.2.0 changed the default from "write, then show the user" to "show the user, then write". That is the point of the release, and it is a breaking change: an application written against 0.1.16 will keep running but stop writing, because the workflow now waits for an approval its client never sends. No error is raised, so the symptom is a workflow that appears to hang.

You have two ways forward.

Keep the old behaviour

Opt out per domain. Nothing else in your integration has to change:

from wayfinder.decorators import domain
from wayfinder.domain import PreviewPolicy

@domain("product", api="/api/products", preview_policy=PreviewPolicy(mode="after_publish"))
class Product:
    ...

Two smaller breaks still apply. ConfirmationPolicy(on_timeout="proceed") now raises ValueError, because silence is never treated as approval; use "cancel" or "hold". And deleting always pauses for a person, so either handle the gate or drop "delete" from ConfirmationPolicy.require_rationale_for and approve with a rationale.

Adopt the gate

Handle the confirm event and send the decision back:

# The stream ends with a confirm event instead of a done event when a
# person is needed. Everything you need to render the decision is in it.
{
  "type": "confirm",
  "data": {
    "pending_action": "publish",         # publish | delete | context
    "summary": "Update: Sacred Ireland\nChanges: 2 field changes...",
    "changes": {"added": [], "changed": [...], "removed": [], "total": 2},
    "waived_issues": ["needs more sites"],   # the reviewer overrode itself
    "machine_edits": [...],                  # an LLM rewrote this, unattended
    "required_scopes": ["tour:update:abc:difficulty"],
    "missing_scopes": [],                    # non-empty means escalate
    "actions": ["proceed", "cancel", "edit"],
    "requires_rationale": false
  }
}
# Then resume the same thread with the person's decision.
curl -X POST /api/agents/framework/tour/confirm \
  -d '{"thread_id": "...", "action": "proceed"}'

Three client-side details matter:

  • Read until the stream closes, not until done. A finished turn now emits an audit event after done with the action log.
  • Treat 409 from /confirm as "already resolved", not as a retryable error. It previously resumed the thread regardless, which restarted finished work.
  • Pass thread_id on later requests to continue a conversation; it comes back in the X-Thread-Id header.

What did not change

Verified against 0.1.16 code: every public export from wayfinder is still present, register_pending still accepts its original keyword arguments, and event_stream keeps its original positional parameters. Least-privilege scope checks are inert until you install a provider, since the default grants every user every scope.

The changelog lists all eight breaking changes with the before-and-after for each.


Domain Registration

Using the @domain decorator

from wayfinder.decorators import domain, subdoc, Field, Subdoc, Rel

@subdoc
class Address:
    street: str = Field(required=True)
    city: str = Field(required=True)

@domain("venue", api="/api/venues", progressive_save="rooms:5")
class Venue:
    """A physical event venue."""
    name: str = Field(required=True)
    address: Address = Subdoc(Address)
    capacity: int = Field(type="int")
    events: object = Rel("event", has_many=True, foreign_key="venue_id")

LLM de-identification (field aliasing & redaction)

For sensitive data (for example HIPAA/PHI fields like patient names or SSNs), you can mark specific fields so Wayfinder masks them before sending prompts to the LLM and restores the originals from structured JSON responses.

Two complementary mechanisms are available per-field:

  • Aliasing (llm_alias=True) — replaces values with deterministic referenceable tokens (PATIENT_1, CLINICIAN_2). The LLM can still reason about and reference the entity in its output.
  • Redaction (llm_redact=True) — replaces values with an opaque placeholder ([REDACTED] by default, configurable via llm_redact_value). The LLM cannot meaningfully reference the value; originals are restored from a positional store after the LLM call.
from wayfinder.decorators import domain, Field, subdoc, Subdoc

@subdoc
class Visit:
    clinician_name: str = Field(llm_alias=True, llm_alias_prefix="clinician")
    note: str = Field()

@domain("patient_record", api="/api/patient-records")
class PatientRecord:
    patient_name: str = Field(required=True, llm_alias=True, llm_alias_prefix="patient")
    ssn: str = Field(llm_redact=True, llm_redact_value="***")
    diagnosis: str = Field(required=True)
    visits: list[Visit] = Subdoc(Visit)

For brevity, two classmethod shortcuts produce the same configuration:

patient_name: str = Field.aliased("patient", required=True)
ssn: str = Field.redacted("***")

Behavior summary:

  • Aliased fields: same value at the same field path reuses the same token; restored to original after the LLM returns JSON.
  • Redacted fields: every value at that path is replaced with the placeholder; originals are stashed by position (including list index) and restored even if the LLM mutates or drops the field. Redaction takes precedence when both llm_alias and llm_redact are set.

Current scope:

  • Built into draft-based LLM flows (compose, modify, review).
  • Targets domain fields you explicitly mark with llm_alias=True or llm_redact=True.
  • Does not automatically scrub arbitrary free-text conversation history; if you include PHI in user messages, redact upstream before sending.

Using a Pydantic model

from pydantic import BaseModel
from wayfinder.adapters import domain_from_model

class Article(BaseModel):
    """A blog article."""
    title: str
    body: str
    tags: list[str] = []

domain_from_model(Article, api="/api/articles")

Domain Customisation

Override a node

from wayfinder.decorators import node

@node("modify", domain="product")
async def my_modifier(state):
    # custom modification logic
    return {"draft": {...}, "next_step": "review"}

Lifecycle events

from wayfinder.decorators import on

@on("before_publish", domain="product")
async def validate_images(state):
    if not state.draft.get("images"):
        state.draft["images"] = []
    return state

Custom validator

from wayfinder.domain import DomainValidator

class ProductValidator(DomainValidator):
    def validate(self, draft: dict, errors: list[str]) -> list[str]:
        if draft.get("price_cents", 0) <= 0:
            errors.append("price_cents must be greater than 0")
        return errors

API Adapters

Out of the box the framework ships an adapter for Express/Mongoose APIs. Implement APIAdapter for other backends:

from wayfinder.api_adapter import APIAdapter, set_adapter

class DjangoRestAdapter(APIAdapter):
    def parse_response(self, json_body, entity_name): ...
    def parse_list_response(self, json_body, plural_name): ...
    def build_pagination_params(self, page, limit): ...
    async def discover_schema(self, client, api_base_url, model_name): ...

set_adapter(DjangoRestAdapter())

Configuration

All settings use the WAYFINDER_ env prefix with __ as the nested delimiter:

WAYFINDER__LLM__DEFAULT_PROVIDER=openai
WAYFINDER__LLM__OPENAI_MODEL=gpt-4.1
WAYFINDER__API__BASE_URL=http://myapi:3000/api
WAYFINDER__API__SERVICE_TOKEN=my-internal-token
WAYFINDER__CHECKPOINT__BACKEND=redis
WAYFINDER__CHECKPOINT__REDIS_URL=redis://redis:6379/0
WAYFINDER__OBSERVABILITY__LANGSMITH_ENABLED=true
WAYFINDER__OBSERVABILITY__LANGSMITH_API_KEY=ls-...

Checkpointing Backends

# PostgreSQL
pip install "wayfinder-agents[postgres]"
WAYFINDER__CHECKPOINT__BACKEND=postgres
WAYFINDER__CHECKPOINT__CONNECTION_STRING=postgresql+asyncpg://user:pass@host/db

# Redis
pip install "wayfinder-agents[redis]"
WAYFINDER__CHECKPOINT__BACKEND=redis
WAYFINDER__CHECKPOINT__REDIS_URL=redis://localhost:6379/0

# MongoDB
pip install "wayfinder-agents[mongodb]"
WAYFINDER__CHECKPOINT__BACKEND=mongodb
WAYFINDER__CHECKPOINT__CONNECTION_STRING=mongodb://localhost:27017

Publishing to PyPI

# Build
python -m build

# Test on TestPyPI first
twine upload --repository testpypi dist/*

# Publish
twine upload dist/*

The GitHub Actions workflow at .github/workflows/publish.yml automates this on tagged releases.


License

wayfinder-agents is available under the Wayfinder Community License 1.0.

Free use is permitted for:

  • individuals using the project for personal, educational, research, or non-commercial work;
  • nonprofits and educational institutions for non-commercial internal use; and
  • organizations with less than USD 100,000 in annual gross revenue.

A separate commercial license is required for:

  • organizations at or above USD 100,000 in annual gross revenue;
  • paid products, paid client work, and internal commercial production use; and
  • hosted or managed-service offerings based on the software.

See LICENSE, licensing strategy, and commercial terms summary.

Metadata

Release files for wayfinder-agents 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wayfinder-agents 0.2.0
File Size Uploaded
wayfinder_agents-0.2.0.tar.gz 156.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wayfinder-agents 0.2.0
File Interpreter ABI Platform
wayfinder_agents-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 283.5 kB

Release files / wayfinder_agents-0.2.0.tar.gz

Download URL wayfinder_agents-0.2.0.tar.gz
Size 156.3 kB
Tags Source
SHA-256 checksum
How to use checksums
baaf0c5b89891c61dde72ae8b0ba50b7b7d0cb8bc15fd4a36c556a68e1f8f407
BLAKE2b-256 checksum
How to use checksums
211e1b109efa0f09333981a79199957053dc60e5327063e7f9ed4efd2b9ddb57
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 12, 2026.

Transparency log

Release files / wayfinder_agents-0.2.0-py3-none-any.whl

Download URL wayfinder_agents-0.2.0-py3-none-any.whl
Size 127.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
744a04b9a96e7da6e0534945869ba44e60166d292f10eb77cfe9c4d3b2b0adcf
BLAKE2b-256 checksum
How to use checksums
b0696779c3455e867a83800de51449995541ae788f11e8aef7e69d86f419867a
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 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.16

2 release files

0.1.15

2 release files

0.1.14

2 release files

0.1.13

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 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