Build one AsyncOpenAI and one Compactor per engine run#50
Conversation
|
|
||
|
|
||
| def build_compactor_factory( | ||
| def build_compactor( |
There was a problem hiding this comment.
Id avoid builder methods for simple functions, instead expose a pure function like async def compact(client: AsyncOpenAI, engine_config: EngineConfig, item: AgentContextItem) -> str so that we dont need to be nesting functions and passing them around. Or if too much context to drill down we can create a compactor class which is initialized in the same place we call build_compactor and then passed around.
Per francescov1's PR #50 review: drop the build_compactor builder and the Compactor type alias in favor of a pure async def compact(*, client, compaction_model, item) -> str. AgentContext.compact_old_items now takes the AsyncOpenAI directly and calls compact() with self.compaction_model, removing the closure and the compactor field on EngineRunState. Tests that monkeypatched build_compactor now monkeypatch engine.agents.agent_context.compact instead; example_compaction.py switches from a recording-callable stub to a duck-typed _FakeAsyncOpenAI plus observable post-state assertions.
- Engine builds one AsyncOpenAI and installs the SDK default with use_for_tracing=False at run start; closed in stream_engine_async's outer finally so connection-pool lifecycle is deterministic. - Compaction is a pure async def compact(*, client, compaction_model, item) -> str. build_compactor / CompactorFactory / Compactor type alias are gone; AgentContext.compact_old_items takes the client directly and calls compact() with self.compaction_model. - OpenAiAgentRunner takes the client (not a compactor); the runner_protocol seam is deleted from production code (tests monkeypatch via the SDK seam). - SynthesisTool requires its injected client; lazy construction is gone. - Tests: shared probe_kit helpers updated; integration tests cover client lifecycle (normal exit + early consumer break); compaction tests monkeypatch agent_context.compact instead of build_compactor; example probes switched to duck-typed fakes and post-state assertions. Closes INF-2933 items 8 and 9.
cfeada8 to
a23590f
Compare
Match the real compact() signature on the keyword-only params (client, compaction_model, item) so a signature change breaks visibly instead of being silently absorbed by **_kwargs. Mirrors the pattern already used by fake_compact in test_engine_compaction.py.
Match the signature shape of the sibling _capturing_run_streamed at the top of the file: keyword-only starting_agent / input / context / max_turns / run_config with types and a concrete return. Replaces the `*args, **kwargs` and `**_kwargs` variants the previous monkeypatch seam swap left in place, so a future SDK signature change breaks visibly rather than getting absorbed by the catchall.
Closes bugbot 3269820113 (FakeRunner) and tightens the other PR-introduced stubs to the same shape: - FakeRunner.run_streamed: declare max_turns and run_config explicitly (probes already assert on calls[i]["max_turns"] / "run_config"). - _FakeCompletions.create: declare model/messages/temperature so a signature change in compact() breaks visibly here, not silently. - test_engine_client_lifecycle: extract _install_stub_client helper with typed AsyncOpenAI / set_default_openai_client stand-ins, drops the lambda **kw / lambda *a, **kw pair from both lifecycle tests. - test_main: _capture_client and the set_default recorder now declare the kwargs they actually receive (base_url/api_key/default_headers, use_for_tracing).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 44c492b. Configure here.
| async def fake_compact(*, client, compaction_model, item: AgentContextItem) -> str: | ||
| del client, compaction_model | ||
| compacted_items.append((item.item_id, item.role)) | ||
| return f"summary for {item.item_id}" |
There was a problem hiding this comment.
Test stub missing type annotations on parameters
Low Severity
The fake_compact stub in test_engine_compaction.py declares client and compaction_model without type annotations, while every other stub for the same compact function in this PR (in test_agent_context.py, test_engine_client_lifecycle.py, test_main.py) explicitly types them as client: AsyncOpenAI and compaction_model: ModelConfig. This inconsistency means a signature change in the real compact function won't be caught by static analysis at this call site.
Triggered by learned rule: Use explicit typed parameters in test stubs, not **kwargs
Reviewed by Cursor Bugbot for commit 44c492b. Configure here.
…bal default PR #50 ("Isolate the OpenAI Agents SDK") replaced an env-var-driven configure_default_sdk_client(provider) with an unconditional set_default_openai_client(client, use_for_tracing=False) inside stream_engine_async. That works in production (where every Runner.run_streamed goes through that entrypoint) but breaks tests that invoke the call_subagent tool directly via tests/integration/tool_isolation_kit.wired_tools — those never enter stream_engine_async, so the SDK default stays None and OpenAIProvider lazy-constructs its own AsyncOpenAI from OPENAI_BASE_URL / OPENAI_API_KEY env vars, dropping default_headers and any deterministic close path. test_call_subagent_through_sdk_adapter_live started failing on 2026-05-22 with `APIConnectionError: Connection error.` raised mid-stream against a request that never left the runner. Fix: pass model_provider=OpenAIProvider(openai_client=client) on every RunConfig the engine constructs. This makes prod and tests symmetric — both paths now wire HALO's configured AsyncOpenAI to the SDK explicitly, no global state, no env-var fallback. The engine still owns the client's lifecycle (constructed in stream_engine_async, closed in its finally; the wired_tools test path owns its own instance). Bonus: removes a layer of SDK global state, which was already a footgun for callers running multiple engines / multiple test modules in one process. Closes the regression that motivated the (now-closed) workaround PR #54. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>


Summary
engine_config.model_providerin exactly one place (engine.main.stream_engine_async), builds theAsyncOpenAI+Compactoronce per run, installs the SDK default withuse_for_tracing=False, and stashes both onEngineRunStateso the subagent path reuses them.OpenAiAgentRunnernow takes aCompactordirectly — thecompactor_factoryindirection and its unusedAgentExecutionarg are gone.AsyncOpenAIis closed instream_engine_async's outerfinally, wrapped intry/exceptso close-time errors can't mask in-flight exceptions.configure_default_sdk_client,build_compactor_factory, theCompactorFactoryalias, andSynthesisTool's lazy-construct branch.noop_compactorexported fromtests.probes.probe_kit(collapses 7 duplicate local stubs);build_compactormonkeypatch replaces the deeperbuild_compactor_factorypatch; the subagent test no longer needs its own monkeypatch sinceEngineRunStatecarries the compactor.Closes INF-2933 items 8 and 9.
Test plan
uv run pytest -q— 276 passed (baseline 278 − 3 deletedtest_configure_default_sdk_client_*+ 1 new SDK-default test)uv run pytest tests/integration -q— 54 passed (+ 2 new lifecycle tests intest_engine_client_lifecycle.pycovering normal exit and early consumer break). One pre-existing live-API flake unrelated to this change.configure_default_sdk_client,build_compactor_factory,compactor_factory,CompactorFactoryall zero hits acrossengine/andtests/.basedpyrightclean on the changed engine files.Behaviour changes (intentional)
base_url,api_key, anddefault_headerswere allNone. Now always installsAsyncOpenAI(...)(equivalent to env-drivenAsyncOpenAI()in that case) withuse_for_tracing=False.OPENAI_API_KEYis missing andmodel_provider.api_keyisNone,AsyncOpenAI()raises at engine startup instead of lazily on first compactor/synthesis call.await client.close()instream_engine_async's outerfinally— no behaviour change visible from the public API, just cleaner httpx lifecycle.Note
Medium Risk
Medium risk because it changes engine startup/client lifecycle (eager
AsyncOpenAIconstruction, globalset_default_openai_client, and explicitclose()teardown) and rewires compaction/synthesis call paths used by both root and subagents.Overview
The engine now creates a single per-run
AsyncOpenAIclient instream_engine_async, installs it as the Agents SDK default withuse_for_tracing=False, stores it onEngineRunState, and closes it on run teardown (including early consumer break) with close failures logged but not raised.Compaction is refactored from a pluggable callable/factory into a direct call to
engine.agents.compactor.compact(client, compaction_model, item):AgentContext.compact_old_itemsnow takes the run client,OpenAiAgentRunnertakes the client directly, and the oldbuild_compactor_factory/CompactorFactoryindirection is removed.SynthesisToolsimilarly stops lazily constructing its own client and instead requires the injected run client (wired fromrun_state.openai_client).The previous test seam via
RunnerProtocoland a per-runrunnerarg is removed; tests/probes now patchagents.Runner.run_streamed(via a newinstall_fake_runnerhelper), add coverage for SDK default installation + client close lifecycle, and set a placeholderOPENAI_API_KEYfor non-live test runs to satisfy eager client construction.Reviewed by Cursor Bugbot for commit 44c492b. Bugbot is set up for automated code reviews on this repo. Configure here.