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
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Hawk SDK for Go is part of the [hawk-eco](https://github.com/GrayCodeAI/hawk-eco
| **hawk** | AI-powered coding agent for the terminal |
| **hawk-sdk-go** | Go SDK for the Hawk daemon API |
| **hawk-sdk-python** | Python SDK for the Hawk daemon API |
| **hawk-sdk-typescript** | TypeScript SDK for the Hawk daemon API |
| **hawk-core-contracts** | Shared cross-repo contracts (types, events, tools) |

## Architecture
Expand Down Expand Up @@ -106,6 +107,28 @@ for {
}
fmt.Print(event.Data)
}

// Chat with tools — run the tool execution loop
tools := []hawksdk.Tool{
{
Schema: hawksdk.ToolSchema{
Name: "get_weather",
Description: "Get the current weather for a city",
Parameters: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}}}`),
},
Run: func(ctx context.Context, args map[string]any) (string, error) {
city := args["city"].(string)
return fmt.Sprintf("Sunny in %s", city), nil
},
},
}
resp, err = client.ChatWithTools(ctx, hawksdk.ChatRequest{
Prompt: "What's the weather in San Francisco?",
}, tools, 10)
if err != nil {
log.Fatalf("chat with tools failed: %v", err)
}
fmt.Printf("Response: %s\n", resp.Response)
```

## API Reference
Expand All @@ -132,9 +155,11 @@ hawksdk.New(opts ...ClientOption) *Client
| `Health(ctx) (*HealthResponse, error)` | Check daemon health and version |
| `Chat(ctx, req ChatRequest) (*ChatResponse, error)` | Send a chat message |
| `ChatStream(ctx, req ChatRequest) (*StreamReader, error)` | Stream chat responses via SSE |
| `ChatWithTools(ctx, req ChatRequest, tools []Tool, maxRounds int) (*ChatResponse, error)` | Run the tool execution loop |
| `Sessions(ctx) ([]SessionSummary, error)` | List active sessions |
| `Session(ctx, id string) (*SessionDetail, error)` | Get session details |
| `Messages(ctx, sessionID, opts *ListOptions) (*PaginatedResponse[Message], error)` | Get paginated messages |
| `Graph(ctx, sessionID, opts *GraphOptions) (*GraphExport, error)` | Get and validate a privacy-safe session execution graph |
| `DeleteSession(ctx, id string) error` | Delete a session |
| `Stats(ctx) (*StatsResponse, error)` | Get aggregated usage stats |

Expand All @@ -144,11 +169,28 @@ hawksdk.New(opts ...ClientOption) *Client
|------|-------------|
| `ChatRequest` | Request body for `/v1/chat` |
| `ChatResponse` | Response from `/v1/chat` |
| `ChatWithToolsRequest` | Request with tool definitions for `/v1/chat` |
| `ChatWithToolsResponse` | Response with tool call information |
| `HealthResponse` | Response from `/v1/health` |
| `SessionSummary` | Session listing entry |
| `SessionDetail` | Full session information |
| `Message` | Conversation message with tool calls |
| `StatsResponse` | Aggregated usage statistics |
| `Tool` | Callable tool with schema and execution function |
| `ToolSchema` | Tool function signature in OpenAI format |
| `ToolCall` | Tool invocation requested by the model |
| `ToolResult` | Result of executing a tool call |
| `Agent` | Higher-level agent with conversation management |
| `Workflow` | Workflow engine types |
| `StreamReader` | SSE stream reader for streaming responses |
| `GraphExport` | Validated, data-only `*.graph/v1` portable projection |

`GraphExport.Validate()` checks the shared six node kinds, six relationship
kinds, five lifecycle event types, unique identities, provenance, timestamps,
and self-contained topology. `Client.Graph` retrieves the authenticated
`/v1/sessions/{id}/graph` projection and validates it before returning. The SDK
consumes graph projections but does not publish authoritative facts or
implement graph storage.

### Error Types

Expand All @@ -171,7 +213,7 @@ type RetryConfig struct {
InitialBackoff time.Duration
MaxBackoff time.Duration
BackoffMultiplier float64
RetryableStatusCodes []int
RetryableStatuses []int
}
```

Expand Down
305 changes: 305 additions & 0 deletions a2a/protocol.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
// Package a2a implements the Agent-to-Agent (A2A) protocol for
// cross-framework agent communication.
package a2a

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

// A2AProtocol implements the Agent-to-Agent protocol.
type A2AProtocol struct {
baseURL string
client *http.Client
agentCard *AgentCard
}

// AgentCard describes an agent's capabilities (A2A spec).
type AgentCard struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
URL string `json:"url"`
Provider *Provider `json:"provider,omitempty"`
Version string `json:"version"`
Capabilities *Capabilities `json:"capabilities"`
Skills []Skill `json:"skills"`
DefaultInputModes []string `json:"defaultInputModes,omitempty"`
DefaultOutputModes []string `json:"defaultOutputModes,omitempty"`
}

// Provider represents the agent provider.
type Provider struct {
URL string `json:"url,omitempty"`
}

// Capabilities represents what the agent can do.
type Capabilities struct {
Streaming bool `json:"streaming"`
PushNotifications bool `json:"pushNotifications"`
StateTransition bool `json:"stateTransition"`
Extensions []Extension `json:"extensions,omitempty"`
}

// Extension represents an extension capability.
type Extension struct {
URI string `json:"uri"`
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
}

// Skill represents a skill the agent has.
type Skill struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags,omitempty"`
Examples []string `json:"examples,omitempty"`
}

// Task represents an A2A task.
type Task struct {
ID string `json:"id"`
Type string `json:"type"`
Status *TaskStatus `json:"status"`
History []Message `json:"history,omitempty"`
Artifacts []Artifact `json:"artifacts,omitempty"`
Requirements []Requirement `json:"requirements,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// TaskStatus represents the status of a task.
type TaskStatus struct {
State string `json:"state"` // "submitted", "working", "finished", "failed", "rejected"
Message *Message `json:"message,omitempty"`
Timestamp time.Time `json:"timestamp"`
}

// Message represents a message in A2A.
type Message struct {
Role string `json:"role"` // "user", "agent"
Parts []Part `json:"parts"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// Part represents a part of a message.
type Part struct {
Type string `json:"type"` // "text", "data", "file"
Text string `json:"text,omitempty"`
Data interface{} `json:"data,omitempty"`
File *File `json:"file,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// Artifact represents an artifact produced by an agent.
type Artifact struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Parts []Part `json:"parts"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// Requirement represents a requirement for the task.
type Requirement struct {
Name string `json:"name"`
Type string `json:"type"`
Value interface{} `json:"value,omitempty"`
}

// File represents a file in A2A.
type File struct {
Name string `json:"name"`
MIMEType string `json:"mimeType"`
URI string `json:"uri,omitempty"`
Bytes string `json:"bytes,omitempty"`
}

// NewA2AProtocol creates a new A2A protocol client.
func NewA2AProtocol(baseURL string) *A2AProtocol {
return &A2AProtocol{
baseURL: baseURL,
client: &http.Client{Timeout: 30 * time.Second},
}
}

// GetAgentCard retrieves the agent card.
func (a *A2AProtocol) GetAgentCard(ctx context.Context) (*AgentCard, error) {
req, err := http.NewRequestWithContext(ctx, "GET", a.baseURL+"/.well-known/agent.json", nil)
if err != nil {
return nil, err
}

resp, err := a.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get agent card: %s", resp.Status)
}

var card AgentCard
if err := json.NewDecoder(resp.Body).Decode(&card); err != nil {
return nil, err
}

a.agentCard = &card
return &card, nil
}

// SendTask sends a task to the agent.
func (a *A2AProtocol) SendTask(ctx context.Context, task *Task) (*Task, error) {
reqBody, err := json.Marshal(map[string]interface{}{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": task,
"id": task.ID,
})
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, "POST", a.baseURL, bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")

resp, err := a.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to send task: %s - %s", resp.Status, string(body))
}

var result struct {
Result *Task `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}

return result.Result, nil
}

// SendTaskStreaming sends a task and streams the response.
func (a *A2AProtocol) SendTaskStreaming(ctx context.Context, task *Task) (<-chan *Task, error) {
reqBody, err := json.Marshal(map[string]interface{}{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": task,
"id": task.ID,
})
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, "POST", a.baseURL, bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")

// nolint:bodyclose
resp, err := a.client.Do(req)
if err != nil {
return nil, err
}

ch := make(chan *Task, 100)

go func() {
defer close(ch)
defer resp.Body.Close()

decoder := json.NewDecoder(resp.Body)
for {
var task *Task
if err := decoder.Decode(&task); err != nil {
if err == io.EOF {
break
}
continue
}
ch <- task
}
}()

return ch, nil
}

// GetTask retrieves a task by ID.
func (a *A2AProtocol) GetTask(ctx context.Context, taskID string) (*Task, error) {
reqBody, err := json.Marshal(map[string]interface{}{
"jsonrpc": "2.0",
"method": "tasks/get",
"params": map[string]string{"id": taskID},
"id": taskID,
})
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, "POST", a.baseURL, bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")

resp, err := a.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var result struct {
Result *Task `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}

return result.Result, nil
}

// CancelTask cancels a task by ID.
func (a *A2AProtocol) CancelTask(ctx context.Context, taskID string) error {
reqBody, err := json.Marshal(map[string]interface{}{
"jsonrpc": "2.0",
"method": "tasks/cancel",
"params": map[string]string{"id": taskID},
"id": taskID,
})
if err != nil {
return err
}

req, err := http.NewRequestWithContext(ctx, "POST", a.baseURL, bytes.NewReader(reqBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")

resp, err := a.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

return nil
}

// AgentCard returns the cached agent card.
func (a *A2AProtocol) AgentCard() *AgentCard {
return a.agentCard
}
Loading
Loading