Skip to content

Run agent.on(...) lifecycle hooks in TemporalOpenAIRunner - #236

Closed
campoy wants to merge 1 commit into
mainfrom
fix/openai-agents-typescript-agent-hooks-never-fire
Closed

campoy wants to merge 1 commit into
mainfrom
fix/openai-agents-typescript-agent-hooks-never-fire

Conversation

@campoy

@campoy campoy commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

What was changed

agent.on(...) lifecycle hooks now run when an agent goes through TemporalOpenAIRunner.run, as they do with the upstream Runner.

  • convertAgent runs a clone of each agent (to swap in the Activity-backed model), and the clone has its own event emitter. With the new internal forwardHooks flag, each clone re-emits agent_start, agent_end, agent_handoff, agent_tool_start and agent_tool_end on the original agent. Agent arguments are mapped back to the caller's agents, so agent_start gets the caller's agent and agent_handoff the caller's handoff target. Handoff targets are converted the same way, so their listeners run too.
  • TemporalOpenAIRunner.run sets that flag from patched('openai-agents.forward-agent-hooks').
  • Listeners are added to the clones, which are built for each run, so nothing accumulates on the caller's agent.
  • A row in the README's feature table: listeners run in the Workflow and must be deterministic.

API change: none. convertAgent is not exported, and no option or signature of the public API changes.

Replay and rollback: a listener that writes the run context runs in Workflow code and can change the commands that follow, so the forwarding is behind a patched() gate. Runs recorded before this change keep skipping the hooks on replay. The gate can't be narrowed to agents that have listeners: in the Workflow bundle the agent's emitter is the framework's browser emitter, which keeps its listeners in private fields and whose emit always returns true. So new runs record the marker on their first run() call, and an earlier plugin version can't replay them ([TMPRL1100] on the marker). To roll back, keep this version and decline the patch with the Worker's patchActivationCallback (experimental in SDK 1.24), or use Worker Versioning.

Why?

See #235. agent.clone() gives the clone a new event emitter, and the run loop emits lifecycle events on the running agent, the clone, so listeners on the caller's agent never heard them. Nothing failed or warned. A listener that writes the run context, which the upstream context guide allows, silently never ran, so later model requests and the next turn didn't see the value, and the result could differ from the same code outside Temporal.

Checklist

  1. Closes [Bug] openai-agents (TypeScript): agent.on(...) lifecycle hooks never run through TemporalOpenAIRunner #235

  2. How was this tested:

    • New unit tests in src/__tests__/convert-agent.test.ts: with forwardHooks, events emitted on the clones reach listeners on the caller's agents, with the caller's agent in agent_start and the caller's handoff target in agent_handoff; without it, they don't.
    • New integration test agent.on listeners run and their context writes reach later turns in src/__tests__/test-openai-agents.ts. Its Workflow has an activityAsTool tool and three listeners; the tool-end listener writes the tool result into the run context, the instructions show the context, and a second turn runs only when the listener wrote it.
    • New test agent.on listeners stay off when replaying a run recorded before they were forwarded. It replays a History of that Workflow recorded on main (src/__tests__/histories/agent-hooks-before-forwarding.json), where no listener ran and the run ended after one turn.
    • On main with only the new integration tests copied in, the listener test fails: result.events is [] instead of the five events. The replay test passes there too.
    • With the forwarding call removed and the gate kept, the forwarding unit test and the listener test fail the same way.
    • Without the gate (forwarding always on), the replay test fails with [TMPRL1100] Nondeterminism error: Activity machine does not handle this event: HistoryEvent(id: 23, WorkflowExecutionCompleted): the listener writes the context, so the Workflow starts a second turn where the old run ended.
    • Two existing tests in src/__tests__/agent-tools.test.ts fake the Workflow activator to call run(); the fake now has patchInternal.
    • Locally, in Docker (Node 24.22.0, pnpm 10.27.0, Linux arm64): make sync, make lint (8 existing warnings, none in files this PR touches), make test (211 tests), make build, scripts/ci/check_npm_package.py, node ../_shared/smoke.mjs and scripts/ci/check_conventions.py pass, and Prettier with the SDK settings reports the changed files clean.
    • [Bug] openai-agents (TypeScript): agent.on(...) lifecycle hooks never run through TemporalOpenAIRunner #235's reproduction exits 1 on npm 1.24.0 and on main at c664a34, and 0 on this branch.
    • Replay check, with a separate harness: Workers with no Workflow cache, and the model and tool Activities on their own queues so waiting runs survive Worker changes. Histories from main and from this branch, with and without listeners, finished and still waiting:
      • This branch replays all 13 histories: main's, its own, and the runs carried across versions.
      • main's waiting runs finish on this branch with the old behavior: no listener events, no marker, 0 failed Workflow Tasks.
      • Rollback build (this branch, with patchActivationCallback declining the patch): this branch's waiting runs finish with the listeners still on (the second turn runs), 0 failed Workflow Tasks; a new run takes the old path and records no marker.
      • main replays the 7 histories without the marker and fails the 6 with it with [TMPRL1100] Nondeterminism error: Non-deprecated patch marker encountered for change openai-agents.forward-agent-hooks, as expected for patched().
      • Without the gate, main's finished run with listeners fails with [TMPRL1100]; its other 3 histories replay.
      • A control that renames the fetchValue Activity in a history fails with [TMPRL1100] Nondeterminism error: Activity type of scheduled event 'fetchRenamed' does not match activity type of activity command 'fetchValue'.
      • The replay check ran on the commit before a formatting fix and the activator fake; the plugin code differs only in whitespace.

    CI covers the other Node versions and platforms. Not run against a real model provider.

  3. Any docs updates needed?
    The README row, included here. Release notes should say that agent.on(...) listeners now run in new runs, in Workflow code, and that runs started on this version can't be replayed by an earlier one.

Not covered:

  • Runner-level hooks (runner.on(...)): TemporalOpenAIRunner keeps its inner Runner private, so there is nothing to register them on.

Related: the other open drafts for this plugin (#209, #210, #215, #216, #217, #218) also add tests at the end of src/__tests__/test-openai-agents.ts and src/__tests__/workflows/openai-agents.ts and edit the README, so whichever merges later may need a small rebase there. None of them touches convert-agent.ts or the convertAgent call in runner.ts.

Open questions:

  • Is a marker on every new run acceptable? The gate can't tell whether an agent has listeners, so new runs record it on their first run() call even without listeners, and rolling back to an earlier version needs patchActivationCallback (experimental) or Worker Versioning. An opt-in option would avoid the marker for others, but it would be a public API addition and leave listeners silently skipped by default.
  • Should listeners get the caller's agent (as now, matching the upstream Runner) or the clone the plugin runs?

Draft while these questions are open.

convertAgent runs a clone of each agent, and the clone has its own event
emitter, so listeners on the caller's agent never heard agent_start,
agent_end, agent_handoff, agent_tool_start or agent_tool_end. Each clone
now re-emits them on the original agent, with agent arguments mapped back
to the caller's agents.

A listener that writes the run context runs in Workflow code and can
change the commands that follow, so the forwarding is behind the
openai-agents.forward-agent-hooks patch: runs recorded before it keep
skipping the hooks on replay.
@campoy

campoy commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Replaced by #270. It runs agents as written instead of cloning them, so agent.on(...) listeners fire without the forwarding code here. This PR's Worker test is carried over there and passes.

@campoy campoy closed this Oct 29, 2026
@campoy
campoy deleted the fix/openai-agents-typescript-agent-hooks-never-fire branch October 29, 2026 15:34
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.

[Bug] openai-agents (TypeScript): agent.on(...) lifecycle hooks never run through TemporalOpenAIRunner

1 participant