Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Codex Go SDK

Go Reference Codecov

Embed the Codex app-server in Go workflows.

This SDK speaks JSON-RPC to the codex app-server process. By default it spawns the CLI and communicates over stdio.

Requirements

  • Go 1.25.12 or newer
  • codex available on your PATH

Install

go get github.com/pmenglund/codex-sdk-go

Quickstart

package main

import (
    "context"
    "fmt"
    "log/slog"
    "os"

    "github.com/pmenglund/codex-sdk-go"
)

func main() {
    ctx := context.Background()
    logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
    prompt := "Diagnose the test failure and propose a fix"

    client, err := codex.New(ctx, codex.Options{Logger: logger})
    if err != nil {
        panic(err)
    }
    defer client.Close()

    thread, err := client.StartThread(ctx, codex.ThreadStartOptions{})
    if err != nil {
        panic(err)
    }

    result, err := thread.Run(ctx, prompt, nil)
    if err != nil {
        panic(err)
    }

    fmt.Println(result.FinalResponse)
}

New uses its context.Context for initialization requests (initialize/initialized). After New returns successfully, the spawned app-server lifetime is managed by Close, so canceling the constructor context later does not terminate the process.

Streaming

Use RunStreamed to receive notifications as the turn progresses.

prompt := "Inspect the repo"
stream, err := thread.RunStreamed(ctx, []codex.Input{codex.TextInput(prompt)}, nil)
if err != nil {
    panic(err)
}

defer stream.Close()

for {
    note, err := stream.Next(ctx)
    if err != nil {
		panic(err)
    }
    fmt.Printf("%s\n", note.Method)
	if note.Method == "turn/failed" {
		panic("turn failed")
	}
	if note.Method == "turn/completed" {
		if completed, ok := note.Params.(protocol.TurnCompletedNotification); ok &&
			completed.Turn != nil && completed.Turn.Status == protocol.TurnStatusFailed {
			panic(fmt.Errorf("turn failed: %v", completed.Turn.Error))
		}
		break
    }
}

RunStreamed returns thread-scoped events plus notifications that omit threadId (for example account/session updates) so global events are not silently dropped.

Turn handles

Use StartTurn when you need to steer or interrupt a running turn.

handle, err := thread.StartTurn(ctx, []codex.Input{codex.TextInput("Inspect the repo")}, nil)
if err != nil {
    panic(err)
}

if _, err := handle.Steer(ctx, []codex.Input{codex.TextInput("Focus on tests")}); err != nil {
    panic(err)
}

result, err := handle.Run(ctx)
if err != nil {
    panic(err)
}

fmt.Println(result.FinalResponse)

TurnHandle owns its notification subscription. Call Close if you stop before Run returns.

Choose exactly one consumption style per handle: Run, repeated Next calls, or the stream returned by Stream. Mixing styles or consuming concurrently returns codex.ErrTurnConsumptionMode immediately.

If a turn fails, Run returns both the partial result and a *codex.TurnError. Use errors.Is(err, codex.ErrTurnFailed) to classify the failure and errors.As to inspect its structured details, retry metadata, and raw payload:

result, err := handle.Run(ctx)
if errors.Is(err, codex.ErrTurnFailed) {
    var turnErr *codex.TurnError
    if errors.As(err, &turnErr) {
        fmt.Printf("received %d items before failure: %v\n", len(result.Items), turnErr.Detail)
    }
}

FinalResponse is populated only by completed agentMessage items. Plan, reasoning, and commentary items remain available in Items but are never reported as the final assistant answer.

Canceling or expiring the context passed to Run, RunInputs, or TurnHandle.Run best-effort interrupts the remote turn before returning the original context error. Cleanup is bounded to two seconds. To detach without interrupting remote work, use RunStreamed (or StartTurn plus manual Next) and close only the local stream or handle.

Low-level rpc.Client.SubscribeNotifications buffers at most the requested number of pending notifications (64 by default). If a consumer falls behind, only that iterator closes and Next returns an rpc.NotificationOverflowError; JSON-RPC responses and other subscribers continue normally.

note, err := iterator.Next(ctx)
var overflow *rpc.NotificationOverflowError
if errors.As(err, &overflow) {
    // Events were lost. Increase the capacity, drain faster, and subscribe
    // again; overflow.Capacity reports the exhausted hard limit.
}

Account, models, and threads

High-level helpers wrap common app-server operations without requiring direct JSON-RPC calls.

account, err := client.Account(ctx, codex.AccountOptions{})
models, err := client.ListModels(ctx, codex.ListModelsOptions{})
threads, err := client.ListThreads(ctx, codex.ThreadListOptions{})

Thread list, read, fork, and unarchive helpers return concrete protocol values. List sorting uses protocol.SortDirection and protocol.ThreadSortKey rather than arbitrary strings.

Thread values also expose lifecycle helpers:

if _, err := thread.SetName(ctx, "Investigation"); err != nil {
    panic(err)
}

forked, _, err := thread.Fork(ctx, codex.ThreadForkOptions{})
if err != nil {
    panic(err)
}

_ = forked

For lower-level or less stable protocol features, use client.Client() and the generated rpc package.

Low-level union migration

Discriminated protocol unions are concrete generated wrapper types rather than interface{}. Construct values with functions such as protocol.NewUserInput, inspect the typed Kind, and use RawJSON when a variant-specific payload is needed:

input, err := protocol.NewUserInput(map[string]any{
    "type": "text",
    "text": "Inspect the repository",
})
if err != nil {
    return err
}
if input.Kind() == protocol.UserInputKindText {
    var payload struct {
        Text string `json:"text"`
    }
    if err := json.Unmarshal(input.RawJSON(), &payload); err != nil {
        return err
    }
}

Constructors validate JSON encoding, the required non-empty discriminator, and required-field presence for known variants. Field value types and other schema constraints remain server-validated. Known variants have generated kind constants; a well-formed future discriminator remains round-trippable and reports IsKnown() == false. Existing opaque schemas remain interface{} only through reviewed generator allowlists, and generation fails when a new weak fallback appears. The 31 affected wrappers—including UserInput, ResponseItem, SandboxPolicy, ThreadStatus, and LoginAccountParams—are listed in protocol/unions_gen.go.

Generated protocol declarations use Go initialisms consistently, including MCP, OAuth, ID, URL, and JSON. Older Mcp.../Oauth... spellings and Sanitized...JSON implementation names remain deprecated aliases for a migration window. Prefer canonical declarations such as protocol.MCPServerOAuthLoginParams and canonical methods such as rpc.Client.MCPServerOAuthLogin.

These API changes are currently unreleased and require the first SDK release after v0.145.0; protocol.GeneratedCodexVersion continues to describe the upstream wire schema, not the SDK release number. This API-hardening work also includes Options.CompatibilityPolicy and changes StartLogin to accept JSON-marshalable login parameters; code using unkeyed Options literals, interface method sets, or assigned StartLogin method values must be updated.

Approvals

Configure app-server requests with optional callbacks. Unset callbacks return rpc.ErrServerRequestUnsupported, so adding a future server request does not break existing applications at compile time.

logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
client, err := codex.New(ctx, codex.Options{
    Logger:         logger,
    RequestHandler: codex.RejectingApprovalCallbacks(),
})

For custom logic, set only the callbacks your application supports:

callbacks := &codex.ServerRequestCallbacks{
    ApproveFileChange: func(ctx context.Context, request protocol.FileChangeRequestApprovalParams) (*protocol.FileChangeRequestApprovalResponse, error) {
        return &protocol.FileChangeRequestApprovalResponse{
            Decision: protocol.FileChangeApprovalDecisionDecline,
        }, nil
    },
}
client, err := codex.New(ctx, codex.Options{RequestHandler: callbacks})

Options.ApprovalHandler and the broad generated rpc.ServerRequestHandler remain as deprecated compatibility surfaces. Low-level handlers that must implement only selected methods can embed rpc.UnimplementedServerRequestHandler. For methods whose historical Go spelling used non-canonical initialisms, such as McpServerElicitationRequest, implement the exported canonical capability interface (for example, rpc.MCPServerElicitationRequestHandler); dispatch prefers that method over the legacy spelling. The client may invoke different server-request callbacks concurrently, so callbacks that share state must be concurrency-safe.

AutoApproveHandler accepts command, file-change, and permission requests and must only be used in a trusted environment. Its default logs are redacted. If sensitive command and path logging is explicitly required, construct NewUnsafeLoggingAutoApproveHandler(logger) and protect those logs accordingly. Use codex.AutoApproveCallbacks(codex.AutoApproveHandler{Logger: logger}) to attach a safe auto-approver through the preferred callback API. The unsafe handler can be passed to the same adapter when explicitly required.

Codex CLI compatibility

When the SDK spawns codex, it requires the CLI major/minor version to match the generated protocol version; patch differences are accepted. A mismatch, missing binary, or unparseable version returns *codex.CodexCompatibilityError before the process starts. Set CompatibilityPolicy: codex.Warn only after validating compatibility (and provide a logger to see the warning), or codex.Ignore to skip the probe. Custom transports are never probed.

Structured Output

Provide a JSON Schema to constrain the final assistant message.

prompt := "Summarize repo status"
schema := codex.MustJSON(map[string]any{
    "type": "object",
    "properties": map[string]any{
        "summary": map[string]any{"type": "string"},
        "status": map[string]any{"type": "string", "enum": []string{"ok", "action_required"}},
    },
    "required": []string{"summary", "status"},
    "additionalProperties": false,
})

_, err := thread.RunInputs(ctx, []codex.Input{codex.TextInput(prompt)}, &codex.TurnOptions{
    OutputSchema: schema,
})

JSON-typed options

Fields like ApprovalPolicy, SandboxPolicy, Effort, Summary, and OutputSchema accept any JSON-marshalable value. If you already have raw JSON, pass a json.RawMessage (or codex.MustJSON(...)) to avoid double encoding.

For common values, prefer typed constants from this package:

  • codex.ApprovalPolicyNever, codex.ApprovalPolicyOnFailure, codex.ApprovalPolicyOnRequest, codex.ApprovalPolicyUntrusted
  • codex.SandboxModeReadOnly, codex.SandboxModeWorkspaceWrite, codex.SandboxModeDangerFullAccess
  • codex.ReasoningEffortNone, codex.ReasoningEffortMinimal, codex.ReasoningEffortLow, codex.ReasoningEffortMedium, codex.ReasoningEffortHigh, codex.ReasoningEffortXHigh

Inputs and overload errors

Use helpers to build structured inputs:

inputs := []codex.Input{
    codex.TextInput("Inspect this file"),
    codex.MentionInput("AGENTS.md"),
}

Overload classification uses ordinary Go errors:

if codex.IsOverloaded(err) && operationIsIdempotent {
    // Retry according to your caller policy.
}

IsOverloaded classifies structured overload failures; it does not prove that repeating an operation is safe. Base retries on idempotency or an application-level idempotency key. IsRetryable remains as a deprecated alias for source compatibility.

Low-level RPC

Use the RPC client directly for full control.

rpcClient := client.Client()
models, err := rpcClient.ModelList(ctx, protocol.ModelListParams{})

Use checked constructors such as rpc.NewClientChecked, rpc.NewConnTransportChecked, and rpc.NewReplayTransportChecked when a dependency is dynamic. Their legacy counterparts panic immediately on invalid programmer input instead of failing later in a background goroutine.

All outbound writes are serialized through a bounded queue. Call and Notify return when their contexts end; with a legacy rpc.Transport, the underlying write may remain blocked until Close. Implement rpc.ContextTransport when writes themselves must observe cancellation. App-server request handling defaults to four workers and a queue of 64; tune rpc.ClientOptions.ServerRequestWorkers and ServerRequestQueueCapacity when using the low-level client.

Inbound JSON-RPC messages are limited to 8 MiB by default. Set rpc.ClientOptions.MaxMessageBytes to apply a smaller client-side acceptance limit. Built-in line transports retain the 8 MiB allocation ceiling even if the client option is larger. Custom transports return an already allocated string, so they must enforce their own read/allocation limit; the client option then rejects oversized returned values. Oversized messages close the client with an error matching rpc.ErrMessageTooLarge.

About

Go SDK for Codex app-server: spawn codex app-server, run threads/turns with streaming, approvals, structured output, and typed JSON-RPC access.

Resources

Stars

14 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages