Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cc30d87
fix(tools,types,examples): fix dead tool loop, add tool_calls to Chat…
Patel230 Jul 23, 2026
7f46228
fix(tracing): add opentelemetry optional extra, fix docstring claims
Patel230 Jul 23, 2026
710ba5c
feat(agent): add system_prompt to AgentConfig, wire into chat kwargs
Patel230 Jul 23, 2026
b6f4dd3
fix(evaluate): consult EvalTask.expected_tools against response tool_…
Patel230 Jul 23, 2026
c9f8f05
feat(sdk): add tool_choice field to ChatRequest and AgentConfig
Patel230 Jul 23, 2026
38db571
docs: document tool_choice and parallel_tool_calls in README method s…
Patel230 Jul 23, 2026
143ecda
docs: document chat_with_tools, Agent, Toolkit, tracing, workflow, an…
Patel230 Jul 23, 2026
80e21b0
fix(sdk-python): close stream response on early break; harden tool lo…
Patel230 Jul 24, 2026
54e581b
fix: export HawkConnectionError from public API; add error class tests
Patel230 Jul 24, 2026
e9a1971
fix: sort __all__ alphabetically, add CompositeResolver, fix import o…
Patel230 Jul 24, 2026
f3030e8
feat: add graph module
Patel230 Jul 25, 2026
7f4b7b3
feat: add graph query DSL
Patel230 Jul 25, 2026
8d07de5
feat: add context engineering and test harness
Patel230 Jul 25, 2026
55f948b
test: add test suites for context optimizer, test harness, and graph DSL
Patel230 Jul 25, 2026
b809fcb
fix: resolve ruff lint and mypy strict type-check failures
Patel230 Jul 25, 2026
50748fe
fix: resolve ruff lint issues in test files
Patel230 Jul 25, 2026
2ec4f7f
fix: ruff format compliance
Patel230 Jul 25, 2026
eca5c7a
fix: ruff format compliance for all files
Patel230 Jul 25, 2026
cd8c854
fix: ruff format remaining files
Patel230 Jul 25, 2026
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
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ the same API surface on each.
- **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), `pydantic` v2, and `eval-type-backport` (Python < 3.10)
- **Agent orchestration** - `Agent` / `AsyncAgent` with conversation memory, planning, and tool loops
- **Toolkit** - `Toolkit` for grouping and managing collections of tools
- **Tracing** - OpenTelemetry-compatible tracing with `trace`, `trace_chat`, and `trace_tool`
- **Workflow engine** - `Workflow` / `AsyncWorkflow` for multi-step agent pipelines
- **Planning** - `Plan` / `PlanNotebook` for task breakdown and tracking
- **Evaluation** - `EvalTask` and `run_benchmark` for benchmarking agent performance

## Installation

Expand Down Expand Up @@ -72,6 +78,7 @@ with HawkClient() as client:
import asyncio
from hawk import AsyncHawkClient


async def main():
async with AsyncHawkClient() as client:
health = await client.health()
Expand All @@ -80,6 +87,7 @@ async def main():
response = await client.chat("Hello!")
print(response.response)


asyncio.run(main())
```

Expand Down Expand Up @@ -152,6 +160,8 @@ chat(
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 chat
Expand All @@ -165,6 +175,8 @@ chat_stream(
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 management
Expand All @@ -179,10 +191,71 @@ list_messages(
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

# Stats
stats() -> StatsResponse
```

### Portable graph models

`GraphExport`, `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:

```python
from hawk import GraphExport

graph = 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.

### Tool Execution Loop

`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.

```python
from hawk import HawkClient, Tool, chat_with_tools


def get_weather(location: str) -> str:
return f"Sunny in {location}"


tools = [
Tool(
name="get_weather",
description="Get the weather",
parameters={
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
fn=get_weather,
)
]

with HawkClient() as client:
response = chat_with_tools(
client,
prompt="What's the weather in San Francisco?",
tools=tools,
max_rounds=10,
)
print(response.response)
```

### Typed Errors

All API errors inherit from `HawkAPIError`. Catch specific subclasses:
Expand Down
149 changes: 149 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,106 @@ components:
tool_use: {}
tool_results: {}

GraphScope:
type: object
properties:
tenant_id: {type: string}
project_id: {type: string}
repository_id: {type: string}

GraphRef:
type: object
required: [kind, id]
properties:
kind:
type: string
enum: [system, knowledge, execution, policy, quality, operations]
id: {type: string}

GraphProvenance:
type: object
required: [producer]
properties:
producer: {type: string}
version: {type: string}
source_id: {type: string}
evidence:
type: array
items:
type: object
required: [uri]
properties:
uri: {type: string}
digest: {type: string}
media_type: {type: string}

GraphNode:
type: object
required: [id, kind, created_at, provenance]
properties:
id: {type: string}
kind:
type: string
enum: [system, knowledge, execution, policy, quality, operations]
scope: {$ref: "#/components/schemas/GraphScope"}
created_at: {type: string, format: date-time}
effective_at: {type: string, format: date-time}
provenance: {$ref: "#/components/schemas/GraphProvenance"}
attributes:
type: object
additionalProperties: {type: string}

GraphEdge:
type: object
required: [id, kind, from, to, created_at, provenance]
properties:
id: {type: string}
kind:
type: string
enum: [contains, depends_on, references, produced, governed_by, validated_by]
from: {$ref: "#/components/schemas/GraphRef"}
to: {$ref: "#/components/schemas/GraphRef"}
scope: {$ref: "#/components/schemas/GraphScope"}
created_at: {type: string, format: date-time}
effective_at: {type: string, format: date-time}
provenance: {$ref: "#/components/schemas/GraphProvenance"}
attributes:
type: object
additionalProperties: {type: string}

GraphEvent:
type: object
required: [id, type, subject, occurred_at, provenance]
properties:
id: {type: string}
type:
type: string
enum: [created, updated, transitioned, observed, deleted]
subject: {$ref: "#/components/schemas/GraphRef"}
scope: {$ref: "#/components/schemas/GraphScope"}
occurred_at: {type: string, format: date-time}
correlation_id: {type: string}
causation_id: {type: string}
idempotency_key: {type: string}
provenance: {$ref: "#/components/schemas/GraphProvenance"}

ExecutionGraph:
type: object
required: [schema_version, generated_at, scope, nodes, edges, events]
properties:
schema_version: {type: string, enum: [hawk.graph/v1]}
generated_at: {type: string, format: date-time}
scope: {$ref: "#/components/schemas/GraphScope"}
nodes:
type: array
items: {$ref: "#/components/schemas/GraphNode"}
edges:
type: array
items: {$ref: "#/components/schemas/GraphEdge"}
events:
type: array
items: {$ref: "#/components/schemas/GraphEvent"}

PaginatedMessages:
type: object
properties:
Expand Down Expand Up @@ -259,6 +359,8 @@ tags:
description: Session management
- name: messages
description: Message history
- name: graphs
description: Portable read-only execution graph projections
- name: stats
description: Usage statistics
- name: review
Expand Down Expand Up @@ -458,6 +560,53 @@ paths:
schema:
$ref: "#/components/schemas/Error"

/v1/sessions/{id}/graph:
get:
tags: [graphs]
summary: Project a persisted session as a portable execution graph
parameters:
- name: id
in: path
required: true
schema: {type: string, maxLength: 128, pattern: '^[A-Za-z0-9._-]+$'}
- name: repository
in: query
schema: {type: string, maxLength: 256}
- name: trace_checkpoint
in: query
schema:
type: array
maxItems: 64
items: {type: string, pattern: '^[0-9a-f]{12}$'}
style: form
explode: true
responses:
"200":
description: Portable execution graph
content:
application/json:
schema: {$ref: "#/components/schemas/ExecutionGraph"}
"400":
description: Invalid graph request
content:
application/json:
schema: {$ref: "#/components/schemas/Error"}
"401":
description: Unauthorized
content:
application/json:
schema: {$ref: "#/components/schemas/Error"}
"404":
description: Session not found
content:
application/json:
schema: {$ref: "#/components/schemas/Error"}
"503":
description: Graph projection unavailable
content:
application/json:
schema: {$ref: "#/components/schemas/Error"}

/v1/stats:
get:
tags: [stats]
Expand Down
5 changes: 3 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ abstractions, no leaked internals.
```python
# client.py — direct HTTP calls with httpx
class HawkClient:
def _request(self, method: str, path: str, **kwargs) -> Response:
...
def _request(self, method: str, path: str, **kwargs) -> Response: ...
def health(self) -> HealthResponse:
return self._request("GET", "/v1/health")

def chat(self, prompt: str) -> ChatResponse:
return self._request("POST", "/v1/chat", json={"prompt": prompt})
```
Expand Down Expand Up @@ -82,6 +82,7 @@ class HawkClient:
def chat(self, prompt: str) -> ChatResponse: ...
def chat_stream(self, prompt: str) -> StreamReader: ...


class AsyncHawkClient:
# async methods
async def health(self) -> HealthResponse: ...
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def main():
with HawkClient() as client:
# Health check
health = client.health()
print(f"Hawk daemon: version={health.version}, sessions={health.sessions}")
print(f"Hawk daemon: version={health.version}, sessions={health.active_sessions}")

# Chat
response = client.chat("Explain what a decorator is in Python")
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ dependencies = [
]

[project.optional-dependencies]
tracing = [
"opentelemetry-api>=1.20.0",
"opentelemetry-sdk>=1.20.0",
"opentelemetry-exporter-otlp>=1.20.0",
]
dev = [
"pytest>=7.0",
"pytest-asyncio>=0.21",
"pytest-cov>=4.0",
"respx>=0.21",
"ruff>=0.4.0",
"mypy>=1.0,<2",
Expand Down
Loading
Loading