Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions openai_agents_058/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=sk-proj-your-key-here
29 changes: 29 additions & 0 deletions openai_agents_058/01_hosted_sandbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Demo 1: the harness as an API.

One call creates an agent, a sandbox, and a session, then streams the agent
writing a script, running it, and reporting the real output. OpenAI runs the
loop. You never see a tool-call round trip.
"""

from common import MODEL, Printer, make_client

client = make_client()
printer = Printer()

with client.beta.agents.sessions.create(
agent={
"model": MODEL,
"instructions": "Write clean code, run it, and report the actual output. Be brief.",
},
environment={"type": "openai_hosted"},
input=(
"Create primes.py that prints the first 10 prime numbers and their sum. "
"Run it and show me the output."
),
stream=True,
) as events:
for event in events:
if printer.handle(event):
break

print("\nsession id:", printer.session_id)
85 changes: 85 additions & 0 deletions openai_agents_058/02_function_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Demo 2: your tools, your session.

The agent calls a function you own. When it needs a result the session emits
requires_action; you run the function and post the result back. Then a second
message continues the SAME session, so the agent still knows the earlier answer.

No sandbox here (environment type "none"), so this is the cheapest way to run
the harness: tokens only, no container.
"""

import json

from common import MODEL, Printer, make_client

client = make_client()

TOOLS = [
{
"type": "function",
"name": "lookup_member",
"description": "Look up a Skool community member by email.",
"parameters": {
"type": "object",
"properties": {"email": {"type": "string"}},
"required": ["email"],
"additionalProperties": False,
},
}
]

MEMBERS = {
"sam@example.com": {"name": "Sam Rivera", "plan": "pro", "joined": "2026-03-02", "last_seen_days": 41},
}


def lookup_member(args: dict) -> dict:
member = MEMBERS.get(args["email"])
return {"found": member is not None, "member": member}


# Turn 1: create the session with the first task and handle the tool call by hand.
printer = Printer()
with client.beta.agents.sessions.create(
agent={
"model": MODEL,
"instructions": "You are a CRM assistant. Use lookup_member for member questions. Be brief.",
"tools": TOOLS,
},
environment={"type": "none"},
input="Is sam@example.com at risk of churning?",
stream=True,
) as events:
for event in events:
if event.type == "agent.session.requires_action":
for action in event.session.required_actions:
if action.type == "function_call" and action.name == "lookup_member":
result = lookup_member(action.arguments)
client.beta.agents.sessions.events.create(
event.session.id,
events=[{
"type": "agent.session.input.tool_result",
"turn_id": action.turn_id,
"call_id": action.call_id,
"success": True,
"output": json.dumps(result),
}],
)
if printer.handle(event):
break

session_id = printer.session_id

# Turn 2: same session, so the agent remembers Sam. The stream helper runs the
# tool handler for us this time.
print("\n--- follow-up on the same session ---")
printer = Printer()
with client.beta.agents.sessions.stream(
session_id,
input="Draft a two-sentence check-in message to them.",
tool_handlers={"lookup_member": lookup_member},
) as stream:
for event in stream:
printer.handle(event)

print("\nsession id:", session_id)
36 changes: 36 additions & 0 deletions openai_agents_058/03_subagents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Demo 3: subagents, one flag.

Turn on multi_agent and the harness can split a task across subagents that run
in parallel inside the same sandbox. You watch them get created and closed in
the event stream. No orchestration code on your side.
"""

from common import MODEL, Printer, make_client

client = make_client()
printer = Printer()

with client.beta.agents.sessions.create(
agent={
"model": MODEL,
"instructions": (
"You lead a small team. For multi-part tasks, delegate each part to a "
"subagent, run them in parallel, then combine the results into one short answer."
),
"multi_agent": {"enabled": True, "max_concurrent_subagents": 3},
},
environment={"type": "openai_hosted"},
input=(
"Three independent jobs, one subagent each: "
"(1) write fib.py that prints the 20th Fibonacci number and run it; "
"(2) write words.py that counts the words in the sentence 'the quick brown fox jumps over the lazy dog' and run it; "
"(3) write pi.py that prints pi to 8 decimal places using only the math module and run it. "
"Report the three outputs in one line each."
),
stream=True,
) as events:
for event in events:
if printer.handle(event):
break

print("\nsession id:", printer.session_id)
115 changes: 115 additions & 0 deletions openai_agents_058/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# OpenAI Agents API demo (video 058)

Three small scripts against the OpenAI Agents API (public beta, launched 2026-09-10). The API exposes the Codex harness as a managed service: OpenAI runs the agent loop, the sandbox, context compaction, and subagents. You send input and read events.

Each script is under 80 lines and meant to be read top to bottom on screen.

## What is in this folder

| File | Section | What it does | Read it when |
|---|---|---|---|
| `common.py` | Setup | Loads `.env`, builds the `OpenAI` client, sets `MODEL`. `Printer` turns the ~30 event types into a short timestamped transcript. | You want to change the model or print more event types. |
| `01_hosted_sandbox.py` | Demo 1 | One `sessions.create(stream=True)` call: agent + hosted sandbox + session. The agent writes and runs a Python script, reports real output. | First run. Proves the API works and shows sandbox provisioning time. |
| `02_function_tools.py` | Demo 2 | Your own function tool with `environment: none`. Turn 1 handles `requires_action` by hand; turn 2 uses `sessions.stream` with `tool_handlers` on the same session. | You want the agent to call your code, and want to see session memory across turns. |
| `03_subagents.py` | Demo 3 | `multi_agent.enabled` on a hosted sandbox. The harness spawns parallel subagents; you watch them in the event stream. | You want fan-out without writing an orchestrator. |
| `pyproject.toml`, `uv.lock` | Deps | `openai>=3.14`, `python-dotenv`. Python 3.11+. | Installing. |
| `.env.example` | Secrets | Template for `OPENAI_API_KEY`. Copy to `.env`, never commit `.env`. | Before the first run. |
| `CLAUDE.md` | Agent notes | Conventions for Claude Code or Hermes extending this folder. | You ask an agent to add a demo. |

Sections below: [Install](#install), [Run](#run), [Env vars](#env-vars), [What to expect](#what-to-expect-when-you-run-it), [On camera](#things-worth-saying-on-camera), [Docs](#docs).

| Script | What it shows | Sandbox | Cost on top of tokens |
|---|---|---|---|
| `01_hosted_sandbox.py` | One call creates an agent, a sandbox, and a session. The agent writes a script, runs it, reports the real output. | `openai_hosted` | Container rate (1 GB sandbox is $0.03 per 20 min, 5 min minimum) |
| `02_function_tools.py` | Your own function tool. Handle `requires_action` by hand on turn 1, then let `sessions.stream` run the handler on turn 2. Same session, so the agent remembers turn 1. | `none` | Nothing |
| `03_subagents.py` | One flag, `multi_agent.enabled`, and the harness spawns subagents in parallel inside one sandbox. You watch them appear in the event stream. | `openai_hosted` | Container rate |

`common.py` holds the client setup and a small event printer. The harness streams around 30 event types; the printer keeps commands, tool calls, messages, subagents, and the turn end.

## Install

Requires Python 3.11 or newer and an OpenAI API key with `api.agents.read`, `api.agents.write`, and `api.responses.write` scopes (a normal project key has all three).

With uv:

```bash
cd openai_agents_058
cp .env.example .env # then paste your key into .env
uv sync
```

With pip:

```bash
cd openai_agents_058
cp .env.example .env # then paste your key into .env
python -m venv .venv && source .venv/bin/activate
pip install "openai>=3.14" python-dotenv
```

## Run

```bash
uv run python 01_hosted_sandbox.py
uv run python 02_function_tools.py
uv run python 03_subagents.py
```

Or with the pip venv active, `python 01_hosted_sandbox.py` and so on.

## Env vars

| Var | Where | Notes |
|---|---|---|
| `OPENAI_API_KEY` | `.env` in this folder | Loaded by `python-dotenv` in `common.py`. `.env` is gitignored at the repo root. Never commit it. |

Model is `gpt-6-astra`, set once in `common.py`.

## What to expect when you run it

Each script prints a timestamped transcript built by `Printer` in `common.py`. Lines you will see, in order:

### 01_hosted_sandbox.py

1. `session sess_... env: openai_hosted` within a few seconds.
2. A `commentary:` line where the agent says what it is about to do.
3. `READY environment` then `CONNECTED environment`. Sandbox provisioning takes roughly 20 to 30 seconds. This is the part worth pointing at on camera.
4. One or more `$ /bin/bash -lc ...` lines. The agent usually looks for `AGENTS.md` first, the same habit Codex has locally, then runs `python3 /workspace/outputs/primes.py`.
5. `final_answer:` with the script's output: the first 10 primes (2 through 29) and their sum, 129.
6. `turn done.` and the session id. Expect about a minute end to end.

### 02_function_tools.py

1. `session sess_... env: none` almost immediately. No sandbox, no container charge.
2. `tool call: lookup_member {'email': 'sam@example.com'}`. That is the `requires_action` branch in the script firing and posting the result back.
3. `final_answer:` saying Sam is a churn risk (41 days inactive, Pro plan).
4. `--- follow-up on the same session ---`, then a `final_answer:` with a short check-in message. No second tool call: the session already holds Sam's record from turn 1. That is the point of the demo.
5. Both turns together take about 40 seconds.

### 03_subagents.py

1. `session sess_... env: openai_hosted`, then the environment `READY` and `CONNECTED` lines as in demo 1.
2. Three `subagent created: <name>` lines. The harness names its subagents itself.
3. `final_answer:` with one line per job: the 20th Fibonacci number (6765), the word count (9), and pi to 8 places (3.14159265).
4. `turn done.` Expect about a minute.

Subagent shell commands run on the subagents' own turns and do not appear on the root stream. List them with `client.beta.agents.sessions.subagents.items.list(...)` if you want them on screen.

`turn.usage` may come back `None` on completed turns, so the `turn done.` line does not always carry token counts. Use the pricing page for cost numbers.

## Things worth saying on camera

- `environment: {"type": "none"}` needs `input` at create time. The API returns `conversation-only sessions currently require initial input` without it. Hosted sandboxes can be created idle.
- `sessions.create(stream=True)` is the one-call path. `sessions.stream(session_id, input=..., tool_handlers=...)` is the follow-up path and it runs your tool handlers for you. Demo 2 shows both.
- Sessions with a hosted sandbox are deleted after keep-alives stop for an hour. Store the session id if you want to come back.
- US-only data residency, no Zero Data Retention, even self-hosted. Check before pointing this at customer data.
- Self-hosting the sandbox is `npm install -g @openai/codex@alpha` then `codex exec-server --remote <url> --environment-id <id>` with a separate restricted environment key. Not in these scripts; it needs the key from the dashboard Agents tab.

## Docs

- https://developers.openai.com/api/docs/guides/agents-api/overview
- https://developers.openai.com/api/docs/guides/agents-api/quickstart
- https://developers.openai.com/api/docs/guides/agents-api/tools/functions
- https://developers.openai.com/api/docs/guides/agents-api/sessions/events
- https://developers.openai.com/api/docs/guides/agents-api/environments/self-hosted
- https://developers.openai.com/api/docs/pricing (Containers row)
77 changes: 77 additions & 0 deletions openai_agents_058/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Shared helpers for the 058 demos. Keeps each demo file short enough to read on camera."""

import os
import sys
import time

from dotenv import load_dotenv
from openai import OpenAI

MODEL = "gpt-6-astra"


def make_client() -> OpenAI:
load_dotenv()
if not os.environ.get("OPENAI_API_KEY"):
sys.exit("OPENAI_API_KEY is not set. Copy .env.example to .env and add your key.")
return OpenAI()


class Printer:
"""Turns the raw event stream into a readable transcript.

The harness emits ~30 event types. On screen we only care about:
commands the agent ran, text it produced, tool calls, subagents, and the turn ending.
"""

def __init__(self) -> None:
self.t0 = time.time()
self.session_id: str | None = None
self.final_text = ""

def _stamp(self) -> str:
return f"[{time.time() - self.t0:5.1f}s]"

def handle(self, ev) -> bool:
"""Print one event. Returns True when the root turn is finished."""
t = ev.type

if t == "agent.session.created":
self.session_id = ev.session.id
print(self._stamp(), "session", self.session_id, "env:", ev.session.environment.type)

elif t.startswith("agent.session.environment."):
print(self._stamp(), t.rsplit(".", 1)[-1].upper(), "environment")

elif t == "agent.session.turn.item.added" and ev.item.type == "command_execution":
print(self._stamp(), "$", ev.item.command)

elif t == "agent.session.turn.item.added" and ev.item.type == "function_call":
print(self._stamp(), "tool call:", ev.item.name, ev.item.arguments)

elif t == "agent.session.turn.item.done" and ev.item.type == "message":
text = "".join(c.text for c in ev.item.content if c.type == "output_text")
label = ev.item.phase or "message"
print(self._stamp(), f"{label}:", text.strip())
if ev.item.phase == "final_answer":
self.final_text = text

elif t == "agent.session.subagent.created":
print(self._stamp(), "subagent created:", ev.subagent.name or ev.subagent.id)

elif t == "agent.session.subagent.closed":
print(self._stamp(), "subagent closed:", ev.subagent.name or ev.subagent.id)

elif t == "agent.session.turn.completed" and ev.turn.subagent_id is None:
u = ev.turn.usage
if u:
print(self._stamp(), f"turn done. tokens in={u.input_tokens} out={u.output_tokens}")
else:
print(self._stamp(), "turn done.")
return True

elif t in ("agent.session.turn.failed", "agent.session.failed", "error"):
print(self._stamp(), "FAILED:", ev.to_json(indent=None)[:500])
return True

return False
9 changes: 9 additions & 0 deletions openai_agents_058/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
name = "openai-agents-058"
version = "0.1.0"
description = "Video 058: OpenAI Agents API demos (hosted sandbox, function tools, subagents)"
requires-python = ">=3.11"
dependencies = [
"openai>=3.14.0",
"python-dotenv>=1.0",
]
Loading