Official Python client for the Hawk daemon API
The Hawk SDK for Python is the official client for interacting with the Hawk daemon API. It provides an idiomatic Python interface for chat, streaming, sessions, stats, and agent orchestration. Both synchronous and async clients are provided, with the same API surface on each.
- Dual client pattern -
HawkClient(sync) andAsyncHawkClientwith identical method signatures - Streaming - SSE-based real-time chat streaming via
StreamReader/AsyncStreamReader - Type safety - Pydantic v2 models with full type hints (Python 3.9+)
- Context managers -
with HawkClient() as client:for automatic resource cleanup - Typed errors - Status-code-based exception hierarchy (
AuthenticationError,NotFoundError,RateLimitError, etc.) - Retry with backoff - Configurable exponential backoff with jitter via
RetryConfig - Minimal dependencies -
httpx(HTTP),pydanticv2, andeval-type-backport(Python < 3.10) - Agent orchestration -
Agent/AsyncAgentwith conversation memory, planning, and tool loops - Toolkit -
Toolkitfor grouping and managing collections of tools - Tracing - OpenTelemetry-compatible tracing with
trace,trace_chat, andtrace_tool - Workflow engine -
Workflow/AsyncWorkflowfor multi-step agent pipelines - Planning -
Plan/PlanNotebookfor task breakdown and tracking - Evaluation -
EvalTaskandrun_benchmarkfor benchmarking agent performance
⚠️ Pre-release — not yet published to PyPI.
hawk-sdkis under active development and has not yet been published to the Python Package Index. Installing from PyPI will fail until a stable release is tagged. In the meantime, install directly from source:git clone https://github.com/GrayCodeAI/hawk-sdk-python.git cd hawk-sdk-python pip install -e .Once the first stable release is tagged, the standard install will work:
pip install hawk-sdk # available after first stable release
fromhawkimportHawkClientwithHawkClient() asclient:
# Health checkhealth=client.health()
print(f"Status: {health.status}, Version: {health.version}")
print(f"Active sessions: {health.active_sessions}")
# Chatresponse=client.chat("Explain decorators in Python")
print(response.response)
print(f"Tokens: in={response.tokens_in}, out={response.tokens_out}")importasynciofromhawkimportAsyncHawkClientasyncdefmain():
asyncwithAsyncHawkClient() asclient:
health=awaitclient.health()
print(f"Status: {health.status}, Version: {health.version}")
response=awaitclient.chat("Hello!")
print(response.response)
asyncio.run(main())fromhawkimportHawkClientwithHawkClient() asclient:
withclient.chat_stream("Write a haiku about coding") asstream:
foreventinstream.events():
ifevent.eventisNoneorevent.event=="content":
print(event.data, end="", flush=True)
print()
# Or collect all text at oncewithclient.chat_stream("Write a haiku about coding") asstream:
text=stream.collect_text()
print(f"Full response: {text}")
# Or collect tool callswithclient.chat_stream("What tools do I have?", tools=[{"type": "list"}]) asstream:
calls=stream.collect_tool_calls()
forcallincalls:
print(f"Tool: {call.name}({call.arguments})")Both clients share the same constructor and method signatures.
HawkClient(
base_url: str="http://127.0.0.1:4590",
api_key: str|None=None,
retry_config: RetryConfig|None=None,
timeout: float=30.0,
pool_connections: int=10,
pool_maxsize: int=100,
)| Parameter | Default | Description |
|---|---|---|
base_url | http://127.0.0.1:4590 | Base URL of the Hawk daemon |
api_key | None | Bearer token for authenticated requests |
retry_config | DEFAULT_RETRY_CONFIG | Retry configuration (429, 5xx, etc.) |
timeout | 30.0 | HTTP request timeout in seconds |
pool_connections | 10 | Max keep-alive connections |
pool_maxsize | 100 | Max total connections |
# Healthhealth() ->HealthResponse# Chatchat(
prompt: str,
session_id: str|None=None,
model: str|None=None,
max_turns: int|None=None,
autonomy: str|None=None,
cwd: str|None=None,
agent: str|None=None,
tools: list[dict[str, Any]] |None=None,
tool_results: list[ToolResult] |None=None,
tool_choice: str|None=None,
parallel_tool_calls: bool|None=None,
) ->ChatResponse# Streaming chatchat_stream(
prompt: str,
session_id: str|None=None,
model: str|None=None,
max_turns: int|None=None,
autonomy: str|None=None,
cwd: str|None=None,
agent: str|None=None,
tools: list[dict[str, Any]] |None=None,
tool_results: list[ToolResult] |None=None,
tool_choice: str|None=None,
parallel_tool_calls: bool|None=None,
) ->StreamReader# or AsyncStreamReader# Session managementget_session(session_id: str) ->SessionDetaillist_sessions() ->list[SessionSummary]
delete_session(session_id: str) ->None# Messages with paginationlist_messages(
session_id: str,
limit: int=50,
offset: int=0,
) ->PaginatedResponse[Message]
# Privacy-safe execution graph (same method on sync and async clients)get_graph(
session_id: str,
repository: str|None=None,
trace_checkpoints: list[str] |None=None,
) ->GraphExport# Statsstats() ->StatsResponseGraphExport, GraphNode, GraphEdge, and GraphEvent model the shared
*.graph/v1 wire contract. Pydantic validates vocabulary, timestamps,
provenance, duplicate IDs, and dangling topology at the SDK boundary:
fromhawkimportGraphExportgraph=GraphExport.model_validate(payload)get_graph() retrieves the authenticated /v1/sessions/{id}/graph projection
and validates it with these models. They remain data-only consumer models; the
SDK does not own graph facts or storage.
chat_with_tools (and its async counterpart chat_with_tools_async) implements
the tool-use loop: it sends a chat request, checks for tool calls in the response,
executes matching tools, appends results to the conversation, and repeats until
either no more tool calls are requested or max_rounds is reached.
fromhawkimportHawkClient, Tool, chat_with_toolsdefget_weather(location: str) ->str:
returnf"Sunny in {location}"tools= [
Tool(
name="get_weather",
description="Get the weather",
parameters={
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
fn=get_weather,
)
]
withHawkClient() asclient:
response=chat_with_tools(
client,
prompt="What's the weather in San Francisco?",
tools=tools,
max_rounds=10,
)
print(response.response)All API errors inherit from HawkAPIError. Catch specific subclasses:
fromhawkimportHawkClient, HawkAPIError, RateLimitError, AuthenticationErrorwithHawkClient() asclient:
try:
response=client.chat("Hello")
exceptAuthenticationError:
print("Invalid API key")
exceptRateLimitErrorase:
print(f"Rate limited, retry after {e.retry_after}s")
exceptHawkAPIErrorase:
print(f"API error {e.status_code}: {e.message}")Error classes: BadRequestError (400), AuthenticationError (401), ForbiddenError (403),
NotFoundError (404), RateLimitError (429), InternalServerError (500),
ServiceUnavailableError (503).
Every public method wraps its logic in a retry closure that automatically:
- Respects
Retry-Afterheaders on 429 responses - Uses exponential backoff with jitter for retryable statuses (429, 500, 502, 503, 504)
- Raises typed exceptions matching the HTTP status code
hawk-sdk-python is a consumer of Hawk public APIs and contracts. It does not
import from engine repos such as eyrie, yaad, tok, trace, sight, or
inspect. If a capability is needed across repos, it should be exposed through
Hawk or hawk-core-contracts, not through engine internals.
Contributions are welcome — please read CONTRIBUTING.md before opening a pull request.
MIT - see LICENSE for details.