Skip to content

[Deep Agent 15/15] Serve the deep agent behind chat_with_agent: engine switch, durable threads, streaming, HITL (#43) - #64

Open
stavp-star wants to merge 1 commit into
deep-agent/44-evalsfrom
deep-agent/43-api
Open

stavp-star wants to merge 1 commit into
deep-agent/44-evalsfrom
deep-agent/43-api

Conversation

@stavp-star

Copy link
Copy Markdown
Collaborator

Closes #43 · Part of epic #49 · Stack position 15/15. Base: deep-agent/44-evals (#63), auto-retargets on merge.

Where the integration lives

The issue said "route /chat", but agent/main.py doesn't mount routers/chat.py: the agent service app is the MCP server. The real path is UI → backend POST /api/agent/chat → MCP tool chat_with_agent, and that's where the engine switch is.

What

Engine selection (agent/mcp_server.py)

  • New engine argument (legacy | deep), defaulting to AGENT_ENGINE (default legacy, so nothing changes until it's chosen).
  • Deep threads get deep- IDs, and resumes are routed by thread ID: a deep thread always returns to the deep agent and a legacy thread to the legacy graph, even if the caller doesn't resend engine.
  • Backend (backend/app/routers/agent.py): optional engine in the request, forwarded only when set (older agent deployments keep working). Responses gain optional engine, verification, and confidence.

deep_agent/service.py

  • One agent per process, built lazily under a lock. Per-request data is DeepAgentContext (connection, user) plus the thread ID.
  • Durable threads: AsyncPostgresSaver over a pooled connection when DEEP_AGENT_CHECKPOINT_DB_URL is set (setup() is idempotent); otherwise an in-memory saver, with a warning.
  • Streaming: a callback handler publishes the same Redis events the legacy graph does (agent_stream:{thread_id}, {"thread_id", "node", "status"}), for deep:<tool> active/completed/error and a final deep:answer. The backend SSE relay works unchanged. The response's execution_path lists the tool steps.
  • Response: the legacy JSON contract (status, summary, sql_query, sql_explanation, trace_id, execution_path, is_unanswerable, raw_data_ref). summary is rendered from the verified Trino rows ([Deep Agent 9/15] Structured final answer bound to the actually-executed SQL (#41) #58).
  • HITL / clarification: needs_clarification → status: "interrupted", interrupt_details: {"type": "hitl_escalation", "subtype": "clarification", "reason": <question>}. That's the free-text prompt the UI already renders. The reply (resume_value string or {"feedback": ...}) becomes the next user message on the same thread. With hitl_enabled=false, the question is returned as a completed answer instead.
  • Run timeout ([Deep Agent 10/15] Bound the agent loop and cut latency: limits, tool surface, connection reuse, caching (#42) #59), tracing ([Deep Agent 8/15] End-to-end Langfuse tracing: one trace per run, span metadata, run scores (#37) #57), and run scores are applied as in the CLI.

Bugs found and fixed while building this

Acceptance criteria

  • [~] The UI chat goes end-to-end through the deep agent when selected, and the legacy graph is unchanged otherwise. Verified at the MCP tool and backend levels (tests). A live UI run is pending infra.
  • Conversations survive a restart: verified against a real Postgres 16, using a throwaway container that was removed afterwards. One agent wrote the thread (13 checkpoint rows); a fresh agent with a new pool read it back. The test is opt-in via DEEP_AGENT_TEST_PG_URL.
  • An ambiguous question triggers a clarification interrupt, and the run resumes after the answer (test: clarify → resume → matched answer)
  • run_deep_agent.py has no sys.path hacks and runs via uv run ([Deep Agent 1/15] Hygiene: CLI entry point, remove committed logs & scratch files (#46) #50)

Known gaps / follow-ups

  • UI node names: stream events use new node names (deep:*). If the execution-graph visualisation only knows legacy node names, it may need a small mapping. Not verified without the UI running.
  • The deep path ignores allowed_tables, allowed_statuses, extractors, active_skills, and execution_mode for now. Mapping allowed_tables onto the catalog / tools is the natural next step.
  • The legacy SQL query-approval interrupt (query_approval) has no deep-agent equivalent, because the deep agent executes exploratory SQL itself. If approval-before-final-execution is required, add an interrupt_on gate on the final answer.
  • Playwright e2e for a deep-agent chat flow: pending live services.

Rollout

AGENT_ENGINE=legacy with per-request engine: "deep" for internal users → deep-agent-eval --gate passes on the expert gold set (#63) → AGENT_ENGINE=deep → remove the legacy graph.

Testing

  • agent/tests/deep_agent/test_service.py: 16 tests (contract mapping, clarify→resume, stream events, stale-answer regression, engine routing incl. resume affinity, hitl off, Postgres durability)
  • backend/tests/test_agent_engine.py: 3 tests (engine forwarding, omission, validation). Note: the backend suite creates/drops a text2sql_test database on the configured Postgres, by design.
  • Agent suite: 434 passed, 3 skipped · backend suite: 14 passed (run with ENABLE_KEYCLOAK=false)

🤖 Generated with Claude Code

…rable threads

- deep_agent/service.py: process-wide agent, AsyncPostgresSaver when
  DEEP_AGENT_CHECKPOINT_DB_URL is set (else in-memory), legacy Redis
  stream events (deep:<tool>, deep:answer), legacy JSON response contract
  + engine/verification/confidence, clarification as free-text HITL
  interrupt resumed with the user's answer on the same thread
- chat_with_agent: engine argument / AGENT_ENGINE default; deep-* thread
  ids are routed back to the deep agent on resume; non-interactive calls
  get the clarifying question as the answer
- answer middleware resets the structured answer each turn (a turn
  without a new answer no longer re-reports the previous one)
- backend: optional engine passthrough and engine/verification/confidence
  response fields
- create_deep_agent(checkpointer=...)

Closes #43

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 8245afb8-5c74-4214-8cb7-86f66e2100ae

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant