Skip to content

Commit d0d57cd

Browse files
committed
chore: restore AGENTS.md — project instructions for AI agents
1 parent 3972c24 commit d0d57cd

1 file changed

Lines changed: 132 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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

Comments
 (0)