diff --git a/CHANGELOG.md b/CHANGELOG.md index 2115d77..4b62452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **"Defaults & divergences across SDKs" README section** documenting how + retry, backoff, jitter, and timeout defaults differ between the Go, + TypeScript, and Python SDKs. + ## [0.2.0] — 2026-07-14 ### Changed diff --git a/README.md b/README.md index 7b0d2f5..bf4aee9 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,23 @@ with HawkClient() as client: print(f"Tool: {call.name}({call.arguments})") ``` +## Defaults & divergences across SDKs + +The three Hawk SDKs (Go, TypeScript, Python) share wire behavior but have +drifted in transport defaults. Actual current values: + +| Default | Python (this SDK) | Go | TypeScript | +| --- | --- | --- | --- | +| Retries | **On** — `retry_config or DEFAULT_RETRY_CONFIG` (`src/hawk/client.py`, both sync and async clients) | **Off** — opt in with `WithRetry(DefaultRetryConfig())` (`client.go`) | **Off** — opt in with `{ retry: defaultRetryConfig() }` (`src/client.ts`) | +| Initial backoff | 0.5s (`src/hawk/retry.py`, `RetryConfig`) | 1s (`retry.go`, `DefaultRetryConfig`) | 1s (`src/retry.ts`, `defaultRetryConfig`) | +| Backoff jitter | Equal + jitter: `backoff + rand(0, backoff/2)` (`src/hawk/retry.py`, `_compute_backoff`) | Full jitter: `rand(0, backoff)` (`retry.go`, `backoffDuration`) | Full jitter: `rand(0, backoff)` (`src/retry.ts`, `backoffDurationMs`) | +| Request timeout | httpx timeout, 30s (`src/hawk/client.py`, `DEFAULT_TIMEOUT`) | `ResponseHeaderTimeout: 5s`, headers only (`client.go`) | Whole-request deadline, 30s, includes retries (`src/client.ts`, `timeoutMs`) | + +Max retries (3), max backoff (30s), retryable statuses (429/500/502/503/504), +and the non-idempotent rule (only 429 is retried for POST `/v1/chat`) are +identical in all three SDKs. This table documents current behavior; it is not +a compatibility contract between the SDKs. + ## API Reference ### HawkClient / AsyncHawkClient diff --git a/api/openapi.yaml b/api/openapi.yaml index e8bf615..4f81c14 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -16,7 +16,7 @@ info: url: https://github.com/GrayCodeAI/hawk servers: - - url: http://localhost:4590 + - url: http://127.0.0.1:4590 description: Local daemon (default port) security: @@ -417,6 +417,7 @@ tags: paths: /v1/health: get: + operationId: healthCheck tags: [system] summary: Health check security: [] @@ -427,9 +428,16 @@ paths: application/json: schema: $ref: "#/components/schemas/HealthResponse" + "400": + description: Invalid request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /v1/ready: get: + operationId: readinessProbe tags: [system] summary: Readiness probe description: | @@ -444,6 +452,12 @@ paths: application/json: schema: $ref: "#/components/schemas/ReadyResponse" + "400": + description: Invalid request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "503": description: Daemon is not ready content: @@ -453,6 +467,7 @@ paths: /v1/chat: post: + operationId: sendChat tags: [agent] summary: Send a prompt to the agent description: | @@ -508,8 +523,63 @@ paths: schema: $ref: "#/components/schemas/Error" + /v1/cancel: + post: + operationId: cancelGeneration + tags: [agent] + summary: Cancel an in-flight generation + description: | + Aborts the active generation for the given session, if one is running. + The per-session generation is otherwise serialized (one at a time); + this endpoint lets a caller stop a long-running response early. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [session_id] + properties: + session_id: + type: string + responses: + "200": + description: Generation cancelled (or completed before the request was processed) + content: + application/json: + schema: + type: object + properties: + cancelled: + type: boolean + "400": + description: Invalid or missing session_id + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: No active generation for the session + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "429": + description: Rate limit exceeded + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /v1/sessions: get: + operationId: listSessions tags: [sessions] summary: List active daemon sessions responses: @@ -530,6 +600,7 @@ paths: /v1/sessions/{id}: get: + operationId: getSession tags: [sessions] summary: Get a persisted session parameters: @@ -552,6 +623,7 @@ paths: schema: $ref: "#/components/schemas/Error" delete: + operationId: deleteSession tags: [sessions] summary: Delete a session parameters: @@ -578,6 +650,7 @@ paths: /v1/sessions/{id}/messages: get: + operationId: listSessionMessages tags: [messages] summary: Get session messages with pagination parameters: @@ -610,6 +683,7 @@ paths: /v1/sessions/{id}/graph: get: + operationId: getSessionGraph tags: [graphs] summary: Project a persisted session as a portable execution graph description: | @@ -676,6 +750,7 @@ paths: /v1/stats: get: + operationId: getStats tags: [stats] summary: Aggregated usage statistics responses: @@ -692,8 +767,35 @@ paths: schema: $ref: "#/components/schemas/Error" + /v1/metrics: + get: + operationId: getMetrics + tags: [stats] + summary: Daemon metrics in Prometheus exposition format + description: | + Returns daemon-level metrics (request counts, concurrency usage, + active sessions) as Prometheus text exposition format. + Use `?format=json` for JSON output. + responses: + "200": + description: Metrics output + content: + text/plain: + schema: + type: string + application/json: + schema: + type: object + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /v1/review: post: + operationId: createReview tags: [review] summary: Trigger an asynchronous code review of a commit requestBody: @@ -718,6 +820,7 @@ paths: /v1/review/status: get: + operationId: getReviewStatus tags: [review] summary: Get current review status responses: @@ -727,6 +830,12 @@ paths: application/json: schema: $ref: "#/components/schemas/ReviewStatusResponse" + "400": + description: Invalid request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "500": description: Status command failed content: diff --git a/tests/test_openapi_coverage.py b/tests/test_openapi_coverage.py index a81fc10..04bfcdf 100644 --- a/tests/test_openapi_coverage.py +++ b/tests/test_openapi_coverage.py @@ -20,5 +20,7 @@ def test_every_daemon_path_has_an_sdk_support_decision() -> None: "/v1/stats": "supported", "/v1/review": "unsupported: asynchronous review orchestration", "/v1/review/status": "unsupported: review worker status", + "/v1/cancel": "unsupported: in-flight request cancellation", + "/v1/metrics": "unsupported: daemon metrics endpoint", } assert paths == sorted(decisions)