Skip to content

feat(examples): OpenAI Agents SDK local-sandbox tutorials (sync + async + temporal)#377

Merged
danielmillerp merged 5 commits into
nextfrom
dm/oai-local-sandbox-example
May 30, 2026
Merged

feat(examples): OpenAI Agents SDK local-sandbox tutorials (sync + async + temporal)#377
danielmillerp merged 5 commits into
nextfrom
dm/oai-local-sandbox-example

Conversation

@danielmillerp
Copy link
Copy Markdown
Contributor

@danielmillerp danielmillerp commented May 29, 2026

What

Adds examples/tutorials/00_sync/050_openai_agents_local_sandbox/ — a sync FastACP tutorial demonstrating the OpenAI Agents SDK with a LOCAL sandbox.

Pattern

A SandboxAgent (OpenAI Agents SDK) with the Shell capability runs real shell commands on the host via Runner.run, using the built-in UnixLocalSandboxClient (backend_id="unix_local") — no Docker, Temporal, or remote infra. The SDK runs its tool-call loop internally; this sync handler returns the final answer.

agent = SandboxAgent(name="Local Sandbox Assistant", capabilities=[Shell()], ...)
run_config = RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient(), options=UnixLocalSandboxClientOptions()))
result = await Runner.run(agent, input=user_message, run_config=run_config)

Structure

Mirrors 040_pydantic_ai: project/{acp,agent,tools}.py, manifest.yaml, Dockerfile, pyproject.toml, README.md, and tests/test_agent.py (exercises the sandbox: real python3 --version + a compute). set_tracing_disabled(True) + OPENAI_AGENTS_DISABLE_TRACING=1 to avoid api.openai.com tracing 401s behind a gateway.

Uses openai-agents>=0.14.3 (now on next via #375).

Refs: https://developers.openai.com/api/docs/guides/agents , https://openai.com/index/the-next-evolution-of-the-agents-sdk/

🤖 Generated with Claude Code

Greptile Summary

Adds three new tutorial variants — sync, async-base, and Temporal — that demonstrate running an OpenAI Agents SDK SandboxAgent with the UnixLocalSandboxClient local sandbox. Each variant ships a complete project (acp.py, agent.py, tools.py), manifest.yaml, Dockerfile, and integration tests; a new sync-openai-agents-local-sandbox CLI template is also registered in init.py.

  • The sync and async-base tutorials expose a Shell-capable SandboxAgent via Runner.run with max_turns=10; multi-turn memory for the async variant is persisted through adk.state.
  • The Temporal variant wires the local sandbox through SandboxClientProvider so shell tool calls execute as durable Temporal activities, using TemporalStreamingModelProvider and ContextInterceptor for real-time streaming.
  • The async-base acp.py and Temporal workflow.py register the SGP tracing processor unconditionally (even when SGP_API_KEY/SGP_ACCOUNT_ID are empty), unlike the guarded pattern used in the sync variant and the CLI template.

Confidence Score: 5/5

Safe to merge; all changes are additive tutorial examples and a new CLI template with no modifications to core library code.

The change is purely additive — new example directories and a template scaffold. Core SDK and library code is untouched. The two tracing-guard inconsistencies and the wrong return-type annotation are cosmetic/style issues that don't affect runtime correctness in typical deployments.

examples/tutorials/10_async/00_base/120_openai_agents_local_sandbox/project/acp.py and examples/tutorials/10_async/10_temporal/120_openai_agents_local_sandbox/project/workflow.py — unconditional SGP tracing registration should be guarded the same way the sync tutorial does it.

Important Files Changed

Filename Overview
examples/tutorials/00_sync/050_openai_agents_local_sandbox/project/agent.py Sync sandbox agent with max_turns=10 limit; correctly disables SDK tracing and constructs RunConfig with local Unix sandbox.
examples/tutorials/00_sync/050_openai_agents_local_sandbox/project/acp.py Sync ACP handler; guards LITELLM_API_KEY copy and SGP tracing registration correctly with env-var presence checks.
examples/tutorials/10_async/00_base/120_openai_agents_local_sandbox/project/acp.py Async base ACP handler; registers SGP tracing processor unconditionally (unlike the guarded sync version), which could spam errors when SGP credentials are absent.
examples/tutorials/10_async/00_base/120_openai_agents_local_sandbox/project/agent.py Async base agent; return type annotation -> "Runner" is wrong (returns RunResult, not a Runner); otherwise functionally correct with max_turns=10.
examples/tutorials/10_async/10_temporal/120_openai_agents_local_sandbox/project/workflow.py Temporal workflow with Streaming + local sandbox; calls add_tracing_processor_config unconditionally with potentially empty SGP credentials.
examples/tutorials/10_async/10_temporal/120_openai_agents_local_sandbox/project/acp.py Temporal ACP setup; correctly registers OpenAIAgentsPlugin with UnixLocalSandboxClient and debug scaffolding is guarded.
examples/tutorials/10_async/10_temporal/120_openai_agents_local_sandbox/project/run_worker.py Worker setup; registers TemporalStreamingModelProvider, ContextInterceptor, and local sandbox client cleanly.
src/agentex/lib/cli/commands/init.py Adds SYNC_OPENAI_AGENTS_LOCAL_SANDBOX template type and wires it into the init command correctly.
src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/agent.py.j2 CLI template for the sync local-sandbox agent; mirrors the tutorial correctly with max_turns=10 and tracing disabled.

Sequence Diagram

sequenceDiagram
    participant User
    participant FastACP
    participant RunAgent
    participant OpenAIAgentsSDK as OpenAI Agents SDK (Runner.run)
    participant UnixLocalSandbox as UnixLocalSandboxClient

    User->>FastACP: send_message / task_event_send
    FastACP->>RunAgent: run_agent(user_message / input_list)
    RunAgent->>OpenAIAgentsSDK: "Runner.run(agent, input, run_config, max_turns=10)"
    loop Tool-call loop (max 10 turns)
        OpenAIAgentsSDK->>UnixLocalSandbox: execute shell command
        UnixLocalSandbox-->>OpenAIAgentsSDK: stdout / stderr
    end
    OpenAIAgentsSDK-->>RunAgent: RunResult (final_output)
    RunAgent-->>FastACP: final text
    FastACP-->>User: TextContent response
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
examples/tutorials/10_async/00_base/120_openai_agents_local_sandbox/project/agent.py:82
The return type annotation `-> "Runner"` is wrong — `Runner.run()` returns a run-result object (e.g. `RunResult`), not a `Runner` instance. As written, type checkers and readers of this tutorial will assume the caller gets back a `Runner`, leading to confusion when they try to call `.final_output` or `.to_input_list()` on it.

```suggestion
async def run_agent(input_list: list):
```

### Issue 2 of 3
examples/tutorials/10_async/00_base/120_openai_agents_local_sandbox/project/acp.py:47-53
The SGP tracing processor is registered unconditionally here, even when `SGP_API_KEY` / `SGP_ACCOUNT_ID` are absent. The sync variant of this tutorial correctly guards the call with `if SGP_API_KEY and SGP_ACCOUNT_ID:`. Registering with empty strings will attempt to authenticate every trace export, producing repeated auth-failure errors in environments that don't have SGP credentials — exactly the local-sandbox scenario this tutorial targets. The same pattern also applies in `workflow.py` for the Temporal variant.

```suggestion
SGP_API_KEY = os.environ.get("SGP_API_KEY", "")
SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "")
SGP_CLIENT_BASE_URL = os.environ.get("SGP_CLIENT_BASE_URL", "")

if SGP_API_KEY and SGP_ACCOUNT_ID:
    add_tracing_processor_config(
        SGPTracingProcessorConfig(
            sgp_api_key=SGP_API_KEY,
            sgp_account_id=SGP_ACCOUNT_ID,
            sgp_base_url=SGP_CLIENT_BASE_URL,
        )
    )
```

### Issue 3 of 3
examples/tutorials/10_async/10_temporal/120_openai_agents_local_sandbox/project/workflow.py:64-70
Same unconditional tracing registration issue as in the async-base variant: `add_tracing_processor_config` is called at module load time with empty strings when SGP credentials are not set, which will attempt (and fail) to export every trace to the SGP endpoint.

```suggestion
_sgp_api_key = os.environ.get("SGP_API_KEY", "")
_sgp_account_id = os.environ.get("SGP_ACCOUNT_ID", "")
if _sgp_api_key and _sgp_account_id:
    add_tracing_processor_config(
        SGPTracingProcessorConfig(
            sgp_api_key=_sgp_api_key,
            sgp_account_id=_sgp_account_id,
        )
    )
```

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'origin/nex..." | Re-trigger Greptile

Sync FastACP tutorial demonstrating the OpenAI Agents SDK with a LOCAL sandbox
(UnixLocalSandboxClient): a SandboxAgent with the Shell capability runs real
shell commands on the host via Runner.run. Mirrors 040_pydantic_ai; tests
exercise the sandbox (python version + compute). Uses openai-agents>=0.14.3
(already on next via #375).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread examples/tutorials/00_sync/050_openai_agents_local_sandbox/project/agent.py Outdated
…tutorials

Companion to the sync 050 example, covering all three execution modes:
- 10_async/00_base/120: async (non-Temporal) FastACP
- 10_async/10_temporal/120: Temporal — OpenAIAgentsPlugin + SandboxClientProvider("local", UnixLocalSandboxClient()) + temporal_sandbox_client("local"); uses NoopSnapshotSpec to skip the per-turn workspace snapshot and relies on the streaming model to emit the assistant message (no double-post).
Each mirrors its family's sibling (110_pydantic_ai / 060_open_ai_agents_sdk) and has integration tests exercising the sandbox.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danielmillerp danielmillerp changed the title feat(examples): local-sandbox OpenAI Agents SDK tutorial (050) feat(examples): OpenAI Agents SDK local-sandbox tutorials (sync + async + temporal) May 29, 2026
@danielmillerp
Copy link
Copy Markdown
Contributor Author

Expanded to cover all three execution modes, each with integration tests:

  • 00_sync/050_openai_agents_local_sandbox — sync FastACP
  • 10_async/00_base/120_openai_agents_local_sandbox — async (non-Temporal)
  • 10_async/10_temporal/120_openai_agents_local_sandbox — Temporal (standard OpenAIAgentsPlugin + SandboxClientProvider("local", UnixLocalSandboxClient()) + temporal_sandbox_client("local"); NoopSnapshotSpec() to skip the per-turn workspace snapshot; relies on the streaming model to emit the assistant message — no double-post).

All use the OpenAI Agents SDK SandboxAgent+Shell with the built-in local unix_local backend (no Docker/remote infra). Tests exercise the sandbox (python version + a compute).

…50 test helper

- New 'agentex init' template 'sync-openai-agents-local-sandbox' (registered in
  init.py: TemplateType enum + project_files + menu) that scaffolds an OpenAI
  Agents SDK agent on the local (unix_local) sandbox, based on the 050 tutorial.
- Fix 050 sync test's _response_text helper to dig through TaskMessage->TextContent
  (it previously dropped non-str .content; verified locally the agent returns the
  correct answer).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danielmillerp
Copy link
Copy Markdown
Contributor Author

Local verification (Scale LiteLLM proxy, gpt-4o-mini — the proxy serves the Responses API the OpenAI Agents SDK needs):

  • async (10_async/00_base/120): 2/2 tests passed end-to-end — sandbox shell ran for real, answers contained "Python 3" and "42".
  • sync (00_sync/050): the agent works (returned Python 3.12.12 from a real exec_command); fixed a bug in the test's _response_text helper (it dropped TaskMessage.content because it wasn't a bare str — now digs through to the string).
  • temporal (10_async/10_temporal/120): same agent pattern; not re-run live here (needs local Temporal+Redis), but it's the exact OpenAIAgentsPlugin + SandboxClientProvider/temporal_sandbox_client bridge pattern already proven live elsewhere.

Also added an agentex init template sync-openai-agents-local-sandbox (registered in init.py) so this pattern can be scaffolded directly.

danielmillerp and others added 2 commits May 29, 2026 19:54
…ard, temporal instructions

- Add max_turns=10 to all Runner.run calls (sync/async/temporal + template) so an
  agent with shell access can't loop unbounded.
- LITELLM_API_KEY only sets OPENAI_API_KEY when one isn't already set (don't clobber
  a real key).
- Temporal workflow: instructions no longer call .format(timestamp=...) (the
  placeholder was missing -> no-op, and datetime.now() is non-deterministic inside a
  Temporal workflow); use the static instructions and drop the unused import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danielmillerp
Copy link
Copy Markdown
Contributor Author

Addressed Greptile's review (commit b709d874), applied consistently across all three variants + the init template:

  • P1 (no max_turns): added max_turns=10 to every Runner.run (sync/async/temporal + template) so an agent with shell access can't loop unbounded.
  • P2 (LITELLM_API_KEY clobber): the shim now only sets OPENAI_API_KEY when one isn't already present (if _litellm_key and not os.environ.get("OPENAI_API_KEY")).
  • Temporal INSTRUCTIONS {timestamp} no-op: dropped the .format(timestamp=datetime.now()...) — the placeholder was missing (no-op) AND datetime.now() is non-deterministic inside a Temporal workflow. Uses the static instructions now; removed the unused import.

Also merged latest next. Remaining: human approval + CI.

@danielmillerp danielmillerp merged commit a66d239 into next May 30, 2026
46 checks passed
@danielmillerp danielmillerp deleted the dm/oai-local-sandbox-example branch May 30, 2026 00:14
@stainless-app stainless-app Bot mentioned this pull request May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant