From 63e1a7becfcb629444ef228092adc2671518555d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Oct 2025 20:55:23 +0000 Subject: [PATCH] feat: Add Jules Agent SDK for Go This commit introduces a complete Go SDK equivalent to the existing Python SDK for the Jules Agent API. Features: - Sessions API: Create and manage AI agent sessions with full lifecycle support - Activities API: Track and retrieve session activities with automatic pagination - Sources API: Manage source repositories and retrieve repository information - Comprehensive error handling with specific error types (Authentication, NotFound, Validation, RateLimit, Server) - Automatic retry logic with exponential backoff for failed requests - HTTP connection pooling for efficient resource management - Context support for cancellation and timeouts - Wait for completion with configurable polling intervals Project Structure: - jules/: Core SDK package with all API clients and models - examples/: Sample code demonstrating SDK usage - README.md: Comprehensive documentation with examples - go.mod: Go module definition The SDK follows Go best practices and provides the same functionality as the Python SDK with idiomatic Go patterns including: - Struct-based configuration - Context for request lifecycle management - Error wrapping for better error handling - Interfaces for extensibility Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- jules-agent-sdk-go/.gitignore | 33 ++ jules-agent-sdk-go/README.md | 339 ++++++++++++++++++ jules-agent-sdk-go/examples/basic_usage.go | 89 +++++ jules-agent-sdk-go/examples/simple_example.go | 45 +++ jules-agent-sdk-go/go.mod | 3 + jules-agent-sdk-go/jules/activities.go | 127 +++++++ jules-agent-sdk-go/jules/client.go | 208 +++++++++++ jules-agent-sdk-go/jules/config.go | 93 +++++ jules-agent-sdk-go/jules/errors.go | 115 ++++++ jules-agent-sdk-go/jules/jules_client.go | 44 +++ jules-agent-sdk-go/jules/models.go | 180 ++++++++++ jules-agent-sdk-go/jules/sessions.go | 213 +++++++++++ jules-agent-sdk-go/jules/sources.go | 130 +++++++ 13 files changed, 1619 insertions(+) create mode 100644 jules-agent-sdk-go/.gitignore create mode 100644 jules-agent-sdk-go/README.md create mode 100644 jules-agent-sdk-go/examples/basic_usage.go create mode 100644 jules-agent-sdk-go/examples/simple_example.go create mode 100644 jules-agent-sdk-go/go.mod create mode 100644 jules-agent-sdk-go/jules/activities.go create mode 100644 jules-agent-sdk-go/jules/client.go create mode 100644 jules-agent-sdk-go/jules/config.go create mode 100644 jules-agent-sdk-go/jules/errors.go create mode 100644 jules-agent-sdk-go/jules/jules_client.go create mode 100644 jules-agent-sdk-go/jules/models.go create mode 100644 jules-agent-sdk-go/jules/sessions.go create mode 100644 jules-agent-sdk-go/jules/sources.go diff --git a/jules-agent-sdk-go/.gitignore b/jules-agent-sdk-go/.gitignore new file mode 100644 index 0000000..9697173 --- /dev/null +++ b/jules-agent-sdk-go/.gitignore @@ -0,0 +1,33 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS specific files +.DS_Store +Thumbs.db + +# Environment variables +.env +.env.local diff --git a/jules-agent-sdk-go/README.md b/jules-agent-sdk-go/README.md new file mode 100644 index 0000000..7eff2b1 --- /dev/null +++ b/jules-agent-sdk-go/README.md @@ -0,0 +1,339 @@ +# Jules Agent SDK for Go + +The official Go SDK for the Jules Agent API. Jules is an AI-powered agent that helps automate software development tasks. + +## Features + +- **Sessions API**: Create and manage AI agent sessions +- **Activities API**: Track and retrieve session activities +- **Sources API**: Manage source repositories +- **Automatic Retries**: Built-in exponential backoff for failed requests +- **Connection Pooling**: Efficient HTTP connection management +- **Comprehensive Error Handling**: Specific error types for different failure scenarios +- **Context Support**: Full support for Go's context package for cancellation and timeouts + +## Installation + +```bash +go get github.com/sashimikun/jules-agent-sdk-go +``` + +## Quick Start + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/sashimikun/jules-agent-sdk-go/jules" +) + +func main() { + // Create client + client, err := jules.NewClient(os.Getenv("JULES_API_KEY")) + if err != nil { + log.Fatal(err) + } + defer client.Close() + + ctx := context.Background() + + // Create a session + session, err := client.Sessions.Create(ctx, &jules.CreateSessionRequest{ + Prompt: "Fix the bug in the login function", + Source: "sources/my-repo", + Title: "Fix Login Bug", + }) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Session created: %s\n", session.ID) + + // Wait for completion + result, err := client.Sessions.WaitForCompletion(ctx, session.ID, nil) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Final state: %s\n", result.State) +} +``` + +## API Reference + +### Client Initialization + +#### Basic Client + +```go +client, err := jules.NewClient(apiKey) +if err != nil { + log.Fatal(err) +} +defer client.Close() +``` + +#### Custom Configuration + +```go +config := &jules.Config{ + APIKey: apiKey, + BaseURL: "https://julius.googleapis.com/v1alpha", + Timeout: 30 * time.Second, + MaxRetries: 3, + RetryBackoffFactor: 1.0, + MaxBackoff: 10 * time.Second, + VerifySSL: true, +} + +client, err := jules.NewClientWithConfig(config) +if err != nil { + log.Fatal(err) +} +defer client.Close() +``` + +### Sessions API + +#### Create a Session + +```go +session, err := client.Sessions.Create(ctx, &jules.CreateSessionRequest{ + Prompt: "Add authentication to the API", + Source: "sources/my-repo", + StartingBranch: "main", + Title: "Add Authentication", + RequirePlanApproval: false, +}) +``` + +#### Get a Session + +```go +session, err := client.Sessions.Get(ctx, "session-id") +``` + +#### List Sessions + +```go +response, err := client.Sessions.List(ctx, &jules.ListOptions{ + PageSize: 10, + PageToken: "", +}) +``` + +#### Approve a Plan + +```go +err := client.Sessions.ApprovePlan(ctx, "session-id") +``` + +#### Send a Message + +```go +err := client.Sessions.SendMessage(ctx, "session-id", "Please also add rate limiting") +``` + +#### Wait for Completion + +```go +session, err := client.Sessions.WaitForCompletion(ctx, "session-id", &jules.WaitForCompletionOptions{ + PollInterval: 5 * time.Second, + Timeout: 600 * time.Second, +}) +``` + +### Activities API + +#### Get an Activity + +```go +activity, err := client.Activities.Get(ctx, "session-id", "activity-id") +``` + +#### List Activities + +```go +response, err := client.Activities.List(ctx, "session-id", &jules.ListOptions{ + PageSize: 10, + PageToken: "", +}) +``` + +#### List All Activities (with automatic pagination) + +```go +activities, err := client.Activities.ListAll(ctx, "session-id") +``` + +### Sources API + +#### Get a Source + +```go +source, err := client.Sources.Get(ctx, "source-id") +``` + +#### List Sources + +```go +response, err := client.Sources.List(ctx, &jules.SourcesListOptions{ + Filter: "owner:myorg", + PageSize: 10, + PageToken: "", +}) +``` + +#### List All Sources (with automatic pagination) + +```go +sources, err := client.Sources.ListAll(ctx, "owner:myorg") +``` + +## Data Models + +### Session States + +The SDK defines the following session states: + +- `SessionStateUnspecified`: Default unspecified state +- `SessionStateQueued`: Session is queued +- `SessionStatePlanning`: Session is in planning phase +- `SessionStateAwaitingPlanApproval`: Session is waiting for plan approval +- `SessionStateAwaitingUserFeedback`: Session is waiting for user feedback +- `SessionStateInProgress`: Session is in progress +- `SessionStatePaused`: Session is paused +- `SessionStateFailed`: Session has failed +- `SessionStateCompleted`: Session has completed + +### Error Types + +The SDK provides specific error types for different failure scenarios: + +- `APIError`: Base error type for all API errors +- `AuthenticationError`: 401 authentication errors +- `NotFoundError`: 404 not found errors +- `ValidationError`: 400 validation errors +- `RateLimitError`: 429 rate limit errors (includes RetryAfter value) +- `ServerError`: 5xx server errors +- `TimeoutError`: Timeout errors + +### Error Handling Example + +```go +session, err := client.Sessions.Get(ctx, "session-id") +if err != nil { + switch e := err.(type) { + case *jules.AuthenticationError: + log.Printf("Authentication failed: %s", e.Message) + case *jules.NotFoundError: + log.Printf("Session not found: %s", e.Message) + case *jules.RateLimitError: + log.Printf("Rate limited. Retry after %d seconds", e.RetryAfter) + default: + log.Printf("Error: %v", err) + } + return +} +``` + +## Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| APIKey | string | (required) | Your Jules API key | +| BaseURL | string | `https://julius.googleapis.com/v1alpha` | API base URL | +| Timeout | time.Duration | 30s | HTTP request timeout | +| MaxRetries | int | 3 | Maximum number of retry attempts | +| RetryBackoffFactor | float64 | 1.0 | Exponential backoff factor | +| MaxBackoff | time.Duration | 10s | Maximum backoff duration | +| VerifySSL | bool | true | Enable SSL certificate verification | + +## Advanced Features + +### Connection Pooling + +The SDK automatically manages HTTP connection pooling with: +- 10 max idle connections +- 10 max idle connections per host +- 90 second idle connection timeout + +### Automatic Retries + +The SDK automatically retries: +- Network errors (connection failures, timeouts) +- 5xx server errors + +The SDK does NOT retry: +- 4xx client errors (these indicate a problem with your request) +- 429 rate limit errors (returns immediately with retry-after information) + +Retry behavior uses exponential backoff: +``` +backoff = RetryBackoffFactor * 2^(attempt-1) +capped at MaxBackoff +``` + +### Statistics + +You can get request statistics from the client: + +```go +stats := client.Stats() +fmt.Printf("Total Requests: %d\n", stats["request_count"]) +fmt.Printf("Total Errors: %d\n", stats["error_count"]) +``` + +## Examples + +See the [examples](./examples) directory for more usage examples: + +- `simple_example.go`: Basic session creation and completion +- `basic_usage.go`: Comprehensive example covering all major features + +## Development + +### Running Examples + +```bash +# Set your API key +export JULES_API_KEY="your-api-key-here" + +# Run the simple example +go run examples/simple_example.go + +# Run the comprehensive example +go run examples/basic_usage.go +``` + +### Building + +```bash +cd jules-agent-sdk-go +go build ./jules +``` + +### Testing + +```bash +go test ./jules -v +``` + +## License + +MIT License - See LICENSE file for details + +## Support + +For issues and questions: +- GitHub Issues: [https://github.com/sashimikun/jules-agent-sdk-go/issues](https://github.com/sashimikun/jules-agent-sdk-go/issues) +- Documentation: [https://docs.julius.googleapis.com](https://docs.julius.googleapis.com) + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/jules-agent-sdk-go/examples/basic_usage.go b/jules-agent-sdk-go/examples/basic_usage.go new file mode 100644 index 0000000..3a67c28 --- /dev/null +++ b/jules-agent-sdk-go/examples/basic_usage.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/sashimikun/jules-agent-sdk-go/jules" +) + +func main() { + // Get API key from environment variable + apiKey := os.Getenv("JULES_API_KEY") + if apiKey == "" { + log.Fatal("JULES_API_KEY environment variable is required") + } + + // Create a new Jules client + client, err := jules.NewClient(apiKey) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + defer client.Close() + + ctx := context.Background() + + // Example 1: List all sources + fmt.Println("=== Listing Sources ===") + sources, err := client.Sources.ListAll(ctx, "") + if err != nil { + log.Fatalf("Failed to list sources: %v", err) + } + + for _, source := range sources { + fmt.Printf("Source: %s (ID: %s)\n", source.Name, source.ID) + if source.GitHubRepo != nil { + fmt.Printf(" GitHub: %s/%s\n", source.GitHubRepo.Owner, source.GitHubRepo.Repo) + } + } + + // Example 2: Create a new session + fmt.Println("\n=== Creating Session ===") + session, err := client.Sessions.Create(ctx, &jules.CreateSessionRequest{ + Prompt: "Add a new feature to improve error handling", + Source: sources[0].Name, // Use the first source + Title: "Improve Error Handling", + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + fmt.Printf("Session created: %s\n", session.ID) + fmt.Printf("State: %s\n", session.State) + fmt.Printf("URL: %s\n", session.URL) + + // Example 3: Wait for session completion + fmt.Println("\n=== Waiting for Completion ===") + completedSession, err := client.Sessions.WaitForCompletion(ctx, session.ID, nil) + if err != nil { + log.Fatalf("Failed to wait for completion: %v", err) + } + + fmt.Printf("Session completed: %s\n", completedSession.State) + if completedSession.Output != nil && completedSession.Output.PullRequest != nil { + fmt.Printf("Pull Request: %s\n", completedSession.Output.PullRequest.URL) + } + + // Example 4: List activities for the session + fmt.Println("\n=== Listing Activities ===") + activities, err := client.Activities.ListAll(ctx, session.ID) + if err != nil { + log.Fatalf("Failed to list activities: %v", err) + } + + for _, activity := range activities { + fmt.Printf("Activity: %s\n", activity.Description) + fmt.Printf(" Originator: %s\n", activity.Originator) + if activity.CreateTime != nil { + fmt.Printf(" Created: %s\n", activity.CreateTime) + } + } + + // Display client statistics + fmt.Println("\n=== Client Statistics ===") + stats := client.Stats() + fmt.Printf("Total Requests: %d\n", stats["request_count"]) + fmt.Printf("Total Errors: %d\n", stats["error_count"]) +} diff --git a/jules-agent-sdk-go/examples/simple_example.go b/jules-agent-sdk-go/examples/simple_example.go new file mode 100644 index 0000000..e005c4f --- /dev/null +++ b/jules-agent-sdk-go/examples/simple_example.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/sashimikun/jules-agent-sdk-go/jules" +) + +func main() { + // Create client + client, err := jules.NewClient(os.Getenv("JULES_API_KEY")) + if err != nil { + log.Fatal(err) + } + defer client.Close() + + ctx := context.Background() + + // Create a session + session, err := client.Sessions.Create(ctx, &jules.CreateSessionRequest{ + Prompt: "Fix the bug in the login function", + Source: "sources/my-repo", + Title: "Fix Login Bug", + }) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Session created: %s\n", session.ID) + fmt.Printf("State: %s\n", session.State) + + // Wait for completion + result, err := client.Sessions.WaitForCompletion(ctx, session.ID, nil) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Final state: %s\n", result.State) + if result.Output != nil && result.Output.PullRequest != nil { + fmt.Printf("PR URL: %s\n", result.Output.PullRequest.URL) + } +} diff --git a/jules-agent-sdk-go/go.mod b/jules-agent-sdk-go/go.mod new file mode 100644 index 0000000..2f6885b --- /dev/null +++ b/jules-agent-sdk-go/go.mod @@ -0,0 +1,3 @@ +module github.com/sashimikun/jules-agent-sdk-go + +go 1.24.7 diff --git a/jules-agent-sdk-go/jules/activities.go b/jules-agent-sdk-go/jules/activities.go new file mode 100644 index 0000000..5be7169 --- /dev/null +++ b/jules-agent-sdk-go/jules/activities.go @@ -0,0 +1,127 @@ +package jules + +import ( + "context" + "encoding/json" + "fmt" + "strings" +) + +// ActivitiesAPI provides methods for interacting with the Activities API +type ActivitiesAPI struct { + client *BaseClient +} + +// NewActivitiesAPI creates a new ActivitiesAPI instance +func NewActivitiesAPI(client *BaseClient) *ActivitiesAPI { + return &ActivitiesAPI{client: client} +} + +// Get retrieves an activity by ID +func (a *ActivitiesAPI) Get(ctx context.Context, sessionID, activityID string) (*Activity, error) { + path := a.buildActivityPath(sessionID, activityID) + + resp, err := a.client.Get(ctx, path) + if err != nil { + return nil, err + } + + return a.parseActivity(resp) +} + +// List retrieves a list of activities for a session +func (a *ActivitiesAPI) List(ctx context.Context, sessionID string, opts *ListOptions) (*ActivitiesListResponse, error) { + sessionPath := a.buildSessionPath(sessionID) + path := sessionPath + "/activities" + + if opts != nil { + query := "" + if opts.PageSize > 0 { + query += fmt.Sprintf("pageSize=%d", opts.PageSize) + } + if opts.PageToken != "" { + if query != "" { + query += "&" + } + query += fmt.Sprintf("pageToken=%s", opts.PageToken) + } + if query != "" { + path += "?" + query + } + } + + resp, err := a.client.Get(ctx, path) + if err != nil { + return nil, err + } + + // Parse response + var result ActivitiesListResponse + respBytes, _ := json.Marshal(resp) + if err := json.Unmarshal(respBytes, &result); err != nil { + return nil, fmt.Errorf("failed to parse activities list: %w", err) + } + + return &result, nil +} + +// ListAll retrieves all activities for a session (handles pagination automatically) +func (a *ActivitiesAPI) ListAll(ctx context.Context, sessionID string) ([]Activity, error) { + var allActivities []Activity + pageToken := "" + + for { + opts := &ListOptions{ + PageToken: pageToken, + } + + resp, err := a.List(ctx, sessionID, opts) + if err != nil { + return nil, err + } + + allActivities = append(allActivities, resp.Activities...) + + // Check if there are more pages + if resp.NextPageToken == "" { + break + } + pageToken = resp.NextPageToken + } + + return allActivities, nil +} + +// buildSessionPath builds the API path for a session +func (a *ActivitiesAPI) buildSessionPath(sessionID string) string { + if strings.HasPrefix(sessionID, "sessions/") { + return "/" + sessionID + } + return "/sessions/" + sessionID +} + +// buildActivityPath builds the API path for an activity +func (a *ActivitiesAPI) buildActivityPath(sessionID, activityID string) string { + sessionPath := a.buildSessionPath(sessionID) + + if strings.HasPrefix(activityID, "activities/") { + return sessionPath + "/" + activityID + } + return sessionPath + "/activities/" + activityID +} + +// parseActivity parses an activity from a response +func (a *ActivitiesAPI) parseActivity(data map[string]interface{}) (*Activity, error) { + // Convert to JSON and back to properly parse the activity + jsonBytes, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal activity data: %w", err) + } + + var activity Activity + if err := json.Unmarshal(jsonBytes, &activity); err != nil { + return nil, fmt.Errorf("failed to parse activity: %w", err) + } + + return &activity, nil +} diff --git a/jules-agent-sdk-go/jules/client.go b/jules-agent-sdk-go/jules/client.go new file mode 100644 index 0000000..4cec5ff --- /dev/null +++ b/jules-agent-sdk-go/jules/client.go @@ -0,0 +1,208 @@ +package jules + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "time" +) + +// BaseClient is the base HTTP client for making API requests +type BaseClient struct { + config *Config + httpClient *http.Client + requestCount int + errorCount int +} + +// NewBaseClient creates a new BaseClient +func NewBaseClient(config *Config) (*BaseClient, error) { + if err := config.Validate(); err != nil { + return nil, err + } + + // Create HTTP client with connection pooling + transport := &http.Transport{ + MaxIdleConns: 10, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + } + + if !config.VerifySSL { + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + httpClient := &http.Client{ + Transport: transport, + Timeout: config.Timeout, + } + + return &BaseClient{ + config: config, + httpClient: httpClient, + }, nil +} + +// Get performs a GET request +func (c *BaseClient) Get(ctx context.Context, path string) (map[string]interface{}, error) { + return c.request(ctx, http.MethodGet, path, nil) +} + +// Post performs a POST request +func (c *BaseClient) Post(ctx context.Context, path string, body interface{}) (map[string]interface{}, error) { + return c.request(ctx, http.MethodPost, path, body) +} + +// request performs an HTTP request with retry logic +func (c *BaseClient) request(ctx context.Context, method, path string, body interface{}) (map[string]interface{}, error) { + url := c.config.BaseURL + path + var lastErr error + + for attempt := 0; attempt <= c.config.MaxRetries; attempt++ { + // Apply backoff delay for retries + if attempt > 0 { + backoff := c.calculateBackoff(attempt) + select { + case <-time.After(backoff): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + + // Prepare request body + var bodyReader io.Reader + if body != nil { + bodyBytes, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + bodyReader = bytes.NewReader(bodyBytes) + } + + // Create request + req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Goog-Api-Key", c.config.APIKey) + + // Execute request + c.requestCount++ + resp, err := c.httpClient.Do(req) + if err != nil { + lastErr = err + // Retry on network errors + continue + } + + // Read response body + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Handle HTTP errors + if resp.StatusCode >= 400 { + var responseData map[string]interface{} + if len(respBody) > 0 { + json.Unmarshal(respBody, &responseData) + } + + lastErr = c.handleError(resp.StatusCode, respBody, responseData) + + // Retry on 5xx errors + if resp.StatusCode >= 500 { + c.errorCount++ + continue + } + + // Don't retry on client errors (4xx) + return nil, lastErr + } + + // Parse successful response + var result map[string]interface{} + if len(respBody) > 0 { + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + } + + return result, nil + } + + if lastErr != nil { + c.errorCount++ + return nil, lastErr + } + + return nil, fmt.Errorf("request failed after %d attempts", c.config.MaxRetries+1) +} + +// calculateBackoff calculates the exponential backoff duration +func (c *BaseClient) calculateBackoff(attempt int) time.Duration { + backoff := c.config.RetryBackoffFactor * math.Pow(2, float64(attempt-1)) + backoffDuration := time.Duration(backoff * float64(time.Second)) + if backoffDuration > c.config.MaxBackoff { + backoffDuration = c.config.MaxBackoff + } + return backoffDuration +} + +// handleError converts HTTP errors to appropriate error types +func (c *BaseClient) handleError(statusCode int, body []byte, response map[string]interface{}) error { + message := string(body) + if errMsg, ok := response["error"].(map[string]interface{}); ok { + if msg, ok := errMsg["message"].(string); ok { + message = msg + } + } + + switch statusCode { + case 400: + return NewValidationError(message, response) + case 401: + return NewAuthenticationError(message, response) + case 404: + return NewNotFoundError(message, response) + case 429: + // Try to extract Retry-After header + retryAfter := 0 + if ra, ok := response["retryAfter"].(float64); ok { + retryAfter = int(ra) + } + return NewRateLimitError(message, retryAfter, response) + default: + if statusCode >= 500 { + return NewServerError(message, statusCode, response) + } + return &APIError{ + Message: message, + StatusCode: statusCode, + Response: response, + } + } +} + +// Close closes the HTTP client and releases resources +func (c *BaseClient) Close() error { + c.httpClient.CloseIdleConnections() + return nil +} + +// Stats returns statistics about the client +func (c *BaseClient) Stats() map[string]int { + return map[string]int{ + "request_count": c.requestCount, + "error_count": c.errorCount, + } +} diff --git a/jules-agent-sdk-go/jules/config.go b/jules-agent-sdk-go/jules/config.go new file mode 100644 index 0000000..16d82da --- /dev/null +++ b/jules-agent-sdk-go/jules/config.go @@ -0,0 +1,93 @@ +package jules + +import ( + "fmt" + "time" +) + +const ( + // DefaultBaseURL is the default API base URL + DefaultBaseURL = "https://julius.googleapis.com/v1alpha" + + // DefaultTimeout is the default request timeout + DefaultTimeout = 30 * time.Second + + // DefaultMaxRetries is the default maximum number of retries + DefaultMaxRetries = 3 + + // DefaultRetryBackoffFactor is the default exponential backoff factor + DefaultRetryBackoffFactor = 1.0 + + // DefaultMaxBackoff is the default maximum backoff duration + DefaultMaxBackoff = 10 * time.Second + + // DefaultPollInterval is the default polling interval for wait operations + DefaultPollInterval = 5 * time.Second + + // DefaultSessionTimeout is the default timeout for session completion + DefaultSessionTimeout = 600 * time.Second +) + +// Config holds the configuration for the Jules API client +type Config struct { + // APIKey is the API key for authentication (required) + APIKey string + + // BaseURL is the base URL for the API + BaseURL string + + // Timeout is the HTTP request timeout + Timeout time.Duration + + // MaxRetries is the maximum number of retry attempts + MaxRetries int + + // RetryBackoffFactor is the exponential backoff factor for retries + RetryBackoffFactor float64 + + // MaxBackoff is the maximum backoff duration between retries + MaxBackoff time.Duration + + // VerifySSL controls SSL certificate verification + VerifySSL bool +} + +// NewConfig creates a new Config with default values +func NewConfig(apiKey string) (*Config, error) { + if apiKey == "" { + return nil, fmt.Errorf("API key is required") + } + + return &Config{ + APIKey: apiKey, + BaseURL: DefaultBaseURL, + Timeout: DefaultTimeout, + MaxRetries: DefaultMaxRetries, + RetryBackoffFactor: DefaultRetryBackoffFactor, + MaxBackoff: DefaultMaxBackoff, + VerifySSL: true, + }, nil +} + +// Validate validates the configuration +func (c *Config) Validate() error { + if c.APIKey == "" { + return fmt.Errorf("API key is required") + } + if c.BaseURL == "" { + return fmt.Errorf("base URL is required") + } + if c.Timeout <= 0 { + return fmt.Errorf("timeout must be positive") + } + if c.MaxRetries < 0 { + return fmt.Errorf("max retries must be non-negative") + } + if c.RetryBackoffFactor < 0 { + return fmt.Errorf("retry backoff factor must be non-negative") + } + if c.MaxBackoff <= 0 { + return fmt.Errorf("max backoff must be positive") + } + return nil +} diff --git a/jules-agent-sdk-go/jules/errors.go b/jules-agent-sdk-go/jules/errors.go new file mode 100644 index 0000000..138f7be --- /dev/null +++ b/jules-agent-sdk-go/jules/errors.go @@ -0,0 +1,115 @@ +package jules + +import "fmt" + +// APIError is the base error type for all Jules API errors +type APIError struct { + Message string + StatusCode int + Response map[string]interface{} +} + +// Error implements the error interface +func (e *APIError) Error() string { + if e.StatusCode > 0 { + return fmt.Sprintf("Jules API error (status %d): %s", e.StatusCode, e.Message) + } + return fmt.Sprintf("Jules API error: %s", e.Message) +} + +// AuthenticationError represents a 401 authentication error +type AuthenticationError struct { + *APIError +} + +// NewAuthenticationError creates a new AuthenticationError +func NewAuthenticationError(message string, response map[string]interface{}) *AuthenticationError { + return &AuthenticationError{ + APIError: &APIError{ + Message: message, + StatusCode: 401, + Response: response, + }, + } +} + +// NotFoundError represents a 404 not found error +type NotFoundError struct { + *APIError +} + +// NewNotFoundError creates a new NotFoundError +func NewNotFoundError(message string, response map[string]interface{}) *NotFoundError { + return &NotFoundError{ + APIError: &APIError{ + Message: message, + StatusCode: 404, + Response: response, + }, + } +} + +// ValidationError represents a 400 validation error +type ValidationError struct { + *APIError +} + +// NewValidationError creates a new ValidationError +func NewValidationError(message string, response map[string]interface{}) *ValidationError { + return &ValidationError{ + APIError: &APIError{ + Message: message, + StatusCode: 400, + Response: response, + }, + } +} + +// RateLimitError represents a 429 rate limit error +type RateLimitError struct { + *APIError + RetryAfter int // Retry-After header value in seconds +} + +// NewRateLimitError creates a new RateLimitError +func NewRateLimitError(message string, retryAfter int, response map[string]interface{}) *RateLimitError { + return &RateLimitError{ + APIError: &APIError{ + Message: message, + StatusCode: 429, + Response: response, + }, + RetryAfter: retryAfter, + } +} + +// ServerError represents a 5xx server error +type ServerError struct { + *APIError +} + +// NewServerError creates a new ServerError +func NewServerError(message string, statusCode int, response map[string]interface{}) *ServerError { + return &ServerError{ + APIError: &APIError{ + Message: message, + StatusCode: statusCode, + Response: response, + }, + } +} + +// TimeoutError represents a timeout error +type TimeoutError struct { + Message string +} + +// Error implements the error interface +func (e *TimeoutError) Error() string { + return fmt.Sprintf("Timeout: %s", e.Message) +} + +// NewTimeoutError creates a new TimeoutError +func NewTimeoutError(message string) *TimeoutError { + return &TimeoutError{Message: message} +} diff --git a/jules-agent-sdk-go/jules/jules_client.go b/jules-agent-sdk-go/jules/jules_client.go new file mode 100644 index 0000000..07167b1 --- /dev/null +++ b/jules-agent-sdk-go/jules/jules_client.go @@ -0,0 +1,44 @@ +package jules + +// JulesClient is the main client for the Jules API +type JulesClient struct { + baseClient *BaseClient + Sessions *SessionsAPI + Activities *ActivitiesAPI + Sources *SourcesAPI +} + +// NewClient creates a new JulesClient with the given API key +func NewClient(apiKey string) (*JulesClient, error) { + config, err := NewConfig(apiKey) + if err != nil { + return nil, err + } + + return NewClientWithConfig(config) +} + +// NewClientWithConfig creates a new JulesClient with a custom configuration +func NewClientWithConfig(config *Config) (*JulesClient, error) { + baseClient, err := NewBaseClient(config) + if err != nil { + return nil, err + } + + return &JulesClient{ + baseClient: baseClient, + Sessions: NewSessionsAPI(baseClient), + Activities: NewActivitiesAPI(baseClient), + Sources: NewSourcesAPI(baseClient), + }, nil +} + +// Close closes the client and releases all resources +func (c *JulesClient) Close() error { + return c.baseClient.Close() +} + +// Stats returns statistics about the client +func (c *JulesClient) Stats() map[string]int { + return c.baseClient.Stats() +} diff --git a/jules-agent-sdk-go/jules/models.go b/jules-agent-sdk-go/jules/models.go new file mode 100644 index 0000000..ceaa1de --- /dev/null +++ b/jules-agent-sdk-go/jules/models.go @@ -0,0 +1,180 @@ +package jules + +import "time" + +// SessionState represents the state of a session +type SessionState string + +const ( + // SessionStateUnspecified is the default unspecified state + SessionStateUnspecified SessionState = "STATE_UNSPECIFIED" + // SessionStateQueued indicates the session is queued + SessionStateQueued SessionState = "QUEUED" + // SessionStatePlanning indicates the session is in planning phase + SessionStatePlanning SessionState = "PLANNING" + // SessionStateAwaitingPlanApproval indicates the session is waiting for plan approval + SessionStateAwaitingPlanApproval SessionState = "AWAITING_PLAN_APPROVAL" + // SessionStateAwaitingUserFeedback indicates the session is waiting for user feedback + SessionStateAwaitingUserFeedback SessionState = "AWAITING_USER_FEEDBACK" + // SessionStateInProgress indicates the session is in progress + SessionStateInProgress SessionState = "IN_PROGRESS" + // SessionStatePaused indicates the session is paused + SessionStatePaused SessionState = "PAUSED" + // SessionStateFailed indicates the session has failed + SessionStateFailed SessionState = "FAILED" + // SessionStateCompleted indicates the session has completed + SessionStateCompleted SessionState = "COMPLETED" +) + +// IsTerminal returns true if the session state is terminal (completed or failed) +func (s SessionState) IsTerminal() bool { + return s == SessionStateCompleted || s == SessionStateFailed +} + +// Session represents a Jules session +type Session struct { + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + Prompt string `json:"prompt,omitempty"` + SourceContext *SourceContext `json:"sourceContext,omitempty"` + Title string `json:"title,omitempty"` + State SessionState `json:"state,omitempty"` + URL string `json:"url,omitempty"` + CreateTime *time.Time `json:"createTime,omitempty"` + UpdateTime *time.Time `json:"updateTime,omitempty"` + Output *SessionOutput `json:"output,omitempty"` +} + +// SessionOutput represents the output of a session +type SessionOutput struct { + PullRequest *PullRequest `json:"pullRequest,omitempty"` +} + +// SourceContext represents the source context for a session +type SourceContext struct { + Source string `json:"source,omitempty"` + GitHubRepoContext *GitHubRepoContext `json:"githubRepoContext,omitempty"` +} + +// GitHubRepoContext represents GitHub repository context +type GitHubRepoContext struct { + StartingBranch string `json:"startingBranch,omitempty"` +} + +// PullRequest represents a GitHub pull request +type PullRequest struct { + URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` +} + +// Source represents a source repository +type Source struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + GitHubRepo *GitHubRepo `json:"githubRepo,omitempty"` +} + +// GitHubRepo represents a GitHub repository +type GitHubRepo struct { + Owner string `json:"owner,omitempty"` + Repo string `json:"repo,omitempty"` + IsPrivate bool `json:"isPrivate,omitempty"` + DefaultBranch string `json:"defaultBranch,omitempty"` + Branches []GitHubBranch `json:"branches,omitempty"` +} + +// GitHubBranch represents a GitHub branch +type GitHubBranch struct { + DisplayName string `json:"displayName,omitempty"` +} + +// Activity represents an activity in a session +type Activity struct { + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + CreateTime *time.Time `json:"createTime,omitempty"` + Originator string `json:"originator,omitempty"` + Artifacts []Artifact `json:"artifacts,omitempty"` + + // Activity event types (only one should be set) + AgentMessaged map[string]interface{} `json:"agentMessaged,omitempty"` + UserMessaged map[string]interface{} `json:"userMessaged,omitempty"` + PlanGenerated map[string]interface{} `json:"planGenerated,omitempty"` + PlanApproved map[string]interface{} `json:"planApproved,omitempty"` + ProgressUpdated map[string]interface{} `json:"progressUpdated,omitempty"` + SessionCompleted map[string]interface{} `json:"sessionCompleted,omitempty"` + SessionFailed map[string]interface{} `json:"sessionFailed,omitempty"` +} + +// Artifact represents an artifact in an activity +type Artifact struct { + ChangeSet *ChangeSet `json:"changeSet,omitempty"` + Media *Media `json:"media,omitempty"` + BashOutput *BashOutput `json:"bashOutput,omitempty"` +} + +// ChangeSet represents a set of changes +type ChangeSet struct { + Source string `json:"source,omitempty"` + GitPatch *GitPatch `json:"gitPatch,omitempty"` +} + +// GitPatch represents a git patch +type GitPatch struct { + UnidiffPatch string `json:"unidiffPatch,omitempty"` + BaseCommitID string `json:"baseCommitId,omitempty"` + SuggestedCommitMessage string `json:"suggestedCommitMessage,omitempty"` +} + +// Media represents media content +type Media struct { + Data string `json:"data,omitempty"` + MimeType string `json:"mimeType,omitempty"` +} + +// BashOutput represents bash command output +type BashOutput struct { + Command string `json:"command,omitempty"` + Output string `json:"output,omitempty"` + ExitCode int `json:"exitCode,omitempty"` +} + +// Plan represents a plan for a session +type Plan struct { + ID string `json:"id,omitempty"` + Steps []PlanStep `json:"steps,omitempty"` + CreateTime *time.Time `json:"createTime,omitempty"` +} + +// PlanStep represents a step in a plan +type PlanStep struct { + ID string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Index int `json:"index,omitempty"` +} + +// ListResponse represents a paginated list response +type ListResponse struct { + NextPageToken string `json:"nextPageToken,omitempty"` +} + +// SessionsListResponse represents a list of sessions +type SessionsListResponse struct { + Sessions []Session `json:"sessions,omitempty"` + NextPageToken string `json:"nextPageToken,omitempty"` +} + +// ActivitiesListResponse represents a list of activities +type ActivitiesListResponse struct { + Activities []Activity `json:"activities,omitempty"` + NextPageToken string `json:"nextPageToken,omitempty"` +} + +// SourcesListResponse represents a list of sources +type SourcesListResponse struct { + Sources []Source `json:"sources,omitempty"` + NextPageToken string `json:"nextPageToken,omitempty"` +} diff --git a/jules-agent-sdk-go/jules/sessions.go b/jules-agent-sdk-go/jules/sessions.go new file mode 100644 index 0000000..f2d38f9 --- /dev/null +++ b/jules-agent-sdk-go/jules/sessions.go @@ -0,0 +1,213 @@ +package jules + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" +) + +// SessionsAPI provides methods for interacting with the Sessions API +type SessionsAPI struct { + client *BaseClient +} + +// NewSessionsAPI creates a new SessionsAPI instance +func NewSessionsAPI(client *BaseClient) *SessionsAPI { + return &SessionsAPI{client: client} +} + +// CreateSessionRequest represents a request to create a new session +type CreateSessionRequest struct { + Prompt string `json:"prompt"` + Source string `json:"source"` + StartingBranch string `json:"startingBranch,omitempty"` + Title string `json:"title,omitempty"` + RequirePlanApproval bool `json:"requirePlanApproval,omitempty"` +} + +// Create creates a new session +func (s *SessionsAPI) Create(ctx context.Context, req *CreateSessionRequest) (*Session, error) { + // Build request body + body := map[string]interface{}{ + "prompt": req.Prompt, + "sourceContext": map[string]interface{}{ + "source": req.Source, + }, + } + + if req.StartingBranch != "" { + sourceContext := body["sourceContext"].(map[string]interface{}) + sourceContext["githubRepoContext"] = map[string]interface{}{ + "startingBranch": req.StartingBranch, + } + } + + if req.Title != "" { + body["title"] = req.Title + } + + if req.RequirePlanApproval { + body["requirePlanApproval"] = true + } + + // Make request + resp, err := s.client.Post(ctx, "/sessions", body) + if err != nil { + return nil, err + } + + // Parse response + return s.parseSession(resp) +} + +// Get retrieves a session by ID +func (s *SessionsAPI) Get(ctx context.Context, sessionID string) (*Session, error) { + // Handle both short IDs and full names + path := s.buildSessionPath(sessionID) + + resp, err := s.client.Get(ctx, path) + if err != nil { + return nil, err + } + + return s.parseSession(resp) +} + +// ListOptions represents options for listing sessions +type ListOptions struct { + PageSize int + PageToken string +} + +// List retrieves a list of sessions +func (s *SessionsAPI) List(ctx context.Context, opts *ListOptions) (*SessionsListResponse, error) { + path := "/sessions" + + if opts != nil { + query := "" + if opts.PageSize > 0 { + query += fmt.Sprintf("pageSize=%d", opts.PageSize) + } + if opts.PageToken != "" { + if query != "" { + query += "&" + } + query += fmt.Sprintf("pageToken=%s", opts.PageToken) + } + if query != "" { + path += "?" + query + } + } + + resp, err := s.client.Get(ctx, path) + if err != nil { + return nil, err + } + + // Parse response + var result SessionsListResponse + respBytes, _ := json.Marshal(resp) + if err := json.Unmarshal(respBytes, &result); err != nil { + return nil, fmt.Errorf("failed to parse sessions list: %w", err) + } + + return &result, nil +} + +// ApprovePlan approves a session plan +func (s *SessionsAPI) ApprovePlan(ctx context.Context, sessionID string) error { + path := s.buildSessionPath(sessionID) + ":approvePlan" + + _, err := s.client.Post(ctx, path, nil) + return err +} + +// SendMessage sends a message to a session +func (s *SessionsAPI) SendMessage(ctx context.Context, sessionID string, prompt string) error { + path := s.buildSessionPath(sessionID) + ":sendMessage" + + body := map[string]interface{}{ + "prompt": prompt, + } + + _, err := s.client.Post(ctx, path, body) + return err +} + +// WaitForCompletionOptions represents options for waiting for session completion +type WaitForCompletionOptions struct { + PollInterval time.Duration + Timeout time.Duration +} + +// WaitForCompletion polls a session until it reaches a terminal state +func (s *SessionsAPI) WaitForCompletion(ctx context.Context, sessionID string, opts *WaitForCompletionOptions) (*Session, error) { + pollInterval := DefaultPollInterval + timeout := DefaultSessionTimeout + + if opts != nil { + if opts.PollInterval > 0 { + pollInterval = opts.PollInterval + } + if opts.Timeout > 0 { + timeout = opts.Timeout + } + } + + // Create timeout context + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + for { + // Get session status + session, err := s.Get(ctx, sessionID) + if err != nil { + return nil, err + } + + // Check if terminal state reached + if session.State.IsTerminal() { + if session.State == SessionStateFailed { + return session, fmt.Errorf("session failed") + } + return session, nil + } + + // Wait for next poll or timeout + select { + case <-ticker.C: + continue + case <-ctx.Done(): + return nil, NewTimeoutError(fmt.Sprintf("session did not complete within %v", timeout)) + } + } +} + +// buildSessionPath builds the API path for a session +func (s *SessionsAPI) buildSessionPath(sessionID string) string { + if strings.HasPrefix(sessionID, "sessions/") { + return "/" + sessionID + } + return "/sessions/" + sessionID +} + +// parseSession parses a session from a response +func (s *SessionsAPI) parseSession(data map[string]interface{}) (*Session, error) { + // Convert to JSON and back to properly parse the session + jsonBytes, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal session data: %w", err) + } + + var session Session + if err := json.Unmarshal(jsonBytes, &session); err != nil { + return nil, fmt.Errorf("failed to parse session: %w", err) + } + + return &session, nil +} diff --git a/jules-agent-sdk-go/jules/sources.go b/jules-agent-sdk-go/jules/sources.go new file mode 100644 index 0000000..4f34e2e --- /dev/null +++ b/jules-agent-sdk-go/jules/sources.go @@ -0,0 +1,130 @@ +package jules + +import ( + "context" + "encoding/json" + "fmt" + "strings" +) + +// SourcesAPI provides methods for interacting with the Sources API +type SourcesAPI struct { + client *BaseClient +} + +// NewSourcesAPI creates a new SourcesAPI instance +func NewSourcesAPI(client *BaseClient) *SourcesAPI { + return &SourcesAPI{client: client} +} + +// SourcesListOptions represents options for listing sources +type SourcesListOptions struct { + Filter string + PageSize int + PageToken string +} + +// Get retrieves a source by ID +func (s *SourcesAPI) Get(ctx context.Context, sourceID string) (*Source, error) { + path := s.buildSourcePath(sourceID) + + resp, err := s.client.Get(ctx, path) + if err != nil { + return nil, err + } + + return s.parseSource(resp) +} + +// List retrieves a list of sources +func (s *SourcesAPI) List(ctx context.Context, opts *SourcesListOptions) (*SourcesListResponse, error) { + path := "/sources" + + if opts != nil { + query := "" + if opts.Filter != "" { + query += fmt.Sprintf("filter=%s", opts.Filter) + } + if opts.PageSize > 0 { + if query != "" { + query += "&" + } + query += fmt.Sprintf("pageSize=%d", opts.PageSize) + } + if opts.PageToken != "" { + if query != "" { + query += "&" + } + query += fmt.Sprintf("pageToken=%s", opts.PageToken) + } + if query != "" { + path += "?" + query + } + } + + resp, err := s.client.Get(ctx, path) + if err != nil { + return nil, err + } + + // Parse response + var result SourcesListResponse + respBytes, _ := json.Marshal(resp) + if err := json.Unmarshal(respBytes, &result); err != nil { + return nil, fmt.Errorf("failed to parse sources list: %w", err) + } + + return &result, nil +} + +// ListAll retrieves all sources (handles pagination automatically) +func (s *SourcesAPI) ListAll(ctx context.Context, filter string) ([]Source, error) { + var allSources []Source + pageToken := "" + + for { + opts := &SourcesListOptions{ + Filter: filter, + PageToken: pageToken, + } + + resp, err := s.List(ctx, opts) + if err != nil { + return nil, err + } + + allSources = append(allSources, resp.Sources...) + + // Check if there are more pages + if resp.NextPageToken == "" { + break + } + pageToken = resp.NextPageToken + } + + return allSources, nil +} + +// buildSourcePath builds the API path for a source +func (s *SourcesAPI) buildSourcePath(sourceID string) string { + if strings.HasPrefix(sourceID, "sources/") { + return "/" + sourceID + } + return "/sources/" + sourceID +} + +// parseSource parses a source from a response +func (s *SourcesAPI) parseSource(data map[string]interface{}) (*Source, error) { + // Convert to JSON and back to properly parse the source + jsonBytes, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal source data: %w", err) + } + + var source Source + if err := json.Unmarshal(jsonBytes, &source); err != nil { + return nil, fmt.Errorf("failed to parse source: %w", err) + } + + return &source, nil +}