Add OpenAIAgentSessionOperator for OpenAI Managed Agents - #73447
Conversation
Run one turn in a fresh OpenAI Managed Agents session from Airflow, with deferrable polling of the first turn, cancellation of the active turn on timeout, failure or kill, and session, turn and usage IDs pushed to XCom. The Agents API is feature-detected on the installed SDK so the provider floor stays compatible with libraries pinning openai<3.
482ef3c to
445b9cb
Compare
Kunal8954
left a comment
There was a problem hiding this comment.
Nice work overall — the feature-detection of beta.agents (so the floor stays compatible with openai<3), the session ownership/XCom handling, and the trigger/hook split all look clean. I checked the whole diff including tests and docs.
One thing I verified explicitly since it's easy to get wrong: the defer() path is safe. TaskDeferred subclasses BaseException (not Exception) on every supported Airflow version (>=2.11), so the except Exception: raise in execute() doesn't intercept the deferral and won't call on_kill() / cancel the fresh session at defer time. Good.
A few minor points:
-
test_agent.py— unguardedimport httpx2.httpx2is the vendored HTTP client that only ships inopenai3.x. Because it's imported at module top level (not behindpytest.importorskip), in an env that pinsopenai<3(which this PR explicitly claims to keep supported) the entire test module fails at import withModuleNotFoundError, instead of skipping like theimportorskiplines below. Consider guarding it the same way. -
hooks/openai.pypoll_agent_session—session.required_actionsmay beNone.any(action.type == "function_call" for action in session.required_actions)raisesTypeErrorif a response ever omits the field.for action in (session.required_actions or [])is a cheap hardening. -
operators/agent.pyexecute—session_kwargs["agent"]assumed to be a dict.self.session_kwargs.get("agent", {}).get("model")raisesAttributeError(instead of the intendedValueError) if a user passes an actualAgentpydantic object — which the SDK accepts — orNone.getattr(...)/isinstancehandling would give a clearer error. -
hooks/openai.py—turn.usage.model_dump(mode="json")assumes a pydantic model; if an endpoint ever returns usage as a plain dict this breaks. Minor robustness nit. -
Nit:
f"Agent turn {turn.id} {turn.status}: {turn.error}"printsNonewhenturn.errorhas no message.
Everything else (reserved-key enforcement, max-consecutive-failure retry, cancel-on-timeout/kill, end_time persisted across triggerer restarts, docs exampleinclude path) checks out.
Format the SDK error code and message when a Managed Agents turn fails, omit the suffix when a cancelled turn carries no error, and reject a non-dict session_kwargs['agent'] with a ValueError before creating a session.
|
Thanks for the careful read. I pushed 4785899 for the error message and the agent validation. A failed or cancelled turn now reports the SDK error's code and message. When a cancelled turn carries no error the message is just the turn and its status, so the The I left |
OpenAIAgentSessionOperator for OpenAI Managed Agents
Kunal8954
left a comment
There was a problem hiding this comment.
Approved — the fixes look good and I verified the remaining items:
- Deferral is safe: TaskDeferred subclasses BaseException (not Exception) on all supported Airflow versions (>=2.11), so the except-Exception on_kill() path in execute() cannot cancel the fresh session at defer time.
- template_fields_renderers={'environment': 'json', ...} only affects RTIF/UI display (airflow-core renderedtifields.py) and does not mutate session_kwargs/environment into strings at execution, so the reserved-keys and .get('model') checks stay correct after templating.
- httpx2 is behind pytest.importorskip so the module skips cleanly on openai<3.
- The required_actions / usage guards you declined are reasonable given the SDK models guarantee those shapes.
No blockers.
Summary
OpenAI shipped a Managed Agents API in
openaiSDK 3.13 (client.beta.agents, headerOpenAI-Beta: agents=v1): persisted agents, sessions with turns, hosted or self-hosted environments, vaults for MCP credentials, and subagents. It is the counterpart of the Anthropic Managed Agents surface this provider's sibling already integrates.This adds
OpenAIAgentSessionOperator, which creates a fresh session with an initial message, waits for the first turn to finish, and returns the session ID.deferrable=Truehands the wait toOpenAIAgentSessionTrigger. When XCom pushing is on it also recordssession_id,turn_idand the turn's tokenusage. FiveOpenAIHookhelpers back it (create agent, create session, get session, cancel session, poll session).Usage from the example DAG:
Pass
agent_idinstead of an inlineagentto run a saved agent, andvault_idsor an environment template reference throughsession_kwargsorenvironment.Design rationale
The SDK floor stays at
openai>=2.37.0; the Agents API is feature-detected. Raising the floor to 3.13 fails the provider dependency resolution becausellama-index-llms-openai(pulled in through the Common AI provider's LlamaIndex extra) still pinsopenai<3. The hook checks forclient.beta.agentsat first use and raises an error naming the required version, so every other OpenAI operator keeps working on older SDKs and the new test module skips itself when the installed SDK predates the API.Each task attempt owns one fresh session and waits only for its first turn. A retry creates a new session. Reusing the previous one would mean picking the right turn out of a session with history the operator did not write. The poll lists turns in ascending order with
limit=1and reads that turn's status. An idle session with no turn yet counts as still waiting, because the initial input can sit queued before a turn object exists, and treating idle as done would return success for work that has not started.Client-side function tools fail the task. The worker is deferred or asleep while the agent runs, and a
function_callrequired action would need Airflow to run arbitrary tool code mid-wait and post results back. The error message points at service-side tools, MCP servers or hosted environments. Self-hosted environments are supported but their worker (codex exec-server) has to be run independently; anenvironment_connectionrequired action is treated as still waiting.Failure paths cancel the active turn but keep the session. Timeout, polling failure, a bad trigger event, a failed turn and a killed task all send
agent.session.input.cancel, so a runaway turn stops consuming quota while the items and artifacts remain retrievable by session ID. Up to three consecutive polling errors are retried before the session is cancelled, the same tolerance the Anthropic operators use. A shorterexecution_timeoutcaps the deferral timeout.A mock-transport test against the real SDK covers the wire format, including the beta header, request payloads, turn listing parameters and the cancel event. The failure path was also exercised against the live API: a turn that failed server-side was cancelled and surfaced as the task error.
Live run
The example DAG run through a local scheduler and triggerer against the live Managed Agents API with
deferrable=True. The task deferred, the trigger polled the session and its first turn, and the task resumed and succeeded. Session ID, turn ID and token usage landed in XCom.Gotchas
openai>=3.13.0on workers and triggerers. Libraries pinningopenai<3, including current LlamaIndex OpenAI integrations, cannot share that environment.on_killcleanup, which needs Airflow 3.3 or newer. On older versions a killed deferred task leaves the turn running until it finishes or times out server-side.try_number; it is not cumulative across retries.{pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.