Skip to content

feat(sdk/python): @rule, @agent, exception mapping + bridge protocol fixes - #23

Merged
evansibok merged 14 commits into
nextfrom
feat/python-sdk
Apr 17, 2026
Merged

feat(sdk/python): @rule, @agent, exception mapping + bridge protocol fixes#23
evansibok merged 14 commits into
nextfrom
feat/python-sdk

Conversation

@evansibok

@evansibok evansibok commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Python SDK (sdks/python/nanny-sdk)

  • @tool(cost=N): sync + async wrappers, /tool/call POST, deny → typed exception, passthrough mode
  • @rule("name"): client-side evaluation via _RULES ordered dict; RuleDenied raised before bridge is contacted; _reset_rules autouse fixture keeps tests isolated
  • @agent("name"): /agent/enter + /agent/exit with try/finally; AgentNotFound on 404; sync and async both supported
  • agent_exit fix: silently ignores ConnectError when bridge closes connection after issuing a stop — prevents cleanup call from masking the real stop reason
  • Exception mapping: all StopReason variants map to typed exceptions (BudgetExhausted, MaxStepsReached, RuleDenied, Timeout)
  • Passthrough mode: zero-overhead when NANNY_BRIDGE_SOCKET / NANNY_BRIDGE_PORT are unset — decorators are no-ops
  • 66 tests across test_tool.py, test_rule.py, test_agent.py, test_exceptions.py, test_passthrough.py
  • Python floor raised to >=3.11 — 3.10 EOL is October 2026; ruff target-version and mypy python_version updated to match

Bridge protocol corrections

Found via Rust bridge source audit — the original implementation had drifted from the actual bridge:

  • Auth header: X-Nanny-TokenX-Nanny-Session-Token
  • /health response: {"status":"ok"}{"state":"running"}
  • /tool/call deny fields: generic "detail""tool_name" / "rule_name" as separate fields
  • /agent/enter: 404 → AgentNotFound, not {"status":"denied"}
  • /agent/exit: request body {} not {"name": name}

Transport fix

The SDK previously only read NANNY_BRIDGE_PORT — silently broken on macOS/Linux where the CLI injects NANNY_BRIDGE_SOCKET (Unix domain socket) instead. NANNY_BRIDGE_PORT is Windows-only.

  • _make_client() routes to HTTPTransport(uds=...) on Unix, plain TCP on Windows
  • is_passthrough() checks both env vars

dev_assist — LangChain example app (examples/python/dev_assist)

  • Single-agent debug assistant: given a stack trace, diagnoses root cause and suggests a fix
  • Two execution modes: ReAct (self-controlled Thought/Action/Observation loop) and Plan-and-Execute (structured JSON plan → deterministic execution → synthesis)
  • Tools: file_reader, ripgrep, write_file — each decorated with @nanny_tool
  • @rule("no_read_loop"): fires if last 5 tool calls are all file_reader
  • @nanny_agent("debugger"): activates [limits.debugger] scope for the run
  • CLI: uv run dev debug --trace <file> [--mode react|plan]
  • nanny.toml demo limits: demo-budget, demo-steps, demo-rule — all three stop reasons verified under nanny run

metrics_crew — CrewAI example app (examples/python/metrics_crew)

  • Four-agent sequential pipeline: Ingestion → Analysis → Visualization → Reporter
  • Each agent has a designated tool subset; cross-role tool calls result in ToolDenied
  • 7 tools: validate_schema, load_metrics, compute_stats, detect_anomalies, correlate_signals, generate_chart, write_report — each decorated with @nanny_tool
  • @rule("no_analysis_loop"): fires if last 5 tool calls are all compute_stats
  • @nanny_agent("analysis"): activates [limits.analysis] scope for the run
  • CLI: uv run metrics-crew analyze --data <csv>
  • nanny.toml demo limits: demo-budget, demo-steps, demo-rule — all three stop reasons verified under nanny run
  • CrewAI 1.14 compatibility: patches BaseTool.run and Tool.run at import time to re-raise NannyStop as _NannySignal(BaseException), bypassing CrewAI's executor except Exception handlers that would otherwise swallow stop signals

CI

  • ci.yml renamed to ci-rust.yml (name: CI — Rust)
  • ci-python.yml added (name: CI — Python SDK) — pytest + ruff + mypy, macOS + Linux, Python 3.11 + 3.13, triggers on sdks/python/** changes

Test plan

  • uv run pytest -q — 66 tests passing
  • uv run mypy nanny_sdk — zero errors
  • uv run ruff check . — zero warnings
  • cd examples/python/dev_assist && nanny run --limits=demo-budgetBudgetExhausted panel, NDJSON ExecutionStopped on stdout
  • cd examples/python/metrics_crew && nanny run --limits=demo-budgetBudgetExhausted panel, NDJSON ExecutionStopped on stdout
  • cd examples/python/metrics_crew && nanny run --limits=demo-stepsMaxStepsReached panel
  • cd examples/python/metrics_crew && nanny run --limits=demo-ruleRuleDenied panel

Adds sdks/python/ with the full package structure for nanny-sdk:

- pyproject.toml: hatchling build, Apache-2.0, requires-python >=3.10,
  httpx as the only runtime dep; dev group: pytest, pytest-httpserver,
  pytest-asyncio, ruff, mypy
- nanny_sdk/__init__.py: public API surface (__all__ defined)
- nanny_sdk/exceptions.py: all StopReason variants as typed Python
  exceptions matching Rust names exactly (NannyStop base, ToolDenied,
  RuleDenied carry detail fields)
- nanny_sdk/_context.py: PolicyContext @DataClass mirroring Rust
  struct field-for-field, with from_dict() constructor
- nanny_sdk/_client.py: lazy env-var bridge client (reads
  NANNY_BRIDGE_PORT at call time so monkeypatch works in tests);
  call_tool() raises NannyStop subclass on deny, returns None on allow
- nanny_sdk/_decorators.py: @tool fully wired — contacts bridge before
  each call, stringifies args by parameter name, raises on deny, never
  calls function body; @rule and @agent are Day 3/4 stubs
- nanny_sdk/py.typed: PEP 561 marker
- tests/conftest.py: mock_bridge fixture (pytest-httpserver fake bridge)
- tests/test_client.py: 4 connectivity smoke tests
- tests/test_tool.py: 13 @tool tests — routing, payload, all deny
  reasons, function body not called on deny, passthrough, async

Also:
- .gitignore: add Python build/cache patterns (.venv, __pycache__,
  .mypy_cache, .ruff_cache, .pytest_cache, *.egg-info, dist);
  note uv.lock is intentionally committed
- examples/rust/qabud/.env.example: add API_KEY sentinel with
  instructions to cp .env.example .env before recording demos

17 tests passing. ruff clean. mypy strict clean.
…e protocol fixes

- @rule: client-side evaluation via _RULES ordered dict; RuleDenied raised
  before bridge is ever contacted; _reset_rules autouse fixture added to conftest
- @agent: /agent/enter + /agent/exit with try/finally; AgentNotFound on 404;
  sync and async both supported
- test_rule.py, test_agent.py, test_exceptions.py: 51 tests total, all passing

Bridge protocol corrections (found via Rust bridge source audit):
- Auth header: X-Nanny-Token → X-Nanny-Session-Token
- /health response: {"status":"ok"} → {"state":"running"}
- /tool/call deny fields: "detail" → "tool_name" / "rule_name" (separate fields)
- /agent/enter: 404 → AgentNotFound, not {"status":"denied"}
- /agent/exit: request body {} not {"name": name}

Transport fix (Unix socket + TCP):
- SDK previously only read NANNY_BRIDGE_PORT — silently broken on macOS/Linux
  where CLI injects NANNY_BRIDGE_SOCKET (Unix domain socket) instead
- _make_client() now routes to HTTPTransport(uds=...) on Unix, TCP on Windows
- is_passthrough() checks both NANNY_BRIDGE_SOCKET and NANNY_BRIDGE_PORT
@evansibok evansibok changed the title feat(sdk/python): Days 3–5 — @rule, @agent, exception mapping + bridge protocol fixes feat(sdk/python): @rule, @agent, exception mapping + bridge protocol fixes Apr 7, 2026
evansibok and others added 10 commits April 11, 2026 11:40
…line

Four-agent CrewAI pipeline governed by Nanny. Demonstrates multi-agent
role-based limits, tool allowlist enforcement per role, and three stop
reasons (BudgetExhausted, MaxStepsReached, RuleDenied) all verified
under `nanny run`.

Covers the four agentic patterns from the v0.1.4 plan: tool-calling,
multi-agent/role-based limits, Plan-and-Execute (CrewAI sequential
process), and partial event-driven (task context chaining).

CrewAI 1.14 compatibility: patches BaseTool.run and Tool.run at import
time to re-raise NannyStop as _NannySignal(BaseException), bypassing
CrewAI's executor `except Exception` handlers that would otherwise
swallow stop signals silently.

Also fixes nanny_sdk._client.agent_exit to silently ignore ConnectError
when the bridge closes the connection after issuing a stop — prevents
the cleanup call from masking the real stop reason in both dev_assist
and metrics_crew.
Consistent with metrics_crew. Excludes __pycache__, .venv, dist/,
and other Python/uv build artefacts from version control.
Consistent naming convention: ci-<language>.yml makes it immediately
clear which pipeline tests which SDK. Scales cleanly when Node is added.

ci-rust.yml: renamed from ci.yml, name field updated to "CI — Rust"
ci-python.yml: new — pytest + ruff + mypy on macOS + Linux,
               Python 3.10 and 3.12, triggers on sdks/python/** changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…11+3.13

3.10 EOL is October 2026 — 6 months after launch. LangChain and CrewAI
both target 3.11+. 3.11 is 10-60% faster than 3.10.

- requires-python: >=3.10 → >=3.11
- ruff target-version: py310 → py311
- mypy python_version: 3.10 → 3.11
- ci-python.yml matrix: ["3.10", "3.12"] → ["3.11", "3.13"]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PyPI publish as a fifth job in the existing release pipeline,
following the same pattern as homebrew-tap-publish and publish-crates.

- Triggers on v* tag push after release succeeds; skips rc tags
- workflow_dispatch re-run support: same version input as other publish
  jobs — safe to re-run all publish jobs simultaneously (PyPI rejects
  duplicate versions gracefully)
- Uses PyPI Trusted Publishing (OIDC) — no API token in secrets
- Verifies pyproject.toml version matches the tag before building
- uv build produces sdist + wheel; pure-Python, single wheel covers all
  platforms

Pre-requisite before first publish: configure Trusted Publishing on
pypi.org — project: nanny-sdk, repo: nanny-run/nanny,
workflow: release.yml, environment: pypi

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch to minor-level versioning: v0.1.x covers all v0.1 patches
- Rename v0.1.4/ → v0.1/, drop v0.1.3/ (superseded)
- Remove old unversioned root-level doc pages
- Update all internal links from /v0.1.4/ to /v0.1/
- Add redirects for /quickstart, /v0.1.3/:slug*, /v0.1.4/:slug*
- Collapse CLI and Guides tabs into Getting Started as groups
- Reorder groups: Concepts → SDKs → CLI → Reference
- Fix broken links in v0.1.3 that were missing version prefix
- Remove patch-specific version mentions from prose
- Document versioning policy (max 4 versions) in AGENTS.md
- Add sdks/python/README.md with usage docs for @tool, @rule, @agent
- Add readme field to sdks/python/pyproject.toml so PyPI shows description
- Bump nannyd dep to 0.1.4 in rust examples (qabud, webdingo)
- Pin nanny-sdk==0.1.4 and drop local path override in python examples
@evansibok
evansibok merged commit e9d4f19 into next Apr 17, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant