Build with SPILLWAY · Python 3.10+

Connect your tool loop.

Put the guard between proposed actions and your tools. Try a working local HTTP example, then use its boundary with your application's own tool contracts.

Minimal example · Run the app example · The four steps · Data paths

Run it without model credentials.

The example starts a temporary server on 127.0.0.1, runs a fixed sequence of proposed calls with synthetic data, records the HTTP requests and effects, and shuts the server down. It uses the SPILLWAY 0.1.0 API, Pydantic and the standard library.

git clone https://github.com/saintmachines/spillway.git
cd spillway
python -m pip install git+https://github.com/saintmachines/spillway.git@v0.1.0
python docs/examples/local_app_demo.py --report app-check.json

Use the current repository checkout for these examples. They are separate from the wheel and source distribution. The test app stores messages and notes in memory and sends no email. Read the example guide · Boundary source · Runnable example.

The application boundary

Validate, check, execute, observe.

Give the agent only the text returned by the boundary. Keep a separate execution record for the application.

  1. 1

    Validate the call

    Register literal tool names and strict argument models. Reject unknown tools, extra fields and malformed recipient collections before transport.

  2. 2

    Check before execution

    Reads are checked too. The guard sees the same canonical arguments the application will send. A refusal makes no HTTP request.

  3. 3

    Run the allowed tool

    The example uses a fixed loopback endpoint with bounded responses and timeouts. It follows no redirects and uses no environment proxy.

  4. 4

    Observe the return

    Validate raw responses before observing them. Observe error bodies as untyped text. Return only sanitized model_text to the agent.

Use one guard for each user task and keep this example sequential. The tool owner supplies trusted schemas and a complete role vocabulary. Schema validation does not authenticate a sender or infer new roles.

For an optional judge using an Ollama reasoning model, set Judge(endpoint, "gemma4:31b", extra_body={"reasoning_effort": "none"}). Turn the model's thinking off, as the paper's runs did. The local HTTP examples run without a model.

Ordinary data and adapters

Keep the tool's contract at the boundary.

The library accepts typed models, dictionaries, lists and text. What becomes an admitted identity depends on the tool's declared type and role contract.

InputHow to connect it
Pydantic models and nested resultsPass the raw declared model before serialization. Only read-only returns can add typed identities.
JSON dictionaries and listsUse provider-controlled routing keys and a declared schema. A dictionary key name is a role convention, not sender authentication.
Text, retrieved context and memoryObserve and sanitize it before the agent reads it. Text alone does not declare trusted identity fields.
CSVParse rows explicitly and validate them with the tool's model. Raw CSV remains text.
New tool or argument namesDeclare read/write and target roles with Lexicon.extend. The example uses the existing sender and recipients vocabulary.

Lists of records.

For a trusted response model containing independent records, use ToolSpec(..., record_field="messages"). The boundary indexes the complete typed response once and renders each declared field separately, so the filter's region stays within that field. The application supplies the schema and record boundary.

Declare routing arguments such as target_user in target_fields and add their role with Lexicon.extend. Registration checks that the declared fields exist in the argument model and that the vocabulary recognizes their role. Review the complete tool contract before connecting a new service.

Run python docs/examples/collection_demo.py --report collection-check.json from the current checkout for the local HTTP replay. The collection example renders fields separately; paper reproduction uses the evaluated runner under reference/. Its benchmark values describe that evaluated path.

Collection guide · Collection replay source · Integration conditions · Guard API · Role vocabulary.

Read the execution record

An error can arrive after a write.

requests_sent=0 means transport was never attempted, 1 means an HTTP response confirmed a request, and None means delivery is unknown after a transport failure. A write's effects remain unknown until the application reconciles them. The fixture records its actual effects separately.

Do not retry a write blindly or return unchecked text after a guard failure. The example safely stops a failed final-answer check and never falls back to the original answer. Use your application's authentication, idempotency and reconciliation when adapting its transport.

This is a local integration example with synthetic data and fixed proposals. Live model or Gmail/Slack account integration, framework plugins, concurrent execution and long-running operation are separate verification tasks.