Skip to content

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

20 stars

Watchers

0 watching

Forks

Latest commit

 

History

75 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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.26.9 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.

Updating to protocol 0.162.1

This SDK targets Codex CLI 0.162.x. Low-level RPC adds ThreadAttachmentOwnerList for reverse attachment lookup and thread/prediction/updated notifications with raw-preserving completed/failed results. TurnStartParams.ParentTurnID and RootTurnID attribute delegated work; leave them unset for work started directly by the user. Returned Turn values expose nullable RootTurnID.

Goal set/clear parameters now accept optional Origin (user or automatic). Missing origin does not supply user authorization. MCP OAuth responses and completion notifications expose optional LoginID for matching login attempts. Assistant message phases now include partial_answer, which may precede more output or tools.

CodexErrorInfo is now an alias of json.RawMessage, preserving arbitrary future error objects and large integers. Callers assigning strings or maps must marshal those values first; use json.Unmarshal to inspect the received JSON. ResponseItem recognizes additional_tools and preserves its arbitrary tool payloads. Upstream removed ModelProviderCapabilitiesReadResponse.NamespaceTools; that field is removed from the generated SDK type.

Updating to protocol 0.160.0

The 0.160.0 update added gateway OAuth status, login, and cancellation, plus thread attachment add/list/remove calls and their notifications. Attachment payloads use json.RawMessage to preserve arbitrary JSON numbers and future fields. Upstream removed thread/rollback; its generated method and types are removed. Use ThreadRevert with an explicit BeforeTurnID instead.

History pagination still accepts ThreadItemsListParams.Cursor as a string pointer. For an exclusive item position, construct a protocol.ThreadItemsListAnchor with protocol.NewThreadItemsListAnchor, set CursorAnchor, and provide a nonempty TurnID. Supplying both cursor forms is an error. History entries expose optional StartedAtMs and CompletedAtMs.

Image inputs and function outputs accept either a URL or an uploaded file ID through the existing raw-preserving union constructors. At least one image source is required. Lifecycle responses retain DisabledPluginIDs, and resume responses expose CollaborationMode. In low-level TurnStartParams, a nil DisabledPluginIDs preserves the saved list; a pointer to an empty slice clears it. These saved plugin IDs do not yet filter plugin capabilities upstream.

MCP tool calls expose descriptor UI metadata, and resource reads can select an explicit app/account target. Its nullable LinkID is required on the wire; null requests no-auth access subject to the app's policy. Personality selection is deprecated upstream: friendly and pragmatic no longer select a style. ConfigRequirements.WindowsSandboxPrivateDesktop and its helper type are removed upstream. AllowedWindowsSandboxImplementations now uses WindowsSandboxImplementation, which includes mxc, instead of WindowsSandboxSetupMode.

Updating to protocol 0.154.0

Protocol 0.154.0 introduced the following additions. AccountRateLimitsRead(ctx) remains available; low-level AccountRateLimitsReadWithParams accepts optional protocol.GetAccountRateLimitsParams capabilities for Luna Reserve and background reads that exclude reset-credit details. Rate-limit responses expose nullable OrdinaryUsageAllowed and quota NormalModelSlug values. A missing ordinary-usage permission does not imply that usage has recovered.

protocol.Thread.Originator identifies the client that created a thread. ThreadListOptions.Originators filters by exact originator on hosted backends; the local app-server rejects nonempty originator filters. Nil or empty filters leave originators unrestricted.

The protocol also adds the configuration_update response-item variant, application network requirement types, and MCP tool-discovery errors. Detached review delivery is deprecated upstream; start a separate thread and request an inline review instead.

Updating to protocol 0.153.4

Protocol 0.153.4 introduced low-level RPC for paginated turn and item history, thread revert, plugin reconciliation, and new realtime, project, queue, and authentication-recovery notifications.

AccountUsageRead(ctx) remains available. Use AccountUsageReadWithParams(ctx, &protocol.GetAccountTokenUsageParams{ThreadID: &id}) for thread-specific usage. ThreadForkOptions.ExcludeTurns is supported again. Low-level resume and turn-start parameters expose the new history and per-turn options. Thread.ProjectID is nullable and always serializes as projectId, as required by the new protocol. Approval requests that omit kind decode as command, matching the upstream default for older servers.

Section appearance updates distinguish omission, clearing, and replacement: leave ThreadSectionUpdateParams.Appearance nil to preserve it, assign a pointer to a nil *protocol.ThreadSectionAppearance to clear it, or point to an appearance value to replace it. Existing section response names remain available.

CapabilityRootLocation, ClientNotification, DynamicToolNamespaceTool, LocalShellAction, and ReasoningItemReasoningSummary now use the same raw-preserving union wrappers as the other discriminated types. Replace direct map assignments with the corresponding protocol.New<Type>(value) constructor, and use RawJSON() to inspect the payload. Known variants validate shared required fields; unknown future variants continue to round-trip unchanged.

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

20 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages