Skip to content

Commit 2dbb38f

Browse files
committed
feat(ci): expand AGENTS.md, add jscpd
- Expand AGENTS.md from 44 to 132 lines with naming conventions, API patterns, testing patterns, refactoring guidelines, key files - Add jscpd duplication detection job to CI
1 parent 8d1a116 commit 2dbb38f

2 files changed

Lines changed: 103 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ jobs:
8989
- name: pip-audit
9090
run: pip-audit . --skip-editable
9191

92+
# -------------------------------------------------------------------------
93+
# Duplication detection — jscpd.
94+
# -------------------------------------------------------------------------
95+
jscpd:
96+
name: duplication
97+
runs-on: ubuntu-latest
98+
steps:
99+
- uses: actions/checkout@v4
100+
- uses: actions/setup-node@v4
101+
with:
102+
node-version: '20'
103+
- name: jscpd
104+
run: |
105+
npx jscpd --min-lines 5 --min-tokens 50 --reporters console --blame --exit-code . 2>&1 | head -50
106+
92107
build:
93108
name: build (sdist + wheel)
94109
runs-on: ubuntu-latest

AGENTS.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,91 @@ mypy . # Type check
4242
- `HawkClient` uses context manager (`with` statement) for cleanup
4343
- Discovery scans localhost ports — tests must mock this
4444
- `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)