Skip to content

test(harness): a Deep Agents reference agent for the hosted path (TH-8810) - #306

Open
khushalsonawat wants to merge 2 commits into
feat/TH-8804-source-subfolderfrom
feat/th-8810-deep-agents-reference
Open

khushalsonawat wants to merge 2 commits into
feat/TH-8804-source-subfolderfrom
feat/th-8810-deep-agents-reference

Conversation

@khushalsonawat

@khushalsonawat khushalsonawat commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

Summary

TH-8810 is part of T43: qualify the hosted harness across frameworks. This is Deep Agents' offline half, on the same desk as #301 (OpenAI Agents SDK), #304 (Pydantic AI) and #305 (Google ADK). A local end-to-end run with a real reading and a real model is summarised under Verification.

  • examples/harness/deep_agents_support/ is written the way LangGraph apps ship: create_deep_agent(...) builds the graph once at import, and langgraph.json names it. The kit already runs that natively through its LangGraph adapter, which gives the graph the whole conversation each turn and returns the last message's text.
    • Model: ChatOpenAI pointed at the model gateway (OPENAI_BASE_URL, OPENAI_API_KEY, vertex_ai/gemini-3.7-flash).
    • Request style: use_responses_api=False, because ChatOpenAI can switch to the Responses API by itself, which the gateway serves only for OpenAI-format routes.
    • Key: a missing key falls back to a placeholder, because ChatOpenAI builds its client eagerly and would otherwise fail the import.
    • Tools: the same three as the other references, as plain functions.
  • tests/harness/test_deep_agents_reference.py, with stand-ins for the libraries:
    • Detection: it's detected as deepagents with no blocker, and a deepagents.py shadow flips it.
    • Build: the build runs it through the LangGraph adapter, with the tool tracer bound to exactly its three tools.
    • Tracer: the real tracer bootstrap records each tool as langchain-core calls it (plain tools on a worker thread).
    • Model wiring: pinned, including the placeholder key and the chat-completions setting.
    • Files: the folder's file list is pinned.

No kit code changes.

Verification

  • ruff check on the new files
  • Focused pytest, run locally on Python 3.12.3: the new file, framework detection, the tracer bootstrap, and the LangGraph tests in test_bundle_author_v2.py. 321 passed.
  • Full uv run pytest -q: not run, because no shared runtime is touched.
  • CI: sdk-smoke runs a fixed list of test files, which didn't include this one. It now does (tests/harness/test_deep_agents_reference.py).
  • Fix-only checks: the fixes made after the review cleared (the stand-in ChatOpenAI made keyword-only like the real class, and a no-blockers assert) had a fix-only check: CLEAR, nothing above Low, and its plant (a positional ChatOpenAI(MODEL)) was caught. The audit fixes (this CI-list line, and this description) had a fix-only check as well: CLEAR, nothing above Low.
  • Local end-to-end run (12 Nov, a real reading, then the agent run from the reading's contract): the reading chose langgraph dev, which the agent doesn't install, so the start failed before any model call. An earlier local run through the kit's own LangGraph adapter (this folder's langgraph.json) recorded the tool calls.
  • Eleven planted bugs, each caught by its named test:
    • the chat-completions setting, base_url or the key placeholder dropped;
    • an async tool;
    • a renamed tool;
    • tools not passed;
    • a second graph;
    • a wrong graph path;
    • langchain-openai dropped;
    • a deepagents.py shadow;
    • a positional ChatOpenAI(MODEL), which the real class rejects.

Known limits

  • No CI job installs deepagents or langchain-openai. sdk-smoke.yml installs only .[livekit], and the kit's langchain extra has neither. So these tests run against stand-ins only; the pinned sources were read in review.
  • A reading picks langgraph dev for this agent, which isn't installed, instead of the kit's langgraph.json route. Outside this PR; a register row.
  • The kit's LangGraph adapter starts a new event loop for each turn, while a LangGraph app's model client is built once at import. Pooled connections tied to the first turn's closed loop may cost latency or a 500 on later turns. It isn't proven: the real-model run stopped before a model call, and an earlier local step's three stub turns through the adapter gave no 500. It affects every LangGraph agent, not only Deep Agents (a register row).
  • No test runs a turn through the adapter on this example. The adapter reads the last message's content only when it's a string.
  • Deep Agents' built-in tools (to-do list, scratch files, sub-agents) are visible to the model but untraced and ungraded.
  • The agent's orders are its own state, not the run's world, as in test(harness): an OpenAI Agents SDK reference agent for the hosted path (TH-8800) #301 (a register row).

Checklist

  • Public API names stay under agent_learning.*, agent-learn, or @future-agi/agent-learning-kit
  • New user-facing workflow has docs or an example
  • New release claim has executable gate coverage (no release claim)
  • No hosted-service dependency was added to release-gated examples
  • Security-sensitive output is redacted or justified (no credentials; the key and URL come from the environment)

…8810)

A small support agent on LangChain Deep Agents 0.7.32, the same desk as the
other T43 references, written the way LangGraph apps ship: the graph built
once at import and named in langgraph.json. Its model is ChatOpenAI pointed
at the model gateway, held on chat completions (use_responses_api=False,
since ChatOpenAI can switch to the Responses API by itself), with a
placeholder key so a missing key fails a turn rather than the import. Its
three tools are plain functions.

Offline tests pin what the hosted path does with it, with stand-ins for the
libraries: it is detected as deepagents; the build runs it through the kit's
LangGraph adapter with the tool tracer bound to its three tools; the real
tracer bootstrap records each tool as langchain-core calls it (a worker
thread for plain tools); and the model wiring.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>

@cdileep23 cdileep23 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the comments.

# the turn is what fails without a real key. `use_responses_api=False` because ChatOpenAI
# switches to the Responses API by itself for some models and arguments, and the gateway
# serves that only for OpenAI-format routes.
graph = create_deep_agent(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 (cause is in the adapter, not this diff). The second turn of every conversation fails. The kit's LangGraph adapter calls asyncio.run per request (langgraph_http_adapter.py:70), and the ChatOpenAI built here at import keeps its pooled connection on the first turn's loop, which is closed by turn 2.

Failure: turn 1 returns 200, turn 2 returns 500 {"error": "RuntimeError: Event loop is closed"}. Seen 3 of 3 times with deepagents 0.7.32 and langchain-openai 1.7.3 through the kit adapter against a local stand-in gateway. The description says this may cost a 500 and isn't proven offline; it is proven, and it happens every time.

Fix: give the adapter one long-lived loop (a background thread on run_forever, requests sent with run_coroutine_threadsafe). With that, both turns return 200 and both tool calls are traced. Or build the model per call as #301, #304 and #311 do. Either way, correct the known-limit text.

graph = create_deep_agent(
model=ChatOpenAI(
model=MODEL,
base_url=os.getenv("OPENAI_BASE_URL"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2. If the job maps OPENAI_API_KEY but not OPENAI_BASE_URL, ChatOpenAI falls back to https://api.openai.com/v1/ and sends the gateway key there as a bearer token on every turn, with model vertex_ai/gemini-3.7-flash. The README documents that fallback. #311 refuses to start in the same case, because CrewAI won't take custom_openai without a URL.

Fix: refuse at import when OPENAI_BASE_URL is unset, with a test.

assert agent.MODEL == DEFAULT_MODEL
# Built at import, the way a graph named in langgraph.json is loaded: reaching the
# graph again builds nothing.
assert agent.graph is agent.graph

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3. agent.graph is agent.graph is always true. The next line (len(calls) == 1) already pins "built once", so this one can go.

}


def _copy_example(destination: Path) -> Path:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3. Same copied setup as the other reference-agent PRs (_copy_example, the stand-in writer, the fresh-import fixture, the detection tests, the build test skeleton, the tracer driver, the pin tests). See the shared-helper comment on #301: this file would import from that one module instead.

This branch has not been deployed

No deployments
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.

2 participants