|
| 1 | +# AGENTS.md — hawk-sdk-python |
| 2 | + |
| 3 | +Python SDK for the Hawk daemon API. Provides an idiomatic Python client for chat, streaming, sessions, and stats. |
| 4 | + |
| 5 | +## Design Principles |
| 6 | + |
| 7 | +- **Thin wrapper** — maps directly to the hawk daemon HTTP API |
| 8 | +- **Type-hinted** — full type annotations for IDE support |
| 9 | +- **Zero dependencies** — only `requests` and `typing_extensions` |
| 10 | + |
| 11 | +## Build & Test |
| 12 | + |
| 13 | +```bash |
| 14 | +pip install -e ".[dev]" # Install with dev deps |
| 15 | +pytest # Run tests |
| 16 | +pytest --cov=hawk --cov-report=term-missing # Coverage |
| 17 | +ruff check . # Lint |
| 18 | +ruff format . # Format |
| 19 | +mypy . # Type check |
| 20 | +``` |
| 21 | + |
| 22 | +## Architecture |
| 23 | + |
| 24 | +- `hawk/client.py` — Main `HawkClient` class |
| 25 | +- `hawk/models.py` — Pydantic models for API responses |
| 26 | +- `hawk/errors.py` — Typed error classes (`HawkAPIError`, etc.) |
| 27 | +- `hawk/discovery.py` — Auto-discover running hawk daemon |
| 28 | +- `hawk/memory_tools.py` — Memory graph operations |
| 29 | +- `tests/` — Test suite with mocked HTTP responses |
| 30 | + |
| 31 | +## Conventions |
| 32 | + |
| 33 | +- Python 3.10+ |
| 34 | +- `ruff` for linting and formatting (enforced in CI) |
| 35 | +- Type annotations required on all public APIs |
| 36 | +- Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`, `test:` |
| 37 | +- No `Co-authored-by:` trailers |
| 38 | +- `pytest` for testing, `httpretty` for HTTP mocking |
| 39 | + |
| 40 | +## Common Pitfalls |
| 41 | + |
| 42 | +- `HawkClient` uses context manager (`with` statement) for cleanup |
| 43 | +- Discovery scans localhost ports — tests must mock this |
| 44 | +- `Retry-After: 0` is valid — respect it, don't retry immediately |
| 45 | + |
| 46 | +## Naming Conventions |
| 47 | + |
| 48 | +- **Modules**: snake_case, one concept per file (`client.py`, `errors.py`, `retry.py`, `streaming.py`) |
| 49 | +- **Classes**: PascalCase, noun-based (`HawkClient`, `AsyncHawkClient`, `StreamReader`, `RetryConfig`) |
| 50 | +- **Error classes**: suffix with `Error` (`HawkAPIError`, `NotFoundError`, `RateLimitError`, `InternalServerError`) |
| 51 | +- **Private methods**: leading underscore (`_request`, `_build_headers`, `_validate_base_url`, `_compute_backoff`) |
| 52 | +- **Type aliases**: use `TypeVar` for generics (`T = TypeVar("T")` in `types.py` and `retry.py`) |
| 53 | +- **Pydantic fields**: use `Field(alias="snake_case")` for API-mapped names (`session_id`, `tokens_in`, `active_sessions`) |
| 54 | +- **Test classes**: `Test` prefix + subject (`TestHawkClientSync`, `TestAsyncHawkClient`, `TestParseError`, `TestRetryConfig`) |
| 55 | +- **Test methods**: `test_` + behavior (`test_health`, `test_chat_stream_error`, `test_no_retry_on_404`) |
| 56 | + |
| 57 | +## API Patterns |
| 58 | + |
| 59 | +### Pydantic Models |
| 60 | +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: |
| 61 | +```python |
| 62 | +class ChatResponse(BaseModel): |
| 63 | + session_id: str = Field(alias="session_id") |
| 64 | + tokens_in: int = Field(alias="tokens_in") |
| 65 | + model_config = {"populate_by_name": True} |
| 66 | +``` |
| 67 | + |
| 68 | +### Error Hierarchy |
| 69 | +`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. |
| 70 | + |
| 71 | +### Retry Pattern |
| 72 | +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). |
| 73 | + |
| 74 | +### Streaming |
| 75 | +`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. |
| 76 | + |
| 77 | +### Dual Client Pattern |
| 78 | +`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`). |
| 79 | + |
| 80 | +## Testing Patterns |
| 81 | + |
| 82 | +### HTTP Mocking |
| 83 | +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"`: |
| 84 | +```python |
| 85 | +def test_health(self, respx_mock: respx.MockRouter) -> None: |
| 86 | + respx_mock.get(f"{BASE_URL}/v1/health").mock( |
| 87 | + return_value=httpx.Response(200, json={...}) |
| 88 | + ) |
| 89 | +``` |
| 90 | + |
| 91 | +### Async Tests |
| 92 | +Async test classes use `@pytest.mark.asyncio` decorator. The `respx_mock` fixture works for both sync and async. |
| 93 | + |
| 94 | +### Error Tests |
| 95 | +`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`). |
| 96 | + |
| 97 | +### Streaming Tests |
| 98 | +SSE body strings follow the format `"event: content\ndata: Hello\n\n"`. Tests verify both `collect_text()` and mid-stream `break` behavior. |
| 99 | + |
| 100 | +### Retry Tests |
| 101 | +`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. |
| 102 | + |
| 103 | +## Key File Locations |
| 104 | + |
| 105 | +| What | Where | |
| 106 | +|------|-------| |
| 107 | +| Public API exports | `src/hawk/__init__.py` | |
| 108 | +| Client (sync + async) | `src/hawk/client.py` | |
| 109 | +| Pydantic models | `src/hawk/types.py` | |
| 110 | +| Error types + parser | `src/hawk/errors.py` | |
| 111 | +| Retry logic | `src/hawk/retry.py` | |
| 112 | +| SSE streaming | `src/hawk/streaming.py` | |
| 113 | +| Agent abstraction | `src/hawk/agent.py` | |
| 114 | +| Tool decorator + loop | `src/hawk/tools.py` | |
| 115 | +| Workflow engine | `src/hawk/workflow.py` | |
| 116 | +| Agent discovery | `src/hawk/discovery.py` | |
| 117 | +| Memory graph ops | `src/hawk/memory_tools.py` | |
| 118 | +| Evaluation/benchmarks | `src/hawk/evaluate.py` | |
| 119 | +| Tracing/observability | `src/hawk/tracing.py` | |
| 120 | +| Client tests | `tests/test_client.py` | |
| 121 | +| Error tests | `tests/test_errors.py` | |
| 122 | +| Retry tests | `tests/test_retry.py` | |
| 123 | +| Streaming tests | `tests/test_streaming.py` | |
| 124 | + |
| 125 | +## Refactoring Guidelines |
| 126 | + |
| 127 | +- **Safe to refactor**: internal helpers like `_build_headers`, `_validate_base_url`, `_compute_backoff` — they are private and well-tested |
| 128 | +- **Do not change**: `parse_error()` status-code mapping without updating all error subclasses and tests |
| 129 | +- **Do not change**: Pydantic `Field(alias=...)` values — they match the daemon's JSON contract |
| 130 | +- **Safe to extend**: add new error subclasses by adding to `_STATUS_TO_ERROR` dict and creating the class |
| 131 | +- **Safe to extend**: add new client methods by following the `_do()` + `with_retry_sync()` pattern |
| 132 | +- **When adding streaming endpoints**: follow the `chat_stream` pattern — build request, send with `stream=True`, check status before returning `StreamReader` |
0 commit comments