agents-go tracks openai-agents-python v0.17.7: the run loop, item model, defaults (max turns 10, strict schemas on, tool errors fed back to the model, tool_choice reset after tool use) and most names map one-to-one. This page lists everything that intentionally differs — first how the same concepts look in Go, then what each side has that the other lacks.
| Python | Go |
|---|---|
Agent(name=..., instructions=...) |
&agents.Agent{Name: ..., Instructions: agents.StaticInstructions(...)} |
instructions= callable |
agents.InstructionsFunc(func(ctx, rc, agent) (string, error)) |
Runner.run / Runner.run_sync |
agents.Run(ctx, agent, input, opts) (Go has no sync/async split) |
Runner.run_streamed |
agents.RunStreamed(ctx, agent, input, opts) |
run_config / Runner.run(...) kwargs |
agents.RunOptions{...} |
@function_tool decorator |
agents.NewFunctionTool[Args, Result](name, desc, fn) |
| pydantic argument model + docstring | argument struct + json:"..."/jsonschema:"..." tags |
output_type=MyModel |
OutputType: agents.OutputType[MyModel]() |
ToolOutputText / ToolOutputImage / ToolOutputFileContent |
agents.ToolOutputText / ToolOutputImage / ToolOutputFile (return one, or []agents.ToolOutputContent, from a function tool) |
result.final_output_as(T) |
agents.FinalOutputAs[T](res) |
handoff(agent) / agent.handoffs |
agents.HandoffTo(agent) / Agent.Handoffs |
agent.as_tool(...) |
agent.AsTool(agents.AgentToolConfig{...}) |
@input_guardrail / @output_guardrail |
agents.InputGuardrail{Name, Run} / agents.OutputGuardrail{Name, Run} struct values, or agents.NewInputGuardrail(name, fn) / agents.NewOutputGuardrail(name, fn) simplified constructors |
RunContextWrapper[T] |
*agents.RunContext with Context any (type-assert back) |
SQLiteSession |
memory.FileSession (JSONL file; same Session interface) |
reset_tool_choice=True (default) |
DisableToolChoiceReset (zero value = Python's default behavior) |
max_turns=10 |
RunOptions.MaxTurns (0 means the same default of 10) |
exceptions (MaxTurnsExceeded, …) |
error values (*MaxTurnsError, …) matched with errors.As |
RunErrorDetails on exceptions |
AgentsError.Details, reachable via agents.AsAgentsError(err) |
set_default_openai_key / globals |
none — pass openai.NewProvider(...) explicitly in RunOptions |
custom_data_extractor= (function tools) |
FunctionTool.CustomDataExtractor (SDK-only tool output metadata; tools) |
RunConfig.tool_execution.pre_approval_tool_input_guardrails |
RunOptions.PreApprovalToolInputGuardrails |
resume a paused run (state as input to Runner.run / Runner.run_streamed) |
agents.ResumeRun(ctx, state, opts) / agents.ResumeRunStreamed(ctx, state, opts) |
Generics and reflection instead of pydantic. Tool schemas come from struct reflection at construction time (NewFunctionTool[A, R]), structured outputs from OutputType[T](). Validation on the way back in uses encoding/json plus a root-level required-key check — looser than pydantic's full validation (nested required fields are not enforced). Two schema-shape limits surface at construction time as explicit errors rather than as API 400s: any/interface{} fields (no strict-mode schema exists for "anything") and recursive types (pydantic emits $defs/$ref for these; the Go reflector rejects cycles).
Two contexts instead of one wrapper. Python's RunContextWrapper[T] carries both your data and run state. Go splits them: context.Context handles cancellation/deadlines (and is honored mid-run, mid-stream and inside tools), while RunContext.Context any carries your data without generics on every type.
Errors instead of exceptions. Every failure is a returned error. SDK error types embed AgentsError; errors.As matches concrete types even through %w wrapping, and agents.AsAgentsError extracts the embedded base (with RunErrorDetails) generically.
Concurrency is explicit. Tools requested in one turn run concurrently via goroutines (Python interleaves on the event loop). Hooks and shared context values must be goroutine-safe. Streaming uses iter.Seq2 (for event, err := range sr.Events()) instead of async for, and there is no run_sync because Run is already synchronous.
Sealed interfaces instead of unions. Tool, StreamEvent, RunItem and ToolUseBehavior are closed interfaces you type-switch on, mirroring Python's Union types.
| Area | Python v0.17.7 | Go |
|---|---|---|
| Tool errors | failure_error_function default feeds the error to the model |
Same default (DefaultToolErrorFunction); set the field to nil for fatal |
| Tool timeout | timeout_seconds + timeout_behavior (error_as_result / raise_exception) |
FunctionTool.Timeout → *ToolTimeoutError, fed back via FailureErrorFunction when set (≈ error_as_result), else fatal (≈ raise_exception). Enforced by the runner: the call returns at the deadline even if the tool ignores its context (the tool goroutine finishes in the background, its late result discarded) |
| Tool panics | tool exceptions flow into failure_error_function |
same: a panicking tool (or guardrail) is recovered and converted to an error instead of crashing the process |
| HITL interruption scope | tools not needing approval still execute in the interrupted turn; only approval-gated calls pause | all tool calls in the turn wait until ResumeRun when any of them needs approval (keeps RunState free of partial results; side effect: "safe" tools run with post-approval context) |
| Model refusal | refusal text surfaces as plain content | run fails with *ModelRefusalError carrying the refusal |
| Handoff input filter | receives input_history / pre_handoff_items / new_items separately |
receives one flattened InputHistory; the session always keeps the unfiltered conversation. NestHandoffHistory ports nest_handoff_history (fold + flatten) on top of this |
| HITL state | RunState JSON (Python format) |
RunState JSON round-trips Go↔Go only, and rebuilding needs an agent-name registry (Go functions don't serialize). The state carries max_turns so ResumeRun continues under the original budget; resumed NewItems deserialize as raw items (ItemType() survives, concrete type assertions don't) |
| Input guardrail timing | parallel with the first model call | same for Run; RunStreamed runs them synchronously before the first call |
| Streamed text items | message_output_created fires once per completed message |
same (use raw delta events for token-level UI) |
| Session backends | SQLite / SQLAlchemy / Redis / encrypted / OpenAI Conversations / compaction | InMemorySession + FileSession (JSONL) in core; sessions module adds SQLite/PostgreSQL via bun; openai.ConversationsSession (server-side via the Conversations API); openai.CompactionSession (responses.compact decorator, attempted once per run vs Python's per turn); implement Session for anything else |
| Tracing backend | OpenAI traces dashboard by default | generic tracer → processor → exporter pipeline (console/HTTP/custom); not the OpenAI dashboard wire format. Traces export at start, spans at finish |
| Sensitive trace data | RunConfig.trace_include_sensitive_data (env OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA) gates span content |
RunOptions.TraceIncludeSensitiveData *bool, same env var and default (true); gates the generation span's request/response data keys (model, system_instructions, input, tools, model_settings, handoffs, output_schema, output, …) and the function span's input/output |
| Compaction tracing | not traced | Go-only: the runner wraps RunCompaction in a "compaction" span, opened lazily via CompactionArgs.StartSpan so no-op passes emit nothing; sessions annotate before/after item counts |
| Compaction failure | raises, failing the run | best-effort: the run's items are already saved and the final output produced, so the error is recorded on the compaction span and the run still succeeds |
| MCP tool errors | AgentsException aborts the run |
fed back to the model as tool output by default (fast-fail is opt-in via the tool's error function); strict-schema normalization failures silently fall back to non-strict for that tool (Python logs). Duplicate tool names across servers/local tools are a UserError on both sides |
| Server-side conversation state | previous_response_id / conversation_id parameters |
RunOptions.UsePreviousResponseID and RunOptions.ConversationID (both send only deltas; neither combines with a local Session). openai.ConversationsSession also persists history server-side via the Conversations API |
| Stored prompts | Agent(prompt=Prompt(id, version, variables)) / DynamicPromptFunction |
Agent.Prompt = StaticPrompt(agents.Prompt{...}) or PromptFunc(...) (OpenAI Responses backend only) |
Usage of nested as_tool runs |
separate from parent | same (separate), but nested spans join the parent trace |
- Hosted OpenAI tools: web search, file search, code interpreter, computer use, image generation,
local_shell,apply_patch— deliberately not modeled; tools are provider-agnostic function tools, and a non-standardtool_choiceis sent as a function name. (For file editing, Go providesapply_patchas a sandbox-backed function tool — Codex-style patches applied through theSandboxabstraction, not the hosted OpenAIapply_patch; tools) - Chat Completions model layer — only the Responses API (use a Responses-compatible gateway, or implement
Model) - LiteLLM adapter — but native multi-provider routing, retry and fallback are supported via
Modeldecorators (models) - Redis / encrypted / SQLAlchemy session backends — only SQLite & PostgreSQL are provided (
sessionsmodule); implementSessionfor others. (OpenAIConversationsSessionandOpenAIResponsesCompactionSessionare ported, asopenai.ConversationsSessionandopenai.CompactionSession.) - Realtime and voice agents
- REPL utility (
run_demo_loop) and visualization (Graphviz) - MCP-level
custom_data_extractor— Python's MCP servers (and hosted tools) accept their own custom-data extractors with access to the rawCallToolResult; in Go onlyFunctionTool.CustomDataExtractorexists, and MCP-bridged tools don't expose the raw result to it
- Self-hosted sandboxes: run model-written code in your own infrastructure — locked-down Docker containers (
sandbox/docker) or a remote host over SSH (sandbox/ssh) — exposed viasandbox.CodeTool. Python's sandboxes target hosted providers (e2b / modal / blaxel) rather than self-hosted backends - Hooks can veto: any hook returning an error aborts the run (Python hooks are observe-only)
FileSession: zero-dependency JSONL persistence with per-path locking and atomic rewrites- Skills (
skillsmodule): the open Agent SkillsSKILL.mdformat implemented onInstructions+ a function tool — provider-agnostic and sandbox-free, unlike Python's sandbox-capability skills - Session forking (
ForkSession/ForkSessionAt/IndexOfItemID): clone a conversation or branch at a specific point — works across anySessionbackend pair. Python's closest isAdvancedSQLiteSession's branch support, which is tied to that one backend ItemsReplacer/ReplaceSessionItems: optional Session capability for atomically swapping the whole history, used by compaction and summarization so a failure mid-rewrite cannot leave the session empty; all built-in backends implement itSlidingWindowSession: provider-agnostic history summarization (anyModel, pair-aware split points) as an alternative to the OpenAI-onlyresponses.compactdecorator- Provider-level decorators:
NewRetryProvider(inner, policy)andNewFallbackProvider(primary, fallbacks...)wrap aModelProviderso everyModelit produces automatically retries or falls back — the provider-level counterparts ofNewRetryModel/NewFallbackModel, useful when you know the policy at configuration time but not the model name. Fallback error classification is configurable viaWithShouldFallback(default: everything except context cancellation advances the chain) NewDynamicOutputSchema: builds anOutputSchemafrom amap[string]anyJSON Schema at runtime, complementing the compile-timeOutputType[T]()for config-driven agentsWrapInstructions: decorates anInstructionsvalue with a prefix and/or suffix applied at resolution time, eliminating theGetInstructions(ctx, nil, nil)+ concatenate + re-wrap patternCompositeRunHooks: combines multipleRunHooksinto one, dispatching each callback to every hook in order with first-error short-circuitRetryPolicyJSON round-trip:RetryPolicyimplementsjson.Unmarshaler/json.Marshalerwith millisecond-based fields (base_delay_ms,max_delay_ms), making it directly usable withjson.Unmarshalfrom configuration stores- Simplified guardrail constructors:
NewInputGuardrail(name, fn)andNewOutputGuardrail(name, fn)accept a callback that receives only the input/output, skipping ctx/rc/agent when you don't need them - Session item helpers:
MarshalItems/UnmarshalItemshandle the common JSON ↔[]TResponseInputItemround-trip (including nil/empty/"null" edge cases) so DB session backends don't rewrite it NewRawFunctionTool: builds aFunctionToolfrom a pre-built JSON Schemamap[string]anyand a raw-JSON callback, for tools whose schema is loaded at runtime rather than reflected from a Go type- Enum parse helpers:
ParseToolNotFoundBehavior(string)/ToolNotFoundBehavior.String()andParseToolUseBehavior(string)convert between configuration strings and SDK enum types