Skip to content

Latest commit

 

History

History
132 lines (103 loc) · 6.75 KB

File metadata and controls

132 lines (103 loc) · 6.75 KB

AGENTS.md — hawk-sdk-python

Python SDK for the Hawk daemon API. Provides an idiomatic Python client for chat, streaming, sessions, and stats.

Design Principles

  • Thin wrapper — maps directly to the hawk daemon HTTP API
  • Type-hinted — full type annotations for IDE support
  • Zero dependencies — only requests and typing_extensions

Build & Test

pip install -e ".[dev]"          # Install with dev deps
pytest                           # Run tests
pytest --cov=hawk --cov-report=term-missing  # Coverage
ruff check .                     # Lint
ruff format .                    # Format
mypy .                           # Type check

Architecture

  • hawk/client.py — Main HawkClient class
  • hawk/models.py — Pydantic models for API responses
  • hawk/errors.py — Typed error classes (HawkAPIError, etc.)
  • hawk/discovery.py — Auto-discover running hawk daemon
  • hawk/memory_tools.py — Memory graph operations
  • tests/ — Test suite with mocked HTTP responses

Conventions

  • Python 3.10+
  • ruff for linting and formatting (enforced in CI)
  • Type annotations required on all public APIs
  • Conventional Commits: feat:, fix:, docs:, refactor:, test:
  • No Co-authored-by: trailers
  • pytest for testing, httpretty for HTTP mocking

Common Pitfalls

  • HawkClient uses context manager (with statement) for cleanup
  • Discovery scans localhost ports — tests must mock this
  • Retry-After: 0 is valid — respect it, don't retry immediately

Naming Conventions

  • Modules: snake_case, one concept per file (client.py, errors.py, retry.py, streaming.py)
  • Classes: PascalCase, noun-based (HawkClient, AsyncHawkClient, StreamReader, RetryConfig)
  • Error classes: suffix with Error (HawkAPIError, NotFoundError, RateLimitError, InternalServerError)
  • Private methods: leading underscore (_request, _build_headers, _validate_base_url, _compute_backoff)
  • Type aliases: use TypeVar for generics (T = TypeVar("T") in types.py and retry.py)
  • Pydantic fields: use Field(alias="snake_case") for API-mapped names (session_id, tokens_in, active_sessions)
  • Test classes: Test prefix + subject (TestHawkClientSync, TestAsyncHawkClient, TestParseError, TestRetryConfig)
  • Test methods: test_ + behavior (test_health, test_chat_stream_error, test_no_retry_on_404)

API Patterns

Pydantic Models

All API types live in src/hawk/types.py. Every model uses BaseModel with model_config = {"populate_by_name": True} to allow both alias and field name access. Fields use Field(alias="snake_case") to map to the daemon's JSON keys:

class ChatResponse(BaseModel):
    session_id: str = Field(alias="session_id")
    tokens_in: int = Field(alias="tokens_in")
    model_config = {"populate_by_name": True}

Error Hierarchy

errors.py defines a status-code-based hierarchy: HawkAPIError is the base, with subclasses for each HTTP status (400, 401, 403, 404, 429, 500, 503). The parse_error() factory reads the JSON body (error, code, details fields) and returns the correct typed error. Always catch specific subclasses, not HawkAPIError broadly.

Retry Pattern

Every public client method wraps its logic in a _do() closure passed to with_retry_sync() (sync) or with_retry() (async). The retry function respects Retry-After headers on 429 responses and uses exponential backoff with jitter for other retryable statuses (429, 500, 502, 503, 504).

Streaming

StreamReader and AsyncStreamReader wrap httpx.Response with SSE parsing. They implement context manager protocol (with/async with). The events() method yields StreamEvent objects; collect_text() and collect_tool_calls() are convenience collectors.

Dual Client Pattern

HawkClient (sync) and AsyncHawkClient (async) are structurally identical. Every method exists on both. Sync uses httpx.Client + with_retry_sync; async uses httpx.AsyncClient + with_retry. When adding a new method, add it to both classes with identical signatures (minus async/await).

Testing Patterns

HTTP Mocking

Tests use respx (via pytest fixture respx_mock) to mock HTTP responses. Each test registers routes on BASE_URL = "http://127.0.0.1:4590":

def test_health(self, respx_mock: respx.MockRouter) -> None:
    respx_mock.get(f"{BASE_URL}/v1/health").mock(
        return_value=httpx.Response(200, json={...})
    )

Async Tests

Async test classes use @pytest.mark.asyncio decorator. The respx_mock fixture works for both sync and async.

Error Tests

test_errors.py uses a helper _make_response() to construct httpx.Response objects with specific status codes, JSON bodies, and headers. Tests verify both the error type (isinstance) and properties (status_code, message, retry_after).

Streaming Tests

SSE body strings follow the format "event: content\ndata: Hello\n\n". Tests verify both collect_text() and mid-stream break behavior.

Retry Tests

test_retry.py tests retry logic by wrapping call counters in closures. Uses tiny backoff values (initial_backoff=0.01) to keep tests fast. Verifies both retry-on-retryable and no-retry-on-non-retryable paths.

Key File Locations

What Where
Public API exports src/hawk/__init__.py
Client (sync + async) src/hawk/client.py
Pydantic models src/hawk/types.py
Error types + parser src/hawk/errors.py
Retry logic src/hawk/retry.py
SSE streaming src/hawk/streaming.py
Agent abstraction src/hawk/agent.py
Tool decorator + loop src/hawk/tools.py
Workflow engine src/hawk/workflow.py
Agent discovery src/hawk/discovery.py
Memory graph ops src/hawk/memory_tools.py
Evaluation/benchmarks src/hawk/evaluate.py
Tracing/observability src/hawk/tracing.py
Client tests tests/test_client.py
Error tests tests/test_errors.py
Retry tests tests/test_retry.py
Streaming tests tests/test_streaming.py

Refactoring Guidelines

  • Safe to refactor: internal helpers like _build_headers, _validate_base_url, _compute_backoff — they are private and well-tested
  • Do not change: parse_error() status-code mapping without updating all error subclasses and tests
  • Do not change: Pydantic Field(alias=...) values — they match the daemon's JSON contract
  • Safe to extend: add new error subclasses by adding to _STATUS_TO_ERROR dict and creating the class
  • Safe to extend: add new client methods by following the _do() + with_retry_sync() pattern
  • When adding streaming endpoints: follow the chat_stream pattern — build request, send with stream=True, check status before returning StreamReader