Skip to content

Commit 502cbe2

Browse files
committed
Merge main
2 parents 4ff2f69 + cd8c854 commit 502cbe2

27 files changed

Lines changed: 2035 additions & 87 deletions

README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ the same API surface on each.
2626
- **Typed errors** - Status-code-based exception hierarchy (`AuthenticationError`, `NotFoundError`, `RateLimitError`, etc.)
2727
- **Retry with backoff** - Configurable exponential backoff with jitter via `RetryConfig`
2828
- **Minimal dependencies** - `httpx` (HTTP), `pydantic` v2, and `eval-type-backport` (Python < 3.10)
29+
- **Agent orchestration** - `Agent` / `AsyncAgent` with conversation memory, planning, and tool loops
30+
- **Toolkit** - `Toolkit` for grouping and managing collections of tools
31+
- **Tracing** - OpenTelemetry-compatible tracing with `trace`, `trace_chat`, and `trace_tool`
32+
- **Workflow engine** - `Workflow` / `AsyncWorkflow` for multi-step agent pipelines
33+
- **Planning** - `Plan` / `PlanNotebook` for task breakdown and tracking
34+
- **Evaluation** - `EvalTask` and `run_benchmark` for benchmarking agent performance
2935

3036
## Installation
3137

@@ -72,6 +78,7 @@ with HawkClient() as client:
7278
import asyncio
7379
from hawk import AsyncHawkClient
7480

81+
7582
async def main():
7683
async with AsyncHawkClient() as client:
7784
health = await client.health()
@@ -80,6 +87,7 @@ async def main():
8087
response = await client.chat("Hello!")
8188
print(response.response)
8289

90+
8391
asyncio.run(main())
8492
```
8593

@@ -152,6 +160,8 @@ chat(
152160
agent: str | None = None,
153161
tools: list[dict[str, Any]] | None = None,
154162
tool_results: list[ToolResult] | None = None,
163+
tool_choice: str | None = None,
164+
parallel_tool_calls: bool | None = None,
155165
) -> ChatResponse
156166

157167
# Streaming chat
@@ -165,6 +175,8 @@ chat_stream(
165175
agent: str | None = None,
166176
tools: list[dict[str, Any]] | None = None,
167177
tool_results: list[ToolResult] | None = None,
178+
tool_choice: str | None = None,
179+
parallel_tool_calls: bool | None = None,
168180
) -> StreamReader # or AsyncStreamReader
169181

170182
# Session management
@@ -179,10 +191,71 @@ list_messages(
179191
offset: int = 0,
180192
) -> PaginatedResponse[Message]
181193

194+
# Privacy-safe execution graph (same method on sync and async clients)
195+
get_graph(
196+
session_id: str,
197+
repository: str | None = None,
198+
trace_checkpoints: list[str] | None = None,
199+
) -> GraphExport
200+
182201
# Stats
183202
stats() -> StatsResponse
184203
```
185204

205+
### Portable graph models
206+
207+
`GraphExport`, `GraphNode`, `GraphEdge`, and `GraphEvent` model the shared
208+
`*.graph/v1` wire contract. Pydantic validates vocabulary, timestamps,
209+
provenance, duplicate IDs, and dangling topology at the SDK boundary:
210+
211+
```python
212+
from hawk import GraphExport
213+
214+
graph = GraphExport.model_validate(payload)
215+
```
216+
217+
`get_graph()` retrieves the authenticated `/v1/sessions/{id}/graph` projection
218+
and validates it with these models. They remain data-only consumer models; the
219+
SDK does not own graph facts or storage.
220+
221+
### Tool Execution Loop
222+
223+
`chat_with_tools` (and its async counterpart `chat_with_tools_async`) implements
224+
the tool-use loop: it sends a chat request, checks for tool calls in the response,
225+
executes matching tools, appends results to the conversation, and repeats until
226+
either no more tool calls are requested or `max_rounds` is reached.
227+
228+
```python
229+
from hawk import HawkClient, Tool, chat_with_tools
230+
231+
232+
def get_weather(location: str) -> str:
233+
return f"Sunny in {location}"
234+
235+
236+
tools = [
237+
Tool(
238+
name="get_weather",
239+
description="Get the weather",
240+
parameters={
241+
"type": "object",
242+
"properties": {"location": {"type": "string"}},
243+
"required": ["location"],
244+
},
245+
fn=get_weather,
246+
)
247+
]
248+
249+
with HawkClient() as client:
250+
response = chat_with_tools(
251+
client,
252+
prompt="What's the weather in San Francisco?",
253+
tools=tools,
254+
max_rounds=10,
255+
)
256+
print(response.response)
257+
```
258+
186259
### Typed Errors
187260

188261
All API errors inherit from `HawkAPIError`. Catch specific subclasses:

api/openapi.yaml

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,106 @@ components:
136136
tool_use: {}
137137
tool_results: {}
138138

139+
GraphScope:
140+
type: object
141+
properties:
142+
tenant_id: {type: string}
143+
project_id: {type: string}
144+
repository_id: {type: string}
145+
146+
GraphRef:
147+
type: object
148+
required: [kind, id]
149+
properties:
150+
kind:
151+
type: string
152+
enum: [system, knowledge, execution, policy, quality, operations]
153+
id: {type: string}
154+
155+
GraphProvenance:
156+
type: object
157+
required: [producer]
158+
properties:
159+
producer: {type: string}
160+
version: {type: string}
161+
source_id: {type: string}
162+
evidence:
163+
type: array
164+
items:
165+
type: object
166+
required: [uri]
167+
properties:
168+
uri: {type: string}
169+
digest: {type: string}
170+
media_type: {type: string}
171+
172+
GraphNode:
173+
type: object
174+
required: [id, kind, created_at, provenance]
175+
properties:
176+
id: {type: string}
177+
kind:
178+
type: string
179+
enum: [system, knowledge, execution, policy, quality, operations]
180+
scope: {$ref: "#/components/schemas/GraphScope"}
181+
created_at: {type: string, format: date-time}
182+
effective_at: {type: string, format: date-time}
183+
provenance: {$ref: "#/components/schemas/GraphProvenance"}
184+
attributes:
185+
type: object
186+
additionalProperties: {type: string}
187+
188+
GraphEdge:
189+
type: object
190+
required: [id, kind, from, to, created_at, provenance]
191+
properties:
192+
id: {type: string}
193+
kind:
194+
type: string
195+
enum: [contains, depends_on, references, produced, governed_by, validated_by]
196+
from: {$ref: "#/components/schemas/GraphRef"}
197+
to: {$ref: "#/components/schemas/GraphRef"}
198+
scope: {$ref: "#/components/schemas/GraphScope"}
199+
created_at: {type: string, format: date-time}
200+
effective_at: {type: string, format: date-time}
201+
provenance: {$ref: "#/components/schemas/GraphProvenance"}
202+
attributes:
203+
type: object
204+
additionalProperties: {type: string}
205+
206+
GraphEvent:
207+
type: object
208+
required: [id, type, subject, occurred_at, provenance]
209+
properties:
210+
id: {type: string}
211+
type:
212+
type: string
213+
enum: [created, updated, transitioned, observed, deleted]
214+
subject: {$ref: "#/components/schemas/GraphRef"}
215+
scope: {$ref: "#/components/schemas/GraphScope"}
216+
occurred_at: {type: string, format: date-time}
217+
correlation_id: {type: string}
218+
causation_id: {type: string}
219+
idempotency_key: {type: string}
220+
provenance: {$ref: "#/components/schemas/GraphProvenance"}
221+
222+
ExecutionGraph:
223+
type: object
224+
required: [schema_version, generated_at, scope, nodes, edges, events]
225+
properties:
226+
schema_version: {type: string, enum: [hawk.graph/v1]}
227+
generated_at: {type: string, format: date-time}
228+
scope: {$ref: "#/components/schemas/GraphScope"}
229+
nodes:
230+
type: array
231+
items: {$ref: "#/components/schemas/GraphNode"}
232+
edges:
233+
type: array
234+
items: {$ref: "#/components/schemas/GraphEdge"}
235+
events:
236+
type: array
237+
items: {$ref: "#/components/schemas/GraphEvent"}
238+
139239
PaginatedMessages:
140240
type: object
141241
properties:
@@ -259,6 +359,8 @@ tags:
259359
description: Session management
260360
- name: messages
261361
description: Message history
362+
- name: graphs
363+
description: Portable read-only execution graph projections
262364
- name: stats
263365
description: Usage statistics
264366
- name: review
@@ -458,6 +560,53 @@ paths:
458560
schema:
459561
$ref: "#/components/schemas/Error"
460562

563+
/v1/sessions/{id}/graph:
564+
get:
565+
tags: [graphs]
566+
summary: Project a persisted session as a portable execution graph
567+
parameters:
568+
- name: id
569+
in: path
570+
required: true
571+
schema: {type: string, maxLength: 128, pattern: '^[A-Za-z0-9._-]+$'}
572+
- name: repository
573+
in: query
574+
schema: {type: string, maxLength: 256}
575+
- name: trace_checkpoint
576+
in: query
577+
schema:
578+
type: array
579+
maxItems: 64
580+
items: {type: string, pattern: '^[0-9a-f]{12}$'}
581+
style: form
582+
explode: true
583+
responses:
584+
"200":
585+
description: Portable execution graph
586+
content:
587+
application/json:
588+
schema: {$ref: "#/components/schemas/ExecutionGraph"}
589+
"400":
590+
description: Invalid graph request
591+
content:
592+
application/json:
593+
schema: {$ref: "#/components/schemas/Error"}
594+
"401":
595+
description: Unauthorized
596+
content:
597+
application/json:
598+
schema: {$ref: "#/components/schemas/Error"}
599+
"404":
600+
description: Session not found
601+
content:
602+
application/json:
603+
schema: {$ref: "#/components/schemas/Error"}
604+
"503":
605+
description: Graph projection unavailable
606+
content:
607+
application/json:
608+
schema: {$ref: "#/components/schemas/Error"}
609+
461610
/v1/stats:
462611
get:
463612
tags: [stats]

docs/architecture.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ abstractions, no leaked internals.
4747
```python
4848
# client.py — direct HTTP calls with httpx
4949
class HawkClient:
50-
def _request(self, method: str, path: str, **kwargs) -> Response:
51-
...
50+
def _request(self, method: str, path: str, **kwargs) -> Response: ...
5251
def health(self) -> HealthResponse:
5352
return self._request("GET", "/v1/health")
53+
5454
def chat(self, prompt: str) -> ChatResponse:
5555
return self._request("POST", "/v1/chat", json={"prompt": prompt})
5656
```
@@ -82,6 +82,7 @@ class HawkClient:
8282
def chat(self, prompt: str) -> ChatResponse: ...
8383
def chat_stream(self, prompt: str) -> StreamReader: ...
8484

85+
8586
class AsyncHawkClient:
8687
# async methods
8788
async def health(self) -> HealthResponse: ...

examples/basic/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def main():
88
with HawkClient() as client:
99
# Health check
1010
health = client.health()
11-
print(f"Hawk daemon: version={health.version}, sessions={health.sessions}")
11+
print(f"Hawk daemon: version={health.version}, sessions={health.active_sessions}")
1212

1313
# Chat
1414
response = client.chat("Explain what a decorator is in Python")

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@ dependencies = [
3535
]
3636

3737
[project.optional-dependencies]
38+
tracing = [
39+
"opentelemetry-api>=1.20.0",
40+
"opentelemetry-sdk>=1.20.0",
41+
"opentelemetry-exporter-otlp>=1.20.0",
42+
]
3843
dev = [
3944
"pytest>=7.0",
4045
"pytest-asyncio>=0.21",
46+
"pytest-cov>=4.0",
4147
"respx>=0.21",
4248
"ruff>=0.4.0",
4349
"mypy>=1.0,<2",

0 commit comments

Comments
 (0)