Skip to content

OpenAI @function_tool tools fail with SpawnSafetyError, on Linux as well as macOS #448

Description

@nickorkes

Summary

Install conductor-python[openai-agents] and run the sample from Conductor's OpenAI Agents SDK quickstart, the one shown in the UI under Agents → Create Agent → Python → OpenAI. It works. Add a tool to that agent and it stops working.

An OpenAI Agents SDK agent with a @function_tool fails at registration with SpawnSafetyError. The error tells you to define the callable at module level. It is already at module level.

This is a 2.x regression. The same script runs on conductor-agent-sdk 0.4.4, which has no spawn probe.

The quickstart does not catch this because its sample agent has no tools. An agent with no tools registers no worker, so the guided path never reaches the code that breaks.

Environment

conductor-python 2.0.0rc2 · openai-agents 0.18.2 · server 3.32.0-rc.15

Reproduced on macOS arm64 (Python 3.13.12) and on python:3.13-slim, Linux aarch64 (Python 3.13.14).

Reproduction

# get_inventory is defined at module level
from agents import Agent, function_tool
from conductor.ai import Runner

@function_tool
def get_inventory(sku: str) -> str:
    """Return the inventory count for a SKU."""
    return f"SKU {sku}: 42 units"

agent = Agent(name="inventory_helper", model="gpt-4o-mini", tools=[get_inventory],
              instructions="Call a tool at most once, then reply briefly.")

print(Runner.run_sync(agent, "How many units of SKU ABC-123?").final_output)

Actual behavior

_worker_entries.py:787 in probe_spawn_safety
SpawnSafetyError: worker 'get_inventory' is not spawn-safe (PicklingError("Can't pickle
<function get_inventory at 0x100e18400>: it's not the same object as __main__.get_inventory")).
Define the callable at module level (importable by qualified name).

This is not macOS-only

The SDK selects the start method itself. task_handler.py:42 calls set_start_method("spawn") at module import, with no platform check. A Linux host whose own default is fork still ends up on spawn:

1. OS default:                              None (unset)
2. after importing openai-agents:           None
3. after "from conductor.ai import Runner": spawn

So the probe is active everywhere, and a Linux server running a tool-using agent hits this on its first run.

Cause

@function_tool rebinds the module global to a FunctionTool object and keeps the original function inside it. LangChain's @tool does the same thing with StructuredTool.

The framework serializer pulls the original function out and hands it to ToolWorkerEntry, which transports it by reference as a module path plus a qualified name. The child process imports that path and looks up get_inventory. It finds the FunctionTool, not the function. Pickle sees the mismatch and refuses.

FunctionRef.of() already handles this for LangChain by hopping through a container attribute:

# _worker_entries.py
_CONTAINER_ATTRS = ("func", "coroutine")

OpenAI's FunctionTool has neither attribute. The only route back to the original function is on_invoke_tool._invoke_tool_impl.__closure__[2], which is private and positional, so an attribute hop is not a real option here.

The fn_direct fallback does not help either. It holds a plain function, and plain functions pickle by reference no matter how they are stored.

Not the same as #443

Issue #443 covers tools defined inside main(). Those are local closures and they fail with AttributeError: Can't get local object. This one is a module-level function failing with PicklingError: not the same object. Moving example tools to module level, which is the fix in #443, does not change this case.

Workaround

import multiprocessing
multiprocessing.set_start_method("fork", force=True)  # before importing conductor

This has to run before the import. task_handler.py:42 sets spawn when it loads, and only a context that is already set survives that call.

CONDUCTOR_MP_START_METHOD covered this in 0.4.x. It is no longer read anywhere in 2.x.

Suggested direction

Transport the callable by value when it cannot be referenced by name. cloudpickle >= 2.0 is already a declared dependency in pyproject.toml and is imported nowhere under src/. Cloudpickling fn_direct in ToolWorkerEntry.__getstate__ and __setstate__ would cover OpenAI, and it would also make the local functions in #443 work rather than fail earlier.

Two smaller changes are worth making regardless. Correct the error message when the global name resolves to a container holding the callable out of reach. Add a tool to the quickstart sample so the guided path registers a worker. That file lives in conductor-oss/conductor, not this repo.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions