Skip to content

Commit e101cd6

Browse files
authored
Client, error handling, and streaming hardening pass (#22)
* chore: commit wip changes on feature/wip-20260705 * docs: add architecture documentation to hawk-sdk-python - Add comprehensive architecture.md covering SDK design principles - Document dual client pattern (sync/async) - Document error hierarchy, streaming, and retry logic - Document security practices and ecosystem boundaries - Add key file locations and refactoring guidelines * chore: professionalize Python SDK documentation - Update AGENTS.md with file tree diagram, client API reference, key file locations - Rewrite README.md with features list, expanded quick start, API reference, error handling - Standardize CODE_OF_CONDUCT.md and SECURITY.md across ecosystem * Client, error handling, and streaming hardening pass - Update AGENTS.md to the shared hawk-eco extension-authoring format ---------
1 parent 8c410df commit e101cd6

18 files changed

Lines changed: 1558 additions & 378 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Canonical CI workflow for hawk-eco Python repos.
2-
# Source of truth: .shared-templates/workflows/python-ci.yml.tmpl
2+
# Source of truth: https://github.com/GrayCodeAI/hawk/blob/main/.shared-templates/workflows/python-ci.yml.tmpl
33

44
name: CI
55

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Canonical PyPI publish workflow for hawk-eco Python repos.
22
# Triggered by release-please when it pushes a v* tag.
3-
# Source of truth: .shared-templates/workflows/python-release.yml.tmpl
3+
# Source of truth: https://github.com/GrayCodeAI/hawk/blob/main/.shared-templates/workflows/python-release.yml.tmpl
44
#
55
# Uses PyPI Trusted Publishing (OIDC) — no API tokens stored in GitHub.
66
# Configure once at https://pypi.org/manage/account/publishing/

AGENTS.md

Lines changed: 151 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,168 @@
1-
# AGENTS.md — hawk-sdk-python
1+
---
2+
description: Extending hawk-eco — how to write AGENTS.md files, custom specialists, skills, hooks, MCP servers, and plugins.
3+
globs: "*.go, *.js, *.md, *.json, *.toml, *.yaml, *.yml"
4+
alwaysApply: false
5+
---
26

3-
Python SDK for the Hawk daemon API. Provides an idiomatic Python client for chat, streaming, sessions, and stats.
7+
# Extending hawk-eco
48

5-
## Design Principles
9+
hawk-eco is an open-source code intelligence platform. This document describes how to extend it with custom tools, skills, hooks, and integrations.
610

7-
- **Thin wrapper** — maps directly to the hawk daemon HTTP API
8-
- **Type-hinted** — full type annotations for IDE support
9-
- **Minimal dependencies**`httpx` (HTTP), `pydantic` v2 (models), and
10-
`eval-type-backport` (only on Python < 3.10)
11+
## 1. Drop a project `AGENTS.md`
1112

12-
## Build & Test
13+
When hawk-eco starts in a directory, it looks for project-level instructions and injects them into the system prompt. The lookup walks from your current working directory **up to the nearest git root** and reads the first matching file at each level — general rules at the repo root, more specific rules in sub-trees. Files are labeled with their directory in the prompt (e.g. `## Project guidelines (services/api/AGENTS.md)`).
14+
15+
Accepted file names, in priority order at each level:
16+
17+
| Path | Notes |
18+
| --- | --- |
19+
| `./AGENTS.md` | The classic spot — committed to your repo, shared with the team. |
20+
| `./ZERO.md` | Brand-specific alias. Same format, lower priority. |
21+
| `./.zero/AGENTS.md` | Project-local, hidden, gitignored. Personal notes that stay out of git. |
22+
23+
Matching is **case-insensitive** on the basename, so `AGENTS.md`, `Agents.md`, and `agents.md` resolve to the same file on Windows and macOS. The git-tracked filename in this repo is `AGENTS.md` — keep that on case-sensitive filesystems (Linux, the WSL filesystem, or a CI runner) to match what the loader looks for.
24+
25+
Both files use the same format. YAML frontmatter is optional; the markdown body is loaded as instructions for the agent. hawk-eco reads the file once at session start, so changes take effect on the next launch — not mid-session.
26+
27+
```markdown
28+
# Project conventions for <your project>
29+
30+
- Build with `make`, not `go build` directly.
31+
- Tests live next to the source file (`foo_test.go` next to `foo.go`).
32+
- Run `make lint` before opening a PR.
33+
- Never edit files under `third_party/` — those are vendored.
34+
```
35+
36+
Tips:
37+
38+
- Keep each file under ~8 KiB. hawk-eco caps the **total** across all matched files at 32 KiB; everything past the cap is dropped.
39+
- Re-state rules in the imperative voice: "Run `make lint`", not "you should consider running the linter".
40+
- Don't put secrets, model IDs, or environment-specific paths in `AGENTS.md`. Use config files for those.
41+
- In a monorepo, drop a narrower `AGENTS.md` in each sub-tree (e.g. `services/api/AGENTS.md`). hawk-eco picks those up automatically when you launch from inside the sub-tree.
42+
- A YAML frontmatter block (`---\n...\n---`) at the top is preserved verbatim in the injected prompt but is not parsed for `globs:` or `alwaysApply:` scoping today — keep the body self-contained.
43+
44+
### Personal guidelines, across every project
45+
46+
For preferences that follow *you*, not a specific repo (tone, tooling habits, workflow), drop a `ZERO.md` in your user config directory: `~/.config/hawk-eco/ZERO.md` on Linux/macOS, `%AppData%\Roaming\hawk-eco\ZERO.md` on Windows — the same directory as config files and your personal specialists. Same format and 8 KiB cap as the project files above, and the same case-insensitive basename match.
47+
48+
This file is injected as its own `## User guidelines` section, before the project's `AGENTS.md`/`ZERO.md`, and is labeled as personal preference in the prompt: project guidelines are the later, more specific instruction and take precedence over it when the two conflict.
49+
50+
## 2. Custom specialists
51+
52+
Specialists are hawk-eco's sub-agents. Three scopes, in priority order:
53+
54+
| Scope | Path | Shared? |
55+
| --- | --- | --- |
56+
| Built-in | compiled into hawk-eco | yes |
57+
| User | `~/.config/hawk-eco/specialists/*.md` | no — your machine only |
58+
| Project | `./.zero/specialists/*.md` | yes — the repo team |
59+
60+
Project overrides user overrides built-in when names collide.
61+
62+
A specialist is a markdown manifest with frontmatter and a system prompt:
63+
64+
```markdown
65+
---
66+
description: Reviews API changes for breaking-change risk and missing tests.
67+
tools: read-only,plan
68+
---
69+
70+
You review API changes. For every changed hunk in `internal/api/` or any file
71+
that ends in `_api.go`:
72+
73+
1. Confirm the public signature is backward-compatible, or note the breaking
74+
change explicitly with the migration path.
75+
2. Confirm a corresponding test exists in `internal/api/*_test.go` and that
76+
the new behaviour is exercised.
77+
3. Flag any new exported symbol without a doc comment.
78+
79+
Reply with one JSON object per finding: `{"file", "line", "severity", "message", "fix"}`.
80+
```
81+
82+
CLI management:
83+
84+
```bash
85+
hawk-eco specialist list
86+
hawk-eco specialist show api-reviewer
87+
hawk-eco specialist create api-reviewer \
88+
--project \
89+
--description "Reviews API changes" \
90+
--tools read-only,plan \
91+
--prompt "$(cat api-reviewer.md)"
92+
hawk-eco specialist edit api-reviewer --project
93+
hawk-eco specialist delete api-reviewer --project
94+
hawk-eco specialist path # prints the resolved specialists directory
95+
```
96+
97+
## 3. Skills
98+
99+
Skills are markdown instruction files that extend agent capabilities. They can be:
100+
- Project-scoped: dropped in `./.zero/skills/` or `./skills/`
101+
- User-scoped: dropped in `~/.config/hawk-eco/skills/`
102+
103+
A skill manifest:
104+
105+
```markdown
106+
---
107+
description: How to review Go code for security issues
108+
globs: "*.go"
109+
alwaysApply: true
110+
---
111+
112+
When reviewing Go code for security:
113+
114+
1. Check for SQL injection patterns
115+
2. Verify error handling doesn't expose sensitive data
116+
3. Confirm secrets are not hardcoded
117+
4. Validate input sanitization
118+
```
119+
120+
## 4. Hooks
121+
122+
Hooks allow custom commands to run at specific lifecycle points:
123+
- `beforeReview` — runs before code review starts
124+
- `afterReview` — runs after code review completes
125+
- `sessionStart` — runs at session initialization
126+
- `sessionEnd` — runs at session teardown
13127

14128
```bash
15-
pip install -e ".[dev]" # Install with dev deps
16-
pytest # Run tests
17-
pytest --cov=hawk --cov-report=term-missing # Coverage
18-
ruff check . # Lint
19-
ruff format . # Format
20-
mypy . # Type check
129+
hawk-eco hook add beforeReview --command "lint-check"
130+
hawk-eco hook remove beforeReview
131+
hawk-eco hook list
21132
```
22133

23-
## Architecture
24-
25-
- `hawk/client.py` — Main `HawkClient` class
26-
- `hawk/types.py` — Pydantic models for API responses
27-
- `hawk/errors.py` — Typed error classes (`HawkAPIError`, etc.)
28-
- `hawk/discovery.py` — Auto-discover running hawk daemon
29-
- `hawk/memory_tools.py` — Memory graph operations
30-
- `tests/` — Test suite with mocked HTTP responses
31-
32-
## Conventions
33-
34-
- Python 3.9+
35-
- `ruff` for linting and formatting (enforced in CI)
36-
- Type annotations required on all public APIs
37-
- Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`
38-
- No `Co-authored-by:` trailers
39-
- `pytest` for testing, `respx` for HTTP mocking
40-
41-
## Common Pitfalls
42-
43-
- `HawkClient` uses context manager (`with` statement) for cleanup
44-
- Discovery scans localhost ports — tests must mock this
45-
- `Retry-After: 0` is valid — respect it, don't retry immediately
46-
47-
## Naming Conventions
48-
49-
- **Modules**: snake_case, one concept per file (`client.py`, `errors.py`, `retry.py`, `streaming.py`)
50-
- **Classes**: PascalCase, noun-based (`HawkClient`, `AsyncHawkClient`, `StreamReader`, `RetryConfig`)
51-
- **Error classes**: suffix with `Error` (`HawkAPIError`, `NotFoundError`, `RateLimitError`, `InternalServerError`)
52-
- **Private methods**: leading underscore (`_request`, `_build_headers`, `_validate_base_url`, `_compute_backoff`)
53-
- **Type aliases**: use `TypeVar` for generics (`T = TypeVar("T")` in `types.py` and `retry.py`)
54-
- **Pydantic fields**: use `Field(alias="snake_case")` for API-mapped names (`session_id`, `tokens_in`, `active_sessions`)
55-
- **Test classes**: `Test` prefix + subject (`TestHawkClientSync`, `TestAsyncHawkClient`, `TestParseError`, `TestRetryConfig`)
56-
- **Test methods**: `test_` + behavior (`test_health`, `test_chat_stream_error`, `test_no_retry_on_404`)
57-
58-
## API Patterns
59-
60-
### Pydantic Models
61-
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:
62-
```python
63-
class ChatResponse(BaseModel):
64-
session_id: str = Field(alias="session_id")
65-
tokens_in: int = Field(alias="tokens_in")
66-
model_config = {"populate_by_name": True}
134+
## 5. MCP integration
135+
136+
MCP (Model Context Protocol) servers can expose tools to hawk-eco:
137+
138+
```bash
139+
hawk-eco mcp add --name server --url http://localhost:8080
140+
hawk-eco mcp remove server
141+
hawk-eco mcp list
67142
```
68143

69-
### Error Hierarchy
70-
`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.
144+
## 6. Plugins
71145

72-
### Retry Pattern
73-
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).
146+
Plugins extend hawk-eco with custom tools and capabilities:
74147

75-
### Streaming
76-
`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.
148+
```bash
149+
hawk-eco plugin add --name my-plugin --path ./my-plugin
150+
hawk-eco plugin remove my-plugin
151+
hawk-eco plugin list
152+
```
77153

78-
### Dual Client Pattern
79-
`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`).
154+
## 7. Verification
80155

81-
## Testing Patterns
156+
hawk-eco includes a self-verification system to validate local changes before contributing:
82157

83-
### HTTP Mocking
84-
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"`:
85-
```python
86-
def test_health(self, respx_mock: respx.MockRouter) -> None:
87-
respx_mock.get(f"{BASE_URL}/v1/health").mock(
88-
return_value=httpx.Response(200, json={...})
89-
)
158+
```bash
159+
hawk-eco verify
160+
hawk-eco verify --fix
90161
```
91162

92-
### Async Tests
93-
Async test classes use `@pytest.mark.asyncio` decorator. The `respx_mock` fixture works for both sync and async.
94-
95-
### Error Tests
96-
`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`).
97-
98-
### Streaming Tests
99-
SSE body strings follow the format `"event: content\ndata: Hello\n\n"`. Tests verify both `collect_text()` and mid-stream `break` behavior.
100-
101-
### Retry Tests
102-
`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.
103-
104-
## Key File Locations
105-
106-
| What | Where |
107-
|------|-------|
108-
| Public API exports | `src/hawk/__init__.py` |
109-
| Client (sync + async) | `src/hawk/client.py` |
110-
| Pydantic models | `src/hawk/types.py` |
111-
| Error types + parser | `src/hawk/errors.py` |
112-
| Retry logic | `src/hawk/retry.py` |
113-
| SSE streaming | `src/hawk/streaming.py` |
114-
| Agent abstraction | `src/hawk/agent.py` |
115-
| Tool decorator + loop | `src/hawk/tools.py` |
116-
| Workflow engine | `src/hawk/workflow.py` |
117-
| Agent discovery | `src/hawk/discovery.py` |
118-
| Memory graph ops | `src/hawk/memory_tools.py` |
119-
| Evaluation/benchmarks | `src/hawk/evaluate.py` |
120-
| Tracing/observability | `src/hawk/tracing.py` |
121-
| Client tests | `tests/test_client.py` |
122-
| Error tests | `tests/test_errors.py` |
123-
| Retry tests | `tests/test_retry.py` |
124-
| Streaming tests | `tests/test_streaming.py` |
125-
126-
## Refactoring Guidelines
127-
128-
- **Safe to refactor**: internal helpers like `_build_headers`, `_validate_base_url`, `_compute_backoff` — they are private and well-tested
129-
- **Do not change**: `parse_error()` status-code mapping without updating all error subclasses and tests
130-
- **Do not change**: Pydantic `Field(alias=...)` values — they match the daemon's JSON contract
131-
- **Safe to extend**: add new error subclasses by adding to `_STATUS_TO_ERROR` dict and creating the class
132-
- **Safe to extend**: add new client methods by following the `_do()` + `with_retry_sync()` pattern
133-
- **When adding streaming endpoints**: follow the `chat_stream` pattern — build request, send with `stream=True`, check status before returning `StreamReader`
163+
## Development
164+
165+
```bash
166+
make lint
167+
hawk-eco verify
168+
```

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- **BREAKING: `list_sessions()` now returns `list[SessionSummary]`** and no
12+
longer accepts `limit`/`offset`. The daemon's `GET /v1/sessions` returns a
13+
bare JSON array with no pagination envelope, so the previous
14+
`PaginatedResponse[SessionSummary]` return type could never validate a
15+
real response. `list_messages()` is unchanged — that endpoint is
16+
paginated. Applies to both `HawkClient` and `AsyncHawkClient`.
17+
- **`Message.tool_result` alias fixed** from `tool_result` to
18+
`tool_results` to match the daemon's message payload.
19+
20+
### Removed
21+
- **BREAKING: `create_session()`** on both `HawkClient` and
22+
`AsyncHawkClient`. The daemon has no `POST /v1/sessions` endpoint —
23+
sessions are created implicitly by `POST /v1/chat`. The method always
24+
raised a 404 against a real daemon. Use `chat()`/`chat_stream()` instead.
25+
1026
### Fixed
1127
- **`__version__` now agrees with `pyproject.toml`.** The prior hardening
1228
commit bumped `pyproject.toml` to `0.1.0` but missed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2024 Hawk Contributors
3+
Copyright (c) 2026 GrayCode AI
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

Makefile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Canonical hawk-eco Makefile for Python repos.
2-
# Source of truth: .shared-templates/Makefile.python.tmpl at the eco root.
2+
# Source of truth: https://github.com/GrayCodeAI/hawk/blob/main/.shared-templates/Makefile.python.tmpl
33
# Placeholders rendered per repo: hawk-sdk.
44

55
# ---------------------------------------------------------------------------
@@ -105,5 +105,7 @@ help: ## Show this help.
105105
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
106106

107107
.PHONY: hooks
108-
hooks:
109-
git config core.hooksPath .githooks
108+
hooks: ## Install git hooks via lefthook (format, lint, conventional commits, co-author strip).
109+
@command -v lefthook >/dev/null 2>&1 || (echo "install: go install github.com/evilmartians/lefthook@latest" && exit 1)
110+
git config --unset core.hooksPath 2>/dev/null || true
111+
lefthook install

0 commit comments

Comments
 (0)