From ee159e214e2ffda83c32c3a5a6fe955896cc74ab Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 20:02:35 +0000 Subject: [PATCH] feat: Add Go SDK for the Jules API This commit introduces a new Go SDK for interacting with the Jules API. The SDK is located in the `go_sdk` directory and is separate from the existing Python SDK. The Go SDK includes: - A base HTTP client with support for API key authentication, retries with exponential backoff, and configurable timeouts. - Data models for all API resources, including Sessions, Activities, and Sources. - Services for each API resource, providing methods for creating, getting, and listing resources. - A main `JulesClient` that provides a single entry point for all API services. - A usage example in the `examples/go_simple_test` directory to demonstrate how to use the SDK. --- examples/go_simple_test/go.mod | 7 ++ examples/go_simple_test/main.go | 46 ++++++++++++ go_sdk/activities.go | 34 +++++++++ go_sdk/client.go | 107 +++++++++++++++++++++++++++ go_sdk/go.mod | 3 + go_sdk/jules.go | 20 +++++ go_sdk/models.go | 125 ++++++++++++++++++++++++++++++++ go_sdk/sessions.go | 99 +++++++++++++++++++++++++ go_sdk/sources.go | 33 +++++++++ 9 files changed, 474 insertions(+) create mode 100644 examples/go_simple_test/go.mod create mode 100644 examples/go_simple_test/main.go create mode 100644 go_sdk/activities.go create mode 100644 go_sdk/client.go create mode 100644 go_sdk/go.mod create mode 100644 go_sdk/jules.go create mode 100644 go_sdk/models.go create mode 100644 go_sdk/sessions.go create mode 100644 go_sdk/sources.go diff --git a/examples/go_simple_test/go.mod b/examples/go_simple_test/go.mod new file mode 100644 index 0000000..f2745a2 --- /dev/null +++ b/examples/go_simple_test/go.mod @@ -0,0 +1,7 @@ +module example.com/simple_test + +go 1.24.3 + +replace github.com/jules-labs/jules-go-sdk => ../../go_sdk + +require github.com/jules-labs/jules-go-sdk v0.0.0-00010101000000-000000000000 diff --git a/examples/go_simple_test/main.go b/examples/go_simple_test/main.go new file mode 100644 index 0000000..8e5aeaa --- /dev/null +++ b/examples/go_simple_test/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/jules-labs/jules-go-sdk" +) + +func main() { + apiKey := os.Getenv("JULES_API_KEY") + if apiKey == "" { + log.Fatal("JULES_API_KEY environment variable not set") + } + + client := jules.NewJulesClient(apiKey) + + // List sources + sourcesResp, err := client.Sources.List() + if err != nil { + log.Fatalf("Error listing sources: %v", err) + } + + if len(sourcesResp.Sources) == 0 { + log.Fatal("No sources found") + } + + fmt.Printf("Found %d sources\n", len(sourcesResp.Sources)) + + // Create a session + createReq := &jules.CreateSessionRequest{ + Prompt: "Add error handling to the authentication module", + SourceContext: &jules.SourceContext{ + Source: sourcesResp.Sources[0].Name, + }, + } + + session, err := client.Sessions.Create(createReq) + if err != nil { + log.Fatalf("Error creating session: %v", err) + } + + fmt.Printf("Session created: %s\n", session.ID) + fmt.Printf("View at: %s\n", session.URL) +} \ No newline at end of file diff --git a/go_sdk/activities.go b/go_sdk/activities.go new file mode 100644 index 0000000..f71a6b7 --- /dev/null +++ b/go_sdk/activities.go @@ -0,0 +1,34 @@ +package jules + +import "fmt" + +// ActivitiesService is the service for interacting with the Activities API. +type ActivitiesService struct { + client *Client +} + +// NewActivitiesService creates a new ActivitiesService. +func NewActivitiesService(client *Client) *ActivitiesService { + return &ActivitiesService{client: client} +} + +// Get retrieves an activity by session and activity ID. +func (s *ActivitiesService) Get(sessionID, activityID string) (*Activity, error) { + var activity Activity + path := fmt.Sprintf("sessions/%s/activities/%s", sessionID, activityID) + _, err := s.client.get(path, &activity) + return &activity, err +} + +type ListActivitiesResponse struct { + Activities []Activity `json:"activities"` + NextPageToken string `json:"nextPageToken"` +} + +// List lists all activities for a session. +func (s *ActivitiesService) List(sessionID string) (*ListActivitiesResponse, error) { + var resp ListActivitiesResponse + path := fmt.Sprintf("sessions/%s/activities", sessionID) + _, err := s.client.get(path, &resp) + return &resp, err +} \ No newline at end of file diff --git a/go_sdk/client.go b/go_sdk/client.go new file mode 100644 index 0000000..60a9154 --- /dev/null +++ b/go_sdk/client.go @@ -0,0 +1,107 @@ +package jules + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const ( + defaultBaseURL = "https://jules.googleapis.com/v1alpha" + defaultMaxRetries = 3 + DefaultTimeout = 30 * time.Second +) + +// Client is the base client for the Jules API. +type Client struct { + apiKey string + baseURL string + httpClient *http.Client + maxRetries int +} + +// NewClient creates a new Jules API client. +func NewClient(apiKey string) *Client { + return &Client{ + apiKey: apiKey, + baseURL: defaultBaseURL, + httpClient: &http.Client{Timeout: DefaultTimeout}, + maxRetries: defaultMaxRetries, + } +} + +func (c *Client) newRequest(method, path string, body interface{}) (*http.Request, error) { + url := fmt.Sprintf("%s/%s", c.baseURL, path) + + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, url, buf) + if err != nil { + return nil, err + } + + req.Header.Set("X-Goog-Api-Key", c.apiKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "jules-go-sdk/0.1.0") + + return req, nil +} + +func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) { + var resp *http.Response + var err error + + for i := 0; i < c.maxRetries; i++ { + resp, err = c.httpClient.Do(req) + if err == nil && resp.StatusCode < 500 { + break // Success or non-retriable error + } + time.Sleep(time.Duration(i*i) * time.Second) // Exponential backoff + } + + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return resp, fmt.Errorf("API error: %s", resp.Status) + } + + if v != nil { + if w, ok := v.(io.Writer); ok { + io.Copy(w, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(v) + } + } + + return resp, err +} + +func (c *Client) get(path string, v interface{}) (*http.Response, error) { + req, err := c.newRequest("GET", path, nil) + if err != nil { + return nil, err + } + return c.do(req, v) +} + +func (c *Client) post(path string, body, v interface{}) (*http.Response, error) { + req, err := c.newRequest("POST", path, body) + if err != nil { + return nil, err + } + return c.do(req, v) +} \ No newline at end of file diff --git a/go_sdk/go.mod b/go_sdk/go.mod new file mode 100644 index 0000000..237108f --- /dev/null +++ b/go_sdk/go.mod @@ -0,0 +1,3 @@ +module github.com/jules-labs/jules-go-sdk + +go 1.24.3 diff --git a/go_sdk/jules.go b/go_sdk/jules.go new file mode 100644 index 0000000..4986980 --- /dev/null +++ b/go_sdk/jules.go @@ -0,0 +1,20 @@ +package jules + +// JulesClient is the main client for the Jules API. +type JulesClient struct { + client *Client + Sessions *SessionsService + Activities *ActivitiesService + Sources *SourcesService +} + +// NewJulesClient creates a new JulesClient. +func NewJulesClient(apiKey string) *JulesClient { + baseClient := NewClient(apiKey) + return &JulesClient{ + client: baseClient, + Sessions: NewSessionsService(baseClient), + Activities: NewActivitiesService(baseClient), + Sources: NewSourcesService(baseClient), + } +} \ No newline at end of file diff --git a/go_sdk/models.go b/go_sdk/models.go new file mode 100644 index 0000000..e874413 --- /dev/null +++ b/go_sdk/models.go @@ -0,0 +1,125 @@ +package jules + +import "time" + +type SessionState string + +const ( + StateUnspecified SessionState = "STATE_UNSPECIFIED" + Queued SessionState = "QUEUED" + Planning SessionState = "PLANNING" + AwaitingPlanApproval SessionState = "AWAITING_PLAN_APPROVAL" + AwaitingUserFeedback SessionState = "AWAITING_USER_FEEDBACK" + InProgress SessionState = "IN_PROGRESS" + Paused SessionState = "PAUSED" + Failed SessionState = "FAILED" + Completed SessionState = "COMPLETED" +) + +type GitHubBranch struct { + DisplayName string `json:"displayName"` +} + +type GitHubRepo struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + IsPrivate bool `json:"isPrivate"` + DefaultBranch *GitHubBranch `json:"defaultBranch,omitempty"` + Branches []GitHubBranch `json:"branches,omitempty"` +} + +type Source struct { + Name string `json:"name"` + ID string `json:"id"` + GithubRepo *GitHubRepo `json:"githubRepo,omitempty"` +} + +type GitHubRepoContext struct { + StartingBranch string `json:"startingBranch"` +} + +type SourceContext struct { + Source string `json:"source"` + GithubRepoContext *GitHubRepoContext `json:"githubRepoContext,omitempty"` +} + +type PullRequest struct { + URL string `json:"url"` + Title string `json:"title"` + Description string `json:"description"` +} + +type SessionOutput struct { + PullRequest *PullRequest `json:"pullRequest,omitempty"` +} + +type Session struct { + Prompt string `json:"prompt"` + SourceContext SourceContext `json:"sourceContext"` + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + RequirePlanApproval bool `json:"requirePlanApproval,omitempty"` + CreateTime time.Time `json:"createTime,omitempty"` + UpdateTime time.Time `json:"updateTime,omitempty"` + State SessionState `json:"state,omitempty"` + URL string `json:"url,omitempty"` + Outputs []SessionOutput `json:"outputs,omitempty"` +} + +type PlanStep struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Index int `json:"index"` +} + +type Plan struct { + ID string `json:"id"` + Steps []PlanStep `json:"steps"` + CreateTime time.Time `json:"createTime,omitempty"` +} + +type GitPatch struct { + UnidiffPatch string `json:"unidiffPatch"` + BaseCommitID string `json:"baseCommitId"` + SuggestedCommitMessage string `json:"suggestedCommitMessage"` +} + +type ChangeSet struct { + Source string `json:"source"` + GitPatch *GitPatch `json:"gitPatch,omitempty"` +} + +type Media struct { + Data string `json:"data"` + MimeType string `json:"mimeType"` +} + +type BashOutput struct { + Command string `json:"command"` + Output string `json:"output"` + ExitCode int `json:"exitCode"` +} + +type Artifact struct { + ChangeSet *ChangeSet `json:"changeSet,omitempty"` + Media *Media `json:"media,omitempty"` + BashOutput *BashOutput `json:"bashOutput,omitempty"` +} + +type Activity struct { + Name string `json:"name"` + 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"` + AgentMessaged map[string]string `json:"agentMessaged,omitempty"` + UserMessaged map[string]string `json:"userMessaged,omitempty"` + PlanGenerated map[string]interface{} `json:"planGenerated,omitempty"` + PlanApproved map[string]string `json:"planApproved,omitempty"` + ProgressUpdated map[string]string `json:"progressUpdated,omitempty"` + SessionCompleted map[string]interface{} `json:"sessionCompleted,omitempty"` + SessionFailed map[string]string `json:"sessionFailed,omitempty"` +} \ No newline at end of file diff --git a/go_sdk/sessions.go b/go_sdk/sessions.go new file mode 100644 index 0000000..acb622e --- /dev/null +++ b/go_sdk/sessions.go @@ -0,0 +1,99 @@ +package jules + +import ( + "fmt" + "time" +) + +const ( + DefaultPollInterval = 5 * time.Second +) + +// SessionsService is the service for interacting with the Sessions API. +type SessionsService struct { + client *Client +} + +// NewSessionsService creates a new SessionsService. +func NewSessionsService(client *Client) *SessionsService { + return &SessionsService{client: client} +} + +type CreateSessionRequest struct { + Prompt string `json:"prompt"` + SourceContext *SourceContext `json:"sourceContext"` + Title string `json:"title,omitempty"` + RequirePlanApproval bool `json:"requirePlanApproval,omitempty"` +} + +// Create creates a new session. +func (s *SessionsService) Create(req *CreateSessionRequest) (*Session, error) { + var session Session + _, err := s.client.post("sessions", req, &session) + return &session, err +} + +// Get retrieves a session by ID. +func (s *SessionsService) Get(sessionID string) (*Session, error) { + var session Session + path := fmt.Sprintf("sessions/%s", sessionID) + _, err := s.client.get(path, &session) + return &session, err +} + +type ListSessionsResponse struct { + Sessions []Session `json:"sessions"` + NextPageToken string `json:"nextPageToken"` +} + +// List lists all sessions. +func (s *SessionsService) List() (*ListSessionsResponse, error) { + var resp ListSessionsResponse + _, err := s.client.get("sessions", &resp) + return &resp, err +} + +// ApprovePlan approves a session's plan. +func (s *SessionsService) ApprovePlan(sessionID string) error { + path := fmt.Sprintf("sessions/%s:approvePlan", sessionID) + _, err := s.client.post(path, nil, nil) + return err +} + +type SendMessageRequest struct { + Prompt string `json:"prompt"` +} + +// SendMessage sends a message to a session. +func (s *SessionsService) SendMessage(sessionID string, prompt string) error { + path := fmt.Sprintf("sessions/%s:sendMessage", sessionID) + req := &SendMessageRequest{Prompt: prompt} + _, err := s.client.post(path, req, nil) + return err +} + +// WaitForCompletion polls a session until it completes or fails. +func (s *SessionsService) WaitForCompletion(sessionID string) (*Session, error) { + timeout := time.After(DefaultTimeout) + ticker := time.NewTicker(DefaultPollInterval) + defer ticker.Stop() + + for { + select { + case <-timeout: + return nil, fmt.Errorf("timed out waiting for session %s to complete", sessionID) + case <-ticker.C: + session, err := s.Get(sessionID) + if err != nil { + return nil, err + } + + switch session.State { + case Completed: + return session, nil + case Failed: + return session, fmt.Errorf("session %s failed", sessionID) + } + } + } +} \ No newline at end of file diff --git a/go_sdk/sources.go b/go_sdk/sources.go new file mode 100644 index 0000000..e0a9a31 --- /dev/null +++ b/go_sdk/sources.go @@ -0,0 +1,33 @@ +package jules + +import "fmt" + +// SourcesService is the service for interacting with the Sources API. +type SourcesService struct { + client *Client +} + +// NewSourcesService creates a new SourcesService. +func NewSourcesService(client *Client) *SourcesService { + return &SourcesService{client: client} +} + +// Get retrieves a source by ID. +func (s *SourcesService) Get(sourceID string) (*Source, error) { + var source Source + path := fmt.Sprintf("sources/%s", sourceID) + _, err := s.client.get(path, &source) + return &source, err +} + +type ListSourcesResponse struct { + Sources []Source `json:"sources"` + NextPageToken string `json:"nextPageToken"` +} + +// List lists all sources. +func (s *SourcesService) List() (*ListSourcesResponse, error) { + var resp ListSourcesResponse + _, err := s.client.get("sources", &resp) + return &resp, err +} \ No newline at end of file