BullMQ vs LangGraph (HISTORICAL)
HISTORICAL — NOT THE CURRENT RUNTIME. Cinatra’s agent runtime is WayFlow, the reference OAS implementation, running as a Python sidecar invoked over A2A. The BullMQ-side content below is still accurate; the LangGraph-side content describes the prior runtime and is retained for historical reference only.
Canonical reference for the separation of concerns between the BullMQ job layer and the LangGraph agent-state-machine layer, and the migration plan for replacing Cinatra’s hand-rolled HITL state machine with native LangGraph interrupt() / Command(resume=...) primitives.
Phase 110-G complete — TypeScript agent execution layer retired.
agentic-execution.ts,agentic-resume.ts,agentic-tools.ts,tool-interceptor.ts, andresume.tshave been deleted.execution.tsnow dispatches exclusively torunLangGraphJob. Theplanned_actionsandreview_taskstables have been dropped from the schema and the live DB. All HITL routing uses synthetic ID prefixes (setup-{runId}andlg-{runId}). See Migration plan → Phase D below for the completed work.
Phase 110-H complete — architecture finalized. WR-01 concurrent dispatch guard applied (
updateAgentRunStatusConditionalCAS beforerunLangGraphJob). WR-04 REST polling path now returns realinputSchemafields for setup-loop runs. IN-02agent_updateMCP tool no longer accepts"default"asexecutionProvider.AGENTS.mdboundary rule promoted. Migration plan fully closed.
Standard approach
Section titled “Standard approach”Every agent run in Cinatra passes through two different engines. Respect the boundary between them:
- BullMQ owns scheduling, durability, retries, worker pooling, and cancellation signals. It is the trigger layer — it schedules a job, guarantees the job eventually runs, and gives operators a dashboard (
/settings/operations/jobs) to see queue state. - LangGraph owns the agent execution graph — nodes, edges, conditional routing, shared state, and HITL interrupt/resume. It is the agent runtime layer.
The two layers meet at exactly one function: runLangGraphJob in packages/agents/src/langgraph-execution.ts. BullMQ dispatches a run to this worker; the worker creates a LangGraph thread and streams events back. No other code path should cross the boundary.
Layer responsibilities (canonical)
Section titled “Layer responsibilities (canonical)”BullMQ — the job layer
Section titled “BullMQ — the job layer”What BullMQ owns (and will continue to own after the migration):
- Queue:
cinatra-background-jobs— single queue for all background work (src/lib/background-jobs.ts, line 53). - Worker pool:
concurrency: 4, singleWorkerinstance per process, registered viagetBackgroundJobRuntime(). - Job names:
BACKGROUND_JOB_NAMESenum (line 9-38) —AGENT_BUILDER_LANGGRAPH_EXECUTIONis the agent-run entry point. Other names cover non-agent work (blog, transcript, email-outreach sub-steps, Ross, scrape, research, enrichment, pricing sync). - Durability: Redis persistence of job payloads, retry state, and scheduled jobs.
- Retry / backoff:
attempts,backoffoptions onenqueueBackgroundJob(...). - Cancellation:
AbortControllermap (runtime.abortControllers) + poll-based cancellation metadata written tosystem_metadata. Workers check the flag everyBACKGROUND_JOB_ABORT_POLL_INTERVAL_MS(750ms). - Observability: BullMQ Board mounted at
/settings/operations/jobs/[[...slug]](QueueDash).
What BullMQ does NOT own:
- agent conversation state
- HITL pause/resume semantics
- per-step retry budgets (execution-layer concept, not a queue concept)
- presentation-hint parsing
Removed job names (post-110-G): AGENT_BUILDER_RESUME has been removed. It was the resume entry point for the hand-rolled TS HITL loop (resume.ts, agentic-resume.ts). Do not add new code that enqueues it. The two valid HITL resume paths are AGENT_BUILDER_EXECUTION (setup loop re-entry) and AGENT_BUILDER_LANGGRAPH_EXECUTION (LangGraph resume with resume: payload).
LangGraph — the agent runtime layer
Section titled “LangGraph — the agent runtime layer”What LangGraph owns (Phase 110-G onward):
- Graph registry: 6 stable type graphs in
packages/langgraph-agents/graphs/(leaf_v1,proxy_v1,orchestrator_v1,parallel_v1,supervisor_v1,iterative_v1) declared inpackages/langgraph-agents/langgraph.json. - Dispatch:
runLangGraphJobresolvestemplate.type → graphId(TYPE_TO_GRAPH map inlanggraph-execution.ts, line 36) and callsclient.runs.stream(thread_id, graphId, { input, streamMode: "events" }). - Thread lifecycle:
client.threads.create()→thread_idpersisted onagent_runs.lg_thread_id(column added Phase 92). - Execution state: LangGraph Server persists the graph’s
StateGraphinside its own Postgres instance — separate from Cinatra’scinatraschema. - Event stream: LangChain callback events (
on_chain_start,on_chat_model_stream,on_tool_start,on_tool_end,on_chain_end,interrupt,on_chain_error) consumed byrunLangGraphJoband translated to AG-UI events viaAgUiAdapter. - HITL interrupt/resume:
interrupt()inside graph nodes;client.runs.stream(..., { command: { resume } })for approval. TherunLangGraphJobinterrupt handler emitsagUiAdapter.onInterrupt(...)with a syntheticlg-{runId}ID — no DB writes during interrupt emission. - Setup field collection: the setup interrupt loop in
execution.tspassessetup-{runId}synthetic IDs and re-entersrunAgentBuilderExecutionJobafter each field approval; the LangGraph graph receives all collected fields in its input dict.
HITL routing (post-110-G)
Section titled “HITL routing (post-110-G)”All HITL approval routing uses synthetic ID prefixes. The planned_actions and review_tasks tables are dropped.
| Prefix | Source | What it does on approval |
|---|---|---|
setup-{runId} | Setup interrupt loop in execution.ts | Merges submitted field into agent_runs.inputParams; re-enqueues AGENT_BUILDER_EXECUTION |
lg-{runId} | LangGraph interrupt() handler in langgraph-execution.ts | Enqueues AGENT_BUILDER_LANGGRAPH_EXECUTION with resume: { values, fieldName } |
| Real UUID | Pre-migration rows (no longer exist) | Throws — “real UUID paths are not supported after Phase 110-G migration” |
Rule: Never pass a real UUID to approveReviewTaskInternal. Both interrupt emitters now pass synthetic IDs directly to agUiAdapter.onInterrupt with no DB writes.
The overlap — hand-rolled state machine (historical, now deleted)
Section titled “The overlap — hand-rolled state machine (historical, now deleted)”The following files existed in the hand-rolled TS execution layer and have been deleted in Phase 110-G:
| File | Deleted in | What it owned |
|---|---|---|
agentic-execution.ts | 110-G | LLM agent loop, HitlPauseSignal catch branch |
agentic-resume.ts | 110-G | BullMQ resume worker for agentic path |
agentic-tools.ts | 110-G | Child agent spawner, invokeAgentAsTool |
tool-interceptor.ts | 110-G | Risk-class map, HitlPauseSignal, budget guards |
resume.ts | 110-G | Deterministic resume step loop |
orchestrator-execution.ts survives with its non-dispatch symbols only: cancelOrchestratorRun, TERMINAL_STATUSES, OrchestratorLedgerEntrySchema, OrchestratorLedgerSchema, buildLedgerFromChildren, resolveInstalledVersion, computeChildInput. The dispatch phase (runOrchestratorJob, enqueueChildFlow, WaitingForHumanError) was deleted in Phase 110-F.
Function-Level Inventory
Section titled “Function-Level Inventory”One row per code span (function or named block) that was deleted, moved, or reduced. Listed for historical reference and to help sequence any remaining cleanup.
execution.ts (now ~372 lines, post-110-G)
Section titled “execution.ts (now ~372 lines, post-110-G)”| Function / Span | Lines | What it does | Migration fate | Phase |
|---|---|---|---|---|
assertOrchestratorReady | 36–74 | Validates all declared agentDependencies are installed; orchestrator gate | KEPT | — |
| Version pinning block | 127–179 | Reads agent_template_versions snapshot for run.packageVersion | KEPT | — |
| Setup Interrupt Loop | 187–355 | For each required inputSchema field missing from run.inputParams: sets pending_approval + emits AG-UI INTERRUPT with synthetic setup-{runId} ID; no DB writes | KEPT (rewritten — DB writes removed in 110-G) | 110-G |
| LangGraph dispatch | 362–365 | if executionProvider: "wayflow" or "default" → runLangGraphJob | KEPT — sole dispatch branch | — |
| Unsupported provider throw | 369–371 | Throws for null or unknown executionProvider | ADDED in 110-G | 110-G |
| Orchestrator dispatch | deleted | if type === "orchestrator" → runOrchestratorJob | DELETED | 110-F |
| Agentic dispatch | deleted | if executionMode === "agentic" → runAgentBuilderAgenticJob | DELETED | 110-G |
| Deterministic step loop | deleted | Sequential step iteration with approval gates | DELETED | 110-G |
resume.ts (deleted)
Section titled “resume.ts (deleted)”Entire file deleted in 110-G. resumeAgentBuilderExecutionJob (BullMQ worker) is gone. LangGraph resumes via AGENT_BUILDER_LANGGRAPH_EXECUTION with resume: payload.
agentic-execution.ts (deleted)
Section titled “agentic-execution.ts (deleted)”Entire file deleted in 110-G. All spans removed.
agentic-resume.ts (deleted)
Section titled “agentic-resume.ts (deleted)”Entire file deleted in 110-G. All spans removed.
agentic-tools.ts (deleted)
Section titled “agentic-tools.ts (deleted)”Entire file deleted in 110-G. All spans removed.
tool-interceptor.ts (deleted)
Section titled “tool-interceptor.ts (deleted)”Entire file deleted in 110-G. All spans removed.
orchestrator-execution.ts (reduced — ~200 lines)
Section titled “orchestrator-execution.ts (reduced — ~200 lines)”| Function / Span | Lines | What it does | Migration fate | Phase |
|---|---|---|---|---|
WaitingForHumanError class | deleted | Error for parking BullMQ job | DELETED | 110-F |
TERMINAL_STATUSES constant | kept | Set of terminal status strings | KEPT | — |
OrchestratorLedgerEntrySchema / OrchestratorLedgerSchema | kept | Zod schemas for ledger entries | KEPT | — |
buildLedgerFromChildren | kept | Maps child run rows to ledger entries | KEPT | — |
resolveInstalledVersion | kept | Reads installed semver for a package dep | KEPT | — |
computeChildInput | kept | Returns orchestrator’s inputParams as child input | KEPT | — |
runOrchestratorJob dispatch phase | deleted | Created child runs, FlowProducer dispatch | DELETED | 110-F |
enqueueChildFlow (FlowProducer) | deleted | BullMQ parent/child flow | DELETED | 110-F |
runOrchestratorRollup | kept (reduced) | Aggregates terminal child statuses; audit-side write for agent_runs | KEPT | — |
cancelOrchestratorRun | kept | Fan-out cancel across child runs | KEPT | — |
Symbol Dependency Map
Section titled “Symbol Dependency Map”Symbols deleted in the migration. Listed for reference; all importers have been updated.
| Symbol | Source File | Replaced by | Phase |
|---|---|---|---|
runAgentBuilderExecutionJob | execution.ts | Still exists — now a thin dispatcher to runLangGraphJob only | — |
resumeAgentBuilderExecutionJob | resume.ts (deleted) | runLangGraphJob with resume: payload | 110-G |
runAgentBuilderAgenticJob | agentic-execution.ts (deleted) | Deleted | 110-G |
runAgentBuilderAgenticResumeJob | agentic-resume.ts (deleted) | Deleted | 110-G |
runOrchestratorJob | orchestrator-execution.ts (deleted span) | Deleted | 110-F |
WaitingForHumanError | orchestrator-execution.ts (deleted span) | Deleted | 110-F |
cancelOrchestratorRun | orchestrator-execution.ts | KEPT | — |
OrchestratorLedgerSchema | orchestrator-execution.ts | KEPT | — |
HitlPauseSignal | tool-interceptor.ts (deleted) | LangGraph interrupt() | 110-G |
AGENTIC_MAX_STEPS | tool-interceptor.ts (deleted) | LangGraph recursion_limit | 110-G |
TOOL_RISK_CLASSES | tool-interceptor.ts (deleted) | JSON artifact for Python (pending D-03) | 110-G |
buildAgenticAgentTools | agentic-tools.ts (deleted) | Deleted | 110-G |
BACKGROUND_JOB_NAMES.AGENT_BUILDER_RESUME | background-jobs.ts | Deleted — resume is AGENT_BUILDER_LANGGRAPH_EXECUTION with resume: data | 110-G |
Routing (post-110-G)
Section titled “Routing (post-110-G)”runAgentBuilderExecutionJob (BullMQ worker) ├─ version pinning (packageVersion → readAgentTemplateVersionBySemver) ├─ assertOrchestratorReady (orchestrator dep-check) ├─ Setup Interrupt Loop (emits INTERRUPT per pending inputSchema field — no DB writes) └─ Dispatch: ├─ executionProvider: "wayflow" | "default" → runLangGraphJob (only valid path) └─ anything else → throws "Unsupported executionProvider after 110-G migration: ..."All other dispatch branches (orchestrator, agentic, deterministic step loop) have been deleted.
HITL surface — current state (post-110-G)
Section titled “HITL surface — current state (post-110-G)”Setup field collection
Section titled “Setup field collection”execution.tssetup interrupt loop detects a pending requiredinputSchemafield.- Sets
agent_runs.status → pending_approval. Constructs synthetic IDsetup-{runId}. - Calls
agUiAdapter.onInterrupt(fieldSchema, xRenderer, inputParams, syntheticId, fieldName)— no DB writes. - Human submits field value →
approveReviewTaskInternalroutes onsetup-prefix → merges value intoagent_runs.inputParams→ re-enqueuesAGENT_BUILDER_EXECUTION. runAgentBuilderExecutionJobre-enters, re-evaluates pending fields, either emits next INTERRUPT or falls through torunLangGraphJob.
LangGraph mid-run HITL
Section titled “LangGraph mid-run HITL”- LangGraph node calls
interrupt({ toolName, args, provenance }). - LangGraph Server persists thread state and emits an
interruptevent on the SSE stream. runLangGraphJobsees theinterruptevent → constructs synthetic IDlg-{runId}→ emitsagUiAdapter.onInterrupt(...)— no DB writes.- Human clicks Approve →
approveReviewTaskInternalroutes onlg-prefix → enqueuesAGENT_BUILDER_LANGGRAPH_EXECUTIONwithresume:payload. runLangGraphJobcallsclient.runs.stream(thread_id, graphId, { command: { resume: values } }). No synthetic messages, no re-running of prior tools.
The key invariant preserved:
agent_runs.statustransitions (running → pending_approval → running → completed/failed/stopped) stay byte-identical so existing UI code (polling route +AgenticRunPanel) keeps working.- AG-UI event shape stays byte-identical (
onInterrupt,onRunFinished,onToolCallStart/End) so the Tier 1 renderer and chatuseAgUiRunStreamkeep working.
Typical package mapping
Section titled “Typical package mapping”Keep using BullMQ
Section titled “Keep using BullMQ”- Non-agent background jobs — blog post generation, transcripts, email outreach per-draft workers, Apify scraping, LiteLLM pricing sync. These are short-lived deterministic jobs with no state machine; BullMQ’s retry + durability is the right primitive.
- Scheduled work — anything triggered on a cron. LangGraph has no native scheduler.
- Agent-run dispatch —
AGENT_BUILDER_LANGGRAPH_EXECUTIONstays as the single entry point intorunLangGraphJob.
Stays TypeScript regardless
Section titled “Stays TypeScript regardless”packages/llm-orchestrationpermanent components (registry.ts,mcp-access.ts,telemetry.ts,tools/skills.ts) — used by TypeScript connector packages (email outreach, blog generation) independently of the agent execution layer.packages/connector-*— all connectors stay TypeScript.- MCP server routes (
/api/mcp) — TS owns the MCP surface. - All Next.js routes and server actions.
Target-shape patterns
Section titled “Target-shape patterns”BullMQ trigger → LangGraph run (post-migration runLangGraphJob entry)
Section titled “BullMQ trigger → LangGraph run (post-migration runLangGraphJob entry)”export async function runLangGraphJob( data: { runId: string; resume?: { approved: boolean; values?: unknown } }, jobId: string,): Promise<void> { const { runId, resume } = data;
const run = await readAgentRunById(runId); if (!run) throw new Error(`Run ${runId} not found`);
const template = await readAgentTemplateById(run.templateId); const graphId = resolveGraphId(template);
const thread_id = run.lgThreadId ?? (await client.threads.create()).thread_id; if (!run.lgThreadId) await updateAgentRunLgThreadId(runId, thread_id);
if (resume) { // Resume a paused graph — Command(resume=...) is the single path for await (const event of client.runs.stream( thread_id, graphId, { command: { resume: resume } } )) { await handleLangGraphEvent(event, run, agUiAdapter); } } else { // Fresh run for await (const event of client.runs.stream( thread_id, graphId, { input: buildGraphInput(run) } )) { await handleLangGraphEvent(event, run, agUiAdapter); } }}Enqueue a LangGraph resume from an approval action
Section titled “Enqueue a LangGraph resume from an approval action”// packages/agents/src/review-task-actions.ts (post-110-G shape)await enqueueBackgroundJob(BACKGROUND_JOB_NAMES.AGENT_BUILDER_LANGGRAPH_EXECUTION, { runId: run.id, resume: { values, fieldName },});Python graph with interrupt() and PostgresSaver checkpointer
Section titled “Python graph with interrupt() and PostgresSaver checkpointer”# packages/langgraph-agents/graphs/leaf_v1.py (post-migration shape)from langgraph.checkpoint.postgres import PostgresSaverfrom langgraph.graph import StateGraphfrom langgraph.types import interrupt, Command
def build_graph(config: dict): # noqa: ARG001 builder = StateGraph(LeafState) builder.add_node("setup_collector", setup_collector_node) builder.add_node("agent_loop", agent_loop_node) builder.set_entry_point("setup_collector") builder.add_edge("setup_collector", "agent_loop")
checkpointer = PostgresSaver.from_conn_string(os.environ["SUPABASE_DB_URL"]) # checkpointer.setup() is a one-time DDL migration — run separately, not on every server restart: # python -c "import os; from langgraph.checkpoint.postgres import PostgresSaver; PostgresSaver.from_conn_string(os.environ['SUPABASE_DB_URL']).setup()" return builder.compile(checkpointer=checkpointer)
def setup_collector_node(state: LeafState, config: RunnableConfig): """Single setup node replaces execution.ts lines 193-293 interrupt loop.""" for field in state["inputSchema"]["required"]: if field not in state["collectedInputs"]: value = interrupt({"field": field, "schema": state["inputSchema"]}) state["collectedInputs"][field] = value return stateMigration plan
Section titled “Migration plan”Four phases to move from the hand-rolled state machine to native LangGraph HITL while preserving the existing audit surface and UI contract.
Status as of Phase 110-G: Phases A through D are complete. The TypeScript agent execution layer has been retired.
Mapping to ROADMAP sub-phases: Phase A = 110-C; Phase B = 110-D + 110-E + 110-F; Phase C = 110-F (orchestrator leg); Phase D = 110-G. Phase 110-B audited and extended this document. Phase 110-H finalises the post-migration architecture.
| Doc phase | ROADMAP sub-phase | Primary deletion target | Status |
|---|---|---|---|
| A (HITL pilot) | 110-C | tool-interceptor.ts throw path, execution.ts interrupt loop | Complete |
| B (agentic loop) | 110-D + 110-E | agentic-execution.ts, agentic-resume.ts, agentic-tools.ts | Complete |
| B (resume leg) | 110-F | resume.ts, agentic-resume.ts | Complete |
| C (orchestrator) | 110-F (orchestrator leg) | orchestrator-execution.ts, FlowProducer wiring | Complete |
| D (deterministic) | 110-G | execution.ts step loop, WaitingForHumanError, planned_actions + review_tasks table drop | Complete |
| — | 110-H | Post-migration audit + AGENTS.md rule | Complete |
Phase A — HITL inside a graph (complete)
Section titled “Phase A — HITL inside a graph (complete)”Wired interrupt() inside leaf_v1 and extended runLangGraphJob interrupt handler to emit AG-UI HITL events.
Phase B — Agentic loop to LangGraph (complete)
Section titled “Phase B — Agentic loop to LangGraph (complete)”agentic-execution.ts, agentic-resume.ts, agentic-tools.ts, tool-interceptor.ts deleted.
Phase C — Orchestrator to subgraph dispatch (complete)
Section titled “Phase C — Orchestrator to subgraph dispatch (complete)”runOrchestratorJob, enqueueChildFlow, and WaitingForHumanError deleted from orchestrator-execution.ts. The file survives with its non-dispatch symbols only.
Phase D — Deterministic step loop retirement (complete, Phase 110-G)
Section titled “Phase D — Deterministic step loop retirement (complete, Phase 110-G)”Completed work:
- Deleted
execution.tsdeterministic step loop (lines 326–534 in the pre-110-G file). - Deleted
resume.tsentirely —resumeAgentBuilderExecutionJobremoved fromBACKGROUND_JOB_NAMES. - Removed DB writes from the setup interrupt loop —
planned_actionsandreview_tasksrows are no longer created during interrupt emission. Syntheticsetup-{runId}ID is passed directly toagUiAdapter.onInterrupt. - Dropped
planned_actionsandreview_taskstables from schema and live DB. - Updated
approveReviewTaskInternalinreview-task-actions.tsto route exclusively on synthetic ID prefixes (setup-{runId},lg-{runId}). Real UUID path throws. - Added explicit throw in
execution.tsfor null or unknownexecutionProviderafter setup loop. AGENT_BUILDER_RESUMEremoved fromBACKGROUND_JOB_NAMES.
Post-110-G invariant: execution.ts is a ~372-line BullMQ entry function that delegates exclusively to runLangGraphJob. Every agent template must have executionProvider: "wayflow" set.
Post-110-H invariant: The concurrent dispatch race window is closed — updateAgentRunStatusConditional(runId, "queued", "running") is the atomic CAS gate inside runAgentBuilderExecutionJob immediately before the runLangGraphJob dynamic import. A second BullMQ retry that dequeues the same job will fail the CAS and return early without entering runLangGraphJob. The agent_update MCP tool no longer accepts "default" as an executionProvider value (IN-02). Legacy DB rows that still carry "default" continue to route correctly via the runtime alias || template.executionProvider === "default" in execution.ts — the two concerns are separate. The REST polling route /runs/[runId] now returns the actual template.inputSchema in the setup-fallback hitlContext instead of {} (WR-04), so REST polling clients have field metadata to render HITL approval forms.
Schema decisions
Section titled “Schema decisions”Tables that stay
Section titled “Tables that stay”agent_runs— unchanged.lg_thread_idcolumn (Phase 92) is load-bearing.agent_templates—execution_modecolumn is advisory;execution_providervalue"default"is a transitional alias only.agent_run_messages— retained for chat-history display, populated by LangChain message callbacks.agent_template_versions— unchanged. Version pinning still runs in the TS dispatcher before graph input is built.audit_events— unchanged.
Tables that were dropped (Phase 110-G)
Section titled “Tables that were dropped (Phase 110-G)”planned_actions— dropped. Was the execution-state store for the hand-rolled HITL loop. Audit-projection role (110-D) was transitional; the authoritative “what is this run paused on?” state lives in the LangGraph checkpointer.review_tasks— dropped. Was the UI-facing HITL approval queue. Approval routing now uses synthetic ID prefixes; the LangGraph checkpointer is the authoritative pause state.
Do not re-create these tables. Do not add new code that reads or writes them.
Cancellation and timeouts
Section titled “Cancellation and timeouts”This is the one place where the two layers meaningfully interact after the migration.
- BullMQ cancellation (
cancelBackgroundJob(jobId)) continues to be the operator-facing kill switch. It writes to thebackground_job_cancellation_requestsmetadata key. runLangGraphJobMUST pollAbortController.signal.abortedevery 750ms (same pattern as all other workers). On abort it must:- Call
client.runs.cancel(thread_id, run_id)to stop the LangGraph execution. - Transition
agent_runs.status → stoppedviaupdateAgentRunStatus. - Emit
AgUiAdapter.onRunFinished("stopped")for UI closure.
- Call
- Server-side timeout (
run.timeoutSeconds) is enforced inrunLangGraphJobaround the stream loop. LangGraph itself does not know about this timeout — it’s a BullMQ-worker concern.
Do not push the BullMQ abort signal into the LangGraph graph as a LangGraph-native primitive. Keep the kill switch at the worker boundary.
What to avoid
Section titled “What to avoid”- Adding any code that enqueues
AGENT_BUILDER_RESUME— the job no longer exists. - Referencing
planned_actionsorreview_taskstables in new code — they are dropped. - Writing DB rows during interrupt emission (setup loop or LangGraph interrupt handler) — both paths now use synthetic IDs and no DB writes.
- Passing real UUIDs to
approveReviewTaskInternal— onlysetup-{runId}andlg-{runId}prefixes are valid after Phase 110-G. - Adding new code paths to
execution.tsbeyond the setup interrupt loop and LangGraph dispatch — all new agent execution logic belongs in LangGraph graphs or inrunLangGraphJob. - Pushing BullMQ cancellation into the graph as a LangGraph primitive — keep the kill switch at the worker boundary.
- Building a second queue alongside
cinatra-background-jobs— a single queue with job-name routing is sufficient for v1. - Using
client.threads.create()outside ofrunLangGraphJob— thread creation and lifecycle belongs in one place. - Running LangGraph Server in the same process as Next.js — LangGraph Server is a separate service (
docker compose upinpackages/langgraph-agents). - Passing
executionProvider: "wayflow"]only (IN-02). Legacy DB rows that still carry"default"continue to route correctly via the runtime alias inexecution.ts; do not conflate MCP input validation with runtime dispatch logic. - Calling
runLangGraphJobfromrunAgentBuilderExecutionJobwithout a precedingupdateAgentRunStatusConditionalCAS — two concurrent BullMQ retries can both pass therun.status !== "queued"read-check at line 92 before either writes anything; the CAS is the only serialization point. - Calling
orchestrateGeneratefrom a LangGraph graph — use LangChain model classes (ChatOpenAI,ChatAnthropic) inside the graph; reserveorchestrateGeneratefor non-agent LLM work inside TypeScript connector packages.
Related documentation
Section titled “Related documentation”docs/ai/llm-orchestration.md— orchestration layer that stays TypeScript.docs/ai/mcp-patterns.md— MCP primitive conventions used by both TS and Python layers.docs/ai/agent-development.md— authoring new agent graphs.packages/langgraph-agents/README.md— LangGraph Server setup and deployment..planning/ROADMAP.mdPhase 999.8 — TypeScript agent execution layer retirement (the terminal cleanup phase after the migration is done).