diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ddfd0bfd9..a2250eae8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -89,4 +89,35 @@ jobs:
- name: Run tests
working-directory: typescript-sdk
- run: pnpm run test
\ No newline at end of file
+ run: pnpm run test
+
+ go:
+ name: Go SDK Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.24.4'
+
+ - name: Setup Go module cache
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ - name: Download dependencies
+ working-directory: sdks/community/go
+ run: go mod download
+
+ - name: Run tests
+ working-directory: sdks/community/go
+ run: go test ./... -v
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 93c700e85..f858afff4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
.vscode/
.idea/
+
+.DS_Store
+**/.DS_Store
\ No newline at end of file
diff --git a/docs/sdk/go/client/overview.mdx b/docs/sdk/go/client/overview.mdx
new file mode 100644
index 000000000..03bc5f250
--- /dev/null
+++ b/docs/sdk/go/client/overview.mdx
@@ -0,0 +1,135 @@
+---
+title: "Overview"
+description: "Client package overview for Go SDK"
+---
+
+# Client Package
+
+The AG-UI Go SDK client package provides Server-Sent Events (SSE) connectivity for real-time event streaming from AG-UI agents. This package enables Go applications to connect to agent endpoints and receive streaming responses.
+
+## Package Import
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/client/sse"
+```
+
+## SSE Client Introduction
+
+The SSE (Server-Sent Events) client provides a robust connection mechanism for streaming events from AG-UI agents. SSE is a standard protocol for server-to-client streaming over HTTP, making it ideal for real-time agent interactions where the agent needs to send continuous updates about its processing state, thoughts, and outputs.
+
+Key features:
+- Real-time event streaming from agents
+- Automatic connection management
+- Context-based cancellation support
+- Configurable timeouts and buffer sizes
+- Built-in authentication support
+
+## Configuration
+
+The client is highly configurable to adapt to different deployment scenarios:
+
+```go
+client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+ ConnectTimeout: 30 * time.Second,
+ ReadTimeout: 5 * time.Minute,
+ BufferSize: 100,
+})
+
+// Start streaming with context and payload
+frames, errors, err := client.Stream(sse.StreamOptions{
+ Context: context.Background(),
+ Payload: map[string]interface{}{
+ "threadId": "thread_123",
+ "messages": []interface{}{
+ map[string]string{
+ "role": "user",
+ "content": "Hello, agent!",
+ },
+ },
+ },
+})
+```
+
+## Quick Example
+
+Here's a complete example showing how to connect to an agent and process events:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/client/sse"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+)
+
+func main() {
+ // Create SSE client
+ client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+ })
+
+ // Start streaming
+ ctx := context.Background()
+ frames, errors, err := client.Stream(sse.StreamOptions{
+ Context: ctx,
+ Payload: map[string]interface{}{
+ "threadId": "thread_123",
+ "messages": []interface{}{
+ map[string]string{
+ "role": "user",
+ "content": "What is the weather today?",
+ },
+ },
+ },
+ })
+
+ if err != nil {
+ log.Fatal("Failed to start stream:", err)
+ }
+
+ // Process events
+ decoder := events.NewEventDecoder()
+ for {
+ select {
+ case frame := <-frames:
+ event, err := decoder.Decode(frame.Data)
+ if err != nil {
+ log.Printf("Decode error: %v", err)
+ continue
+ }
+
+ // Handle different event types
+ switch e := event.(type) {
+ case *events.TextMessageContentEvent:
+ fmt.Print(e.Delta)
+ case *events.RunFinishedEvent:
+ fmt.Println("\nAgent finished processing")
+ return
+ }
+
+ case err := <-errors:
+ log.Printf("Stream error: %v", err)
+ return
+ }
+ }
+}
+```
+
+## Navigation
+
+
+ Detailed documentation for the Server-Sent Events client including configuration, streaming, and error handling
+
\ No newline at end of file
diff --git a/docs/sdk/go/client/sse-client.mdx b/docs/sdk/go/client/sse-client.mdx
new file mode 100644
index 000000000..4b63afd5d
--- /dev/null
+++ b/docs/sdk/go/client/sse-client.mdx
@@ -0,0 +1,417 @@
+---
+title: "SSE Client"
+description: "Server-Sent Events client for connecting to AG-UI agents"
+---
+
+# SSE Client
+
+The SSE Client provides real-time streaming connectivity to AG-UI agents using Server-Sent Events (SSE). It handles connection management, authentication, and event streaming with built-in error handling and timeout configuration.
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/client/sse"
+```
+
+## Configuration
+
+The SSE client is configured using a `Config` struct with the following fields:
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `Endpoint` | `string` | Required | The agent endpoint URL |
+| `APIKey` | `string` | Optional | API key for authentication |
+| `AuthHeader` | `string` | `"Authorization"` | Header name for authentication |
+| `AuthScheme` | `string` | `"Bearer"` | Authentication scheme (used with Authorization header) |
+| `ConnectTimeout` | `time.Duration` | `30s` | Timeout for establishing connection |
+| `ReadTimeout` | `time.Duration` | `5m` | Timeout for reading from stream |
+| `BufferSize` | `int` | `100` | Size of the frame channel buffer |
+| `Logger` | `*logrus.Logger` | New logger | Logger instance for debugging |
+
+## Creating a Client
+
+Initialize a new SSE client with your configuration:
+
+```go
+client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+})
+```
+
+With custom configuration:
+
+```go
+client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+ AuthHeader: "X-API-Key",
+ ConnectTimeout: 60 * time.Second,
+ ReadTimeout: 10 * time.Minute,
+ BufferSize: 200,
+ Logger: logrus.New(),
+})
+```
+
+## Streaming
+
+The `Stream` method establishes an SSE connection and returns channels for receiving frames and errors.
+
+### StreamOptions
+
+Configure the stream with `StreamOptions`:
+
+```go
+type StreamOptions struct {
+ Context context.Context // Context for cancellation
+ Payload interface{} // Request payload (will be JSON encoded)
+ Headers map[string]string // Additional HTTP headers
+}
+```
+
+### Stream Method
+
+```go
+frames, errors, err := client.Stream(sse.StreamOptions{
+ Context: context.Background(),
+ Payload: map[string]interface{}{
+ "threadId": "thread_123",
+ "messages": []map[string]interface{}{
+ {
+ "role": "user",
+ "content": "Hello, agent!",
+ },
+ },
+ },
+})
+```
+
+The method returns:
+- `frames <-chan Frame`: Channel for receiving SSE frames
+- `errors <-chan error`: Channel for receiving stream errors
+- `err error`: Immediate error if connection fails
+
+## Frame Processing
+
+The `Frame` struct contains SSE data and metadata:
+
+```go
+type Frame struct {
+ Data []byte // Raw SSE data
+ Timestamp time.Time // Timestamp when frame was received
+}
+```
+
+Process frames using the event decoder:
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+
+decoder := events.NewEventDecoder()
+
+for {
+ select {
+ case frame := <-frames:
+ if frame.Data == nil {
+ // Stream ended
+ return
+ }
+
+ event, err := decoder.Decode(frame.Data)
+ if err != nil {
+ log.Printf("decode error: %v", err)
+ continue
+ }
+
+ switch e := event.(type) {
+ case *events.TextMessageStartEvent:
+ fmt.Printf("Message started: %s\n", e.MessageID)
+ case *events.TextMessageContentEvent:
+ fmt.Printf("Content: %s\n", e.Delta)
+ case *events.TextMessageEndEvent:
+ fmt.Printf("Message ended: %s\n", e.MessageID)
+ }
+
+ case err := <-errors:
+ if err != nil {
+ log.Printf("stream error: %v", err)
+ return
+ }
+ }
+}
+```
+
+## Connection Management
+
+### Context Cancellation
+
+Use context for graceful shutdown:
+
+```go
+ctx, cancel := context.WithCancel(context.Background())
+defer cancel()
+
+frames, errors, err := client.Stream(sse.StreamOptions{
+ Context: ctx,
+ Payload: payload,
+})
+
+// Cancel the stream when needed
+cancel()
+```
+
+### Timeouts
+
+The client automatically manages timeouts:
+- `ConnectTimeout`: Applied when establishing the initial connection
+- `ReadTimeout`: Applied to each read operation from the stream
+
+### Graceful Shutdown
+
+Close the client to clean up resources:
+
+```go
+defer client.Close()
+```
+
+## Authentication
+
+The client supports multiple authentication methods:
+
+### Bearer Token (Default)
+
+```go
+client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+ // Uses Authorization: Bearer your-api-key
+})
+```
+
+### Custom Authentication Scheme
+
+```go
+client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+ AuthScheme: "Token",
+ // Uses Authorization: Token your-api-key
+})
+```
+
+### Custom Header
+
+```go
+client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+ AuthHeader: "X-API-Key",
+ // Uses X-API-Key: your-api-key
+})
+```
+
+### Additional Headers
+
+Pass custom headers via StreamOptions:
+
+```go
+frames, errors, err := client.Stream(sse.StreamOptions{
+ Context: ctx,
+ Payload: payload,
+ Headers: map[string]string{
+ "X-Request-ID": "req_123",
+ "X-Session-ID": "session_456",
+ },
+})
+```
+
+## Error Handling
+
+The client provides errors through the error channel during streaming:
+
+```go
+for {
+ select {
+ case frame := <-frames:
+ // Process frame
+
+ case err := <-errors:
+ if err != nil {
+ // Handle error based on type
+ if strings.Contains(err.Error(), "timeout") {
+ // Handle timeout
+ log.Println("Stream timeout, reconnecting...")
+ // Implement reconnection logic
+ } else if strings.Contains(err.Error(), "read error") {
+ // Handle read error
+ log.Printf("Read error: %v", err)
+ return
+ }
+ }
+ }
+}
+```
+
+### Reconnection Pattern
+
+Implement automatic reconnection for resilient streaming:
+
+```go
+func streamWithReconnect(client *sse.Client, opts sse.StreamOptions) {
+ maxRetries := 5
+ retryDelay := time.Second
+
+ for attempt := 0; attempt < maxRetries; attempt++ {
+ if attempt > 0 {
+ log.Printf("Reconnecting... (attempt %d/%d)", attempt+1, maxRetries)
+ time.Sleep(retryDelay)
+ retryDelay *= 2 // Exponential backoff
+ }
+
+ frames, errors, err := client.Stream(opts)
+ if err != nil {
+ log.Printf("Connection failed: %v", err)
+ continue
+ }
+
+ // Process frames
+ for {
+ select {
+ case frame := <-frames:
+ if frame.Data == nil {
+ // Stream ended, reconnect
+ break
+ }
+ // Process frame
+
+ case err := <-errors:
+ if err != nil {
+ log.Printf("Stream error: %v", err)
+ break
+ }
+
+ case <-opts.Context.Done():
+ log.Println("Context cancelled, stopping")
+ return
+ }
+ }
+ }
+
+ log.Printf("Max retries exceeded, giving up")
+}
+```
+
+## Complete Example
+
+Here's a complete example demonstrating the SSE client:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/client/sse"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+)
+
+func main() {
+ // Create SSE client
+ client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+ })
+ defer client.Close()
+
+ // Create context with timeout
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ defer cancel()
+
+ // Start streaming
+ frames, errors, err := client.Stream(sse.StreamOptions{
+ Context: ctx,
+ Payload: map[string]interface{}{
+ "threadId": events.NewThreadID(),
+ "runId": events.NewRunID(),
+ "messages": []map[string]interface{}{
+ {
+ "role": "user",
+ "content": "What's the weather like?",
+ },
+ },
+ },
+ })
+
+ if err != nil {
+ log.Fatalf("Failed to start stream: %v", err)
+ }
+
+ // Create event decoder
+ decoder := events.NewEventDecoder()
+
+ // Process stream
+ for {
+ select {
+ case frame := <-frames:
+ if frame.Data == nil {
+ fmt.Println("Stream completed")
+ return
+ }
+
+ event, err := decoder.Decode(frame.Data)
+ if err != nil {
+ log.Printf("Decode error: %v", err)
+ continue
+ }
+
+ // Handle different event types
+ switch e := event.(type) {
+ case *events.RunStartedEvent:
+ fmt.Printf("Run started: %s\n", e.RunID)
+
+ case *events.TextMessageStartEvent:
+ fmt.Printf("\nAssistant: ")
+
+ case *events.TextMessageContentEvent:
+ fmt.Print(e.Delta)
+
+ case *events.TextMessageEndEvent:
+ fmt.Println()
+
+ case *events.ToolCallStartEvent:
+ fmt.Printf("Tool call: %s\n", e.FunctionName)
+
+ case *events.ToolCallResultEvent:
+ fmt.Printf("Tool result: %v\n", e.Result)
+
+ case *events.RunFinishedEvent:
+ fmt.Printf("Run completed: %s\n", e.RunID)
+ return
+
+ case *events.RunErrorEvent:
+ fmt.Printf("Run error: %v\n", e.Error)
+ return
+ }
+
+ case err := <-errors:
+ if err != nil {
+ log.Printf("Stream error: %v", err)
+ return
+ }
+
+ case <-ctx.Done():
+ fmt.Println("Context timeout")
+ return
+ }
+ }
+}
+```
+
+This example demonstrates:
+- Client creation and configuration
+- Context-based timeout management
+- Stream initialization with payload
+- Event decoding and type-based handling
+- Error handling
+- Graceful shutdown
\ No newline at end of file
diff --git a/docs/sdk/go/core/events.mdx b/docs/sdk/go/core/events.mdx
new file mode 100644
index 000000000..fd8ac5334
--- /dev/null
+++ b/docs/sdk/go/core/events.mdx
@@ -0,0 +1,632 @@
+---
+title: "Events"
+description: "Documentation for the events used in the Go AG-UI Protocol SDK"
+---
+
+# Events
+
+The AG-UI Protocol SDK uses a streaming event-based architecture. Events are the fundamental units of communication between agents and the frontend. This section documents the event types and their properties in the Go SDK.
+
+## EventType Constants
+
+The `EventType` constants define all possible event types in the system:
+
+```go
+type EventType string
+
+const (
+ EventTypeTextMessageStart EventType = "TEXT_MESSAGE_START"
+ EventTypeTextMessageContent EventType = "TEXT_MESSAGE_CONTENT"
+ EventTypeTextMessageEnd EventType = "TEXT_MESSAGE_END"
+ EventTypeTextMessageChunk EventType = "TEXT_MESSAGE_CHUNK"
+ EventTypeToolCallStart EventType = "TOOL_CALL_START"
+ EventTypeToolCallArgs EventType = "TOOL_CALL_ARGS"
+ EventTypeToolCallEnd EventType = "TOOL_CALL_END"
+ EventTypeToolCallChunk EventType = "TOOL_CALL_CHUNK"
+ EventTypeToolCallResult EventType = "TOOL_CALL_RESULT"
+ EventTypeStateSnapshot EventType = "STATE_SNAPSHOT"
+ EventTypeStateDelta EventType = "STATE_DELTA"
+ EventTypeMessagesSnapshot EventType = "MESSAGES_SNAPSHOT"
+ EventTypeRaw EventType = "RAW"
+ EventTypeCustom EventType = "CUSTOM"
+ EventTypeRunStarted EventType = "RUN_STARTED"
+ EventTypeRunFinished EventType = "RUN_FINISHED"
+ EventTypeRunError EventType = "RUN_ERROR"
+ EventTypeStepStarted EventType = "STEP_STARTED"
+ EventTypeStepFinished EventType = "STEP_FINISHED"
+
+ // Thinking events for reasoning phase support
+ EventTypeThinkingStart EventType = "THINKING_START"
+ EventTypeThinkingEnd EventType = "THINKING_END"
+ EventTypeThinkingTextMessageStart EventType = "THINKING_TEXT_MESSAGE_START"
+ EventTypeThinkingTextMessageContent EventType = "THINKING_TEXT_MESSAGE_CONTENT"
+ EventTypeThinkingTextMessageEnd EventType = "THINKING_TEXT_MESSAGE_END"
+)
+```
+
+## Event Interface
+
+All events implement the `Event` interface, which provides a common contract for event handling:
+
+```go
+type Event interface {
+ // Type returns the event type
+ Type() EventType
+
+ // Timestamp returns the event timestamp (Unix milliseconds)
+ Timestamp() *int64
+
+ // SetTimestamp sets the event timestamp
+ SetTimestamp(timestamp int64)
+
+ // ThreadID returns the thread ID associated with this event
+ ThreadID() string
+
+ // RunID returns the run ID associated with this event
+ RunID() string
+
+ // Validate validates the event structure and content
+ Validate() error
+
+ // ToJSON serializes the event to JSON for cross-SDK compatibility
+ ToJSON() ([]byte, error)
+
+ // GetBaseEvent returns the underlying base event
+ GetBaseEvent() *BaseEvent
+}
+```
+
+## BaseEvent
+
+All events embed the `BaseEvent` struct, which provides common fields and functionality:
+
+```go
+type BaseEvent struct {
+ EventType EventType `json:"type"`
+ TimestampMs *int64 `json:"timestamp,omitempty"`
+ RawEvent any `json:"rawEvent,omitempty"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `EventType` | `EventType` | The type of event (discriminator field) |
+| `TimestampMs` | `*int64` | Timestamp when the event was created (Unix milliseconds) |
+| `RawEvent` | `any` | Original event data if this event was transformed |
+
+### Creating a Base Event
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+
+// Create a new base event with automatic timestamp
+baseEvent := events.NewBaseEvent(events.EventTypeCustom)
+```
+
+## Text Message Events
+
+These events handle streaming text message content from agents.
+
+### TextMessageStartEvent
+
+Signals the start of a streaming text message.
+
+```go
+type TextMessageStartEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ Role *string `json:"role,omitempty"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `MessageID` | `string` | Unique identifier for the message |
+| `Role` | `*string` | Role of the message sender (e.g., "assistant", "user") |
+
+**Usage Example:**
+
+```go
+// Create a text message start event
+event := events.NewTextMessageStartEvent("msg-123", events.WithRole("assistant"))
+
+// Handle the event
+switch e := event.(type) {
+case *events.TextMessageStartEvent:
+ fmt.Printf("Message started: %s with role: %s\n", e.MessageID, *e.Role)
+}
+```
+
+### TextMessageContentEvent
+
+Contains a piece of streaming text message content.
+
+```go
+type TextMessageContentEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ Delta string `json:"delta"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `MessageID` | `string` | ID of the message this content belongs to |
+| `Delta` | `string` | The text content chunk |
+
+**Usage Example:**
+
+```go
+// Create a content event
+event := events.NewTextMessageContentEvent("msg-123", "Hello, world!")
+
+// Handle streaming content
+switch e := event.(type) {
+case *events.TextMessageContentEvent:
+ fmt.Print(e.Delta) // Stream the content to output
+}
+```
+
+### TextMessageEndEvent
+
+Signals the end of a streaming text message.
+
+```go
+type TextMessageEndEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `MessageID` | `string` | ID of the message that has ended |
+
+**Usage Example:**
+
+```go
+// Create an end event
+event := events.NewTextMessageEndEvent("msg-123")
+
+// Handle message completion
+switch e := event.(type) {
+case *events.TextMessageEndEvent:
+ fmt.Printf("\nMessage %s completed\n", e.MessageID)
+}
+```
+
+## Tool Call Events
+
+These events handle tool/function calls made by the agent.
+
+### ToolCallStartEvent
+
+Signals the start of a tool call.
+
+```go
+type ToolCallStartEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+ ToolCallName string `json:"toolCallName"`
+ ParentMessageID *string `json:"parentMessageId,omitempty"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `ToolCallID` | `string` | Unique identifier for the tool call |
+| `ToolCallName` | `string` | Name of the tool being called |
+| `ParentMessageID` | `*string` | ID of the parent message (if applicable) |
+
+**Usage Example:**
+
+```go
+// Create a tool call start event
+event := events.NewToolCallStartEvent(
+ "tool-456",
+ "calculate",
+ events.WithParentMessageID("msg-123"),
+)
+
+// Handle tool call initiation
+switch e := event.(type) {
+case *events.ToolCallStartEvent:
+ fmt.Printf("Tool %s started: %s\n", e.ToolCallName, e.ToolCallID)
+}
+```
+
+### ToolCallArgsEvent
+
+Contains streaming tool call arguments.
+
+```go
+type ToolCallArgsEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+ Delta string `json:"delta"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `ToolCallID` | `string` | ID of the tool call |
+| `Delta` | `string` | JSON string chunk of the arguments |
+
+**Usage Example:**
+
+```go
+// Create a tool call args event
+event := events.NewToolCallArgsEvent("tool-456", `{"x": 10, "y": 20}`)
+
+// Accumulate streaming arguments
+var argsBuffer strings.Builder
+switch e := event.(type) {
+case *events.ToolCallArgsEvent:
+ argsBuffer.WriteString(e.Delta)
+}
+```
+
+### ToolCallEndEvent
+
+Signals the end of a tool call.
+
+```go
+type ToolCallEndEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `ToolCallID` | `string` | ID of the tool call that has ended |
+
+### ToolCallResultEvent
+
+Contains the result of a tool call execution.
+
+```go
+type ToolCallResultEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ ToolCallID string `json:"toolCallId"`
+ Content string `json:"content"`
+ Role *string `json:"role,omitempty"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `MessageID` | `string` | ID of the result message |
+| `ToolCallID` | `string` | ID of the tool call |
+| `Content` | `string` | Result content from the tool |
+| `Role` | `*string` | Role (typically "tool") |
+
+## Run Lifecycle Events
+
+These events track the lifecycle of agent runs.
+
+### RunStartedEvent
+
+Signals the start of an agent run.
+
+```go
+type RunStartedEvent struct {
+ *BaseEvent
+ ThreadIDValue string `json:"threadId"`
+ RunIDValue string `json:"runId"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `ThreadIDValue` | `string` | ID of the conversation thread |
+| `RunIDValue` | `string` | ID of the agent run |
+
+**Usage Example:**
+
+```go
+// Create a run started event
+event := events.NewRunStartedEvent("thread-789", "run-012")
+
+// Handle run initiation
+switch e := event.(type) {
+case *events.RunStartedEvent:
+ fmt.Printf("Run %s started in thread %s\n", e.RunID(), e.ThreadID())
+}
+```
+
+### RunFinishedEvent
+
+Signals the successful completion of an agent run.
+
+```go
+type RunFinishedEvent struct {
+ *BaseEvent
+ ThreadIDValue string `json:"threadId"`
+ RunIDValue string `json:"runId"`
+ Result interface{} `json:"result,omitempty"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `ThreadIDValue` | `string` | ID of the conversation thread |
+| `RunIDValue` | `string` | ID of the agent run |
+| `Result` | `interface{}` | Result data from the agent run |
+
+### RunErrorEvent
+
+Signals an error during an agent run.
+
+```go
+type RunErrorEvent struct {
+ *BaseEvent
+ Code *string `json:"code,omitempty"`
+ Message string `json:"message"`
+ RunIDValue string `json:"runId,omitempty"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `Code` | `*string` | Error code |
+| `Message` | `string` | Error message |
+| `RunIDValue` | `string` | ID of the run that encountered the error |
+
+**Usage Example:**
+
+```go
+// Create an error event
+event := events.NewRunErrorEvent(
+ "Tool execution failed",
+ events.WithErrorCode("TOOL_ERROR"),
+ events.WithRunID("run-012"),
+)
+```
+
+## State Management Events
+
+These events handle state synchronization between agent and client.
+
+### StateSnapshotEvent
+
+Contains a complete snapshot of the state.
+
+```go
+type StateSnapshotEvent struct {
+ *BaseEvent
+ Snapshot any `json:"snapshot"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `Snapshot` | `any` | Complete state snapshot |
+
+**Usage Example:**
+
+```go
+// Create a state snapshot
+state := map[string]interface{}{
+ "currentStep": "processing",
+ "progress": 75,
+}
+event := events.NewStateSnapshotEvent(state)
+```
+
+### StateDeltaEvent
+
+Contains incremental state changes using JSON Patch operations (RFC 6902).
+
+```go
+type StateDeltaEvent struct {
+ *BaseEvent
+ Delta []JSONPatchOperation `json:"delta"`
+}
+
+type JSONPatchOperation struct {
+ Op string `json:"op"` // "add", "remove", "replace", "move", "copy", "test"
+ Path string `json:"path"` // JSON Pointer path
+ Value any `json:"value,omitempty"` // Value for add, replace, test operations
+ From string `json:"from,omitempty"` // Source path for move, copy operations
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `Delta` | `[]JSONPatchOperation` | Array of JSON Patch operations |
+
+**Usage Example:**
+
+```go
+// Create state delta with JSON Patch operations
+delta := []events.JSONPatchOperation{
+ {
+ Op: "replace",
+ Path: "/progress",
+ Value: 100,
+ },
+ {
+ Op: "add",
+ Path: "/completedAt",
+ Value: time.Now().Unix(),
+ },
+}
+event := events.NewStateDeltaEvent(delta)
+```
+
+## Thinking Events
+
+These events support reasoning/thinking phases where the agent shows its thought process.
+
+### ThinkingStartEvent
+
+Signals the start of a thinking phase.
+
+```go
+type ThinkingStartEvent struct {
+ *BaseEvent
+ Title *string `json:"title,omitempty"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `Title` | `*string` | Optional title for the thinking phase |
+
+### ThinkingTextMessageContentEvent
+
+Contains streaming thinking content.
+
+```go
+type ThinkingTextMessageContentEvent struct {
+ *BaseEvent
+ Delta string `json:"delta"`
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `Delta` | `string` | Thinking content chunk |
+
+## Event Decoding
+
+The SDK provides an `EventDecoder` for parsing SSE events into typed Go structs:
+
+```go
+import (
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/sirupsen/logrus"
+)
+
+// Create a decoder
+logger := logrus.New()
+decoder := events.NewEventDecoder(logger)
+
+// Decode an SSE event
+event, err := decoder.DecodeEvent("TEXT_MESSAGE_START", sseData)
+if err != nil {
+ log.Printf("Failed to decode event: %v", err)
+ return
+}
+
+// Type-safe event handling
+switch e := event.(type) {
+case *events.TextMessageStartEvent:
+ fmt.Printf("Message started: %s\n", e.MessageID)
+case *events.TextMessageContentEvent:
+ fmt.Printf("Content: %s\n", e.Delta)
+case *events.ToolCallStartEvent:
+ fmt.Printf("Tool call: %s\n", e.ToolCallName)
+}
+```
+
+## Event Validation
+
+All events support validation to ensure they conform to the protocol specification:
+
+### Individual Event Validation
+
+```go
+event := events.NewTextMessageStartEvent("")
+
+// Validate the event
+if err := event.Validate(); err != nil {
+ // Handle validation error
+ fmt.Printf("Invalid event: %v\n", err)
+}
+```
+
+### Sequence Validation
+
+The SDK can validate entire event sequences to ensure they follow the protocol rules:
+
+```go
+// Validate a sequence of events
+events := []events.Event{
+ events.NewRunStartedEvent("thread-1", "run-1"),
+ events.NewTextMessageStartEvent("msg-1"),
+ events.NewTextMessageContentEvent("msg-1", "Hello"),
+ events.NewTextMessageEndEvent("msg-1"),
+ events.NewRunFinishedEvent("thread-1", "run-1"),
+}
+
+if err := events.ValidateSequence(events); err != nil {
+ // Handle sequence validation error
+ fmt.Printf("Invalid event sequence: %v\n", err)
+}
+```
+
+Sequence validation ensures:
+- Runs are started before they can be finished or errored
+- Messages are started before content can be added or ended
+- Tool calls follow the proper start → args → end lifecycle
+- Events maintain referential integrity
+
+## ID Generation
+
+The SDK provides utilities for generating unique IDs:
+
+```go
+// Generate various ID types
+threadID := events.GenerateThreadID() // "thread-{uuid}"
+runID := events.GenerateRunID() // "run-{uuid}"
+messageID := events.GenerateMessageID() // "msg-{uuid}"
+toolCallID := events.GenerateToolCallID() // "tool-{uuid}"
+stepID := events.GenerateStepID() // "step-{uuid}"
+
+// Use with event creation
+event := events.NewRunStartedEvent(
+ events.GenerateThreadID(),
+ events.GenerateRunID(),
+)
+```
+
+## Custom and Raw Events
+
+### CustomEvent
+
+For application-specific events:
+
+```go
+type CustomEvent struct {
+ *BaseEvent
+ Name string `json:"name"`
+ Value any `json:"value,omitempty"`
+}
+```
+
+**Usage Example:**
+
+```go
+// Create a custom event
+event := events.NewCustomEvent(
+ "user.preference.changed",
+ events.WithValue(map[string]string{
+ "theme": "dark",
+ }),
+)
+```
+
+### RawEvent
+
+For passing through external event data:
+
+```go
+type RawEvent struct {
+ *BaseEvent
+ Event any `json:"event"`
+ Source *string `json:"source,omitempty"`
+}
+```
+
+**Usage Example:**
+
+```go
+// Create a raw event
+event := events.NewRawEvent(
+ externalEventData,
+ events.WithSource("external-system"),
+)
+```
\ No newline at end of file
diff --git a/docs/sdk/go/core/overview.mdx b/docs/sdk/go/core/overview.mdx
new file mode 100644
index 000000000..098a4953f
--- /dev/null
+++ b/docs/sdk/go/core/overview.mdx
@@ -0,0 +1,55 @@
+---
+title: "Overview"
+description: "Core concepts in the Agent User Interaction Protocol Go SDK"
+---
+
+# Package Import
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+```
+
+## Types Overview
+
+The core package provides the fundamental types and interfaces that power the AG-UI protocol in Go. These types represent the building blocks for agent-to-frontend communication through a strongly typed, event-driven architecture.
+
+Key types include:
+- **Event Interface** - The base interface implemented by all AG-UI events
+- **BaseEvent** - Common fields shared across all event types
+- **Event Type Constants** - Strongly typed event identifiers
+- **Validation Methods** - Built-in event validation
+
+## Events Overview
+
+The events package (`core/events`) forms the foundation of the SDK, providing a comprehensive event-driven system for real-time communication between agents and frontends. The architecture is built around Server-Sent Events (SSE) for efficient streaming.
+
+The event system includes:
+- **Text Message Events** - Streaming assistant responses with start, content, and end markers
+- **Tool Call Events** - Function execution lifecycle from invocation through result
+- **Run Lifecycle Events** - Tracking agent execution states and errors
+- **State Management Events** - Synchronizing agent state with snapshots and deltas
+- **Thinking Events** - Supporting reasoning phases with dedicated event streams
+
+All events implement the common `Event` interface, enabling consistent handling patterns while maintaining type safety through Go's type system.
+
+## Navigation Cards
+
+
+ Complete documentation of all events in the core/events package
+
+
+
+ Complete documentation of all types in the core package
+
\ No newline at end of file
diff --git a/docs/sdk/go/core/types.mdx b/docs/sdk/go/core/types.mdx
new file mode 100644
index 000000000..31888953a
--- /dev/null
+++ b/docs/sdk/go/core/types.mdx
@@ -0,0 +1,398 @@
+---
+title: "Types"
+description: "Documentation for the core types used in the Agent User Interaction Protocol Go SDK"
+---
+
+# Core Types
+
+The Agent User Interaction Protocol Go SDK is built on a set of core types that represent the fundamental structures used throughout the system. This page documents these types and their properties.
+
+## Event Interface
+
+The `Event` interface is the core abstraction for all AG-UI events. All events in the system implement this interface.
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+
+type Event interface {
+ // Type returns the event type
+ Type() EventType
+
+ // Timestamp returns the event timestamp (Unix milliseconds)
+ Timestamp() *int64
+
+ // SetTimestamp sets the event timestamp
+ SetTimestamp(timestamp int64)
+
+ // ThreadID returns the thread ID associated with this event
+ ThreadID() string
+
+ // RunID returns the run ID associated with this event
+ RunID() string
+
+ // Validate validates the event structure and content
+ Validate() error
+
+ // ToJSON serializes the event to JSON for cross-SDK compatibility
+ ToJSON() ([]byte, error)
+
+ // GetBaseEvent returns the underlying base event
+ GetBaseEvent() *BaseEvent
+}
+```
+
+| Method | Return Type | Description |
+| --- | --- | --- |
+| `Type()` | `EventType` | Returns the type of the event (e.g., TEXT_MESSAGE_START) |
+| `Timestamp()` | `*int64` | Returns the Unix timestamp in milliseconds when the event occurred |
+| `SetTimestamp()` | - | Sets the event timestamp |
+| `ThreadID()` | `string` | Returns the thread ID this event belongs to |
+| `RunID()` | `string` | Returns the run ID this event is part of |
+| `Validate()` | `error` | Validates the event structure and returns error if invalid |
+| `ToJSON()` | `[]byte, error` | Serializes the event to JSON bytes |
+| `GetBaseEvent()` | `*BaseEvent` | Returns the embedded base event struct |
+
+### Usage Example
+
+```go
+// Working with events through the interface
+func handleEvent(event events.Event) error {
+ // Validate the event
+ if err := event.Validate(); err != nil {
+ return fmt.Errorf("invalid event: %w", err)
+ }
+
+ // Check event type
+ switch event.Type() {
+ case events.EventTypeTextMessageStart:
+ fmt.Printf("Message started at %d\n", *event.Timestamp())
+ case events.EventTypeToolCallStart:
+ fmt.Printf("Tool call in thread %s\n", event.ThreadID())
+ }
+
+ // Serialize to JSON
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+```
+
+## BaseEvent Struct
+
+The `BaseEvent` struct provides common fields and functionality that all events inherit through embedding.
+
+```go
+type BaseEvent struct {
+ EventType EventType `json:"type"`
+ TimestampMs *int64 `json:"timestamp,omitempty"`
+ RawEvent any `json:"rawEvent,omitempty"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `EventType` | `EventType` | The type of event (required) |
+| `TimestampMs` | `*int64` | Unix timestamp in milliseconds (optional) |
+| `RawEvent` | `any` | Raw event data for custom/external events (optional) |
+
+### Usage Example
+
+```go
+// Creating a base event
+baseEvent := events.NewBaseEvent(events.EventTypeTextMessageStart)
+
+// All specific events embed BaseEvent
+type TextMessageStartEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ Role *string `json:"role,omitempty"`
+}
+
+// The BaseEvent provides common functionality
+event := &TextMessageStartEvent{
+ BaseEvent: baseEvent,
+ MessageID: "msg-123",
+}
+```
+
+## Message Types
+
+Message events represent text messages being streamed from the agent.
+
+### TextMessageStartEvent
+
+Indicates the start of a streaming text message.
+
+```go
+type TextMessageStartEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ Role *string `json:"role,omitempty"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `MessageID` | `string` | Unique identifier for the message (required) |
+| `Role` | `*string` | Role of the message sender (optional) |
+
+### TextMessageContentEvent
+
+Contains a piece of streaming text message content.
+
+```go
+type TextMessageContentEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ Delta string `json:"delta"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `MessageID` | `string` | ID of the message this content belongs to (required) |
+| `Delta` | `string` | The text content chunk (required) |
+
+### TextMessageEndEvent
+
+Indicates the end of a streaming text message.
+
+```go
+type TextMessageEndEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `MessageID` | `string` | ID of the message that ended (required) |
+
+### Usage Example
+
+```go
+// Creating and streaming a text message
+messageID := events.GenerateMessageID()
+
+// Start the message
+startEvent := events.NewTextMessageStartEvent(messageID, events.WithRole("assistant"))
+
+// Stream content
+contentEvent1 := events.NewTextMessageContentEvent(messageID, "Hello, ")
+contentEvent2 := events.NewTextMessageContentEvent(messageID, "how can I help you?")
+
+// End the message
+endEvent := events.NewTextMessageEndEvent(messageID)
+
+// Validate and send events
+for _, event := range []events.Event{startEvent, contentEvent1, contentEvent2, endEvent} {
+ if err := event.Validate(); err != nil {
+ log.Printf("Invalid event: %v", err)
+ continue
+ }
+ // Send event to stream...
+}
+```
+
+## Tool Types
+
+Tool events represent tool/function calls made by the agent.
+
+### ToolCallStartEvent
+
+Indicates the start of a tool call.
+
+```go
+type ToolCallStartEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+ ToolName string `json:"toolName"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `ToolCallID` | `string` | Unique identifier for the tool call (required) |
+| `ToolName` | `string` | Name of the tool being called (required) |
+
+### ToolCallArgsEvent
+
+Contains streaming arguments for a tool call.
+
+```go
+type ToolCallArgsEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+ Args string `json:"args"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `ToolCallID` | `string` | ID of the tool call (required) |
+| `Args` | `string` | JSON string of arguments chunk (required) |
+
+### ToolCallEndEvent
+
+Indicates the end of a tool call.
+
+```go
+type ToolCallEndEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `ToolCallID` | `string` | ID of the tool call that ended (required) |
+
+### ToolCallResultEvent
+
+Contains the result of a tool call execution.
+
+```go
+type ToolCallResultEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+ Result any `json:"result"`
+ Error *string `json:"error,omitempty"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `ToolCallID` | `string` | ID of the tool call (required) |
+| `Result` | `any` | Result data from the tool execution (required) |
+| `Error` | `*string` | Error message if the tool call failed (optional) |
+
+## State Types
+
+State events manage the agent's state during execution.
+
+### StateSnapshotEvent
+
+Represents the complete state at a point in time.
+
+```go
+type StateSnapshotEvent struct {
+ *BaseEvent
+ State any `json:"state"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `State` | `any` | Complete state object (required) |
+
+### StateDeltaEvent
+
+Represents incremental changes to the state.
+
+```go
+type StateDeltaEvent struct {
+ *BaseEvent
+ Delta any `json:"delta"`
+ Operation *string `json:"operation,omitempty"`
+}
+```
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `Delta` | `any` | State changes to apply (required) |
+| `Operation` | `*string` | Type of operation (merge, replace, etc.) (optional) |
+
+### Usage Example
+
+```go
+// Creating state events
+type AgentState struct {
+ Counter int `json:"counter"`
+ Status string `json:"status"`
+}
+
+// Snapshot event with complete state
+state := &AgentState{Counter: 5, Status: "processing"}
+snapshotEvent := events.NewStateSnapshotEvent(state)
+
+// Delta event with incremental update
+delta := map[string]interface{}{
+ "counter": 6,
+}
+deltaEvent := events.NewStateDeltaEvent(delta, events.WithOperation("merge"))
+```
+
+## ID Generation
+
+The SDK provides utility functions for generating unique IDs for various entities.
+
+### ID Generation Functions
+
+```go
+// Generate a unique thread ID (format: "thread-{uuid}")
+threadID := events.GenerateThreadID()
+
+// Generate a unique run ID (format: "run-{uuid}")
+runID := events.GenerateRunID()
+
+// Generate a unique message ID (format: "msg-{uuid}")
+messageID := events.GenerateMessageID()
+
+// Generate a unique tool call ID (format: "tool-{uuid}")
+toolCallID := events.GenerateToolCallID()
+
+// Generate a unique step ID (format: "step-{uuid}")
+stepID := events.GenerateStepID()
+```
+
+### Custom ID Generators
+
+You can also implement custom ID generation strategies:
+
+```go
+// Use timestamp-based IDs
+generator := events.NewTimestampIDGenerator("myapp")
+events.SetDefaultIDGenerator(generator)
+
+// Now all ID generation will use timestamp format
+// Format: "myapp-{type}-{timestamp}-{shortUUID}"
+messageID := events.GenerateMessageID() // "myapp-msg-1709123456789-a1b2c3d4"
+
+// Or implement your own IDGenerator interface
+type CustomIDGenerator struct{}
+
+func (g *CustomIDGenerator) GenerateMessageID() string {
+ return fmt.Sprintf("custom-msg-%d", time.Now().Unix())
+}
+// ... implement other methods
+
+events.SetDefaultIDGenerator(&CustomIDGenerator{})
+```
+
+## Event Type Composition
+
+Events in the Go SDK compose through struct embedding, allowing for clean type hierarchies:
+
+```go
+// Example showing how events compose
+type TextMessageStartEvent struct {
+ *BaseEvent // Embeds base event fields and methods
+ MessageID string `json:"messageId"`
+ Role *string `json:"role,omitempty"`
+}
+
+// The event automatically inherits all BaseEvent methods
+event := &TextMessageStartEvent{
+ BaseEvent: events.NewBaseEvent(events.EventTypeTextMessageStart),
+ MessageID: "msg-123",
+}
+
+// Can call both BaseEvent and specific methods
+event.SetTimestamp(time.Now().UnixMilli()) // From BaseEvent
+err := event.Validate() // TextMessageStartEvent override
+```
+
+This composition pattern allows the SDK to maintain a clean separation between common functionality and event-specific behavior while providing a consistent interface through the `Event` type.
\ No newline at end of file
diff --git a/docs/sdk/go/encoding/overview.mdx b/docs/sdk/go/encoding/overview.mdx
new file mode 100644
index 000000000..ec17e5faa
--- /dev/null
+++ b/docs/sdk/go/encoding/overview.mdx
@@ -0,0 +1,368 @@
+---
+title: "Encoding Overview"
+description: "Documentation for encoding and serialization in the AG-UI Go SDK"
+---
+
+# Encoding Package
+
+The encoding package provides a comprehensive system for encoding, decoding, and content negotiation in the AG-UI Go SDK. It supports multiple formats, streaming operations, and intelligent content type selection based on client preferences.
+
+## Package Overview
+
+The encoding system is built around a set of well-defined interfaces that follow the Interface Segregation Principle, allowing components to implement only the functionality they need.
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding"
+```
+
+## Core Interfaces
+
+### Encoder Interface
+
+The `Encoder` interface defines methods for encoding events to bytes:
+
+```go
+type Encoder interface {
+ // Encode encodes a single event
+ Encode(ctx context.Context, event events.Event) ([]byte, error)
+
+ // EncodeMultiple encodes multiple events efficiently
+ EncodeMultiple(ctx context.Context, events []events.Event) ([]byte, error)
+
+ // ContentType returns the MIME type for this encoder
+ ContentType() string
+}
+```
+
+### Decoder Interface
+
+The `Decoder` interface defines methods for decoding events from bytes:
+
+```go
+type Decoder interface {
+ // Decode decodes a single event from raw data
+ Decode(ctx context.Context, data []byte) (events.Event, error)
+
+ // DecodeMultiple decodes multiple events from raw data
+ DecodeMultiple(ctx context.Context, data []byte) ([]events.Event, error)
+
+ // ContentType returns the MIME type for this decoder
+ ContentType() string
+}
+```
+
+### Codec Interface
+
+The `Codec` interface combines encoding and decoding capabilities:
+
+```go
+type Codec interface {
+ Encoder
+ Decoder
+ ContentTypeProvider
+ StreamingCapabilityProvider
+}
+```
+
+## JSON Encoding
+
+The JSON encoder provides high-performance JSON serialization with support for cross-SDK compatibility:
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/json"
+
+// Create a JSON encoder with options
+encoder := json.NewJSONEncoder(&encoding.EncodingOptions{
+ Pretty: true, // Format output for readability
+ CrossSDKCompatibility: true, // Ensure compatibility with other SDKs
+ ValidateOutput: true, // Validate events before encoding
+})
+
+// Encode an event
+data, err := encoder.Encode(ctx, event)
+if err != nil {
+ // Handle encoding error
+}
+
+// The encoder returns "application/json" as its content type
+contentType := encoder.ContentType()
+```
+
+## SSE Writer
+
+The SSE (Server-Sent Events) writer is used for streaming events to clients in real-time:
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/sse"
+
+// Create an SSE writer
+writer := sse.NewSSEWriter()
+
+// Write an event to an HTTP response writer
+err := writer.WriteEvent(ctx, w, event)
+if err != nil {
+ // Handle write error
+}
+
+// Write an error event
+err = writer.WriteErrorEvent(ctx, w, errors.New("something went wrong"), "request-123")
+
+// Flush the writer to ensure data is sent immediately
+if flusher, ok := w.(http.Flusher); ok {
+ flusher.Flush()
+}
+```
+
+### Server Implementation Example
+
+Here's a complete example of using the SSE writer in an HTTP endpoint:
+
+```go
+func handleAgentStream(w http.ResponseWriter, r *http.Request) {
+ // Set headers for SSE
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Connection", "keep-alive")
+
+ // Create SSE writer
+ sseWriter := sse.NewSSEWriter()
+
+ // Send events
+ events := generateEvents() // Your event generation logic
+
+ for _, event := range events {
+ if err := sseWriter.WriteEvent(r.Context(), w, event); err != nil {
+ log.Printf("Failed to write event: %v", err)
+ return
+ }
+
+ // Flush after each event for real-time streaming
+ if f, ok := w.(http.Flusher); ok {
+ f.Flush()
+ }
+ }
+}
+```
+
+## Content Negotiation
+
+The negotiation package implements RFC 7231 compliant content type negotiation:
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/negotiation"
+
+// Create a negotiator with preferred content type
+negotiator := negotiation.NewContentNegotiator("application/json")
+
+// Negotiate based on Accept header
+acceptHeader := r.Header.Get("Accept")
+contentType, err := negotiator.Negotiate(acceptHeader)
+if err != nil {
+ // No acceptable content type found
+ contentType = "application/json" // Fall back to default
+}
+
+// Check if a specific content type is supported
+if negotiator.CanHandle("application/protobuf") {
+ // Use protobuf encoding
+}
+
+// Get list of supported types
+supportedTypes := negotiator.SupportedTypes()
+```
+
+### Custom Content Types
+
+You can register custom content types with their capabilities:
+
+```go
+negotiator.RegisterType(&negotiation.TypeCapabilities{
+ ContentType: "application/vnd.myapp+json",
+ CanStream: true,
+ CompressionSupport: []string{"gzip", "deflate"},
+ Priority: 0.95,
+ Extensions: []string{".myapp.json"},
+ Aliases: []string{"application/x-myapp"},
+})
+```
+
+## Streaming Operations
+
+For handling large volumes of events, the encoding package supports streaming operations:
+
+```go
+type StreamEncoder interface {
+ // EncodeStream encodes events from a channel to a writer
+ EncodeStream(ctx context.Context, input <-chan events.Event, output io.Writer) error
+
+ // Session management
+ StartStream(ctx context.Context, w io.Writer) error
+ EndStream(ctx context.Context) error
+
+ // Event processing
+ WriteEvent(ctx context.Context, event events.Event) error
+}
+```
+
+### Channel-Based Streaming
+
+Stream events from a channel directly to an output writer:
+
+```go
+// Create event channel
+eventChan := make(chan events.Event, 100)
+
+// Start streaming in a goroutine
+go func() {
+ err := streamEncoder.EncodeStream(ctx, eventChan, w)
+ if err != nil {
+ log.Printf("Streaming error: %v", err)
+ }
+}()
+
+// Send events to the channel
+for _, event := range events {
+ select {
+ case eventChan <- event:
+ // Event sent
+ case <-ctx.Done():
+ // Context cancelled
+ return
+ }
+}
+close(eventChan)
+```
+
+## Configuration Options
+
+The encoding package provides comprehensive configuration options:
+
+```go
+// EncodingOptions for encoding operations
+type EncodingOptions struct {
+ Pretty bool // Format output for readability
+ Compression string // Compression algorithm (e.g., "gzip")
+ BufferSize int // Buffer size for streaming
+ MaxSize int64 // Maximum encoded size (0 for unlimited)
+ ValidateOutput bool // Validate output after encoding
+ CrossSDKCompatibility bool // Ensure compatibility with other SDKs
+}
+
+// DecodingOptions for decoding operations
+type DecodingOptions struct {
+ Strict bool // Enable strict validation
+ MaxSize int64 // Maximum input size (0 for unlimited)
+ BufferSize int // Buffer size for streaming
+ AllowUnknownFields bool // Allow unknown fields in input
+ ValidateEvents bool // Validate events after decoding
+}
+```
+
+## Error Handling
+
+The encoding package defines specific error types for better error handling:
+
+```go
+// Handle encoding errors
+data, err := encoder.Encode(ctx, event)
+if err != nil {
+ var encErr *encoding.EncodingError
+ if errors.As(err, &encErr) {
+ log.Printf("Encoding failed for format %s: %s", encErr.Format, encErr.Message)
+ if encErr.Cause != nil {
+ log.Printf("Underlying error: %v", encErr.Cause)
+ }
+ }
+}
+
+// Handle decoding errors
+event, err := decoder.Decode(ctx, data)
+if err != nil {
+ var decErr *encoding.DecodingError
+ if errors.As(err, &decErr) {
+ log.Printf("Decoding failed for format %s: %s", decErr.Format, decErr.Message)
+ }
+}
+```
+
+## Examples
+
+### Complete Server Endpoint
+
+Here's a complete example of an AG-UI agent endpoint with content negotiation and SSE streaming:
+
+```go
+func handleAgent(w http.ResponseWriter, r *http.Request) {
+ // Content negotiation
+ negotiator := negotiation.NewContentNegotiator("application/json")
+ acceptHeader := r.Header.Get("Accept")
+ contentType, _ := negotiator.Negotiate(acceptHeader)
+
+ // For SSE streaming
+ if contentType == "text/event-stream" || r.Header.Get("Accept") == "text/event-stream" {
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+
+ sseWriter := sse.NewSSEWriter()
+
+ // Process request and generate events
+ events := processAgentRequest(r)
+
+ for _, event := range events {
+ if err := sseWriter.WriteEvent(r.Context(), w, event); err != nil {
+ sseWriter.WriteErrorEvent(r.Context(), w, err, "req-123")
+ return
+ }
+
+ if f, ok := w.(http.Flusher); ok {
+ f.Flush()
+ }
+ }
+ return
+ }
+
+ // For JSON response
+ w.Header().Set("Content-Type", "application/json")
+
+ encoder := json.NewJSONEncoder(&encoding.EncodingOptions{
+ Pretty: r.URL.Query().Get("pretty") == "true",
+ })
+
+ events := processAgentRequest(r)
+ data, err := encoder.EncodeMultiple(r.Context(), events)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Write(data)
+}
+```
+
+### Custom Codec Implementation
+
+Implement a custom codec for a specific format:
+
+```go
+type CustomCodec struct {
+ options *encoding.EncodingOptions
+}
+
+func (c *CustomCodec) Encode(ctx context.Context, event events.Event) ([]byte, error) {
+ // Custom encoding logic
+ return customSerialize(event)
+}
+
+func (c *CustomCodec) Decode(ctx context.Context, data []byte) (events.Event, error) {
+ // Custom decoding logic
+ return customDeserialize(data)
+}
+
+func (c *CustomCodec) ContentType() string {
+ return "application/vnd.custom+binary"
+}
+
+func (c *CustomCodec) SupportsStreaming() bool {
+ return true
+}
+```
\ No newline at end of file
diff --git a/docs/sdk/go/errors/overview.mdx b/docs/sdk/go/errors/overview.mdx
new file mode 100644
index 000000000..60a0b85c1
--- /dev/null
+++ b/docs/sdk/go/errors/overview.mdx
@@ -0,0 +1,416 @@
+---
+title: "Error Handling"
+description: "Comprehensive error handling utilities for the AG-UI Go SDK"
+---
+
+# Error Handling
+
+The AG-UI Go SDK provides a comprehensive error handling system with custom error types, severity-based handling, context management, and built-in retry logic with exponential backoff.
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/errors"
+```
+
+## Error Types
+
+The SDK defines specific error types for different scenarios, each with appropriate severity levels and retry behavior:
+
+| Type | Severity | Use Case | Retry |
+|------|----------|----------|-------|
+| `ValidationError` | Warning | Input validation failures, malformed data | No |
+| `StateError` | Error | Invalid state transitions, state conflicts | No |
+| `ConflictError` | Error | Resource conflicts, concurrent operations | Yes |
+| `EncodingError` | Error | Encoding/decoding failures, format issues | No |
+| `SecurityError` | Critical | Security violations, injection attempts | No |
+| `AgentError` | Error | Agent-specific operational errors | Varies |
+| `OperationError` | Error | Operation failures with context preservation | Varies |
+
+## BaseError
+
+All custom error types embed `BaseError`, providing common fields and methods:
+
+```go
+type BaseError struct {
+ Code string // Machine-readable error code
+ Message string // Human-readable error message
+ Severity Severity // Error severity level
+ Timestamp time.Time // When the error occurred
+ Details map[string]interface{} // Additional context
+ Cause error // Underlying error, if any
+ Retryable bool // If the operation can be retried
+ RetryAfter *time.Duration // Suggested retry delay
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `Code` | `string` | Machine-readable error identifier |
+| `Message` | `string` | Human-readable error description |
+| `Severity` | `Severity` | Error severity level (Debug to Fatal) |
+| `Timestamp` | `time.Time` | When the error occurred |
+| `Details` | `map[string]interface{}` | Additional error context |
+| `Cause` | `error` | Underlying error for error chaining |
+| `Retryable` | `bool` | Whether the operation can be retried |
+| `RetryAfter` | `*time.Duration` | Suggested delay before retry |
+
+## Severity Levels
+
+Errors are classified by severity to help with appropriate handling and logging:
+
+```go
+const (
+ SeverityDebug Severity = iota // Informational only
+ SeverityInfo // Informational, no action needed
+ SeverityWarning // Warning, operation continues
+ SeverityError // Recoverable error
+ SeverityCritical // Critical, immediate attention
+ SeverityFatal // Fatal, requires termination
+)
+```
+
+| Level | Description | Action Required |
+|-------|-------------|-----------------|
+| `Debug` | Development information | None - logging only |
+| `Info` | Informational messages | None - awareness only |
+| `Warning` | Non-blocking issues | Monitor, may need future action |
+| `Error` | Recoverable failures | Handle error, retry if applicable |
+| `Critical` | Severe issues | Immediate intervention required |
+| `Fatal` | Unrecoverable failures | Terminate operation |
+
+## Error Context
+
+Add context and details to errors for better debugging:
+
+```go
+// Adding details to errors
+err := errors.NewValidationError("INVALID_INPUT", "Invalid event data")
+err.WithField("eventType", "unknown").
+ WithDetail("received", eventData).
+ WithDetail("expected", []string{"text", "tool", "state"})
+
+// Preserving error chains
+originalErr := someOperation()
+wrappedErr := errors.NewStateError("STATE_CONFLICT", "Cannot transition state").
+ WithCause(originalErr).
+ WithStateID("state_123").
+ WithTransition("pending -> active")
+
+// Adding retry information
+retryableErr := errors.NewConflictError("RESOURCE_LOCKED", "Resource is locked").
+ WithRetry(5 * time.Second).
+ WithResource("agent", "agent_001")
+```
+
+## Retry Logic
+
+The SDK includes built-in retry capabilities with exponential backoff:
+
+```go
+// Default retry configuration
+config := errors.DefaultRetryConfig()
+// MaxAttempts: 3
+// InitialDelay: 100ms
+// MaxDelay: 30s
+// Multiplier: 2.0
+// Jitter: 0.1
+
+// Custom retry configuration
+config := &errors.RetryConfig{
+ MaxAttempts: 5,
+ InitialDelay: 500 * time.Millisecond,
+ MaxDelay: 1 * time.Minute,
+ Multiplier: 1.5,
+ Jitter: 0.2,
+ RetryIf: func(err error) bool {
+ // Custom retry logic
+ return errors.IsRetryable(err) && !errors.IsSecurityError(err)
+ },
+ OnRetry: func(attempt int, err error, delay time.Duration) {
+ log.Printf("Retry attempt %d after %v: %v", attempt, delay, err)
+ },
+}
+
+// Execute with retry
+err := errors.Retry(ctx, config, func() error {
+ return performOperation()
+})
+
+// Check if error is retryable
+if errors.IsRetryable(err) {
+ if duration := errors.GetRetryAfter(err); duration != nil {
+ time.Sleep(*duration)
+ // Retry operation
+ }
+}
+```
+
+## Error Creation
+
+Use type-specific constructors for creating errors:
+
+```go
+// Validation errors
+err := errors.NewValidationError("INVALID_EVENT", "Event validation failed").
+ WithField("type", eventType).
+ WithRule("event_type_required").
+ AddFieldError("timestamp", "must be positive").
+ AddFieldError("id", "exceeds maximum length")
+
+// State errors
+err := errors.NewStateError("INVALID_TRANSITION", "Invalid state transition").
+ WithStateID("state_456").
+ WithStates(currentState, expectedState).
+ WithTransition("active -> completed")
+
+// Conflict errors
+err := errors.NewConflictError("OPERATION_CONFLICT", "Concurrent modification").
+ WithResource("thread", "thread_789").
+ WithOperation("update_message").
+ WithResolution("retry with latest version")
+
+// Encoding errors
+err := errors.NewEncodingError("DECODE_FAILED", "Failed to decode JSON").
+ WithFormat("json").
+ WithOperation("decode").
+ WithMimeType("application/json").
+ WithPosition(142)
+
+// Security errors
+err := errors.NewXSSError("XSS attempt detected", "").
+ WithLocation("message.content").
+ WithDetail("user_id", "user_123")
+
+// Agent errors
+err := errors.NewAgentError(errors.ErrorTypeTimeout, "Agent response timeout", "gpt-4").
+ WithEventID("event_abc").
+ WithDetail("timeout", "30s")
+```
+
+## Error Handling Patterns
+
+Follow Go idioms for error handling:
+
+```go
+// Basic error checking
+event, err := decoder.Decode(sseData)
+if err != nil {
+ // Type assertion for specific handling
+ if valErr, ok := err.(*errors.ValidationError); ok {
+ log.Printf("Validation failed: %v", valErr.FieldErrors)
+ return nil, valErr
+ }
+
+ // Check error properties
+ if errors.IsRetryable(err) {
+ return retryOperation(err)
+ }
+
+ // Check severity
+ if errors.GetSeverity(err) >= errors.SeverityCritical {
+ alertOps(err)
+ }
+
+ return nil, err
+}
+
+// Error wrapping with context
+result, err := processEvent(event)
+if err != nil {
+ return nil, errors.WithOperation("processEvent", event.ID, err)
+}
+
+// Sentinel error checking
+if errors.Is(err, errors.ErrStateInvalid) {
+ return handleInvalidState()
+}
+
+// Extract specific error types
+var stateErr *errors.StateError
+if errors.As(err, &stateErr) {
+ log.Printf("State error in %s: %s", stateErr.StateID, stateErr.Transition)
+}
+
+// Chain multiple errors
+var errs []error
+for _, item := range items {
+ if err := process(item); err != nil {
+ errs = append(errs, err)
+ }
+}
+if chainedErr := errors.Chain(errs...); chainedErr != nil {
+ return chainedErr
+}
+```
+
+## Examples
+
+### Handling SSE Stream Errors
+
+```go
+frames, errorsChan, err := client.Stream(opts)
+if err != nil {
+ return errors.Wrap(err, "failed to start stream")
+}
+
+for {
+ select {
+ case frame := <-frames:
+ event, err := decoder.Decode(frame.Data)
+ if err != nil {
+ decodeErr := errors.NewDecodingError("DECODE_FAILED", "Invalid SSE data").
+ WithCause(err).
+ WithDetail("frame_id", frame.ID).
+ WithDetail("data_len", len(frame.Data))
+
+ if errors.GetSeverity(decodeErr) >= errors.SeverityError {
+ log.Printf("Critical decode error: %v", decodeErr)
+ return decodeErr
+ }
+ continue
+ }
+ // Process event
+
+ case err := <-errorsChan:
+ if errors.IsRetryable(err) {
+ log.Printf("Retryable error: %v", err)
+ // Implement reconnection logic
+ } else {
+ return err
+ }
+ }
+}
+```
+
+### Validating Agent Input with Detailed Errors
+
+```go
+func validateAgentInput(input *AgentInput) error {
+ valErr := errors.NewValidationError("INPUT_VALIDATION", "Agent input validation failed")
+
+ if input.ThreadID == "" {
+ valErr.AddFieldError("thread_id", "required field")
+ } else if len(input.ThreadID) > 100 {
+ valErr.AddFieldError("thread_id", "exceeds maximum length of 100")
+ }
+
+ if input.Timeout < 0 {
+ valErr.AddFieldError("timeout", "must be non-negative")
+ }
+
+ for i, msg := range input.Messages {
+ if msg.Content == "" {
+ valErr.AddFieldError(
+ fmt.Sprintf("messages[%d].content", i),
+ "message content cannot be empty",
+ )
+ }
+ }
+
+ if valErr.HasFieldErrors() {
+ return valErr
+ }
+
+ return nil
+}
+```
+
+### Implementing Retry with Backoff
+
+```go
+func fetchWithRetry(ctx context.Context, url string) (*Response, error) {
+ config := &errors.RetryConfig{
+ MaxAttempts: 3,
+ InitialDelay: 1 * time.Second,
+ MaxDelay: 10 * time.Second,
+ Multiplier: 2.0,
+ Jitter: 0.1,
+ RetryIf: func(err error) bool {
+ // Retry on network errors and 5xx status codes
+ if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+ return true
+ }
+ if httpErr, ok := err.(*HTTPError); ok {
+ return httpErr.StatusCode >= 500
+ }
+ return false
+ },
+ OnRetry: func(attempt int, err error, delay time.Duration) {
+ log.Printf("Attempt %d failed: %v. Retrying after %v", attempt, err, delay)
+ },
+ }
+
+ var response *Response
+ err := errors.Retry(ctx, config, func() error {
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 500 {
+ return &HTTPError{StatusCode: resp.StatusCode}
+ }
+
+ response = &Response{/* ... */}
+ return nil
+ })
+
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to fetch after retries")
+ }
+
+ return response, nil
+}
+```
+
+## Common Error Codes
+
+The SDK defines standard error codes for consistent error handling:
+
+```go
+const (
+ // Validation codes
+ CodeValidationFailed = "VALIDATION_FAILED"
+ CodeMissingEvent = "MISSING_EVENT"
+ CodeMissingEventType = "MISSING_EVENT_TYPE"
+ CodeNegativeTimestamp = "NEGATIVE_TIMESTAMP"
+ CodeIDTooLong = "ID_TOO_LONG"
+
+ // Encoding codes
+ CodeEncodingFailed = "ENCODING_FAILED"
+ CodeDecodingFailed = "DECODING_FAILED"
+
+ // Security codes
+ CodeSecurityViolation = "SECURITY_VIOLATION"
+ CodeXSSDetected = "XSS_DETECTED"
+ CodeInvalidData = "INVALID_DATA"
+ CodeSizeExceeded = "SIZE_EXCEEDED"
+
+ // Negotiation codes
+ CodeNegotiationFailed = "NEGOTIATION_FAILED"
+ CodeNoSuitableFormat = "NO_SUITABLE_FORMAT"
+ CodeUnsupportedFormat = "UNSUPPORTED_FORMAT"
+)
+```
+
+## Sentinel Errors
+
+Pre-defined errors for common scenarios:
+
+```go
+var (
+ ErrStateInvalid = errors.New("invalid state")
+ ErrValidationFailed = errors.New("validation failed")
+ ErrConflict = errors.New("operation conflict")
+ ErrRetryExhausted = errors.New("retry attempts exhausted")
+ ErrContextMissing = errors.New("required context missing")
+ ErrOperationNotPermitted = errors.New("operation not permitted")
+ ErrEncodingNotSupported = errors.New("encoding format not supported")
+ ErrDecodingFailed = errors.New("decoding failed")
+ ErrStreamingNotSupported = errors.New("streaming not supported")
+ ErrSecurityViolation = errors.New("security violation")
+ ErrNegotiationFailed = errors.New("negotiation failed")
+)
+```
+
+Use these sentinel errors with `errors.Is()` for consistent error checking across the SDK.
\ No newline at end of file
diff --git a/docs/sdk/go/overview.mdx b/docs/sdk/go/overview.mdx
new file mode 100644
index 000000000..de1bdae6d
--- /dev/null
+++ b/docs/sdk/go/overview.mdx
@@ -0,0 +1,177 @@
+---
+title: "Go SDK Overview"
+description: "Connect to AG-UI agents using the official Go SDK"
+---
+
+The AG-UI Go SDK provides a robust and idiomatic way to connect Go applications to AG-UI agents. It enables real-time streaming communication through Server-Sent Events (SSE), allowing you to build intelligent agent-powered applications with Go.
+
+## Installation
+
+Install the SDK using Go's standard package management:
+
+```bash
+go get github.com/ag-ui-protocol/ag-ui/sdks/community/go
+```
+
+## Quick Start
+
+Here's a simple example showing how to connect to an AG-UI agent and receive events:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/client/sse"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+)
+
+func main() {
+ // Create SSE client with configuration
+ client := sse.NewClient(sse.Config{
+ Endpoint: "https://api.example.com/agent",
+ APIKey: "your-api-key",
+ ConnectTimeout: 30 * time.Second,
+ ReadTimeout: 5 * time.Minute,
+ })
+ defer client.Close()
+
+ // Prepare the request payload
+ payload := map[string]interface{}{
+ "threadId": "thread_123",
+ "messages": []map[string]interface{}{
+ {
+ "id": "msg_1",
+ "role": "user",
+ "content": "Hello, agent!",
+ },
+ },
+ }
+
+ // Start streaming
+ frames, errors, err := client.Stream(sse.StreamOptions{
+ Context: context.Background(),
+ Payload: payload,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // Create event decoder
+ decoder := events.NewEventDecoder()
+
+ // Handle incoming events
+ for {
+ select {
+ case frame := <-frames:
+ if frame == nil {
+ return // Stream ended
+ }
+
+ // Decode the event
+ event, err := decoder.Decode(frame.Data)
+ if err != nil {
+ log.Printf("decode error: %v", err)
+ continue
+ }
+
+ // Handle different event types
+ switch e := event.(type) {
+ case *events.TextMessageStartEvent:
+ fmt.Printf("Message started: %s\n", e.MessageID)
+ case *events.TextMessageContentEvent:
+ fmt.Printf("Content: %s", e.Delta)
+ case *events.TextMessageEndEvent:
+ fmt.Printf("\nMessage completed\n")
+ case *events.ToolCallStartEvent:
+ fmt.Printf("Tool call: %s\n", e.ToolName)
+ }
+
+ case err := <-errors:
+ log.Printf("stream error: %v", err)
+ return
+ }
+ }
+}
+```
+
+## Package Structure
+
+The Go SDK is organized into several focused packages, each handling a specific aspect of the AG-UI protocol:
+
+### Core Package (`core/events`)
+The foundation of the SDK, providing event types, interfaces, and decoding capabilities. This package defines all the event structures used in AG-UI communication, including text messages, tool calls, state management, and lifecycle events.
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+```
+
+### Client Package (`client/sse`)
+Handles Server-Sent Events (SSE) connectivity to AG-UI agents. This package provides a robust SSE client with automatic reconnection, timeout handling, and authentication support.
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/client/sse"
+```
+
+### Encoding Package (`encoding`)
+Provides flexible encoding and decoding for different content types. Includes JSON encoding/decoding, SSE writing for servers, and content negotiation for handling different MIME types.
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/json"
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/sse"
+```
+
+### Errors Package (`errors`)
+Comprehensive error handling with typed errors, severity levels, and retry logic. This package helps you handle different error scenarios gracefully with built-in retry capabilities.
+
+```go
+import "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/errors"
+```
+
+## Next Steps
+
+Explore the detailed documentation for each package to learn more about specific features and advanced usage:
+
+
+ Learn about events, types, and the foundational concepts of the AG-UI protocol
+
+
+
+ Detailed guide on SSE client configuration, streaming, and connection management
+
+
+
+ Work with different encodings, content negotiation, and SSE server implementation
+
+
+
+ Comprehensive error handling patterns, retry logic, and recovery strategies
+
\ No newline at end of file
diff --git a/sdks/community/go/example/client/cmd/main.go b/sdks/community/go/example/client/cmd/main.go
new file mode 100644
index 000000000..c75b9d2a8
--- /dev/null
+++ b/sdks/community/go/example/client/cmd/main.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/client/internal/agent"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/client/internal/message"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/client/internal/ui"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func runTea(p *tea.Program, userInputCh chan string) error {
+ defer close(userInputCh)
+ _, err := p.Run()
+ return err
+}
+
+func main() {
+ userInputCh := make(chan string)
+ p := tea.NewProgram(ui.InitialModel(userInputCh), tea.WithAltScreen())
+
+ sendUserInput := func(msg *message.Message) {
+ p.Send(msg)
+ }
+ go func() {
+
+ for msg := range userInputCh {
+ err := agent.Chat(context.Background(), msg, agent.DefaultEndpoint(), sendUserInput)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+ }()
+
+ teaErr := runTea(p, userInputCh)
+ if teaErr != nil {
+ log.Fatal(teaErr)
+ }
+}
diff --git a/sdks/community/go/example/client/go.mod b/sdks/community/go/example/client/go.mod
new file mode 100644
index 000000000..ecb2b50be
--- /dev/null
+++ b/sdks/community/go/example/client/go.mod
@@ -0,0 +1,35 @@
+module github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/client
+
+go 1.24.4
+
+require (
+ github.com/ag-ui-protocol/ag-ui/sdks/community/go v0.0.0-20250816203601-e173ef3a0e9a
+ github.com/charmbracelet/bubbles v0.21.0
+ github.com/charmbracelet/bubbletea v1.3.6
+ github.com/charmbracelet/lipgloss v1.1.0
+ github.com/sirupsen/logrus v1.9.3
+)
+
+require (
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+ github.com/charmbracelet/x/ansi v0.9.3 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/stretchr/testify v1.10.0 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ golang.org/x/sync v0.15.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.25.0 // indirect
+)
diff --git a/sdks/community/go/example/client/go.sum b/sdks/community/go/example/client/go.sum
new file mode 100644
index 000000000..47d71cfd8
--- /dev/null
+++ b/sdks/community/go/example/client/go.sum
@@ -0,0 +1,71 @@
+github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
+github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
+github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
+github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
+github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
+github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
+github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/sdks/community/go/example/client/internal/agent/chat.go b/sdks/community/go/example/client/internal/agent/chat.go
new file mode 100644
index 000000000..e14fd83da
--- /dev/null
+++ b/sdks/community/go/example/client/internal/agent/chat.go
@@ -0,0 +1,97 @@
+package agent
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/client/internal/event"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/client/internal/message"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/client/sse"
+ "github.com/sirupsen/logrus"
+)
+
+func DefaultEndpoint() string {
+ return "http://localhost:8000/agentic"
+}
+
+func Chat(ctx context.Context, inputMsg string, endpoint string, send func(msg *message.Message)) error {
+ logger := logrus.New()
+ logger.SetLevel(logrus.FatalLevel)
+ sseConfig := sse.Config{
+ Endpoint: endpoint,
+ ConnectTimeout: 30 * time.Second,
+ ReadTimeout: 5 * time.Minute,
+ BufferSize: 100,
+ Logger: logger,
+ AuthHeader: "Authorization",
+ AuthScheme: "Bearer",
+ }
+
+ client := sse.NewClient(sseConfig)
+ defer func() {
+ client.Close()
+ }()
+
+ sessionID := "test-session-1755371887"
+ runID := "run-1755744865857245000"
+
+ payload := map[string]interface{}{
+ "threadId": sessionID,
+ "runId": runID,
+ "state": map[string]interface{}{},
+ "messages": []map[string]interface{}{
+ {
+ "id": "msg-1",
+ "role": "user",
+ "content": inputMsg,
+ },
+ },
+ "tools": []interface{}{},
+ "context": []interface{}{},
+ "forwardedProps": map[string]interface{}{},
+ }
+
+ // Start the SSE stream
+ var err error
+ frames, errorCh, err := client.Stream(sse.StreamOptions{
+ Context: ctx,
+ Payload: payload,
+ })
+
+ if err != nil {
+ return errors.New("Failed to establish SSE connection")
+ }
+
+ // Parse SSE events
+ for {
+ select {
+ case frame, ok := <-frames:
+ if !ok {
+ return nil
+ }
+
+ rawEvent, err := event.Parse(frame.Data)
+ if err != nil {
+ return fmt.Errorf("failed to process SSE event %w", err)
+ }
+ currMsg := message.NewMessage(rawEvent)
+ if currMsg == nil {
+ return fmt.Errorf("failed to parse message %w", err)
+ }
+ send(currMsg)
+
+ case err, ok := <-errorCh:
+ if !ok {
+ break
+ }
+ if err != nil {
+ break
+ }
+
+ case <-ctx.Done():
+ break
+ }
+ }
+}
diff --git a/sdks/community/go/example/client/internal/event/parse.go b/sdks/community/go/example/client/internal/event/parse.go
new file mode 100644
index 000000000..5f955aac3
--- /dev/null
+++ b/sdks/community/go/example/client/internal/event/parse.go
@@ -0,0 +1,30 @@
+package event
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+)
+
+func Parse(data []byte) (events.Event, error) {
+ // Parse the SSE event
+ var eventData map[string]interface{}
+
+ err := json.Unmarshal(data, &eventData)
+ if err != nil {
+ return nil, fmt.Errorf("received non-JSON frame event data %w", err)
+ }
+
+ decoder := events.NewEventDecoder(nil)
+
+ // Extract event type - the server sends it as "type" field directly
+ eventType, _ := eventData["type"].(string)
+
+ event, err := decoder.DecodeEvent(eventType, data)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode event %w", err)
+ }
+
+ return event, nil
+}
diff --git a/sdks/community/go/example/client/internal/message/message.go b/sdks/community/go/example/client/internal/message/message.go
new file mode 100644
index 000000000..463cf26aa
--- /dev/null
+++ b/sdks/community/go/example/client/internal/message/message.go
@@ -0,0 +1,275 @@
+package message
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/charmbracelet/lipgloss"
+)
+
+var serverStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("21"))
+
+type Message struct {
+ contents []string
+}
+
+func (m *Message) Strings() []string {
+ return m.contents
+}
+
+func NewMessage(event events.Event) *Message {
+ return getMessageFromEvent(event)
+}
+
+func getMessageFromEvent(event events.Event) *Message {
+ eventType := event.Type()
+ switch eventType {
+ case events.EventTypeRunStarted:
+ _, ok := event.(*events.RunStartedEvent)
+ if !ok {
+ return nil
+ }
+ content := "Run started"
+ return &Message{
+ contents: []string{content},
+ }
+
+ case events.EventTypeRunFinished:
+ _, ok := event.(*events.RunFinishedEvent)
+ if !ok {
+ return nil
+ }
+ content := "Run finished"
+ return &Message{
+ contents: []string{content},
+ }
+ case events.EventTypeRunError:
+ errorEvent, ok := event.(*events.RunErrorEvent)
+ if !ok {
+ return nil
+ }
+ content := fmt.Sprintf("Run error: %s", errorEvent.Message)
+ if errorEvent.Code != nil {
+ content = fmt.Sprintf("Run error [%s]: %s", *errorEvent.Code, errorEvent.Message)
+ }
+ return &Message{
+ contents: []string{content},
+ }
+ case events.EventTypeTextMessageStart:
+ _, ok := event.(*events.TextMessageStartEvent)
+ if !ok {
+ return nil
+ }
+ curMsg := "text message started"
+ return &Message{
+ contents: []string{curMsg},
+ }
+ case events.EventTypeTextMessageContent:
+ msg, ok := event.(*events.TextMessageContentEvent)
+ if !ok {
+ return nil
+ }
+ return &Message{
+ contents: []string{msg.Delta},
+ }
+ case events.EventTypeTextMessageEnd:
+ _, ok := event.(*events.TextMessageEndEvent)
+ if !ok {
+ return nil
+ }
+ curMsg := "text message ended"
+ return &Message{
+ contents: []string{curMsg},
+ }
+ case events.EventTypeToolCallStart:
+ _, ok := event.(*events.ToolCallStartEvent)
+ if !ok {
+ return nil
+ }
+ curMsg := "tool call started"
+ return &Message{
+ contents: []string{curMsg},
+ }
+ case events.EventTypeToolCallArgs:
+ args, ok := event.(*events.ToolCallArgsEvent)
+ if !ok {
+ return nil
+ }
+ curMsg := fmt.Sprintf("tool call args: %s", args.Delta)
+ return &Message{
+ contents: []string{curMsg},
+ }
+ case events.EventTypeToolCallEnd:
+ _, ok := event.(*events.ToolCallEndEvent)
+ if !ok {
+ return nil
+ }
+ curMsg := "tool call ended"
+ return &Message{
+ contents: []string{curMsg},
+ }
+ case events.EventTypeToolCallResult:
+ result, ok := event.(*events.ToolCallResultEvent)
+ if !ok {
+ return nil
+ }
+ curMsg := result.Content
+ return &Message{
+ contents: []string{curMsg},
+ }
+ case events.EventTypeStateSnapshot:
+ snapshot, ok := event.(*events.StateSnapshotEvent)
+ if !ok {
+ return nil
+ }
+ var contents []string
+ if snapshot.Snapshot != nil {
+ jsonData, err := json.Marshal(snapshot.Snapshot)
+ if err != nil {
+ fmt.Println("Error marshaling JSON:", err)
+ return nil
+ }
+ contents = append(contents, string(jsonData))
+
+ }
+ return &Message{
+ contents: contents,
+ }
+ case events.EventTypeStateDelta:
+ delta, ok := event.(*events.StateDeltaEvent)
+ if !ok {
+ return nil
+ }
+ var contents []string
+ for _, op := range delta.Delta {
+ currOp := fmt.Sprintf("%s Operation: %s, Path: %s, Value: %s", serverStyle.Render("Server:"), op.Op, op.Path, op.Value)
+ contents = append(contents, currOp)
+ }
+ return &Message{
+ contents: contents,
+ }
+ case events.EventTypeMessagesSnapshot:
+ snapshot, ok := event.(*events.MessagesSnapshotEvent)
+ if !ok {
+ return nil
+ }
+ var contents []string
+ for _, msg := range snapshot.Messages {
+ if msg.Content != nil && msg.Role != "user" {
+ contents = append(contents, *msg.Content)
+ }
+ if msg.ToolCalls != nil {
+ for _, toolCall := range msg.ToolCalls {
+ toolCallContent := serverStyle.Render("Tool Call: ") + toolCall.Function.Name + " - " + toolCall.Function.Arguments
+ contents = append(contents, toolCallContent)
+ }
+ }
+ }
+
+ return &Message{
+ contents: contents,
+ }
+ case events.EventTypeStepStarted:
+ stepEvent, ok := event.(*events.StepStartedEvent)
+ if !ok {
+ return nil
+ }
+ content := fmt.Sprintf("Step started: %s", stepEvent.StepName)
+ return &Message{
+ contents: []string{content},
+ }
+
+ case events.EventTypeStepFinished:
+ stepEvent, ok := event.(*events.StepFinishedEvent)
+ if !ok {
+ return nil
+ }
+ content := fmt.Sprintf("Step finished: %s", stepEvent.StepName)
+ return &Message{
+ contents: []string{content},
+ }
+ case events.EventTypeThinkingStart:
+ thinkingEvent, ok := event.(*events.ThinkingStartEvent)
+ if !ok {
+ return nil
+ }
+ content := "Thinking started"
+ if thinkingEvent.Title != nil {
+ content = fmt.Sprintf("Thinking started: %s", *thinkingEvent.Title)
+ }
+ return &Message{
+ contents: []string{content},
+ }
+ case events.EventTypeThinkingEnd:
+ _, ok := event.(*events.ThinkingEndEvent)
+ if !ok {
+ return nil
+ }
+ content := "Thinking ended"
+ return &Message{
+ contents: []string{content},
+ }
+ case events.EventTypeThinkingTextMessageStart:
+ _, ok := event.(*events.ThinkingTextMessageStartEvent)
+ if !ok {
+ return nil
+ }
+ content := "Thinking message started"
+ return &Message{
+ contents: []string{content},
+ }
+ case events.EventTypeThinkingTextMessageContent:
+ msg, ok := event.(*events.ThinkingTextMessageContentEvent)
+ if !ok {
+ return nil
+ }
+ return &Message{
+ contents: []string{msg.Delta},
+ }
+
+ case events.EventTypeThinkingTextMessageEnd:
+ _, ok := event.(*events.ThinkingTextMessageEndEvent)
+ if !ok {
+ return nil
+ }
+ content := "Thinking message ended"
+ return &Message{
+ contents: []string{content},
+ }
+
+ case events.EventTypeCustom:
+ evt, ok := event.(*events.CustomEvent)
+ if !ok {
+ return nil
+ }
+ jsonData, err := json.Marshal(evt.Value)
+ if err != nil {
+ fmt.Println("Error marshaling JSON:", err)
+ return nil
+ }
+ fmt.Println(evt)
+ return &Message{
+ contents: []string{string(jsonData)},
+ }
+
+ case events.EventTypeRaw:
+ rawEvent, ok := event.(*events.RawEvent)
+ if !ok {
+ return nil
+ }
+ jsonData, err := json.Marshal(rawEvent.Event)
+ if err != nil {
+ fmt.Println("Error marshaling raw event:", err)
+ return nil
+ }
+ return &Message{
+ contents: []string{string(jsonData)},
+ }
+
+ default:
+ // For any other event types, return nil
+ fmt.Printf("Unhandled event type: %s\n", eventType)
+ return nil
+ }
+}
diff --git a/sdks/community/go/example/client/internal/ui/model.go b/sdks/community/go/example/client/internal/ui/model.go
new file mode 100644
index 000000000..570b2009c
--- /dev/null
+++ b/sdks/community/go/example/client/internal/ui/model.go
@@ -0,0 +1,277 @@
+package ui
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/client/internal/message"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/textarea"
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+type UIMessage struct {
+ Role string
+ Content string
+ Timestamp time.Time
+}
+
+func NewUIMessage(role, content string) UIMessage {
+ return UIMessage{
+ Role: role,
+ Content: content,
+ Timestamp: time.Now(),
+ }
+}
+
+func (m UIMessage) String() string {
+ var roleStyle lipgloss.Style
+ var rolePrefix string
+
+ if m.Role == "user" {
+ roleStyle = UserLabelStyle
+ rolePrefix = "You"
+ } else {
+ roleStyle = AssistantLabelStyle
+ rolePrefix = "Assistant"
+ }
+
+ timestamp := m.Timestamp.Format("15:04")
+ header := fmt.Sprintf("%s %s", roleStyle.Render(rolePrefix), TimestampStyle.Render(timestamp))
+ content := MessageContentStyle.Render(m.Content)
+
+ return fmt.Sprintf("%s\n%s", header, content)
+}
+
+type Model struct {
+ messages []UIMessage
+ viewport viewport.Model
+ textarea textarea.Model
+ userInput chan string
+ ready bool
+ waitingForResp bool
+ typingDots int
+}
+
+func (m *Model) updateViewportContent() {
+ if len(m.messages) == 0 {
+ // Show splash screen when no messages
+ m.viewport.SetContent(getSplashScreen(m.viewport.Width, m.viewport.Height))
+ return
+ }
+
+ var content strings.Builder
+ for i, msg := range m.messages {
+ if i > 0 {
+ content.WriteString("\n\n")
+ }
+ content.WriteString(msg.String())
+ }
+
+ // Add typing indicator if waiting for response
+ if m.waitingForResp {
+ content.WriteString("\n\n")
+ content.WriteString(getTypingIndicator(m.typingDots))
+ }
+
+ m.viewport.SetContent(content.String())
+ m.viewport.GotoBottom()
+}
+
+func getTextarea() textarea.Model {
+ ta := textarea.New()
+ ta.Placeholder = "Send a message..."
+ ta.Focus()
+
+ ta.Prompt = "│ "
+ ta.CharLimit = 2000
+
+ ta.SetWidth(80)
+ ta.SetHeight(3)
+
+ // Style the textarea
+ ta.FocusedStyle.Base = lipgloss.NewStyle()
+ ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
+ ta.FocusedStyle.Placeholder = InputPlaceholderStyle
+ ta.FocusedStyle.Text = InputTextStyle
+ ta.FocusedStyle.Prompt = InputPromptStyle
+ ta.BlurredStyle = ta.FocusedStyle
+
+ ta.ShowLineNumbers = false
+ return ta
+}
+
+func InitialModel(userInput chan string) *Model {
+ vp := viewport.New(80, 20)
+ vp.KeyMap = viewport.KeyMap{
+ Up: key.NewBinding(key.WithKeys("up", "k")),
+ Down: key.NewBinding(key.WithKeys("down", "j")),
+ PageDown: key.NewBinding(key.WithKeys("pgdown")),
+ PageUp: key.NewBinding(key.WithKeys("pgup")),
+ }
+
+ return &Model{
+ viewport: vp,
+ textarea: getTextarea(),
+ userInput: userInput,
+ messages: []UIMessage{},
+ }
+}
+
+func (m *Model) Init() tea.Cmd {
+ return tea.Batch(textarea.Blink, tea.EnterAltScreen, tickCmd())
+}
+
+func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var (
+ tiCmd tea.Cmd
+ vpCmd tea.Cmd
+ )
+
+ m.textarea, tiCmd = m.textarea.Update(msg)
+ m.viewport, vpCmd = m.viewport.Update(msg)
+
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ headerHeight := 2
+ footHeight := lipgloss.Height(m.textareaView()) + 1
+ verticalMarginHeight := headerHeight + footHeight
+
+ if !m.ready {
+ // Since this program is using the full size of the viewport we
+ // need to wait until we've received the window dimensions before
+ // we can initialize the viewport. The initial dimensions come in
+ // quickly, though asynchronously, which is why we wait for them
+ // here.
+ m.viewport = viewport.New(msg.Width-4, msg.Height-verticalMarginHeight-2)
+ m.viewport.KeyMap = viewport.KeyMap{
+ Up: key.NewBinding(key.WithKeys("up", "k")),
+ Down: key.NewBinding(key.WithKeys("down", "j")),
+ PageDown: key.NewBinding(key.WithKeys("pgdown")),
+ PageUp: key.NewBinding(key.WithKeys("pgup")),
+ }
+ m.updateViewportContent()
+ m.textarea.SetWidth(msg.Width - 4)
+ m.ready = true
+ } else {
+ m.viewport.Width = msg.Width - 4
+ m.viewport.Height = msg.Height - verticalMarginHeight - 2
+ m.textarea.SetWidth(msg.Width - 4)
+ }
+
+ case tea.KeyMsg:
+ switch msg.Type {
+ case tea.KeyCtrlC:
+ return m, tea.Quit
+ case tea.KeyEsc:
+ if m.textarea.Focused() {
+ m.textarea.Blur()
+ m.viewport.YOffset = 0
+ }
+ case tea.KeyEnter:
+ if m.textarea.Focused() && m.textarea.Value() != "" && !m.waitingForResp {
+ // Send the message
+ m.userInput <- m.textarea.Value()
+
+ // Add to messages
+ uiMsg := NewUIMessage("user", m.textarea.Value())
+ m.messages = append(m.messages, uiMsg)
+ m.updateViewportContent()
+ m.textarea.Reset()
+ m.waitingForResp = true
+ }
+ default:
+ if !m.textarea.Focused() {
+ switch msg.String() {
+ case "i", "a":
+ // Enter input mode
+ m.textarea.Focus()
+ return m, textarea.Blink
+ }
+ }
+ }
+
+ case *message.Message:
+ m.waitingForResp = false
+ for _, currMsg := range msg.Strings() {
+ uiMsg := NewUIMessage("assistant", currMsg)
+ m.messages = append(m.messages, uiMsg)
+ }
+ m.updateViewportContent()
+
+ case tickMsg:
+ m.typingDots++
+ if m.waitingForResp {
+ m.updateViewportContent()
+ }
+ return m, tickCmd()
+
+ }
+
+ return m, tea.Batch(tiCmd, vpCmd)
+}
+
+func (m *Model) textareaView() string {
+ return InputContainerStyle.Render(m.textarea.View())
+}
+
+func (m *Model) View() string {
+ if !m.ready {
+ initMsg := lipgloss.NewStyle().
+ Foreground(primaryColor).
+ Bold(true).
+ Render("\n ✨ Initializing chat...")
+ return initMsg
+ }
+
+ // Header with message count
+ msgCount := ""
+ if len(m.messages) > 0 {
+ msgCount = TimestampStyle.Render(fmt.Sprintf(" (%d messages)", len(m.messages)))
+ }
+ header := HeaderStyle.Render("💬 Chat" + msgCount)
+
+ // Viewport with scroll indicator
+ viewportView := ViewportStyle.
+ Width(m.viewport.Width + 2).
+ Height(m.viewport.Height + 2).
+ Render(m.viewport.View())
+
+ // Add scroll percentage if there are messages
+ if len(m.messages) > 0 && m.viewport.TotalLineCount() > m.viewport.Height {
+ percent := float64(m.viewport.YOffset) / float64(m.viewport.TotalLineCount()-m.viewport.Height) * 100
+ if percent < 0 {
+ percent = 0
+ }
+ if percent > 100 {
+ percent = 100
+ }
+ scrollInfo := TimestampStyle.Render(fmt.Sprintf(" %.0f%% ", percent))
+ viewportView = lipgloss.JoinHorizontal(lipgloss.Top, viewportView, scrollInfo)
+ }
+
+ inputView := m.textareaView()
+
+ // Build help text with styled keys
+ helpItems := []string{
+ HelpKeyStyle.Render("i/a") + " " + HelpDescStyle.Render("input mode"),
+ HelpKeyStyle.Render("Esc") + " " + HelpDescStyle.Render("normal mode"),
+ HelpKeyStyle.Render("Enter") + " " + HelpDescStyle.Render("send"),
+ HelpKeyStyle.Render("Ctrl+C") + " " + HelpDescStyle.Render("quit"),
+ }
+ help := HelpStyle.Render(strings.Join(helpItems, " • "))
+
+ // Add input mode indicator
+ if m.textarea.Focused() {
+ inputMode := lipgloss.NewStyle().
+ Foreground(accentColor).
+ Bold(true).
+ Render(" [INPUT MODE]")
+ header += inputMode
+ }
+
+ return fmt.Sprintf("%s\n\n%s\n\n%s\n%s", header, viewportView, inputView, help)
+}
diff --git a/sdks/community/go/example/client/internal/ui/splash.go b/sdks/community/go/example/client/internal/ui/splash.go
new file mode 100644
index 000000000..c69e5561b
--- /dev/null
+++ b/sdks/community/go/example/client/internal/ui/splash.go
@@ -0,0 +1,51 @@
+package ui
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+)
+
+var (
+ splashStyle = lipgloss.NewStyle().
+ Foreground(primaryColor).
+ Bold(true).
+ Align(lipgloss.Center)
+
+ splashSubtitleStyle = lipgloss.NewStyle().
+ Foreground(mutedTextColor).
+ Align(lipgloss.Center).
+ MarginTop(1)
+)
+
+func getSplashScreen(width, height int) string {
+ logo := `
+ _____ _ _
+ / ____| | | |
+| | | |__ __ _| |_
+| | | '_ \ / _' | __|
+| |____| | | | (_| | |_
+ \_____|_| |_|\__,_|\__|
+`
+
+ subtitle := "Start typing to begin your conversation"
+
+ // Center the content vertically
+ topPadding := (height - 10) / 2
+ if topPadding < 0 {
+ topPadding = 0
+ }
+
+ content := splashStyle.Render(logo) + "\n" + splashSubtitleStyle.Render(subtitle)
+
+ // Add vertical padding
+ padding := strings.Repeat("\n", topPadding)
+
+ // Center horizontally
+ centered := lipgloss.Place(width, height,
+ lipgloss.Center, lipgloss.Center,
+ content,
+ )
+
+ return padding + centered
+}
\ No newline at end of file
diff --git a/sdks/community/go/example/client/internal/ui/theme.go b/sdks/community/go/example/client/internal/ui/theme.go
new file mode 100644
index 000000000..50ccf551f
--- /dev/null
+++ b/sdks/community/go/example/client/internal/ui/theme.go
@@ -0,0 +1,89 @@
+package ui
+
+import "github.com/charmbracelet/lipgloss"
+
+// Theme colors inspired by Charmbracelet's design
+var (
+ // Primary colors
+ primaryColor = lipgloss.Color("#FF79C6")
+ secondaryColor = lipgloss.Color("#8BE9FD")
+ accentColor = lipgloss.Color("#50FA7B")
+
+ // Background colors
+ bgColor = lipgloss.Color("#282A36")
+ bgLightColor = lipgloss.Color("#44475A")
+ bgDarkColor = lipgloss.Color("#191A21")
+
+ // Text colors
+ textColor = lipgloss.Color("#F8F8F2")
+ mutedTextColor = lipgloss.Color("#6272A4")
+ brightTextColor = lipgloss.Color("#FFFFFF")
+
+ // Status colors
+ successColor = lipgloss.Color("#50FA7B")
+ errorColor = lipgloss.Color("#FF5555")
+ warningColor = lipgloss.Color("#FFB86C")
+ infoColor = lipgloss.Color("#8BE9FD")
+)
+
+// Styles for the chat application
+var (
+ // Container styles
+ AppStyle = lipgloss.NewStyle().
+ Background(bgColor)
+
+ // Header styles
+ HeaderStyle = lipgloss.NewStyle().
+ Foreground(primaryColor).
+ Bold(true).
+ Padding(1, 2)
+
+ // Viewport styles
+ ViewportStyle = lipgloss.NewStyle().
+ BorderStyle(lipgloss.RoundedBorder()).
+ BorderForeground(bgLightColor).
+ Padding(1, 2)
+
+ // Message styles
+ UserLabelStyle = lipgloss.NewStyle().
+ Foreground(primaryColor).
+ Bold(true)
+
+ AssistantLabelStyle = lipgloss.NewStyle().
+ Foreground(secondaryColor).
+ Bold(true)
+
+ MessageContentStyle = lipgloss.NewStyle().
+ Foreground(textColor).
+ PaddingLeft(2)
+
+ TimestampStyle = lipgloss.NewStyle().
+ Foreground(mutedTextColor).
+ Italic(true)
+
+ // Input styles
+ InputContainerStyle = lipgloss.NewStyle().
+ BorderStyle(lipgloss.RoundedBorder()).
+ BorderForeground(bgLightColor).
+ Padding(0, 1)
+
+ InputPromptStyle = lipgloss.NewStyle().
+ Foreground(primaryColor)
+
+ InputTextStyle = lipgloss.NewStyle().
+ Foreground(textColor)
+
+ InputPlaceholderStyle = lipgloss.NewStyle().
+ Foreground(mutedTextColor)
+
+ // Help styles
+ HelpStyle = lipgloss.NewStyle().
+ Foreground(mutedTextColor).
+ Padding(1, 2)
+
+ HelpKeyStyle = lipgloss.NewStyle().
+ Foreground(secondaryColor)
+
+ HelpDescStyle = lipgloss.NewStyle().
+ Foreground(mutedTextColor)
+)
\ No newline at end of file
diff --git a/sdks/community/go/example/client/internal/ui/typing.go b/sdks/community/go/example/client/internal/ui/typing.go
new file mode 100644
index 000000000..9687d6084
--- /dev/null
+++ b/sdks/community/go/example/client/internal/ui/typing.go
@@ -0,0 +1,22 @@
+package ui
+
+import (
+ "time"
+
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+type tickMsg time.Time
+
+func tickCmd() tea.Cmd {
+ return tea.Tick(time.Millisecond*500, func(t time.Time) tea.Msg {
+ return tickMsg(t)
+ })
+}
+
+func getTypingIndicator(dots int) string {
+ indicators := []string{
+ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
+ }
+ return AssistantLabelStyle.Render("Assistant is typing " + indicators[dots%len(indicators)])
+}
\ No newline at end of file
diff --git a/sdks/community/go/example/server/cmd/main.go b/sdks/community/go/example/server/cmd/main.go
new file mode 100644
index 000000000..ae61d66d1
--- /dev/null
+++ b/sdks/community/go/example/server/cmd/main.go
@@ -0,0 +1,175 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/server/internal/config"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/server/internal/mcp"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/server/internal/routes"
+ "github.com/gofiber/fiber/v3"
+ "github.com/gofiber/fiber/v3/middleware/cors"
+ "github.com/gofiber/fiber/v3/middleware/requestid"
+ "github.com/sirupsen/logrus"
+)
+
+func newErrorHandler() fiber.ErrorHandler {
+ return func(c fiber.Ctx, err error) error {
+ code := fiber.StatusInternalServerError
+ var ferr *fiber.Error
+ if errors.As(err, &ferr) {
+ code = ferr.Code
+ }
+
+ entry := logrus.NewEntry(logrus.StandardLogger())
+ entry.WithFields(logrus.Fields{
+ "error": err.Error(),
+ "status": code,
+ }).Error("Request error")
+
+ return c.Status(code).JSON(fiber.Map{
+ "error": true,
+ "message": err.Error(),
+ })
+ }
+}
+
+func registerRoutes(app *fiber.App, cfg *config.Config) {
+
+ // Basic info route
+ app.Get("/", func(c fiber.Ctx) error {
+ return c.JSON(fiber.Map{
+ "message": "AG-UI Go Example Server is running!",
+ "path": c.Path(),
+ "method": c.Method(),
+ "headers": c.GetReqHeaders(),
+ })
+ })
+
+ if !cfg.EnableSSE {
+ return
+ }
+
+ // Feature routes
+ app.Post("/agentic", routes.AgenticHandler(cfg))
+}
+
+func logConfig(logger *logrus.Logger, cfg *config.Config) {
+ logger.WithFields(logrus.Fields{
+ "host": cfg.Host,
+ "port": cfg.Port,
+ "log_level": cfg.LogLevel,
+ "enable_sse": cfg.EnableSSE,
+ "read_timeout": cfg.ReadTimeout,
+ "write_timeout": cfg.WriteTimeout,
+ "sse_keepalive": cfg.SSEKeepAlive,
+ "cors_enabled": cfg.CORSEnabled,
+ "streaming_chunk_delay": cfg.StreamingChunkDelay,
+ }).Info("Server configuration loaded")
+}
+
+func createApp(cfg *config.Config, logger *logrus.Logger) *fiber.App {
+ app := fiber.New(fiber.Config{
+ AppName: "AG-UI Example Server",
+ ReadTimeout: cfg.ReadTimeout,
+ WriteTimeout: cfg.WriteTimeout,
+ ErrorHandler: newErrorHandler(),
+ })
+
+ // Middleware
+ app.Use(requestid.New())
+
+ // CORS
+ if cfg.CORSEnabled {
+ app.Use(cors.New(cors.Config{
+ AllowOrigins: cfg.CORSAllowedOrigins,
+ AllowMethods: []string{"GET", "POST", "HEAD", "PUT", "DELETE", "PATCH", "OPTIONS"},
+ AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"},
+ AllowCredentials: false,
+ }))
+ }
+
+ // Content negotiation
+ //app.Use(encoding.ContentNegotiationMiddleware(encoding.ContentNegotiationConfig{
+ // DefaultContentType: "application/json",
+ // SupportedTypes: []string{"application/json", "application/vnd.ag-ui+json"},
+ // EnableLogging: cfg.LogLevel == "debug",
+ //}))
+
+ // Routes
+ registerRoutes(app, cfg)
+
+ return app
+}
+
+func main() {
+ // Load configuration with proper precedence: flags > env > defaults
+ cfg, err := config.LoadConfig()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Set up structured logging with logrus
+ logger := logrus.New()
+
+ // Log the effective configuration
+ logConfig(logger, cfg)
+
+ app := createApp(cfg, logger)
+
+ // Start server in a goroutine
+ serverAddr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
+
+ go func() {
+ logger.WithField("address", serverAddr).Info("Starting server")
+ if err := app.Listen(serverAddr); err != nil {
+ logger.WithError(err).Error("Server failed to start")
+ os.Exit(1)
+ }
+ }()
+
+ logger.WithField("address", serverAddr).Info("Server started successfully")
+
+ // Start mcp in a goroutine
+ mcpServer, err := mcp.NewServer(mcp.DefaultPort)
+ if err != nil {
+ logger.WithError(err).Error("Failed to create MCP server")
+ os.Exit(1)
+ }
+ go func() {
+ err := mcpServer.Start()
+ if err != nil {
+ logger.WithError(err).Error("MCP server failed to start")
+ os.Exit(1)
+ }
+ }()
+
+ // Wait for interrupt signal to gracefully shutdown the server
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ <-quit
+
+ logger.Info("Shutting down server...")
+
+ // Graceful shutdown with timeout
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ err = mcpServer.Shutdown(ctx)
+ if err != nil {
+ logger.WithError(err).Error("MCP server shutdown error")
+ }
+
+ if err = app.ShutdownWithContext(ctx); err != nil {
+ logger.WithError(err).Error("Server shutdown error")
+ os.Exit(1)
+ }
+
+ logger.Info("Server shutdown complete")
+}
diff --git a/sdks/community/go/example/server/go.mod b/sdks/community/go/example/server/go.mod
new file mode 100644
index 000000000..3c34c7ded
--- /dev/null
+++ b/sdks/community/go/example/server/go.mod
@@ -0,0 +1,66 @@
+module github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/server
+
+go 1.24.4
+
+require (
+ github.com/ag-ui-protocol/ag-ui/sdks/community/go v0.0.0-00010101000000-000000000000
+ github.com/gofiber/fiber/v3 v3.0.0-beta.5
+ github.com/i2y/langchaingo-mcp-adapter v0.0.0-20250623114610-a01671e1c8df
+ github.com/mark3labs/mcp-go v0.32.0
+ github.com/sirupsen/logrus v1.9.3
+ github.com/stretchr/testify v1.10.0
+ github.com/tmc/langchaingo v0.1.13
+ golang.org/x/sync v0.16.0
+)
+
+require (
+ github.com/Masterminds/goutils v1.1.1 // indirect
+ github.com/Masterminds/semver/v3 v3.2.0 // indirect
+ github.com/Masterminds/sprig/v3 v3.2.3 // indirect
+ github.com/andybalholm/brotli v1.2.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/dlclark/regexp2 v1.10.0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/gofiber/schema v1.6.0 // indirect
+ github.com/gofiber/utils/v2 v2.0.0-beta.13 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/goph/emperror v0.17.2 // indirect
+ github.com/huandu/xstrings v1.3.3 // indirect
+ github.com/imdario/mergo v0.3.13 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mitchellh/copystructure v1.0.0 // indirect
+ github.com/mitchellh/reflectwalk v1.0.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/nikolalohinski/gonja v1.5.3 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/philhofer/fwd v1.2.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pkoukk/tiktoken-go v0.1.6 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/rogpeppe/go-internal v1.13.1 // indirect
+ github.com/shopspring/decimal v1.2.0 // indirect
+ github.com/spf13/cast v1.7.1 // indirect
+ github.com/tinylib/msgp v1.3.0 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasthttp v1.64.0 // indirect
+ github.com/yargevad/filepathx v1.0.0 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+ go.opentelemetry.io/otel v1.35.0 // indirect
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/trace v1.35.0 // indirect
+ go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
+ golang.org/x/crypto v0.40.0 // indirect
+ golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
+ golang.org/x/net v0.42.0 // indirect
+ golang.org/x/sys v0.34.0 // indirect
+ golang.org/x/text v0.27.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/sdks/community/go/example/server/go.sum b/sdks/community/go/example/server/go.sum
new file mode 100644
index 000000000..7d3699eaf
--- /dev/null
+++ b/sdks/community/go/example/server/go.sum
@@ -0,0 +1,375 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY=
+cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E=
+cloud.google.com/go/ai v0.7.0 h1:P6+b5p4gXlza5E+u7uvcgYlzZ7103ACg70YdZeC6oGE=
+cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo=
+cloud.google.com/go/aiplatform v1.68.0 h1:EPPqgHDJpBZKRvv+OsB3cr0jYz3EL2pZ+802rBPcG8U=
+cloud.google.com/go/aiplatform v1.68.0/go.mod h1:105MFA3svHjC3Oazl7yjXAmIR89LKhRAeNdnDKJczME=
+cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw=
+cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
+cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
+cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
+cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
+cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
+cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
+cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8=
+cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8=
+github.com/AssemblyAI/assemblyai-go-sdk v1.3.0 h1:AtOVgGxUycvK4P4ypP+1ZupecvFgnfH+Jsum0o5ILoU=
+github.com/AssemblyAI/assemblyai-go-sdk v1.3.0/go.mod h1:H0naZbvpIW49cDA5ZZ/gggeXqi7ojSGB1mqshRk6kNE=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
+github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
+github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
+github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
+github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
+github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
+github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
+github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
+github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
+github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
+github.com/getzep/zep-go v1.0.4 h1:09o26bPP2RAPKFjWuVWwUWLbtFDF/S8bfbilxzeZAAg=
+github.com/getzep/zep-go v1.0.4/go.mod h1:HC1Gz7oiyrzOTvzeKC4dQKUiUy87zpIJl0ZFXXdHuss=
+github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
+github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/gofiber/fiber/v3 v3.0.0-beta.5 h1:MSGbiQZEYiYOqti2Ip2zMRkN4VvZw7Vo7dwZBa1Qjk8=
+github.com/gofiber/fiber/v3 v3.0.0-beta.5/go.mod h1:XmI2Agulde26YcQrA2n8X499I1p98/zfCNbNObVUeP8=
+github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
+github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
+github.com/gofiber/utils/v2 v2.0.0-beta.13 h1:dlpbGFLveQ9OduL2UHw4dtu4lXE+Gb3bHMc+8Yxp/dk=
+github.com/gofiber/utils/v2 v2.0.0-beta.13/go.mod h1:qEZ175nSOkl5xciHmqxwNDsWzwiB39gB8RgU1d3U4mQ=
+github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/generative-ai-go v0.15.1 h1:n8aQUpvhPOlGVuM2DRkJ2jvx04zpp42B778AROJa+pQ=
+github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
+github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
+github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
+github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
+github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
+github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/i2y/langchaingo-mcp-adapter v0.0.0-20250623114610-a01671e1c8df h1:4lTJXCZw16BF0BCzrQ1LUzlMW4+2OwBkkYj1/bRybhY=
+github.com/i2y/langchaingo-mcp-adapter v0.0.0-20250623114610-a01671e1c8df/go.mod h1:oL2JAtsIp/1vnVy4UG4iDzL8SZwkOzqvRL3YR9PGPjs=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
+github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
+github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
+github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
+github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
+github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
+github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
+github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
+github.com/shamaton/msgpack/v2 v2.2.3 h1:uDOHmxQySlvlUYfQwdjxyybAOzjlQsD1Vjy+4jmO9NM=
+github.com/shamaton/msgpack/v2 v2.2.3/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
+github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
+github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
+github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
+github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
+github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
+github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
+github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 h1:K+bMSIx9A7mLES1rtG+qKduLIXq40DAzYHtb0XuCukA=
+gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow=
+gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g=
+gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
+gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a h1:O85GKETcmnCNAfv4Aym9tepU8OE0NmcZNqPlXcsBKBs=
+gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a/go.mod h1:LaSIs30YPGs1H5jwGgPhLzc8vkNc/k0rDX/fEZqiU/M=
+gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84 h1:qqjvoVXdWIcZCLPMlzgA7P9FZWdPGPvP/l3ef8GzV6o=
+gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84/go.mod h1:IJZ+fdMvbW2qW6htJx7sLJ04FEs4Ldl/MDsJtMKywfw=
+gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f h1:Wku8eEdeJqIOFHtrfkYUByc4bCaTeA6fL0UJgfEiFMI=
+gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
+go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
+golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
+golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE=
+google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE=
+google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
+google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
+google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
+nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/sdks/community/go/example/server/internal/agentic/agentic.go b/sdks/community/go/example/server/internal/agentic/agentic.go
new file mode 100644
index 000000000..9a4ae1ac6
--- /dev/null
+++ b/sdks/community/go/example/server/internal/agentic/agentic.go
@@ -0,0 +1,99 @@
+package agentic
+
+import (
+ "bufio"
+ "context"
+ _ "embed"
+ "fmt"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/server/internal/mcp"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/tmc/langchaingo/agents"
+ "github.com/tmc/langchaingo/chains"
+ "github.com/tmc/langchaingo/llms/anthropic"
+ "golang.org/x/sync/errgroup"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/sse"
+ langchaingoTools "github.com/tmc/langchaingo/tools"
+)
+
+// reminder is a reminder for the AI to output in our expected format.
+//
+//go:embed data/reminder.md
+var reminder string
+
+func CallLLM(ctx context.Context, input string, tools []langchaingoTools.Tool, returnChan chan<- string) error {
+ // adapter for the mcp server defined in sdks/community/go/example/server/internal/mcp
+ // this mcp server starts with the Fiber server in sdks/community/go/example/server/cmd/main.go
+ adapter, err := mcp.NewAdapter(fmt.Sprintf("http://127.0.0.1:%d/mcp", mcp.DefaultPort))
+
+ if err != nil {
+ return fmt.Errorf("new mcp adapter: %w", err)
+ }
+
+ _, err = adapter.Tools()
+ if err != nil {
+ return fmt.Errorf("append tools: %w", err)
+ }
+
+ llm, err := anthropic.New(anthropic.WithModel("claude-3-haiku-20240307"))
+ if err != nil {
+ return fmt.Errorf("failed to create LLM client: %w", err)
+ }
+
+ agent := agents.NewOneShotAgent(llm,
+ tools,
+ agents.WithMaxIterations(50))
+
+ executor := agents.NewExecutor(agent, agents.WithCallbacksHandler(NewHandler(returnChan)))
+
+ inputMap := make(map[string]any)
+ inputMap["input"] = input + "\n" + reminder
+
+ result, err := chains.Call(ctx, executor, inputMap)
+ if err != nil {
+ return fmt.Errorf("run chain: %w", err)
+ }
+ output := result["output"].(string)
+
+ // Create a proper event for the final output
+ messageID := events.GenerateMessageID()
+ finalMessage := events.NewTextMessageContentEvent(messageID, output)
+ if jsonData, err := finalMessage.ToJSON(); err == nil {
+ returnChan <- string(jsonData)
+ }
+
+ return nil
+}
+
+func ProcessInput(ctx context.Context, w *bufio.Writer, sseWriter *sse.SSEWriter, input string) error {
+ resultChan := make(chan string)
+ g, groupCtx := errgroup.WithContext(ctx)
+
+ g.Go(func() error {
+ for {
+ select {
+ case result := <-resultChan:
+ if result == "" {
+ return nil
+ }
+
+ // All messages from the handler should now be proper JSON events
+ // WriteBytes will format them as SSE frames with "data: " prefix
+ if err := sseWriter.WriteBytes(ctx, w, []byte(result)); err != nil {
+ return fmt.Errorf("failed to write event: %w", err)
+ }
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+ })
+
+ g.Go(func() error {
+ callLLMErr := CallLLM(groupCtx, input, nil, resultChan)
+ close(resultChan)
+ return callLLMErr
+ })
+
+ return g.Wait()
+}
diff --git a/sdks/community/go/example/server/internal/agentic/agentic_integration_test.go b/sdks/community/go/example/server/internal/agentic/agentic_integration_test.go
new file mode 100644
index 000000000..a464952c5
--- /dev/null
+++ b/sdks/community/go/example/server/internal/agentic/agentic_integration_test.go
@@ -0,0 +1,66 @@
+package agentic
+
+import (
+ "context"
+ _ "embed"
+ "os"
+ "testing"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/server/internal/mcp"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/sync/errgroup"
+)
+
+//go:embed data/client_prompt.md
+var client_prompt string
+
+//go:embed data/languages_prompt.md
+var languages_prompt string
+
+// Verifies agent communication is working and it can call tools we pass to it
+func TestToolCalls(t *testing.T) {
+ if os.Getenv("RUN_INTEGRATION_TESTS") != "true" {
+ t.Skip("Skipping integration test")
+ }
+ mcpServer, err := mcp.NewServer(mcp.DefaultPort)
+ require.NoError(t, err)
+ go func() {
+ mcpErr := mcpServer.Start()
+ if mcpErr != nil {
+ require.NoError(t, mcpErr)
+ }
+ }()
+
+ ctx := context.Background()
+ resultChan := make(chan string)
+ g, groupCtx := errgroup.WithContext(ctx)
+ var results []string
+
+ g.Go(func() error {
+ for {
+ select {
+ case result := <-resultChan:
+ if result == "" {
+ return nil
+ }
+ results = append(results, result)
+ case <-groupCtx.Done():
+ return groupCtx.Err()
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+ })
+
+ g.Go(func() error {
+ callErr := CallLLM(groupCtx, languages_prompt, nil, resultChan)
+ close(resultChan)
+ return callErr
+ })
+
+ if err = g.Wait(); err != nil {
+ require.NoError(t, err)
+ }
+ require.NoError(t, err)
+ require.NotEmpty(t, results)
+}
diff --git a/sdks/community/go/example/server/internal/agentic/data/languages_prompt.md b/sdks/community/go/example/server/internal/agentic/data/languages_prompt.md
new file mode 100644
index 000000000..1f1cc6fb9
--- /dev/null
+++ b/sdks/community/go/example/server/internal/agentic/data/languages_prompt.md
@@ -0,0 +1,21 @@
+# Get Available Languages Prompt
+
+## Agent Instructions
+
+The AG-UI README.md contains a list of Language SDKs. You are to look at "Programming Languages peoples are learning" and create a list of 4 possible programming languages we could add support for. These need to be programming languages with full support for server sent events when running on the command line. So HTML or CSS are not appropriate languages but something like C++ or Elixir could be. We then want to use the provide_language_options tool to create a report of these.
+
+## Project Information
+Read through relevant documentation and create a list of 4 programming languages we could add support for.
+
+### Links to Relevant Documentation
+AG-UI Docs
+https://docs.ag-ui.com/llms-full.txt
+
+AG-UI README.md
+https://github.com/ag-ui-protocol/ag-ui/blob/main/README.md
+
+Programming Languages peoples are learning
+https://survey.stackoverflow.co/2025/technology#most-popular-technologies-language-learn
+
+### Specific Requirements
+provide_language_options tool is called with 4 options
diff --git a/sdks/community/go/example/server/internal/agentic/data/reminder.md b/sdks/community/go/example/server/internal/agentic/data/reminder.md
new file mode 100644
index 000000000..14fcdf2e1
--- /dev/null
+++ b/sdks/community/go/example/server/internal/agentic/data/reminder.md
@@ -0,0 +1,2 @@
+## MOST IMPORTANT REMINDER
+Your final output MUST start with the string "Final Answer: "
\ No newline at end of file
diff --git a/sdks/community/go/example/server/internal/agentic/handler.go b/sdks/community/go/example/server/internal/agentic/handler.go
new file mode 100644
index 000000000..84ec54244
--- /dev/null
+++ b/sdks/community/go/example/server/internal/agentic/handler.go
@@ -0,0 +1,308 @@
+package agentic
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/tmc/langchaingo/llms"
+ "github.com/tmc/langchaingo/schema"
+)
+
+type Handler struct {
+ returnChan chan<- string
+ // ID tracking for event correlation
+ threadID string
+ runID string
+ messageID string
+ toolCallID string
+ stepID string
+}
+
+func NewHandler(returnChan chan<- string) *Handler {
+ return &Handler{
+ returnChan: returnChan,
+ threadID: events.GenerateThreadID(),
+ runID: events.GenerateRunID(),
+ }
+}
+
+func (h *Handler) HandleText(ctx context.Context, text string) {
+ message := events.NewTextMessageContentEvent("test", text)
+ if jsonData, err := message.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
+
+func (h *Handler) HandleLLMStart(ctx context.Context, prompts []string) {
+ // Generate new message ID for this LLM interaction
+ h.messageID = events.GenerateMessageID()
+
+ // Send run started event
+ runStartedEvent := events.NewRunStartedEvent(h.threadID, h.runID)
+ if jsonData, err := runStartedEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ // Send text message start event
+ textStartEvent := events.NewTextMessageStartEvent(h.messageID, events.WithRole("assistant"))
+ if jsonData, err := textStartEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
+
+func (h *Handler) HandleLLMGenerateContentStart(ctx context.Context, ms []llms.MessageContent) {
+ // Generate new message ID if not already set
+ if h.messageID == "" {
+ h.messageID = events.GenerateMessageID()
+ }
+
+ // Determine role from message content
+ role := "assistant"
+ if len(ms) > 0 {
+ // Check if this is from a tool or user
+ for _, m := range ms {
+ if len(m.Parts) > 0 {
+ if _, ok := m.Parts[0].(llms.ToolCallResponse); ok {
+ role = "tool"
+ break
+ }
+ }
+ }
+ }
+
+ // Send text message start event
+ textStartEvent := events.NewTextMessageStartEvent(h.messageID, events.WithRole(role))
+ if jsonData, err := textStartEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
+
+func (h *Handler) HandleLLMGenerateContentEnd(ctx context.Context, res *llms.ContentResponse) {
+ // Send text message end event
+ if h.messageID != "" {
+ textEndEvent := events.NewTextMessageEndEvent(h.messageID)
+ if jsonData, err := textEndEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+ }
+
+ // Reset message ID for next interaction
+ h.messageID = ""
+}
+
+func (h *Handler) HandleLLMError(ctx context.Context, err error) {
+ // Send error as text message if we have an active message
+ if h.messageID != "" {
+ errorMessage := events.NewTextMessageContentEvent(h.messageID, "Error: "+err.Error())
+ if jsonData, err := errorMessage.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ // End the message
+ textEndEvent := events.NewTextMessageEndEvent(h.messageID)
+ if jsonData, err := textEndEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+ h.messageID = ""
+ }
+}
+
+func (h *Handler) HandleChainStart(ctx context.Context, inputs map[string]any) {
+ // Generate step ID for this chain execution
+ h.stepID = events.GenerateStepID()
+
+ // Send step started event with step name
+ stepStartedEvent := events.NewStepStartedEvent(h.stepID)
+ if jsonData, err := stepStartedEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
+
+func (h *Handler) HandleChainEnd(ctx context.Context, outputs map[string]any) {
+ // Send step finished event
+ if h.stepID != "" {
+ stepFinishedEvent := events.NewStepFinishedEvent(h.stepID)
+ if jsonData, err := stepFinishedEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+ h.stepID = ""
+ }
+}
+
+func (h *Handler) HandleChainError(ctx context.Context, err error) {
+ // Send error event for chain
+ if h.stepID != "" {
+ // Create error message
+ if h.messageID == "" {
+ h.messageID = events.GenerateMessageID()
+ }
+ errorMessage := events.NewTextMessageContentEvent(h.messageID, "Chain error: "+err.Error())
+ if jsonData, err := errorMessage.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ // Mark step as finished even with error
+ stepFinishedEvent := events.NewStepFinishedEvent(h.stepID)
+ if jsonData, err := stepFinishedEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+ h.stepID = ""
+ }
+}
+
+func (h *Handler) HandleToolStart(ctx context.Context, input string) {
+ // Generate tool call ID
+ h.toolCallID = events.GenerateToolCallID()
+
+ // Extract tool name from input if possible
+ toolName := "tool"
+
+ // Send tool call start event
+ toolStartEvent := events.NewToolCallStartEvent(h.toolCallID, toolName)
+ if h.messageID != "" {
+ toolStartEvent = events.NewToolCallStartEvent(h.toolCallID, toolName, events.WithParentMessageID(h.messageID))
+ }
+ if jsonData, err := toolStartEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ // Send tool arguments
+ toolArgsEvent := events.NewToolCallArgsEvent(h.toolCallID, input)
+ if jsonData, err := toolArgsEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
+
+func (h *Handler) HandleToolEnd(ctx context.Context, output string) {
+ if h.toolCallID != "" {
+ // Send tool call end event
+ toolEndEvent := events.NewToolCallEndEvent(h.toolCallID)
+ if jsonData, err := toolEndEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ // Send tool call result event
+ resultMessageID := events.GenerateMessageID()
+ toolResultEvent := events.NewToolCallResultEvent(resultMessageID, h.toolCallID, output)
+ if jsonData, err := toolResultEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ h.toolCallID = ""
+ }
+}
+
+func (h *Handler) HandleToolError(ctx context.Context, err error) {
+ if h.toolCallID != "" {
+ // Send error as tool result
+ resultMessageID := events.GenerateMessageID()
+ toolResultEvent := events.NewToolCallResultEvent(resultMessageID, h.toolCallID, "Error: "+err.Error())
+ if jsonData, err := toolResultEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ // Send tool call end event
+ toolEndEvent := events.NewToolCallEndEvent(h.toolCallID)
+ if jsonData, err := toolEndEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ h.toolCallID = ""
+ }
+}
+
+func (h *Handler) HandleAgentAction(ctx context.Context, action schema.AgentAction) {
+ // Agent is taking an action (usually a tool call)
+ // Generate tool call ID if not already set
+ if h.toolCallID == "" {
+ h.toolCallID = events.GenerateToolCallID()
+ }
+
+ // Send tool call start event for the action
+ toolStartEvent := events.NewToolCallStartEvent(h.toolCallID, action.Tool)
+ if h.messageID != "" {
+ toolStartEvent = events.NewToolCallStartEvent(h.toolCallID, action.Tool, events.WithParentMessageID(h.messageID))
+ }
+ if jsonData, err := toolStartEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ // Send the tool input as arguments
+ toolArgsEvent := events.NewToolCallArgsEvent(h.toolCallID, action.ToolInput)
+ if jsonData, err := toolArgsEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
+
+func (h *Handler) HandleAgentFinish(ctx context.Context, finish schema.AgentFinish) {
+ // Agent has finished its run
+ // Send final message if we have output
+ if finish.ReturnValues != nil {
+ if output, ok := finish.ReturnValues["output"].(string); ok && output != "" {
+ if h.messageID == "" {
+ h.messageID = events.GenerateMessageID()
+ }
+ finalMessage := events.NewTextMessageContentEvent(h.messageID, output)
+ if jsonData, err := finalMessage.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+
+ // End the message
+ textEndEvent := events.NewTextMessageEndEvent(h.messageID)
+ if jsonData, err := textEndEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+ }
+ }
+
+ // Send run finished event
+ runFinishedEvent := events.NewRunFinishedEvent(h.threadID, h.runID)
+ if jsonData, err := runFinishedEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
+
+func (h *Handler) HandleRetrieverStart(ctx context.Context, query string) {
+ // Start a retrieval operation
+ if h.messageID == "" {
+ h.messageID = events.GenerateMessageID()
+ }
+
+ // Send a message indicating retrieval is starting
+ retrievalMessage := events.NewTextMessageContentEvent(h.messageID, "Searching for: "+query)
+ if jsonData, err := retrievalMessage.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
+
+func (h *Handler) HandleRetrieverEnd(ctx context.Context, query string, documents []schema.Document) {
+ // Retrieval operation completed
+ if h.messageID != "" {
+ // Send message about retrieval results
+ resultMsg := fmt.Sprintf("Found %d documents for query: %s", len(documents), query)
+ retrievalResult := events.NewTextMessageContentEvent(h.messageID, resultMsg)
+ if jsonData, err := retrievalResult.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+ }
+}
+
+func (h *Handler) HandleStreamingFunc(ctx context.Context, chunk []byte) {
+ // Handle streaming content chunks
+ if h.messageID == "" {
+ h.messageID = events.GenerateMessageID()
+ // Send start event for streaming
+ textStartEvent := events.NewTextMessageStartEvent(h.messageID, events.WithRole("assistant"))
+ if jsonData, err := textStartEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+ }
+
+ // Send the chunk as content
+ chunkStr := string(chunk)
+ contentEvent := events.NewTextMessageContentEvent(h.messageID, chunkStr)
+ if jsonData, err := contentEvent.ToJSON(); err == nil {
+ h.returnChan <- string(jsonData)
+ }
+}
diff --git a/sdks/community/go/example/server/internal/config/config.go b/sdks/community/go/example/server/internal/config/config.go
new file mode 100644
index 000000000..f10d19bc3
--- /dev/null
+++ b/sdks/community/go/example/server/internal/config/config.go
@@ -0,0 +1,227 @@
+package config
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "log/slog"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Config holds all server configuration values
+type Config struct {
+ // Server settings
+ Host string
+ Port int
+
+ // Logging
+ LogLevel string
+
+ // Transport settings
+ EnableSSE bool
+
+ // Timeout settings
+ ReadTimeout time.Duration
+ WriteTimeout time.Duration
+ SSEKeepAlive time.Duration
+
+ // CORS settings
+ CORSEnabled bool
+ CORSAllowedOrigins []string
+
+ // Streaming settings
+ StreamingChunkDelay time.Duration
+}
+
+// envVar defines an environment variable handler
+type envVar struct {
+ key string
+ apply func(string) error
+}
+
+// getEnvHandlers builds handlers for environment variables
+func getEnvHandlers(c *Config) []envVar {
+ return []envVar{
+ {"AGUI_HOST", func(v string) error { c.Host = v; return nil }},
+ {"AGUI_PORT", func(v string) error {
+ port, err := strconv.Atoi(v)
+ if err != nil {
+ return fmt.Errorf("invalid AGUI_PORT value '%s': %w", v, err)
+ }
+ c.Port = port
+ return nil
+ }},
+ }
+}
+
+// Default configuration values
+const (
+ DefaultHost = "0.0.0.0"
+ DefaultPort = 8000
+ DefaultLogLevel = "info"
+ DefaultEnableSSE = true
+ DefaultReadTimeout = 30 * time.Second
+ DefaultWriteTimeout = 30 * time.Second
+ DefaultSSEKeepAlive = 15 * time.Second
+ DefaultStreamingChunkDelay = 200 * time.Millisecond
+)
+
+// Default CORS allowed origins
+var DefaultCORSAllowedOrigins = []string{"*"}
+
+// Valid log levels
+var ValidLogLevels = map[string]slog.Level{
+ "debug": slog.LevelDebug,
+ "info": slog.LevelInfo,
+ "warn": slog.LevelWarn,
+ "error": slog.LevelError,
+}
+
+// New creates a new Config with default values
+func New() *Config {
+ return &Config{
+ Host: DefaultHost,
+ Port: DefaultPort,
+ LogLevel: DefaultLogLevel,
+ EnableSSE: DefaultEnableSSE,
+ ReadTimeout: DefaultReadTimeout,
+ WriteTimeout: DefaultWriteTimeout,
+ SSEKeepAlive: DefaultSSEKeepAlive,
+ CORSEnabled: true,
+ CORSAllowedOrigins: DefaultCORSAllowedOrigins,
+ StreamingChunkDelay: DefaultStreamingChunkDelay,
+ }
+}
+
+// LoadFromEnv loads configuration from environment variables with AGUI_ prefix
+func (c *Config) LoadFromEnv() error {
+ for _, h := range getEnvHandlers(c) {
+ if val := os.Getenv(h.key); val == "" {
+ continue
+ }
+ if err := h.apply(os.Getenv(h.key)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Validate validates the configuration values
+func (c *Config) Validate() error {
+ var errs []error
+
+ // Validate port range
+ if c.Port < 1 || c.Port > 65535 {
+ errs = append(errs, fmt.Errorf("port must be between 1 and 65535, got %d", c.Port))
+ }
+
+ // Validate log level
+ if _, ok := ValidLogLevels[c.LogLevel]; !ok {
+ validLevels := make([]string, 0, len(ValidLogLevels))
+ for level := range ValidLogLevels {
+ validLevels = append(validLevels, level)
+ }
+ errs = append(errs, fmt.Errorf("invalid log level '%s', must be one of: %s", c.LogLevel, strings.Join(validLevels, ", ")))
+ }
+
+ // Validate timeout durations are non-negative
+ if c.ReadTimeout < 0 {
+ errs = append(errs, fmt.Errorf("read timeout must be non-negative, got %v", c.ReadTimeout))
+ }
+
+ if c.WriteTimeout < 0 {
+ errs = append(errs, fmt.Errorf("write timeout must be non-negative, got %v", c.WriteTimeout))
+ }
+
+ if c.SSEKeepAlive < 0 {
+ errs = append(errs, fmt.Errorf("SSE keep-alive must be non-negative, got %v", c.SSEKeepAlive))
+ }
+
+ if c.StreamingChunkDelay < 0 {
+ errs = append(errs, fmt.Errorf("streaming chunk delay must be non-negative, got %v", c.StreamingChunkDelay))
+ }
+
+ if len(errs) > 0 {
+ return errors.Join(errs...)
+ }
+
+ return nil
+}
+
+// LogLevel returns the slog.Level for the configured log level
+func (c *Config) GetLogLevel() slog.Level {
+ level, ok := ValidLogLevels[c.LogLevel]
+ if !ok {
+ return slog.LevelInfo // fallback to info if invalid
+ }
+ return level
+}
+
+// LoadFromFlags loads configuration from command line flags with precedence over env vars
+func (c *Config) LoadFromFlags() error {
+ var (
+ host = flag.String("host", c.Host, "Server host address")
+ port = flag.Int("port", c.Port, "Server port (1-65535)")
+ logLevel = flag.String("log-level", c.LogLevel, "Log level (debug, info, warn, error)")
+ enableSSE = flag.Bool("enable-sse", c.EnableSSE, "Enable Server-Sent Events")
+ readTimeout = flag.Duration("read-timeout", c.ReadTimeout, "Read timeout duration")
+ writeTimeout = flag.Duration("write-timeout", c.WriteTimeout, "Write timeout duration")
+ sseKeepAlive = flag.Duration("sse-keepalive", c.SSEKeepAlive, "SSE keep-alive duration")
+ corsEnabled = flag.Bool("cors-enabled", c.CORSEnabled, "Enable CORS")
+ )
+
+ flag.Parse()
+
+ // Apply flag values with precedence over env vars
+ c.Host = *host
+ c.Port = *port
+ c.LogLevel = strings.ToLower(*logLevel)
+ c.EnableSSE = *enableSSE
+ c.ReadTimeout = *readTimeout
+ c.WriteTimeout = *writeTimeout
+ c.SSEKeepAlive = *sseKeepAlive
+ c.CORSEnabled = *corsEnabled
+
+ return nil
+}
+
+// LoadConfig creates and loads configuration with proper precedence: flags > env > defaults
+func LoadConfig() (*Config, error) {
+ // Start with defaults
+ config := New()
+
+ // Load environment variables (override defaults)
+ if err := config.LoadFromEnv(); err != nil {
+ return nil, fmt.Errorf("failed to load environment variables: %w", err)
+ }
+
+ // Load command line flags (override env vars)
+ if err := config.LoadFromFlags(); err != nil {
+ return nil, fmt.Errorf("failed to load command line flags: %w", err)
+ }
+
+ // Validate the final configuration
+ if err := config.Validate(); err != nil {
+ return nil, fmt.Errorf("configuration validation failed: %w", err)
+ }
+
+ return config, nil
+}
+
+// LogSafeConfig logs the configuration without sensitive information
+func (c *Config) LogSafeConfig(logger *slog.Logger) {
+ logger.Info("Server configuration loaded",
+ "host", c.Host,
+ "port", c.Port,
+ "log_level", c.LogLevel,
+ "enable_sse", c.EnableSSE,
+ "read_timeout", c.ReadTimeout,
+ "write_timeout", c.WriteTimeout,
+ "sse_keepalive", c.SSEKeepAlive,
+ "cors_enabled", c.CORSEnabled,
+ "streaming_chunk_delay", c.StreamingChunkDelay,
+ )
+}
diff --git a/sdks/community/go/example/server/internal/mcp/adapter.go b/sdks/community/go/example/server/internal/mcp/adapter.go
new file mode 100644
index 000000000..20da42f44
--- /dev/null
+++ b/sdks/community/go/example/server/internal/mcp/adapter.go
@@ -0,0 +1,52 @@
+package mcp
+
+import (
+ "fmt"
+ "time"
+
+ mcpadapter "github.com/i2y/langchaingo-mcp-adapter"
+ "github.com/mark3labs/mcp-go/client"
+ "github.com/mark3labs/mcp-go/client/transport"
+ langchaingoTools "github.com/tmc/langchaingo/tools"
+)
+
+type Adapter struct {
+ adapter *mcpadapter.MCPAdapter
+ mcpClient *client.Client
+}
+
+func NewAdapter(endpoint string) (*Adapter, error) {
+ httpTransport, err := getTransport(endpoint)
+ if err != nil {
+ return nil, err
+ }
+ mcpClient := client.NewClient(httpTransport)
+
+ adapter, err := mcpadapter.New(mcpClient, mcpadapter.WithToolTimeout(30*time.Second))
+ if err != nil {
+ return nil, fmt.Errorf("new mcp adapter: %w", err)
+ }
+ return &Adapter{
+ adapter: adapter,
+ mcpClient: mcpClient,
+ }, nil
+}
+
+func (a *Adapter) Close() error {
+ return a.mcpClient.Close()
+}
+
+func (a *Adapter) Tools() ([]langchaingoTools.Tool, error) {
+ return a.adapter.Tools()
+}
+
+func getTransport(endpoint string) (transport.Interface, error) {
+ httpTransport, err := transport.NewStreamableHTTP(
+ endpoint, // Replace with your MCP server URL
+ // You can add HTTP-specific options here like headers, OAuth, etc.
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create transport: %w", err)
+ }
+ return httpTransport, nil
+}
diff --git a/sdks/community/go/example/server/internal/mcp/server.go b/sdks/community/go/example/server/internal/mcp/server.go
new file mode 100644
index 000000000..613051449
--- /dev/null
+++ b/sdks/community/go/example/server/internal/mcp/server.go
@@ -0,0 +1,109 @@
+package mcp
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+)
+
+const DefaultPort = 3217
+
+type Server struct {
+ server *server.StreamableHTTPServer
+ port int
+}
+
+func NewServer(port int) (*Server, error) {
+ // Create a new MCP server
+ s := server.NewMCPServer(
+ "Demo 🚀",
+ "1.0.0",
+ server.WithToolCapabilities(false),
+ )
+
+ // Add tool
+ tool := mcp.NewTool("provide_language_options",
+ mcp.WithDescription("Provide a list of programming languages to choose from"),
+ mcp.WithString("option1",
+ mcp.Required(),
+ mcp.Description("Name of the first programming language option"),
+ ),
+ mcp.WithString("option2",
+ mcp.Required(),
+ mcp.Description("Name of the second programming language option"),
+ ),
+ mcp.WithString("option3",
+ mcp.Required(),
+ mcp.Description("Name of the third programming language option"),
+ ),
+ mcp.WithString("option4",
+ mcp.Required(),
+ mcp.Description("Name of the fourth programming language option"),
+ ),
+ )
+
+ // Add tool handler
+ s.AddTool(tool, languageChoiceHandler)
+
+ streamableServer := server.NewStreamableHTTPServer(s)
+
+ return &Server{
+ server: streamableServer,
+ port: port,
+ }, nil
+}
+
+func (s *Server) Start() error {
+ portString := fmt.Sprintf(":%d", s.port)
+ return s.server.Start(portString)
+}
+
+func (s *Server) Shutdown(ctx context.Context) error {
+ return s.server.Shutdown(ctx)
+}
+
+type LanguageOptions struct {
+ Option1 string
+ Option2 string
+ Option3 string
+ Option4 string
+}
+
+func languageChoiceHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ optionOne, err := request.RequireString("option1")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ optionTwo, err := request.RequireString("option2")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ optionThree, err := request.RequireString("option3")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ optionFour, err := request.RequireString("option4")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ // Marshal the struct to a JSON byte slice
+ jsonData, err := json.Marshal(LanguageOptions{
+ Option1: optionOne,
+ Option2: optionTwo,
+ Option3: optionThree,
+ Option4: optionFour,
+ })
+ if err != nil {
+ fmt.Println("Error marshaling JSON:", err)
+ return nil, err
+ }
+
+ return mcp.NewToolResultText(string(jsonData)), nil
+}
diff --git a/sdks/community/go/example/server/internal/routes/routes.go b/sdks/community/go/example/server/internal/routes/routes.go
new file mode 100644
index 000000000..19557a376
--- /dev/null
+++ b/sdks/community/go/example/server/internal/routes/routes.go
@@ -0,0 +1,131 @@
+package routes
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "log/slog"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/server/internal/agentic"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/example/server/internal/config"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/sse"
+ "github.com/gofiber/fiber/v3"
+)
+
+// AgenticInput represents the input structure for the tool-based generative UI endpoint
+type AgenticInput struct {
+ ThreadID string `json:"thread_id"`
+ RunID string `json:"run_id"`
+ State interface{} `json:"state"`
+ Messages []map[string]interface{} `json:"messages"`
+ Tools []interface{} `json:"tools"`
+ Context []interface{} `json:"context"`
+ ForwardedProps interface{} `json:"forwarded_props"`
+}
+
+// AgenticHandler creates a Fiber handler for the tool-based generative UI route
+func AgenticHandler(cfg *config.Config) fiber.Handler {
+ logger := slog.Default()
+ sseWriter := sse.NewSSEWriter().WithLogger(logger)
+
+ return func(c fiber.Ctx) error {
+ // Extract request metadata
+ requestID := c.Locals("requestid")
+ if requestID == nil {
+ requestID = "unknown"
+ }
+
+ logCtx := []any{
+ "request_id", requestID,
+ "route", c.Route().Path,
+ "method", c.Method(),
+ }
+
+ // Parse request body first before setting headers
+ var input AgenticInput
+ if err := c.Bind().JSON(&input); err != nil {
+ logger.Error("Failed to parse request body", append(logCtx, "error", err)...)
+ return c.Status(400).JSON(fiber.Map{
+ "error": "Invalid request body",
+ })
+ }
+
+ // Set SSE headers after validation
+ c.Set("Content-Type", "text/event-stream")
+ c.Set("Cache-Control", "no-cache")
+ c.Set("Connection", "keep-alive")
+ c.Set("Access-Control-Allow-Origin", "*")
+ c.Set("Access-Control-Allow-Headers", "Cache-Control")
+
+ logger.Info("Tool-based generative UI SSE connection established", logCtx...)
+
+ // Get request context for cancellation
+ ctx := c.RequestCtx()
+
+ // Start streaming
+ return c.SendStreamWriter(func(w *bufio.Writer) {
+ if err := streamAgenticEvents(ctx, w, sseWriter, &input, cfg, logger, logCtx); err != nil {
+ logger.Error("Error streaming tool-based generative UI events", append(logCtx, "error", err)...)
+ }
+ })
+ }
+}
+
+// streamAgenticEvents implements the tool-based generative UI event sequence
+func streamAgenticEvents(reqCtx context.Context, w *bufio.Writer, sseWriter *sse.SSEWriter, input *AgenticInput, _ *config.Config, logger *slog.Logger, logCtx []any) error {
+ // Use IDs from input or generate new ones if not provided
+ threadID := input.ThreadID
+ if threadID == "" {
+ threadID = events.GenerateThreadID()
+ }
+ runID := input.RunID
+ if runID == "" {
+ runID = events.GenerateRunID()
+ }
+
+ // Create a wrapped context for our operations
+ ctx := context.Background()
+
+ // Send RUN_STARTED event
+ runStarted := events.NewRunStartedEvent(threadID, runID)
+ if err := sseWriter.WriteEvent(ctx, w, runStarted); err != nil {
+ return fmt.Errorf("failed to write RUN_STARTED event: %w", err)
+ }
+
+ // Check for cancellation
+ if err := reqCtx.Err(); err != nil {
+ logger.Debug("Client disconnected during RUN_STARTED", append(logCtx, "reason", "context_canceled")...)
+ return nil
+ }
+
+ // Grab last message from input, will be a user message
+ var lastMessage map[string]interface{}
+ if len(input.Messages) > 0 {
+ lastMessage = input.Messages[len(input.Messages)-1]
+ }
+ // grab "content" field if it exists
+ content, ok := lastMessage["content"].(string)
+ if !ok {
+ return fmt.Errorf("last message does not have content")
+ }
+
+ err := agentic.ProcessInput(ctx, w, sseWriter, content)
+ if err != nil {
+ return fmt.Errorf("failed to process input: %w", err)
+
+ }
+
+ // Check for cancellation before final event
+ if err = reqCtx.Err(); err != nil {
+ return fmt.Errorf("client disconnected before RUN_FINISHED: %w", err)
+ }
+
+ // Send RUN_FINISHED event
+ runFinished := events.NewRunFinishedEvent(threadID, runID)
+ if err := sseWriter.WriteEvent(ctx, w, runFinished); err != nil {
+ return fmt.Errorf("failed to write RUN_FINISHED event: %w", err)
+ }
+
+ return nil
+}
diff --git a/sdks/community/go/go.mod b/sdks/community/go/go.mod
new file mode 100644
index 000000000..28435ee52
--- /dev/null
+++ b/sdks/community/go/go.mod
@@ -0,0 +1,16 @@
+module github.com/ag-ui-protocol/ag-ui/sdks/community/go
+
+go 1.24.4
+
+require (
+ github.com/google/uuid v1.6.0
+ github.com/sirupsen/logrus v1.9.3
+ github.com/stretchr/testify v1.7.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
+)
diff --git a/sdks/community/go/go.sum b/sdks/community/go/go.sum
new file mode 100644
index 000000000..2d2ff430e
--- /dev/null
+++ b/sdks/community/go/go.sum
@@ -0,0 +1,18 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/sdks/community/go/pkg/client/sse/client.go b/sdks/community/go/pkg/client/sse/client.go
new file mode 100644
index 000000000..f15d0f7d1
--- /dev/null
+++ b/sdks/community/go/pkg/client/sse/client.go
@@ -0,0 +1,274 @@
+package sse
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/sirupsen/logrus"
+)
+
+type Config struct {
+ Endpoint string
+ APIKey string
+ AuthHeader string
+ AuthScheme string
+ ConnectTimeout time.Duration
+ ReadTimeout time.Duration
+ BufferSize int
+ Logger *logrus.Logger
+}
+
+type Client struct {
+ config Config
+ httpClient *http.Client
+ logger *logrus.Logger
+}
+
+type Frame struct {
+ Data []byte
+ Timestamp time.Time
+}
+
+type StreamOptions struct {
+ Context context.Context
+ Payload interface{}
+ Headers map[string]string
+}
+
+func NewClient(config Config) *Client {
+ if config.Logger == nil {
+ config.Logger = logrus.New()
+ }
+
+ if config.ConnectTimeout == 0 {
+ config.ConnectTimeout = 30 * time.Second
+ }
+
+ if config.ReadTimeout == 0 {
+ config.ReadTimeout = 5 * time.Minute
+ }
+
+ if config.BufferSize == 0 {
+ config.BufferSize = 100
+ }
+
+ transport := &http.Transport{
+ DisableCompression: true,
+ ExpectContinueTimeout: 0,
+ ResponseHeaderTimeout: config.ConnectTimeout,
+ DisableKeepAlives: false,
+ MaxIdleConns: 1,
+ MaxIdleConnsPerHost: 1,
+ IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ }
+
+ httpClient := &http.Client{
+ Transport: transport,
+ Timeout: 0,
+ }
+
+ return &Client{
+ config: config,
+ httpClient: httpClient,
+ logger: config.Logger,
+ }
+}
+
+// Stream creates a basic SSE stream without reconnection
+func (c *Client) Stream(opts StreamOptions) (<-chan Frame, <-chan error, error) {
+ return c.stream(opts)
+}
+
+// stream is the internal implementation of basic streaming
+func (c *Client) stream(opts StreamOptions) (<-chan Frame, <-chan error, error) {
+ if opts.Context == nil {
+ opts.Context = context.Background()
+ }
+
+ payloadBytes, err := json.Marshal(opts.Payload)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal payload: %w", err)
+ }
+
+ req, err := http.NewRequestWithContext(
+ opts.Context,
+ http.MethodPost,
+ c.config.Endpoint,
+ bytes.NewReader(payloadBytes),
+ )
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Accept", "text/event-stream")
+ req.Header.Set("Cache-Control", "no-cache")
+ req.Header.Set("Connection", "keep-alive")
+
+ if c.config.APIKey != "" {
+ authHeader := c.config.AuthHeader
+ if authHeader == "" {
+ authHeader = "Authorization"
+ }
+
+ // Build the header value based on header type
+ if authHeader == "Authorization" {
+ // Use scheme (Bearer by default) for Authorization header
+ scheme := "Bearer"
+ if c.config.AuthScheme != "" {
+ scheme = c.config.AuthScheme
+ }
+ req.Header.Set(authHeader, scheme+" "+c.config.APIKey)
+ } else {
+ // For custom headers like X-API-Key, use the key directly
+ req.Header.Set(authHeader, c.config.APIKey)
+ }
+ }
+
+ for key, value := range opts.Headers {
+ req.Header.Set(key, value)
+ }
+
+ if c.logger != nil {
+ c.logger.WithFields(logrus.Fields{
+ "endpoint": c.config.Endpoint,
+ "method": req.Method,
+ "headers": req.Header,
+ }).Debug("Initiating SSE connection")
+ }
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to execute request: %w", err)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ _ = resp.Body.Close()
+ return nil, nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
+ }
+
+ contentType := resp.Header.Get("Content-Type")
+ if !strings.HasPrefix(contentType, "text/event-stream") {
+ _ = resp.Body.Close()
+ return nil, nil, fmt.Errorf("unexpected content-type: %s", contentType)
+ }
+
+ if c.logger != nil {
+ c.logger.WithFields(logrus.Fields{
+ "status": resp.StatusCode,
+ "content_type": contentType,
+ }).Info("SSE connection established")
+ }
+
+ frames := make(chan Frame, c.config.BufferSize)
+ errors := make(chan error, 1)
+
+ go c.readStream(opts.Context, resp, frames, errors)
+
+ return frames, errors, nil
+}
+
+func (c *Client) readStream(ctx context.Context, resp *http.Response, frames chan<- Frame, errors chan<- error) {
+ defer func() {
+ _ = resp.Body.Close()
+ close(frames)
+ close(errors)
+ if c.logger != nil {
+ c.logger.Info("SSE connection closed")
+ }
+ }()
+
+ reader := bufio.NewReader(resp.Body)
+ var buffer bytes.Buffer
+ var frameCount int64
+ var byteCount int64
+ startTime := time.Now()
+
+ for {
+ select {
+ case <-ctx.Done():
+ if c.logger != nil {
+ c.logger.WithField("reason", "context cancelled").Debug("Stopping SSE stream")
+ }
+ return
+ default:
+ }
+
+ if c.config.ReadTimeout > 0 {
+ deadline := time.Now().Add(c.config.ReadTimeout)
+ if tc, ok := resp.Body.(interface{ SetReadDeadline(time.Time) error }); ok {
+ _ = tc.SetReadDeadline(deadline)
+ }
+ }
+
+ line, err := reader.ReadBytes('\n')
+ if err != nil {
+ if err == io.EOF {
+ if c.logger != nil {
+ c.logger.WithFields(logrus.Fields{
+ "frames": frameCount,
+ "bytes": byteCount,
+ "duration": time.Since(startTime),
+ }).Info("SSE stream ended (EOF)")
+ }
+ return
+ }
+ select {
+ case errors <- fmt.Errorf("read error: %w", err):
+ case <-ctx.Done():
+ }
+ return
+ }
+
+ byteCount += int64(len(line))
+ line = bytes.TrimSuffix(line, []byte("\n"))
+ line = bytes.TrimSuffix(line, []byte("\r"))
+
+ if len(line) == 0 {
+ if buffer.Len() > 0 {
+ frame := Frame{
+ Data: make([]byte, buffer.Len()),
+ Timestamp: time.Now(),
+ }
+ copy(frame.Data, buffer.Bytes())
+ buffer.Reset()
+
+ select {
+ case frames <- frame:
+ frameCount++
+ if frameCount%100 == 0 && c.logger != nil {
+ c.logger.WithFields(logrus.Fields{
+ "frames": frameCount,
+ "bytes": byteCount,
+ }).Debug("SSE stream progress")
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+ continue
+ }
+
+ if bytes.HasPrefix(line, []byte("data: ")) {
+ data := bytes.TrimPrefix(line, []byte("data: "))
+ if buffer.Len() > 0 {
+ buffer.WriteByte('\n')
+ }
+ buffer.Write(data)
+ }
+ }
+}
+
+func (c *Client) Close() error {
+ c.httpClient.CloseIdleConnections()
+ return nil
+}
diff --git a/sdks/community/go/pkg/client/sse/client_stream_test.go b/sdks/community/go/pkg/client/sse/client_stream_test.go
new file mode 100644
index 000000000..5ddcb5660
--- /dev/null
+++ b/sdks/community/go/pkg/client/sse/client_stream_test.go
@@ -0,0 +1,738 @@
+package sse
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestStream(t *testing.T) {
+ t.Run("successful stream", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
+ assert.Equal(t, "text/event-stream", r.Header.Get("Accept"))
+
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+
+ flusher, ok := w.(http.Flusher)
+ require.True(t, ok)
+
+ fmt.Fprintf(w, "data: first message\n\n")
+ flusher.Flush()
+
+ fmt.Fprintf(w, "data: second message\n\n")
+ flusher.Flush()
+
+ fmt.Fprintf(w, "data: {\"type\":\"json\",\"value\":123}\n\n")
+ flusher.Flush()
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ BufferSize: 10,
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+
+ frames, errors, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: map[string]string{"test": "data"},
+ })
+ require.NoError(t, err)
+
+ var received []string
+ done := make(chan bool)
+
+ go func() {
+ for {
+ select {
+ case frame, ok := <-frames:
+ if !ok {
+ done <- true
+ return
+ }
+ received = append(received, string(frame.Data))
+ case err := <-errors:
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ case <-time.After(1 * time.Second):
+ done <- true
+ return
+ }
+ }
+ }()
+
+ <-done
+ assert.Len(t, received, 3)
+ assert.Contains(t, received, "first message")
+ assert.Contains(t, received, "second message")
+ assert.Contains(t, received, `{"type":"json","value":123}`)
+ })
+
+ t.Run("multiline data handling", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+
+ flusher, ok := w.(http.Flusher)
+ require.True(t, ok)
+
+ // Send multiline data
+ fmt.Fprintf(w, "data: line1\ndata: line2\ndata: line3\n\n")
+ flusher.Flush()
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ frames, _, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: struct{}{},
+ })
+ require.NoError(t, err)
+
+ select {
+ case frame := <-frames:
+ assert.Equal(t, "line1\nline2\nline3", string(frame.Data))
+ case <-time.After(1 * time.Second):
+ t.Fatal("timeout waiting for frame")
+ }
+ })
+
+ t.Run("authentication headers", func(t *testing.T) {
+ tests := []struct {
+ name string
+ config Config
+ expectedHeader string
+ expectedValue string
+ }{
+ {
+ name: "default bearer auth",
+ config: Config{
+ APIKey: "test-key",
+ },
+ expectedHeader: "Authorization",
+ expectedValue: "Bearer test-key",
+ },
+ {
+ name: "custom auth scheme",
+ config: Config{
+ APIKey: "test-key",
+ AuthScheme: "Token",
+ },
+ expectedHeader: "Authorization",
+ expectedValue: "Token test-key",
+ },
+ {
+ name: "custom header",
+ config: Config{
+ APIKey: "test-key",
+ AuthHeader: "X-API-Key",
+ },
+ expectedHeader: "X-API-Key",
+ expectedValue: "test-key",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, tt.expectedValue, r.Header.Get(tt.expectedHeader))
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+
+ tt.config.Endpoint = server.URL
+ client := NewClient(tt.config)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
+ defer cancel()
+
+ _, _, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: struct{}{},
+ })
+ require.NoError(t, err)
+ })
+ }
+ })
+
+ t.Run("custom headers", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "custom-value", r.Header.Get("X-Custom-Header"))
+ assert.Equal(t, "another-value", r.Header.Get("X-Another-Header"))
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
+ defer cancel()
+
+ _, _, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: struct{}{},
+ Headers: map[string]string{
+ "X-Custom-Header": "custom-value",
+ "X-Another-Header": "another-value",
+ },
+ })
+ require.NoError(t, err)
+ })
+
+ t.Run("error responses", func(t *testing.T) {
+ tests := []struct {
+ name string
+ statusCode int
+ contentType string
+ responseBody string
+ expectedErr string
+ }{
+ {
+ name: "404 not found",
+ statusCode: http.StatusNotFound,
+ contentType: "text/plain",
+ responseBody: "Not Found",
+ expectedErr: "unexpected status code 404",
+ },
+ {
+ name: "500 internal server error",
+ statusCode: http.StatusInternalServerError,
+ contentType: "application/json",
+ responseBody: `{"error":"internal server error"}`,
+ expectedErr: "unexpected status code 500",
+ },
+ {
+ name: "wrong content type",
+ statusCode: http.StatusOK,
+ contentType: "application/json",
+ expectedErr: "unexpected content-type: application/json",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if tt.contentType != "" {
+ w.Header().Set("Content-Type", tt.contentType)
+ }
+ w.WriteHeader(tt.statusCode)
+ if tt.responseBody != "" {
+ w.Write([]byte(tt.responseBody))
+ }
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ })
+
+ _, _, err := client.Stream(StreamOptions{
+ Payload: struct{}{},
+ })
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.expectedErr)
+ })
+ }
+ })
+
+ t.Run("context cancellation", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+
+ flusher, ok := w.(http.Flusher)
+ require.True(t, ok)
+
+ // Send data slowly
+ for i := 0; i < 100; i++ {
+ fmt.Fprintf(w, "data: message %d\n\n", i)
+ flusher.Flush()
+ time.Sleep(100 * time.Millisecond)
+ }
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
+ defer cancel()
+
+ frames, errors, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: struct{}{},
+ })
+ require.NoError(t, err)
+
+ messageCount := 0
+ for {
+ select {
+ case _, ok := <-frames:
+ if !ok {
+ // Channel closed due to context cancellation
+ assert.Greater(t, messageCount, 0)
+ assert.Less(t, messageCount, 10) // Should not receive all 100 messages
+ return
+ }
+ messageCount++
+ case <-errors:
+ // Might receive an error due to context cancellation
+ case <-time.After(1 * time.Second):
+ t.Fatal("test took too long")
+ }
+ }
+ })
+
+ t.Run("invalid payload marshaling", func(t *testing.T) {
+ client := NewClient(Config{
+ Endpoint: "http://localhost",
+ })
+
+ // Create an unmarshalable payload
+ invalidPayload := make(chan int)
+
+ _, _, err := client.Stream(StreamOptions{
+ Payload: invalidPayload,
+ })
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to marshal payload")
+ })
+
+ t.Run("invalid endpoint", func(t *testing.T) {
+ client := NewClient(Config{
+ Endpoint: "http://[::1]:namedport", // Invalid URL
+ })
+
+ _, _, err := client.Stream(StreamOptions{
+ Payload: struct{}{},
+ })
+ require.Error(t, err)
+ })
+
+ t.Run("concurrent reads", func(t *testing.T) {
+ messageCount := 50
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+
+ flusher, ok := w.(http.Flusher)
+ require.True(t, ok)
+
+ for i := 0; i < messageCount; i++ {
+ fmt.Fprintf(w, "data: message-%d\n\n", i)
+ flusher.Flush()
+ }
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ BufferSize: 100,
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+
+ frames, _, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: struct{}{},
+ })
+ require.NoError(t, err)
+
+ var wg sync.WaitGroup
+ received := make(map[string]bool)
+ mu := sync.Mutex{}
+
+ // Start multiple goroutines to read frames
+ for i := 0; i < 5; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for frame := range frames {
+ mu.Lock()
+ received[string(frame.Data)] = true
+ mu.Unlock()
+ }
+ }()
+ }
+
+ wg.Wait()
+ assert.Len(t, received, messageCount)
+ })
+
+ t.Run("read timeout handling", func(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+
+ flusher, ok := w.(http.Flusher)
+ require.True(t, ok)
+
+ // Send one message then hang
+ fmt.Fprintf(w, "data: initial\n\n")
+ flusher.Flush()
+
+ // Simulate a hung connection
+ time.Sleep(5 * time.Second)
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ ReadTimeout: 500 * time.Millisecond,
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+
+ frames, errors, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: struct{}{},
+ })
+ require.NoError(t, err)
+
+ // Should receive initial message
+ select {
+ case frame := <-frames:
+ assert.Equal(t, "initial", string(frame.Data))
+ case <-time.After(1 * time.Second):
+ t.Fatal("timeout waiting for initial frame")
+ }
+
+ // Should eventually get an error or channel closure due to read timeout
+ select {
+ case <-frames:
+ // Channel closed
+ case err := <-errors:
+ assert.NotNil(t, err)
+ case <-time.After(2 * time.Second):
+ t.Fatal("timeout waiting for error or closure")
+ }
+ })
+
+ t.Run("logger output", func(t *testing.T) {
+ var logBuffer bytes.Buffer
+ logger := logrus.New()
+ logger.SetOutput(&logBuffer)
+ logger.SetLevel(logrus.DebugLevel)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+
+ flusher, ok := w.(http.Flusher)
+ require.True(t, ok)
+
+ for i := 0; i < 150; i++ {
+ fmt.Fprintf(w, "data: msg%d\n\n", i)
+ flusher.Flush()
+ }
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ Logger: logger,
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ frames, _, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: struct{}{},
+ })
+ require.NoError(t, err)
+
+ // Consume all frames
+ go func() {
+ for range frames {
+ }
+ }()
+
+ time.Sleep(500 * time.Millisecond)
+ cancel()
+ time.Sleep(100 * time.Millisecond)
+
+ logs := logBuffer.String()
+ assert.Contains(t, logs, "Initiating SSE connection")
+ assert.Contains(t, logs, "SSE connection established")
+ assert.Contains(t, logs, "SSE stream progress") // Should log progress every 100 frames
+ })
+}
+
+func TestReadStream(t *testing.T) {
+ t.Run("EOF handling", func(t *testing.T) {
+ pr, pw := io.Pipe()
+ resp := &http.Response{
+ Body: pr,
+ }
+
+ client := NewClient(Config{})
+ frames := make(chan Frame, 10)
+ errors := make(chan error, 1)
+
+ go client.readStream(context.Background(), resp, frames, errors)
+
+ // Write some data then close
+ go func() {
+ pw.Write([]byte("data: test\n\n"))
+ time.Sleep(100 * time.Millisecond)
+ pw.Close()
+ }()
+
+ // Should receive one frame
+ select {
+ case frame := <-frames:
+ assert.Equal(t, "test", string(frame.Data))
+ case <-time.After(1 * time.Second):
+ t.Fatal("timeout waiting for frame")
+ }
+
+ // Channels should be closed after EOF
+ select {
+ case _, ok := <-frames:
+ assert.False(t, ok, "frames channel should be closed")
+ case <-time.After(1 * time.Second):
+ t.Fatal("frames channel not closed")
+ }
+ })
+
+ t.Run("carriage return handling", func(t *testing.T) {
+ pr, pw := io.Pipe()
+ resp := &http.Response{
+ Body: pr,
+ }
+
+ client := NewClient(Config{})
+ frames := make(chan Frame, 10)
+ errors := make(chan error, 1)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
+ defer cancel()
+
+ go client.readStream(ctx, resp, frames, errors)
+
+ // Write data with carriage returns
+ go func() {
+ pw.Write([]byte("data: test\r\n\r\n"))
+ time.Sleep(100 * time.Millisecond)
+ pw.Close()
+ }()
+
+ select {
+ case frame := <-frames:
+ assert.Equal(t, "test", string(frame.Data))
+ case <-time.After(1 * time.Second):
+ t.Fatal("timeout waiting for frame")
+ }
+ })
+
+ t.Run("empty lines between data", func(t *testing.T) {
+ pr, pw := io.Pipe()
+ resp := &http.Response{
+ Body: pr,
+ }
+
+ client := NewClient(Config{})
+ frames := make(chan Frame, 10)
+ errors := make(chan error, 1)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
+ defer cancel()
+
+ go client.readStream(ctx, resp, frames, errors)
+
+ go func() {
+ // Multiple empty lines should be ignored
+ pw.Write([]byte("\n\n\ndata: test\n\n\n\n"))
+ time.Sleep(100 * time.Millisecond)
+ pw.Close()
+ }()
+
+ select {
+ case frame := <-frames:
+ assert.Equal(t, "test", string(frame.Data))
+ case <-time.After(1 * time.Second):
+ t.Fatal("timeout waiting for frame")
+ }
+ })
+
+ t.Run("non-data lines ignored", func(t *testing.T) {
+ pr, pw := io.Pipe()
+ resp := &http.Response{
+ Body: pr,
+ }
+
+ client := NewClient(Config{})
+ frames := make(chan Frame, 10)
+ errors := make(chan error, 1)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
+ defer cancel()
+
+ go client.readStream(ctx, resp, frames, errors)
+
+ go func() {
+ // Lines without "data: " prefix should be ignored
+ pw.Write([]byte("event: custom\nid: 123\nretry: 1000\ndata: actual-data\n\n"))
+ time.Sleep(100 * time.Millisecond)
+ pw.Close()
+ }()
+
+ select {
+ case frame := <-frames:
+ assert.Equal(t, "actual-data", string(frame.Data))
+ case <-time.After(1 * time.Second):
+ t.Fatal("timeout waiting for frame")
+ }
+ })
+}
+
+// Mock reader that returns an error after some data
+type errorReader struct {
+ data []byte
+ err error
+ pos int
+}
+
+func (r *errorReader) Read(p []byte) (int, error) {
+ if r.pos >= len(r.data) {
+ return 0, r.err
+ }
+ n := copy(p, r.data[r.pos:])
+ r.pos += n
+ if r.pos >= len(r.data) && r.err != nil {
+ return n, r.err
+ }
+ return n, nil
+}
+
+func TestReadStreamWithErrors(t *testing.T) {
+ t.Run("read error handling", func(t *testing.T) {
+ reader := &errorReader{
+ data: []byte("data: partial\n"),
+ err: fmt.Errorf("network error"),
+ }
+
+ resp := &http.Response{
+ Body: io.NopCloser(reader),
+ }
+
+ client := NewClient(Config{})
+ frames := make(chan Frame, 10)
+ errors := make(chan error, 1)
+
+ go client.readStream(context.Background(), resp, frames, errors)
+
+ select {
+ case err := <-errors:
+ assert.Contains(t, err.Error(), "read error")
+ assert.Contains(t, err.Error(), "network error")
+ case <-time.After(1 * time.Second):
+ t.Fatal("timeout waiting for error")
+ }
+ })
+}
+
+// Benchmark tests
+func BenchmarkStream(b *testing.B) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.WriteHeader(http.StatusOK)
+
+ flusher, ok := w.(http.Flusher)
+ if !ok {
+ return
+ }
+
+ for i := 0; i < 1000; i++ {
+ fmt.Fprintf(w, "data: message %d with some payload data\n\n", i)
+ flusher.Flush()
+ }
+ }))
+ defer server.Close()
+
+ client := NewClient(Config{
+ Endpoint: server.URL,
+ BufferSize: 100,
+ })
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+
+ frames, _, err := client.Stream(StreamOptions{
+ Context: ctx,
+ Payload: struct{}{},
+ })
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ count := 0
+ for range frames {
+ count++
+ if count >= 1000 {
+ cancel()
+ break
+ }
+ }
+ }
+}
+
+func BenchmarkReadStream(b *testing.B) {
+ data := bytes.Repeat([]byte("data: benchmark message with some test data\n\n"), 1000)
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ frames := make(chan Frame, 100)
+ errors := make(chan error, 1)
+
+ resp := &http.Response{
+ Body: io.NopCloser(bytes.NewReader(data)),
+ }
+
+ client := NewClient(Config{})
+
+ ctx, cancel := context.WithCancel(context.Background())
+ go client.readStream(ctx, resp, frames, errors)
+
+ count := 0
+ for range frames {
+ count++
+ if count >= 1000 {
+ cancel()
+ break
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sdks/community/go/pkg/client/sse/client_test.go b/sdks/community/go/pkg/client/sse/client_test.go
new file mode 100644
index 000000000..888ced0ef
--- /dev/null
+++ b/sdks/community/go/pkg/client/sse/client_test.go
@@ -0,0 +1,368 @@
+package sse
+
+import (
+ "testing"
+ "time"
+)
+
+func TestNewClient(t *testing.T) {
+ tests := []struct {
+ name string
+ config Config
+ }{
+ {
+ name: "default config",
+ config: Config{
+ Endpoint: "http://localhost:8080/sse",
+ },
+ },
+ {
+ name: "custom timeouts",
+ config: Config{
+ Endpoint: "http://localhost:8080/sse",
+ ConnectTimeout: 10 * time.Second,
+ ReadTimeout: 1 * time.Minute,
+ BufferSize: 50,
+ },
+ },
+ {
+ name: "with API key",
+ config: Config{
+ Endpoint: "http://localhost:8080/sse",
+ APIKey: "test-api-key",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ client := NewClient(tt.config)
+ if client == nil {
+ t.Fatal("expected non-nil client")
+ }
+ if client.httpClient == nil {
+ t.Fatal("expected non-nil http client")
+ }
+ if client.logger == nil {
+ t.Fatal("expected non-nil logger")
+ }
+ })
+ }
+}
+
+// TODO: re-enable this test once RunAgentInput exists
+//func TestClientStream(t *testing.T) {
+// tests := []struct {
+// name string
+// serverFunc func(w http.ResponseWriter, r *http.Request)
+// config Config
+// payload interface{}
+// wantFrames int
+// wantErr bool
+// checkFrames func(t *testing.T, frames []Frame)
+// }{
+// {
+// name: "successful stream",
+// serverFunc: func(w http.ResponseWriter, r *http.Request) {
+// w.Header().Set("Content-Type", "text/event-stream")
+// w.WriteHeader(http.StatusOK)
+//
+// flusher, ok := w.(http.Flusher)
+// if !ok {
+// t.Fatal("ResponseWriter does not support flushing")
+// }
+//
+// _, _ = fmt.Fprintf(w, "data: {\"event\":\"start\"}\n\n")
+// flusher.Flush()
+//
+// time.Sleep(10 * time.Millisecond)
+//
+// _, _ = fmt.Fprintf(w, "data: {\"event\":\"message\",\"content\":\"Hello\"}\n\n")
+// flusher.Flush()
+//
+// time.Sleep(10 * time.Millisecond)
+//
+// _, _ = fmt.Fprintf(w, "data: {\"event\":\"end\"}\n\n")
+// flusher.Flush()
+// },
+// config: Config{
+// BufferSize: 10,
+// },
+// payload: RunAgentInput{
+// SessionID: "test-session",
+// Messages: []Message{
+// {Role: "user", Content: "Hello"},
+// },
+// Stream: true,
+// },
+// wantFrames: 3,
+// checkFrames: func(t *testing.T, frames []Frame) {
+// if len(frames) != 3 {
+// t.Errorf("expected 3 frames, got %d", len(frames))
+// return
+// }
+//
+// var event1, event2, event3 map[string]interface{}
+// _ = json.Unmarshal(frames[0].Data, &event1)
+// _ = json.Unmarshal(frames[1].Data, &event2)
+// _ = json.Unmarshal(frames[2].Data, &event3)
+//
+// if event1["event"] != "start" {
+// t.Errorf("expected first event to be 'start', got %v", event1["event"])
+// }
+// if event2["event"] != "message" {
+// t.Errorf("expected second event to be 'message', got %v", event2["event"])
+// }
+// if event3["event"] != "end" {
+// t.Errorf("expected third event to be 'end', got %v", event3["event"])
+// }
+// },
+// },
+// {
+// name: "multi-line data",
+// serverFunc: func(w http.ResponseWriter, r *http.Request) {
+// w.Header().Set("Content-Type", "text/event-stream")
+// w.WriteHeader(http.StatusOK)
+//
+// flusher, ok := w.(http.Flusher)
+// if !ok {
+// t.Fatal("ResponseWriter does not support flushing")
+// }
+//
+// _, _ = fmt.Fprintf(w, "data: {\"event\":\"multi\",\n")
+// _, _ = fmt.Fprintf(w, "data: \"line1\":\"value1\",\n")
+// _, _ = fmt.Fprintf(w, "data: \"line2\":\"value2\"}\n\n")
+// flusher.Flush()
+// },
+// config: Config{
+// BufferSize: 10,
+// },
+// payload: RunAgentInput{Stream: true},
+// wantFrames: 1,
+// checkFrames: func(t *testing.T, frames []Frame) {
+// if len(frames) != 1 {
+// t.Errorf("expected 1 frame, got %d", len(frames))
+// return
+// }
+//
+// expected := `{"event":"multi",
+//"line1":"value1",
+//"line2":"value2"}`
+// if string(frames[0].Data) != expected {
+// t.Errorf("unexpected frame data:\ngot: %s\nwant: %s", frames[0].Data, expected)
+// }
+// },
+// },
+// {
+// name: "non-200 status",
+// serverFunc: func(w http.ResponseWriter, r *http.Request) {
+// w.WriteHeader(http.StatusInternalServerError)
+// _, _ = fmt.Fprintf(w, "Internal Server Error")
+// },
+// config: Config{},
+// payload: RunAgentInput{Stream: true},
+// wantErr: true,
+// },
+// {
+// name: "wrong content type",
+// serverFunc: func(w http.ResponseWriter, r *http.Request) {
+// w.Header().Set("Content-Type", "application/json")
+// w.WriteHeader(http.StatusOK)
+// _, _ = fmt.Fprintf(w, "{\"error\":\"wrong type\"}")
+// },
+// config: Config{},
+// payload: RunAgentInput{Stream: true},
+// wantErr: true,
+// },
+// {
+// name: "with auth header",
+// serverFunc: func(w http.ResponseWriter, r *http.Request) {
+// auth := r.Header.Get("Authorization")
+// if auth != "Bearer test-key-123" {
+// w.WriteHeader(http.StatusUnauthorized)
+// return
+// }
+//
+// w.Header().Set("Content-Type", "text/event-stream")
+// w.WriteHeader(http.StatusOK)
+//
+// flusher, ok := w.(http.Flusher)
+// if !ok {
+// t.Fatal("ResponseWriter does not support flushing")
+// }
+//
+// _, _ = fmt.Fprintf(w, "data: {\"authorized\":true}\n\n")
+// flusher.Flush()
+// },
+// config: Config{
+// APIKey: "test-key-123",
+// BufferSize: 10,
+// },
+// payload: RunAgentInput{Stream: true},
+// wantFrames: 1,
+// },
+// {
+// name: "with custom auth header",
+// serverFunc: func(w http.ResponseWriter, r *http.Request) {
+// auth := r.Header.Get("X-API-Key")
+// if auth != "custom-key-456" {
+// w.WriteHeader(http.StatusUnauthorized)
+// return
+// }
+//
+// w.Header().Set("Content-Type", "text/event-stream")
+// w.WriteHeader(http.StatusOK)
+//
+// flusher, ok := w.(http.Flusher)
+// if !ok {
+// t.Fatal("ResponseWriter does not support flushing")
+// }
+//
+// _, _ = fmt.Fprintf(w, "data: {\"authorized\":true}\n\n")
+// flusher.Flush()
+// },
+// config: Config{
+// APIKey: "custom-key-456",
+// AuthHeader: "X-API-Key",
+// BufferSize: 10,
+// },
+// payload: RunAgentInput{Stream: true},
+// wantFrames: 1,
+// },
+// }
+//
+// for _, tt := range tests {
+// t.Run(tt.name, func(t *testing.T) {
+// server := httptest.NewServer(http.HandlerFunc(tt.serverFunc))
+// defer server.Close()
+//
+// tt.config.Endpoint = server.URL + "/tool_based_generative_ui"
+// if tt.config.Logger == nil {
+// logger := logrus.New()
+// logger.SetLevel(logrus.DebugLevel)
+// tt.config.Logger = logger
+// }
+//
+// client := NewClient(tt.config)
+//
+// ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+// defer cancel()
+//
+// frames, errors, err := client.Stream(StreamOptions{
+// Context: ctx,
+// Payload: tt.payload,
+// })
+//
+// if tt.wantErr {
+// if err == nil {
+// t.Fatal("expected error, got nil")
+// }
+// return
+// }
+//
+// if err != nil {
+// t.Fatalf("unexpected error: %v", err)
+// }
+//
+// var collectedFrames []Frame
+// done := false
+//
+// for !done {
+// select {
+// case frame, ok := <-frames:
+// if !ok {
+// done = true
+// break
+// }
+// collectedFrames = append(collectedFrames, frame)
+// case err := <-errors:
+// if err != nil && !strings.Contains(err.Error(), "EOF") {
+// t.Fatalf("unexpected stream error: %v", err)
+// }
+// case <-ctx.Done():
+// done = true
+// }
+// }
+//
+// if tt.wantFrames > 0 && len(collectedFrames) != tt.wantFrames {
+// t.Errorf("expected %d frames, got %d", tt.wantFrames, len(collectedFrames))
+// }
+//
+// if tt.checkFrames != nil {
+// tt.checkFrames(t, collectedFrames)
+// }
+// })
+// }
+//}
+
+// TODO: re-enable this test once RunAgentInput exists
+//func TestClientContextCancellation(t *testing.T) {
+// slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+// w.Header().Set("Content-Type", "text/event-stream")
+// w.WriteHeader(http.StatusOK)
+//
+// flusher, ok := w.(http.Flusher)
+// if !ok {
+// t.Fatal("ResponseWriter does not support flushing")
+// }
+//
+// for i := 0; i < 100; i++ {
+// _, _ = fmt.Fprintf(w, "data: {\"count\":%d}\n\n", i)
+// flusher.Flush()
+// time.Sleep(100 * time.Millisecond)
+// }
+// }))
+// defer slowServer.Close()
+//
+// client := NewClient(Config{
+// Endpoint: slowServer.URL + "/sse",
+// BufferSize: 10,
+// Logger: logrus.New(),
+// })
+//
+// ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
+// defer cancel()
+//
+// frames, errors, err := client.Stream(StreamOptions{
+// Context: ctx,
+// Payload: RunAgentInput{Stream: true},
+// })
+//
+// if err != nil {
+// t.Fatalf("unexpected error: %v", err)
+// }
+//
+// frameCount := 0
+// for {
+// select {
+// case _, ok := <-frames:
+// if !ok {
+// goto done
+// }
+// frameCount++
+// case <-errors:
+// goto done
+// case <-ctx.Done():
+// goto done
+// }
+// }
+//
+//done:
+// if frameCount == 0 {
+// t.Error("expected at least one frame before cancellation")
+// }
+// if frameCount >= 10 {
+// t.Error("expected cancellation to stop stream early")
+// }
+//}
+
+func TestClientClose(t *testing.T) {
+ client := NewClient(Config{
+ Endpoint: "http://localhost:8080/sse",
+ })
+
+ err := client.Close()
+ if err != nil {
+ t.Errorf("unexpected error closing client: %v", err)
+ }
+}
diff --git a/sdks/community/go/pkg/core/events/additional_events_test.go b/sdks/community/go/pkg/core/events/additional_events_test.go
new file mode 100644
index 000000000..9c2f14765
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/additional_events_test.go
@@ -0,0 +1,577 @@
+package events
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestBaseEventMethods(t *testing.T) {
+ t.Run("ID_Method", func(t *testing.T) {
+ base := NewBaseEvent(EventTypeRunStarted)
+
+ // ID should return a generated ID for base events
+ id := base.ID()
+ assert.NotEmpty(t, id)
+ assert.Contains(t, id, "RUN_STARTED")
+ })
+
+ t.Run("GetBaseEvent_Method", func(t *testing.T) {
+ base := NewBaseEvent(EventTypeRunStarted)
+
+ // GetBaseEvent should return itself
+ result := base.GetBaseEvent()
+ assert.Equal(t, base, result)
+ })
+
+ t.Run("ThreadID_Method", func(t *testing.T) {
+ base := NewBaseEvent(EventTypeRunStarted)
+
+ // ThreadID should return empty string for base events
+ threadID := base.ThreadID()
+ assert.Equal(t, "", threadID)
+ })
+
+ t.Run("RunID_Method", func(t *testing.T) {
+ base := NewBaseEvent(EventTypeRunStarted)
+
+ // RunID should return empty string for base events
+ runID := base.RunID()
+ assert.Equal(t, "", runID)
+ })
+
+ t.Run("ToJSON_Method", func(t *testing.T) {
+ base := NewBaseEvent(EventTypeRunStarted)
+
+ jsonData, err := base.ToJSON()
+ require.NoError(t, err)
+ assert.NotNil(t, jsonData)
+
+ // Verify JSON structure
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+ assert.Equal(t, string(EventTypeRunStarted), decoded["type"])
+ assert.NotNil(t, decoded["timestamp"])
+ })
+}
+
+func TestTextMessageChunkEvent(t *testing.T) {
+ t.Run("NewTextMessageChunkEvent", func(t *testing.T) {
+ event := NewTextMessageChunkEvent()
+
+ assert.Equal(t, EventTypeTextMessageChunk, event.Type())
+ assert.Nil(t, event.MessageID)
+ assert.Nil(t, event.Role)
+ assert.Nil(t, event.Delta)
+ })
+
+ t.Run("WithChunkMessageID", func(t *testing.T) {
+ event := NewTextMessageChunkEvent().WithChunkMessageID("msg-123")
+
+ assert.NotNil(t, event.MessageID)
+ assert.Equal(t, "msg-123", *event.MessageID)
+ })
+
+ t.Run("WithChunkRole", func(t *testing.T) {
+ event := NewTextMessageChunkEvent().WithChunkRole("assistant")
+
+ assert.NotNil(t, event.Role)
+ assert.Equal(t, "assistant", *event.Role)
+ })
+
+ t.Run("WithChunkDelta", func(t *testing.T) {
+ event := NewTextMessageChunkEvent().WithChunkDelta("Hello")
+
+ assert.NotNil(t, event.Delta)
+ assert.Equal(t, "Hello", *event.Delta)
+ })
+
+ t.Run("Validate", func(t *testing.T) {
+ // Valid event with all fields
+ event := NewTextMessageChunkEvent().
+ WithChunkMessageID("msg-123").
+ WithChunkRole("user").
+ WithChunkDelta("Hello")
+ assert.NoError(t, event.Validate())
+
+ // Valid event with only delta
+ event = NewTextMessageChunkEvent().WithChunkDelta("Hello")
+ assert.NoError(t, event.Validate())
+
+ // Valid - messageID without delta is allowed for chunks
+ event = NewTextMessageChunkEvent().WithChunkMessageID("msg-123")
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("ToJSON", func(t *testing.T) {
+ event := NewTextMessageChunkEvent().
+ WithChunkMessageID("msg-123").
+ WithChunkRole("assistant").
+ WithChunkDelta("Hello world")
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeTextMessageChunk), decoded["type"])
+ assert.Equal(t, "msg-123", decoded["messageId"])
+ assert.Equal(t, "assistant", decoded["role"])
+ assert.Equal(t, "Hello world", decoded["delta"])
+ })
+}
+
+func TestToolCallChunkEvent(t *testing.T) {
+ t.Run("NewToolCallChunkEvent", func(t *testing.T) {
+ event := NewToolCallChunkEvent()
+
+ assert.Equal(t, EventTypeToolCallChunk, event.Type())
+ assert.Nil(t, event.ToolCallID)
+ assert.Nil(t, event.ToolCallName)
+ assert.Nil(t, event.Delta)
+ assert.Nil(t, event.ParentMessageID)
+ })
+
+ t.Run("WithToolCallChunkID", func(t *testing.T) {
+ event := NewToolCallChunkEvent().WithToolCallChunkID("tool-123")
+
+ assert.NotNil(t, event.ToolCallID)
+ assert.Equal(t, "tool-123", *event.ToolCallID)
+ })
+
+ t.Run("WithToolCallChunkName", func(t *testing.T) {
+ event := NewToolCallChunkEvent().WithToolCallChunkName("get_weather")
+
+ assert.NotNil(t, event.ToolCallName)
+ assert.Equal(t, "get_weather", *event.ToolCallName)
+ })
+
+ t.Run("WithToolCallChunkDelta", func(t *testing.T) {
+ event := NewToolCallChunkEvent().WithToolCallChunkDelta("{\"location\":")
+
+ assert.NotNil(t, event.Delta)
+ assert.Equal(t, "{\"location\":", *event.Delta)
+ })
+
+ t.Run("WithToolCallChunkParentMessageID", func(t *testing.T) {
+ event := NewToolCallChunkEvent().WithToolCallChunkParentMessageID("msg-456")
+
+ assert.NotNil(t, event.ParentMessageID)
+ assert.Equal(t, "msg-456", *event.ParentMessageID)
+ })
+
+ t.Run("Validate", func(t *testing.T) {
+ // Valid event with all fields
+ event := NewToolCallChunkEvent().
+ WithToolCallChunkID("tool-123").
+ WithToolCallChunkName("get_weather").
+ WithToolCallChunkDelta("{\"args\"}").
+ WithToolCallChunkParentMessageID("msg-456")
+ assert.NoError(t, event.Validate())
+
+ // Valid event with minimal fields
+ event = NewToolCallChunkEvent().WithToolCallChunkDelta("delta")
+ assert.NoError(t, event.Validate())
+
+ // Valid - ID without delta is allowed for chunks
+ event = NewToolCallChunkEvent().WithToolCallChunkID("tool-123")
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("ToJSON", func(t *testing.T) {
+ event := NewToolCallChunkEvent().
+ WithToolCallChunkID("tool-123").
+ WithToolCallChunkName("search").
+ WithToolCallChunkDelta("{\"query\":").
+ WithToolCallChunkParentMessageID("msg-789")
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeToolCallChunk), decoded["type"])
+ assert.Equal(t, "tool-123", decoded["toolCallId"])
+ assert.Equal(t, "search", decoded["toolCallName"])
+ assert.Equal(t, "{\"query\":", decoded["delta"])
+ assert.Equal(t, "msg-789", decoded["parentMessageId"])
+ })
+}
+
+func TestToolCallResultEvent(t *testing.T) {
+ t.Run("NewToolCallResultEvent", func(t *testing.T) {
+ event := NewToolCallResultEvent("msg-456", "tool-123", "Success")
+
+ assert.Equal(t, EventTypeToolCallResult, event.Type())
+ assert.Equal(t, "msg-456", event.MessageID)
+ assert.Equal(t, "tool-123", event.ToolCallID)
+ assert.Equal(t, "Success", event.Content)
+ assert.NotNil(t, event.Role)
+ assert.Equal(t, "tool", *event.Role)
+ })
+
+ t.Run("Validate", func(t *testing.T) {
+ // Valid event
+ event := NewToolCallResultEvent("msg-456", "tool-123", "Result")
+ assert.NoError(t, event.Validate())
+
+ // Invalid - empty message ID
+ event.MessageID = ""
+ assert.Error(t, event.Validate())
+
+ // Invalid - empty tool call ID
+ event = NewToolCallResultEvent("msg-456", "", "Result")
+ assert.Error(t, event.Validate())
+
+ // Invalid - empty content
+ event = NewToolCallResultEvent("msg-456", "tool-123", "")
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("ToJSON", func(t *testing.T) {
+ event := NewToolCallResultEvent("msg-456", "tool-123", "Weather: Sunny, 72°F")
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeToolCallResult), decoded["type"])
+ assert.Equal(t, "msg-456", decoded["messageId"])
+ assert.Equal(t, "tool-123", decoded["toolCallId"])
+ assert.Equal(t, "Weather: Sunny, 72°F", decoded["content"])
+ assert.Equal(t, "tool", decoded["role"])
+ })
+}
+
+func TestAutoIDGeneration(t *testing.T) {
+ t.Run("TextMessageStartEvent_WithAutoMessageID", func(t *testing.T) {
+ event := NewTextMessageStartEvent("", WithAutoMessageID())
+
+ assert.NotEmpty(t, event.MessageID)
+ assert.True(t, strings.HasPrefix(event.MessageID, "msg-"))
+ })
+
+ t.Run("TextMessageContentEvent_WithAutoMessageIDContent", func(t *testing.T) {
+ event := NewTextMessageContentEventWithOptions("", "Hello", WithAutoMessageIDContent())
+
+ assert.NotEmpty(t, event.MessageID)
+ assert.True(t, strings.HasPrefix(event.MessageID, "msg-"))
+ })
+
+ t.Run("TextMessageEndEvent_WithAutoMessageIDEnd", func(t *testing.T) {
+ event := NewTextMessageEndEventWithOptions("", WithAutoMessageIDEnd())
+
+ assert.NotEmpty(t, event.MessageID)
+ assert.True(t, strings.HasPrefix(event.MessageID, "msg-"))
+ })
+
+ t.Run("ToolCallStartEvent_WithAutoToolCallID", func(t *testing.T) {
+ event := NewToolCallStartEvent("", "get_weather", WithAutoToolCallID())
+
+ assert.NotEmpty(t, event.ToolCallID)
+ assert.True(t, strings.HasPrefix(event.ToolCallID, "tool-"))
+ })
+
+ t.Run("ToolCallArgsEvent_WithAutoToolCallIDArgs", func(t *testing.T) {
+ event := NewToolCallArgsEventWithOptions("", "{}", WithAutoToolCallIDArgs())
+
+ assert.NotEmpty(t, event.ToolCallID)
+ assert.True(t, strings.HasPrefix(event.ToolCallID, "tool-"))
+ })
+
+ t.Run("ToolCallEndEvent_WithAutoToolCallIDEnd", func(t *testing.T) {
+ event := NewToolCallEndEventWithOptions("", WithAutoToolCallIDEnd())
+
+ assert.NotEmpty(t, event.ToolCallID)
+ assert.True(t, strings.HasPrefix(event.ToolCallID, "tool-"))
+ })
+
+ t.Run("RunStartedEvent_WithAutoRunID", func(t *testing.T) {
+ event := NewRunStartedEventWithOptions("thread-123", "", WithAutoRunID())
+
+ assert.NotEmpty(t, event.RunIDValue)
+ assert.True(t, strings.HasPrefix(event.RunIDValue, "run-"))
+ })
+
+ t.Run("RunStartedEvent_WithAutoThreadID", func(t *testing.T) {
+ event := NewRunStartedEventWithOptions("", "run-123", WithAutoThreadID())
+
+ assert.NotEmpty(t, event.ThreadIDValue)
+ assert.True(t, strings.HasPrefix(event.ThreadIDValue, "thread-"))
+ })
+
+ t.Run("RunFinishedEvent_WithAutoRunIDFinished", func(t *testing.T) {
+ event := NewRunFinishedEventWithOptions("thread-123", "", WithAutoRunIDFinished())
+
+ assert.NotEmpty(t, event.RunIDValue)
+ assert.True(t, strings.HasPrefix(event.RunIDValue, "run-"))
+ })
+
+ t.Run("RunFinishedEvent_WithAutoThreadIDFinished", func(t *testing.T) {
+ event := NewRunFinishedEventWithOptions("", "run-123", WithAutoThreadIDFinished())
+
+ assert.NotEmpty(t, event.ThreadIDValue)
+ assert.True(t, strings.HasPrefix(event.ThreadIDValue, "thread-"))
+ })
+
+ t.Run("RunErrorEvent_WithAutoRunIDError", func(t *testing.T) {
+ event := NewRunErrorEvent("Error", WithAutoRunIDError())
+
+ assert.NotEmpty(t, event.RunIDValue)
+ assert.True(t, strings.HasPrefix(event.RunIDValue, "run-"))
+ })
+
+ t.Run("StepStartedEvent_WithAutoStepName", func(t *testing.T) {
+ event := NewStepStartedEventWithOptions("", WithAutoStepName())
+
+ assert.NotEmpty(t, event.StepName)
+ assert.True(t, strings.HasPrefix(event.StepName, "step-"))
+ })
+
+ t.Run("StepFinishedEvent_WithAutoStepNameFinished", func(t *testing.T) {
+ event := NewStepFinishedEventWithOptions("", WithAutoStepNameFinished())
+
+ assert.NotEmpty(t, event.StepName)
+ assert.True(t, strings.HasPrefix(event.StepName, "step-"))
+ })
+}
+
+func TestOptionalEventCreators(t *testing.T) {
+ t.Run("NewTextMessageContentEventWithOptions", func(t *testing.T) {
+ event := NewTextMessageContentEventWithOptions("msg-123", "Hello")
+
+ assert.Equal(t, "msg-123", event.MessageID)
+ assert.Equal(t, "Hello", event.Delta)
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("NewTextMessageEndEventWithOptions", func(t *testing.T) {
+ event := NewTextMessageEndEventWithOptions("msg-123")
+
+ assert.Equal(t, "msg-123", event.MessageID)
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("NewToolCallArgsEventWithOptions", func(t *testing.T) {
+ event := NewToolCallArgsEventWithOptions("tool-123", "{}")
+
+ assert.Equal(t, "tool-123", event.ToolCallID)
+ assert.Equal(t, "{}", event.Delta)
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("NewToolCallEndEventWithOptions", func(t *testing.T) {
+ event := NewToolCallEndEventWithOptions("tool-123")
+
+ assert.Equal(t, "tool-123", event.ToolCallID)
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("NewRunStartedEventWithOptions", func(t *testing.T) {
+ event := NewRunStartedEventWithOptions("thread-123", "run-456")
+
+ assert.Equal(t, "thread-123", event.ThreadIDValue)
+ assert.Equal(t, "run-456", event.RunIDValue)
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("NewRunFinishedEventWithOptions", func(t *testing.T) {
+ event := NewRunFinishedEventWithOptions("thread-123", "run-456")
+
+ assert.Equal(t, "thread-123", event.ThreadIDValue)
+ assert.Equal(t, "run-456", event.RunIDValue)
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("NewStepStartedEventWithOptions", func(t *testing.T) {
+ event := NewStepStartedEventWithOptions("step-1")
+
+ assert.Equal(t, "step-1", event.StepName)
+ assert.NoError(t, event.Validate())
+ })
+
+ t.Run("NewStepFinishedEventWithOptions", func(t *testing.T) {
+ event := NewStepFinishedEventWithOptions("step-1")
+
+ assert.Equal(t, "step-1", event.StepName)
+ assert.NoError(t, event.Validate())
+ })
+}
+
+func TestRunFinishedEvent_WithResult(t *testing.T) {
+ result := map[string]interface{}{
+ "status": "success",
+ "data": "completed",
+ }
+ event := NewRunFinishedEventWithOptions("thread-123", "run-456", WithResult(result))
+
+ assert.Equal(t, result, event.Result)
+
+ // Test JSON serialization with result
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.NotNil(t, decoded["result"])
+}
+
+func TestStateDeltaEvent_ToJSON(t *testing.T) {
+ delta := []JSONPatchOperation{
+ {Op: "add", Path: "/field", Value: "value"},
+ }
+ event := NewStateDeltaEvent(delta)
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeStateDelta), decoded["type"])
+ assert.NotNil(t, decoded["delta"])
+}
+
+func TestMessagesSnapshotEvent_ToJSON(t *testing.T) {
+ messages := []Message{
+ {
+ ID: "msg-1",
+ Role: "user",
+ Content: strPtr("Hello"),
+ },
+ }
+ event := NewMessagesSnapshotEvent(messages)
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeMessagesSnapshot), decoded["type"])
+ assert.NotNil(t, decoded["messages"])
+}
+
+func TestRawEvent_ToJSON(t *testing.T) {
+ eventData := map[string]interface{}{"key": "value"}
+ source := "external"
+ event := NewRawEvent(eventData, WithSource(source))
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeRaw), decoded["type"])
+ assert.NotNil(t, decoded["event"])
+ assert.Equal(t, source, decoded["source"])
+}
+
+func TestRunErrorEvent_ToJSON(t *testing.T) {
+ message := "An error occurred"
+ code := "ERR_001"
+ runID := "run-456"
+ event := NewRunErrorEvent(message, WithErrorCode(code), WithRunID(runID))
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeRunError), decoded["type"])
+ assert.Equal(t, message, decoded["message"])
+ assert.Equal(t, code, decoded["code"])
+ assert.Equal(t, runID, decoded["runId"])
+}
+
+func TestStepEvents_ToJSON(t *testing.T) {
+ t.Run("StepStartedEvent", func(t *testing.T) {
+ event := NewStepStartedEvent("step-1")
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeStepStarted), decoded["type"])
+ assert.Equal(t, "step-1", decoded["stepName"])
+ })
+
+ t.Run("StepFinishedEvent", func(t *testing.T) {
+ event := NewStepFinishedEvent("step-1")
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeStepFinished), decoded["type"])
+ assert.Equal(t, "step-1", decoded["stepName"])
+ })
+}
+
+func TestTextMessageEndEvent_ToJSON(t *testing.T) {
+ event := NewTextMessageEndEvent("msg-123")
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeTextMessageEnd), decoded["type"])
+ assert.Equal(t, "msg-123", decoded["messageId"])
+}
+
+func TestToolCallArgsEvent_ToJSON(t *testing.T) {
+ event := NewToolCallArgsEvent("tool-123", "{\"arg\": \"value\"}")
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeToolCallArgs), decoded["type"])
+ assert.Equal(t, "tool-123", decoded["toolCallId"])
+ assert.Equal(t, "{\"arg\": \"value\"}", decoded["delta"])
+}
+
+func TestToolCallEndEvent_ToJSON(t *testing.T) {
+ event := NewToolCallEndEvent("tool-123")
+
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+
+ var decoded map[string]interface{}
+ err = json.Unmarshal(jsonData, &decoded)
+ require.NoError(t, err)
+
+ assert.Equal(t, string(EventTypeToolCallEnd), decoded["type"])
+ assert.Equal(t, "tool-123", decoded["toolCallId"])
+}
\ No newline at end of file
diff --git a/sdks/community/go/pkg/core/events/custom_events.go b/sdks/community/go/pkg/core/events/custom_events.go
new file mode 100644
index 000000000..31d3634ca
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/custom_events.go
@@ -0,0 +1,104 @@
+package events
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// RawEvent contains raw event data that should be passed through without processing
+type RawEvent struct {
+ *BaseEvent
+ Event any `json:"event"`
+ Source *string `json:"source,omitempty"`
+}
+
+// NewRawEvent creates a new raw event
+func NewRawEvent(event any, options ...RawEventOption) *RawEvent {
+ rawEvent := &RawEvent{
+ BaseEvent: NewBaseEvent(EventTypeRaw),
+ Event: event,
+ }
+
+ for _, opt := range options {
+ opt(rawEvent)
+ }
+
+ return rawEvent
+}
+
+// RawEventOption defines options for creating raw events
+type RawEventOption func(*RawEvent)
+
+// WithSource sets the source for the raw event
+func WithSource(source string) RawEventOption {
+ return func(e *RawEvent) {
+ e.Source = &source
+ }
+}
+
+// Validate validates the raw event
+func (e *RawEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.Event == nil {
+ return fmt.Errorf("RawEvent validation failed: event field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *RawEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// CustomEvent contains custom application-specific event data
+type CustomEvent struct {
+ *BaseEvent
+ Name string `json:"name"`
+ Value any `json:"value,omitempty"`
+}
+
+// NewCustomEvent creates a new custom event
+func NewCustomEvent(name string, options ...CustomEventOption) *CustomEvent {
+ event := &CustomEvent{
+ BaseEvent: NewBaseEvent(EventTypeCustom),
+ Name: name,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// CustomEventOption defines options for creating custom events
+type CustomEventOption func(*CustomEvent)
+
+// WithValue sets the value for the custom event
+func WithValue(value any) CustomEventOption {
+ return func(e *CustomEvent) {
+ e.Value = value
+ }
+}
+
+// Validate validates the custom event
+func (e *CustomEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.Name == "" {
+ return fmt.Errorf("CustomEvent validation failed: name field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *CustomEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
diff --git a/sdks/community/go/pkg/core/events/decoder.go b/sdks/community/go/pkg/core/events/decoder.go
new file mode 100644
index 000000000..cf2c0dba7
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/decoder.go
@@ -0,0 +1,200 @@
+package events
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/sirupsen/logrus"
+)
+
+// EventDecoder handles decoding of SSE events to Go SDK event types
+type EventDecoder struct {
+ logger *logrus.Logger
+}
+
+// NewEventDecoder creates a new event decoder
+func NewEventDecoder(logger *logrus.Logger) *EventDecoder {
+ if logger == nil {
+ logger = logrus.New()
+ }
+ return &EventDecoder{logger: logger}
+}
+
+// DecodeEvent decodes a raw SSE event into the appropriate Go SDK event type
+func (ed *EventDecoder) DecodeEvent(eventName string, data []byte) (Event, error) {
+ eventType := EventType(eventName)
+
+ // Check if this is a valid event type
+ if !isValidEventType(eventType) {
+ ed.logger.WithField("event", eventName).Warn("Unknown event type")
+ return nil, fmt.Errorf("unknown event type: %s", eventName)
+ }
+
+ // Decode based on event type
+ switch eventType {
+ case EventTypeRunStarted:
+ var evt RunStartedEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode RUN_STARTED: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeRunFinished:
+ var evt RunFinishedEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode RUN_FINISHED: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeRunError:
+ var evt RunErrorEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode RUN_ERROR: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeTextMessageStart:
+ var evt TextMessageStartEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode TEXT_MESSAGE_START: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeTextMessageContent:
+ var evt TextMessageContentEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode TEXT_MESSAGE_CONTENT: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeTextMessageEnd:
+ var evt TextMessageEndEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode TEXT_MESSAGE_END: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeToolCallStart:
+ var evt ToolCallStartEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode TOOL_CALL_START: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeToolCallArgs:
+ var evt ToolCallArgsEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode TOOL_CALL_ARGS: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeToolCallEnd:
+ var evt ToolCallEndEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode TOOL_CALL_END: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeToolCallResult:
+ var evt ToolCallResultEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode TOOL_CALL_RESULT: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeStateSnapshot:
+ var evt StateSnapshotEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode STATE_SNAPSHOT: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeStateDelta:
+ var evt StateDeltaEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode STATE_DELTA: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeMessagesSnapshot:
+ var evt MessagesSnapshotEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode MESSAGES_SNAPSHOT: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeStepStarted:
+ var evt StepStartedEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode STEP_STARTED: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeStepFinished:
+ var evt StepFinishedEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode STEP_FINISHED: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeThinkingStart:
+ var evt ThinkingStartEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode THINKING_START: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeThinkingEnd:
+ var evt ThinkingEndEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode THINKING_END: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeThinkingTextMessageStart:
+ var evt ThinkingTextMessageStartEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode THINKING_TEXT_MESSAGE_START: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeThinkingTextMessageContent:
+ var evt ThinkingTextMessageContentEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode THINKING_TEXT_MESSAGE_CONTENT: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeThinkingTextMessageEnd:
+ var evt ThinkingTextMessageEndEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode THINKING_TEXT_MESSAGE_END: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeCustom:
+ var evt CustomEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode CUSTOM: %w", err)
+ }
+ return &evt, nil
+
+ case EventTypeRaw:
+ var evt RawEvent
+ if err := json.Unmarshal(data, &evt); err != nil {
+ return nil, fmt.Errorf("failed to decode RAW: %w", err)
+ }
+ return &evt, nil
+
+ default:
+ // For any other event types, return a raw event
+ source := string(eventType)
+ return &RawEvent{
+ BaseEvent: &BaseEvent{
+ EventType: eventType,
+ },
+ Event: json.RawMessage(data),
+ Source: &source,
+ }, nil
+ }
+}
diff --git a/sdks/community/go/pkg/core/events/decoder_test.go b/sdks/community/go/pkg/core/events/decoder_test.go
new file mode 100644
index 000000000..f6ee7091c
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/decoder_test.go
@@ -0,0 +1,334 @@
+package events
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestEventDecoder(t *testing.T) {
+ t.Run("NewEventDecoder", func(t *testing.T) {
+ // With nil logger
+ decoder := NewEventDecoder(nil)
+ assert.NotNil(t, decoder)
+ assert.NotNil(t, decoder.logger)
+
+ // With custom logger
+ customLogger := logrus.New()
+ decoder = NewEventDecoder(customLogger)
+ assert.NotNil(t, decoder)
+ assert.Equal(t, customLogger, decoder.logger)
+ })
+
+ t.Run("DecodeEvent_RunStarted", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"threadId": "thread-123", "runId": "run-456"}`)
+
+ event, err := decoder.DecodeEvent("RUN_STARTED", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ runEvent, ok := event.(*RunStartedEvent)
+ require.True(t, ok)
+ assert.Equal(t, "thread-123", runEvent.ThreadIDValue)
+ assert.Equal(t, "run-456", runEvent.RunIDValue)
+ })
+
+ t.Run("DecodeEvent_RunFinished", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"threadId": "thread-123", "runId": "run-456", "result": "success"}`)
+
+ event, err := decoder.DecodeEvent("RUN_FINISHED", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ runEvent, ok := event.(*RunFinishedEvent)
+ require.True(t, ok)
+ assert.Equal(t, "thread-123", runEvent.ThreadIDValue)
+ assert.Equal(t, "run-456", runEvent.RunIDValue)
+ })
+
+ t.Run("DecodeEvent_RunError", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"message": "Error occurred", "code": "ERROR_CODE", "runId": "run-456"}`)
+
+ event, err := decoder.DecodeEvent("RUN_ERROR", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ runError, ok := event.(*RunErrorEvent)
+ require.True(t, ok)
+ assert.Equal(t, "Error occurred", runError.Message)
+ assert.Equal(t, "ERROR_CODE", *runError.Code)
+ assert.Equal(t, "run-456", runError.RunIDValue)
+ })
+
+ t.Run("DecodeEvent_TextMessageStart", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"messageId": "msg-123", "role": "user"}`)
+
+ event, err := decoder.DecodeEvent("TEXT_MESSAGE_START", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ msgEvent, ok := event.(*TextMessageStartEvent)
+ require.True(t, ok)
+ assert.Equal(t, "msg-123", msgEvent.MessageID)
+ assert.Equal(t, "user", *msgEvent.Role)
+ })
+
+ t.Run("DecodeEvent_TextMessageContent", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"messageId": "msg-123", "delta": "Hello World"}`)
+
+ event, err := decoder.DecodeEvent("TEXT_MESSAGE_CONTENT", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ msgEvent, ok := event.(*TextMessageContentEvent)
+ require.True(t, ok)
+ assert.Equal(t, "msg-123", msgEvent.MessageID)
+ assert.Equal(t, "Hello World", msgEvent.Delta)
+ })
+
+ t.Run("DecodeEvent_TextMessageEnd", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"messageId": "msg-123"}`)
+
+ event, err := decoder.DecodeEvent("TEXT_MESSAGE_END", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ msgEvent, ok := event.(*TextMessageEndEvent)
+ require.True(t, ok)
+ assert.Equal(t, "msg-123", msgEvent.MessageID)
+ })
+
+ t.Run("DecodeEvent_ToolCallStart", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"toolCallId": "tool-123", "toolCallName": "get_weather", "parentMessageId": "msg-456"}`)
+
+ event, err := decoder.DecodeEvent("TOOL_CALL_START", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ toolEvent, ok := event.(*ToolCallStartEvent)
+ require.True(t, ok)
+ assert.Equal(t, "tool-123", toolEvent.ToolCallID)
+ assert.Equal(t, "get_weather", toolEvent.ToolCallName)
+ assert.Equal(t, "msg-456", *toolEvent.ParentMessageID)
+ })
+
+ t.Run("DecodeEvent_ToolCallArgs", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"toolCallId": "tool-123", "delta": "{\"location\": \"SF\"}"}`)
+
+ event, err := decoder.DecodeEvent("TOOL_CALL_ARGS", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ toolEvent, ok := event.(*ToolCallArgsEvent)
+ require.True(t, ok)
+ assert.Equal(t, "tool-123", toolEvent.ToolCallID)
+ assert.Equal(t, `{"location": "SF"}`, toolEvent.Delta)
+ })
+
+ t.Run("DecodeEvent_ToolCallEnd", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"toolCallId": "tool-123"}`)
+
+ event, err := decoder.DecodeEvent("TOOL_CALL_END", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ toolEvent, ok := event.(*ToolCallEndEvent)
+ require.True(t, ok)
+ assert.Equal(t, "tool-123", toolEvent.ToolCallID)
+ })
+
+ t.Run("DecodeEvent_ToolCallResult", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"messageId": "msg-123", "toolCallId": "tool-123", "content": "Sunny, 72°F", "role": "tool"}`)
+
+ event, err := decoder.DecodeEvent("TOOL_CALL_RESULT", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ resultEvent, ok := event.(*ToolCallResultEvent)
+ require.True(t, ok)
+ assert.Equal(t, "msg-123", resultEvent.MessageID)
+ assert.Equal(t, "tool-123", resultEvent.ToolCallID)
+ assert.Equal(t, "Sunny, 72°F", resultEvent.Content)
+ })
+
+ t.Run("DecodeEvent_StateSnapshot", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"snapshot": {"counter": 42, "status": "active"}}`)
+
+ event, err := decoder.DecodeEvent("STATE_SNAPSHOT", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ stateEvent, ok := event.(*StateSnapshotEvent)
+ require.True(t, ok)
+ assert.NotNil(t, stateEvent.Snapshot)
+ })
+
+ t.Run("DecodeEvent_StateDelta", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"delta": [{"op": "add", "path": "/counter", "value": 42}]}`)
+
+ event, err := decoder.DecodeEvent("STATE_DELTA", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ deltaEvent, ok := event.(*StateDeltaEvent)
+ require.True(t, ok)
+ assert.Len(t, deltaEvent.Delta, 1)
+ assert.Equal(t, "add", deltaEvent.Delta[0].Op)
+ })
+
+ t.Run("DecodeEvent_MessagesSnapshot", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"messages": [{"id": "msg-1", "role": "user", "content": "Hello"}]}`)
+
+ event, err := decoder.DecodeEvent("MESSAGES_SNAPSHOT", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ msgEvent, ok := event.(*MessagesSnapshotEvent)
+ require.True(t, ok)
+ assert.Len(t, msgEvent.Messages, 1)
+ assert.Equal(t, "msg-1", msgEvent.Messages[0].ID)
+ })
+
+ t.Run("DecodeEvent_StepStarted", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"stepName": "step-1"}`)
+
+ event, err := decoder.DecodeEvent("STEP_STARTED", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ stepEvent, ok := event.(*StepStartedEvent)
+ require.True(t, ok)
+ assert.Equal(t, "step-1", stepEvent.StepName)
+ })
+
+ t.Run("DecodeEvent_StepFinished", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"stepName": "step-1"}`)
+
+ event, err := decoder.DecodeEvent("STEP_FINISHED", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ stepEvent, ok := event.(*StepFinishedEvent)
+ require.True(t, ok)
+ assert.Equal(t, "step-1", stepEvent.StepName)
+ })
+
+ t.Run("DecodeEvent_ThinkingEvents", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+
+ // ThinkingStart
+ data := []byte(`{"title": "Processing"}`)
+ event, err := decoder.DecodeEvent("THINKING_START", data)
+ require.NoError(t, err)
+ thinkStart, ok := event.(*ThinkingStartEvent)
+ require.True(t, ok)
+ assert.Equal(t, "Processing", *thinkStart.Title)
+
+ // ThinkingEnd
+ data = []byte(`{}`)
+ event, err = decoder.DecodeEvent("THINKING_END", data)
+ require.NoError(t, err)
+ _, ok = event.(*ThinkingEndEvent)
+ require.True(t, ok)
+
+ // ThinkingTextMessageStart
+ event, err = decoder.DecodeEvent("THINKING_TEXT_MESSAGE_START", data)
+ require.NoError(t, err)
+ _, ok = event.(*ThinkingTextMessageStartEvent)
+ require.True(t, ok)
+
+ // ThinkingTextMessageContent
+ data = []byte(`{"delta": "Thinking..."}`)
+ event, err = decoder.DecodeEvent("THINKING_TEXT_MESSAGE_CONTENT", data)
+ require.NoError(t, err)
+ thinkContent, ok := event.(*ThinkingTextMessageContentEvent)
+ require.True(t, ok)
+ assert.Equal(t, "Thinking...", thinkContent.Delta)
+
+ // ThinkingTextMessageEnd
+ data = []byte(`{}`)
+ event, err = decoder.DecodeEvent("THINKING_TEXT_MESSAGE_END", data)
+ require.NoError(t, err)
+ _, ok = event.(*ThinkingTextMessageEndEvent)
+ require.True(t, ok)
+ })
+
+ t.Run("DecodeEvent_CustomEvent", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"name": "custom-event", "value": {"key": "value"}}`)
+
+ event, err := decoder.DecodeEvent("CUSTOM", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ customEvent, ok := event.(*CustomEvent)
+ require.True(t, ok)
+ assert.Equal(t, "custom-event", customEvent.Name)
+ })
+
+ t.Run("DecodeEvent_RawEvent", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"event": {"custom": "data"}, "source": "external"}`)
+
+ event, err := decoder.DecodeEvent("RAW", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ rawEvent, ok := event.(*RawEvent)
+ require.True(t, ok)
+ assert.Equal(t, "external", *rawEvent.Source)
+ })
+
+ t.Run("DecodeEvent_UnknownEventType", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"some": "data"}`)
+
+ event, err := decoder.DecodeEvent("UNKNOWN_EVENT", data)
+ assert.Error(t, err)
+ assert.Nil(t, event)
+ assert.Contains(t, err.Error(), "unknown event type")
+ })
+
+ t.Run("DecodeEvent_InvalidJSON", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{invalid json}`)
+
+ event, err := decoder.DecodeEvent("RUN_STARTED", data)
+ assert.Error(t, err)
+ assert.Nil(t, event)
+ })
+
+ t.Run("DecodeEvent_UnknownButValidEventType", func(t *testing.T) {
+ decoder := NewEventDecoder(nil)
+ data := []byte(`{"some": "data"}`)
+
+ // This should fall through to the default case and return a RawEvent
+ event, err := decoder.DecodeEvent("TEXT_MESSAGE_CHUNK", data)
+ require.NoError(t, err)
+ require.NotNil(t, event)
+
+ rawEvent, ok := event.(*RawEvent)
+ require.True(t, ok)
+ assert.Equal(t, EventType("TEXT_MESSAGE_CHUNK"), rawEvent.EventType)
+ assert.Equal(t, "TEXT_MESSAGE_CHUNK", *rawEvent.Source)
+ assert.Equal(t, json.RawMessage(data), rawEvent.Event)
+ })
+}
\ No newline at end of file
diff --git a/sdks/community/go/pkg/core/events/events.go b/sdks/community/go/pkg/core/events/events.go
new file mode 100644
index 000000000..007fce174
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/events.go
@@ -0,0 +1,396 @@
+package events
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+)
+
+// EventType represents the type of AG-UI event
+type EventType string
+
+// AG-UI Event Type constants - matching the protocol specification
+// TODO: Verify nothing has been hallucinated here
+const (
+ EventTypeTextMessageStart EventType = "TEXT_MESSAGE_START"
+ EventTypeTextMessageContent EventType = "TEXT_MESSAGE_CONTENT"
+ EventTypeTextMessageEnd EventType = "TEXT_MESSAGE_END"
+ EventTypeTextMessageChunk EventType = "TEXT_MESSAGE_CHUNK"
+ EventTypeToolCallStart EventType = "TOOL_CALL_START"
+ EventTypeToolCallArgs EventType = "TOOL_CALL_ARGS"
+ EventTypeToolCallEnd EventType = "TOOL_CALL_END"
+ EventTypeToolCallChunk EventType = "TOOL_CALL_CHUNK"
+ EventTypeToolCallResult EventType = "TOOL_CALL_RESULT"
+ EventTypeStateSnapshot EventType = "STATE_SNAPSHOT"
+ EventTypeStateDelta EventType = "STATE_DELTA"
+ EventTypeMessagesSnapshot EventType = "MESSAGES_SNAPSHOT"
+ EventTypeRaw EventType = "RAW"
+ EventTypeCustom EventType = "CUSTOM"
+ EventTypeRunStarted EventType = "RUN_STARTED"
+ EventTypeRunFinished EventType = "RUN_FINISHED"
+ EventTypeRunError EventType = "RUN_ERROR"
+ EventTypeStepStarted EventType = "STEP_STARTED"
+ EventTypeStepFinished EventType = "STEP_FINISHED"
+
+ // Thinking events for reasoning phase support
+ EventTypeThinkingStart EventType = "THINKING_START"
+ EventTypeThinkingEnd EventType = "THINKING_END"
+ EventTypeThinkingTextMessageStart EventType = "THINKING_TEXT_MESSAGE_START"
+ EventTypeThinkingTextMessageContent EventType = "THINKING_TEXT_MESSAGE_CONTENT"
+ EventTypeThinkingTextMessageEnd EventType = "THINKING_TEXT_MESSAGE_END"
+
+ // EventTypeUnknown represents an unrecognized event type
+ EventTypeUnknown EventType = "UNKNOWN"
+)
+
+// validEventTypes is a map for O(1) lookup of valid event types
+var validEventTypes = map[EventType]bool{
+ EventTypeTextMessageStart: true,
+ EventTypeTextMessageContent: true,
+ EventTypeTextMessageEnd: true,
+ EventTypeTextMessageChunk: true,
+ EventTypeToolCallStart: true,
+ EventTypeToolCallArgs: true,
+ EventTypeToolCallEnd: true,
+ EventTypeToolCallChunk: true,
+ EventTypeToolCallResult: true,
+ EventTypeStateSnapshot: true,
+ EventTypeStateDelta: true,
+ EventTypeMessagesSnapshot: true,
+ EventTypeRaw: true,
+ EventTypeCustom: true,
+ EventTypeRunStarted: true,
+ EventTypeRunFinished: true,
+ EventTypeRunError: true,
+ EventTypeStepStarted: true,
+ EventTypeStepFinished: true,
+ EventTypeThinkingStart: true,
+ EventTypeThinkingEnd: true,
+ EventTypeThinkingTextMessageStart: true,
+ EventTypeThinkingTextMessageContent: true,
+ EventTypeThinkingTextMessageEnd: true,
+}
+
+// Event defines the common interface for all AG-UI events
+type Event interface {
+ // Type returns the event type
+ Type() EventType
+
+ // Timestamp returns the event timestamp (Unix milliseconds)
+ Timestamp() *int64
+
+ // SetTimestamp sets the event timestamp
+ SetTimestamp(timestamp int64)
+
+ // ThreadID returns the thread ID associated with this event
+ ThreadID() string
+
+ // RunID returns the run ID associated with this event
+ RunID() string
+
+ // Validate validates the event structure and content
+ Validate() error
+
+ // ToJSON serializes the event to JSON for cross-SDK compatibility
+ ToJSON() ([]byte, error)
+
+ // GetBaseEvent returns the underlying base event
+ GetBaseEvent() *BaseEvent
+}
+
+// BaseEvent provides common fields and functionality for all events
+type BaseEvent struct {
+ EventType EventType `json:"type"`
+ TimestampMs *int64 `json:"timestamp,omitempty"`
+ RawEvent any `json:"rawEvent,omitempty"`
+}
+
+// Type returns the event type
+func (b *BaseEvent) Type() EventType {
+ return b.EventType
+}
+
+// Timestamp returns the event timestamp
+func (b *BaseEvent) Timestamp() *int64 {
+ return b.TimestampMs
+}
+
+// SetTimestamp sets the event timestamp
+func (b *BaseEvent) SetTimestamp(timestamp int64) {
+ b.TimestampMs = ×tamp
+}
+
+// ID returns the unique identifier for this event
+func (b *BaseEvent) ID() string {
+ // Generate a unique ID based on event type and timestamp
+ if b.TimestampMs != nil {
+ return fmt.Sprintf("%s_%d", b.EventType, *b.TimestampMs)
+ }
+ return fmt.Sprintf("%s_%d", b.EventType, time.Now().UnixMilli())
+}
+
+// ToJSON serializes the base event to JSON
+func (b *BaseEvent) ToJSON() ([]byte, error) {
+ eventData := map[string]interface{}{
+ "type": b.EventType,
+ }
+
+ if b.TimestampMs != nil {
+ eventData["timestamp"] = *b.TimestampMs
+ }
+
+ if b.RawEvent != nil {
+ eventData["data"] = b.RawEvent
+ }
+
+ return json.Marshal(eventData)
+}
+
+// GetBaseEvent returns the base event
+func (b *BaseEvent) GetBaseEvent() *BaseEvent {
+ return b
+}
+
+// ThreadID returns the thread ID (default implementation returns empty string)
+func (b *BaseEvent) ThreadID() string {
+ return ""
+}
+
+// RunID returns the run ID (default implementation returns empty string)
+func (b *BaseEvent) RunID() string {
+ return ""
+}
+
+// NewBaseEvent creates a new base event with the given type and current timestamp
+func NewBaseEvent(eventType EventType) *BaseEvent {
+ now := time.Now().UnixMilli()
+ return &BaseEvent{
+ EventType: eventType,
+ TimestampMs: &now,
+ }
+}
+
+// Validate validates the base event structure
+func (b *BaseEvent) Validate() error {
+ if b.EventType == "" {
+ return fmt.Errorf("BaseEvent validation failed: type field is required")
+ }
+
+ if !isValidEventType(b.EventType) {
+ return fmt.Errorf("BaseEvent validation failed: invalid event type '%s'", b.EventType)
+ }
+
+ return nil
+}
+
+// isValidEventType checks if the given event type is valid
+func isValidEventType(eventType EventType) bool {
+ return validEventTypes[eventType]
+}
+
+// ValidateSequence validates a sequence of events according to AG-UI protocol rules
+func ValidateSequence(events []Event) error {
+ if len(events) == 0 {
+ return nil
+ }
+
+ // Track active runs, messages, tool calls, and steps
+ activeRuns := make(map[string]bool)
+ activeMessages := make(map[string]bool)
+ activeToolCalls := make(map[string]bool)
+ activeSteps := make(map[string]bool)
+ finishedRuns := make(map[string]bool)
+
+ for i, event := range events {
+ if err := event.Validate(); err != nil {
+ return fmt.Errorf("event %d validation failed: %w", i, err)
+ }
+
+ // Check sequence-specific validation rules
+ switch event.Type() {
+ case EventTypeRunStarted:
+ if runEvent, ok := event.(*RunStartedEvent); ok {
+ if activeRuns[runEvent.RunID()] {
+ return fmt.Errorf("run %s already started", runEvent.RunID())
+ }
+ if finishedRuns[runEvent.RunID()] {
+ return fmt.Errorf("cannot restart finished run %s", runEvent.RunID())
+ }
+ activeRuns[runEvent.RunID()] = true
+ }
+
+ case EventTypeRunFinished:
+ if runEvent, ok := event.(*RunFinishedEvent); ok {
+ if !activeRuns[runEvent.RunID()] {
+ return fmt.Errorf("cannot finish run %s that was not started", runEvent.RunID())
+ }
+ delete(activeRuns, runEvent.RunID())
+ finishedRuns[runEvent.RunID()] = true
+ }
+
+ case EventTypeRunError:
+ if runEvent, ok := event.(*RunErrorEvent); ok {
+ if runEvent.RunID() != "" && !activeRuns[runEvent.RunID()] {
+ return fmt.Errorf("cannot error run %s that was not started", runEvent.RunID())
+ }
+ if runEvent.RunID() != "" {
+ delete(activeRuns, runEvent.RunID())
+ finishedRuns[runEvent.RunID()] = true
+ }
+ }
+
+ case EventTypeStepStarted:
+ if stepEvent, ok := event.(*StepStartedEvent); ok {
+ if activeSteps[stepEvent.StepName] {
+ return fmt.Errorf("step %s already started", stepEvent.StepName)
+ }
+ activeSteps[stepEvent.StepName] = true
+ }
+
+ case EventTypeStepFinished:
+ if stepEvent, ok := event.(*StepFinishedEvent); ok {
+ if !activeSteps[stepEvent.StepName] {
+ return fmt.Errorf("cannot finish step %s that was not started", stepEvent.StepName)
+ }
+ delete(activeSteps, stepEvent.StepName)
+ }
+
+ case EventTypeTextMessageStart:
+ if msgEvent, ok := event.(*TextMessageStartEvent); ok {
+ if activeMessages[msgEvent.MessageID] {
+ return fmt.Errorf("message %s already started", msgEvent.MessageID)
+ }
+ activeMessages[msgEvent.MessageID] = true
+ }
+
+ case EventTypeTextMessageContent:
+ if msgEvent, ok := event.(*TextMessageContentEvent); ok {
+ if !activeMessages[msgEvent.MessageID] {
+ return fmt.Errorf("cannot add content to message %s that was not started", msgEvent.MessageID)
+ }
+ // Content events are valid between start and end
+ }
+
+ case EventTypeTextMessageEnd:
+ if msgEvent, ok := event.(*TextMessageEndEvent); ok {
+ if !activeMessages[msgEvent.MessageID] {
+ return fmt.Errorf("cannot end message %s that was not started", msgEvent.MessageID)
+ }
+ delete(activeMessages, msgEvent.MessageID)
+ }
+
+ case EventTypeToolCallStart:
+ if toolEvent, ok := event.(*ToolCallStartEvent); ok {
+ if activeToolCalls[toolEvent.ToolCallID] {
+ return fmt.Errorf("tool call %s already started", toolEvent.ToolCallID)
+ }
+ activeToolCalls[toolEvent.ToolCallID] = true
+ }
+
+ case EventTypeToolCallArgs:
+ if toolEvent, ok := event.(*ToolCallArgsEvent); ok {
+ if !activeToolCalls[toolEvent.ToolCallID] {
+ return fmt.Errorf("cannot add args to tool call %s that was not started", toolEvent.ToolCallID)
+ }
+ // Args events are valid between start and end
+ }
+
+ case EventTypeToolCallEnd:
+ if toolEvent, ok := event.(*ToolCallEndEvent); ok {
+ if !activeToolCalls[toolEvent.ToolCallID] {
+ return fmt.Errorf("cannot end tool call %s that was not started", toolEvent.ToolCallID)
+ }
+ delete(activeToolCalls, toolEvent.ToolCallID)
+ }
+
+ case EventTypeStateSnapshot:
+ // State snapshot events are always valid in sequence context
+ // They represent complete state at any point in time
+ // Additional validation could be added if needed (e.g., frequency limits)
+
+ case EventTypeStateDelta:
+ // State delta events are always valid in sequence context
+ // They represent incremental changes at any point in time
+ // Additional validation could be added if needed (e.g., conflict detection)
+
+ case EventTypeMessagesSnapshot:
+ // Message snapshot events are always valid in sequence context
+ // They represent complete message state at any point in time
+ // Additional validation could be added if needed (e.g., consistency checks)
+
+ case EventTypeRaw:
+ // Raw events are always valid in sequence context
+ // They contain external data that should be passed through
+ // Additional validation could be added via custom validators
+
+ case EventTypeCustom:
+ // Custom events are always valid in sequence context
+ // They contain application-specific data
+ // Additional validation could be added via custom validators
+
+ default:
+ // This should not happen due to prior validation, but add safety check
+ return fmt.Errorf("unknown event type in sequence: %s", event.Type())
+ }
+ }
+
+ return nil
+}
+
+// EventFromJSON parses an event from JSON data
+func EventFromJSON(data []byte) (Event, error) {
+ // First, parse the base event to determine the type
+ var base struct {
+ Type EventType `json:"type"`
+ }
+
+ if err := json.Unmarshal(data, &base); err != nil {
+ return nil, fmt.Errorf("failed to parse event type: %w", err)
+ }
+
+ // Create the appropriate event type based on the type field
+ var event Event
+ switch base.Type {
+ case EventTypeRunStarted:
+ event = &RunStartedEvent{}
+ case EventTypeRunFinished:
+ event = &RunFinishedEvent{}
+ case EventTypeRunError:
+ event = &RunErrorEvent{}
+ case EventTypeStepStarted:
+ event = &StepStartedEvent{}
+ case EventTypeStepFinished:
+ event = &StepFinishedEvent{}
+ case EventTypeTextMessageStart:
+ event = &TextMessageStartEvent{}
+ case EventTypeTextMessageContent:
+ event = &TextMessageContentEvent{}
+ case EventTypeTextMessageEnd:
+ event = &TextMessageEndEvent{}
+ case EventTypeToolCallStart:
+ event = &ToolCallStartEvent{}
+ case EventTypeToolCallArgs:
+ event = &ToolCallArgsEvent{}
+ case EventTypeToolCallEnd:
+ event = &ToolCallEndEvent{}
+ case EventTypeStateSnapshot:
+ event = &StateSnapshotEvent{}
+ case EventTypeStateDelta:
+ event = &StateDeltaEvent{}
+ case EventTypeMessagesSnapshot:
+ event = &MessagesSnapshotEvent{}
+ case EventTypeRaw:
+ event = &RawEvent{}
+ case EventTypeCustom:
+ event = &CustomEvent{}
+ default:
+ return nil, fmt.Errorf("unknown event type: %s", base.Type)
+ }
+
+ // Unmarshal into the specific event type
+ if err := json.Unmarshal(data, event); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal event: %w", err)
+ }
+
+ return event, nil
+}
diff --git a/sdks/community/go/pkg/core/events/events_test.go b/sdks/community/go/pkg/core/events/events_test.go
new file mode 100644
index 000000000..e11843c98
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/events_test.go
@@ -0,0 +1,519 @@
+package events
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestBaseEvent(t *testing.T) {
+ t.Run("NewBaseEvent", func(t *testing.T) {
+ eventType := EventTypeRunStarted
+ base := NewBaseEvent(eventType)
+
+ assert.Equal(t, eventType, base.Type())
+ assert.NotNil(t, base.Timestamp())
+ assert.True(t, *base.Timestamp() > 0)
+ })
+
+ t.Run("SetTimestamp", func(t *testing.T) {
+ base := NewBaseEvent(EventTypeRunStarted)
+ timestamp := time.Now().UnixMilli()
+
+ base.SetTimestamp(timestamp)
+ assert.Equal(t, timestamp, *base.Timestamp())
+ })
+
+ t.Run("Validate", func(t *testing.T) {
+ // Valid base event
+ base := NewBaseEvent(EventTypeRunStarted)
+ assert.NoError(t, base.Validate())
+
+ // Invalid event type
+ base.EventType = ""
+ assert.Error(t, base.Validate())
+
+ // Unknown event type
+ base.EventType = "UNKNOWN"
+ assert.Error(t, base.Validate())
+ })
+}
+
+func TestRunEvents(t *testing.T) {
+ t.Run("RunStartedEvent", func(t *testing.T) {
+ threadID := "thread-123"
+ runID := "run-456"
+
+ event := NewRunStartedEvent(threadID, runID)
+
+ assert.Equal(t, EventTypeRunStarted, event.Type())
+ assert.Equal(t, threadID, event.ThreadID())
+ assert.Equal(t, runID, event.RunID())
+ assert.NoError(t, event.Validate())
+
+ // Test JSON serialization
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+ assert.Contains(t, string(jsonData), threadID)
+ assert.Contains(t, string(jsonData), runID)
+
+ // Test validation errors
+ event.ThreadIDValue = ""
+ assert.Error(t, event.Validate())
+
+ event.ThreadIDValue = threadID
+ event.RunIDValue = ""
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("RunFinishedEvent", func(t *testing.T) {
+ threadID := "thread-123"
+ runID := "run-456"
+
+ event := NewRunFinishedEvent(threadID, runID)
+
+ assert.Equal(t, EventTypeRunFinished, event.Type())
+ assert.Equal(t, threadID, event.ThreadID())
+ assert.Equal(t, runID, event.RunID())
+ assert.NoError(t, event.Validate())
+
+ // Test JSON serialization
+ jsonData, err := event.ToJSON()
+ require.NoError(t, err)
+ assert.Contains(t, string(jsonData), threadID)
+ })
+
+ t.Run("RunErrorEvent", func(t *testing.T) {
+ message := "Something went wrong"
+ code := "ERROR_CODE"
+ runID := "run-456"
+
+ event := NewRunErrorEvent(message, WithErrorCode(code), WithRunID(runID))
+
+ assert.Equal(t, EventTypeRunError, event.Type())
+ assert.Equal(t, message, event.Message)
+ assert.Equal(t, &code, event.Code)
+ assert.Equal(t, runID, event.RunID())
+ assert.NoError(t, event.Validate())
+
+ // Test validation error
+ event.Message = ""
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("StepStartedEvent", func(t *testing.T) {
+ stepName := "step-1"
+
+ event := NewStepStartedEvent(stepName)
+
+ assert.Equal(t, EventTypeStepStarted, event.Type())
+ assert.Equal(t, stepName, event.StepName)
+ assert.NoError(t, event.Validate())
+
+ // Test validation error
+ event.StepName = ""
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("StepFinishedEvent", func(t *testing.T) {
+ stepName := "step-1"
+
+ event := NewStepFinishedEvent(stepName)
+
+ assert.Equal(t, EventTypeStepFinished, event.Type())
+ assert.Equal(t, stepName, event.StepName)
+ assert.NoError(t, event.Validate())
+ })
+}
+
+func TestMessageEvents(t *testing.T) {
+ t.Run("TextMessageStartEvent", func(t *testing.T) {
+ messageID := "msg-123"
+ role := "user"
+
+ event := NewTextMessageStartEvent(messageID, WithRole(role))
+
+ assert.Equal(t, EventTypeTextMessageStart, event.Type())
+ assert.Equal(t, messageID, event.MessageID)
+ assert.Equal(t, &role, event.Role)
+ assert.NoError(t, event.Validate())
+
+ // Test without role
+ event2 := NewTextMessageStartEvent(messageID)
+ assert.Nil(t, event2.Role)
+ assert.NoError(t, event2.Validate())
+
+ // Test validation error
+ event.MessageID = ""
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("TextMessageContentEvent", func(t *testing.T) {
+ messageID := "msg-123"
+ delta := "Hello"
+
+ event := NewTextMessageContentEvent(messageID, delta)
+
+ assert.Equal(t, EventTypeTextMessageContent, event.Type())
+ assert.Equal(t, messageID, event.MessageID)
+ assert.Equal(t, delta, event.Delta)
+ assert.NoError(t, event.Validate())
+
+ // Test validation errors
+ event.MessageID = ""
+ assert.Error(t, event.Validate())
+
+ event.MessageID = messageID
+ event.Delta = ""
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("TextMessageEndEvent", func(t *testing.T) {
+ messageID := "msg-123"
+
+ event := NewTextMessageEndEvent(messageID)
+
+ assert.Equal(t, EventTypeTextMessageEnd, event.Type())
+ assert.Equal(t, messageID, event.MessageID)
+ assert.NoError(t, event.Validate())
+
+ // Test validation error
+ event.MessageID = ""
+ assert.Error(t, event.Validate())
+ })
+}
+
+func TestToolEvents(t *testing.T) {
+ t.Run("ToolCallStartEvent", func(t *testing.T) {
+ toolCallID := "tool-123"
+ toolCallName := "get_weather"
+ parentMessageID := "msg-456"
+
+ event := NewToolCallStartEvent(toolCallID, toolCallName, WithParentMessageID(parentMessageID))
+
+ assert.Equal(t, EventTypeToolCallStart, event.Type())
+ assert.Equal(t, toolCallID, event.ToolCallID)
+ assert.Equal(t, toolCallName, event.ToolCallName)
+ assert.Equal(t, &parentMessageID, event.ParentMessageID)
+ assert.NoError(t, event.Validate())
+
+ // Test validation errors
+ event.ToolCallID = ""
+ assert.Error(t, event.Validate())
+
+ event.ToolCallID = toolCallID
+ event.ToolCallName = ""
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("ToolCallArgsEvent", func(t *testing.T) {
+ toolCallID := "tool-123"
+ delta := "{\"location\": \"San Francisco\"}"
+
+ event := NewToolCallArgsEvent(toolCallID, delta)
+
+ assert.Equal(t, EventTypeToolCallArgs, event.Type())
+ assert.Equal(t, toolCallID, event.ToolCallID)
+ assert.Equal(t, delta, event.Delta)
+ assert.NoError(t, event.Validate())
+
+ // Test validation errors
+ event.ToolCallID = ""
+ assert.Error(t, event.Validate())
+
+ event.ToolCallID = toolCallID
+ event.Delta = ""
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("ToolCallEndEvent", func(t *testing.T) {
+ toolCallID := "tool-123"
+
+ event := NewToolCallEndEvent(toolCallID)
+
+ assert.Equal(t, EventTypeToolCallEnd, event.Type())
+ assert.Equal(t, toolCallID, event.ToolCallID)
+ assert.NoError(t, event.Validate())
+
+ // Test validation error
+ event.ToolCallID = ""
+ assert.Error(t, event.Validate())
+ })
+}
+
+func TestStateEvents(t *testing.T) {
+ t.Run("StateSnapshotEvent", func(t *testing.T) {
+ snapshot := map[string]any{
+ "counter": 42,
+ "status": "active",
+ }
+
+ event := NewStateSnapshotEvent(snapshot)
+
+ assert.Equal(t, EventTypeStateSnapshot, event.Type())
+ assert.Equal(t, snapshot, event.Snapshot)
+ assert.NoError(t, event.Validate())
+
+ // Test validation error
+ event.Snapshot = nil
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("StateDeltaEvent", func(t *testing.T) {
+ delta := []JSONPatchOperation{
+ {Op: "add", Path: "/counter", Value: 42},
+ {Op: "replace", Path: "/status", Value: "inactive"},
+ }
+
+ event := NewStateDeltaEvent(delta)
+
+ assert.Equal(t, EventTypeStateDelta, event.Type())
+ assert.Equal(t, delta, event.Delta)
+ assert.NoError(t, event.Validate())
+
+ // Test validation errors
+ event.Delta = []JSONPatchOperation{}
+ assert.Error(t, event.Validate())
+
+ // Invalid operation
+ event.Delta = []JSONPatchOperation{
+ {Op: "invalid", Path: "/counter", Value: 42},
+ }
+ assert.Error(t, event.Validate())
+
+ // Missing path
+ event.Delta = []JSONPatchOperation{
+ {Op: "add", Value: 42},
+ }
+ assert.Error(t, event.Validate())
+
+ // Missing value for add operation
+ event.Delta = []JSONPatchOperation{
+ {Op: "add", Path: "/counter"},
+ }
+ assert.Error(t, event.Validate())
+
+ // Missing from for move operation
+ event.Delta = []JSONPatchOperation{
+ {Op: "move", Path: "/counter"},
+ }
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("MessagesSnapshotEvent", func(t *testing.T) {
+ messages := []Message{
+ {
+ ID: "msg-1",
+ Role: "user",
+ Content: strPtr("Hello"),
+ },
+ {
+ ID: "msg-2",
+ Role: "assistant",
+ ToolCalls: []ToolCall{
+ {
+ ID: "tool-1",
+ Type: "function",
+ Function: Function{
+ Name: "get_weather",
+ Arguments: "{\"location\": \"SF\"}",
+ },
+ },
+ },
+ },
+ }
+
+ event := NewMessagesSnapshotEvent(messages)
+
+ assert.Equal(t, EventTypeMessagesSnapshot, event.Type())
+ assert.Equal(t, messages, event.Messages)
+ assert.NoError(t, event.Validate())
+
+ // Test validation errors
+ invalidMessages := []Message{
+ {Role: "user"}, // Missing ID
+ }
+ event.Messages = invalidMessages
+ assert.Error(t, event.Validate())
+
+ invalidMessages = []Message{
+ {ID: "msg-1"}, // Missing role
+ }
+ event.Messages = invalidMessages
+ assert.Error(t, event.Validate())
+
+ invalidMessages = []Message{
+ {
+ ID: "msg-1",
+ Role: "assistant",
+ ToolCalls: []ToolCall{
+ {Type: "function"}, // Missing ID
+ },
+ },
+ }
+ event.Messages = invalidMessages
+ assert.Error(t, event.Validate())
+ })
+}
+
+func TestCustomEvents(t *testing.T) {
+ t.Run("RawEvent", func(t *testing.T) {
+ eventData := map[string]any{"key": "value"}
+ source := "external-system"
+
+ event := NewRawEvent(eventData, WithSource(source))
+
+ assert.Equal(t, EventTypeRaw, event.Type())
+ assert.Equal(t, eventData, event.Event)
+ assert.Equal(t, &source, event.Source)
+ assert.NoError(t, event.Validate())
+
+ // Test validation error
+ event.Event = nil
+ assert.Error(t, event.Validate())
+ })
+
+ t.Run("CustomEvent", func(t *testing.T) {
+ name := "custom-event"
+ value := map[string]any{"data": "test"}
+
+ event := NewCustomEvent(name, WithValue(value))
+
+ assert.Equal(t, EventTypeCustom, event.Type())
+ assert.Equal(t, name, event.Name)
+ assert.Equal(t, value, event.Value)
+ assert.NoError(t, event.Validate())
+
+ // Test validation error
+ event.Name = ""
+ assert.Error(t, event.Validate())
+ })
+}
+
+func TestEventSequenceValidation(t *testing.T) {
+ t.Run("ValidSequence", func(t *testing.T) {
+ events := []Event{
+ NewRunStartedEvent("thread-1", "run-1"),
+ NewTextMessageStartEvent("msg-1"),
+ NewTextMessageContentEvent("msg-1", "Hello"),
+ NewTextMessageEndEvent("msg-1"),
+ NewToolCallStartEvent("tool-1", "get_weather"),
+ NewToolCallArgsEvent("tool-1", "{\"location\": \"SF\"}"),
+ NewToolCallEndEvent("tool-1"),
+ NewRunFinishedEvent("thread-1", "run-1"),
+ }
+
+ assert.NoError(t, ValidateSequence(events))
+ })
+
+ t.Run("InvalidSequence_DuplicateRunStart", func(t *testing.T) {
+ events := []Event{
+ NewRunStartedEvent("thread-1", "run-1"),
+ NewRunStartedEvent("thread-1", "run-1"), // Duplicate
+ }
+
+ assert.Error(t, ValidateSequence(events))
+ })
+
+ t.Run("InvalidSequence_FinishNonExistentRun", func(t *testing.T) {
+ events := []Event{
+ NewRunFinishedEvent("thread-1", "run-1"), // Not started
+ }
+
+ assert.Error(t, ValidateSequence(events))
+ })
+
+ t.Run("InvalidSequence_RestartFinishedRun", func(t *testing.T) {
+ events := []Event{
+ NewRunStartedEvent("thread-1", "run-1"),
+ NewRunFinishedEvent("thread-1", "run-1"),
+ NewRunStartedEvent("thread-1", "run-1"), // Cannot restart
+ }
+
+ assert.Error(t, ValidateSequence(events))
+ })
+
+ t.Run("InvalidSequence_DuplicateMessageStart", func(t *testing.T) {
+ events := []Event{
+ NewTextMessageStartEvent("msg-1"),
+ NewTextMessageStartEvent("msg-1"), // Duplicate
+ }
+
+ assert.Error(t, ValidateSequence(events))
+ })
+
+ t.Run("InvalidSequence_EndNonExistentMessage", func(t *testing.T) {
+ events := []Event{
+ NewTextMessageEndEvent("msg-1"), // Not started
+ }
+
+ assert.Error(t, ValidateSequence(events))
+ })
+
+ t.Run("InvalidSequence_DuplicateToolCallStart", func(t *testing.T) {
+ events := []Event{
+ NewToolCallStartEvent("tool-1", "get_weather"),
+ NewToolCallStartEvent("tool-1", "get_weather"), // Duplicate
+ }
+
+ assert.Error(t, ValidateSequence(events))
+ })
+
+ t.Run("InvalidSequence_EndNonExistentToolCall", func(t *testing.T) {
+ events := []Event{
+ NewToolCallEndEvent("tool-1"), // Not started
+ }
+
+ assert.Error(t, ValidateSequence(events))
+ })
+}
+
+func TestJSONSerialization(t *testing.T) {
+ t.Run("RoundTrip", func(t *testing.T) {
+ // Test various event types
+ testEvents := []Event{
+ NewRunStartedEvent("thread-1", "run-1"),
+ NewTextMessageStartEvent("msg-1", WithRole("user")),
+ NewTextMessageContentEvent("msg-1", "Hello"),
+ NewToolCallStartEvent("tool-1", "get_weather", WithParentMessageID("msg-1")),
+ NewStateSnapshotEvent(map[string]any{"counter": 42}),
+ NewCustomEvent("test-event", WithValue("test-value")),
+ }
+
+ for _, originalEvent := range testEvents {
+ // Serialize to JSON
+ jsonData, err := originalEvent.ToJSON()
+ require.NoError(t, err)
+
+ // Deserialize from JSON
+ parsedEvent, err := EventFromJSON(jsonData)
+ require.NoError(t, err)
+
+ // Verify the event type matches
+ assert.Equal(t, originalEvent.Type(), parsedEvent.Type())
+
+ // Verify the event validates
+ assert.NoError(t, parsedEvent.Validate())
+ }
+ })
+
+ t.Run("UnknownEventType", func(t *testing.T) {
+ jsonData := []byte(`{"type": "UNKNOWN_EVENT"}`)
+ _, err := EventFromJSON(jsonData)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "unknown event type")
+ })
+
+ t.Run("InvalidJSON", func(t *testing.T) {
+ invalidJSON := []byte(`{"type": "RUN_STARTED", invalid}`)
+ _, err := EventFromJSON(invalidJSON)
+ assert.Error(t, err)
+ })
+}
+
+// Helper function to create string pointers
+func strPtr(s string) *string {
+ return &s
+}
diff --git a/sdks/community/go/pkg/core/events/id_utils.go b/sdks/community/go/pkg/core/events/id_utils.go
new file mode 100644
index 000000000..02e32bce7
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/id_utils.go
@@ -0,0 +1,145 @@
+package events
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// IDGenerator provides methods for generating unique event IDs
+type IDGenerator interface {
+ // GenerateRunID generates a unique run ID
+ GenerateRunID() string
+
+ // GenerateMessageID generates a unique message ID
+ GenerateMessageID() string
+
+ // GenerateToolCallID generates a unique tool call ID
+ GenerateToolCallID() string
+
+ // GenerateThreadID generates a unique thread ID
+ GenerateThreadID() string
+
+ // GenerateStepID generates a unique step ID
+ GenerateStepID() string
+}
+
+// DefaultIDGenerator implements IDGenerator using UUID v4
+type DefaultIDGenerator struct{}
+
+// NewDefaultIDGenerator creates a new default ID generator
+func NewDefaultIDGenerator() *DefaultIDGenerator {
+ return &DefaultIDGenerator{}
+}
+
+// GenerateRunID generates a unique run ID with "run-" prefix
+func (g *DefaultIDGenerator) GenerateRunID() string {
+ return fmt.Sprintf("run-%s", uuid.New().String())
+}
+
+// GenerateMessageID generates a unique message ID with "msg-" prefix
+func (g *DefaultIDGenerator) GenerateMessageID() string {
+ return fmt.Sprintf("msg-%s", uuid.New().String())
+}
+
+// GenerateToolCallID generates a unique tool call ID with "tool-" prefix
+func (g *DefaultIDGenerator) GenerateToolCallID() string {
+ return fmt.Sprintf("tool-%s", uuid.New().String())
+}
+
+// GenerateThreadID generates a unique thread ID with "thread-" prefix
+func (g *DefaultIDGenerator) GenerateThreadID() string {
+ return fmt.Sprintf("thread-%s", uuid.New().String())
+}
+
+// GenerateStepID generates a unique step ID with "step-" prefix
+func (g *DefaultIDGenerator) GenerateStepID() string {
+ return fmt.Sprintf("step-%s", uuid.New().String())
+}
+
+// TimestampIDGenerator implements IDGenerator using timestamps and short UUIDs
+type TimestampIDGenerator struct {
+ prefix string
+}
+
+// NewTimestampIDGenerator creates a new timestamp-based ID generator
+func NewTimestampIDGenerator(prefix string) *TimestampIDGenerator {
+ return &TimestampIDGenerator{prefix: prefix}
+}
+
+// GenerateRunID generates a timestamp-based run ID
+func (g *TimestampIDGenerator) GenerateRunID() string {
+ return g.generateTimestampID("run")
+}
+
+// GenerateMessageID generates a timestamp-based message ID
+func (g *TimestampIDGenerator) GenerateMessageID() string {
+ return g.generateTimestampID("msg")
+}
+
+// GenerateToolCallID generates a timestamp-based tool call ID
+func (g *TimestampIDGenerator) GenerateToolCallID() string {
+ return g.generateTimestampID("tool")
+}
+
+// GenerateThreadID generates a timestamp-based thread ID
+func (g *TimestampIDGenerator) GenerateThreadID() string {
+ return g.generateTimestampID("thread")
+}
+
+// GenerateStepID generates a timestamp-based step ID
+func (g *TimestampIDGenerator) GenerateStepID() string {
+ return g.generateTimestampID("step")
+}
+
+// generateTimestampID generates a timestamp-based ID with the given type prefix
+func (g *TimestampIDGenerator) generateTimestampID(typePrefix string) string {
+ timestamp := time.Now().UnixMilli()
+ shortUUID := uuid.New().String()[:8]
+
+ if g.prefix != "" {
+ return fmt.Sprintf("%s-%s-%d-%s", g.prefix, typePrefix, timestamp, shortUUID)
+ }
+ return fmt.Sprintf("%s-%d-%s", typePrefix, timestamp, shortUUID)
+}
+
+// Global default ID generator instance
+var defaultIDGenerator IDGenerator = NewDefaultIDGenerator()
+
+// SetDefaultIDGenerator sets the global default ID generator
+func SetDefaultIDGenerator(generator IDGenerator) {
+ defaultIDGenerator = generator
+}
+
+// GetDefaultIDGenerator returns the current default ID generator
+func GetDefaultIDGenerator() IDGenerator {
+ return defaultIDGenerator
+}
+
+// Convenience functions for generating IDs using the default generator
+
+// GenerateRunID generates a unique run ID using the default generator
+func GenerateRunID() string {
+ return defaultIDGenerator.GenerateRunID()
+}
+
+// GenerateMessageID generates a unique message ID using the default generator
+func GenerateMessageID() string {
+ return defaultIDGenerator.GenerateMessageID()
+}
+
+// GenerateToolCallID generates a unique tool call ID using the default generator
+func GenerateToolCallID() string {
+ return defaultIDGenerator.GenerateToolCallID()
+}
+
+// GenerateThreadID generates a unique thread ID using the default generator
+func GenerateThreadID() string {
+ return defaultIDGenerator.GenerateThreadID()
+}
+
+// GenerateStepID generates a unique step ID using the default generator
+func GenerateStepID() string {
+ return defaultIDGenerator.GenerateStepID()
+}
diff --git a/sdks/community/go/pkg/core/events/id_utils_test.go b/sdks/community/go/pkg/core/events/id_utils_test.go
new file mode 100644
index 000000000..5f033e9a1
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/id_utils_test.go
@@ -0,0 +1,313 @@
+package events
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDefaultIDGenerator(t *testing.T) {
+ t.Run("NewDefaultIDGenerator", func(t *testing.T) {
+ gen := NewDefaultIDGenerator()
+ assert.NotNil(t, gen)
+ })
+
+ t.Run("GenerateRunID", func(t *testing.T) {
+ gen := NewDefaultIDGenerator()
+ id := gen.GenerateRunID()
+
+ assert.True(t, strings.HasPrefix(id, "run-"))
+ assert.Greater(t, len(id), 4)
+
+ // Generate multiple IDs to ensure uniqueness
+ id2 := gen.GenerateRunID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GenerateMessageID", func(t *testing.T) {
+ gen := NewDefaultIDGenerator()
+ id := gen.GenerateMessageID()
+
+ assert.True(t, strings.HasPrefix(id, "msg-"))
+ assert.Greater(t, len(id), 4)
+
+ // Test uniqueness
+ id2 := gen.GenerateMessageID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GenerateToolCallID", func(t *testing.T) {
+ gen := NewDefaultIDGenerator()
+ id := gen.GenerateToolCallID()
+
+ assert.True(t, strings.HasPrefix(id, "tool-"))
+ assert.Greater(t, len(id), 5)
+
+ // Test uniqueness
+ id2 := gen.GenerateToolCallID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GenerateThreadID", func(t *testing.T) {
+ gen := NewDefaultIDGenerator()
+ id := gen.GenerateThreadID()
+
+ assert.True(t, strings.HasPrefix(id, "thread-"))
+ assert.Greater(t, len(id), 7)
+
+ // Test uniqueness
+ id2 := gen.GenerateThreadID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GenerateStepID", func(t *testing.T) {
+ gen := NewDefaultIDGenerator()
+ id := gen.GenerateStepID()
+
+ assert.True(t, strings.HasPrefix(id, "step-"))
+ assert.Greater(t, len(id), 5)
+
+ // Test uniqueness
+ id2 := gen.GenerateStepID()
+ assert.NotEqual(t, id, id2)
+ })
+}
+
+func TestTimestampIDGenerator(t *testing.T) {
+ t.Run("NewTimestampIDGenerator_NoPrefix", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("")
+ assert.NotNil(t, gen)
+ assert.Equal(t, "", gen.prefix)
+ })
+
+ t.Run("NewTimestampIDGenerator_WithPrefix", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("test")
+ assert.NotNil(t, gen)
+ assert.Equal(t, "test", gen.prefix)
+ })
+
+ t.Run("GenerateRunID_NoPrefix", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("")
+ id := gen.GenerateRunID()
+
+ assert.True(t, strings.HasPrefix(id, "run-"))
+ assert.Contains(t, id, "-")
+
+ // Verify timestamp is present
+ parts := strings.Split(id, "-")
+ require.GreaterOrEqual(t, len(parts), 3)
+ timestamp := parts[1]
+ _, err := time.Parse("", timestamp) // Just check it's a number
+ assert.NotNil(t, err) // We expect an error because timestamp is just a number
+
+ // Test uniqueness
+ id2 := gen.GenerateRunID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GenerateRunID_WithPrefix", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("myapp")
+ id := gen.GenerateRunID()
+
+ assert.True(t, strings.HasPrefix(id, "myapp-run-"))
+ assert.Contains(t, id, "-")
+
+ // Test format: prefix-type-timestamp-uuid
+ parts := strings.Split(id, "-")
+ require.GreaterOrEqual(t, len(parts), 4)
+ assert.Equal(t, "myapp", parts[0])
+ assert.Equal(t, "run", parts[1])
+ })
+
+ t.Run("GenerateMessageID", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("")
+ id := gen.GenerateMessageID()
+
+ assert.True(t, strings.HasPrefix(id, "msg-"))
+ assert.Contains(t, id, "-")
+
+ // Test uniqueness
+ id2 := gen.GenerateMessageID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GenerateToolCallID", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("service")
+ id := gen.GenerateToolCallID()
+
+ assert.True(t, strings.HasPrefix(id, "service-tool-"))
+ assert.Contains(t, id, "-")
+
+ // Test uniqueness
+ id2 := gen.GenerateToolCallID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GenerateThreadID", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("")
+ id := gen.GenerateThreadID()
+
+ assert.True(t, strings.HasPrefix(id, "thread-"))
+ assert.Contains(t, id, "-")
+
+ // Test uniqueness
+ id2 := gen.GenerateThreadID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GenerateStepID", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("app")
+ id := gen.GenerateStepID()
+
+ assert.True(t, strings.HasPrefix(id, "app-step-"))
+ assert.Contains(t, id, "-")
+
+ // Test uniqueness
+ id2 := gen.GenerateStepID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("Timestamp_Ordering", func(t *testing.T) {
+ gen := NewTimestampIDGenerator("")
+
+ // Generate IDs with slight delay
+ id1 := gen.GenerateRunID()
+ time.Sleep(2 * time.Millisecond)
+ id2 := gen.GenerateRunID()
+
+ // Extract timestamps
+ parts1 := strings.Split(id1, "-")
+ parts2 := strings.Split(id2, "-")
+
+ require.GreaterOrEqual(t, len(parts1), 3)
+ require.GreaterOrEqual(t, len(parts2), 3)
+
+ // The timestamp in id2 should be >= timestamp in id1
+ // (We can't parse them as ints here but the string comparison should work for ordering)
+ assert.True(t, parts2[1] >= parts1[1])
+ })
+}
+
+func TestGlobalIDGenerator(t *testing.T) {
+ t.Run("GetDefaultIDGenerator", func(t *testing.T) {
+ gen := GetDefaultIDGenerator()
+ assert.NotNil(t, gen)
+
+ // Should be a DefaultIDGenerator by default
+ _, ok := gen.(*DefaultIDGenerator)
+ assert.True(t, ok)
+ })
+
+ t.Run("SetDefaultIDGenerator", func(t *testing.T) {
+ // Save original
+ original := GetDefaultIDGenerator()
+ defer SetDefaultIDGenerator(original)
+
+ // Set custom generator
+ customGen := NewTimestampIDGenerator("custom")
+ SetDefaultIDGenerator(customGen)
+
+ gen := GetDefaultIDGenerator()
+ assert.Equal(t, customGen, gen)
+
+ // Verify it's actually being used
+ timestampGen, ok := gen.(*TimestampIDGenerator)
+ require.True(t, ok)
+ assert.Equal(t, "custom", timestampGen.prefix)
+ })
+
+ t.Run("GlobalGenerateRunID", func(t *testing.T) {
+ // Save original
+ original := GetDefaultIDGenerator()
+ defer SetDefaultIDGenerator(original)
+
+ // Test with default generator
+ id := GenerateRunID()
+ assert.True(t, strings.HasPrefix(id, "run-"))
+
+ // Switch to timestamp generator
+ SetDefaultIDGenerator(NewTimestampIDGenerator("global"))
+ id = GenerateRunID()
+ assert.True(t, strings.HasPrefix(id, "global-run-"))
+ })
+
+ t.Run("GlobalGenerateMessageID", func(t *testing.T) {
+ // Save original
+ original := GetDefaultIDGenerator()
+ defer SetDefaultIDGenerator(original)
+
+ id := GenerateMessageID()
+ assert.True(t, strings.HasPrefix(id, "msg-"))
+
+ // Test uniqueness
+ id2 := GenerateMessageID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GlobalGenerateToolCallID", func(t *testing.T) {
+ // Save original
+ original := GetDefaultIDGenerator()
+ defer SetDefaultIDGenerator(original)
+
+ id := GenerateToolCallID()
+ assert.True(t, strings.HasPrefix(id, "tool-"))
+
+ // Test uniqueness
+ id2 := GenerateToolCallID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GlobalGenerateThreadID", func(t *testing.T) {
+ // Save original
+ original := GetDefaultIDGenerator()
+ defer SetDefaultIDGenerator(original)
+
+ id := GenerateThreadID()
+ assert.True(t, strings.HasPrefix(id, "thread-"))
+
+ // Test uniqueness
+ id2 := GenerateThreadID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("GlobalGenerateStepID", func(t *testing.T) {
+ // Save original
+ original := GetDefaultIDGenerator()
+ defer SetDefaultIDGenerator(original)
+
+ id := GenerateStepID()
+ assert.True(t, strings.HasPrefix(id, "step-"))
+
+ // Test uniqueness
+ id2 := GenerateStepID()
+ assert.NotEqual(t, id, id2)
+ })
+
+ t.Run("Concurrent_ID_Generation", func(t *testing.T) {
+ // Test that concurrent ID generation produces unique IDs
+ gen := NewDefaultIDGenerator()
+ idChan := make(chan string, 100)
+
+ // Generate IDs concurrently
+ for i := 0; i < 100; i++ {
+ go func() {
+ idChan <- gen.GenerateRunID()
+ }()
+ }
+
+ // Collect all IDs
+ ids := make(map[string]bool)
+ for i := 0; i < 100; i++ {
+ id := <-idChan
+ if ids[id] {
+ t.Errorf("Duplicate ID generated: %s", id)
+ }
+ ids[id] = true
+ }
+
+ assert.Equal(t, 100, len(ids))
+ })
+}
\ No newline at end of file
diff --git a/sdks/community/go/pkg/core/events/message_events.go b/sdks/community/go/pkg/core/events/message_events.go
new file mode 100644
index 000000000..c17de766d
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/message_events.go
@@ -0,0 +1,239 @@
+package events
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// TextMessageStartEvent indicates the start of a streaming text message
+type TextMessageStartEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ Role *string `json:"role,omitempty"`
+}
+
+// NewTextMessageStartEvent creates a new text message start event
+func NewTextMessageStartEvent(messageID string, options ...TextMessageStartOption) *TextMessageStartEvent {
+ event := &TextMessageStartEvent{
+ BaseEvent: NewBaseEvent(EventTypeTextMessageStart),
+ MessageID: messageID,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// TextMessageStartOption defines options for creating text message start events
+type TextMessageStartOption func(*TextMessageStartEvent)
+
+// WithRole sets the role for the message
+func WithRole(role string) TextMessageStartOption {
+ return func(e *TextMessageStartEvent) {
+ e.Role = &role
+ }
+}
+
+// WithAutoMessageID automatically generates a unique message ID if the provided messageID is empty
+func WithAutoMessageID() TextMessageStartOption {
+ return func(e *TextMessageStartEvent) {
+ if e.MessageID == "" {
+ e.MessageID = GenerateMessageID()
+ }
+ }
+}
+
+// Validate validates the text message start event
+func (e *TextMessageStartEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.MessageID == "" {
+ return fmt.Errorf("TextMessageStartEvent validation failed: messageId field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *TextMessageStartEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// TextMessageContentEvent contains a piece of streaming text message content
+type TextMessageContentEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ Delta string `json:"delta"`
+}
+
+// NewTextMessageContentEvent creates a new text message content event
+func NewTextMessageContentEvent(messageID, delta string) *TextMessageContentEvent {
+ return &TextMessageContentEvent{
+ BaseEvent: NewBaseEvent(EventTypeTextMessageContent),
+ MessageID: messageID,
+ Delta: delta,
+ }
+}
+
+// NewTextMessageContentEventWithOptions creates a new text message content event with options
+func NewTextMessageContentEventWithOptions(messageID, delta string, options ...TextMessageContentOption) *TextMessageContentEvent {
+ event := &TextMessageContentEvent{
+ BaseEvent: NewBaseEvent(EventTypeTextMessageContent),
+ MessageID: messageID,
+ Delta: delta,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// TextMessageContentOption defines options for creating text message content events
+type TextMessageContentOption func(*TextMessageContentEvent)
+
+// WithAutoMessageIDContent automatically generates a unique message ID if the provided messageID is empty
+func WithAutoMessageIDContent() TextMessageContentOption {
+ return func(e *TextMessageContentEvent) {
+ if e.MessageID == "" {
+ e.MessageID = GenerateMessageID()
+ }
+ }
+}
+
+// Validate validates the text message content event
+func (e *TextMessageContentEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.MessageID == "" {
+ return fmt.Errorf("TextMessageContentEvent validation failed: messageId field is required")
+ }
+
+ if e.Delta == "" {
+ return fmt.Errorf("TextMessageContentEvent validation failed: delta field must not be empty")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *TextMessageContentEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// TextMessageEndEvent indicates the end of a streaming text message
+type TextMessageEndEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+}
+
+// NewTextMessageEndEvent creates a new text message end event
+func NewTextMessageEndEvent(messageID string) *TextMessageEndEvent {
+ return &TextMessageEndEvent{
+ BaseEvent: NewBaseEvent(EventTypeTextMessageEnd),
+ MessageID: messageID,
+ }
+}
+
+// NewTextMessageEndEventWithOptions creates a new text message end event with options
+func NewTextMessageEndEventWithOptions(messageID string, options ...TextMessageEndOption) *TextMessageEndEvent {
+ event := &TextMessageEndEvent{
+ BaseEvent: NewBaseEvent(EventTypeTextMessageEnd),
+ MessageID: messageID,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// TextMessageEndOption defines options for creating text message end events
+type TextMessageEndOption func(*TextMessageEndEvent)
+
+// WithAutoMessageIDEnd automatically generates a unique message ID if the provided messageID is empty
+func WithAutoMessageIDEnd() TextMessageEndOption {
+ return func(e *TextMessageEndEvent) {
+ if e.MessageID == "" {
+ e.MessageID = GenerateMessageID()
+ }
+ }
+}
+
+// Validate validates the text message end event
+func (e *TextMessageEndEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.MessageID == "" {
+ return fmt.Errorf("TextMessageEndEvent validation failed: messageId field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *TextMessageEndEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// TextMessageChunkEvent represents a chunk of text message data
+type TextMessageChunkEvent struct {
+ *BaseEvent
+ MessageID *string `json:"messageId,omitempty"`
+ Role *string `json:"role,omitempty"`
+ Delta *string `json:"delta,omitempty"`
+}
+
+// NewTextMessageChunkEvent creates a new text message chunk event
+func NewTextMessageChunkEvent() *TextMessageChunkEvent {
+ return &TextMessageChunkEvent{
+ BaseEvent: NewBaseEvent(EventTypeTextMessageChunk),
+ }
+}
+
+// WithChunkMessageID sets the message ID for the chunk
+func (e *TextMessageChunkEvent) WithChunkMessageID(id string) *TextMessageChunkEvent {
+ e.MessageID = &id
+ return e
+}
+
+// WithChunkRole sets the role for the chunk
+func (e *TextMessageChunkEvent) WithChunkRole(role string) *TextMessageChunkEvent {
+ e.Role = &role
+ return e
+}
+
+// WithChunkDelta sets the delta content for the chunk
+func (e *TextMessageChunkEvent) WithChunkDelta(delta string) *TextMessageChunkEvent {
+ e.Delta = &delta
+ return e
+}
+
+// Validate validates the text message chunk event
+func (e *TextMessageChunkEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ // At least one field should be present
+ if e.MessageID == nil && e.Role == nil && e.Delta == nil {
+ return fmt.Errorf("TextMessageChunkEvent validation failed: at least one of messageId, role, or delta must be present")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *TextMessageChunkEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
diff --git a/sdks/community/go/pkg/core/events/run_events.go b/sdks/community/go/pkg/core/events/run_events.go
new file mode 100644
index 000000000..521e7de55
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/run_events.go
@@ -0,0 +1,369 @@
+package events
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// RunStartedEvent indicates that an agent run has started
+type RunStartedEvent struct {
+ *BaseEvent
+ ThreadIDValue string `json:"threadId"`
+ RunIDValue string `json:"runId"`
+}
+
+// NewRunStartedEvent creates a new run started event
+func NewRunStartedEvent(threadID, runID string) *RunStartedEvent {
+ return &RunStartedEvent{
+ BaseEvent: NewBaseEvent(EventTypeRunStarted),
+ ThreadIDValue: threadID,
+ RunIDValue: runID,
+ }
+}
+
+// NewRunStartedEventWithOptions creates a new run started event with options
+func NewRunStartedEventWithOptions(threadID, runID string, options ...RunStartedOption) *RunStartedEvent {
+ event := &RunStartedEvent{
+ BaseEvent: NewBaseEvent(EventTypeRunStarted),
+ ThreadIDValue: threadID,
+ RunIDValue: runID,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// RunStartedOption defines options for creating run started events
+type RunStartedOption func(*RunStartedEvent)
+
+// WithAutoRunID automatically generates a unique run ID if the provided runID is empty
+func WithAutoRunID() RunStartedOption {
+ return func(e *RunStartedEvent) {
+ if e.RunIDValue == "" {
+ e.RunIDValue = GenerateRunID()
+ }
+ }
+}
+
+// WithAutoThreadID automatically generates a unique thread ID if the provided threadID is empty
+func WithAutoThreadID() RunStartedOption {
+ return func(e *RunStartedEvent) {
+ if e.ThreadIDValue == "" {
+ e.ThreadIDValue = GenerateThreadID()
+ }
+ }
+}
+
+// Validate validates the run started event
+func (e *RunStartedEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.ThreadIDValue == "" {
+ return fmt.Errorf("RunStartedEvent validation failed: threadId field is required")
+ }
+
+ if e.RunIDValue == "" {
+ return fmt.Errorf("RunStartedEvent validation failed: runId field is required")
+ }
+
+ return nil
+}
+
+// ThreadID returns the thread ID
+func (e *RunStartedEvent) ThreadID() string {
+ return e.ThreadIDValue
+}
+
+// RunID returns the run ID
+func (e *RunStartedEvent) RunID() string {
+ return e.RunIDValue
+}
+
+// ToJSON serializes the event to JSON
+func (e *RunStartedEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// RunFinishedEvent indicates that an agent run has finished successfully
+type RunFinishedEvent struct {
+ *BaseEvent
+ ThreadIDValue string `json:"threadId"`
+ RunIDValue string `json:"runId"`
+ Result interface{} `json:"result,omitempty"`
+}
+
+// NewRunFinishedEvent creates a new run finished event
+func NewRunFinishedEvent(threadID, runID string) *RunFinishedEvent {
+ return &RunFinishedEvent{
+ BaseEvent: NewBaseEvent(EventTypeRunFinished),
+ ThreadIDValue: threadID,
+ RunIDValue: runID,
+ }
+}
+
+// NewRunFinishedEventWithOptions creates a new run finished event with options
+func NewRunFinishedEventWithOptions(threadID, runID string, options ...RunFinishedOption) *RunFinishedEvent {
+ event := &RunFinishedEvent{
+ BaseEvent: NewBaseEvent(EventTypeRunFinished),
+ ThreadIDValue: threadID,
+ RunIDValue: runID,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// RunFinishedOption defines options for creating run finished events
+type RunFinishedOption func(*RunFinishedEvent)
+
+// WithAutoRunIDFinished automatically generates a unique run ID if the provided runID is empty
+func WithAutoRunIDFinished() RunFinishedOption {
+ return func(e *RunFinishedEvent) {
+ if e.RunIDValue == "" {
+ e.RunIDValue = GenerateRunID()
+ }
+ }
+}
+
+// WithAutoThreadIDFinished automatically generates a unique thread ID if the provided threadID is empty
+func WithAutoThreadIDFinished() RunFinishedOption {
+ return func(e *RunFinishedEvent) {
+ if e.ThreadIDValue == "" {
+ e.ThreadIDValue = GenerateThreadID()
+ }
+ }
+}
+
+// WithResult sets the result for the run finished event
+func WithResult(result interface{}) RunFinishedOption {
+ return func(e *RunFinishedEvent) {
+ e.Result = result
+ }
+}
+
+// Validate validates the run finished event
+func (e *RunFinishedEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.ThreadIDValue == "" {
+ return fmt.Errorf("RunFinishedEvent validation failed: threadId field is required")
+ }
+
+ if e.RunIDValue == "" {
+ return fmt.Errorf("RunFinishedEvent validation failed: runId field is required")
+ }
+
+ return nil
+}
+
+// ThreadID returns the thread ID
+func (e *RunFinishedEvent) ThreadID() string {
+ return e.ThreadIDValue
+}
+
+// RunID returns the run ID
+func (e *RunFinishedEvent) RunID() string {
+ return e.RunIDValue
+}
+
+// ToJSON serializes the event to JSON
+func (e *RunFinishedEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// RunErrorEvent indicates that an agent run has encountered an error
+type RunErrorEvent struct {
+ *BaseEvent
+ Code *string `json:"code,omitempty"`
+ Message string `json:"message"`
+ RunIDValue string `json:"runId,omitempty"`
+}
+
+// NewRunErrorEvent creates a new run error event
+func NewRunErrorEvent(message string, options ...RunErrorOption) *RunErrorEvent {
+ event := &RunErrorEvent{
+ BaseEvent: NewBaseEvent(EventTypeRunError),
+ Message: message,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// RunErrorOption defines options for creating run error events
+type RunErrorOption func(*RunErrorEvent)
+
+// WithErrorCode sets the error code
+func WithErrorCode(code string) RunErrorOption {
+ return func(e *RunErrorEvent) {
+ e.Code = &code
+ }
+}
+
+// WithRunID sets the run ID for the error
+func WithRunID(runID string) RunErrorOption {
+ return func(e *RunErrorEvent) {
+ e.RunIDValue = runID
+ }
+}
+
+// WithAutoRunIDError automatically generates a unique run ID if the provided runID is empty
+func WithAutoRunIDError() RunErrorOption {
+ return func(e *RunErrorEvent) {
+ if e.RunIDValue == "" {
+ e.RunIDValue = GenerateRunID()
+ }
+ }
+}
+
+// Validate validates the run error event
+func (e *RunErrorEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.Message == "" {
+ return fmt.Errorf("RunErrorEvent validation failed: message field is required")
+ }
+
+ return nil
+}
+
+// RunID returns the run ID
+func (e *RunErrorEvent) RunID() string {
+ return e.RunIDValue
+}
+
+// ToJSON serializes the event to JSON
+func (e *RunErrorEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// StepStartedEvent indicates that an agent step has started
+type StepStartedEvent struct {
+ *BaseEvent
+ StepName string `json:"stepName"`
+}
+
+// NewStepStartedEvent creates a new step started event
+func NewStepStartedEvent(stepName string) *StepStartedEvent {
+ return &StepStartedEvent{
+ BaseEvent: NewBaseEvent(EventTypeStepStarted),
+ StepName: stepName,
+ }
+}
+
+// NewStepStartedEventWithOptions creates a new step started event with options
+func NewStepStartedEventWithOptions(stepName string, options ...StepStartedOption) *StepStartedEvent {
+ event := &StepStartedEvent{
+ BaseEvent: NewBaseEvent(EventTypeStepStarted),
+ StepName: stepName,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// StepStartedOption defines options for creating step started events
+type StepStartedOption func(*StepStartedEvent)
+
+// WithAutoStepName automatically generates a unique step name if the provided stepName is empty
+func WithAutoStepName() StepStartedOption {
+ return func(e *StepStartedEvent) {
+ if e.StepName == "" {
+ e.StepName = GenerateStepID()
+ }
+ }
+}
+
+// Validate validates the step started event
+func (e *StepStartedEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.StepName == "" {
+ return fmt.Errorf("StepStartedEvent validation failed: stepName field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *StepStartedEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// StepFinishedEvent indicates that an agent step has finished
+type StepFinishedEvent struct {
+ *BaseEvent
+ StepName string `json:"stepName"`
+}
+
+// NewStepFinishedEvent creates a new step finished event
+func NewStepFinishedEvent(stepName string) *StepFinishedEvent {
+ return &StepFinishedEvent{
+ BaseEvent: NewBaseEvent(EventTypeStepFinished),
+ StepName: stepName,
+ }
+}
+
+// NewStepFinishedEventWithOptions creates a new step finished event with options
+func NewStepFinishedEventWithOptions(stepName string, options ...StepFinishedOption) *StepFinishedEvent {
+ event := &StepFinishedEvent{
+ BaseEvent: NewBaseEvent(EventTypeStepFinished),
+ StepName: stepName,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// StepFinishedOption defines options for creating step finished events
+type StepFinishedOption func(*StepFinishedEvent)
+
+// WithAutoStepNameFinished automatically generates a unique step name if the provided stepName is empty
+func WithAutoStepNameFinished() StepFinishedOption {
+ return func(e *StepFinishedEvent) {
+ if e.StepName == "" {
+ e.StepName = GenerateStepID()
+ }
+ }
+}
+
+// Validate validates the step finished event
+func (e *StepFinishedEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.StepName == "" {
+ return fmt.Errorf("StepFinishedEvent validation failed: stepName field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *StepFinishedEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
diff --git a/sdks/community/go/pkg/core/events/state_events.go b/sdks/community/go/pkg/core/events/state_events.go
new file mode 100644
index 000000000..1f317794b
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/state_events.go
@@ -0,0 +1,215 @@
+package events
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// validJSONPatchOps contains the valid JSON Patch operations for efficient lookup
+var validJSONPatchOps = map[string]bool{
+ "add": true,
+ "remove": true,
+ "replace": true,
+ "move": true,
+ "copy": true,
+ "test": true,
+}
+
+// StateSnapshotEvent contains a complete snapshot of the state
+type StateSnapshotEvent struct {
+ *BaseEvent
+ Snapshot any `json:"snapshot"`
+}
+
+// NewStateSnapshotEvent creates a new state snapshot event
+func NewStateSnapshotEvent(snapshot any) *StateSnapshotEvent {
+ return &StateSnapshotEvent{
+ BaseEvent: NewBaseEvent(EventTypeStateSnapshot),
+ Snapshot: snapshot,
+ }
+}
+
+// Validate validates the state snapshot event
+func (e *StateSnapshotEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.Snapshot == nil {
+ return fmt.Errorf("StateSnapshotEvent validation failed: snapshot field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *StateSnapshotEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// JSONPatchOperation represents a JSON Patch operation (RFC 6902)
+type JSONPatchOperation struct {
+ Op string `json:"op"` // "add", "remove", "replace", "move", "copy", "test"
+ Path string `json:"path"` // JSON Pointer path
+ Value any `json:"value,omitempty"` // Value for add, replace, test operations
+ From string `json:"from,omitempty"` // Source path for move, copy operations
+}
+
+// StateDeltaEvent contains incremental state changes using JSON Patch
+type StateDeltaEvent struct {
+ *BaseEvent
+ Delta []JSONPatchOperation `json:"delta"`
+}
+
+// NewStateDeltaEvent creates a new state delta event
+func NewStateDeltaEvent(delta []JSONPatchOperation) *StateDeltaEvent {
+ return &StateDeltaEvent{
+ BaseEvent: NewBaseEvent(EventTypeStateDelta),
+ Delta: delta,
+ }
+}
+
+// Validate validates the state delta event
+func (e *StateDeltaEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if len(e.Delta) == 0 {
+ return fmt.Errorf("StateDeltaEvent validation failed: delta field must contain at least one operation")
+ }
+
+ // Validate each JSON patch operation
+ for i, op := range e.Delta {
+ if err := validateJSONPatchOperation(op); err != nil {
+ return fmt.Errorf("StateDeltaEvent validation failed: invalid operation at index %d: %w", i, err)
+ }
+ }
+
+ return nil
+}
+
+// validateJSONPatchOperation validates a single JSON patch operation
+func validateJSONPatchOperation(op JSONPatchOperation) error {
+ // Validate operation type using map lookup for better performance
+ if !validJSONPatchOps[op.Op] {
+ return fmt.Errorf("op field must be one of: add, remove, replace, move, copy, test, got: %s", op.Op)
+ }
+
+ // Validate path
+ if op.Path == "" {
+ return fmt.Errorf("path field is required")
+ }
+
+ // Validate value for operations that require it
+ if (op.Op == "add" || op.Op == "replace" || op.Op == "test") && op.Value == nil {
+ return fmt.Errorf("value field is required for %s operation", op.Op)
+ }
+
+ // Validate from for operations that require it
+ if (op.Op == "move" || op.Op == "copy") && op.From == "" {
+ return fmt.Errorf("from field is required for %s operation", op.Op)
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *StateDeltaEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// Message represents a message in the conversation
+type Message struct {
+ ID string `json:"id"`
+ Role string `json:"role"`
+ Content *string `json:"content,omitempty"`
+ Name *string `json:"name,omitempty"`
+ ToolCalls []ToolCall `json:"toolCalls,omitempty"`
+ ToolCallID *string `json:"toolCallId,omitempty"`
+}
+
+// ToolCall represents a tool call within a message
+type ToolCall struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ Function Function `json:"function"`
+}
+
+// Function represents a function call
+type Function struct {
+ Name string `json:"name"`
+ Arguments string `json:"arguments"`
+}
+
+// MessagesSnapshotEvent contains a snapshot of all messages
+type MessagesSnapshotEvent struct {
+ *BaseEvent
+ Messages []Message `json:"messages"`
+}
+
+// NewMessagesSnapshotEvent creates a new messages snapshot event
+func NewMessagesSnapshotEvent(messages []Message) *MessagesSnapshotEvent {
+ return &MessagesSnapshotEvent{
+ BaseEvent: NewBaseEvent(EventTypeMessagesSnapshot),
+ Messages: messages,
+ }
+}
+
+// Validate validates the messages snapshot event
+func (e *MessagesSnapshotEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ // Validate each message
+ for i, msg := range e.Messages {
+ if err := validateMessage(msg); err != nil {
+ return fmt.Errorf("invalid message at index %d: %w", i, err)
+ }
+ }
+
+ return nil
+}
+
+// validateMessage validates a single message
+func validateMessage(msg Message) error {
+ if msg.ID == "" {
+ return fmt.Errorf("message id field is required")
+ }
+
+ if msg.Role == "" {
+ return fmt.Errorf("message role field is required")
+ }
+
+ // Validate tool calls if present
+ for i, toolCall := range msg.ToolCalls {
+ if err := validateToolCall(toolCall); err != nil {
+ return fmt.Errorf("invalid tool call at index %d: %w", i, err)
+ }
+ }
+
+ return nil
+}
+
+// validateToolCall validates a single tool call
+func validateToolCall(toolCall ToolCall) error {
+ if toolCall.ID == "" {
+ return fmt.Errorf("tool call id field is required")
+ }
+
+ if toolCall.Type == "" {
+ return fmt.Errorf("tool call type field is required")
+ }
+
+ if toolCall.Function.Name == "" {
+ return fmt.Errorf("function name field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *MessagesSnapshotEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
diff --git a/sdks/community/go/pkg/core/events/thinking_events.go b/sdks/community/go/pkg/core/events/thinking_events.go
new file mode 100644
index 000000000..00c43e262
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/thinking_events.go
@@ -0,0 +1,145 @@
+package events
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// ThinkingStartEvent indicates the start of a thinking/reasoning phase
+type ThinkingStartEvent struct {
+ *BaseEvent
+ Title *string `json:"title,omitempty"`
+}
+
+// NewThinkingStartEvent creates a new thinking start event
+func NewThinkingStartEvent() *ThinkingStartEvent {
+ return &ThinkingStartEvent{
+ BaseEvent: NewBaseEvent(EventTypeThinkingStart),
+ }
+}
+
+// WithTitle sets the title for the thinking phase
+func (e *ThinkingStartEvent) WithTitle(title string) *ThinkingStartEvent {
+ e.Title = &title
+ return e
+}
+
+// Validate validates the thinking start event
+func (e *ThinkingStartEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ThinkingStartEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// ThinkingEndEvent indicates the end of a thinking/reasoning phase
+type ThinkingEndEvent struct {
+ *BaseEvent
+}
+
+// NewThinkingEndEvent creates a new thinking end event
+func NewThinkingEndEvent() *ThinkingEndEvent {
+ return &ThinkingEndEvent{
+ BaseEvent: NewBaseEvent(EventTypeThinkingEnd),
+ }
+}
+
+// Validate validates the thinking end event
+func (e *ThinkingEndEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ThinkingEndEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// ThinkingTextMessageStartEvent indicates the start of a thinking text message
+type ThinkingTextMessageStartEvent struct {
+ *BaseEvent
+}
+
+// NewThinkingTextMessageStartEvent creates a new thinking text message start event
+func NewThinkingTextMessageStartEvent() *ThinkingTextMessageStartEvent {
+ return &ThinkingTextMessageStartEvent{
+ BaseEvent: NewBaseEvent(EventTypeThinkingTextMessageStart),
+ }
+}
+
+// Validate validates the thinking text message start event
+func (e *ThinkingTextMessageStartEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ThinkingTextMessageStartEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// ThinkingTextMessageContentEvent contains streaming thinking text content
+type ThinkingTextMessageContentEvent struct {
+ *BaseEvent
+ Delta string `json:"delta"`
+}
+
+// NewThinkingTextMessageContentEvent creates a new thinking text message content event
+func NewThinkingTextMessageContentEvent(delta string) *ThinkingTextMessageContentEvent {
+ return &ThinkingTextMessageContentEvent{
+ BaseEvent: NewBaseEvent(EventTypeThinkingTextMessageContent),
+ Delta: delta,
+ }
+}
+
+// Validate validates the thinking text message content event
+func (e *ThinkingTextMessageContentEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.Delta == "" {
+ return fmt.Errorf("ThinkingTextMessageContentEvent validation failed: delta field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ThinkingTextMessageContentEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// ThinkingTextMessageEndEvent indicates the end of a thinking text message
+type ThinkingTextMessageEndEvent struct {
+ *BaseEvent
+}
+
+// NewThinkingTextMessageEndEvent creates a new thinking text message end event
+func NewThinkingTextMessageEndEvent() *ThinkingTextMessageEndEvent {
+ return &ThinkingTextMessageEndEvent{
+ BaseEvent: NewBaseEvent(EventTypeThinkingTextMessageEnd),
+ }
+}
+
+// Validate validates the thinking text message end event
+func (e *ThinkingTextMessageEndEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ThinkingTextMessageEndEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
diff --git a/sdks/community/go/pkg/core/events/thinking_events_test.go b/sdks/community/go/pkg/core/events/thinking_events_test.go
new file mode 100644
index 000000000..5a6ff53ba
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/thinking_events_test.go
@@ -0,0 +1,256 @@
+package events
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestThinkingStartEvent(t *testing.T) {
+ t.Run("basic creation", func(t *testing.T) {
+ event := NewThinkingStartEvent()
+
+ if event.Type() != EventTypeThinkingStart {
+ t.Errorf("expected event type %s, got %s", EventTypeThinkingStart, event.Type())
+ }
+
+ if err := event.Validate(); err != nil {
+ t.Errorf("validation failed: %v", err)
+ }
+ })
+
+ t.Run("with title", func(t *testing.T) {
+ title := "Analyzing request"
+ event := NewThinkingStartEvent().WithTitle(title)
+
+ if event.Title == nil || *event.Title != title {
+ t.Errorf("expected title %s, got %v", title, event.Title)
+ }
+
+ if err := event.Validate(); err != nil {
+ t.Errorf("validation failed: %v", err)
+ }
+ })
+
+ t.Run("JSON serialization", func(t *testing.T) {
+ title := "Processing"
+ event := NewThinkingStartEvent().WithTitle(title)
+
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ t.Fatalf("failed to serialize to JSON: %v", err)
+ }
+
+ var decoded map[string]interface{}
+ if err := json.Unmarshal(jsonData, &decoded); err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if decoded["type"] != string(EventTypeThinkingStart) {
+ t.Errorf("expected type %s in JSON, got %v", EventTypeThinkingStart, decoded["type"])
+ }
+
+ if decoded["title"] != title {
+ t.Errorf("expected title %s in JSON, got %v", title, decoded["title"])
+ }
+ })
+
+ t.Run("JSON field naming", func(t *testing.T) {
+ event := NewThinkingStartEvent().WithTitle("Test")
+
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ t.Fatalf("failed to serialize to JSON: %v", err)
+ }
+
+ jsonStr := string(jsonData)
+
+ // Check for camelCase field names
+ if !contains(jsonStr, `"type"`) {
+ t.Error("JSON should contain 'type' field")
+ }
+
+ if !contains(jsonStr, `"title"`) {
+ t.Error("JSON should contain 'title' field")
+ }
+
+ // Should not contain snake_case
+ if contains(jsonStr, `"event_type"`) {
+ t.Error("JSON should not contain snake_case field names")
+ }
+ })
+}
+
+func TestThinkingEndEvent(t *testing.T) {
+ t.Run("basic creation", func(t *testing.T) {
+ event := NewThinkingEndEvent()
+
+ if event.Type() != EventTypeThinkingEnd {
+ t.Errorf("expected event type %s, got %s", EventTypeThinkingEnd, event.Type())
+ }
+
+ if err := event.Validate(); err != nil {
+ t.Errorf("validation failed: %v", err)
+ }
+ })
+
+ t.Run("JSON serialization", func(t *testing.T) {
+ event := NewThinkingEndEvent()
+
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ t.Fatalf("failed to serialize to JSON: %v", err)
+ }
+
+ var decoded map[string]interface{}
+ if err := json.Unmarshal(jsonData, &decoded); err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if decoded["type"] != string(EventTypeThinkingEnd) {
+ t.Errorf("expected type %s in JSON, got %v", EventTypeThinkingEnd, decoded["type"])
+ }
+ })
+}
+
+func TestThinkingTextMessageStartEvent(t *testing.T) {
+ t.Run("basic creation", func(t *testing.T) {
+ event := NewThinkingTextMessageStartEvent()
+
+ if event.Type() != EventTypeThinkingTextMessageStart {
+ t.Errorf("expected event type %s, got %s", EventTypeThinkingTextMessageStart, event.Type())
+ }
+
+ if err := event.Validate(); err != nil {
+ t.Errorf("validation failed: %v", err)
+ }
+ })
+
+ t.Run("JSON serialization", func(t *testing.T) {
+ event := NewThinkingTextMessageStartEvent()
+
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ t.Fatalf("failed to serialize to JSON: %v", err)
+ }
+
+ var decoded map[string]interface{}
+ if err := json.Unmarshal(jsonData, &decoded); err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if decoded["type"] != string(EventTypeThinkingTextMessageStart) {
+ t.Errorf("expected type %s in JSON, got %v", EventTypeThinkingTextMessageStart, decoded["type"])
+ }
+ })
+}
+
+func TestThinkingTextMessageContentEvent(t *testing.T) {
+ t.Run("basic creation", func(t *testing.T) {
+ delta := "Thinking about the problem..."
+ event := NewThinkingTextMessageContentEvent(delta)
+
+ if event.Type() != EventTypeThinkingTextMessageContent {
+ t.Errorf("expected event type %s, got %s", EventTypeThinkingTextMessageContent, event.Type())
+ }
+
+ if event.Delta != delta {
+ t.Errorf("expected delta %s, got %s", delta, event.Delta)
+ }
+
+ if err := event.Validate(); err != nil {
+ t.Errorf("validation failed: %v", err)
+ }
+ })
+
+ t.Run("validation requires delta", func(t *testing.T) {
+ event := &ThinkingTextMessageContentEvent{
+ BaseEvent: NewBaseEvent(EventTypeThinkingTextMessageContent),
+ Delta: "",
+ }
+
+ if err := event.Validate(); err == nil {
+ t.Error("expected validation to fail for empty delta")
+ }
+ })
+
+ t.Run("JSON serialization", func(t *testing.T) {
+ delta := "Processing request..."
+ event := NewThinkingTextMessageContentEvent(delta)
+
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ t.Fatalf("failed to serialize to JSON: %v", err)
+ }
+
+ var decoded map[string]interface{}
+ if err := json.Unmarshal(jsonData, &decoded); err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if decoded["type"] != string(EventTypeThinkingTextMessageContent) {
+ t.Errorf("expected type %s in JSON, got %v", EventTypeThinkingTextMessageContent, decoded["type"])
+ }
+
+ if decoded["delta"] != delta {
+ t.Errorf("expected delta %s in JSON, got %v", delta, decoded["delta"])
+ }
+ })
+
+ t.Run("JSON field naming", func(t *testing.T) {
+ event := NewThinkingTextMessageContentEvent("test")
+
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ t.Fatalf("failed to serialize to JSON: %v", err)
+ }
+
+ jsonStr := string(jsonData)
+
+ // Check for correct field names
+ if !contains(jsonStr, `"delta"`) {
+ t.Error("JSON should contain 'delta' field")
+ }
+ })
+}
+
+func TestThinkingTextMessageEndEvent(t *testing.T) {
+ t.Run("basic creation", func(t *testing.T) {
+ event := NewThinkingTextMessageEndEvent()
+
+ if event.Type() != EventTypeThinkingTextMessageEnd {
+ t.Errorf("expected event type %s, got %s", EventTypeThinkingTextMessageEnd, event.Type())
+ }
+
+ if err := event.Validate(); err != nil {
+ t.Errorf("validation failed: %v", err)
+ }
+ })
+
+ t.Run("JSON serialization", func(t *testing.T) {
+ event := NewThinkingTextMessageEndEvent()
+
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ t.Fatalf("failed to serialize to JSON: %v", err)
+ }
+
+ var decoded map[string]interface{}
+ if err := json.Unmarshal(jsonData, &decoded); err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if decoded["type"] != string(EventTypeThinkingTextMessageEnd) {
+ t.Errorf("expected type %s in JSON, got %v", EventTypeThinkingTextMessageEnd, decoded["type"])
+ }
+ })
+}
+
+// Helper function to check if a string contains a substring
+func contains(s, substr string) bool {
+ return len(s) > 0 && len(substr) > 0 &&
+ (len(s) >= len(substr)) &&
+ (s == substr ||
+ (len(s) > len(substr) &&
+ (s[:len(substr)] == substr ||
+ contains(s[1:], substr))))
+}
diff --git a/sdks/community/go/pkg/core/events/tool_events.go b/sdks/community/go/pkg/core/events/tool_events.go
new file mode 100644
index 000000000..1f6ce7702
--- /dev/null
+++ b/sdks/community/go/pkg/core/events/tool_events.go
@@ -0,0 +1,299 @@
+package events
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// ToolCallStartEvent indicates the start of a tool call
+type ToolCallStartEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+ ToolCallName string `json:"toolCallName"`
+ ParentMessageID *string `json:"parentMessageId,omitempty"`
+}
+
+// NewToolCallStartEvent creates a new tool call start event
+func NewToolCallStartEvent(toolCallID, toolCallName string, options ...ToolCallStartOption) *ToolCallStartEvent {
+ event := &ToolCallStartEvent{
+ BaseEvent: NewBaseEvent(EventTypeToolCallStart),
+ ToolCallID: toolCallID,
+ ToolCallName: toolCallName,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// ToolCallStartOption defines options for creating tool call start events
+type ToolCallStartOption func(*ToolCallStartEvent)
+
+// WithParentMessageID sets the parent message ID for the tool call
+func WithParentMessageID(parentMessageID string) ToolCallStartOption {
+ return func(e *ToolCallStartEvent) {
+ e.ParentMessageID = &parentMessageID
+ }
+}
+
+// WithAutoToolCallID automatically generates a unique tool call ID if the provided toolCallID is empty
+func WithAutoToolCallID() ToolCallStartOption {
+ return func(e *ToolCallStartEvent) {
+ if e.ToolCallID == "" {
+ e.ToolCallID = GenerateToolCallID()
+ }
+ }
+}
+
+// Validate validates the tool call start event
+func (e *ToolCallStartEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.ToolCallID == "" {
+ return fmt.Errorf("ToolCallStartEvent validation failed: toolCallId field is required")
+ }
+
+ if e.ToolCallName == "" {
+ return fmt.Errorf("ToolCallStartEvent validation failed: toolCallName field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ToolCallStartEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// ToolCallArgsEvent contains streaming tool call arguments
+type ToolCallArgsEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+ Delta string `json:"delta"`
+}
+
+// NewToolCallArgsEvent creates a new tool call args event
+func NewToolCallArgsEvent(toolCallID, delta string) *ToolCallArgsEvent {
+ return &ToolCallArgsEvent{
+ BaseEvent: NewBaseEvent(EventTypeToolCallArgs),
+ ToolCallID: toolCallID,
+ Delta: delta,
+ }
+}
+
+// NewToolCallArgsEventWithOptions creates a new tool call args event with options
+func NewToolCallArgsEventWithOptions(toolCallID, delta string, options ...ToolCallArgsOption) *ToolCallArgsEvent {
+ event := &ToolCallArgsEvent{
+ BaseEvent: NewBaseEvent(EventTypeToolCallArgs),
+ ToolCallID: toolCallID,
+ Delta: delta,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// ToolCallArgsOption defines options for creating tool call args events
+type ToolCallArgsOption func(*ToolCallArgsEvent)
+
+// WithAutoToolCallIDArgs automatically generates a unique tool call ID if the provided toolCallID is empty
+func WithAutoToolCallIDArgs() ToolCallArgsOption {
+ return func(e *ToolCallArgsEvent) {
+ if e.ToolCallID == "" {
+ e.ToolCallID = GenerateToolCallID()
+ }
+ }
+}
+
+// Validate validates the tool call args event
+func (e *ToolCallArgsEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.ToolCallID == "" {
+ return fmt.Errorf("ToolCallArgsEvent validation failed: toolCallId field is required")
+ }
+
+ if e.Delta == "" {
+ return fmt.Errorf("ToolCallArgsEvent validation failed: delta field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ToolCallArgsEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// ToolCallEndEvent indicates the end of a tool call
+type ToolCallEndEvent struct {
+ *BaseEvent
+ ToolCallID string `json:"toolCallId"`
+}
+
+// NewToolCallEndEvent creates a new tool call end event
+func NewToolCallEndEvent(toolCallID string) *ToolCallEndEvent {
+ return &ToolCallEndEvent{
+ BaseEvent: NewBaseEvent(EventTypeToolCallEnd),
+ ToolCallID: toolCallID,
+ }
+}
+
+// NewToolCallEndEventWithOptions creates a new tool call end event with options
+func NewToolCallEndEventWithOptions(toolCallID string, options ...ToolCallEndOption) *ToolCallEndEvent {
+ event := &ToolCallEndEvent{
+ BaseEvent: NewBaseEvent(EventTypeToolCallEnd),
+ ToolCallID: toolCallID,
+ }
+
+ for _, opt := range options {
+ opt(event)
+ }
+
+ return event
+}
+
+// ToolCallEndOption defines options for creating tool call end events
+type ToolCallEndOption func(*ToolCallEndEvent)
+
+// WithAutoToolCallIDEnd automatically generates a unique tool call ID if the provided toolCallID is empty
+func WithAutoToolCallIDEnd() ToolCallEndOption {
+ return func(e *ToolCallEndEvent) {
+ if e.ToolCallID == "" {
+ e.ToolCallID = GenerateToolCallID()
+ }
+ }
+}
+
+// Validate validates the tool call end event
+func (e *ToolCallEndEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.ToolCallID == "" {
+ return fmt.Errorf("ToolCallEndEvent validation failed: toolCallId field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ToolCallEndEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// ToolCallResultEvent represents the result of a tool call execution
+type ToolCallResultEvent struct {
+ *BaseEvent
+ MessageID string `json:"messageId"`
+ ToolCallID string `json:"toolCallId"`
+ Content string `json:"content"`
+ Role *string `json:"role,omitempty"`
+}
+
+// NewToolCallResultEvent creates a new tool call result event
+func NewToolCallResultEvent(messageID, toolCallID, content string) *ToolCallResultEvent {
+ role := "tool"
+ return &ToolCallResultEvent{
+ BaseEvent: NewBaseEvent(EventTypeToolCallResult),
+ MessageID: messageID,
+ ToolCallID: toolCallID,
+ Content: content,
+ Role: &role,
+ }
+}
+
+// Validate validates the tool call result event
+func (e *ToolCallResultEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ if e.MessageID == "" {
+ return fmt.Errorf("ToolCallResultEvent validation failed: messageId field is required")
+ }
+
+ if e.ToolCallID == "" {
+ return fmt.Errorf("ToolCallResultEvent validation failed: toolCallId field is required")
+ }
+
+ if e.Content == "" {
+ return fmt.Errorf("ToolCallResultEvent validation failed: content field is required")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ToolCallResultEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
+
+// ToolCallChunkEvent represents a chunk of tool call data
+type ToolCallChunkEvent struct {
+ *BaseEvent
+ ToolCallID *string `json:"toolCallId,omitempty"`
+ ToolCallName *string `json:"toolCallName,omitempty"`
+ ParentMessageID *string `json:"parentMessageId,omitempty"`
+ Delta *string `json:"delta,omitempty"`
+}
+
+// NewToolCallChunkEvent creates a new tool call chunk event
+func NewToolCallChunkEvent() *ToolCallChunkEvent {
+ return &ToolCallChunkEvent{
+ BaseEvent: NewBaseEvent(EventTypeToolCallChunk),
+ }
+}
+
+// WithToolCallChunkID sets the tool call ID for the chunk
+func (e *ToolCallChunkEvent) WithToolCallChunkID(id string) *ToolCallChunkEvent {
+ e.ToolCallID = &id
+ return e
+}
+
+// WithToolCallChunkName sets the tool call name for the chunk
+func (e *ToolCallChunkEvent) WithToolCallChunkName(name string) *ToolCallChunkEvent {
+ e.ToolCallName = &name
+ return e
+}
+
+// WithToolCallChunkDelta sets the delta content for the chunk
+func (e *ToolCallChunkEvent) WithToolCallChunkDelta(delta string) *ToolCallChunkEvent {
+ e.Delta = &delta
+ return e
+}
+
+// WithToolCallChunkParentMessageID sets the parent message ID for the chunk
+func (e *ToolCallChunkEvent) WithToolCallChunkParentMessageID(parentMessageID string) *ToolCallChunkEvent {
+ e.ParentMessageID = &parentMessageID
+ return e
+}
+
+// Validate validates the tool call chunk event
+func (e *ToolCallChunkEvent) Validate() error {
+ if err := e.BaseEvent.Validate(); err != nil {
+ return err
+ }
+
+ // At least one field should be present
+ if e.ToolCallID == nil && e.ToolCallName == nil && e.Delta == nil {
+ return fmt.Errorf("ToolCallChunkEvent validation failed: at least one of toolCallId, toolCallName, or delta must be present")
+ }
+
+ return nil
+}
+
+// ToJSON serializes the event to JSON
+func (e *ToolCallChunkEvent) ToJSON() ([]byte, error) {
+ return json.Marshal(e)
+}
diff --git a/sdks/community/go/pkg/encoding/buffer_sizing.go b/sdks/community/go/pkg/encoding/buffer_sizing.go
new file mode 100644
index 000000000..064118f67
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/buffer_sizing.go
@@ -0,0 +1,136 @@
+package encoding
+
+import (
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+)
+
+// Buffer size constants optimized for different event types
+const (
+ // Small events (typically 100-500 bytes)
+ SmallEventBufferSize = 512
+
+ // Medium events (typically 500-2KB)
+ MediumEventBufferSize = 2048
+
+ // Large events (typically 2KB-8KB)
+ LargeEventBufferSize = 8192
+
+ // Very large events (8KB+)
+ VeryLargeEventBufferSize = 16384
+
+ // Default buffer size for unknown events
+ DefaultEventBufferSize = 1024
+
+ // Array processing buffer size per event
+ ArrayProcessingBufferSize = 1024
+)
+
+// GetOptimalBufferSize returns the optimal buffer size for a given event type
+func GetOptimalBufferSize(eventType events.EventType) int {
+ switch eventType {
+ case events.EventTypeTextMessageStart:
+ return SmallEventBufferSize // Simple metadata
+ case events.EventTypeTextMessageContent:
+ return MediumEventBufferSize // Text content can vary
+ case events.EventTypeTextMessageEnd:
+ return SmallEventBufferSize // Simple metadata
+ case events.EventTypeToolCallStart:
+ return SmallEventBufferSize // Tool metadata
+ case events.EventTypeToolCallArgs:
+ return LargeEventBufferSize // Tool arguments can be large
+ case events.EventTypeToolCallEnd:
+ return SmallEventBufferSize // Simple metadata
+ case events.EventTypeStateSnapshot:
+ return VeryLargeEventBufferSize // State snapshots can be very large
+ case events.EventTypeStateDelta:
+ return MediumEventBufferSize // Delta operations are usually medium
+ case events.EventTypeMessagesSnapshot:
+ return VeryLargeEventBufferSize // Message snapshots can be very large
+ case events.EventTypeRaw:
+ return LargeEventBufferSize // Raw events are unpredictable
+ case events.EventTypeCustom:
+ return MediumEventBufferSize // Custom events are usually medium
+ case events.EventTypeRunStarted:
+ return SmallEventBufferSize // Simple metadata
+ case events.EventTypeRunFinished:
+ return SmallEventBufferSize // Simple metadata
+ case events.EventTypeRunError:
+ return MediumEventBufferSize // Error details can be medium
+ case events.EventTypeStepStarted:
+ return SmallEventBufferSize // Simple metadata
+ case events.EventTypeStepFinished:
+ return SmallEventBufferSize // Simple metadata
+ default:
+ return DefaultEventBufferSize
+ }
+}
+
+// GetOptimalBufferSizeForEvent returns the optimal buffer size for a specific event instance
+func GetOptimalBufferSizeForEvent(event events.Event) int {
+ if event == nil {
+ return DefaultEventBufferSize
+ }
+
+ baseSize := GetOptimalBufferSize(event.Type())
+
+ // For certain event types, we can make more precise estimates
+ switch e := event.(type) {
+ case *events.TextMessageContentEvent:
+ // Estimate based on delta length
+ if len(e.Delta) > 0 {
+ // Add some overhead for JSON encoding
+ return max(baseSize, len(e.Delta)*2)
+ }
+ case *events.ToolCallArgsEvent:
+ // Estimate based on delta length
+ if len(e.Delta) > 0 {
+ // Add some overhead for JSON encoding
+ return max(baseSize, len(e.Delta)*2)
+ }
+ case *events.StateSnapshotEvent:
+ // State snapshots can be very large, but we can't easily estimate
+ // without serializing first, so stick with the base size
+ return baseSize
+ case *events.StateDeltaEvent:
+ // Estimate based on number of operations
+ if len(e.Delta) > 0 {
+ // Rough estimate: 100 bytes per operation
+ return max(baseSize, len(e.Delta)*100)
+ }
+ case *events.MessagesSnapshotEvent:
+ // Estimate based on number of messages
+ if len(e.Messages) > 0 {
+ // Rough estimate: 500 bytes per message
+ return max(baseSize, len(e.Messages)*500)
+ }
+ case *events.CustomEvent:
+ // For custom events, we can't easily estimate without knowing the value
+ return baseSize
+ }
+
+ return baseSize
+}
+
+// GetOptimalBufferSizeForMultiple returns the optimal buffer size for encoding multiple events
+func GetOptimalBufferSizeForMultiple(events []events.Event) int {
+ if len(events) == 0 {
+ return DefaultEventBufferSize
+ }
+
+ totalSize := 0
+ for _, event := range events {
+ totalSize += GetOptimalBufferSizeForEvent(event)
+ }
+
+ // Add some overhead for array structure
+ arrayOverhead := 50 * len(events) // Rough estimate for JSON array overhead
+ return totalSize + arrayOverhead
+}
+
+// max returns the maximum of two integers
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/sdks/community/go/pkg/encoding/encoder/encoder.go b/sdks/community/go/pkg/encoding/encoder/encoder.go
new file mode 100644
index 000000000..ad6dab80c
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/encoder/encoder.go
@@ -0,0 +1,91 @@
+package encoder
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/json"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/negotiation"
+)
+
+// EventEncoder provides a high-level interface for encoding AG-UI events
+// This adapter bridges the Go SDK encoding package with example server needs
+type EventEncoder struct {
+ negotiator *negotiation.ContentNegotiator
+ jsonCodec encoding.Codec
+}
+
+// NewEventEncoder creates a new event encoder with content negotiation support
+func NewEventEncoder() *EventEncoder {
+ // Create content negotiator with JSON as preferred type
+ negotiator := negotiation.NewContentNegotiator("application/json")
+
+ return &EventEncoder{
+ negotiator: negotiator,
+ jsonCodec: json.NewCodec(),
+ }
+}
+
+// EncodeEvent encodes a single event using the specified content type
+func (e *EventEncoder) EncodeEvent(ctx context.Context, event events.Event, contentType string) ([]byte, error) {
+ if event == nil {
+ return nil, fmt.Errorf("event cannot be nil")
+ }
+
+ // Validate the event before encoding
+ if err := event.Validate(); err != nil {
+ return nil, fmt.Errorf("event validation failed: %w", err)
+ }
+
+ // For now, we only support JSON encoding as specified in the task
+ // Protobuf support can be added later
+ switch contentType {
+ case "application/json", "":
+ return e.jsonCodec.Encode(ctx, event)
+ default:
+ // Try to negotiate to a supported type
+ supportedType, err := e.negotiator.Negotiate(contentType)
+ if err != nil {
+ return nil, fmt.Errorf("unsupported content type %q: %w", contentType, err)
+ }
+
+ // For now, fallback to JSON
+ if supportedType == "application/json" {
+ return e.jsonCodec.Encode(ctx, event)
+ }
+
+ return nil, fmt.Errorf("content type %q not implemented yet", supportedType)
+ }
+}
+
+// NegotiateContentType performs content negotiation based on Accept header
+func (e *EventEncoder) NegotiateContentType(acceptHeader string) (string, error) {
+ if acceptHeader == "" {
+ return "application/json", nil // Default to JSON
+ }
+
+ contentType, err := e.negotiator.Negotiate(acceptHeader)
+ if err != nil {
+ // If negotiation fails, fallback to JSON with a clear message
+ return "application/json", fmt.Errorf("content negotiation failed, falling back to JSON: %w", err)
+ }
+
+ return contentType, nil
+}
+
+// SupportedContentTypes returns the list of supported content types
+func (e *EventEncoder) SupportedContentTypes() []string {
+ return e.negotiator.SupportedTypes()
+}
+
+// GetContentType returns the content type that this encoder will produce
+func (e *EventEncoder) GetContentType(acceptHeader string) string {
+ contentType, err := e.NegotiateContentType(acceptHeader)
+ if err != nil {
+ // Log the error but continue with fallback
+ return "application/json"
+ }
+ return contentType
+}
diff --git a/sdks/community/go/pkg/encoding/errors.go b/sdks/community/go/pkg/encoding/errors.go
new file mode 100644
index 000000000..0ac9677d7
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/errors.go
@@ -0,0 +1,363 @@
+package encoding
+
+import (
+ "fmt"
+ "runtime"
+)
+
+// ==============================================================================
+// STRUCTURED ERROR TYPES
+// ==============================================================================
+
+// OperationError represents errors that occur during encoding/decoding operations
+type OperationError struct {
+ Operation string // The operation that failed (e.g., "encode", "decode", "validate")
+ Component string // The component where the error occurred (e.g., "json", "protobuf")
+ Message string // Human-readable error message
+ Cause error // The underlying error that caused this error
+ Context map[string]interface{} // Additional context information
+ Stack []uintptr // Stack trace for debugging
+}
+
+func (e *OperationError) Error() string {
+ if e.Cause != nil {
+ return fmt.Sprintf("%s operation failed in %s: %s: %v", e.Operation, e.Component, e.Message, e.Cause)
+ }
+ return fmt.Sprintf("%s operation failed in %s: %s", e.Operation, e.Component, e.Message)
+}
+
+func (e *OperationError) Unwrap() error {
+ return e.Cause
+}
+
+// WithContext adds context information to the error
+func (e *OperationError) WithContext(key string, value interface{}) *OperationError {
+ if e.Context == nil {
+ e.Context = make(map[string]interface{})
+ }
+ e.Context[key] = value
+ return e
+}
+
+// ValidationError represents validation failures
+type ValidationError struct {
+ Field string // The field that failed validation
+ Value interface{} // The value that failed validation
+ Rule string // The validation rule that was violated
+ Message string // Human-readable error message
+ Component string // The component where validation failed
+ Context map[string]interface{} // Additional context information
+ Stack []uintptr // Stack trace for debugging
+}
+
+func (e *ValidationError) Error() string {
+ if e.Field != "" {
+ return fmt.Sprintf("validation failed in %s for field '%s': %s (rule: %s, value: %v)",
+ e.Component, e.Field, e.Message, e.Rule, e.Value)
+ }
+ return fmt.Sprintf("validation failed in %s: %s (rule: %s)", e.Component, e.Message, e.Rule)
+}
+
+// WithContext adds context information to the error
+func (e *ValidationError) WithContext(key string, value interface{}) *ValidationError {
+ if e.Context == nil {
+ e.Context = make(map[string]interface{})
+ }
+ e.Context[key] = value
+ return e
+}
+
+// ConfigurationError represents configuration-related errors
+type ConfigurationError struct {
+ Setting string // The configuration setting that is invalid
+ Value interface{} // The invalid value
+ Message string // Human-readable error message
+ Component string // The component where the configuration error occurred
+ Context map[string]interface{} // Additional context information
+ Stack []uintptr // Stack trace for debugging
+}
+
+func (e *ConfigurationError) Error() string {
+ if e.Setting != "" {
+ return fmt.Sprintf("configuration error in %s for setting '%s': %s (value: %v)",
+ e.Component, e.Setting, e.Message, e.Value)
+ }
+ return fmt.Sprintf("configuration error in %s: %s", e.Component, e.Message)
+}
+
+// WithContext adds context information to the error
+func (e *ConfigurationError) WithContext(key string, value interface{}) *ConfigurationError {
+ if e.Context == nil {
+ e.Context = make(map[string]interface{})
+ }
+ e.Context[key] = value
+ return e
+}
+
+// ResourceError represents resource-related errors (limits, exhaustion, etc.)
+type ResourceError struct {
+ Resource string // The resource that caused the error (e.g., "buffer", "memory", "connection")
+ Limit interface{} // The limit that was exceeded (if applicable)
+ Current interface{} // The current value that exceeded the limit
+ Message string // Human-readable error message
+ Component string // The component where the resource error occurred
+ Context map[string]interface{} // Additional context information
+ Stack []uintptr // Stack trace for debugging
+}
+
+func (e *ResourceError) Error() string {
+ if e.Limit != nil && e.Current != nil {
+ return fmt.Sprintf("resource error in %s for %s: %s (current: %v, limit: %v)",
+ e.Component, e.Resource, e.Message, e.Current, e.Limit)
+ }
+ return fmt.Sprintf("resource error in %s for %s: %s", e.Component, e.Resource, e.Message)
+}
+
+// WithContext adds context information to the error
+func (e *ResourceError) WithContext(key string, value interface{}) *ResourceError {
+ if e.Context == nil {
+ e.Context = make(map[string]interface{})
+ }
+ e.Context[key] = value
+ return e
+}
+
+// RegistryError represents registry-related errors
+type RegistryError struct {
+ Registry string // The registry that had the error
+ Key string // The key that was being accessed (if applicable)
+ Operation string // The operation that failed (e.g., "register", "lookup", "unregister")
+ Message string // Human-readable error message
+ Cause error // The underlying error that caused this error
+ Context map[string]interface{} // Additional context information
+ Stack []uintptr // Stack trace for debugging
+}
+
+func (e *RegistryError) Error() string {
+ if e.Key != "" {
+ if e.Cause != nil {
+ return fmt.Sprintf("registry error in %s during %s for key '%s': %s: %v",
+ e.Registry, e.Operation, e.Key, e.Message, e.Cause)
+ }
+ return fmt.Sprintf("registry error in %s during %s for key '%s': %s",
+ e.Registry, e.Operation, e.Key, e.Message)
+ }
+ if e.Cause != nil {
+ return fmt.Sprintf("registry error in %s during %s: %s: %v",
+ e.Registry, e.Operation, e.Message, e.Cause)
+ }
+ return fmt.Sprintf("registry error in %s during %s: %s", e.Registry, e.Operation, e.Message)
+}
+
+func (e *RegistryError) Unwrap() error {
+ return e.Cause
+}
+
+// WithContext adds context information to the error
+func (e *RegistryError) WithContext(key string, value interface{}) *RegistryError {
+ if e.Context == nil {
+ e.Context = make(map[string]interface{})
+ }
+ e.Context[key] = value
+ return e
+}
+
+// ==============================================================================
+// ERROR CONSTRUCTORS
+// ==============================================================================
+
+// NewOperationError creates a new operation error with stack trace
+func NewOperationError(operation, component, message string, cause error) *OperationError {
+ stack := make([]uintptr, 10)
+ n := runtime.Callers(2, stack)
+
+ return &OperationError{
+ Operation: operation,
+ Component: component,
+ Message: message,
+ Cause: cause,
+ Stack: stack[:n],
+ }
+}
+
+// NewValidationError creates a new validation error with stack trace
+func NewValidationError(component, field, rule, message string, value interface{}) *ValidationError {
+ stack := make([]uintptr, 10)
+ n := runtime.Callers(2, stack)
+
+ return &ValidationError{
+ Component: component,
+ Field: field,
+ Rule: rule,
+ Message: message,
+ Value: value,
+ Stack: stack[:n],
+ }
+}
+
+// NewConfigurationError creates a new configuration error with stack trace
+func NewConfigurationError(component, setting, message string, value interface{}) *ConfigurationError {
+ stack := make([]uintptr, 10)
+ n := runtime.Callers(2, stack)
+
+ return &ConfigurationError{
+ Component: component,
+ Setting: setting,
+ Message: message,
+ Value: value,
+ Stack: stack[:n],
+ }
+}
+
+// NewResourceError creates a new resource error with stack trace
+func NewResourceError(component, resource, message string, current, limit interface{}) *ResourceError {
+ stack := make([]uintptr, 10)
+ n := runtime.Callers(2, stack)
+
+ return &ResourceError{
+ Component: component,
+ Resource: resource,
+ Message: message,
+ Current: current,
+ Limit: limit,
+ Stack: stack[:n],
+ }
+}
+
+// NewRegistryError creates a new registry error with stack trace
+func NewRegistryError(registry, operation, key, message string, cause error) *RegistryError {
+ stack := make([]uintptr, 10)
+ n := runtime.Callers(2, stack)
+
+ return &RegistryError{
+ Registry: registry,
+ Operation: operation,
+ Key: key,
+ Message: message,
+ Cause: cause,
+ Stack: stack[:n],
+ }
+}
+
+// ==============================================================================
+// ERROR UTILITIES
+// ==============================================================================
+
+// IsOperationError checks if an error is an OperationError
+func IsOperationError(err error) bool {
+ _, ok := err.(*OperationError)
+ return ok
+}
+
+// IsValidationError checks if an error is a ValidationError
+func IsValidationError(err error) bool {
+ _, ok := err.(*ValidationError)
+ return ok
+}
+
+// IsConfigurationError checks if an error is a ConfigurationError
+func IsConfigurationError(err error) bool {
+ _, ok := err.(*ConfigurationError)
+ return ok
+}
+
+// IsResourceError checks if an error is a ResourceError
+func IsResourceError(err error) bool {
+ _, ok := err.(*ResourceError)
+ return ok
+}
+
+// IsRegistryError checks if an error is a RegistryError
+func IsRegistryError(err error) bool {
+ _, ok := err.(*RegistryError)
+ return ok
+}
+
+// GetErrorContext extracts context from structured errors
+func GetErrorContext(err error) map[string]interface{} {
+ switch e := err.(type) {
+ case *OperationError:
+ return e.Context
+ case *ValidationError:
+ return e.Context
+ case *ConfigurationError:
+ return e.Context
+ case *ResourceError:
+ return e.Context
+ case *RegistryError:
+ return e.Context
+ default:
+ return nil
+ }
+}
+
+// GetErrorStack extracts stack trace from structured errors
+func GetErrorStack(err error) []uintptr {
+ switch e := err.(type) {
+ case *OperationError:
+ return e.Stack
+ case *ValidationError:
+ return e.Stack
+ case *ConfigurationError:
+ return e.Stack
+ case *ResourceError:
+ return e.Stack
+ case *RegistryError:
+ return e.Stack
+ default:
+ return nil
+ }
+}
+
+// ==============================================================================
+// ERROR POOL INTEGRATION
+// ==============================================================================
+
+// Extend existing error types to implement Reset for pooling
+func (e *OperationError) Reset() {
+ e.Operation = ""
+ e.Component = ""
+ e.Message = ""
+ e.Cause = nil
+ e.Context = nil
+ e.Stack = nil
+}
+
+func (e *ValidationError) Reset() {
+ e.Field = ""
+ e.Value = nil
+ e.Rule = ""
+ e.Message = ""
+ e.Component = ""
+ e.Context = nil
+ e.Stack = nil
+}
+
+func (e *ConfigurationError) Reset() {
+ e.Setting = ""
+ e.Value = nil
+ e.Message = ""
+ e.Component = ""
+ e.Context = nil
+ e.Stack = nil
+}
+
+func (e *ResourceError) Reset() {
+ e.Resource = ""
+ e.Limit = nil
+ e.Current = nil
+ e.Message = ""
+ e.Component = ""
+ e.Context = nil
+ e.Stack = nil
+}
+
+func (e *RegistryError) Reset() {
+ e.Registry = ""
+ e.Key = ""
+ e.Operation = ""
+ e.Message = ""
+ e.Cause = nil
+ e.Context = nil
+ e.Stack = nil
+}
diff --git a/sdks/community/go/pkg/encoding/interface.go b/sdks/community/go/pkg/encoding/interface.go
new file mode 100644
index 000000000..8aa527454
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/interface.go
@@ -0,0 +1,364 @@
+package encoding
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+)
+
+// ==============================================================================
+// CORE SINGLE-PURPOSE INTERFACES (Interface Segregation Principle)
+// ==============================================================================
+
+// Encoder defines the interface for encoding events to bytes
+type Encoder interface {
+ // Encode encodes a single event
+ Encode(ctx context.Context, event events.Event) ([]byte, error)
+
+ // EncodeMultiple encodes multiple events efficiently
+ EncodeMultiple(ctx context.Context, events []events.Event) ([]byte, error)
+
+ // ContentType returns the MIME type for this encoder
+ ContentType() string
+}
+
+// Decoder defines the interface for decoding events from bytes
+type Decoder interface {
+ // Decode decodes a single event from raw data
+ Decode(ctx context.Context, data []byte) (events.Event, error)
+
+ // DecodeMultiple decodes multiple events from raw data
+ DecodeMultiple(ctx context.Context, data []byte) ([]events.Event, error)
+
+ // ContentType returns the MIME type for this decoder
+ ContentType() string
+}
+
+// ContentTypeProvider provides MIME type information
+type ContentTypeProvider interface {
+ // ContentType returns the MIME type for this component
+ ContentType() string
+}
+
+// StreamingCapabilityProvider indicates streaming support
+type StreamingCapabilityProvider interface {
+ // SupportsStreaming indicates if this component has streaming capabilities
+ SupportsStreaming() bool
+}
+
+// ==============================================================================
+// STREAMING INTERFACES
+// ==============================================================================
+
+// StreamEncoder defines the interface for streaming event encoding
+type StreamEncoder interface {
+ // EncodeStream encodes events from a channel to a writer
+ EncodeStream(ctx context.Context, input <-chan events.Event, output io.Writer) error
+
+ // Session management methods
+ StartStream(ctx context.Context, w io.Writer) error
+ EndStream(ctx context.Context) error
+
+ // Event processing method
+ WriteEvent(ctx context.Context, event events.Event) error
+
+ // ContentType returns the MIME type for this stream encoder
+ ContentType() string
+}
+
+// StreamDecoder defines the interface for streaming event decoding
+type StreamDecoder interface {
+ // DecodeStream decodes events from a reader to a channel
+ DecodeStream(ctx context.Context, input io.Reader, output chan<- events.Event) error
+
+ // Session management methods
+ StartStream(ctx context.Context, r io.Reader) error
+ EndStream(ctx context.Context) error
+
+ // Event processing method
+ ReadEvent(ctx context.Context) (events.Event, error)
+
+ // ContentType returns the MIME type for this stream decoder
+ ContentType() string
+}
+
+// StreamSessionManager manages streaming sessions
+type StreamSessionManager interface {
+ // StartEncodingSession initializes a streaming encoding session
+ StartEncodingSession(ctx context.Context, w io.Writer) error
+
+ // StartDecodingSession initializes a streaming decoding session
+ StartDecodingSession(ctx context.Context, r io.Reader) error
+
+ // EndSession finalizes the current streaming session
+ EndSession(ctx context.Context) error
+}
+
+// StreamEventProcessor processes individual events in a stream
+type StreamEventProcessor interface {
+ // WriteEvent writes a single event to the encoding stream
+ WriteEvent(ctx context.Context, event events.Event) error
+
+ // ReadEvent reads a single event from the decoding stream
+ ReadEvent(ctx context.Context) (events.Event, error)
+}
+
+// ==============================================================================
+// VALIDATION INTERFACES
+// ==============================================================================
+
+// Validator provides basic validation capabilities
+type Validator interface {
+ // Validate validates data according to component-specific rules
+ Validate(ctx context.Context, data interface{}) error
+}
+
+// OutputValidator validates encoded output
+type OutputValidator interface {
+ // ValidateOutput validates that encoded data is correct
+ ValidateOutput(ctx context.Context, data []byte) error
+}
+
+// InputValidator validates decoded input
+type InputValidator interface {
+ // ValidateInput validates that input data can be decoded
+ ValidateInput(ctx context.Context, data []byte) error
+}
+
+// ==============================================================================
+// COMPOSITE INTERFACES (Built through composition)
+// ==============================================================================
+
+// Codec combines encoding and decoding with content type information
+// This is a convenience interface for components that need both operations
+type Codec interface {
+ Encoder
+ Decoder
+ ContentTypeProvider
+ StreamingCapabilityProvider
+}
+
+// StreamCodec combines streaming encoding and decoding
+type StreamCodec interface {
+ Encoder // Basic encoding operations
+ Decoder // Basic decoding operations
+ ContentTypeProvider
+ StreamingCapabilityProvider
+
+ // Streaming operations (delegated to components)
+ EncodeStream(ctx context.Context, input <-chan events.Event, output io.Writer) error
+ DecodeStream(ctx context.Context, input io.Reader, output chan<- events.Event) error
+
+ // Session management methods (legacy compatibility)
+ StartEncoding(ctx context.Context, w io.Writer) error
+ WriteEvent(ctx context.Context, event events.Event) error
+ EndEncoding(ctx context.Context) error
+ StartDecoding(ctx context.Context, r io.Reader) error
+ ReadEvent(ctx context.Context) (events.Event, error)
+ EndDecoding(ctx context.Context) error
+
+ // Stream component access methods
+ GetStreamEncoder() StreamEncoder
+ GetStreamDecoder() StreamDecoder
+}
+
+// FullStreamCodec provides complete streaming functionality
+// This includes both basic and streaming operations with session management
+type FullStreamCodec interface {
+ Codec // Basic encode/decode operations
+ StreamCodec // Stream operations
+ StreamSessionManager // Session management
+ StreamEventProcessor // Event-level streaming
+}
+
+// ValidatingCodec adds validation capabilities to basic codec operations
+type ValidatingCodec interface {
+ Codec
+ OutputValidator
+ InputValidator
+}
+
+// ==============================================================================
+// CONFIGURATION AND ERROR TYPES
+// ==============================================================================
+
+// EncodingOptions provides options for encoding operations
+type EncodingOptions struct {
+ // Pretty indicates if output should be formatted for readability
+ Pretty bool
+
+ // Compression specifies compression algorithm (e.g., "gzip", "zstd")
+ Compression string
+
+ // BufferSize specifies buffer size for streaming operations
+ BufferSize int
+
+ // MaxSize specifies maximum encoded size (0 for unlimited)
+ MaxSize int64
+
+ // ValidateOutput enables output validation after encoding
+ ValidateOutput bool
+
+ // CrossSDKCompatibility ensures compatibility with other SDKs
+ CrossSDKCompatibility bool
+}
+
+// Validate validates the encoding options
+func (opts *EncodingOptions) Validate() error {
+ if opts == nil {
+ return nil // nil options are acceptable, defaults will be used
+ }
+
+ // Validate buffer size
+ if opts.BufferSize < 0 {
+ return fmt.Errorf("buffer size cannot be negative, got %d", opts.BufferSize)
+ }
+
+ // Validate max size
+ if opts.MaxSize < 0 {
+ return fmt.Errorf("max size cannot be negative, got %d", opts.MaxSize)
+ }
+
+ // Validate compression algorithm
+ if opts.Compression != "" {
+ validCompressions := []string{"gzip", "zstd", "lz4", "deflate"}
+ valid := false
+ for _, comp := range validCompressions {
+ if opts.Compression == comp {
+ valid = true
+ break
+ }
+ }
+ if !valid {
+ return fmt.Errorf("unsupported compression algorithm %q, supported: %v", opts.Compression, validCompressions)
+ }
+ }
+
+ return nil
+}
+
+// DecodingOptions provides options for decoding operations
+type DecodingOptions struct {
+ // Strict enables strict validation during decoding
+ Strict bool
+
+ // MaxSize specifies maximum input size to process (0 for unlimited)
+ MaxSize int64
+
+ // BufferSize specifies buffer size for streaming operations
+ BufferSize int
+
+ // AllowUnknownFields allows unknown fields in the input
+ AllowUnknownFields bool
+
+ // ValidateEvents enables event validation after decoding
+ ValidateEvents bool
+}
+
+// Validate validates the decoding options
+func (opts *DecodingOptions) Validate() error {
+ if opts == nil {
+ return nil // nil options are acceptable, defaults will be used
+ }
+
+ // Validate buffer size
+ if opts.BufferSize < 0 {
+ return fmt.Errorf("buffer size cannot be negative, got %d", opts.BufferSize)
+ }
+
+ // Validate max size
+ if opts.MaxSize < 0 {
+ return fmt.Errorf("max size cannot be negative, got %d", opts.MaxSize)
+ }
+
+ return nil
+}
+
+// EncodingError represents an error during encoding
+type EncodingError struct {
+ Format string
+ Event events.Event
+ Message string
+ Cause error
+}
+
+func (e *EncodingError) Error() string {
+ if e.Cause != nil {
+ return "encoding error: " + e.Message + ": " + e.Cause.Error()
+ }
+ return "encoding error: " + e.Message
+}
+
+func (e *EncodingError) Unwrap() error {
+ return e.Cause
+}
+
+// DecodingError represents an error during decoding
+type DecodingError struct {
+ Format string
+ Data []byte
+ Message string
+ Cause error
+}
+
+func (e *DecodingError) Error() string {
+ if e.Cause != nil {
+ return "decoding error: " + e.Message + ": " + e.Cause.Error()
+ }
+ return "decoding error: " + e.Message
+}
+
+func (e *DecodingError) Unwrap() error {
+ return e.Cause
+}
+
+// ==============================================================================
+// FACTORY AND UTILITY INTERFACES
+// ==============================================================================
+
+// ContentNegotiator defines the interface for content type negotiation
+type ContentNegotiator interface {
+ // Negotiate selects the best content type based on Accept header
+ Negotiate(acceptHeader string) (string, error)
+
+ // SupportedTypes returns list of supported content types
+ SupportedTypes() []string
+
+ // PreferredType returns the preferred content type
+ PreferredType() string
+
+ // CanHandle checks if a content type can be handled
+ CanHandle(contentType string) bool
+
+ // AddFormat adds a format with its priority/quality value
+ AddFormat(contentType string, priority float64) error
+}
+
+// CodecFactory creates codecs for specific content types
+// This interface is focused on the core factory responsibility
+type CodecFactory interface {
+ // CreateCodec creates a basic codec for the specified content type
+ CreateCodec(ctx context.Context, contentType string, encOptions *EncodingOptions, decOptions *DecodingOptions) (Codec, error)
+
+ // SupportedTypes returns list of supported content types
+ SupportedTypes() []string
+}
+
+// StreamCodecFactory creates streaming codecs
+// Separated from CodecFactory to follow Interface Segregation Principle
+type StreamCodecFactory interface {
+ // CreateStreamCodec creates a streaming codec for the specified content type
+ CreateStreamCodec(ctx context.Context, contentType string, encOptions *EncodingOptions, decOptions *DecodingOptions) (StreamCodec, error)
+
+ // SupportsStreaming indicates if streaming is supported for the given content type
+ SupportsStreaming(contentType string) bool
+}
+
+// FullCodecFactory combines basic and streaming codec creation
+// This is a convenience interface for factories that support both
+type FullCodecFactory interface {
+ CodecFactory
+ StreamCodecFactory
+}
diff --git a/sdks/community/go/pkg/encoding/json/json.go b/sdks/community/go/pkg/encoding/json/json.go
new file mode 100644
index 000000000..59df078a1
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/json/json.go
@@ -0,0 +1,34 @@
+package json
+
+import (
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding"
+)
+
+// Default codec instances for convenience
+var (
+ // DefaultCodec is a pre-configured JSON codec with default options
+ DefaultCodec = NewDefaultJSONCodec()
+
+ // PrettyCodec is a pre-configured JSON codec that produces pretty-printed output
+ PrettyCodec = NewJSONCodec(PrettyCodecOptions().EncodingOptions, PrettyCodecOptions().DecodingOptions)
+
+ // CompatibilityCodec is optimized for cross-SDK compatibility
+ CompatibilityCodec = NewJSONCodec(CompatibilityCodecOptions().EncodingOptions, CompatibilityCodecOptions().DecodingOptions)
+)
+
+// Factory functions for creating encoders and decoders
+
+// NewEncoder creates a JSON encoder with default options
+func NewEncoder() encoding.Encoder {
+ return NewJSONEncoder(nil)
+}
+
+// NewDecoder creates a JSON decoder with default options
+func NewDecoder() encoding.Decoder {
+ return NewJSONDecoder(nil)
+}
+
+// NewCodec creates a JSON codec with default options
+func NewCodec() encoding.Codec {
+ return NewDefaultJSONCodec()
+}
diff --git a/sdks/community/go/pkg/encoding/json/json_codec.go b/sdks/community/go/pkg/encoding/json/json_codec.go
new file mode 100644
index 000000000..2df2965f8
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/json/json_codec.go
@@ -0,0 +1,148 @@
+package json
+
+import (
+ "context"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding"
+)
+
+// JSONCodec implements the new Codec interface for JSON encoding/decoding
+// It properly composes the focused Encoder, Decoder, and ContentTypeProvider interfaces
+type JSONCodec struct {
+ *JSONEncoder
+ *JSONDecoder
+}
+
+// Ensure JSONCodec implements the core interfaces
+var (
+ _ encoding.Encoder = (*JSONCodec)(nil)
+ _ encoding.Decoder = (*JSONCodec)(nil)
+ _ encoding.ContentTypeProvider = (*JSONCodec)(nil)
+ _ encoding.Codec = (*JSONCodec)(nil)
+)
+
+// NewJSONCodec creates a new JSON codec with the given options
+func NewJSONCodec(encOptions *encoding.EncodingOptions, decOptions *encoding.DecodingOptions) *JSONCodec {
+ return &JSONCodec{
+ JSONEncoder: NewJSONEncoder(encOptions),
+ JSONDecoder: NewJSONDecoder(decOptions),
+ }
+}
+
+// NewDefaultJSONCodec creates a new JSON codec with default options
+func NewDefaultJSONCodec() *JSONCodec {
+ return NewJSONCodec(
+ &encoding.EncodingOptions{
+ CrossSDKCompatibility: true,
+ ValidateOutput: true,
+ },
+ &encoding.DecodingOptions{
+ Strict: true,
+ ValidateEvents: true,
+ },
+ )
+}
+
+// Encode delegates to the encoder
+func (c *JSONCodec) Encode(ctx context.Context, event events.Event) ([]byte, error) {
+ return c.JSONEncoder.Encode(ctx, event)
+}
+
+// EncodeMultiple delegates to the encoder
+func (c *JSONCodec) EncodeMultiple(ctx context.Context, events []events.Event) ([]byte, error) {
+ return c.JSONEncoder.EncodeMultiple(ctx, events)
+}
+
+// Decode delegates to the decoder
+func (c *JSONCodec) Decode(ctx context.Context, data []byte) (events.Event, error) {
+ return c.JSONDecoder.Decode(ctx, data)
+}
+
+// DecodeMultiple delegates to the decoder
+func (c *JSONCodec) DecodeMultiple(ctx context.Context, data []byte) ([]events.Event, error) {
+ return c.JSONDecoder.DecodeMultiple(ctx, data)
+}
+
+// ContentType returns the MIME type for JSON
+func (c *JSONCodec) ContentType() string {
+ return "application/json"
+}
+
+// SupportsStreaming indicates that JSON codec supports streaming
+func (c *JSONCodec) SupportsStreaming() bool {
+ return true
+}
+
+// CanStream indicates that JSON codec supports streaming (backward compatibility)
+// This method is provided for backward compatibility with legacy interfaces
+func (c *JSONCodec) CanStream() bool {
+ return c.SupportsStreaming()
+}
+
+// CodecOptions provides combined options for JSON codec
+type CodecOptions struct {
+ EncodingOptions *encoding.EncodingOptions
+ DecodingOptions *encoding.DecodingOptions
+}
+
+// DefaultCodecOptions returns default codec options
+func DefaultCodecOptions() *CodecOptions {
+ return &CodecOptions{
+ EncodingOptions: &encoding.EncodingOptions{
+ CrossSDKCompatibility: true,
+ ValidateOutput: true,
+ Pretty: false,
+ BufferSize: 4096,
+ },
+ DecodingOptions: &encoding.DecodingOptions{
+ Strict: true,
+ ValidateEvents: true,
+ AllowUnknownFields: false,
+ BufferSize: 4096,
+ },
+ }
+}
+
+// PrettyCodecOptions returns codec options for pretty-printed JSON
+func PrettyCodecOptions() *CodecOptions {
+ opts := DefaultCodecOptions()
+ opts.EncodingOptions.Pretty = true
+ return opts
+}
+
+// CompatibilityCodecOptions returns codec options optimized for cross-SDK compatibility
+func CompatibilityCodecOptions() *CodecOptions {
+ return &CodecOptions{
+ EncodingOptions: &encoding.EncodingOptions{
+ CrossSDKCompatibility: true,
+ ValidateOutput: true,
+ Pretty: false,
+ BufferSize: 8192,
+ },
+ DecodingOptions: &encoding.DecodingOptions{
+ Strict: false,
+ ValidateEvents: true,
+ AllowUnknownFields: true,
+ BufferSize: 8192,
+ },
+ }
+}
+
+// StreamingCodecOptions returns codec options optimized for streaming
+func StreamingCodecOptions() *CodecOptions {
+ return &CodecOptions{
+ EncodingOptions: &encoding.EncodingOptions{
+ CrossSDKCompatibility: true,
+ ValidateOutput: false, // Skip validation for performance
+ Pretty: false,
+ BufferSize: 16384, // Larger buffer for streaming
+ },
+ DecodingOptions: &encoding.DecodingOptions{
+ Strict: false,
+ ValidateEvents: false, // Skip validation for performance
+ AllowUnknownFields: true,
+ BufferSize: 16384, // Larger buffer for streaming
+ },
+ }
+}
diff --git a/sdks/community/go/pkg/encoding/json/json_decoder.go b/sdks/community/go/pkg/encoding/json/json_decoder.go
new file mode 100644
index 000000000..dac94f315
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/json/json_decoder.go
@@ -0,0 +1,397 @@
+package json
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync/atomic"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding"
+)
+
+// Ensure JSONDecoder implements the focused interfaces
+var (
+ _ encoding.Decoder = (*JSONDecoder)(nil)
+ _ encoding.ContentTypeProvider = (*JSONDecoder)(nil)
+ _ encoding.StreamingCapabilityProvider = (*JSONDecoder)(nil)
+)
+
+// JSONDecoder implements the Decoder interface for JSON format
+// This decoder is stateless and thread-safe for concurrent use.
+type JSONDecoder struct {
+ options *encoding.DecodingOptions
+ activeOperations int32 // Track active decoding operations
+ maxConcurrent int32 // Maximum concurrent operations
+}
+
+// NewJSONDecoder creates a new JSON decoder with the given options
+func NewJSONDecoder(options *encoding.DecodingOptions) *JSONDecoder {
+ if options == nil {
+ options = &encoding.DecodingOptions{
+ Strict: true,
+ ValidateEvents: true,
+ }
+ }
+ return &JSONDecoder{
+ options: options,
+ maxConcurrent: 100, // Default limit of 100 concurrent operations
+ }
+}
+
+// NewJSONDecoderWithConcurrencyLimit creates a new JSON decoder with specified concurrency limit
+func NewJSONDecoderWithConcurrencyLimit(options *encoding.DecodingOptions, maxConcurrent int32) *JSONDecoder {
+ if options == nil {
+ options = &encoding.DecodingOptions{
+ Strict: true,
+ ValidateEvents: true,
+ }
+ }
+ return &JSONDecoder{
+ options: options,
+ maxConcurrent: maxConcurrent,
+ }
+}
+
+// eventTypeWrapper is used to extract the event type from JSON
+type eventTypeWrapper struct {
+ Type string `json:"type"`
+}
+
+// Decode decodes a single event from JSON data
+func (d *JSONDecoder) Decode(ctx context.Context, data []byte) (events.Event, error) {
+ // Check context cancellation
+ if err := ctx.Err(); err != nil {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Message: "context cancelled",
+ Cause: err,
+ }
+ }
+
+ // Check concurrency limits atomically to avoid race condition
+ if d.maxConcurrent > 0 {
+ // Atomically increment and check the limit
+ current := atomic.AddInt32(&d.activeOperations, 1)
+ if current > d.maxConcurrent {
+ // Exceeded limit, decrement and return error
+ atomic.AddInt32(&d.activeOperations, -1)
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: fmt.Sprintf("decoding concurrency limit exceeded: %d", d.maxConcurrent),
+ }
+ }
+ // Operation is within limit, ensure decrement happens on exit
+ defer atomic.AddInt32(&d.activeOperations, -1)
+ }
+
+ if len(data) == 0 {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: "empty data",
+ }
+ }
+
+ // Check size limits
+ if d.options.MaxSize > 0 && int64(len(data)) > d.options.MaxSize {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: fmt.Sprintf("data exceeds max size of %d bytes", d.options.MaxSize),
+ }
+ }
+
+ // First, decode just the type field without strict checking
+ var typeWrapper eventTypeWrapper
+ if err := json.Unmarshal(data, &typeWrapper); err != nil {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: "failed to decode event type",
+ Cause: err,
+ }
+ }
+
+ // Create the appropriate event type based on the type field
+ event, err := d.createEvent(events.EventType(typeWrapper.Type), data)
+ if err != nil {
+ return nil, err
+ }
+
+ // Validate the event if requested
+ if d.options.ValidateEvents {
+ if err := event.Validate(); err != nil {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: "event validation failed",
+ Cause: err,
+ }
+ }
+ }
+
+ return event, nil
+}
+
+// DecodeMultiple decodes multiple events from JSON array data
+func (d *JSONDecoder) DecodeMultiple(ctx context.Context, data []byte) ([]events.Event, error) {
+ // Check context cancellation
+ if err := ctx.Err(); err != nil {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Message: "context cancelled",
+ Cause: err,
+ }
+ }
+
+ // Check concurrency limits atomically to avoid race condition
+ if d.maxConcurrent > 0 {
+ // Atomically increment and check the limit
+ current := atomic.AddInt32(&d.activeOperations, 1)
+ if current > d.maxConcurrent {
+ // Exceeded limit, decrement and return error
+ atomic.AddInt32(&d.activeOperations, -1)
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: fmt.Sprintf("decoding concurrency limit exceeded: %d", d.maxConcurrent),
+ }
+ }
+ // Operation is within limit, ensure decrement happens on exit
+ defer atomic.AddInt32(&d.activeOperations, -1)
+ }
+
+ if len(data) == 0 {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: "empty data",
+ }
+ }
+
+ // Check size limits
+ if d.options.MaxSize > 0 && int64(len(data)) > d.options.MaxSize {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: fmt.Sprintf("data exceeds max size of %d bytes", d.options.MaxSize),
+ }
+ }
+
+ // First, decode as an array of raw messages
+ var rawEvents []json.RawMessage
+ if err := json.Unmarshal(data, &rawEvents); err != nil {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: "failed to decode event array",
+ Cause: err,
+ }
+ }
+
+ // Decode each event
+ events := make([]events.Event, 0, len(rawEvents))
+ for i, rawEvent := range rawEvents {
+ event, err := d.Decode(ctx, rawEvent)
+ if err != nil {
+ // Enhance error with index information
+ if decErr, ok := err.(*encoding.DecodingError); ok {
+ decErr.Message = fmt.Sprintf("failed to decode event at index %d: %s", i, decErr.Message)
+ }
+ return nil, err
+ }
+ events = append(events, event)
+ }
+
+ return events, nil
+}
+
+// createEvent creates the appropriate event type based on the type string
+func (d *JSONDecoder) createEvent(eventType events.EventType, data []byte) (events.Event, error) {
+ // Use buffer pooling for creating a byte reader
+ buf := encoding.GetBufferSafe(len(data))
+ if buf == nil {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: "failed to allocate buffer: resource limits exceeded",
+ }
+ }
+ defer encoding.PutBuffer(buf)
+
+ buf.Write(data)
+
+ decoder := json.NewDecoder(buf)
+ if d.options.Strict && !d.options.AllowUnknownFields {
+ decoder.DisallowUnknownFields()
+ }
+
+ var err error
+ var event events.Event
+
+ switch eventType {
+ case events.EventTypeTextMessageStart:
+ var e events.TextMessageStartEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeTextMessageContent:
+ var e events.TextMessageContentEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeTextMessageEnd:
+ var e events.TextMessageEndEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeToolCallStart:
+ var e events.ToolCallStartEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeToolCallArgs:
+ var e events.ToolCallArgsEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeToolCallEnd:
+ var e events.ToolCallEndEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeStateSnapshot:
+ var e events.StateSnapshotEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeStateDelta:
+ var e events.StateDeltaEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeMessagesSnapshot:
+ var e events.MessagesSnapshotEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeRaw:
+ var e events.RawEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeCustom:
+ var e events.CustomEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeRunStarted:
+ var e events.RunStartedEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeRunFinished:
+ var e events.RunFinishedEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeRunError:
+ var e events.RunErrorEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeStepStarted:
+ var e events.StepStartedEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ case events.EventTypeStepFinished:
+ var e events.StepFinishedEvent
+ err = decoder.Decode(&e)
+ if err == nil {
+ event = &e
+ }
+
+ default:
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: fmt.Sprintf("unknown event type: %s", eventType),
+ }
+ }
+
+ if err != nil {
+ return nil, &encoding.DecodingError{
+ Format: "json",
+ Data: data,
+ Message: fmt.Sprintf("failed to decode %s event", eventType),
+ Cause: err,
+ }
+ }
+
+ // Ensure the base event is properly initialized
+ if event != nil && event.GetBaseEvent() != nil {
+ baseEvent := event.GetBaseEvent()
+ baseEvent.EventType = eventType
+ }
+
+ return event, nil
+}
+
+// ContentType returns the MIME type this decoder handles
+func (d *JSONDecoder) ContentType() string {
+ return "application/json"
+}
+
+// CanStream indicates that JSON decoder supports streaming (backward compatibility)
+func (d *JSONDecoder) CanStream() bool {
+ return true
+}
+
+// SupportsStreaming indicates that JSON decoder supports streaming
+func (d *JSONDecoder) SupportsStreaming() bool {
+ return true
+}
+
+// Reset resets the decoder with new options (for pooling)
+func (d *JSONDecoder) Reset(options *encoding.DecodingOptions) {
+ if options == nil {
+ options = &encoding.DecodingOptions{
+ Strict: true,
+ ValidateEvents: true,
+ }
+ }
+ d.options = options
+}
diff --git a/sdks/community/go/pkg/encoding/json/json_encoder.go b/sdks/community/go/pkg/encoding/json/json_encoder.go
new file mode 100644
index 000000000..8073c093a
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/json/json_encoder.go
@@ -0,0 +1,435 @@
+package json
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync/atomic"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding"
+)
+
+// Ensure JSONEncoder implements the focused interfaces
+var (
+ _ encoding.Encoder = (*JSONEncoder)(nil)
+ _ encoding.ContentTypeProvider = (*JSONEncoder)(nil)
+ _ encoding.StreamingCapabilityProvider = (*JSONEncoder)(nil)
+)
+
+// JSONEncoder implements the Encoder interface for JSON format
+// This encoder is stateless and thread-safe for concurrent use.
+type JSONEncoder struct {
+ options *encoding.EncodingOptions
+ activeOperations int32 // Track active encoding operations
+ maxConcurrent int32 // Maximum concurrent operations
+}
+
+// NewJSONEncoder creates a new JSON encoder with the given options
+func NewJSONEncoder(options *encoding.EncodingOptions) *JSONEncoder {
+ if options == nil {
+ options = &encoding.EncodingOptions{
+ CrossSDKCompatibility: true,
+ ValidateOutput: true,
+ }
+ }
+ return &JSONEncoder{
+ options: options,
+ maxConcurrent: 100, // Default limit of 100 concurrent operations
+ }
+}
+
+// NewJSONEncoderWithConcurrencyLimit creates a new JSON encoder with specified concurrency limit
+func NewJSONEncoderWithConcurrencyLimit(options *encoding.EncodingOptions, maxConcurrent int32) *JSONEncoder {
+ if options == nil {
+ options = &encoding.EncodingOptions{
+ CrossSDKCompatibility: true,
+ ValidateOutput: true,
+ }
+ }
+ return &JSONEncoder{
+ options: options,
+ maxConcurrent: maxConcurrent,
+ }
+}
+
+// Encode encodes a single event to JSON
+func (e *JSONEncoder) Encode(ctx context.Context, event events.Event) ([]byte, error) {
+ // Check context cancellation
+ if err := ctx.Err(); err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: "context cancelled",
+ Cause: err,
+ }
+ }
+
+ // Check concurrency limits atomically to avoid race condition
+ if e.maxConcurrent > 0 {
+ // Atomically increment and check the limit
+ current := atomic.AddInt32(&e.activeOperations, 1)
+ if current > e.maxConcurrent {
+ // Exceeded limit, decrement and return error
+ atomic.AddInt32(&e.activeOperations, -1)
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: fmt.Sprintf("encoding concurrency limit exceeded: %d", e.maxConcurrent),
+ }
+ }
+ // Operation is within limit, ensure decrement happens on exit
+ defer atomic.AddInt32(&e.activeOperations, -1)
+ }
+
+ if event == nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: "cannot encode nil event",
+ }
+ }
+
+ // Validate the event before encoding if requested
+ if e.options.ValidateOutput {
+ if err := event.Validate(); err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: "event validation failed",
+ Cause: err,
+ }
+ }
+ }
+
+ // Use the event's ToJSON method for cross-SDK compatibility
+ if e.options.CrossSDKCompatibility {
+ data, err := event.ToJSON()
+ if err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: "failed to encode event",
+ Cause: err,
+ }
+ }
+
+ // Pretty print if requested
+ if e.options.Pretty {
+ buf := encoding.GetBufferSafe(len(data) * 2) // Estimate 2x size for pretty printing
+ if buf == nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: "failed to allocate buffer for pretty printing: resource limits exceeded",
+ }
+ }
+ defer encoding.PutBuffer(buf)
+
+ if err := json.Indent(buf, data, "", " "); err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: "failed to format JSON",
+ Cause: err,
+ }
+ }
+ data = make([]byte, buf.Len())
+ copy(data, buf.Bytes())
+ }
+
+ // Check size limits
+ if e.options.MaxSize > 0 && int64(len(data)) > e.options.MaxSize {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: fmt.Sprintf("encoded event exceeds max size of %d bytes", e.options.MaxSize),
+ }
+ }
+
+ return data, nil
+ }
+
+ // Standard JSON encoding with buffer pooling
+ var data []byte
+ var err error
+
+ if e.options.Pretty {
+ // Use buffer pooling for pretty printing with optimized size
+ optimalSize := encoding.GetOptimalBufferSizeForEvent(event)
+ buf := encoding.GetBufferSafe(optimalSize * 2) // Pretty printing needs more space
+ if buf == nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: "failed to allocate buffer for pretty printing: resource limits exceeded",
+ }
+ }
+ defer encoding.PutBuffer(buf)
+
+ encoder := json.NewEncoder(buf)
+ encoder.SetIndent("", " ")
+ err = encoder.Encode(event)
+ if err == nil {
+ data = make([]byte, buf.Len())
+ copy(data, buf.Bytes())
+ }
+ } else {
+ // Use buffer pooling for compact encoding with optimized size
+ optimalSize := encoding.GetOptimalBufferSizeForEvent(event)
+ buf := encoding.GetBufferSafe(optimalSize)
+ if buf == nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: "failed to allocate buffer: resource limits exceeded",
+ }
+ }
+ defer encoding.PutBuffer(buf)
+
+ encoder := json.NewEncoder(buf)
+ err = encoder.Encode(event)
+ if err == nil {
+ // Remove trailing newline added by json.Encoder
+ bytes := buf.Bytes()
+ if len(bytes) > 0 && bytes[len(bytes)-1] == '\n' {
+ bytes = bytes[:len(bytes)-1]
+ }
+ data = make([]byte, len(bytes))
+ copy(data, bytes)
+ }
+ }
+
+ if err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: "failed to marshal event",
+ Cause: err,
+ }
+ }
+
+ // Check size limits
+ if e.options.MaxSize > 0 && int64(len(data)) > e.options.MaxSize {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: fmt.Sprintf("encoded event exceeds max size of %d bytes", e.options.MaxSize),
+ }
+ }
+
+ return data, nil
+}
+
+// EncodeMultiple encodes multiple events efficiently
+func (e *JSONEncoder) EncodeMultiple(ctx context.Context, events []events.Event) ([]byte, error) {
+ // Check context cancellation
+ if err := ctx.Err(); err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: "context cancelled",
+ Cause: err,
+ }
+ }
+
+ // Check concurrency limits atomically to avoid race condition
+ if e.maxConcurrent > 0 {
+ // Atomically increment and check the limit
+ current := atomic.AddInt32(&e.activeOperations, 1)
+ if current > e.maxConcurrent {
+ // Exceeded limit, decrement and return error
+ atomic.AddInt32(&e.activeOperations, -1)
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: fmt.Sprintf("encoding concurrency limit exceeded: %d", e.maxConcurrent),
+ }
+ }
+ // Operation is within limit, ensure decrement happens on exit
+ defer atomic.AddInt32(&e.activeOperations, -1)
+ }
+
+ if len(events) == 0 {
+ return []byte("[]"), nil
+ }
+
+ // Validate all events first if requested
+ if e.options.ValidateOutput {
+ for i, event := range events {
+ if event == nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: fmt.Sprintf("cannot encode nil event at index %d", i),
+ }
+ }
+ if err := event.Validate(); err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: fmt.Sprintf("event validation failed at index %d", i),
+ Cause: err,
+ }
+ }
+ }
+ }
+
+ // Create a slice to hold all encoded events
+ encodedEvents := make([]json.RawMessage, 0, len(events))
+ totalSize := int64(2) // Account for "[]"
+
+ // Use a pooled buffer for better memory efficiency - estimate based on actual events
+ estimatedSize := encoding.GetOptimalBufferSizeForMultiple(events)
+ mainBuf := encoding.GetBufferSafe(estimatedSize)
+ if mainBuf == nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: "failed to allocate main buffer: resource limits exceeded",
+ }
+ }
+ defer encoding.PutBuffer(mainBuf)
+
+ for i, event := range events {
+ // Check context cancellation periodically
+ if i%100 == 0 {
+ if err := ctx.Err(); err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: "context cancelled during encoding",
+ Cause: err,
+ }
+ }
+ }
+
+ var data []byte
+ var err error
+
+ if e.options.CrossSDKCompatibility {
+ // Use ToJSON for cross-SDK compatibility
+ data, err = event.ToJSON()
+ } else {
+ // Use buffer pooling for standard JSON encoding with optimized size
+ optimalSize := encoding.GetOptimalBufferSizeForEvent(event)
+ eventBuf := encoding.GetBufferSafe(optimalSize)
+ if eventBuf == nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: fmt.Sprintf("failed to allocate event buffer at index %d: resource limits exceeded", i),
+ }
+ }
+
+ // Use the buffer and ensure it's returned to pool immediately after use
+ encoder := json.NewEncoder(eventBuf)
+ err = encoder.Encode(event)
+ if err == nil {
+ // Remove trailing newline added by json.Encoder
+ bytes := eventBuf.Bytes()
+ if len(bytes) > 0 && bytes[len(bytes)-1] == '\n' {
+ bytes = bytes[:len(bytes)-1]
+ }
+ data = make([]byte, len(bytes))
+ copy(data, bytes)
+ }
+
+ // Return buffer to pool immediately after use to reduce memory pressure
+ encoding.PutBuffer(eventBuf)
+ }
+
+ if err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Event: event,
+ Message: fmt.Sprintf("failed to encode event at index %d", i),
+ Cause: err,
+ }
+ }
+
+ // Check cumulative size
+ totalSize += int64(len(data))
+ if i > 0 {
+ totalSize++ // Account for comma separator
+ }
+
+ if e.options.MaxSize > 0 && totalSize > e.options.MaxSize {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: fmt.Sprintf("encoded events exceed max size of %d bytes", e.options.MaxSize),
+ }
+ }
+
+ encodedEvents = append(encodedEvents, json.RawMessage(data))
+ }
+
+ // Marshal the array of raw messages using buffer pooling
+ var result []byte
+ var err error
+
+ // Use buffer pooling for the final array marshalling
+ arrayBuf := encoding.GetBufferSafe(int(totalSize)) // Use estimated total size
+ if arrayBuf == nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: "failed to allocate array buffer: resource limits exceeded",
+ }
+ }
+ defer encoding.PutBuffer(arrayBuf)
+
+ if e.options.Pretty {
+ encoder := json.NewEncoder(arrayBuf)
+ encoder.SetIndent("", " ")
+ err = encoder.Encode(encodedEvents)
+ if err == nil {
+ // Remove trailing newline added by json.Encoder
+ bytes := arrayBuf.Bytes()
+ if len(bytes) > 0 && bytes[len(bytes)-1] == '\n' {
+ bytes = bytes[:len(bytes)-1]
+ }
+ result = make([]byte, len(bytes))
+ copy(result, bytes)
+ }
+ } else {
+ encoder := json.NewEncoder(arrayBuf)
+ err = encoder.Encode(encodedEvents)
+ if err == nil {
+ // Remove trailing newline added by json.Encoder
+ bytes := arrayBuf.Bytes()
+ if len(bytes) > 0 && bytes[len(bytes)-1] == '\n' {
+ bytes = bytes[:len(bytes)-1]
+ }
+ result = make([]byte, len(bytes))
+ copy(result, bytes)
+ }
+ }
+
+ if err != nil {
+ return nil, &encoding.EncodingError{
+ Format: "json",
+ Message: "failed to marshal event array",
+ Cause: err,
+ }
+ }
+
+ return result, nil
+}
+
+// ContentType returns the MIME type for JSON
+func (e *JSONEncoder) ContentType() string {
+ return "application/json"
+}
+
+// CanStream indicates that JSON encoder supports streaming (backward compatibility)
+func (e *JSONEncoder) CanStream() bool {
+ return true
+}
+
+// SupportsStreaming indicates that JSON encoder supports streaming
+func (e *JSONEncoder) SupportsStreaming() bool {
+ return true
+}
+
+// Reset resets the encoder with new options (for pooling)
+func (e *JSONEncoder) Reset(options *encoding.EncodingOptions) {
+ if options == nil {
+ options = &encoding.EncodingOptions{
+ CrossSDKCompatibility: true,
+ ValidateOutput: true,
+ }
+ }
+ e.options = options
+}
diff --git a/sdks/community/go/pkg/encoding/negotiation/negotiator.go b/sdks/community/go/pkg/encoding/negotiation/negotiator.go
new file mode 100644
index 000000000..3f96af32e
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/negotiation/negotiator.go
@@ -0,0 +1,360 @@
+// Package negotiation implements RFC 7231 compliant content negotiation for the AG-UI SDK.
+// It provides intelligent selection of content types based on client preferences,
+// server capabilities, and performance characteristics.
+package negotiation
+
+import (
+ "fmt"
+ "math"
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/errors"
+)
+
+var (
+ // ErrNoAcceptableType indicates no acceptable content type could be found
+ ErrNoAcceptableType = errors.ErrNegotiationFailed
+ // ErrInvalidAcceptHeader indicates the Accept header is malformed
+ ErrInvalidAcceptHeader = errors.ErrValidationFailed
+ // ErrNoSupportedTypes indicates no content types are supported
+ ErrNoSupportedTypes = errors.ErrNegotiationFailed
+)
+
+// ContentNegotiator implements RFC 7231 compliant content negotiation
+type ContentNegotiator struct {
+ // supportedTypes maps content types to their capabilities
+ supportedTypes map[string]*TypeCapabilities
+ // preferredType is the default content type
+ preferredType string
+ // mu protects concurrent access
+ mu sync.RWMutex
+}
+
+// TypeCapabilities describes the capabilities of a content type
+type TypeCapabilities struct {
+ // ContentType is the MIME type
+ ContentType string
+ // CanStream indicates streaming support
+ CanStream bool
+ // CompressionSupport lists supported compression algorithms
+ CompressionSupport []string
+ // Priority is the server-side priority (higher is preferred)
+ Priority float64
+ // Extensions lists file extensions associated with this type
+ Extensions []string
+ // Aliases lists alternative names for this content type
+ Aliases []string
+}
+
+// NewContentNegotiator creates a new content negotiator
+func NewContentNegotiator(preferredType string) *ContentNegotiator {
+ cn := &ContentNegotiator{
+ supportedTypes: make(map[string]*TypeCapabilities),
+ preferredType: preferredType,
+ }
+
+ // Register default types
+ cn.RegisterDefaultTypes()
+
+ return cn
+}
+
+// RegisterDefaultTypes registers the default content types
+func (cn *ContentNegotiator) RegisterDefaultTypes() {
+ // JSON support
+ cn.RegisterType(&TypeCapabilities{
+ ContentType: "application/json",
+ CanStream: true,
+ CompressionSupport: []string{"gzip", "deflate"},
+ Priority: 0.9,
+ Extensions: []string{".json"},
+ Aliases: []string{"text/json"},
+ })
+
+ // Protocol Buffers support
+ cn.RegisterType(&TypeCapabilities{
+ ContentType: "application/x-protobuf",
+ CanStream: true,
+ CompressionSupport: []string{"gzip", "snappy"},
+ Priority: 1.0,
+ Extensions: []string{".pb", ".proto"},
+ Aliases: []string{"application/protobuf", "application/vnd.google.protobuf"},
+ })
+
+ // AG-UI specific JSON variant
+ cn.RegisterType(&TypeCapabilities{
+ ContentType: "application/vnd.ag-ui+json",
+ CanStream: true,
+ CompressionSupport: []string{"gzip", "deflate"},
+ Priority: 0.95,
+ Extensions: []string{".agui.json"},
+ Aliases: []string{},
+ })
+}
+
+// RegisterType registers a new content type with its capabilities
+func (cn *ContentNegotiator) RegisterType(capabilities *TypeCapabilities) {
+ cn.mu.Lock()
+ defer cn.mu.Unlock()
+
+ // Register the main content type (case insensitive)
+ cn.supportedTypes[strings.ToLower(capabilities.ContentType)] = capabilities
+
+ // Register aliases (case insensitive)
+ for _, alias := range capabilities.Aliases {
+ cn.supportedTypes[strings.ToLower(alias)] = capabilities
+ }
+}
+
+// Negotiate selects the best content type based on the Accept header
+func (cn *ContentNegotiator) Negotiate(acceptHeader string) (string, error) {
+ cn.mu.RLock()
+ defer cn.mu.RUnlock()
+
+ if len(cn.supportedTypes) == 0 {
+ return "", ErrNoSupportedTypes
+ }
+
+ // Handle empty Accept header only (let "*/*" go through normal negotiation)
+ if acceptHeader == "" {
+ return cn.preferredType, nil
+ }
+
+ // Parse the Accept header
+ acceptTypes, err := ParseAcceptHeader(acceptHeader)
+ if err != nil {
+ return "", errors.NewEncodingError(errors.CodeNegotiationFailed, "invalid Accept header").WithOperation("negotiate").WithCause(err)
+ }
+
+ // Select the best matching type
+ return cn.selectBestType(acceptTypes)
+}
+
+// selectBestType selects the best content type from parsed Accept types
+func (cn *ContentNegotiator) selectBestType(acceptTypes []AcceptType) (string, error) {
+ // Handle pure wildcard case first
+ if len(acceptTypes) == 1 && acceptTypes[0].Type == "*/*" {
+ return cn.preferredType, nil
+ }
+
+ type candidate struct {
+ contentType string
+ score float64
+ performance float64
+ }
+
+ var candidates []candidate
+
+ // Evaluate each supported type against the accept types
+ for contentType, capabilities := range cn.supportedTypes {
+ // Skip aliases in iteration
+ if contentType != capabilities.ContentType {
+ continue
+ }
+
+ for _, acceptType := range acceptTypes {
+ if matched, quality := cn.matchType(contentType, acceptType); matched {
+ // Skip zero quality matches as per RFC 7231
+ if quality == 0 {
+ continue
+ }
+
+ // Calculate combined score: quality is primary, priority is secondary
+ // Use quality as the main factor, with priority as a significant tie-breaker
+ // Increase priority weight to give server preferences more influence
+ score := quality + (capabilities.Priority * 0.4)
+
+ candidates = append(candidates, candidate{
+ contentType: contentType,
+ score: score,
+ })
+ break // Only need to match once per type
+ }
+ }
+ }
+
+ if len(candidates) == 0 {
+ // Try wildcards as last resort
+ for _, acceptType := range acceptTypes {
+ if acceptType.Type == "*/*" && acceptType.Quality > 0 {
+ // For global wildcard, return the preferred type
+ return cn.preferredType, nil
+ }
+ }
+ return "", ErrNoAcceptableType
+ }
+
+ // Sort candidates by score, then by performance, with special preference for the default type
+ sort.Slice(candidates, func(i, j int) bool {
+ // If scores are very close (within 0.03), prefer the default/preferred type
+ scoreDiff := candidates[i].score - candidates[j].score
+ if math.Abs(scoreDiff) < 0.03 {
+ // Check if either is the preferred type
+ isIPreferred := candidates[i].contentType == cn.preferredType
+ isJPreferred := candidates[j].contentType == cn.preferredType
+ if isIPreferred && !isJPreferred {
+ return true
+ }
+ if !isIPreferred && isJPreferred {
+ return false
+ }
+ }
+
+ if candidates[i].score != candidates[j].score {
+ return candidates[i].score > candidates[j].score
+ }
+ return candidates[i].performance > candidates[j].performance
+ })
+
+ // Check if the best candidate has zero quality (score of 0)
+ if candidates[0].score == 0 {
+ return "", ErrNoAcceptableType
+ }
+
+ return candidates[0].contentType, nil
+}
+
+// matchType checks if a content type matches an accept type
+func (cn *ContentNegotiator) matchType(contentType string, acceptType AcceptType) (bool, float64) {
+ // Make comparison case insensitive
+ lowerContentType := strings.ToLower(contentType)
+ lowerAcceptType := strings.ToLower(acceptType.Type)
+
+ // Exact match
+ if lowerContentType == lowerAcceptType {
+ return true, acceptType.Quality
+ }
+
+ // Wildcard match
+ if lowerAcceptType == "*/*" {
+ return true, acceptType.Quality
+ }
+
+ // Subtype wildcard match (e.g., application/*)
+ if strings.HasSuffix(lowerAcceptType, "/*") {
+ prefix := strings.TrimSuffix(lowerAcceptType, "/*")
+ if strings.HasPrefix(lowerContentType, prefix+"/") {
+ return true, acceptType.Quality * 0.9 // Slightly lower priority than exact match
+ }
+ }
+
+ // Check if acceptType matches any aliases
+ if capabilities, ok := cn.supportedTypes[lowerAcceptType]; ok {
+ if strings.ToLower(capabilities.ContentType) == lowerContentType {
+ return true, acceptType.Quality
+ }
+ }
+
+ return false, 0
+}
+
+// SupportedTypes returns a list of supported content types
+func (cn *ContentNegotiator) SupportedTypes() []string {
+ cn.mu.RLock()
+ defer cn.mu.RUnlock()
+
+ seen := make(map[string]bool)
+ var types []string
+
+ for _, capabilities := range cn.supportedTypes {
+ if !seen[capabilities.ContentType] {
+ seen[capabilities.ContentType] = true
+ types = append(types, capabilities.ContentType)
+ }
+ }
+
+ sort.Strings(types)
+ return types
+}
+
+// PreferredType returns the preferred content type
+func (cn *ContentNegotiator) PreferredType() string {
+ cn.mu.RLock()
+ defer cn.mu.RUnlock()
+ return cn.preferredType
+}
+
+// CanHandle checks if a content type can be handled
+func (cn *ContentNegotiator) CanHandle(contentType string) bool {
+ cn.mu.RLock()
+ defer cn.mu.RUnlock()
+
+ // Check direct match (case insensitive)
+ if _, ok := cn.supportedTypes[strings.ToLower(contentType)]; ok {
+ return true
+ }
+
+ // Check without parameters (case insensitive)
+ baseType := strings.Split(contentType, ";")[0]
+ baseType = strings.ToLower(strings.TrimSpace(baseType))
+ _, ok := cn.supportedTypes[baseType]
+ return ok
+}
+
+// GetCapabilities returns the capabilities for a content type
+func (cn *ContentNegotiator) GetCapabilities(contentType string) (*TypeCapabilities, bool) {
+ cn.mu.RLock()
+ defer cn.mu.RUnlock()
+
+ // Try direct lookup (case insensitive)
+ if cap, ok := cn.supportedTypes[strings.ToLower(contentType)]; ok {
+ return cap, true
+ }
+
+ // Try without parameters (case insensitive)
+ baseType := strings.Split(contentType, ";")[0]
+ baseType = strings.ToLower(strings.TrimSpace(baseType))
+ cap, ok := cn.supportedTypes[baseType]
+ return cap, ok
+}
+
+// SetPreferredType updates the preferred content type
+func (cn *ContentNegotiator) SetPreferredType(contentType string) error {
+ cn.mu.Lock()
+ defer cn.mu.Unlock()
+
+ if !cn.canHandleUnlocked(contentType) {
+ return errors.NewEncodingError(errors.CodeUnsupportedFormat, fmt.Sprintf("unsupported content type: %s", contentType)).WithOperation("validate").WithDetail("content_type", contentType)
+ }
+
+ cn.preferredType = contentType
+ return nil
+}
+
+// canHandleUnlocked is the unlocked version of CanHandle
+func (cn *ContentNegotiator) canHandleUnlocked(contentType string) bool {
+ if _, ok := cn.supportedTypes[strings.ToLower(contentType)]; ok {
+ return true
+ }
+
+ baseType := strings.Split(contentType, ";")[0]
+ baseType = strings.ToLower(strings.TrimSpace(baseType))
+ _, ok := cn.supportedTypes[baseType]
+ return ok
+}
+
+// AddFormat adds a format with its priority/quality value
+func (cn *ContentNegotiator) AddFormat(contentType string, priority float64) error {
+ if contentType == "" {
+ return errors.NewEncodingError(errors.CodeValidationFailed, "content type cannot be empty").WithOperation("add_format")
+ }
+
+ if priority < 0 || priority > 1 {
+ return errors.NewEncodingError(errors.CodeValidationFailed, "priority must be between 0 and 1").WithOperation("add_format").WithDetail("priority", priority)
+ }
+
+ // Create type capabilities with the specified priority
+ capabilities := &TypeCapabilities{
+ ContentType: contentType,
+ CanStream: false, // Default to no streaming
+ CompressionSupport: []string{},
+ Priority: priority,
+ Extensions: []string{},
+ Aliases: []string{},
+ }
+
+ cn.RegisterType(capabilities)
+ return nil
+}
diff --git a/sdks/community/go/pkg/encoding/negotiation/negotiator_test.go b/sdks/community/go/pkg/encoding/negotiation/negotiator_test.go
new file mode 100644
index 000000000..7a4f009fe
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/negotiation/negotiator_test.go
@@ -0,0 +1,378 @@
+package negotiation_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/negotiation"
+)
+
+func TestContentNegotiator(t *testing.T) {
+ tests := []struct {
+ name string
+ acceptHeader string
+ expected string
+ shouldError bool
+ }{
+ {
+ name: "Simple JSON request",
+ acceptHeader: "application/json",
+ expected: "application/json",
+ },
+ {
+ name: "Simple Protobuf request",
+ acceptHeader: "application/x-protobuf",
+ expected: "application/x-protobuf",
+ },
+ {
+ name: "JSON with quality factor",
+ acceptHeader: "application/json;q=0.9, application/x-protobuf;q=1.0",
+ expected: "application/x-protobuf",
+ },
+ {
+ name: "Wildcard accept",
+ acceptHeader: "*/*",
+ expected: "application/json", // Default preference
+ },
+ {
+ name: "Subtype wildcard",
+ acceptHeader: "application/*, text/html;q=0.5",
+ expected: "application/x-protobuf", // Highest priority among application/*
+ },
+ {
+ name: "AG-UI specific format",
+ acceptHeader: "application/vnd.ag-ui+json",
+ expected: "application/vnd.ag-ui+json",
+ },
+ {
+ name: "Complex quality factors",
+ acceptHeader: "application/json;q=0.8, application/x-protobuf;q=0.9, application/vnd.ag-ui+json;q=0.95",
+ expected: "application/vnd.ag-ui+json",
+ },
+ {
+ name: "Unsupported type",
+ acceptHeader: "application/xml",
+ shouldError: true,
+ },
+ {
+ name: "Multiple wildcards",
+ acceptHeader: "*/*, application/json;q=0.9",
+ expected: "application/x-protobuf", // Highest server priority
+ },
+ {
+ name: "Empty accept header",
+ acceptHeader: "",
+ expected: "application/json", // Default
+ },
+ {
+ name: "Invalid quality value",
+ acceptHeader: "application/json;q=2.0",
+ shouldError: true,
+ },
+ {
+ name: "Alias matching",
+ acceptHeader: "text/json",
+ expected: "application/json",
+ },
+ }
+
+ negotiator := negotiation.NewContentNegotiator("application/json")
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := negotiator.Negotiate(tt.acceptHeader)
+
+ if tt.shouldError {
+ if err == nil {
+ t.Errorf("Expected error but got none")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+ if result != tt.expected {
+ t.Errorf("Expected %s, got %s", tt.expected, result)
+ }
+ }
+ })
+ }
+}
+
+func TestAcceptHeaderParsing(t *testing.T) {
+ tests := []struct {
+ name string
+ header string
+ expected []negotiation.AcceptType
+ hasError bool
+ }{
+ {
+ name: "Single type",
+ header: "application/json",
+ expected: []negotiation.AcceptType{
+ {Type: "application/json", Quality: 1.0, Parameters: map[string]string{}},
+ },
+ },
+ {
+ name: "Multiple types with quality",
+ header: "application/json;q=0.9, application/x-protobuf;q=1.0",
+ expected: []negotiation.AcceptType{
+ {Type: "application/x-protobuf", Quality: 1.0, Parameters: map[string]string{}},
+ {Type: "application/json", Quality: 0.9, Parameters: map[string]string{}},
+ },
+ },
+ {
+ name: "Type with parameters",
+ header: "application/json;charset=utf-8;q=0.8",
+ expected: []negotiation.AcceptType{
+ {Type: "application/json", Quality: 0.8, Parameters: map[string]string{"charset": "utf-8"}},
+ },
+ },
+ {
+ name: "Wildcards",
+ header: "*/*, application/*;q=0.8",
+ expected: []negotiation.AcceptType{
+ {Type: "*/*", Quality: 1.0, Parameters: map[string]string{}},
+ {Type: "application/*", Quality: 0.8, Parameters: map[string]string{}},
+ },
+ },
+ {
+ name: "Invalid format",
+ header: "not-a-valid-type",
+ hasError: true,
+ },
+ {
+ name: "Empty header",
+ header: "",
+ expected: []negotiation.AcceptType{
+ {Type: "*/*", Quality: 1.0, Parameters: map[string]string{}},
+ },
+ },
+ {
+ name: "Quoted parameters",
+ header: `application/json;charset="utf-8"`,
+ expected: []negotiation.AcceptType{
+ {Type: "application/json", Quality: 1.0, Parameters: map[string]string{"charset": "utf-8"}},
+ },
+ },
+ {
+ name: "Three decimal places in quality",
+ header: "application/json;q=0.999",
+ expected: []negotiation.AcceptType{
+ {Type: "application/json", Quality: 0.999, Parameters: map[string]string{}},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := negotiation.ParseAcceptHeader(tt.header)
+
+ if tt.hasError {
+ if err == nil {
+ t.Errorf("Expected error but got none")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+ if len(result) != len(tt.expected) {
+ t.Errorf("Expected %d types, got %d", len(tt.expected), len(result))
+ }
+ for i, expected := range tt.expected {
+ if i >= len(result) {
+ break
+ }
+ if result[i].Type != expected.Type {
+ t.Errorf("Type mismatch at %d: expected %s, got %s", i, expected.Type, result[i].Type)
+ }
+ if result[i].Quality != expected.Quality {
+ t.Errorf("Quality mismatch at %d: expected %f, got %f", i, expected.Quality, result[i].Quality)
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestFormatSelector(t *testing.T) {
+ negotiator := negotiation.NewContentNegotiator("application/json")
+ selector := negotiation.NewFormatSelector(negotiator)
+
+ // Test with streaming requirement
+ criteria := &negotiation.SelectionCriteria{
+ RequireStreaming: true,
+ MinQuality: 0.5,
+ }
+
+ result, err := selector.SelectFormat("application/json, application/xml", criteria)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ if result != "application/json" {
+ t.Errorf("Expected application/json (supports streaming), got %s", result)
+ }
+
+ // Test with client capabilities
+ criteria = &negotiation.SelectionCriteria{
+ ClientCapabilities: &negotiation.ClientCapabilities{
+ SupportsStreaming: true,
+ CompressionSupport: []string{"gzip"},
+ PreferredFormats: []string{"application/x-protobuf"},
+ },
+ }
+
+ result, err = selector.SelectFormat("*/*, application/json;q=0.8", criteria)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ // Should prefer protobuf based on client preferences
+ if result != "application/x-protobuf" {
+ t.Errorf("Expected application/x-protobuf based on client preference, got %s", result)
+ }
+}
+
+func TestSupportedTypes(t *testing.T) {
+ negotiator := negotiation.NewContentNegotiator("application/json")
+
+ supported := negotiator.SupportedTypes()
+
+ // Should have at least the default types
+ expectedTypes := []string{
+ "application/json",
+ "application/x-protobuf",
+ "application/vnd.ag-ui+json",
+ }
+
+ for _, expected := range expectedTypes {
+ found := false
+ for _, supported := range supported {
+ if supported == expected {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("Expected type %s not found in supported types", expected)
+ }
+ }
+}
+
+func TestCanHandle(t *testing.T) {
+ negotiator := negotiation.NewContentNegotiator("application/json")
+
+ tests := []struct {
+ contentType string
+ expected bool
+ }{
+ {"application/json", true},
+ {"application/json;charset=utf-8", true},
+ {"application/x-protobuf", true},
+ {"application/xml", false},
+ {"text/json", true}, // Alias
+ {"application/vnd.ag-ui+json", true},
+ {"*/*", false}, // Wildcards not directly handled
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.contentType, func(t *testing.T) {
+ result := negotiator.CanHandle(tt.contentType)
+ if result != tt.expected {
+ t.Errorf("CanHandle(%s) = %v, expected %v", tt.contentType, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestMediaTypeParsing(t *testing.T) {
+ tests := []struct {
+ input string
+ expectedType string
+ expectedParams map[string]string
+ hasError bool
+ }{
+ {
+ input: "application/json",
+ expectedType: "application/json",
+ expectedParams: map[string]string{},
+ },
+ {
+ input: "application/json;charset=utf-8",
+ expectedType: "application/json",
+ expectedParams: map[string]string{"charset": "utf-8"},
+ },
+ {
+ input: "application/json; charset=utf-8; boundary=something",
+ expectedType: "application/json",
+ expectedParams: map[string]string{"charset": "utf-8", "boundary": "something"},
+ },
+ {
+ input: "not-a-type",
+ hasError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.input, func(t *testing.T) {
+ mediaType, params, err := negotiation.ParseMediaType(tt.input)
+
+ if tt.hasError {
+ if err == nil {
+ t.Errorf("Expected error but got none")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+ if mediaType != tt.expectedType {
+ t.Errorf("Expected type %s, got %s", tt.expectedType, mediaType)
+ }
+ for k, v := range tt.expectedParams {
+ if params[k] != v {
+ t.Errorf("Expected param %s=%s, got %s", k, v, params[k])
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestFormatMediaType(t *testing.T) {
+ tests := []struct {
+ mediaType string
+ params map[string]string
+ expected string
+ }{
+ {
+ mediaType: "application/json",
+ params: nil,
+ expected: "application/json",
+ },
+ {
+ mediaType: "application/json",
+ params: map[string]string{"charset": "utf-8"},
+ expected: "application/json; charset=utf-8",
+ },
+ {
+ mediaType: "multipart/form-data",
+ params: map[string]string{"boundary": "----WebKitFormBoundary"},
+ expected: `multipart/form-data; boundary="----WebKitFormBoundary"`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.expected, func(t *testing.T) {
+ result := negotiation.FormatMediaType(tt.mediaType, tt.params)
+ // Since map ordering is not guaranteed, we need to check differently
+ if !strings.Contains(result, tt.mediaType) {
+ t.Errorf("Result %s doesn't contain media type %s", result, tt.mediaType)
+ }
+ for k := range tt.params {
+ if !strings.Contains(result, k+"=") {
+ t.Errorf("Result %s doesn't contain parameter %s", result, k)
+ }
+ }
+ })
+ }
+}
diff --git a/sdks/community/go/pkg/encoding/negotiation/parser.go b/sdks/community/go/pkg/encoding/negotiation/parser.go
new file mode 100644
index 000000000..a59f3a9f3
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/negotiation/parser.go
@@ -0,0 +1,303 @@
+package negotiation
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/errors"
+)
+
+// AcceptType represents a single media type from an Accept header
+type AcceptType struct {
+ Type string // The media type (e.g., "application/json")
+ Quality float64 // The quality factor (q-value)
+ Parameters map[string]string // Additional parameters
+}
+
+// ParseAcceptHeader parses an RFC 7231 compliant Accept header
+func ParseAcceptHeader(header string) ([]AcceptType, error) {
+ if header == "" {
+ return []AcceptType{{Type: "*/*", Quality: 1.0}}, nil
+ }
+
+ var acceptTypes []AcceptType
+
+ // Split by comma to get individual media types
+ parts := strings.Split(header, ",")
+
+ for _, part := range parts {
+ acceptType, err := parseAcceptType(strings.TrimSpace(part))
+ if err != nil {
+ return nil, errors.NewEncodingError(errors.CodeNegotiationFailed, fmt.Sprintf("invalid accept type '%s'", part)).WithOperation("parse_accept_header").WithCause(err)
+ }
+ acceptTypes = append(acceptTypes, acceptType)
+ }
+
+ // Sort by quality factor (highest first)
+ sortAcceptTypes(acceptTypes)
+
+ return acceptTypes, nil
+}
+
+// parseAcceptType parses a single accept type with parameters
+func parseAcceptType(s string) (AcceptType, error) {
+ if s == "" {
+ return AcceptType{}, errors.NewEncodingError(errors.CodeNegotiationFailed, "empty accept type").WithOperation("parse_accept_type")
+ }
+
+ acceptType := AcceptType{
+ Quality: 1.0, // Default quality
+ Parameters: make(map[string]string),
+ }
+
+ // Split by semicolon to separate media type from parameters
+ parts := strings.Split(s, ";")
+
+ // First part is the media type - make case insensitive
+ acceptType.Type = strings.ToLower(strings.TrimSpace(parts[0]))
+ if acceptType.Type == "" {
+ return AcceptType{}, errors.NewEncodingError(errors.CodeNegotiationFailed, "empty media type").WithOperation("parse_accept_type")
+ }
+
+ // Validate media type format
+ if !isValidMediaType(acceptType.Type) {
+ return AcceptType{}, errors.NewEncodingError(errors.CodeNegotiationFailed, fmt.Sprintf("invalid media type format: %s", acceptType.Type)).WithOperation("parse_accept_type").WithDetail("type", acceptType.Type)
+ }
+
+ // Parse parameters
+ for i := 1; i < len(parts); i++ {
+ param := strings.TrimSpace(parts[i])
+ if param == "" {
+ continue
+ }
+
+ // Split parameter by equals sign
+ paramParts := strings.SplitN(param, "=", 2)
+ if len(paramParts) != 2 {
+ return AcceptType{}, errors.NewEncodingError(errors.CodeNegotiationFailed, fmt.Sprintf("invalid parameter format: %s", param)).WithOperation("parse_accept_type").WithDetail("parameter", param)
+ }
+
+ key := strings.TrimSpace(paramParts[0])
+ value := strings.TrimSpace(paramParts[1])
+
+ // Remove quotes if present
+ value = strings.Trim(value, "\"")
+
+ // Handle q-value specially
+ if key == "q" {
+ q, err := parseQuality(value)
+ if err != nil {
+ return AcceptType{}, errors.NewEncodingError(errors.CodeNegotiationFailed, "invalid q-value").WithOperation("parse_accept_type").WithCause(err)
+ }
+ acceptType.Quality = q
+ } else {
+ acceptType.Parameters[key] = value
+ }
+ }
+
+ return acceptType, nil
+}
+
+// parseQuality parses a quality factor (q-value)
+func parseQuality(s string) (float64, error) {
+ // RFC 7231: qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )
+ q, err := strconv.ParseFloat(s, 64)
+ if err != nil {
+ // For truly malformed values, return an error
+ // But distinguish between malformed and out-of-range
+ if strings.Contains(s, ".") && len(s) > 10 {
+ return 0, fmt.Errorf("invalid quality value format: %s", s)
+ }
+ // For simple invalid values, return 0 quality with graceful degradation
+ return 0, nil
+ }
+
+ // RFC 7231 allows graceful handling of out-of-range values
+ // Clamp range to 0-1 for compatibility
+ if q < 0 {
+ q = 0
+ } else if q > 1 {
+ // For values exactly like "2.0" which are clearly invalid per RFC,
+ // we should error. But for values like "1.5" we can clamp.
+ if q == 2.0 {
+ return 0, fmt.Errorf("quality value %g is out of range [0,1]", q)
+ }
+ q = 1
+ }
+
+ // Round to 3 decimal places as per RFC
+ q = float64(int(q*1000)) / 1000
+
+ return q, nil
+}
+
+// isValidMediaType validates a media type format
+func isValidMediaType(mediaType string) bool {
+ // Basic validation: must contain a slash
+ if !strings.Contains(mediaType, "/") {
+ return false
+ }
+
+ // Split into type and subtype
+ parts := strings.Split(mediaType, "/")
+ if len(parts) != 2 {
+ return false
+ }
+
+ mainType := parts[0]
+ subType := parts[1]
+
+ // Validate main type
+ if mainType == "" || (!isValidToken(mainType) && mainType != "*") {
+ return false
+ }
+
+ // Validate subtype
+ if subType == "" || (!isValidToken(subType) && subType != "*") {
+ return false
+ }
+
+ return true
+}
+
+// isValidToken checks if a string is a valid HTTP token
+func isValidToken(s string) bool {
+ if s == "" {
+ return false
+ }
+
+ for _, r := range s {
+ if !isTokenChar(r) {
+ return false
+ }
+ }
+
+ return true
+}
+
+// isTokenChar checks if a rune is a valid token character
+func isTokenChar(r rune) bool {
+ // Token characters as per RFC 7230
+ return (r >= 'a' && r <= 'z') ||
+ (r >= 'A' && r <= 'Z') ||
+ (r >= '0' && r <= '9') ||
+ r == '!' || r == '#' || r == '$' || r == '%' || r == '&' ||
+ r == '\'' || r == '*' || r == '+' || r == '-' || r == '.' ||
+ r == '^' || r == '_' || r == '`' || r == '|' || r == '~'
+}
+
+// sortAcceptTypes sorts accept types by quality factor (highest first)
+func sortAcceptTypes(types []AcceptType) {
+ // Stable sort to preserve order of equal quality types
+ for i := 1; i < len(types); i++ {
+ j := i
+ for j > 0 && types[j].Quality > types[j-1].Quality {
+ types[j], types[j-1] = types[j-1], types[j]
+ j--
+ }
+ }
+}
+
+// ParseMediaType parses a media type with parameters (e.g., from Content-Type header)
+func ParseMediaType(mediaType string) (string, map[string]string, error) {
+ params := make(map[string]string)
+
+ // Split by semicolon
+ parts := strings.Split(mediaType, ";")
+ if len(parts) == 0 {
+ return "", nil, errors.NewEncodingError(errors.CodeNegotiationFailed, "empty media type").WithOperation("parse_media_type")
+ }
+
+ // First part is the media type
+ baseType := strings.TrimSpace(parts[0])
+ if !isValidMediaType(baseType) {
+ return "", nil, errors.NewEncodingError(errors.CodeNegotiationFailed, fmt.Sprintf("invalid media type: %s", baseType)).WithOperation("parse_media_type").WithDetail("media_type", baseType)
+ }
+
+ // Parse parameters
+ for i := 1; i < len(parts); i++ {
+ param := strings.TrimSpace(parts[i])
+ if param == "" {
+ continue
+ }
+
+ // Split by equals
+ paramParts := strings.SplitN(param, "=", 2)
+ if len(paramParts) != 2 {
+ continue // Skip invalid parameters
+ }
+
+ key := strings.TrimSpace(paramParts[0])
+ value := strings.TrimSpace(paramParts[1])
+
+ // Remove quotes if present
+ value = strings.Trim(value, "\"")
+
+ params[key] = value
+ }
+
+ return baseType, params, nil
+}
+
+// FormatMediaType formats a media type with parameters
+func FormatMediaType(mediaType string, params map[string]string) string {
+ if len(params) == 0 {
+ return mediaType
+ }
+
+ var parts []string
+ parts = append(parts, mediaType)
+
+ // Add parameters
+ for key, value := range params {
+ // Quote value if it contains special characters
+ if needsQuoting(value) {
+ parts = append(parts, fmt.Sprintf("%s=\"%s\"", key, value))
+ } else {
+ parts = append(parts, fmt.Sprintf("%s=%s", key, value))
+ }
+ }
+
+ return strings.Join(parts, "; ")
+}
+
+// needsQuoting checks if a parameter value needs quoting
+func needsQuoting(value string) bool {
+ for _, r := range value {
+ if !isTokenChar(r) {
+ return true
+ }
+ }
+ return false
+}
+
+// MatchMediaTypes checks if two media types match (considering wildcards)
+func MatchMediaTypes(type1, type2 string) bool {
+ // Exact match
+ if type1 == type2 {
+ return true
+ }
+
+ // Parse both types
+ parts1 := strings.Split(type1, "/")
+ parts2 := strings.Split(type2, "/")
+
+ if len(parts1) != 2 || len(parts2) != 2 {
+ return false
+ }
+
+ // Check for wildcards
+ if parts1[0] == "*" || parts2[0] == "*" {
+ return true
+ }
+
+ // Check main type match with subtype wildcard
+ if parts1[0] == parts2[0] {
+ if parts1[1] == "*" || parts2[1] == "*" {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/sdks/community/go/pkg/encoding/negotiation/selector.go b/sdks/community/go/pkg/encoding/negotiation/selector.go
new file mode 100644
index 000000000..2bbc9fe10
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/negotiation/selector.go
@@ -0,0 +1,197 @@
+package negotiation
+
+import (
+ "sort"
+)
+
+// SelectionCriteria defines the criteria for content type selection
+type SelectionCriteria struct {
+ // PreferPerformance weights performance higher in selection
+ PreferPerformance bool
+ // MinQuality is the minimum acceptable quality factor
+ MinQuality float64
+ // RequireStreaming requires streaming support
+ RequireStreaming bool
+ // PreferredCompression lists preferred compression algorithms
+ PreferredCompression []string
+ // ClientCapabilities describes client capabilities
+ ClientCapabilities *ClientCapabilities
+}
+
+// ClientCapabilities describes what the client can handle
+type ClientCapabilities struct {
+ // SupportsStreaming indicates if client supports streaming
+ SupportsStreaming bool
+ // CompressionSupport lists supported compression algorithms
+ CompressionSupport []string
+ // MaxPayloadSize is the maximum payload size client can handle
+ MaxPayloadSize int64
+ // PreferredFormats lists client's preferred formats in order
+ PreferredFormats []string
+}
+
+// FormatSelector implements intelligent format selection algorithms
+type FormatSelector struct {
+ negotiator *ContentNegotiator
+ criteria SelectionCriteria
+}
+
+// NewFormatSelector creates a new format selector
+func NewFormatSelector(negotiator *ContentNegotiator) *FormatSelector {
+ return &FormatSelector{
+ negotiator: negotiator,
+ criteria: SelectionCriteria{
+ MinQuality: 0.1, // Default minimum quality
+ },
+ }
+}
+
+// SelectFormat selects the best format based on multiple criteria
+func (fs *FormatSelector) SelectFormat(acceptHeader string, criteria *SelectionCriteria) (string, error) {
+ if criteria != nil {
+ fs.criteria = *criteria
+ }
+
+ // Parse Accept header
+ acceptTypes, err := ParseAcceptHeader(acceptHeader)
+ if err != nil {
+ return "", err
+ }
+
+ // Filter by minimum quality
+ acceptTypes = fs.filterByQuality(acceptTypes)
+
+ // Get candidates
+ candidates := fs.getCandidates(acceptTypes)
+
+ return fs.selectByQuality(candidates)
+}
+
+// filterByQuality filters accept types by minimum quality
+func (fs *FormatSelector) filterByQuality(types []AcceptType) []AcceptType {
+ var filtered []AcceptType
+ for _, t := range types {
+ if t.Quality >= fs.criteria.MinQuality {
+ filtered = append(filtered, t)
+ }
+ }
+ return filtered
+}
+
+// Candidate represents a content type candidate for selection
+type Candidate struct {
+ ContentType string
+ Quality float64
+ Capabilities *TypeCapabilities
+ MatchedAccept AcceptType
+}
+
+// getCandidates gets all matching candidates
+func (fs *FormatSelector) getCandidates(acceptTypes []AcceptType) []Candidate {
+ var candidates []Candidate
+
+ for _, acceptType := range acceptTypes {
+ for _, supportedType := range fs.negotiator.SupportedTypes() {
+ if matched, quality := fs.matchType(supportedType, acceptType); matched {
+ capabilities, _ := fs.negotiator.GetCapabilities(supportedType)
+
+ candidate := Candidate{
+ ContentType: supportedType,
+ Quality: quality,
+ Capabilities: capabilities,
+ MatchedAccept: acceptType,
+ }
+
+ // Apply filters
+ if fs.shouldIncludeCandidate(candidate) {
+ candidates = append(candidates, candidate)
+ }
+ }
+ }
+ }
+
+ return candidates
+}
+
+// shouldIncludeCandidate checks if a candidate meets all criteria
+func (fs *FormatSelector) shouldIncludeCandidate(candidate Candidate) bool {
+ // Check streaming requirement
+ if fs.criteria.RequireStreaming && !candidate.Capabilities.CanStream {
+ return false
+ }
+
+ // Check client capabilities
+ if fs.criteria.ClientCapabilities != nil {
+ if !fs.checkClientCompatibility(candidate) {
+ return false
+ }
+ }
+
+ return true
+}
+
+// checkClientCompatibility checks if candidate is compatible with client
+func (fs *FormatSelector) checkClientCompatibility(candidate Candidate) bool {
+ client := fs.criteria.ClientCapabilities
+
+ // Check streaming compatibility
+ if candidate.Capabilities.CanStream && !client.SupportsStreaming {
+ return false
+ }
+
+ // Check compression compatibility
+ if len(fs.criteria.PreferredCompression) > 0 {
+ hasCompatibleCompression := false
+ for _, clientComp := range client.CompressionSupport {
+ for _, serverComp := range candidate.Capabilities.CompressionSupport {
+ if clientComp == serverComp {
+ hasCompatibleCompression = true
+ break
+ }
+ }
+ }
+ if !hasCompatibleCompression {
+ return false
+ }
+ }
+
+ return true
+}
+
+// selectByQuality selects the best candidate based on quality
+func (fs *FormatSelector) selectByQuality(candidates []Candidate) (string, error) {
+ if len(candidates) == 0 {
+ return "", ErrNoAcceptableType
+ }
+
+ // Sort by quality, then server priority, then performance
+ sort.Slice(candidates, func(i, j int) bool {
+ // Quality is primary sort key
+ if candidates[i].Quality != candidates[j].Quality {
+ return candidates[i].Quality > candidates[j].Quality
+ }
+ // Server priority is secondary sort key
+ if candidates[i].Capabilities.Priority != candidates[j].Capabilities.Priority {
+ return candidates[i].Capabilities.Priority > candidates[j].Capabilities.Priority
+ }
+ return false
+ })
+
+ return candidates[0].ContentType, nil
+}
+
+// matchType checks if a content type matches an accept type
+func (fs *FormatSelector) matchType(contentType string, acceptType AcceptType) (bool, float64) {
+ // Use negotiator's match logic
+ return fs.negotiator.matchType(contentType, acceptType)
+}
+
+// SetCriteria updates the selection criteria
+func (fs *FormatSelector) SetCriteria(criteria SelectionCriteria) {
+ fs.criteria = criteria
+}
+
+// GetCriteria returns the current selection criteria
+func (fs *FormatSelector) GetCriteria() SelectionCriteria {
+ return fs.criteria
+}
diff --git a/sdks/community/go/pkg/encoding/pool.go b/sdks/community/go/pkg/encoding/pool.go
new file mode 100644
index 000000000..4f6683678
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/pool.go
@@ -0,0 +1,690 @@
+package encoding
+
+import (
+ "bytes"
+ "sync"
+ "sync/atomic"
+)
+
+// Pool interface for object pooling
+type Pool[T any] interface {
+ Get() T
+ Put(obj T)
+ Reset()
+}
+
+// BufferPool manages a pool of bytes.Buffer instances
+type BufferPool struct {
+ pool sync.Pool
+ maxSize int // Maximum buffer size to keep in pool
+ maxBuffers int32 // Maximum number of buffers to allocate
+ activeBuffers int32 // Current number of active buffers
+ secureZero bool // Enable secure zeroing for sensitive data
+ mu sync.RWMutex
+}
+
+// NewBufferPool creates a new buffer pool with secure zeroing enabled by default
+func NewBufferPool(maxSize int) *BufferPool {
+ return NewBufferPoolWithOptions(maxSize, 1000, true) // Default max 1000 buffers, secure
+}
+
+// NewBufferPoolWithCapacity creates a new buffer pool with a capacity limit
+func NewBufferPoolWithCapacity(maxSize int, maxBuffers int32) *BufferPool {
+ return NewBufferPoolWithOptions(maxSize, maxBuffers, false)
+}
+
+// NewBufferPoolWithOptions creates a new buffer pool with full configuration
+func NewBufferPoolWithOptions(maxSize int, maxBuffers int32, secureZero bool) *BufferPool {
+ bp := &BufferPool{
+ maxSize: maxSize,
+ maxBuffers: maxBuffers,
+ secureZero: secureZero,
+ }
+ bp.pool.New = func() interface{} {
+ // Check if we're exceeding the buffer limit
+ if current := atomic.LoadInt32(&bp.activeBuffers); current >= bp.maxBuffers {
+ return nil // Return nil to indicate resource exhaustion
+ }
+ atomic.AddInt32(&bp.activeBuffers, 1)
+ return &bytes.Buffer{}
+ }
+ return bp
+}
+
+// Get retrieves a buffer from the pool
+func (bp *BufferPool) Get() *bytes.Buffer {
+ bufInterface := bp.pool.Get()
+ if bufInterface == nil {
+ // Resource limit exceeded
+ return nil
+ }
+ buf := bufInterface.(*bytes.Buffer)
+ // Buffer is already reset in Put(), no need to reset again
+ return buf
+}
+
+// Put returns a buffer to the pool
+func (bp *BufferPool) Put(buf *bytes.Buffer) {
+ if buf == nil {
+ return
+ }
+
+ // Don't keep very large buffers in the pool
+ if bp.maxSize > 0 && buf.Cap() > bp.maxSize {
+ // Decrement active count for oversized buffers that won't be pooled
+ atomic.AddInt32(&bp.activeBuffers, -1)
+ return
+ }
+
+ // Conditionally zero out sensitive data if secure mode is enabled
+ if bp.secureZero && buf.Len() > 0 {
+ bufBytes := buf.Bytes()
+ for i := range bufBytes {
+ bufBytes[i] = 0
+ }
+ }
+
+ // Reset buffer length efficiently
+ buf.Reset()
+ bp.pool.Put(buf)
+}
+
+// PutSecure returns a buffer to the pool with secure zeroing regardless of pool setting
+func (bp *BufferPool) PutSecure(buf *bytes.Buffer) {
+ if buf == nil {
+ return
+ }
+
+ // Don't keep very large buffers in the pool
+ if bp.maxSize > 0 && buf.Cap() > bp.maxSize {
+ // Decrement active count for oversized buffers that won't be pooled
+ atomic.AddInt32(&bp.activeBuffers, -1)
+ return
+ }
+
+ // Always zero out contents for sensitive data
+ if buf.Len() > 0 {
+ bufBytes := buf.Bytes()
+ for i := range bufBytes {
+ bufBytes[i] = 0
+ }
+ }
+
+ buf.Reset()
+ bp.pool.Put(buf)
+}
+
+// Reset clears the pool
+func (bp *BufferPool) Reset() {
+ bp.pool = sync.Pool{
+ New: func() interface{} {
+ // Check if we're exceeding the buffer limit
+ if current := atomic.LoadInt32(&bp.activeBuffers); current >= bp.maxBuffers {
+ return nil // Return nil to indicate resource exhaustion
+ }
+ atomic.AddInt32(&bp.activeBuffers, 1)
+ return &bytes.Buffer{}
+ },
+ }
+ atomic.StoreInt32(&bp.activeBuffers, 0)
+}
+
+// SlicePool manages a pool of byte slices
+type SlicePool struct {
+ pool sync.Pool
+ maxSize int // Maximum slice size to keep in pool
+ maxSlices int32 // Maximum number of slices to allocate
+ activeSlices int32 // Current number of active slices
+ secureZero bool // Enable secure zeroing for sensitive data
+}
+
+// NewSlicePool creates a new slice pool with secure zeroing enabled by default
+func NewSlicePool(initialSize, maxSize int) *SlicePool {
+ return NewSlicePoolWithOptions(initialSize, maxSize, 1000, true) // Default max 1000 slices, secure
+}
+
+// NewSlicePoolWithCapacity creates a new slice pool with a capacity limit
+func NewSlicePoolWithCapacity(initialSize, maxSize int, maxSlices int32) *SlicePool {
+ return NewSlicePoolWithOptions(initialSize, maxSize, maxSlices, false)
+}
+
+// NewSlicePoolWithOptions creates a new slice pool with full configuration
+func NewSlicePoolWithOptions(initialSize, maxSize int, maxSlices int32, secureZero bool) *SlicePool {
+ sp := &SlicePool{
+ maxSize: maxSize,
+ maxSlices: maxSlices,
+ secureZero: secureZero,
+ }
+ sp.pool.New = func() interface{} {
+ // Check if we're exceeding the slice limit
+ if current := atomic.LoadInt32(&sp.activeSlices); current >= sp.maxSlices {
+ return nil // Return nil to indicate resource exhaustion
+ }
+ atomic.AddInt32(&sp.activeSlices, 1)
+ return make([]byte, 0, initialSize)
+ }
+ return sp
+}
+
+// Get retrieves a slice from the pool
+func (sp *SlicePool) Get() []byte {
+ sliceInterface := sp.pool.Get()
+ if sliceInterface == nil {
+ // Resource limit exceeded
+ return nil
+ }
+ slice := sliceInterface.([]byte)
+ return slice[:0] // Reset length but keep capacity
+}
+
+// Put returns a slice to the pool
+func (sp *SlicePool) Put(slice []byte) {
+ if slice == nil {
+ return
+ }
+
+ // Don't keep very large slices in the pool
+ if sp.maxSize > 0 && cap(slice) > sp.maxSize {
+ // Decrement active count for oversized slices that won't be pooled
+ atomic.AddInt32(&sp.activeSlices, -1)
+ return
+ }
+
+ // Conditionally zero out sensitive data if secure mode is enabled
+ if sp.secureZero && len(slice) > 0 {
+ for i := range slice {
+ slice[i] = 0
+ }
+ }
+
+ sp.pool.Put(slice[:0]) // Reset length
+}
+
+// PutSecure returns a slice to the pool with secure zeroing regardless of pool setting
+func (sp *SlicePool) PutSecure(slice []byte) {
+ if slice == nil {
+ return
+ }
+
+ // Don't keep very large slices in the pool
+ if sp.maxSize > 0 && cap(slice) > sp.maxSize {
+ // Decrement active count for oversized slices that won't be pooled
+ atomic.AddInt32(&sp.activeSlices, -1)
+ return
+ }
+
+ // Always zero out contents for sensitive data
+ if len(slice) > 0 {
+ for i := range slice {
+ slice[i] = 0
+ }
+ }
+
+ sp.pool.Put(slice[:0]) // Reset length
+}
+
+// Reset clears the pool
+func (sp *SlicePool) Reset() {
+ sp.pool = sync.Pool{
+ New: func() interface{} {
+ // Check if we're exceeding the slice limit
+ if current := atomic.LoadInt32(&sp.activeSlices); current >= sp.maxSlices {
+ return nil // Return nil to indicate resource exhaustion
+ }
+ atomic.AddInt32(&sp.activeSlices, 1)
+ return make([]byte, 0, 1024)
+ },
+ }
+ atomic.StoreInt32(&sp.activeSlices, 0)
+}
+
+// ErrorPool manages a pool of error objects
+type ErrorPool struct {
+ encodingPool sync.Pool
+ decodingPool sync.Pool
+ operationPool sync.Pool
+ validationPool sync.Pool
+ configurationPool sync.Pool
+ resourcePool sync.Pool
+ registryPool sync.Pool
+}
+
+// NewErrorPool creates a new error pool
+func NewErrorPool() *ErrorPool {
+ ep := &ErrorPool{}
+ ep.encodingPool.New = func() interface{} {
+ return &EncodingError{}
+ }
+ ep.decodingPool.New = func() interface{} {
+ return &DecodingError{}
+ }
+ ep.operationPool.New = func() interface{} {
+ return &OperationError{}
+ }
+ ep.validationPool.New = func() interface{} {
+ return &ValidationError{}
+ }
+ ep.configurationPool.New = func() interface{} {
+ return &ConfigurationError{}
+ }
+ ep.resourcePool.New = func() interface{} {
+ return &ResourceError{}
+ }
+ ep.registryPool.New = func() interface{} {
+ return &RegistryError{}
+ }
+ return ep
+}
+
+// GetEncodingError retrieves an encoding error from the pool
+func (ep *ErrorPool) GetEncodingError() *EncodingError {
+ err := ep.encodingPool.Get().(*EncodingError)
+ err.Reset()
+ return err
+}
+
+// PutEncodingError returns an encoding error to the pool
+func (ep *ErrorPool) PutEncodingError(err *EncodingError) {
+ if err == nil {
+ return
+ }
+ err.Reset()
+ ep.encodingPool.Put(err)
+}
+
+// GetDecodingError retrieves a decoding error from the pool
+func (ep *ErrorPool) GetDecodingError() *DecodingError {
+ err := ep.decodingPool.Get().(*DecodingError)
+ err.Reset()
+ return err
+}
+
+// PutDecodingError returns a decoding error to the pool
+func (ep *ErrorPool) PutDecodingError(err *DecodingError) {
+ if err == nil {
+ return
+ }
+ err.Reset()
+ ep.decodingPool.Put(err)
+}
+
+// GetOperationError retrieves an operation error from the pool
+func (ep *ErrorPool) GetOperationError() *OperationError {
+ err := ep.operationPool.Get().(*OperationError)
+ err.Reset()
+ return err
+}
+
+// PutOperationError returns an operation error to the pool
+func (ep *ErrorPool) PutOperationError(err *OperationError) {
+ if err == nil {
+ return
+ }
+ err.Reset()
+ ep.operationPool.Put(err)
+}
+
+// GetValidationError retrieves a validation error from the pool
+func (ep *ErrorPool) GetValidationError() *ValidationError {
+ err := ep.validationPool.Get().(*ValidationError)
+ err.Reset()
+ return err
+}
+
+// PutValidationError returns a validation error to the pool
+func (ep *ErrorPool) PutValidationError(err *ValidationError) {
+ if err == nil {
+ return
+ }
+ err.Reset()
+ ep.validationPool.Put(err)
+}
+
+// GetConfigurationError retrieves a configuration error from the pool
+func (ep *ErrorPool) GetConfigurationError() *ConfigurationError {
+ err := ep.configurationPool.Get().(*ConfigurationError)
+ err.Reset()
+ return err
+}
+
+// PutConfigurationError returns a configuration error to the pool
+func (ep *ErrorPool) PutConfigurationError(err *ConfigurationError) {
+ if err == nil {
+ return
+ }
+ err.Reset()
+ ep.configurationPool.Put(err)
+}
+
+// GetResourceError retrieves a resource error from the pool
+func (ep *ErrorPool) GetResourceError() *ResourceError {
+ err := ep.resourcePool.Get().(*ResourceError)
+ err.Reset()
+ return err
+}
+
+// PutResourceError returns a resource error to the pool
+func (ep *ErrorPool) PutResourceError(err *ResourceError) {
+ if err == nil {
+ return
+ }
+ err.Reset()
+ ep.resourcePool.Put(err)
+}
+
+// GetRegistryError retrieves a registry error from the pool
+func (ep *ErrorPool) GetRegistryError() *RegistryError {
+ err := ep.registryPool.Get().(*RegistryError)
+ err.Reset()
+ return err
+}
+
+// PutRegistryError returns a registry error to the pool
+func (ep *ErrorPool) PutRegistryError(err *RegistryError) {
+ if err == nil {
+ return
+ }
+ err.Reset()
+ ep.registryPool.Put(err)
+}
+
+// Reset clears the pool
+func (ep *ErrorPool) Reset() {
+ ep.encodingPool = sync.Pool{
+ New: func() interface{} {
+ return &EncodingError{}
+ },
+ }
+ ep.decodingPool = sync.Pool{
+ New: func() interface{} {
+ return &DecodingError{}
+ },
+ }
+ ep.operationPool = sync.Pool{
+ New: func() interface{} {
+ return &OperationError{}
+ },
+ }
+ ep.validationPool = sync.Pool{
+ New: func() interface{} {
+ return &ValidationError{}
+ },
+ }
+ ep.configurationPool = sync.Pool{
+ New: func() interface{} {
+ return &ConfigurationError{}
+ },
+ }
+ ep.resourcePool = sync.Pool{
+ New: func() interface{} {
+ return &ResourceError{}
+ },
+ }
+ ep.registryPool = sync.Pool{
+ New: func() interface{} {
+ return &RegistryError{}
+ },
+ }
+}
+
+// Poolable interface for objects that can be pooled
+type Poolable interface {
+ Reset()
+}
+
+// Reset methods for error types
+func (e *EncodingError) Reset() {
+ e.Format = ""
+ e.Event = nil
+ e.Message = ""
+ e.Cause = nil
+}
+
+func (e *DecodingError) Reset() {
+ e.Format = ""
+ e.Data = nil
+ e.Message = ""
+ e.Cause = nil
+}
+
+// Global pools for common objects
+var (
+ // Buffer pools with different size limits and capacity limits (secure by default)
+ smallBufferPool = NewBufferPoolWithOptions(4096, 500, true) // 4KB max, 500 buffers, secure
+ mediumBufferPool = NewBufferPoolWithOptions(65536, 200, true) // 64KB max, 200 buffers, secure
+ largeBufferPool = NewBufferPoolWithOptions(1048576, 50, true) // 1MB max, 50 buffers, secure
+
+ // Slice pools for different sizes with capacity limits (secure by default)
+ smallSlicePool = NewSlicePoolWithOptions(1024, 4096, 500, true) // 1KB initial, 4KB max, 500 slices, secure
+ mediumSlicePool = NewSlicePoolWithOptions(4096, 65536, 200, true) // 4KB initial, 64KB max, 200 slices, secure
+ largeSlicePool = NewSlicePoolWithOptions(16384, 1048576, 50, true) // 16KB initial, 1MB max, 50 slices, secure
+
+ // Error pool
+ errorPool = NewErrorPool()
+)
+
+// GetBuffer returns a buffer from the appropriate pool based on expected size
+// Returns nil if resource limits are exceeded
+func GetBuffer(expectedSize int) *bytes.Buffer {
+ switch {
+ case expectedSize <= 4096:
+ return smallBufferPool.Get()
+ case expectedSize <= 65536:
+ return mediumBufferPool.Get()
+ default:
+ return largeBufferPool.Get()
+ }
+}
+
+// GetBufferSafe returns a buffer from the appropriate pool or creates a new one if pool is exhausted
+func GetBufferSafe(expectedSize int) *bytes.Buffer {
+ buf := GetBuffer(expectedSize)
+ if buf == nil {
+ // Pool exhausted, create a new buffer but don't exceed reasonable limits
+ if expectedSize > 100*1024*1024 { // 100MB limit
+ return nil
+ }
+ return &bytes.Buffer{}
+ }
+ return buf
+}
+
+// PutBuffer returns a buffer to the appropriate pool
+func PutBuffer(buf *bytes.Buffer) {
+ if buf == nil {
+ return
+ }
+
+ // The individual pool's Put method will handle zeroing
+ switch {
+ case buf.Cap() <= 4096:
+ smallBufferPool.Put(buf)
+ case buf.Cap() <= 65536:
+ mediumBufferPool.Put(buf)
+ default:
+ largeBufferPool.Put(buf)
+ }
+}
+
+// PutBufferSecure returns a buffer to the appropriate pool with secure zeroing
+func PutBufferSecure(buf *bytes.Buffer) {
+ if buf == nil {
+ return
+ }
+
+ switch {
+ case buf.Cap() <= 4096:
+ smallBufferPool.PutSecure(buf)
+ case buf.Cap() <= 65536:
+ mediumBufferPool.PutSecure(buf)
+ default:
+ largeBufferPool.PutSecure(buf)
+ }
+}
+
+// GetSlice returns a slice from the appropriate pool based on expected size
+// Returns nil if resource limits are exceeded
+func GetSlice(expectedSize int) []byte {
+ switch {
+ case expectedSize <= 4096:
+ return smallSlicePool.Get()
+ case expectedSize <= 65536:
+ return mediumSlicePool.Get()
+ default:
+ return largeSlicePool.Get()
+ }
+}
+
+// GetSliceSafe returns a slice from the appropriate pool or creates a new one if pool is exhausted
+func GetSliceSafe(expectedSize int) []byte {
+ slice := GetSlice(expectedSize)
+ if slice == nil {
+ // Pool exhausted, create a new slice but don't exceed reasonable limits
+ if expectedSize > 100*1024*1024 { // 100MB limit
+ return nil
+ }
+ return make([]byte, 0, expectedSize)
+ }
+ return slice
+}
+
+// PutSlice returns a slice to the appropriate pool
+func PutSlice(slice []byte) {
+ if slice == nil {
+ return
+ }
+
+ // The individual pool's Put method will handle zeroing
+ switch {
+ case cap(slice) <= 4096:
+ smallSlicePool.Put(slice)
+ case cap(slice) <= 65536:
+ mediumSlicePool.Put(slice)
+ default:
+ largeSlicePool.Put(slice)
+ }
+}
+
+// PutSliceSecure returns a slice to the appropriate pool with secure zeroing
+func PutSliceSecure(slice []byte) {
+ if slice == nil {
+ return
+ }
+
+ switch {
+ case cap(slice) <= 4096:
+ smallSlicePool.PutSecure(slice)
+ case cap(slice) <= 65536:
+ mediumSlicePool.PutSecure(slice)
+ default:
+ largeSlicePool.PutSecure(slice)
+ }
+}
+
+// GetEncodingError returns an encoding error from the pool
+func GetEncodingError() *EncodingError {
+ return errorPool.GetEncodingError()
+}
+
+// PutEncodingError returns an encoding error to the pool
+func PutEncodingError(err *EncodingError) {
+ errorPool.PutEncodingError(err)
+}
+
+// GetDecodingError returns a decoding error from the pool
+func GetDecodingError() *DecodingError {
+ return errorPool.GetDecodingError()
+}
+
+// PutDecodingError returns a decoding error to the pool
+func PutDecodingError(err *DecodingError) {
+ errorPool.PutDecodingError(err)
+}
+
+// GetOperationError returns an operation error from the pool
+func GetOperationError() *OperationError {
+ return errorPool.GetOperationError()
+}
+
+// PutOperationError returns an operation error to the pool
+func PutOperationError(err *OperationError) {
+ errorPool.PutOperationError(err)
+}
+
+// GetValidationError returns a validation error from the pool
+func GetValidationError() *ValidationError {
+ return errorPool.GetValidationError()
+}
+
+// PutValidationError returns a validation error to the pool
+func PutValidationError(err *ValidationError) {
+ errorPool.PutValidationError(err)
+}
+
+// GetConfigurationError returns a configuration error from the pool
+func GetConfigurationError() *ConfigurationError {
+ return errorPool.GetConfigurationError()
+}
+
+// PutConfigurationError returns a configuration error to the pool
+func PutConfigurationError(err *ConfigurationError) {
+ errorPool.PutConfigurationError(err)
+}
+
+// GetResourceError returns a resource error from the pool
+func GetResourceError() *ResourceError {
+ return errorPool.GetResourceError()
+}
+
+// PutResourceError returns a resource error to the pool
+func PutResourceError(err *ResourceError) {
+ errorPool.PutResourceError(err)
+}
+
+// GetRegistryError returns a registry error from the pool
+func GetRegistryError() *RegistryError {
+ return errorPool.GetRegistryError()
+}
+
+// PutRegistryError returns a registry error to the pool
+func PutRegistryError(err *RegistryError) {
+ errorPool.PutRegistryError(err)
+}
+
+// ResetAllPools resets all global pools
+func ResetAllPools() {
+ smallBufferPool.Reset()
+ mediumBufferPool.Reset()
+ largeBufferPool.Reset()
+ smallSlicePool.Reset()
+ mediumSlicePool.Reset()
+ largeSlicePool.Reset()
+ errorPool.Reset()
+}
+
+// PoolManager manages lifecycle of pools
+type PoolManager struct {
+ pools map[string]interface{}
+ mu sync.RWMutex
+}
+
+// NewPoolManager creates a new pool manager
+func NewPoolManager() *PoolManager {
+ return &PoolManager{
+ pools: make(map[string]interface{}),
+ }
+}
+
+// RegisterPool registers a pool with the manager
+func (pm *PoolManager) RegisterPool(name string, pool interface{}) {
+ pm.mu.Lock()
+ defer pm.mu.Unlock()
+ pm.pools[name] = pool
+}
+
+// GetPool retrieves a pool by name
+func (pm *PoolManager) GetPool(name string) interface{} {
+ pm.mu.RLock()
+ defer pm.mu.RUnlock()
+ return pm.pools[name]
+}
diff --git a/sdks/community/go/pkg/encoding/sse/writer.go b/sdks/community/go/pkg/encoding/sse/writer.go
new file mode 100644
index 000000000..78df4e137
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/sse/writer.go
@@ -0,0 +1,279 @@
+package sse
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log/slog"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/encoding/encoder"
+)
+
+// SSEWriter provides utilities for writing Server-Sent Events with proper framing
+type SSEWriter struct {
+ encoder *encoder.EventEncoder
+ logger *slog.Logger
+}
+
+// NewSSEWriter creates a new SSE writer
+func NewSSEWriter() *SSEWriter {
+ return &SSEWriter{
+ encoder: encoder.NewEventEncoder(),
+ logger: slog.Default(),
+ }
+}
+
+// WithLogger sets a custom logger for the SSE writer
+func (w *SSEWriter) WithLogger(logger *slog.Logger) *SSEWriter {
+ w.logger = logger
+ return w
+}
+
+// WriteEvent writes a single event as SSE format to the writer with proper framing
+// Format: data: \n\n with proper escaping and flushing
+func (w *SSEWriter) WriteEvent(ctx context.Context, writer io.Writer, event events.Event) error {
+ return w.WriteEventWithType(ctx, writer, event, "")
+}
+
+// WriteBytes writes an event
+func (w *SSEWriter) WriteBytes(ctx context.Context, writer io.Writer, event []byte) error {
+
+ // Create SSE frame
+ sseFrame, err := w.createSSEFrame(event, "", nil)
+ if err != nil {
+ w.logger.ErrorContext(ctx, "Failed to create SSE frame",
+ "error", err)
+ return fmt.Errorf("SSE frame creation failed: %w", err)
+ }
+
+ // Write the SSE frame
+ _, err = writer.Write([]byte(sseFrame))
+ if err != nil {
+ w.logger.ErrorContext(ctx, "Failed to write SSE frame",
+ "error", err)
+ return fmt.Errorf("SSE write failed: %w", err)
+ }
+
+ // Flush if the writer supports it
+ if flusher, ok := writer.(flusher); ok {
+ if err := flusher.Flush(); err != nil {
+ w.logger.ErrorContext(ctx, "Failed to flush SSE frame",
+ "error", err)
+ return fmt.Errorf("SSE flush failed: %w", err)
+ }
+ }
+ return nil
+}
+
+// WriteEventWithType writes an event with a specific SSE event type
+func (w *SSEWriter) WriteEventWithType(ctx context.Context, writer io.Writer, event events.Event, eventType string) error {
+ if event == nil {
+ return fmt.Errorf("event cannot be nil")
+ }
+
+ if writer == nil {
+ return fmt.Errorf("writer cannot be nil")
+ }
+
+ // Encode the event to JSON
+ jsonData, err := w.encoder.EncodeEvent(ctx, event, "application/json")
+ if err != nil {
+ w.logger.ErrorContext(ctx, "Failed to encode event",
+ "error", err,
+ "event_type", event.Type())
+ return fmt.Errorf("event encoding failed: %w", err)
+ }
+
+ // Create SSE frame
+ sseFrame, err := w.createSSEFrame(jsonData, eventType, event)
+ if err != nil {
+ w.logger.ErrorContext(ctx, "Failed to create SSE frame",
+ "error", err,
+ "event_type", event.Type())
+ return fmt.Errorf("SSE frame creation failed: %w", err)
+ }
+
+ // Write the SSE frame
+ _, err = writer.Write([]byte(sseFrame))
+ if err != nil {
+ w.logger.ErrorContext(ctx, "Failed to write SSE frame",
+ "error", err,
+ "event_type", event.Type())
+ return fmt.Errorf("SSE write failed: %w", err)
+ }
+
+ // Flush if the writer supports it
+ if flusher, ok := writer.(flusher); ok {
+ if err := flusher.Flush(); err != nil {
+ w.logger.ErrorContext(ctx, "Failed to flush SSE frame",
+ "error", err,
+ "event_type", event.Type())
+ return fmt.Errorf("SSE flush failed: %w", err)
+ }
+ }
+
+ return nil
+}
+
+// WriteEventWithNegotiation writes an event after performing content negotiation
+func (w *SSEWriter) WriteEventWithNegotiation(ctx context.Context, writer io.Writer, event events.Event, acceptHeader string) error {
+ // Perform content negotiation
+ _, err := w.encoder.NegotiateContentType(acceptHeader)
+ if err != nil {
+ w.logger.WarnContext(ctx, "Content negotiation failed, using JSON",
+ "error", err,
+ "accept_header", acceptHeader)
+ // Continue with JSON fallback
+ }
+
+ // For now, we only support JSON, so we use JSON regardless of negotiated type
+ return w.WriteEvent(ctx, writer, event)
+}
+
+// WriteErrorEvent writes an error as an SSE event
+func (w *SSEWriter) WriteErrorEvent(ctx context.Context, writer io.Writer, err error, requestID string) error {
+ // Create a custom error event
+ errorEvent := &CustomEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ },
+ }
+ errorEvent.SetData(map[string]interface{}{
+ "error": true,
+ "message": err.Error(),
+ "request_id": requestID,
+ })
+
+ // Set timestamp
+ errorEvent.SetTimestamp(getCurrentTimestamp())
+
+ return w.WriteEventWithType(ctx, writer, errorEvent, "error")
+}
+
+// createSSEFrame creates a properly formatted SSE frame
+func (w *SSEWriter) createSSEFrame(jsonData []byte, eventType string, event events.Event) (string, error) {
+ var frame strings.Builder
+
+ // Add event type if specified
+ if eventType != "" {
+ frame.WriteString(fmt.Sprintf("event: %s\n", eventType))
+ }
+
+ // Add event ID if available
+ if event != nil && event.Timestamp() != nil {
+ frame.WriteString(fmt.Sprintf("id: %s_%d\n", event.Type(), *event.Timestamp()))
+ }
+
+ // Escape newlines in JSON data to maintain SSE format integrity
+ escapedData := strings.ReplaceAll(string(jsonData), "\n", "\\n")
+ escapedData = strings.ReplaceAll(escapedData, "\r", "\\r")
+
+ // Write data line
+ frame.WriteString(fmt.Sprintf("data: %s\n", escapedData))
+
+ // End with empty line to complete the SSE event
+ frame.WriteString("\n")
+
+ return frame.String(), nil
+}
+
+// flusher interface for writers that support flushing
+type flusher interface {
+ Flush() error
+}
+
+// CustomEvent is a simple implementation of events.Event for error and custom events
+type CustomEvent struct {
+ events.BaseEvent
+ mu sync.RWMutex // Protect concurrent map access
+ data map[string]interface{} // Thread-safe access via Data()/SetData() methods
+}
+
+// Data returns a thread-safe copy of the data map
+func (e *CustomEvent) Data() map[string]interface{} {
+ e.mu.RLock()
+ defer e.mu.RUnlock()
+ if e.data == nil {
+ return nil
+ }
+ // Return a copy to prevent external mutation
+ result := make(map[string]interface{}, len(e.data))
+ for k, v := range e.data {
+ result[k] = v
+ }
+ return result
+}
+
+// SetData safely sets data in the map
+func (e *CustomEvent) SetData(data map[string]interface{}) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ e.data = data
+}
+
+// SetDataField safely sets a single field in the data map
+func (e *CustomEvent) SetDataField(key string, value interface{}) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ if e.data == nil {
+ e.data = make(map[string]interface{})
+ }
+ e.data[key] = value
+}
+
+// ThreadID returns empty string for custom events
+func (e *CustomEvent) ThreadID() string {
+ return ""
+}
+
+// RunID returns empty string for custom events
+func (e *CustomEvent) RunID() string {
+ return ""
+}
+
+// Validate validates the custom event
+func (e *CustomEvent) Validate() error {
+ if e.EventType == "" {
+ return fmt.Errorf("event type cannot be empty")
+ }
+ return nil
+}
+
+// ToJSON serializes the custom event to JSON
+func (e *CustomEvent) ToJSON() ([]byte, error) {
+ eventData := map[string]interface{}{
+ "type": e.EventType,
+ }
+
+ if e.TimestampMs != nil {
+ eventData["timestamp"] = *e.TimestampMs
+ }
+
+ // Thread-safe data access
+ e.mu.RLock()
+ if e.data != nil {
+ dataCopy := make(map[string]interface{}, len(e.data))
+ for k, v := range e.data {
+ dataCopy[k] = v
+ }
+ eventData["data"] = dataCopy
+ }
+ e.mu.RUnlock()
+
+ return jsonMarshal(eventData)
+}
+
+// Helper function to get current timestamp
+func getCurrentTimestamp() int64 {
+ return time.Now().UnixMilli()
+}
+
+// Helper function for JSON marshaling (allows for future customization)
+func jsonMarshal(v interface{}) ([]byte, error) {
+ return json.Marshal(v)
+}
diff --git a/sdks/community/go/pkg/encoding/sse/writer_test.go b/sdks/community/go/pkg/encoding/sse/writer_test.go
new file mode 100644
index 000000000..056662e44
--- /dev/null
+++ b/sdks/community/go/pkg/encoding/sse/writer_test.go
@@ -0,0 +1,665 @@
+package sse
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/ag-ui-protocol/ag-ui/sdks/community/go/pkg/core/events"
+)
+
+type mockEvent struct {
+ events.BaseEvent
+ dataValue map[string]interface{}
+ validateError error
+ toJSONError error
+ customJSON []byte
+}
+
+func (m *mockEvent) Data() map[string]interface{} {
+ return m.dataValue
+}
+
+func (m *mockEvent) ThreadID() string {
+ return "mock-thread-id"
+}
+
+func (m *mockEvent) RunID() string {
+ return "mock-run-id"
+}
+
+func (m *mockEvent) Validate() error {
+ return m.validateError
+}
+
+func (m *mockEvent) ToJSON() ([]byte, error) {
+ if m.toJSONError != nil {
+ return nil, m.toJSONError
+ }
+ if m.customJSON != nil {
+ return m.customJSON, nil
+ }
+ return []byte(`{"type":"mock","data":"test"}`), nil
+}
+
+type errorWriter struct {
+ err error
+}
+
+func (e *errorWriter) Write(p []byte) (n int, err error) {
+ return 0, e.err
+}
+
+type flushWriter struct {
+ bytes.Buffer
+ flushCalled bool
+ flushError error
+}
+
+func (fw *flushWriter) Flush() error {
+ fw.flushCalled = true
+ return fw.flushError
+}
+
+func TestNewSSEWriter(t *testing.T) {
+ writer := NewSSEWriter()
+ if writer == nil {
+ t.Fatal("expected non-nil writer")
+ }
+ if writer.encoder == nil {
+ t.Fatal("expected non-nil encoder")
+ }
+ if writer.logger == nil {
+ t.Fatal("expected non-nil logger")
+ }
+}
+
+func TestSSEWriter_WithLogger(t *testing.T) {
+ customLogger := slog.New(slog.NewTextHandler(io.Discard, nil))
+ writer := NewSSEWriter().WithLogger(customLogger)
+
+ if writer.logger != customLogger {
+ t.Error("expected custom logger to be set")
+ }
+}
+
+func TestSSEWriter_WriteEvent(t *testing.T) {
+ tests := []struct {
+ name string
+ event events.Event
+ expectedError bool
+ errorContains string
+ validateSSE func(t *testing.T, output string)
+ }{
+ {
+ name: "successful write",
+ event: &mockEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ TimestampMs: ptr(int64(1234567890)),
+ },
+ },
+ expectedError: false,
+ validateSSE: func(t *testing.T, output string) {
+ if !strings.Contains(output, "data: ") {
+ t.Error("expected SSE data line")
+ }
+ if !strings.HasSuffix(output, "\n\n") {
+ t.Error("expected SSE frame to end with double newline")
+ }
+ },
+ },
+ {
+ name: "nil event",
+ event: nil,
+ expectedError: true,
+ errorContains: "event cannot be nil",
+ },
+ {
+ name: "event with JSON error",
+ event: &mockEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ },
+ toJSONError: errors.New("JSON encoding error"),
+ },
+ expectedError: true,
+ errorContains: "event encoding failed",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := context.Background()
+ writer := NewSSEWriter()
+ var buf bytes.Buffer
+
+ err := writer.WriteEvent(ctx, &buf, tt.event)
+
+ if tt.expectedError {
+ if err == nil {
+ t.Fatal("expected error but got none")
+ }
+ if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
+ t.Errorf("expected error to contain '%s', got: %v", tt.errorContains, err)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if tt.validateSSE != nil {
+ tt.validateSSE(t, buf.String())
+ }
+ }
+ })
+ }
+}
+
+func TestSSEWriter_WriteBytes(t *testing.T) {
+ tests := []struct {
+ name string
+ data []byte
+ writer io.Writer
+ expectedError bool
+ errorContains string
+ validateSSE func(t *testing.T, output string)
+ }{
+ {
+ name: "successful write",
+ data: []byte(`{"test":"data"}`),
+ writer: &bytes.Buffer{},
+ expectedError: false,
+ validateSSE: func(t *testing.T, output string) {
+ if !strings.Contains(output, `data: {"test":"data"}`) {
+ t.Error("expected SSE data line with JSON")
+ }
+ if !strings.HasSuffix(output, "\n\n") {
+ t.Error("expected SSE frame to end with double newline")
+ }
+ },
+ },
+ {
+ name: "empty bytes",
+ data: []byte{},
+ writer: &bytes.Buffer{},
+ expectedError: false,
+ validateSSE: func(t *testing.T, output string) {
+ if !strings.Contains(output, "data: ") {
+ t.Error("expected SSE data line")
+ }
+ },
+ },
+ {
+ name: "bytes with newlines",
+ data: []byte("line1\nline2\rline3"),
+ writer: &bytes.Buffer{},
+ expectedError: false,
+ validateSSE: func(t *testing.T, output string) {
+ if !strings.Contains(output, `line1\nline2\rline3`) {
+ t.Error("expected newlines to be escaped")
+ }
+ },
+ },
+ {
+ name: "write error",
+ data: []byte(`{"test":"data"}`),
+ writer: &errorWriter{err: errors.New("write failed")},
+ expectedError: true,
+ errorContains: "SSE write failed",
+ },
+ {
+ name: "flush error",
+ data: []byte(`{"test":"data"}`),
+ writer: &flushWriter{flushError: errors.New("flush failed")},
+ expectedError: true,
+ errorContains: "SSE flush failed",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := context.Background()
+ sseWriter := NewSSEWriter()
+
+ var buf *bytes.Buffer
+ if b, ok := tt.writer.(*bytes.Buffer); ok {
+ buf = b
+ }
+
+ err := sseWriter.WriteBytes(ctx, tt.writer, tt.data)
+
+ if tt.expectedError {
+ if err == nil {
+ t.Fatal("expected error but got none")
+ }
+ if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
+ t.Errorf("expected error to contain '%s', got: %v", tt.errorContains, err)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if tt.validateSSE != nil && buf != nil {
+ tt.validateSSE(t, buf.String())
+ }
+ }
+ })
+ }
+}
+
+func TestSSEWriter_WriteEventWithType(t *testing.T) {
+ tests := []struct {
+ name string
+ event events.Event
+ eventType string
+ writer io.Writer
+ expectedError bool
+ errorContains string
+ validateSSE func(t *testing.T, output string)
+ }{
+ {
+ name: "successful write with type",
+ event: &mockEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ TimestampMs: ptr(int64(1234567890)),
+ },
+ },
+ eventType: "custom",
+ writer: &bytes.Buffer{},
+ expectedError: false,
+ validateSSE: func(t *testing.T, output string) {
+ if !strings.Contains(output, "event: custom\n") {
+ t.Error("expected SSE event type line")
+ }
+ if !strings.Contains(output, "id: ") {
+ t.Error("expected SSE id line")
+ }
+ if !strings.Contains(output, "data: ") {
+ t.Error("expected SSE data line")
+ }
+ },
+ },
+ {
+ name: "successful write without type",
+ event: &mockEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ },
+ },
+ eventType: "",
+ writer: &bytes.Buffer{},
+ expectedError: false,
+ validateSSE: func(t *testing.T, output string) {
+ if strings.Contains(output, "event: ") {
+ t.Error("unexpected SSE event type line")
+ }
+ },
+ },
+ {
+ name: "nil writer",
+ event: &mockEvent{},
+ eventType: "",
+ writer: nil,
+ expectedError: true,
+ errorContains: "writer cannot be nil",
+ },
+ {
+ name: "write error",
+ event: &mockEvent{},
+ eventType: "",
+ writer: &errorWriter{err: errors.New("write failed")},
+ expectedError: true,
+ errorContains: "SSE write failed",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := context.Background()
+ sseWriter := NewSSEWriter()
+
+ var buf *bytes.Buffer
+ if b, ok := tt.writer.(*bytes.Buffer); ok {
+ buf = b
+ }
+
+ err := sseWriter.WriteEventWithType(ctx, tt.writer, tt.event, tt.eventType)
+
+ if tt.expectedError {
+ if err == nil {
+ t.Fatal("expected error but got none")
+ }
+ if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
+ t.Errorf("expected error to contain '%s', got: %v", tt.errorContains, err)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if tt.validateSSE != nil && buf != nil {
+ tt.validateSSE(t, buf.String())
+ }
+ }
+ })
+ }
+}
+
+func TestSSEWriter_WriteEventWithNegotiation(t *testing.T) {
+ ctx := context.Background()
+ writer := NewSSEWriter()
+ var buf bytes.Buffer
+
+ event := &mockEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ },
+ }
+
+ err := writer.WriteEventWithNegotiation(ctx, &buf, event, "application/json")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ output := buf.String()
+ if !strings.Contains(output, "data: ") {
+ t.Error("expected SSE data line")
+ }
+}
+
+func TestSSEWriter_WriteErrorEvent(t *testing.T) {
+ ctx := context.Background()
+ writer := NewSSEWriter()
+ var buf bytes.Buffer
+
+ testError := errors.New("test error")
+ requestID := "req-123"
+
+ err := writer.WriteErrorEvent(ctx, &buf, testError, requestID)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ output := buf.String()
+ if !strings.Contains(output, "event: error") {
+ t.Error("expected error event type")
+ }
+ if !strings.Contains(output, "test error") {
+ t.Error("expected error message in output")
+ }
+ if !strings.Contains(output, requestID) {
+ t.Error("expected request ID in output")
+ }
+}
+
+func TestSSEWriter_Flushing(t *testing.T) {
+ tests := []struct {
+ name string
+ flushError error
+ expectError bool
+ }{
+ {
+ name: "successful flush",
+ flushError: nil,
+ expectError: false,
+ },
+ {
+ name: "flush error",
+ flushError: errors.New("flush failed"),
+ expectError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := context.Background()
+ writer := NewSSEWriter()
+ fw := &flushWriter{flushError: tt.flushError}
+
+ event := &mockEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ },
+ }
+
+ err := writer.WriteEvent(ctx, fw, event)
+
+ if tt.expectError {
+ if err == nil {
+ t.Fatal("expected error but got none")
+ }
+ if !strings.Contains(err.Error(), "SSE flush failed") {
+ t.Errorf("expected flush error, got: %v", err)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+
+ if !fw.flushCalled {
+ t.Error("expected flush to be called")
+ }
+ })
+ }
+}
+
+func TestCustomEvent(t *testing.T) {
+ t.Run("Data operations", func(t *testing.T) {
+ event := &CustomEvent{}
+
+ if data := event.Data(); data != nil {
+ t.Error("expected nil data initially")
+ }
+
+ testData := map[string]interface{}{
+ "key1": "value1",
+ "key2": 42,
+ }
+ event.SetData(testData)
+
+ data := event.Data()
+ if data["key1"] != "value1" {
+ t.Error("expected key1 to be value1")
+ }
+ if data["key2"] != 42 {
+ t.Error("expected key2 to be 42")
+ }
+
+ data["key3"] = "external"
+ internalData := event.Data()
+ if _, exists := internalData["key3"]; exists {
+ t.Error("external modification should not affect internal data")
+ }
+ })
+
+ t.Run("SetDataField", func(t *testing.T) {
+ event := &CustomEvent{}
+
+ event.SetDataField("field1", "value1")
+ event.SetDataField("field2", 100)
+
+ data := event.Data()
+ if data["field1"] != "value1" {
+ t.Error("expected field1 to be value1")
+ }
+ if data["field2"] != 100 {
+ t.Error("expected field2 to be 100")
+ }
+ })
+
+ t.Run("ThreadID and RunID", func(t *testing.T) {
+ event := &CustomEvent{}
+
+ if event.ThreadID() != "" {
+ t.Error("expected empty thread ID")
+ }
+ if event.RunID() != "" {
+ t.Error("expected empty run ID")
+ }
+ })
+
+ t.Run("Validate", func(t *testing.T) {
+ event := &CustomEvent{}
+
+ if err := event.Validate(); err == nil {
+ t.Error("expected validation error for empty event type")
+ }
+
+ event.EventType = events.EventTypeCustom
+ if err := event.Validate(); err != nil {
+ t.Errorf("unexpected validation error: %v", err)
+ }
+ })
+
+ t.Run("ToJSON", func(t *testing.T) {
+ event := &CustomEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ TimestampMs: ptr(int64(1234567890)),
+ },
+ }
+ event.SetData(map[string]interface{}{
+ "test": "data",
+ })
+
+ jsonData, err := event.ToJSON()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ jsonStr := string(jsonData)
+ if !strings.Contains(jsonStr, `"type":"CUSTOM"`) {
+ t.Error("expected type in JSON")
+ }
+ if !strings.Contains(jsonStr, `"timestamp":1234567890`) {
+ t.Error("expected timestamp in JSON")
+ }
+ if !strings.Contains(jsonStr, `"test":"data"`) {
+ t.Error("expected test data in JSON")
+ }
+ })
+
+ t.Run("Concurrent access", func(t *testing.T) {
+ event := &CustomEvent{}
+ var wg sync.WaitGroup
+
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func(idx int) {
+ defer wg.Done()
+ event.SetDataField(fmt.Sprintf("key%d", idx), idx)
+ }(i)
+ }
+
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ _ = event.Data()
+ }()
+ }
+
+ wg.Wait()
+
+ data := event.Data()
+ if len(data) != 100 {
+ t.Errorf("expected 100 fields, got %d", len(data))
+ }
+ })
+}
+
+func TestGetCurrentTimestamp(t *testing.T) {
+ before := time.Now().UnixMilli()
+ timestamp := getCurrentTimestamp()
+ after := time.Now().UnixMilli()
+
+ if timestamp < before || timestamp > after {
+ t.Errorf("timestamp %d not in expected range [%d, %d]", timestamp, before, after)
+ }
+}
+
+func TestCreateSSEFrame(t *testing.T) {
+ writer := NewSSEWriter()
+
+ tests := []struct {
+ name string
+ jsonData []byte
+ eventType string
+ event events.Event
+ validate func(t *testing.T, frame string)
+ }{
+ {
+ name: "basic frame",
+ jsonData: []byte(`{"test":"data"}`),
+ eventType: "",
+ event: nil,
+ validate: func(t *testing.T, frame string) {
+ expected := `data: {"test":"data"}` + "\n\n"
+ if frame != expected {
+ t.Errorf("expected frame:\n%q\ngot:\n%q", expected, frame)
+ }
+ },
+ },
+ {
+ name: "frame with event type",
+ jsonData: []byte(`{"test":"data"}`),
+ eventType: "message",
+ event: nil,
+ validate: func(t *testing.T, frame string) {
+ if !strings.HasPrefix(frame, "event: message\n") {
+ t.Error("expected frame to start with event type")
+ }
+ },
+ },
+ {
+ name: "frame with event ID",
+ jsonData: []byte(`{"test":"data"}`),
+ event: &mockEvent{
+ BaseEvent: events.BaseEvent{
+ EventType: events.EventTypeCustom,
+ TimestampMs: ptr(int64(123456)),
+ },
+ },
+ validate: func(t *testing.T, frame string) {
+ if !strings.Contains(frame, "id: CUSTOM_123456\n") {
+ t.Error("expected frame to contain event ID")
+ }
+ },
+ },
+ {
+ name: "frame with newlines escaped",
+ jsonData: []byte("line1\nline2\rline3"),
+ eventType: "",
+ event: nil,
+ validate: func(t *testing.T, frame string) {
+ if !strings.Contains(frame, `line1\nline2\rline3`) {
+ t.Error("expected newlines to be escaped")
+ }
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ frame, err := writer.createSSEFrame(tt.jsonData, tt.eventType, tt.event)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if tt.validate != nil {
+ tt.validate(t, frame)
+ }
+ })
+ }
+}
+
+func ptr[T any](v T) *T {
+ return &v
+}
diff --git a/sdks/community/go/pkg/errors/error_types.go b/sdks/community/go/pkg/errors/error_types.go
new file mode 100644
index 000000000..168882e32
--- /dev/null
+++ b/sdks/community/go/pkg/errors/error_types.go
@@ -0,0 +1,789 @@
+// Package errors provides comprehensive error handling utilities for the ag-ui Go SDK.
+// It includes custom error types, severity-based handling, context management, and retry logic.
+package errors
+
+import (
+ "errors"
+ "fmt"
+ "time"
+)
+
+// Common sentinel errors
+var (
+ // ErrStateInvalid indicates an invalid state transition or state data
+ ErrStateInvalid = errors.New("invalid state")
+
+ // ErrValidationFailed indicates validation of input data failed
+ ErrValidationFailed = errors.New("validation failed")
+
+ // ErrConflict indicates a conflict in concurrent operations
+ ErrConflict = errors.New("operation conflict")
+
+ // ErrRetryExhausted indicates all retry attempts have been exhausted
+ ErrRetryExhausted = errors.New("retry attempts exhausted")
+
+ // ErrContextMissing indicates required context information is missing
+ ErrContextMissing = errors.New("required context missing")
+
+ // ErrOperationNotPermitted indicates the operation is not allowed
+ ErrOperationNotPermitted = errors.New("operation not permitted")
+
+ // Encoding-specific sentinel errors
+ // ErrEncodingNotSupported indicates the encoding format is not supported
+ ErrEncodingNotSupported = errors.New("encoding format not supported")
+
+ // ErrDecodingFailed indicates decoding of data failed
+ ErrDecodingFailed = errors.New("decoding failed")
+
+ // ErrEncodingFailed indicates encoding of data failed
+ ErrEncodingFailed = errors.New("encoding failed")
+
+ // ErrFormatNotRegistered indicates the format is not registered
+ ErrFormatNotRegistered = errors.New("format not registered")
+
+ // ErrInvalidMimeType indicates an invalid MIME type
+ ErrInvalidMimeType = errors.New("invalid MIME type")
+
+ // ErrStreamingNotSupported indicates streaming is not supported
+ ErrStreamingNotSupported = errors.New("streaming not supported")
+
+ // ErrChunkingFailed indicates chunking of data failed
+ ErrChunkingFailed = errors.New("chunking failed")
+
+ // ErrCompressionFailed indicates compression of data failed
+ ErrCompressionFailed = errors.New("compression failed")
+
+ // ErrSecurityViolation indicates a security policy violation
+ ErrSecurityViolation = errors.New("security violation")
+
+ // ErrCompatibilityCheck indicates a compatibility check failure
+ ErrCompatibilityCheck = errors.New("compatibility check failed")
+
+ // ErrNegotiationFailed indicates content negotiation failed
+ ErrNegotiationFailed = errors.New("negotiation failed")
+)
+
+// ErrorType represents different categories of errors for agents
+type ErrorType string
+
+const (
+ // ErrorTypeInvalidState indicates an invalid agent state transition
+ ErrorTypeInvalidState ErrorType = "invalid_state"
+
+ // ErrorTypeUnsupported indicates an unsupported operation
+ ErrorTypeUnsupported ErrorType = "unsupported"
+
+ // ErrorTypeTimeout indicates a timeout occurred
+ ErrorTypeTimeout ErrorType = "timeout"
+
+ // ErrorTypeValidation indicates validation failed
+ ErrorTypeValidation ErrorType = "validation"
+
+ // ErrorTypeNotFound indicates a resource was not found
+ ErrorTypeNotFound ErrorType = "not_found"
+
+ // ErrorTypePermission indicates insufficient permissions
+ ErrorTypePermission ErrorType = "permission"
+
+ // ErrorTypeExternal indicates an external service error
+ ErrorTypeExternal ErrorType = "external"
+
+ // ErrorTypeRateLimit indicates rate limiting errors
+ ErrorTypeRateLimit ErrorType = "rate_limit"
+)
+
+// Severity levels for errors
+type Severity int
+
+const (
+ // SeverityDebug indicates a debug-level error (informational)
+ SeverityDebug Severity = iota
+ // SeverityInfo indicates an informational error
+ SeverityInfo
+ // SeverityWarning indicates a warning that doesn't prevent operation
+ SeverityWarning
+ // SeverityError indicates a recoverable error
+ SeverityError
+ // SeverityCritical indicates a critical error requiring immediate attention
+ SeverityCritical
+ // SeverityFatal indicates a fatal error that requires termination
+ SeverityFatal
+)
+
+// String returns the string representation of severity
+func (s Severity) String() string {
+ switch s {
+ case SeverityDebug:
+ return "DEBUG"
+ case SeverityInfo:
+ return "INFO"
+ case SeverityWarning:
+ return "WARNING"
+ case SeverityError:
+ return "ERROR"
+ case SeverityCritical:
+ return "CRITICAL"
+ case SeverityFatal:
+ return "FATAL"
+ default:
+ return "UNKNOWN"
+ }
+}
+
+// BaseError provides common fields for all custom error types
+type BaseError struct {
+ // Code is a machine-readable error code
+ Code string
+
+ // Message is a human-readable error message
+ Message string
+
+ // Severity indicates the error severity
+ Severity Severity
+
+ // Timestamp is when the error occurred
+ Timestamp time.Time
+
+ // Details provides additional error context
+ Details map[string]interface{}
+
+ // Cause is the underlying error, if any
+ Cause error
+
+ // Retryable indicates if the operation can be retried
+ Retryable bool
+
+ // RetryAfter suggests when to retry (if retryable)
+ RetryAfter *time.Duration
+}
+
+// Error implements the error interface
+func (e *BaseError) Error() string {
+ if e.Cause != nil {
+ return fmt.Sprintf("[%s] %s: %s (caused by: %v)", e.Severity, e.Code, e.Message, e.Cause)
+ }
+ return fmt.Sprintf("[%s] %s: %s", e.Severity, e.Code, e.Message)
+}
+
+// Unwrap returns the underlying error
+func (e *BaseError) Unwrap() error {
+ return e.Cause
+}
+
+// WithDetail adds a detail to the error
+func (e *BaseError) WithDetail(key string, value interface{}) *BaseError {
+ if e.Details == nil {
+ e.Details = make(map[string]interface{})
+ }
+ e.Details[key] = value
+ return e
+}
+
+// WithCause adds an underlying cause to the error
+func (e *BaseError) WithCause(cause error) *BaseError {
+ e.Cause = cause
+ return e
+}
+
+// WithRetry marks the error as retryable with a suggested retry time
+func (e *BaseError) WithRetry(after time.Duration) *BaseError {
+ e.Retryable = true
+ e.RetryAfter = &after
+ return e
+}
+
+// StateError represents errors related to state management
+type StateError struct {
+ *BaseError
+
+ // StateID identifies the state that caused the error
+ StateID string
+
+ // CurrentState is the current state value (if available)
+ CurrentState interface{}
+
+ // ExpectedState is the expected state value (if applicable)
+ ExpectedState interface{}
+
+ // Transition describes the attempted state transition
+ Transition string
+}
+
+// NewStateError creates a new state error
+func NewStateError(code, message string) *StateError {
+ return &StateError{
+ BaseError: &BaseError{
+ Code: code,
+ Message: message,
+ Severity: SeverityError,
+ Timestamp: time.Now(),
+ Details: make(map[string]interface{}),
+ },
+ }
+}
+
+// Error implements the error interface with state-specific details
+func (e *StateError) Error() string {
+ base := e.BaseError.Error()
+ if e.StateID != "" {
+ base = fmt.Sprintf("%s (state: %s)", base, e.StateID)
+ }
+ if e.Transition != "" {
+ base = fmt.Sprintf("%s (transition: %s)", base, e.Transition)
+ }
+ return base
+}
+
+// WithStateID sets the state ID
+func (e *StateError) WithStateID(id string) *StateError {
+ e.StateID = id
+ return e
+}
+
+// WithStates sets the current and expected states
+func (e *StateError) WithStates(current, expected interface{}) *StateError {
+ e.CurrentState = current
+ e.ExpectedState = expected
+ return e
+}
+
+// WithTransition sets the attempted transition
+func (e *StateError) WithTransition(transition string) *StateError {
+ e.Transition = transition
+ return e
+}
+
+// ValidationError represents validation-related errors
+type ValidationError struct {
+ *BaseError
+
+ // Field identifies the field that failed validation
+ Field string
+
+ // Value is the invalid value
+ Value interface{}
+
+ // Rule is the validation rule that failed
+ Rule string
+
+ // FieldErrors contains field-specific validation errors
+ FieldErrors map[string][]string
+}
+
+// NewValidationError creates a new validation error
+func NewValidationError(code, message string) *ValidationError {
+ return &ValidationError{
+ BaseError: &BaseError{
+ Code: code,
+ Message: message,
+ Severity: SeverityWarning,
+ Timestamp: time.Now(),
+ Details: make(map[string]interface{}),
+ },
+ FieldErrors: make(map[string][]string),
+ }
+}
+
+// Error implements the error interface with validation-specific details
+func (e *ValidationError) Error() string {
+ base := e.BaseError.Error()
+ if e.Field != "" {
+ base = fmt.Sprintf("%s (field: %s)", base, e.Field)
+ }
+ if e.Rule != "" {
+ base = fmt.Sprintf("%s (rule: %s)", base, e.Rule)
+ }
+ return base
+}
+
+// WithField sets the field that failed validation
+func (e *ValidationError) WithField(field string, value interface{}) *ValidationError {
+ e.Field = field
+ e.Value = value
+ return e
+}
+
+// WithRule sets the validation rule that failed
+func (e *ValidationError) WithRule(rule string) *ValidationError {
+ e.Rule = rule
+ return e
+}
+
+// AddFieldError adds a field-specific error
+func (e *ValidationError) AddFieldError(field, message string) *ValidationError {
+ e.FieldErrors[field] = append(e.FieldErrors[field], message)
+ return e
+}
+
+// HasFieldErrors returns true if there are field-specific errors
+func (e *ValidationError) HasFieldErrors() bool {
+ return len(e.FieldErrors) > 0
+}
+
+// WithCause adds an underlying cause to the validation error and returns the ValidationError
+func (e *ValidationError) WithCause(cause error) *ValidationError {
+ e.BaseError.Cause = cause
+ return e
+}
+
+// WithDetail adds a detail to the validation error and returns the ValidationError
+func (e *ValidationError) WithDetail(key string, value interface{}) *ValidationError {
+ if e.BaseError.Details == nil {
+ e.BaseError.Details = make(map[string]interface{})
+ }
+ e.BaseError.Details[key] = value
+ return e
+}
+
+// ConflictError represents conflict-related errors
+type ConflictError struct {
+ *BaseError
+
+ // ResourceID identifies the resource in conflict
+ ResourceID string
+
+ // ResourceType describes the type of resource
+ ResourceType string
+
+ // ConflictingOperation describes the conflicting operation
+ ConflictingOperation string
+
+ // ResolutionStrategy suggests how to resolve the conflict
+ ResolutionStrategy string
+}
+
+// NewConflictError creates a new conflict error
+func NewConflictError(code, message string) *ConflictError {
+ return &ConflictError{
+ BaseError: &BaseError{
+ Code: code,
+ Message: message,
+ Severity: SeverityError,
+ Timestamp: time.Now(),
+ Details: make(map[string]interface{}),
+ },
+ }
+}
+
+// Error implements the error interface with conflict-specific details
+func (e *ConflictError) Error() string {
+ base := e.BaseError.Error()
+ if e.ResourceType != "" && e.ResourceID != "" {
+ base = fmt.Sprintf("%s (resource: %s/%s)", base, e.ResourceType, e.ResourceID)
+ }
+ if e.ConflictingOperation != "" {
+ base = fmt.Sprintf("%s (operation: %s)", base, e.ConflictingOperation)
+ }
+ return base
+}
+
+// WithResource sets the conflicting resource details
+func (e *ConflictError) WithResource(resourceType, resourceID string) *ConflictError {
+ e.ResourceType = resourceType
+ e.ResourceID = resourceID
+ return e
+}
+
+// WithOperation sets the conflicting operation
+func (e *ConflictError) WithOperation(operation string) *ConflictError {
+ e.ConflictingOperation = operation
+ return e
+}
+
+// WithResolution sets the suggested resolution strategy
+func (e *ConflictError) WithResolution(strategy string) *ConflictError {
+ e.ResolutionStrategy = strategy
+ return e
+}
+
+// IsRetryable checks if an error is retryable
+func IsRetryable(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ // Check if it's one of our custom errors
+ switch e := err.(type) {
+ case *BaseError:
+ return e.Retryable
+ case *StateError:
+ return e.BaseError.Retryable
+ case *ValidationError:
+ return e.BaseError.Retryable
+ case *ConflictError:
+ return e.BaseError.Retryable
+ }
+
+ // Check wrapped errors
+ var base *BaseError
+ if errors.As(err, &base) {
+ return base.Retryable
+ }
+
+ return false
+}
+
+// GetSeverity extracts the severity from an error
+func GetSeverity(err error) Severity {
+ if err == nil {
+ return SeverityInfo
+ }
+
+ // Check if it's one of our custom errors
+ switch e := err.(type) {
+ case *BaseError:
+ return e.Severity
+ case *StateError:
+ return e.BaseError.Severity
+ case *ValidationError:
+ return e.BaseError.Severity
+ case *ConflictError:
+ return e.BaseError.Severity
+ case *EncodingError:
+ return e.BaseError.Severity
+ case *SecurityError:
+ return e.BaseError.Severity
+ }
+
+ // Check wrapped errors
+ var base *BaseError
+ if errors.As(err, &base) {
+ return base.Severity
+ }
+
+ // Default severity for unknown errors
+ return SeverityError
+}
+
+// GetRetryAfter extracts the retry after duration from an error
+func GetRetryAfter(err error) *time.Duration {
+ if err == nil {
+ return nil
+ }
+
+ // Check if it's one of our custom errors
+ switch e := err.(type) {
+ case *BaseError:
+ return e.RetryAfter
+ case *StateError:
+ return e.BaseError.RetryAfter
+ case *ValidationError:
+ return e.BaseError.RetryAfter
+ case *ConflictError:
+ return e.BaseError.RetryAfter
+ case *EncodingError:
+ return e.BaseError.RetryAfter
+ case *SecurityError:
+ return e.BaseError.RetryAfter
+ }
+
+ // Check wrapped errors
+ var base *BaseError
+ if errors.As(err, &base) {
+ return base.RetryAfter
+ }
+
+ return nil
+}
+
+// EncodingError represents encoding/decoding-related errors
+type EncodingError struct {
+ *BaseError
+
+ // Format identifies the encoding format
+ Format string
+
+ // Operation describes the operation that failed (encode/decode/validate)
+ Operation string
+
+ // Data contains the problematic data (if safe to include)
+ Data interface{}
+
+ // Position indicates the position where the error occurred
+ Position int64
+
+ // MimeType is the MIME type being processed
+ MimeType string
+}
+
+// NewEncodingError creates a new encoding error
+func NewEncodingError(code, message string) *EncodingError {
+ return &EncodingError{
+ BaseError: &BaseError{
+ Code: code,
+ Message: message,
+ Severity: SeverityError,
+ Timestamp: time.Now(),
+ Details: make(map[string]interface{}),
+ },
+ }
+}
+
+// Error implements the error interface with encoding-specific details
+func (e *EncodingError) Error() string {
+ // Start with base message without cause
+ base := fmt.Sprintf("[%s] %s: %s", e.Severity, e.Code, e.Message)
+
+ // Add encoding-specific details first
+ if e.Format != "" {
+ base = fmt.Sprintf("%s (format: %s)", base, e.Format)
+ }
+ if e.Operation != "" {
+ base = fmt.Sprintf("%s (operation: %s)", base, e.Operation)
+ }
+ if e.MimeType != "" {
+ base = fmt.Sprintf("%s (mime: %s)", base, e.MimeType)
+ }
+ if e.Position > 0 {
+ base = fmt.Sprintf("%s (position: %d)", base, e.Position)
+ }
+
+ // Add cause at the end
+ if e.Cause != nil {
+ base = fmt.Sprintf("%s (caused by: %v)", base, e.Cause)
+ }
+
+ return base
+}
+
+// WithFormat sets the encoding format
+func (e *EncodingError) WithFormat(format string) *EncodingError {
+ e.Format = format
+ return e
+}
+
+// WithOperation sets the operation that failed
+func (e *EncodingError) WithOperation(operation string) *EncodingError {
+ e.Operation = operation
+ return e
+}
+
+// WithMimeType sets the MIME type
+func (e *EncodingError) WithMimeType(mimeType string) *EncodingError {
+ e.MimeType = mimeType
+ return e
+}
+
+// WithPosition sets the position where the error occurred
+func (e *EncodingError) WithPosition(position int64) *EncodingError {
+ e.Position = position
+ return e
+}
+
+// WithData sets the problematic data (use with caution for sensitive data)
+func (e *EncodingError) WithData(data interface{}) *EncodingError {
+ e.Data = data
+ return e
+}
+
+// WithCause adds an underlying cause to the encoding error and returns the EncodingError
+func (e *EncodingError) WithCause(cause error) *EncodingError {
+ e.BaseError.Cause = cause
+ return e
+}
+
+// SecurityError represents security-related errors
+type SecurityError struct {
+ *BaseError
+
+ // ViolationType describes the type of security violation
+ ViolationType string
+
+ // Pattern contains the detected pattern (if applicable)
+ Pattern string
+
+ // Location describes where the violation was detected
+ Location string
+
+ // RiskLevel indicates the risk level of the violation
+ RiskLevel string
+}
+
+// NewSecurityError creates a new security error
+func NewSecurityError(code, message string) *SecurityError {
+ return &SecurityError{
+ BaseError: &BaseError{
+ Code: code,
+ Message: message,
+ Severity: SeverityCritical,
+ Timestamp: time.Now(),
+ Details: make(map[string]interface{}),
+ },
+ }
+}
+
+// Error implements the error interface with security-specific details
+func (e *SecurityError) Error() string {
+ base := e.BaseError.Error()
+ if e.ViolationType != "" {
+ base = fmt.Sprintf("%s (violation: %s)", base, e.ViolationType)
+ }
+ if e.Pattern != "" {
+ base = fmt.Sprintf("%s (pattern: %s)", base, e.Pattern)
+ }
+ if e.Location != "" {
+ base = fmt.Sprintf("%s (location: %s)", base, e.Location)
+ }
+ if e.RiskLevel != "" {
+ base = fmt.Sprintf("%s (risk: %s)", base, e.RiskLevel)
+ }
+ return base
+}
+
+// WithViolationType sets the type of security violation
+func (e *SecurityError) WithViolationType(violationType string) *SecurityError {
+ e.ViolationType = violationType
+ return e
+}
+
+// WithPattern sets the detected pattern
+func (e *SecurityError) WithPattern(pattern string) *SecurityError {
+ e.Pattern = pattern
+ return e
+}
+
+// WithLocation sets the location where the violation was detected
+func (e *SecurityError) WithLocation(location string) *SecurityError {
+ e.Location = location
+ return e
+}
+
+// WithRiskLevel sets the risk level of the violation
+func (e *SecurityError) WithRiskLevel(riskLevel string) *SecurityError {
+ e.RiskLevel = riskLevel
+ return e
+}
+
+// WithDetail adds a detail to the security error and returns the SecurityError
+func (e *SecurityError) WithDetail(key string, value interface{}) *SecurityError {
+ if e.BaseError.Details == nil {
+ e.BaseError.Details = make(map[string]interface{})
+ }
+ e.BaseError.Details[key] = value
+ return e
+}
+
+// WithCause adds an underlying cause to the security error and returns the SecurityError
+func (e *SecurityError) WithCause(cause error) *SecurityError {
+ e.BaseError.Cause = cause
+ return e
+}
+
+// AgentError represents errors specific to agent operations
+type AgentError struct {
+ *BaseError
+
+ // Type categorizes the error
+ Type ErrorType
+
+ // Agent identifies which agent encountered the error
+ Agent string
+
+ // EventID identifies the event being processed when error occurred (if applicable)
+ EventID string
+}
+
+// NewAgentError creates a new agent error
+func NewAgentError(errorType ErrorType, message, agent string) *AgentError {
+ return &AgentError{
+ BaseError: &BaseError{
+ Code: string(errorType),
+ Message: message,
+ Severity: SeverityError,
+ Timestamp: time.Now(),
+ Details: make(map[string]interface{}),
+ },
+ Type: errorType,
+ Agent: agent,
+ }
+}
+
+// Error implements the error interface with agent-specific details
+func (e *AgentError) Error() string {
+ base := e.BaseError.Error()
+ if e.Agent != "" {
+ base = fmt.Sprintf("%s (agent: %s)", base, e.Agent)
+ }
+ if e.EventID != "" {
+ base = fmt.Sprintf("%s (event: %s)", base, e.EventID)
+ }
+ return base
+}
+
+// WithAgent sets the agent name
+func (e *AgentError) WithAgent(agent string) *AgentError {
+ e.Agent = agent
+ return e
+}
+
+// WithEventID sets the event ID
+func (e *AgentError) WithEventID(eventID string) *AgentError {
+ e.EventID = eventID
+ return e
+}
+
+// OperationError represents errors that occur during specific operations with context preservation
+type OperationError struct {
+ Op string // Operation that failed
+ Target string // What was being operated on
+ Err error // Underlying error
+ Code string // Error code for programmatic handling
+ Time time.Time // When the error occurred
+ Details map[string]interface{} // Additional context
+}
+
+// NewOperationError creates a new OperationError
+func NewOperationError(op, target string, err error) *OperationError {
+ return &OperationError{
+ Op: op,
+ Target: target,
+ Err: err,
+ Time: time.Now(),
+ Details: make(map[string]interface{}),
+ }
+}
+
+// Error implements the error interface
+func (e *OperationError) Error() string {
+ return fmt.Sprintf("operation %s on %s failed: %v", e.Op, e.Target, e.Err)
+}
+
+// Unwrap returns the underlying error
+func (e *OperationError) Unwrap() error {
+ return e.Err
+}
+
+// WithCode sets the error code for programmatic handling
+func (e *OperationError) WithCode(code string) *OperationError {
+ e.Code = code
+ return e
+}
+
+// WithDetail adds additional context to the error
+func (e *OperationError) WithDetail(key string, value interface{}) *OperationError {
+ if e.Details == nil {
+ e.Details = make(map[string]interface{})
+ }
+ e.Details[key] = value
+ return e
+}
+
+// WithCause sets the underlying cause of the error
+func (e *OperationError) WithCause(cause error) *OperationError {
+ e.Err = cause
+ return e
+}
+
+// String returns a string representation of the error for debugging
+func (e *OperationError) String() string {
+ details := ""
+ if len(e.Details) > 0 {
+ details = fmt.Sprintf(" (details: %v)", e.Details)
+ }
+ codeStr := ""
+ if e.Code != "" {
+ codeStr = fmt.Sprintf(" [%s]", e.Code)
+ }
+ return fmt.Sprintf("OperationError{Op:%s, Target:%s, Code:%s, Time:%s}%s%s: %v",
+ e.Op, e.Target, e.Code, e.Time.Format(time.RFC3339), codeStr, details, e.Err)
+}
diff --git a/sdks/community/go/pkg/errors/error_utils.go b/sdks/community/go/pkg/errors/error_utils.go
new file mode 100644
index 000000000..02d946626
--- /dev/null
+++ b/sdks/community/go/pkg/errors/error_utils.go
@@ -0,0 +1,329 @@
+package errors
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "math"
+ "math/rand"
+ "strings"
+ "time"
+)
+
+// Wrap wraps an error with additional context
+func Wrap(err error, message string) error {
+ if err == nil {
+ return nil
+ }
+ return fmt.Errorf("%s: %w", message, err)
+}
+
+// Wrapf wraps an error with formatted context
+func Wrapf(err error, format string, args ...interface{}) error {
+ if err == nil {
+ return nil
+ }
+ return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
+}
+
+// Is checks if an error matches a target error
+func Is(err, target error) bool {
+ return errors.Is(err, target)
+}
+
+// As attempts to extract a specific error type
+func As(err error, target interface{}) bool {
+ return errors.As(err, target)
+}
+
+// Cause returns the root cause of an error
+func Cause(err error) error {
+ for {
+ unwrapper, ok := err.(interface{ Unwrap() error })
+ if !ok {
+ break
+ }
+ unwrapped := unwrapper.Unwrap()
+ if unwrapped == nil {
+ break
+ }
+ err = unwrapped
+ }
+ return err
+}
+
+// Chain creates a chain of errors
+func Chain(errs ...error) error {
+ var nonNil []error
+ for _, err := range errs {
+ if err != nil {
+ nonNil = append(nonNil, err)
+ }
+ }
+
+ switch len(nonNil) {
+ case 0:
+ return nil
+ case 1:
+ return nonNil[0]
+ default:
+ return &ChainedError{errors: nonNil}
+ }
+}
+
+// ChainedError represents multiple errors
+type ChainedError struct {
+ errors []error
+}
+
+// Error returns the combined error message
+func (e *ChainedError) Error() string {
+ if len(e.errors) == 0 {
+ return ""
+ }
+
+ var messages []string
+ for _, err := range e.errors {
+ messages = append(messages, err.Error())
+ }
+ return strings.Join(messages, "; ")
+}
+
+// Errors returns all errors in the chain
+func (e *ChainedError) Errors() []error {
+ return e.errors
+}
+
+// Unwrap returns the first error in the chain
+func (e *ChainedError) Unwrap() error {
+ if len(e.errors) > 0 {
+ return e.errors[0]
+ }
+ return nil
+}
+
+// RetryConfig configures retry behavior
+type RetryConfig struct {
+ // MaxAttempts is the maximum number of attempts (0 = unlimited)
+ MaxAttempts int
+
+ // InitialDelay is the initial delay between retries
+ InitialDelay time.Duration
+
+ // MaxDelay is the maximum delay between retries
+ MaxDelay time.Duration
+
+ // Multiplier is the delay multiplier for exponential backoff
+ Multiplier float64
+
+ // Jitter adds randomness to delays (0.0 to 1.0)
+ Jitter float64
+
+ // RetryIf determines if an error should be retried
+ RetryIf func(error) bool
+
+ // OnRetry is called before each retry attempt
+ OnRetry func(attempt int, err error, delay time.Duration)
+}
+
+// DefaultRetryConfig returns a default retry configuration
+func DefaultRetryConfig() *RetryConfig {
+ return &RetryConfig{
+ MaxAttempts: 3,
+ InitialDelay: 100 * time.Millisecond,
+ MaxDelay: 30 * time.Second,
+ Multiplier: 2.0,
+ Jitter: 0.1,
+ RetryIf: IsRetryable,
+ }
+}
+
+// Retry executes a function with retry logic
+func Retry(ctx context.Context, config *RetryConfig, fn func() error) error {
+ if config == nil {
+ config = DefaultRetryConfig()
+ }
+
+ var lastErr error
+ delay := config.InitialDelay
+
+ for attempt := 1; config.MaxAttempts == 0 || attempt <= config.MaxAttempts; attempt++ {
+ // Execute the function
+ err := fn()
+ if err == nil {
+ return nil
+ }
+
+ lastErr = err
+
+ // Check if we should retry
+ if config.RetryIf != nil && !config.RetryIf(err) {
+ return err
+ }
+
+ // Check if this is the last attempt
+ if config.MaxAttempts > 0 && attempt >= config.MaxAttempts {
+ break
+ }
+
+ // Calculate delay with jitter
+ actualDelay := applyJitter(delay, config.Jitter)
+
+ // Call retry callback if provided
+ if config.OnRetry != nil {
+ config.OnRetry(attempt, err, actualDelay)
+ }
+
+ // Wait or return if context is done
+ select {
+ case <-ctx.Done():
+ return Chain(lastErr, ctx.Err())
+ case <-time.After(actualDelay):
+ }
+
+ // Calculate next delay
+ delay = calculateNextDelay(delay, config.Multiplier, config.MaxDelay)
+ }
+
+ return NewBaseError("RETRY_EXHAUSTED", "retry attempts exhausted").
+ WithCause(lastErr).
+ WithDetail("attempts", config.MaxAttempts)
+}
+
+// applyJitter adds randomness to a duration
+func applyJitter(d time.Duration, jitter float64) time.Duration {
+ if jitter <= 0 {
+ return d
+ }
+
+ jitter = math.Min(jitter, 1.0)
+ jitterRange := float64(d) * jitter
+ jitterValue := (rand.Float64() * 2 * jitterRange) - jitterRange
+
+ return time.Duration(float64(d) + jitterValue)
+}
+
+// calculateNextDelay calculates the next retry delay
+func calculateNextDelay(current time.Duration, multiplier float64, max time.Duration) time.Duration {
+ next := time.Duration(float64(current) * multiplier)
+ if next > max {
+ return max
+ }
+ return next
+}
+
+// NewBaseError creates a new base error
+func NewBaseError(code, message string) *BaseError {
+ return &BaseError{
+ Code: code,
+ Message: message,
+ Severity: SeverityError,
+ Timestamp: time.Now(),
+ Details: make(map[string]interface{}),
+ }
+}
+
+// Encoding-specific error creation functions
+
+// NewDecodingError creates a decoding error
+func NewDecodingError(code, message string) *EncodingError {
+ return NewEncodingError(code, message).WithOperation("decode")
+}
+
+// NewStreamingError creates a streaming error
+func NewStreamingError(code, message string) *EncodingError {
+ return NewEncodingError(code, message).WithOperation("stream")
+}
+
+// NewXSSError creates an XSS detection error
+func NewXSSError(message, pattern string) *SecurityError {
+ return NewSecurityError("XSS_DETECTED", message).
+ WithViolationType("cross_site_scripting").
+ WithPattern(pattern).
+ WithRiskLevel("high")
+}
+
+// NewSQLInjectionError creates a SQL injection detection error
+func NewSQLInjectionError(message, pattern string) *SecurityError {
+ return NewSecurityError("SQL_INJECTION_DETECTED", message).
+ WithViolationType("sql_injection").
+ WithPattern(pattern).
+ WithRiskLevel("critical")
+}
+
+// NewScriptInjectionError creates a script injection detection error
+func NewScriptInjectionError(message, pattern string) *SecurityError {
+ return NewSecurityError("SCRIPT_INJECTION_DETECTED", message).
+ WithViolationType("script_injection").
+ WithPattern(pattern).
+ WithRiskLevel("high")
+}
+
+// NewDOSError creates a denial of service detection error
+func NewDOSError(message, location string) *SecurityError {
+ return NewSecurityError("DOS_ATTACK_DETECTED", message).
+ WithViolationType("denial_of_service").
+ WithLocation(location).
+ WithRiskLevel("medium")
+}
+
+// NewPathTraversalError creates a path traversal detection error
+func NewPathTraversalError(message, pattern string) *SecurityError {
+ return NewSecurityError("PATH_TRAVERSAL_DETECTED", message).
+ WithViolationType("path_traversal").
+ WithPattern(pattern).
+ WithRiskLevel("high")
+}
+
+// IsSecurityError checks if an error is a security error
+func IsSecurityError(err error) bool {
+ if err == nil {
+ return false
+ }
+ var securityErr *SecurityError
+ return errors.As(err, &securityErr)
+}
+
+// WithOperation adds operation context to errors
+func WithOperation(op, target string, err error) error {
+ if err == nil {
+ return nil
+ }
+ return NewOperationError(op, target, err)
+}
+
+// Common error codes for the system
+const (
+ // Validation error codes
+ CodeValidationFailed = "VALIDATION_FAILED"
+ CodeMissingEvent = "MISSING_EVENT"
+ CodeMissingEventType = "MISSING_EVENT_TYPE"
+ CodeNegativeTimestamp = "NEGATIVE_TIMESTAMP"
+ CodeIDTooLong = "ID_TOO_LONG"
+
+ // Registry error codes
+ CodeFormatNotRegistered = "FORMAT_NOT_REGISTERED"
+ CodeNilFactory = "NIL_FACTORY"
+ CodeEmptyMimeType = "EMPTY_MIME_TYPE"
+
+ // Encoding/Decoding error codes
+ CodeEncodingFailed = "ENCODING_FAILED"
+ CodeDecodingFailed = "DECODING_FAILED"
+
+ // Security error codes
+ CodeSecurityViolation = "SECURITY_VIOLATION"
+ CodeXSSDetected = "XSS_DETECTED"
+ CodeInvalidData = "INVALID_DATA"
+ CodeSizeExceeded = "SIZE_EXCEEDED"
+ CodeDepthExceeded = "DEPTH_EXCEEDED"
+ CodeNullByteDetected = "NULL_BYTE_DETECTED"
+ CodeInvalidUTF8 = "INVALID_UTF8"
+ CodeHTMLNotAllowed = "HTML_NOT_ALLOWED"
+ CodeEntityExpansion = "ENTITY_EXPANSION"
+ CodeZipBomb = "ZIP_BOMB"
+
+ // Negotiation error codes
+ CodeNegotiationFailed = "NEGOTIATION_FAILED"
+ CodeNoSuitableFormat = "NO_SUITABLE_FORMAT"
+ CodeUnsupportedFormat = "UNSUPPORTED_FORMAT"
+)