Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Canonical CI workflow for hawk-eco Python repos.
# Source of truth: .shared-templates/workflows/python-ci.yml.tmpl
# Source of truth: https://github.com/GrayCodeAI/hawk/blob/main/.shared-templates/workflows/python-ci.yml.tmpl

name: CI

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Canonical PyPI publish workflow for hawk-eco Python repos.
# Triggered by release-please when it pushes a v* tag.
# Source of truth: .shared-templates/workflows/python-release.yml.tmpl
# Source of truth: https://github.com/GrayCodeAI/hawk/blob/main/.shared-templates/workflows/python-release.yml.tmpl
#
# Uses PyPI Trusted Publishing (OIDC) — no API tokens stored in GitHub.
# Configure once at https://pypi.org/manage/account/publishing/
Expand Down
267 changes: 151 additions & 116 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,133 +1,168 @@
# AGENTS.md — hawk-sdk-python
---
description: Extending hawk-eco — how to write AGENTS.md files, custom specialists, skills, hooks, MCP servers, and plugins.
globs: "*.go, *.js, *.md, *.json, *.toml, *.yaml, *.yml"
alwaysApply: false
---

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

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

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

## Build & Test
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)`).

Accepted file names, in priority order at each level:

| Path | Notes |
| --- | --- |
| `./AGENTS.md` | The classic spot — committed to your repo, shared with the team. |
| `./ZERO.md` | Brand-specific alias. Same format, lower priority. |
| `./.zero/AGENTS.md` | Project-local, hidden, gitignored. Personal notes that stay out of git. |

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.

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.

```markdown
# Project conventions for <your project>

- Build with `make`, not `go build` directly.
- Tests live next to the source file (`foo_test.go` next to `foo.go`).
- Run `make lint` before opening a PR.
- Never edit files under `third_party/` — those are vendored.
```

Tips:

- Keep each file under ~8 KiB. hawk-eco caps the **total** across all matched files at 32 KiB; everything past the cap is dropped.
- Re-state rules in the imperative voice: "Run `make lint`", not "you should consider running the linter".
- Don't put secrets, model IDs, or environment-specific paths in `AGENTS.md`. Use config files for those.
- 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.
- 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.

### Personal guidelines, across every project

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.

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.

## 2. Custom specialists

Specialists are hawk-eco's sub-agents. Three scopes, in priority order:

| Scope | Path | Shared? |
| --- | --- | --- |
| Built-in | compiled into hawk-eco | yes |
| User | `~/.config/hawk-eco/specialists/*.md` | no — your machine only |
| Project | `./.zero/specialists/*.md` | yes — the repo team |

Project overrides user overrides built-in when names collide.

A specialist is a markdown manifest with frontmatter and a system prompt:

```markdown
---
description: Reviews API changes for breaking-change risk and missing tests.
tools: read-only,plan
---

You review API changes. For every changed hunk in `internal/api/` or any file
that ends in `_api.go`:

1. Confirm the public signature is backward-compatible, or note the breaking
change explicitly with the migration path.
2. Confirm a corresponding test exists in `internal/api/*_test.go` and that
the new behaviour is exercised.
3. Flag any new exported symbol without a doc comment.

Reply with one JSON object per finding: `{"file", "line", "severity", "message", "fix"}`.
```

CLI management:

```bash
hawk-eco specialist list
hawk-eco specialist show api-reviewer
hawk-eco specialist create api-reviewer \
--project \
--description "Reviews API changes" \
--tools read-only,plan \
--prompt "$(cat api-reviewer.md)"
hawk-eco specialist edit api-reviewer --project
hawk-eco specialist delete api-reviewer --project
hawk-eco specialist path # prints the resolved specialists directory
```

## 3. Skills

Skills are markdown instruction files that extend agent capabilities. They can be:
- Project-scoped: dropped in `./.zero/skills/` or `./skills/`
- User-scoped: dropped in `~/.config/hawk-eco/skills/`

A skill manifest:

```markdown
---
description: How to review Go code for security issues
globs: "*.go"
alwaysApply: true
---

When reviewing Go code for security:

1. Check for SQL injection patterns
2. Verify error handling doesn't expose sensitive data
3. Confirm secrets are not hardcoded
4. Validate input sanitization
```

## 4. Hooks

Hooks allow custom commands to run at specific lifecycle points:
- `beforeReview` — runs before code review starts
- `afterReview` — runs after code review completes
- `sessionStart` — runs at session initialization
- `sessionEnd` — runs at session teardown

```bash
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
hawk-eco hook add beforeReview --command "lint-check"
hawk-eco hook remove beforeReview
hawk-eco hook list
```

## Architecture

- `hawk/client.py` — Main `HawkClient` class
- `hawk/types.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.9+
- `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, `respx` 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:
```python
class ChatResponse(BaseModel):
session_id: str = Field(alias="session_id")
tokens_in: int = Field(alias="tokens_in")
model_config = {"populate_by_name": True}
## 5. MCP integration

MCP (Model Context Protocol) servers can expose tools to hawk-eco:

```bash
hawk-eco mcp add --name server --url http://localhost:8080
hawk-eco mcp remove server
hawk-eco mcp list
```

### 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.
## 6. Plugins

### 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).
Plugins extend hawk-eco with custom tools and capabilities:

### 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.
```bash
hawk-eco plugin add --name my-plugin --path ./my-plugin
hawk-eco plugin remove my-plugin
hawk-eco plugin list
```

### 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`).
## 7. Verification

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

### 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"`:
```python
def test_health(self, respx_mock: respx.MockRouter) -> None:
respx_mock.get(f"{BASE_URL}/v1/health").mock(
return_value=httpx.Response(200, json={...})
)
```bash
hawk-eco verify
hawk-eco verify --fix
```

### 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`
## Development

```bash
make lint
hawk-eco verify
```
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- **BREAKING: `list_sessions()` now returns `list[SessionSummary]`** and no
longer accepts `limit`/`offset`. The daemon's `GET /v1/sessions` returns a
bare JSON array with no pagination envelope, so the previous
`PaginatedResponse[SessionSummary]` return type could never validate a
real response. `list_messages()` is unchanged — that endpoint is
paginated. Applies to both `HawkClient` and `AsyncHawkClient`.
- **`Message.tool_result` alias fixed** from `tool_result` to
`tool_results` to match the daemon's message payload.

### Removed
- **BREAKING: `create_session()`** on both `HawkClient` and
`AsyncHawkClient`. The daemon has no `POST /v1/sessions` endpoint —
sessions are created implicitly by `POST /v1/chat`. The method always
raised a 404 against a real daemon. Use `chat()`/`chat_stream()` instead.

### Fixed
- **`__version__` now agrees with `pyproject.toml`.** The prior hardening
commit bumped `pyproject.toml` to `0.1.0` but missed
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Hawk Contributors
Copyright (c) 2026 GrayCode AI

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Canonical hawk-eco Makefile for Python repos.
# Source of truth: .shared-templates/Makefile.python.tmpl at the eco root.
# Source of truth: https://github.com/GrayCodeAI/hawk/blob/main/.shared-templates/Makefile.python.tmpl
# Placeholders rendered per repo: hawk-sdk.

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

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