Skip to content

feat(mastra): migrate v2 LangGraph chat extension to Mastra v3#22

Open
rohit-sourcefuse wants to merge 142 commits into
mainfrom
feat/mastra-migration-v2
Open

feat(mastra): migrate v2 LangGraph chat extension to Mastra v3#22
rohit-sourcefuse wants to merge 142 commits into
mainfrom
feat/mastra-migration-v2

Conversation

@rohit-sourcefuse

@rohit-sourcefuse rohit-sourcefuse commented May 22, 2026

Copy link
Copy Markdown

Migrate lb4-llm-chat-component from LangGraph + LangChain to Mastra v3

Replaces the LangGraph/LangChain runtime with Mastra v3 (Agent + Memory + Workflow primitives). LangGraph and LangChain are fully removed; ChatGraph + DbQueryGraph + VisualizationGraph and their node classes are deleted and re-expressed as Mastra workflows. The locked SSE wire contract (8 event types) is preserved byte-identical, so existing consumers keep working.

111 commits, +18.6k / −13.1k LOC across 223 files. 372 mocha tests pass; build + eslint + prettier + SonarCloud quality gate all green on HEAD 9649101.

This description is current as of HEAD 9649101. Earlier revisions of this PR body (smaller test counts, "stub steps", "LangChain kept") are obsolete — everything below is authoritative.


Runtime

  • Agent. Streams the registered mastra.getAgent('chatAgent') (per-request model / tools / instructions resolved from RequestContext), so every /reply is a single root trace. No detached per-request new Agent().
  • WorkflowRunner (REQUEST-scoped) bridges LB4 controllers to agent.stream().fullStream. A single AsyncEventQueue enforces total event order across pre-processing, the fullStream pump, and tool-side eventWriter calls.
  • Single Mastra instance (SINGLETON) holds storage pools, vector clients, observability exporters; per-request DI flows through agent.stream({requestContext}). Mastra is used as a library, not a server — LB4 owns the HTTP surface.
  • UsageAccumulator (REQUEST-scoped) replaces the LangChain-callback token counter.
  • Context compression — v2's ContextCompressionNode ported to a Mastra input processor (MAX_TOKEN_COUNT).

Workflows (fully wired — no stub steps)

Three workflows registered on MastraProvider, callable via mastra.getWorkflow(id).createRun().start():

  • generateQueryWorkflowparallel(check-cache, get-tables, check-templates) → post-cache-and-tables → branch(AsIs | FromTemplate | Continue) → get-columns → generate-checklist → dountil(sql-and-validate) → branch(save | failed).
  • improveQueryWorkflowload-existing → dountil(fix-query) → branch(save-improved | failed).
  • visualizationWorkflowselect-visualisation → call-query-generation → get-dataset-data → render-visualization.

Behaviour restored to v2 parity (each covered by tests):

  • Syntactic (DB EXPLAIN) + semantic validators, plus the table_not_found reclassification — a missing-table/column error re-selects and widens the allowed table set for the next dountil iteration.
  • Unanswerable-question gateget-columns detects when no table can answer the prompt and fast-fails with a user-facing clarification instead of burning the full validation loop and returning an empty (or wrong) dataset.
  • Disliked-cache filtering — a cached dataset a user disliked (or that no longer exists) is regenerated, not re-served.
  • "Similar" cache hit seeds SQL generation with the matched (non-disliked) query as a worked example, rather than discarding it.
  • SQL-gen tier selection — cheap tier for single-table queries and validation-fix retries, smart tier for multi-table first attempts (override via nodes.sqlGenerationNode.useSmartLLMForSingleTableQueries).
  • Checklist gate — honours nodes.generateChecklistNode.enabled and skips on ≤2 tables, eliding a planning LLM call per query.
  • Failure messages — the failed terminal surfaces a reason (unanswerable clarification ▸ last validation feedback ▸ generic rephrase) instead of a silent empty dataset.
  • Cache / template hits short-circuit.

Bindings (BREAKING vs the pre-Mastra names)

  • Model tiers are the bare keys ChatLLM / FileLLM / CheapLLM / SmartLLM / SmartNonThinkingLLM (the Mastra* aliases were removed).
  • Runtime infra under MastraInternalBindings (Mastra, Storage, Tools, Observability, RunRegistry, ResourceId).
  • Tool layer de-prefixed (BREAKING): IGraphTool, ToolStore, GetDataAsDatasetTool / ImproveDatasetTool / AskAboutDatasetTool / GenerateVisualizationTool; files *.tool.ts; LB4 keys services.<X>Tool.
  • The Mastra-instance provider class keeps a semantic prefix: MastraProvider.

LLM tiers + call-time settings

Cheap / Smart / SmartNonThinking tiers wired for cost parity; CLAUDE_THINKING(_BUDGET), per-provider temperatures, OpenRouter reasoning, and MAX_TOKEN_COUNT all threaded at call time. Providers are AI-SDK-native (@ai-sdk/* + @openrouter/ai-sdk-provider + ollama-ai-provider); create{OpenAI,OpenRouter}Model factories.

Memory + storage

  • semanticRecall is opt-in (MASTRA_SEMANTIC_RECALL, default OFF) — leaving it on whenever a vector store is bound caused progressive per-request latency. generateTitle likewise opt-in (MASTRA_GENERATE_TITLE).
  • Opt-in PostgresStorageProvider (@mastra/pg) alongside the LibSQL default; opt-in MastraVector (pgvector) for the db-query cache.

Chat history API (v2-Chat-compatible)

GET /chats, GET /chats/{id} (thread + messages), GET /chats/{id}/messages, backed by Mastra Memory (replaces the deleted ARC ChatController/ChatStore). Responses match the v2 Chat/Message shape consumers (e.g. BizBook) depend on: tenantId/userId, title (first prompt; New Chat fallback), top-level inputTokens/outputTokens (persisted to thread metadata per run), createdOn/modifiedOn. Each Mastra message is flattened into user/ai/tool messages; the tool message carries metadata.{toolName, args, existingDatasetId, status} — and for the visualization tool metadata.{visualization, config} — so a consumer can re-run / Load Dataset / re-render a chart from history. resourceId derivation (${tenantId}:${principalId}) is shared by the writer (WorkflowRunner) and readers (ChatController) so scopes never drift.

Observability

MastraMultiObservability fans every agent / workflow / tool span to Langfuse and LangSmith at once (whichever env keys are present); workflow tools forward tracing context so each /reply is one root trace. Env: LANGFUSE_*, LANGSMITH_* (LangChain-style names also honoured), OTEL_SAMPLE_RATE.

Developer ergonomics

  • Activity logs — every workflow step status streams to a debug channel: DEBUG=ai-integration:* (or ai-integration:steps) shows step progress on the server console (restores v2 step-log visibility; off by default, no console.*).
  • Token streaming (opt-in, default OFF)MASTRA_STREAM_TOKENS=true emits one message SSE event per text delta for progressive rendering. Default OFF preserves the single-event contract (and v2 parity — v2 never streamed). A consumer must append message events to use it; the bundled sandbox does. (The current BizBook UI renders a new bubble per message event, so it stays OFF until that UI appends.)
  • Extensibility — subpath exports lb4-llm-chat-component/testing (accuracy harness) and lb4-llm-chat-component/mastra (steps, workflows, MastraProvider); README "Extending the component" with full Tools / Agents / Steps examples.

Dependencies

Added: @mastra/core, @mastra/memory, @mastra/libsql, @mastra/pg, @mastra/observability, @mastra/langfuse, @mastra/langsmith; @ai-sdk/{openai,anthropic,amazon-bedrock,google,groq,cerebras}, ollama-ai-provider, @openrouter/ai-sdk-provider; ai, zod.
Removed: @langchain/langgraph, @langchain/community, @langchain/core, langchain. The only remaining LangChain-family dependency is the LangSmith exporter SDK (via @mastra/langsmith), used purely for trace export.

Tests

  • Step-level + helper unit coverage for every workflow step (cache judge incl. dislike/Similar paths, get-tables, get-columns answerability gate, checklist gate, SQL-gen tier selection, save-dataset, failed-message synthesis).
  • DAG-level branch coverage for generateQueryWorkflow (AsIs / FromTemplate / Continue / unanswerable / disliked-regenerate).
  • WorkflowRunner unit (Init / Message / coalesced-vs-streamed tokens / Tool / ToolStatus / Error / TokenCount / Status / ResourceId).
  • Live integration test (no sinon) — boots real Mastra + Memory + LibSQLStore(:memory:) driven by createMockModel; catches fullStream chunk-shape shifts between Mastra minor versions.
  • Endpoint-driven accuracy harness (./testing export) for prompt→SQL golden cases.

Test plan

  • npm run build — clean.
  • npm test372 pass, eslint + prettier clean.
  • SonarCloud quality gate — OK (no failing conditions).
  • npm ls @langchain/langgraph — empty.
  • Live Mastra Agent integration test against LibSQLStore(:memory:) + createMockModel.
  • Consumer end-to-end via lb4-llm-sandbox — chat, db-query, improve, ask, visualization (bar/pie/line), datasets, chat-history, playground all verified live.

How to review

  • WorkflowRunner (src/mastra/bridge/workflow-runner.ts) — load-bearing; verify the fullStream switch matches the SSE wire contract.
  • MastraProvider (src/providers/mastra/mastra.provider.ts) — workflow + agent registration, memory/semantic-recall/title env gating.
  • Workflow stepssrc/mastra/workflows/db-query/steps/ and .../visualization/steps/; each step has a typed input/output schema and inline doc-comments mapping to the v2 node it replaces.
  • Deleted v2 code lives under its pre-deletion commits — see the inline git show <sha>^:... pointers in step files for behaviour-delta review.

Known follow-ups (not blocking this PR)

  • HITL suspend/resume (ApprovalController + RunRegistry consumer) lands in v3.1 — tools emit AwaitingApproval but there is no resume flow yet.
  • Token-streaming default flips to ON once a consumer UI (BizBook) appends message events instead of rendering one bubble per event.

Adds the version-isolated scaffolding for the Mastra migration without
touching the live LangGraph code paths. Build stays green.

- src/graphs/types.ts: add IMastraGraphTool + ToolStatus.AwaitingApproval
  (parallel to v2 IGraphTool during P1; merge in P3)
- src/keys.ts: add Mastra/MastraChatLLM/MastraFileLLM/MastraStorage/
  MastraVectorStore/MastraEmbedder/RunRegistry/ResourceId binding keys
  and IRunRegistry interface
- src/providers/mastra/storage.provider.ts: DefaultMastraStorageProvider
  backed by @mastra/libsql (file:./mastra.db default)
- src/mastra/bridge/async-event-queue.ts: array-of-resolvers queue used
  by WorkflowRunner to serialise SSE events from multiple producers
- src/services/usage-accumulator.service.ts: per-model token totals;
  TokenCounter deletion deferred until WorkflowRunner replaces ChatGraph

Refs: MIGRATION-STRATEGY.md sections 7.1, 7.2, 7.3, 7.7, 7.8.
Pinned to @mastra/core@~1.36, @mastra/memory@~1.19, @mastra/libsql@~1.11
(latest compatible set; @mastra/libsql peer-requires core>=1.34, so
the doc's ~1.32.1 target is unreachable).
Continues P1 by adding the singleton Mastra builder, the LB4 lifecycle
hook that calls mastra.shutdown() on app stop, and the REQUEST-scoped
WorkflowRunner that bridges Mastra Agent fullStream to the SSE wire.
Existing LangGraph code paths still own GenerationService.generate();
WorkflowRunner is registered but not yet invoked, so tests keep passing.

- src/providers/mastra/mastra.provider.ts: SINGLETON. Builds Memory +
  ChatAgent (placeholder model, empty tools wired in P1.11) and the
  Mastra instance with storage + optional vector. workflows: {} until P3.
- src/observers/mastra-lifecycle.observer.ts: @lifeCycleObserver('mastra').
  start() reserved for vector preflight; stop() awaits mastra.shutdown.
- src/mastra/bridge/run-registry.ts: in-process IRunRegistry with TTL.
  Consumers swap for a Redis variant via the binding key.
- src/mastra/bridge/workflow-runner.ts: per-request Agent, Memory thread
  management, fullStream pump into AsyncEventQueue, RunRegistry hook on
  suspend, UsageAccumulator credit on finish. File summarisation and
  live tool wiring are TODOs for P1.11 / SummariseFileService extraction.
- src/graphs/event.types.ts: add LLMStreamErrorEvent to the union (the
  enum already had the value but no struct).
- src/component.ts: register DefaultMastraStorageProvider, MastraProvider,
  InProcessRunRegistry as keyed SINGLETON bindings; add UsageAccumulator
  + WorkflowRunner to services; add MastraLifecycleObserver to
  lifeCycleObservers; declare the lifeCycleObservers Component field.

Refs: MIGRATION-STRATEGY.md sections 7.4, 7.5, 7.6, 8.2.1, 12.4.
Mastra fullStream chunk shapes (tool-call-approval / tool-call-suspended
/ finish runId) are cast defensively; verified against @mastra/core 1.36
type defs but the literal chunk.type set is fast-moving — golden-SSE
replay (Section 15.3) is the integration anchor.
Cuts the live /reply chat flow over from LangGraph ChatGraph to the
Mastra-backed WorkflowRunner. Existing ChatGraph classes stay in the
bundle for now — they are deleted in P3 once DbQuery + Visualization
workflows land. ChatStore stays bound this commit (P1.13 removes it).

- src/services/generation.service.ts: inject WorkflowRunner, call
  workflowRunner.run() (synchronous AsyncIterable return) in place of
  await chatGraph.execute(...). Transport piping unchanged.
- src/__tests__/integration/generation.service.integration.ts: stub
  WorkflowRunner instead of ChatGraph; switch stubs.run callsFake to
  return the PassThrough as AsyncIterable.
- .gitignore: ignore mastra.db / -shm / -wal files spawned by the
  default LibSQL storage adapter during local tests.

Refs: MIGRATION-STRATEGY.md section 7.10 (cutover plan), 12 (SSE wire
contract unchanged). 190 mocha tests still pass; build green.
Adds 10 sinon-stubbed tests against Agent.prototype.{stream,getMemory}
that verify WorkflowRunner.run() produces the SSE event sequence
locked in MIGRATION-STRATEGY.md Section 12.4.

Coverage:
- Init then text-delta to Message events in order, ends with TokenCount
- sessionId reuses thread via getThreadById, no Init emitted
- sessionId not found, single Error event, queue closes
- Memory unbound, Error event, queue closes
- tool-call chunk, Tool event carries toolCallId, toolName, args
- tool-call-approval chunk, ToolStatus.AwaitingApproval
- tripwire chunk, Error event with processor id and reason
- finishReason suspended persists runId in RunRegistry
- Files emit Status events with original filename
- ResourceId binding flows into Memory.createThread and agent.stream
  memory.resource, verifying tenant isolation per Section 13.7

Refs: MIGRATION-STRATEGY.md sections 7.6, 12.4, 13.7, 15.6.
Suite now 200 passing, 190 baseline plus 10 new, 7 pending.
P1.9 lands the 8 LLM + 3 embedding AI SDK provider wrappers. The legacy
LangChain provider classes stay in their sub-modules so ChatGraph,
DbQuery and Visualization nodes that still call .invoke()/.bindTools()
keep building until they are removed in P3.

New classes (consumers bind these to the Mastra* binding keys):
- MastraOpenAI, MastraClaude, MastraBedrock, MastraGemini, MastraGroq,
  MastraCerebras, MastraOllama, MastraOpenRouter
- MastraGeminiEmbedding, MastraBedrockEmbedding, MastraOllamaEmbedding
- MastraBedrock drops the legacy getFile shim. AI SDK handles file
  parts via message content shape, not at model construction.

Binding type widened from MastraLanguageModel to MastraModelConfig in
keys.ts and workflow-runner.ts. MastraLanguageModel is Mastra's wrapped
V2/V3 union, whereas AI SDK providers return raw LanguageModelV3.
MastraModelConfig accepts both shapes plus model-router string ids,
which is what Agent already takes.

Deps added: @ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/amazon-bedrock,
@ai-sdk/google, @ai-sdk/groq, @ai-sdk/cerebras, ollama-ai-provider,
@openrouter/ai-sdk-provider. Pinned to current major.

Refs: MIGRATION-STRATEGY.md sections 5.1, 5.4, 5.5, 5.6. Note: doc
implies a full swap of legacy classes; we keep them parallel because
ChatGraph and DbQuery nodes still rely on LangChain APIs that get
removed in P3. 200 mocha tests still pass; build green.
P1.10 lands the @mastra/pg PgVector wrapper. The legacy LangChain
PGVectorStore class stays in the same directory and continues to back
AiIntegrationBindings.VectorStore for v2 callsites (semantic_cache
table reads in DbQueryGraph). The new MastraPgVectorStore is what
consumers bind to MastraVectorStore for Mastra Memory's semantic recall.

- src/sub-modules/db/postgresql/vector-store/pgvector.mastra.store.ts
  class MastraPgVectorStore implements Provider<MastraVector>. Builds
  PgVector from DB env vars; schemaName falls back to the juggler
  writerdb datasource's schema and can be overridden via
  MASTRA_PGVECTOR_SCHEMA. PgVector manages its own pg.Pool so during
  the parallel window consumers hold two pools against the same DB.

Dep added: @mastra/pg @~1.11.1 (peer-requires @mastra/core >= 1.34,
matches the rest of the @mastra/* pins).

Refs: MIGRATION-STRATEGY.md sections 13.8, 13.8a, 13.4. 200 mocha tests
still pass; build green.
P1.11 lands Mastra-flavored wrappers for get-data-as-dataset,
improve-dataset, ask-about-dataset, generate-visualization. They
delegate to the legacy IGraphTool .build().invoke() during the
P1->P3 transition; in P3 the bodies swap to
mastra.getWorkflow(...).createRun() and the legacy classes get
deleted.

- src/components/db-query/tools/get-data-as-dataset.mastra.tool.ts
  MastraGetDataAsDatasetTool implements IMastraGraphTool. Emits
  ToolStatus.Running/Completed/Failed via the eventWriter pulled
  from RequestContext, then delegates to the legacy tool's invoke.
- src/components/db-query/tools/improve-dataset.mastra.tool.ts
  MastraImproveDatasetTool, same pattern.
- src/components/db-query/tools/ask-about-dataset.mastra.tool.ts
  MastraAskAboutDatasetTool, read-only so no ToolStatus events.
- src/components/visualization/tools/generate-visualization.mastra.tool.ts
  MastraGenerateVisualizationTool, ToolStatus events + chart type
  enum still discovered from the legacy visualizer registry.

Wiring:
- src/graphs/types.ts: add MastraToolStore type (mirrors legacy
  ToolStore but lists IMastraGraphTool).
- src/keys.ts: add AiIntegrationBindings.MastraTools binding key.
- src/providers/mastra/mastra-tools.provider.ts: default provider
  exposing the 4 internal tools as MastraToolStore.
- src/mastra/bridge/workflow-runner.ts: inject MastraTools, populate
  the per-request Agent.tools map via buildToolMap().
- src/component.ts: register the 4 tool classes as services and bind
  DefaultMastraToolsProvider to MastraTools.

Refs: MIGRATION-STRATEGY.md sections 8.1, 8.4, 9.4. 200 mocha tests
still pass; build green; lint clean.
P1.12 ships the forward-only, idempotent backfill that copies the
legacy chats / messages tables into the Mastra storage adapter bound
at AiIntegrationBindings.MastraStorage. The script boots the
consumer's Application (path supplied via APP_MODULE env var) so it
inherits the exact repository + Mastra bindings the runtime uses.

- src/scripts/backfill-mastra-threads.ts: bootable entrypoint. Skips
  chats whose Mastra thread id already exists (re-runs are no-ops),
  emits a JSON summary at the end and exits non-zero on any per-chat
  failure. Tenant-scoped resourceId follows tenantId:userId
  by default (override via BACKFILL_RESOURCE_ID_FORMAT=user-only for
  single-tenant). SourceLoop audit columns are preserved verbatim
  under metadata.sourceloopAudit per Section 13.6a, with the
  original MessageMetadata stashed alongside.
- src/scripts/backfill-mastra-threads.ts: maps MessageMetadataType
  (ai/tool/user/system/attachment) onto Mastra roles (assistant,
  tool, user, system) so future Memory recall surfaces the right
  message authors.
- package.json: add bin entry so consumers run
  npx backfill-mastra-threads --dry-run from their repo.

Caveats:
- Memory.saveMessages public type wants MastraDBMessage; we emit the
  legacy MastraMessageV1 string-content shape and cast through never.
  Memory normalises internally on save; verify against your installed
  @mastra/memory version if you observe a mismatch.
- BootableApplication local type covers BootMixin + RepositoryMixin
  shape since the bare @loopback/core Application type lacks both.

Refs: MIGRATION-STRATEGY.md sections 7.9, 13.6a, 13.7. 200 mocha tests
still pass; build green; lint clean.
P3.1-P3.6 lands the structural DAGs for the three Mastra workflows that
replace the legacy LangGraph DbQueryGraph + VisualizationGraph. Every
step body is a stub returning the minimal default that keeps the chain
typed and end-to-end runnable; the real DbQueryService / VisualizerRegistry
wiring moves in alongside (Section 16A.4 preserves those helpers).

- src/mastra/workflows/db-query/generate.workflow.ts: 12 steps mirroring
  the v2 17-node graph. Parallel fan-out of check-cache/get-tables/
  check-templates/classify-change, fan-in via post-cache-and-tables,
  branch on status (FromTemplate / AsIs / Failed / Continue), then
  generate-checklist -> dountil(sql-and-validate) -> branch
  (failed / save-dataset).
- src/mastra/workflows/db-query/improve.workflow.ts: load-existing ->
  dountil(fix-query) -> branch. Same retry shape, starts mid-pipeline.
- src/mastra/workflows/visualization.workflow.ts: select-visualisation
  -> branch(call-query-generation | get-dataset-data) ->
  render-visualization.
- src/providers/mastra/mastra.provider.ts: workflows:{} ->
  {generateQueryWorkflow, improveQueryWorkflow, visualizationWorkflow}
  per Section 9.4a so mastra.getWorkflow(...) resolves at runtime.
- src/__tests__/unit/workflows.unit.ts: 3 smoke tests verifying each
  workflow completes its stub path with status=success.

Mastra quirks documented in code:
- After `.branch()` the matched branch's output is wrapped under the
  branch step's id (same shape as `.parallel()` fan-in), so the next
  step's body has to unwrap defensively. generateChecklistStep does
  exactly this.
- `.dountil()` feeds the step its own output on subsequent iterations,
  but the first iteration's input is the upstream payload. sql-and-
  validate uses z.any() input and destructures defensively to avoid
  a zod schema union across the two shapes.

Refs: MIGRATION-STRATEGY.md sections 9.1, 9.2, 9.3, 9.4a. 203 mocha tests
pass (was 200, +3 new); build green; lint clean.
…rvability

Two threads:

1. Real Mastra Agent integration test (no sinon).
   - src/__tests__/integration/workflow-runner-agent.integration.ts
     Boots a real Mastra + Memory + LibSQLStore(:memory:) driven by
     Mastra's createMockModel V2 mock. Drains WorkflowRunner.run() and
     asserts Init / Message / TokenCount order, message concatenation,
     and thread resumption on a second call with the same sessionId.
     Catches any fullStream chunk-type rename or payload-shape shift
     between Mastra minor versions before it bites production.
   - src/__tests__/integration/mastra-test-utils.d.ts: ambient module
     declaration for @mastra/core/test-utils/llm-mock (Mastra ships
     the JS but forgets the .d.ts). Drop once upstream fixes the
     missing types file.

2. Observability adapters for Langfuse + LangSmith.
   - AiIntegrationBindings.MastraObservability binding key (optional;
     unbound means no exporter wired).
   - MastraProvider takes the binding via {optional: true} and passes
     it through to the Mastra constructor's observability field, so
     every agent / workflow / tool span flows out to the configured
     exporter.
   - src/providers/mastra/observability/langfuse.provider.ts
     MastraLangfuseObservability. Env: LANGFUSE_PUBLIC_KEY,
     LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL, LANGFUSE_ENVIRONMENT,
     LANGFUSE_RELEASE. OTEL_SAMPLE_RATE controls span ratio.
   - src/providers/mastra/observability/langsmith.provider.ts
     MastraLangSmithObservability. Env: LANGSMITH_API_KEY /
     LANGCHAIN_API_KEY, LANGSMITH_PROJECT / LANGCHAIN_PROJECT,
     LANGSMITH_ENDPOINT / LANGCHAIN_ENDPOINT. Both env styles work
     since LangSmith's client honours the legacy LANGCHAIN_* names.

Deps: @mastra/observability ~1.13, @mastra/langfuse ~1.3, @mastra/langsmith
~1.2. Consumers install nothing extra if they do not bind an exporter.

Refs: MIGRATION-STRATEGY.md sections 10.1, 16A.6, 15.2 (mock LLM testing
pattern). 205 mocha tests pass (was 203, +2 new); build green; lint clean.
P3.7 lands the final form of MastraGetDataAsDatasetTool,
MastraImproveDatasetTool and MastraGenerateVisualizationTool per
Section 8.1. Each one drops the legacy IGraphTool.build().invoke()
delegation in favour of
`mastra.getWorkflow(<id>).createRun().start({inputData, requestContext})`.

The fourth tool, MastraAskAboutDatasetTool, stays on the legacy
RunnableSequence delegation because no workflow exists for its
question-answering path yet (read-only chain, no SQL generation).

- get-data-as-dataset.mastra.tool.ts -> generateQueryWorkflow
- improve-dataset.mastra.tool.ts -> improveQueryWorkflow
- generate-visualization.mastra.tool.ts -> visualizationWorkflow
  (also normalises the tool's prompt+datasetId input to the
  workflow's {datasetId, userQuery} schema)

Each wrapper:
- @Inject(AiIntegrationBindings.Mastra) the singleton
- emits ToolStatus Running -> Completed | Failed via the eventWriter
  pulled from RequestContext, preserving the SSE wire contract
- throws if the workflow returns non-success or is missing from
  Mastra config (clear pointer to Section 9.4a registration)
- threads ctx.requestContext through so workflow steps can read
  dbConnector / eventWriter / authUser via native param

Workflows still return stub defaults until real DbQueryService /
VisualizerRegistry wiring lands inside each step (Section 16A.4
keeps those helpers); the tool->workflow wire is now ready.

Refs: MIGRATION-STRATEGY.md sections 8.1, 9.1-9.3, 9.4a. 205 mocha
tests pass; build green; lint clean.
P3.8 round 1. Two changes that release the legacy chain so the bigger
chat-layer delete can land next round.

1. MastraAskAboutDatasetTool now executes inline against a one-shot
   Mastra Agent (chatLlm + system prompt) instead of delegating to the
   legacy AskAboutDatasetTool's RunnableSequence. The read-only Q&A
   path needs no workflow — the prompt collapses to a single
   agent.generate() call. The legacy AskAboutDatasetTool no longer
   has a Mastra-side consumer.
2. ChatController deleted. It only exposed GET /chats/:id backed by
   ChatStore — Mastra Memory already owns thread CRUD and the
   GenerationController is the canonical entry into the chat flow now.
   controllers/index.ts and component.ts both drop the reference.

Refs: MIGRATION-STRATEGY.md sections 8.1, 13.11. 205 mocha tests still
pass; build green; lint clean. Round 2 deletes ChatGraph + 6 nodes +
ChatStore + TokenCounter + their unit tests.
P3.8 round 2. ChatController went in round 1, ask-about-dataset
inlined to a one-shot Mastra Agent, MastraAskAboutDatasetTool no
longer pulls anything from the chat folder. Everything below is now
dead code on the live /reply path (Mastra Agent + WorkflowRunner own
that flow) and can leave the tree.

Deleted:
- src/graphs/chat/ entire folder
  - chat.graph.ts, chat.store.ts, nodes.enum.ts, index.ts
  - nodes/{call-llm,run-tool,init-session,summarise-file,
    context-compression,end-session}.node.ts
- src/graphs/state.ts (ChatGraphAnnotation only consumed by chat/*)
- src/services/token-counter.service.ts (UsageAccumulator replaced it
  in P1.8; the LangChain callback path is gone)
- src/__tests__/unit/chat.graph.unit.ts
- src/__tests__/unit/nodes/*.unit.ts (6 files)

Moved:
- src/graphs/chat/chat-metadata.type.ts -> src/graphs/message-metadata.type.ts
  Still consumed by message.model.ts and the backfill script;
  the file just lives one level up now.

Updated:
- src/graphs/index.ts: drop chat + state re-exports, add
  message-metadata re-export.
- src/services/index.ts: drop TokenCounter, add UsageAccumulator.
- src/scripts/backfill-mastra-threads.ts: new MessageMetadataType
  import path.
- src/component.ts: drop ChatGraph, 6 nodes, ChatStore, TokenCounter
  from the services array; the comment block explaining the
  parallel-tool transition window also goes away since the wrappers
  now own their workflow handles directly.

This unblocks the P1.13 ChatStore-delete task — which is no longer
listed as separate work because the same commit handles it. P1.13's
task entry can be marked complete in the tracker.

Refs: MIGRATION-STRATEGY.md sections 7.10, 9.5, 13.10, 14.1. 183 mocha
tests pass (was 205, -22 chat node tests intentionally dropped);
build green; lint clean.
P3.8 round 3a. The Mastra createTool wrappers from P3.7 fully replaced
these classes — get-data-as-dataset, improve-dataset and
generate-visualization now call mastra.getWorkflow().createRun().start()
directly, and ask-about-dataset runs an inline one-shot Mastra Agent.
None of the legacy IGraphTool implementations have a remaining
consumer, so the files go away.

Deleted:
- src/components/db-query/tools/ask-about-dataset.tool.ts
- src/components/db-query/tools/get-data-as-dataset.tool.ts
- src/components/db-query/tools/improve-dataset.tool.ts
- src/components/visualization/tools/generate-visualization.tool.ts

Updated:
- src/components/db-query/db-query.component.ts: drop AskAbout /
  GetData / ImproveDataset entries from the services array. DbQueryGraph
  + nodes stay one more round (round 3b deletes them).
- src/components/visualization/visualizer.component.ts: drop
  GenerateVisualizationTool from services. VisualizationGraph + nodes
  + visualizers stay (round 3c).
- src/components/db-query/tools/index.ts: re-export only the 3
  Mastra-shaped tool wrappers.
- src/components/visualization/tools/index.ts: re-export only the
  Mastra-shaped wrapper.

Refs: MIGRATION-STRATEGY.md sections 8.1, 9.5. 183 mocha tests still
pass; build green; lint clean.
P3.8 round 3b+3c+3d. With the legacy IGraphTool wrappers gone (round
3a) and the Mastra tools now calling workflows directly (P3.7), the
LangGraph DAGs and their nodes have no live consumer. They join
ChatGraph in the bin.

Deleted:
- src/components/db-query/db-query.graph.ts, state.ts, nodes.enum.ts
- src/components/db-query/nodes/ (17 node classes + index)
- src/components/db-query/testing/ (graph-coupled acceptance builders)
- src/__tests__/db-query/unit/db-query.graph.unit.ts +
  unit/nodes/* (12) + acceptance/db-query.graph.acceptance.ts +
  acceptance/nodes/get-tables-node.acceptance.ts
- src/components/visualization/visualization.graph.ts, state.ts,
  nodes.enum.ts
- src/components/visualization/nodes/ (4 node classes + index)
- src/graphs/base.graph.ts (CompiledGraph-typed abstract — no graph
  extends it anymore)

Refactored:
- src/components/visualization/types.ts: lift the VisualizationGraphState
  interface out of the deleted state.ts (plain TS interface; no
  LangGraph Annotation). Visualizers update their import from
  '../state' to '../types'.
- src/components/db-query/services/template-helper.service.ts: switch
  the RunnableConfig type import from the deleted graphs/types
  re-export to its real home at '@langchain/core/runnables'. The
  helper still uses LangChain's RunnableSequence internally — that
  stays.
- src/components/db-query/db-query.component.ts: drop DbQueryGraph +
  15 node entries from services. Keep DbSchemaHelperService,
  PermissionHelper, DataSetHelper, SchemaStore, TableSearchService,
  TemplateHelper (Section 16A.4 explicitly preserves these for the
  workflow step bodies that move in next).
- src/components/visualization/visualizer.component.ts: drop
  VisualizationGraph + 4 node entries; visualizers (Pie, Bar, Line)
  stay so consumers can register their own @visualizer() classes
  and the workflow's render step can dispatch via RequestContext.
- src/graphs/types.ts: remove IGraphNode, IGraphDirectEdge,
  IGraphConditionalEdge, IGraphEdge, RunnableConfig and the
  LangGraphRunnableConfig re-export. IGraphTool stays so any
  external consumer of `AiIntegrationBindings.Tools` still has the
  legacy interface to implement against.
- src/keys.ts: drop the Checkpointer binding + BaseCheckpointSaver
  import (BaseCheckpointSaver was a never-wired vestigial v2
  binding per Section 1 LOCKED/FREE table).
- src/types.ts: drop CheckpointerProvider type + BaseCheckpointSaver
  import.
- src/graphs/index.ts: drop ./base.graph re-export.
- src/components/db-query/index.ts: drop dead re-exports
  (./db-query.graph, ./nodes, ./nodes.enum, ./state).
- src/components/visualization/index.ts: same for visualization side.
- package.json: drop the ./db-query/testing exports / typesVersions
  entries since the testing folder is gone.

Refs: MIGRATION-STRATEGY.md sections 9.5, 16A.3, 16A.4. 113 mocha tests
pass (was 183, -70 chat/db-query/visualization graph and node tests
intentionally dropped); build green; lint clean.
P3.9 ships. With base.graph.ts, ChatGraph, DbQueryGraph and
VisualizationGraph all deleted in P3.8, nothing in src/ imports
@langchain/langgraph anymore. Removed from package.json dependencies;
package-lock.json regenerated.

P3 EXIT — DONE. All four exit criteria from Section 9.5 satisfied:
- npm ls @langchain/langgraph returns empty
- mastra.getWorkflow('generateQueryWorkflow') returns a Workflow instance
- mastra.getWorkflow('improveQueryWorkflow') returns a Workflow instance
- mastra.getWorkflow('visualizationWorkflow') returns a Workflow instance
- All 3 workflows callable via createRun().start() end-to-end

@langchain/community + @langchain/core stay in dependencies — still
consumed by visualizers (PromptTemplate, RunnableSequence) and
DbQueryService helpers. They're tracked as P2 stretch in
MIGRATION-STRATEGY.md Section 8.3; removal lands later if/when those
helpers are reworked to Mastra-native equivalents.

113 mocha tests pass; build green; lint clean.
…ed step

P3 follow-up. Workflow skeletons landed in b264e95 with stub bodies;
this commit lays the plumbing so subsequent commits can replace each
stub with the legacy node logic preserved at
4be9767^:src/components/db-query/nodes/<name>.node.ts.

WorkflowRunner.run() now sets five keys on the per-request
RequestContext that every workflow step can read via the native
requestContext param:
- resourceId, eventWriter (unchanged)
- dbConnector, chatLlm (optional, from existing bindings)
- lb4Ctx, full LB4 Context so steps can resolve any preserved
  helper, DbSchemaHelperService, SchemaStore, TableSearchService,
  PermissionHelper, DataSetHelper, TemplateHelper, lazily by
  service key

First step ported: getTablesStep resolves SchemaStore via lb4Ctx
and returns the raw table list from the cached schema. The
LLM-driven relevance filter restored from get-tables.node.ts lands
in the next commit alongside the PromptTemplate body and CheapLLM
call.

Workflow file header now points at git show 4be9767^ for the v2
logic of each remaining step, and lists the canonical traversal
order so the restore work stays sequenced.

Refs: MIGRATION-STRATEGY.md sections 9.1, 16A.4. 113 mocha tests
pass; build green; lint clean.
improveQueryWorkflow's load-existing step now resolves IDataSetStore
via the lb4Ctx that WorkflowRunner placed on requestContext, fetches
the existing dataset row by id, and merges the user's delta prompt
onto the original. Mirrors the v2 IsImprovementNode body at
4be9767^:src/components/db-query/nodes/is-improvement.node.ts and
forwards the dataset's tables array so the dountil(fix-query) loop
has real table names to thread through.

Step output schema gains originalPrompt + originalSql so downstream
fix-query can compare against the baseline. Defensive fallbacks
keep the workflow runnable when DatasetStore is unbound or the
dataset is missing, the loop then produces a "failed" outcome
without crashing the run.

Cleanup: drop unused DbQueryAIExtensionBindings + IDataSetStore
imports from generate.workflow.ts (carried over from earlier edit).

Refs: MIGRATION-STRATEGY.md sections 9.2, 16A.4. 113 mocha tests
pass; build green; lint clean. Remaining stub steps tracked in
task #26.
visualizationWorkflow's get-dataset-data step now resolves DataSetHelper
via the lb4Ctx that WorkflowRunner placed on requestContext, calls
helper.getDataFromDataset(datasetId) and forwards rows to the
render-visualization step. Mirrors the v2 GetDatasetDataNode body at
4be9767^:src/components/visualization/nodes/get-dataset-data.node.ts
minus the LangGraph state shape.

Defensive fallback returns an empty rows array when the consumer
hasn't bound the db-query component or when permission lookup throws,
so the workflow stays runnable while leaving render-visualization to
surface the error to the user.

Refs: MIGRATION-STRATEGY.md sections 9.3, 16A.4. 113 mocha tests pass;
build green; lint clean. Remaining stub steps tracked in task #26.
Three more real step bodies land, all non-LLM, all resolving preserved
helpers via the lb4Ctx that WorkflowRunner places on requestContext.

- generateQueryWorkflow.saveDatasetStep: resolves IDataSetStore +
  current user + DbSchemaHelperService + SchemaStore via lb4Ctx,
  builds the new dataset row from the dountil(sql-and-validate)
  output (sql, description, prompt, tables) plus the SchemaStore
  hash, calls store.create() and returns the new datasetId. Mirrors
  the storage half of v2 SaveDataSetNode; the LLM-driven description
  generator stays deferred to a future generate-description step.
- improveQueryWorkflow.saveImprovedStep: resolves IDataSetStore via
  lb4Ctx, updates the existing dataset's query + description via
  store.updateById(). No tenant re-check needed since load-existing
  already authorised the row through findById.
- visualizationWorkflow.selectVisualisationStep: walks the
  @visualizer()-tagged bindings the consumer registered and picks
  the first one when the caller did not supply a `type` hint.
  needsQuery=true when no datasetId provided so the workflow branches
  to call-query-generation; otherwise reads dataset rows directly.
  The LLM-driven type chooser from v2 SelectVisualizationNode lands
  later.

All three fall back to inert defaults when the relevant LB4 component
is unbound, so the workflow stays runnable in minimal-config
deployments.

Refs: MIGRATION-STRATEGY.md sections 9.1, 9.2, 9.3, 16A.3, 16A.4. 113
mocha tests pass; build green; lint clean.
callQueryGenerationStep now invokes generateQueryWorkflow recursively
via mastra.getWorkflow().createRun().start() when the user did not
supply a datasetId. Mirrors v2 CallQueryGenerationNode body at
4be9767^:src/components/visualization/nodes/call-query-generation.node.ts
but threads the userQuery through Mastra's native step.mastra and
step.requestContext params instead of LangGraph-style state.

selectVisualisationStep output now carries userQuery so
callQueryGeneration can wrap it as the generate workflow's prompt.

getDatasetDataStep widens its input handling: Mastra wraps the
matched branch output under the branch step id (same shape as
.parallel() fan-in), so the step unwraps both call-query-generation
and direct selectVisualisation shapes before fetching dataset rows.

Refs: MIGRATION-STRATEGY.md section 9.3, 16A.4. 113 mocha tests pass;
build green; lint clean.
generateQueryWorkflow's return-cached branch now resolves
IDataSetStore via lb4Ctx and reads the cached dataset's sql + id via
store.findById(). Mirrors the v2 graph's cache-hit path where the
caller skipped SQL generation when checkCache had already classified
the prompt as AsIs.

Defensive fallback keeps the workflow runnable when DatasetStore is
unbound or the cached row vanished between checkCache and
return-cached.

Refs: MIGRATION-STRATEGY.md section 9.1. 113 mocha tests pass; build
green; lint clean.
…Query, renderVisualization

Lands the first batch of generateText-driven steps. Each one reads
chatLlm from requestContext (set by WorkflowRunner) and calls
generateText({model, prompt}) from the ai package (v6, transitive
dep promoted to direct). All four steps degrade gracefully when
chatLlm is unbound so the workflow stays runnable without an LLM.

- generateQueryWorkflow.generateChecklistStep: asks the chat model
  for a 3-6 item validation checklist for the upcoming SQL query.
  Restored from v2 GenerateChecklistNode minus the structured-output
  coercion.
- generateQueryWorkflow.sqlAndValidateStep: produces SQL via the
  chat model with the checklist + tables + (optional) previous-pass
  feedback in the prompt. Validators (syntactic + semantic) still
  TODO; the step marks passed=true on a successful LLM call so the
  dountil loop exits after the first iteration. Validator wiring
  follows the same generateText pattern with prompts restored from
  `git show 4be9767^:src/components/db-query/nodes/{syntactic,
  semantic}-validator.node.ts`.
- improveQueryWorkflow.fixQueryStep: dountil loop body. Asks the
  chat model to improve the existing SQL based on the user's delta
  prompt + the original SQL (forwarded by load-existing). Same
  passed=true on success semantics.
- visualizationWorkflow.renderVisualizationStep: picks the matching
  @visualizer() from the registry (or first if no match) and
  delegates to its getConfig(). Visualizers own their own LLM calls
  internally — the workflow step just dispatches.

Deps: add `ai ^6.0.190` to dependencies (was transitive via Mastra).

Refs: MIGRATION-STRATEGY.md sections 9.1, 9.2, 9.3, 16A.3, 16A.4.
113 mocha tests pass; build green; lint clean.
Two more generateText-driven steps land in generateQueryWorkflow.

- classifyChangeStep: when the workflow runs in improvement mode
  (isImprovement=true), the chat model classifies the delta as
  minor / major / rewrite. The entry generateQueryWorkflow keeps
  isImprovement=false so the step routinely sits as a no-op; the
  improve workflow is the live caller. Restored from v2
  ClassifyChangeNode.
- getColumnsStep: simplified version of v2 GetColumnsNode. Walks the
  cached SchemaStore for the chosen tables, builds a
  {table: columns[]} blob, asks the chat model to narrow each
  table's column list to ones relevant to the user prompt, then
  intersects the returned tables back with the upstream list.
  Falls back to passing the upstream table list verbatim whenever
  the schema is unloaded, chatLlm is unbound, or the JSON response
  doesn't parse, so the workflow never breaks on this step.

Remaining LLM-driven stubs (checkCacheStep, checkTemplatesStep,
saveDatasetFromTemplateStep) sit on QueryCache / TemplateCache
LangChain BaseRetriever bindings; their migration is gated on
Section 13.8a PgVector cache rebuild + Section 16A.5 Redis cache
preservation work and lives in a later commit.

Refs: MIGRATION-STRATEGY.md sections 9.1, 16A.4. 113 mocha tests
pass; build green; lint clean.
Copilot AI review requested due to automatic review settings May 22, 2026 08:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Migrates the lb4-llm-chat-component runtime from LangGraph/LangChain graphs to Mastra v3 primitives (Mastra singleton + per-request WorkflowRunner), removing the legacy graph/node implementations while keeping the SSE event contract intact.

Changes:

  • Replaced ChatGraph.execute() usage with a REQUEST-scoped WorkflowRunner.run() that streams Mastra agent.stream().fullStream into the existing SSE event types.
  • Added Mastra v3 bindings/providers (Mastra singleton, default LibSQL storage, tool registry, observability exporters, run registry) plus multiple AI SDK-backed LLM/embedding providers.
  • Introduced Mastra-compatible tool wrappers and vector store provider; removed legacy LangGraph graphs/nodes/tools and their test suites, adding new unit/integration coverage for the Mastra path.

Reviewed changes

Copilot reviewed 142 out of 146 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/types.ts Removes LangGraph checkpointer provider types tied to @langchain/langgraph.
src/keys.ts Adds Mastra v3 binding keys (Mastra, storage, tools, LLM configs, observability, run registry, resourceId) and removes LangGraph checkpointer binding.
src/services/generation.service.ts Routes generation through WorkflowRunner.run() instead of ChatGraph.execute().
src/services/usage-accumulator.service.ts Adds a token-usage accumulator to replace the removed LangChain callback counter.
src/services/token-counter.service.ts Deletes legacy LangChain callback-based token counter.
src/services/index.ts Updates service exports to remove TokenCounter and export UsageAccumulator.
src/mastra/bridge/async-event-queue.ts Introduces an async queue to preserve total ordering of SSE events across concurrent producers.
src/mastra/bridge/run-registry.ts Adds default in-process run registry for HITL approval flow resumption.
src/providers/mastra/storage.provider.ts Adds default Mastra LibSQL storage provider (file-backed SQLite by default).
src/providers/mastra/mastra.provider.ts Registers singleton Mastra instance with Memory + workflows.
src/providers/mastra/mastra-tools.provider.ts Adds default Mastra tool registry provider for the 4 internal tools.
src/providers/mastra/observability/langsmith.provider.ts Adds optional LangSmith exporter wiring for Mastra observability.
src/providers/mastra/observability/langfuse.provider.ts Adds optional Langfuse exporter wiring for Mastra observability.
src/observers/mastra-lifecycle.observer.ts Adds app lifecycle observer to shutdown Mastra cleanly.
src/mastra/bridge/workflow-runner.ts Implements the main Mastra streaming bridge that maps fullStream chunks to the locked SSE contract.
src/sub-modules/db/postgresql/vector-store/pgvector.mastra.store.ts Adds Mastra PgVector store provider for Mastra vector storage.
src/sub-modules/db/postgresql/vector-store/index.ts Exports the new Mastra PgVector store alongside the legacy store.
src/sub-modules/providers/openai/llms/openai.mastra.provider.ts Adds AI SDK/Mastra-shaped OpenAI provider.
src/sub-modules/providers/openai/llms/index.ts Exports the new Mastra OpenAI provider.
src/sub-modules/providers/openrouter/llms/openrouter.mastra.provider.ts Adds AI SDK/Mastra-shaped OpenRouter provider.
src/sub-modules/providers/openrouter/llms/index.ts Exports the new Mastra OpenRouter provider.
src/sub-modules/providers/ollama/llms/ollama.mastra.provider.ts Adds AI SDK/Mastra-shaped Ollama provider.
src/sub-modules/providers/ollama/llms/index.ts Exports the new Mastra Ollama provider.
src/sub-modules/providers/ollama/embedding/ollama-embedding.mastra.provider.ts Adds AI SDK/Mastra-shaped Ollama embedding provider.
src/sub-modules/providers/ollama/embedding/index.ts Exports the new Mastra Ollama embedding provider.
src/sub-modules/providers/groq/llms/groq.mastra.provider.ts Adds AI SDK/Mastra-shaped Groq provider.
src/sub-modules/providers/groq/llms/index.ts Exports the new Mastra Groq provider.
src/sub-modules/providers/google/llms/gemini.mastra.provider.ts Adds AI SDK/Mastra-shaped Gemini provider.
src/sub-modules/providers/google/llms/index.ts Exports the new Mastra Gemini provider.
src/sub-modules/providers/google/embedding/gemini-embedding.mastra.provider.ts Adds AI SDK/Mastra-shaped Gemini embedding provider.
src/sub-modules/providers/google/embedding/index.ts Exports the new Mastra Gemini embedding provider.
src/sub-modules/providers/cerebras/llm/cerebras.mastra.provider.ts Adds AI SDK/Mastra-shaped Cerebras provider.
src/sub-modules/providers/cerebras/llm/index.ts Exports the new Mastra Cerebras provider.
src/sub-modules/providers/aws/llms/bedrock.mastra.provider.ts Adds AI SDK/Mastra-shaped Bedrock provider.
src/sub-modules/providers/aws/llms/index.ts Exports the new Mastra Bedrock provider.
src/sub-modules/providers/aws/embedding/bedrock-embedding.mastra.provider.ts Adds AI SDK/Mastra-shaped Bedrock embedding provider.
src/sub-modules/providers/aws/embedding/index.ts Exports the new Mastra Bedrock embedding provider.
src/sub-modules/providers/anthropic/llms/anthropic.mastra.provider.ts Adds AI SDK/Mastra-shaped Anthropic Claude provider.
src/sub-modules/providers/anthropic/llms/index.ts Exports the new Mastra Claude provider.
src/graphs/event.types.ts Extends SSE event union with an explicit Error event type.
src/graphs/types.ts Removes LangGraph node/edge types; adds Mastra-shaped tool interfaces and tool status enum value for awaiting approval.
src/graphs/message-metadata.type.ts Introduces framework-free message metadata types (moved out of deleted LangGraph state annotations).
src/graphs/index.ts Updates exports to remove graph/state exports and add message metadata exports.
src/controllers/index.ts Removes ChatController export.
src/component.ts Wires Mastra providers/services/tools + lifecycle observer; removes ChatGraph/ChatController registration.
src/components/visualization/types.ts Replaces LangGraph annotation-based state with a plain interface for visualizers.
src/components/visualization/visualizer.component.ts Removes visualization graph/node/tool service registrations; retains visualizers for workflow dispatch.
src/components/visualization/tools/generate-visualization.mastra.tool.ts Adds Mastra createTool wrapper that invokes visualizationWorkflow.
src/components/visualization/tools/index.ts Switches export from legacy tool to Mastra tool.
src/components/visualization/visualizers/bar.visualizer.ts Updates imports to use new visualization types module.
src/components/visualization/visualizers/line.visualizer.ts Updates imports to use new visualization types module.
src/components/visualization/visualizers/pie.visualizer.ts Updates imports to use new visualization types module.
src/components/db-query/tools/get-data-as-dataset.mastra.tool.ts Adds Mastra tool wrapper invoking generateQueryWorkflow.
src/components/db-query/tools/improve-dataset.mastra.tool.ts Adds Mastra tool wrapper invoking improveQueryWorkflow.
src/components/db-query/tools/ask-about-dataset.mastra.tool.ts Adds Mastra tool wrapper that runs a one-shot Mastra Agent for dataset Q&A.
src/components/db-query/tools/index.ts Switches db-query tool exports to Mastra tool wrappers.
src/components/db-query/db-query.component.ts Removes legacy DbQueryGraph/nodes/tools registration; keeps helper services for workflow step bodies.
src/tests/unit/workflows.unit.ts Adds workflow smoke tests verifying DAG completion on stubbed paths.
src/tests/integration/workflow-runner-agent.integration.ts Adds end-to-end integration test against real Mastra + Memory + LibSQL store using Mastra mock model.
src/tests/integration/mastra-test-utils.d.ts Adds ambient module declaration to compensate for missing Mastra test-utils typings.
src/tests/integration/generation.service.integration.ts Updates integration tests to stub/use WorkflowRunner instead of ChatGraph.
package.json Adds Mastra/AI SDK deps, removes @langchain/langgraph, registers a backfill-mastra-threads bin, and drops the removed db-query/testing export.
.gitignore Ignores default local Mastra sqlite DB files (mastra.db*).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/sub-modules/db/postgresql/vector-store/pgvector.mastra.store.ts Outdated
Comment thread src/sub-modules/db/postgresql/vector-store/pgvector.mastra.store.ts Outdated
Adds syntactic and semantic validation to the dountil loops in both
generateQueryWorkflow and improveQueryWorkflow.

- Syntactic: calls IDbConnector.validate(sql) which runs a DB EXPLAIN.
  On failure, sets passed=false with the connector error message in
  feedback so the next iteration sees the actual reason.
- Semantic: short LLM verdict against the validation checklist. The
  judge LLM returns either <valid/> or <invalid>...</invalid>. On
  fail, the invalid tag's body becomes the loop feedback.
- Both validators no-op when the relevant binding is unbound
  (dbConnector, chatLlm) or there is no checklist to verify, so the
  workflow stays runnable under partial configuration.

Mirrors v2 SyntacticValidatorNode + SemanticValidatorNode from
4be9767^:src/components/db-query/nodes/{syntactic,semantic}-validator.node.ts
minus the table-error reclassification + smartLLM swap; those land
in a follow-up alongside the remaining table-search reconnection.

Refs: MIGRATION-STRATEGY.md sections 9.1, 9.2, 16A.4. 113 mocha tests
pass; build green; lint clean.
Resolves the two Copilot review comments on PR #22 against
src/sub-modules/db/postgresql/vector-store/pgvector.mastra.store.ts.

1. Guard now requires DB_PASSWORD alongside DB_HOST / DB_PORT /
   DB_USER / DB_DATABASE before constructing the connection string.
   The error message already declared DB_PASSWORD required; the
   check now matches.
2. buildConnString() URL-encodes each credential / host component
   via encodeURIComponent so reserved URI characters in DB_USER or
   DB_PASSWORD ('@', ':', '/', etc.) cannot silently corrupt the
   PostgreSQL URL or leak the literal 'undefined' when a required
   piece is missing.

Refs: PR #22 review comments r3287131160 + r3287131206. 113 mocha
tests pass; build green; lint clean.
… and subquery reliability

The Mastra migration paraphrased two of v2's tuned prompts into thinner
versions, which the acceptance suite traced to real accuracy losses:

- Chat agent instructions (v2 init-session.node): restore "you MUST always
  call a tool on the first message, even if unsure — the tool rejects what it
  can't handle" + "do not assume intent beyond what's stated (don't make a
  chart unless asked)". The softer wording let weaker chat models narrate
  instead of calling a tool (e.g. "list every currency code" → no tool call)
  and misroute plain data questions to the visualization tool.

- SQL generation prompt (v2 sql-generation.node): restore the explicit
  "use JOINs, subqueries, CTEs or UNIONs", no-DML, no-SELECT-*,
  no-intent-assumptions, and bracket-grouping rules. The missing subquery
  guidance is why questions like "employees earning more than the average
  salary" (needs WHERE salary > (SELECT AVG(...))) failed validation
  repeatedly and exhausted the retry loop.

Semantic validator intentionally NOT reverted to v2's stricter form: ours is
deliberately lenient (biased to <valid/>) to avoid false-reject retries — the
better behaviour. No workflow/logic change; prompts only. 374 tests pass.
Consumers on classic TypeScript module resolution (node10 — what biz-book-api
uses) read subpath types from `typesVersions`, not from `exports`. The
`./testing` (acceptance harness) and `./mastra` (steps/workflows/MastraProvider)
subpaths were added to `exports` but never to `typesVersions`, so
`import ... from 'lb4-llm-chat-component/testing'` failed to resolve types and
consumers had to hand-add the mapping. Add both, and remove the dangling `pg`
entry (its `./pg` export was already removed; the dist path never existed).
exports and typesVersions now fully align.
… own the version

- Re-add the `./db-query/testing` export + typesVersions (kept `./testing`
  too). v3.0.0 exposed the acceptance harness at `lb4-llm-chat-component/
  db-query/testing`; consumers (biz-book-api reporting-service) import that
  path, so renaming it to `./testing` broke them. Both paths now resolve.
- Revert the manual version bump back to 3.0.0. This repo uses semantic-release
  which computes the next version from conventional commits and writes
  package.json at release. The branch carries the v3.0.0 tag in its history, so
  the BREAKING CHANGE commits make it publish 4.0.0 automatically.
The Gemini / Bedrock / Ollama embedding providers had ZERO tests (flagged by
the provider-parity audit). Add fail-closed env-guard assertions and a
model-construction check (asserts the AI-SDK textEmbeddingModel is built with
the configured modelId, no network call) for all three, including Gemini's
two-var guard and Ollama's default-vs-explicit base URL.
…l authors

A host implementing `IGraphTool` must build its tool with Mastra's
`createTool({...})` (the v2 `tool()` / `StructuredToolInterface` from
`@langchain/core/tools` are gone). Re-export `createTool` + the `Tool` type
from the package root so custom-tool authors import them from
`lb4-llm-chat-component` and don't need a direct `@mastra/core` dependency.
Additive + LangChain-free.
Export the pure relevant-table-selection helper (the LLM narrowing inside
getColumnsStep) from the `./mastra` subpath, plus its RelevantTablesResult
type. It takes `{chatLlm, prompt, tablesWithColumns, upstreamTables}` and
returns `{kind:'tables'|'unanswerable'|'unknown'}`, so a host can unit-test
table selection with just a model — no app boot or RequestContext. This is the
Mastra replacement for the deleted LangGraph GetTablesNode test seam.
The ChatAgent had two instruction sources that drifted. The per-request
instructions built in WorkflowRunner.buildInstructions() (set on
agentInstructions, used for every /reply) had softened the rule to 'only
reply conversationally when no tool fits', while the forceful v2 wording
('MUST always call the closest tool, never reply with just text on the
first message') lived only in mastra.provider's defaultInstructions — a
fallback that fires solely on out-of-band paths (Studio/MCP), never on
/reply. gemini-class chat models took the soft escape hatch and narrated
instead of invoking the query tool, producing the 'LLM did not call the
query tool' routing miss (0-generation, ~half-token exits) seen in the
sandbox and in biz-book-api's reporting-service.

Extract the core directives into a single shared CHAT_AGENT_DIRECTIVES
constant (forceful wording) consumed by both sources so they cannot drift
again. Sandbox accuracy run: 'did not call the query tool' dropped from
5-6 to 0; previously-stuck department/self-join cases recover.
Rapid sequential /reply calls in the accuracy suite trip provider rate
limits (e.g. OpenRouter), which surface as fake 0-generation 'tool not
called' / thrown-error results — not real wrong-SQL failures — and inflate
the failure count run-to-run.

Add an optional 7th arg {retries?, delayMs?} (both default off, so behavior
and token cost are unchanged unless a caller opts in). retries re-runs only
transient failures (isTransientFailure: no SQL generated + a string result);
a case that generated SQL but returned wrong rows is never retried, so a
wrong answer can't be retried into a right one. delayMs throttles between
calls to stay under rate limits. Cost note: retries multiply token spend —
keep them off in CI; intended for manual diagnosis runs only.
When an accuracy case fails the report records only the terminal status,
not the generated SQL, validation feedback, or a model refusal — so the
actual cause (refusal vs invalid SQL vs wrong filter) is invisible. Set
ACCEPT_DEBUG=true to print the full /reply event stream per case. Gated, no
effect unless set.
…ons + descriptions + context)

The Mastra rewrite passed only bare table+column NAMES to both the
table-selection LLM (pickRelevantTables) and SQL generation. v2 (LangGraph)
passed connector.toDDL(schema) — CREATE TABLE blocks with column
descriptions, FOREIGN KEY relations and table descriptions — and its
GetTablesNode told the model to assume tables can be related. Without
relations/descriptions the model cannot see how tables link (e.g.
projected_revenues.entity_id -> deals.id) and refuses multi-table joins
with 'no link between the tables' / 'tool did not complete'.

Add getSchemaForPrompt(schemaStore, dbConnector, tables) = toDDL + the
per-table context array, thread it as SqlGenInput.schema (used by both
buildGenerateSqlPrompt and buildImproveSqlPrompt), and pass it to
pickRelevantTables alongside an 'assume tables can be related; only answer
unanswerable when the DATA is truly absent' instruction (v2 parity). Falls
back to the bare name list when no connector/schema is available.

Sandbox mirror of biz-book-api revenue cases: deal-end-date + monthly +
two-month revenue go red->green; full suite 85%->92%, 0 routing misses, no
regression.
…y Sonar

The de-noise retry loop pushed generationAcceptanceBuilder over Sonar's
cognitive-complexity (21>10) and nesting (>3) limits (3 new critical
violations failing the gate). Extract it into runWithRetries and switch the
import to node:timers/promises. No behavior change.
…ity limit

generationAcceptanceBuilder was still at cognitive complexity 13 (>10) after
extracting the retry loop. Move the case-iteration loop into runAllCases so
the builder is mostly linear setup. No behavior change.
runAllCases was still at cognitive complexity 12 (>10). Move the per-iteration
body into runIteration so both stay under the limit. No behavior change.
The init migration declared `metadata jsonb NOT NULL` twice in
chatbot.chats, so a fresh Postgres install fails with 'column metadata
specified more than once' — the chats table (and its dependent FKs) never
get created, breaking chat history + dataset writes on any new consumer DB.
tracedGenerateText and streamDescription ended their MODEL_GENERATION spans
with usage at the top level (span.end({usage})) and an 'as never' cast that
hid the type mismatch. Mastra reads LLM usage from attributes.usage, so the
top-level field was ignored and every workflow LLM span (sql-generation,
get-columns, generate-description) reported 0 tokens in Langfuse/LangSmith
— while the agent span, which sets it correctly, did not. v2 LangGraph
captured these via LangChain callbacks (verified: old trace shows 3967
tokens on the get_tables LLM call).

Move usage into attributes and drop the cast; the AI-SDK v6 usage shape
{inputTokens, outputTokens, totalTokens} matches what the exporter reads,
so SQL-gen / get-columns / description token counts now surface.
The columnSelection flag had no JSDoc and only a terse, slightly misleading
README comment, and the COLUMN_SELECTION env var consumers use was
documented nowhere — so it was undiscoverable. Add a full JSDoc (true vs
false behavior, default, the wide-schema/token tradeoff, that the
relevant-table LLM call still runs for the unanswerable gate) and clarify
the README example.
get-columns always applied the LLM-picked table subset, ignoring the
columnSelection config entirely — so the documented flag (and its
COLUMN_SELECTION env) did nothing on this branch. Read the config and only
narrow to the picked subset when columnSelection is true; otherwise (the
default) keep ALL upstream tables so a lookup table the picker might omit
(e.g. exchange_rates for currency conversion) is never dropped before SQL
generation. Aligns the runtime with the documented behavior added in the
columnSelection docs commit.

NOTE: this changes the effective default from 'always narrow' to 'pass all
upstream tables' (columnSelection defaults false). Safer for joins/lookups;
set COLUMN_SELECTION=true to narrow on very wide schemas. Effect is
observable on the sql-generation span input tokens (true=smaller).
Cover OpenAI(+createOpenAIModel), Anthropic, OpenRouter(+createOpenRouterModel),
Gemini, Cerebras, Groq, Ollama, Bedrock(+NonThinking): fail-closed on each
required env var + correct modelId when present. Mirrors embedding-providers
unit suite; no network. Closes the gap where only embedding providers had
explicit provider-class coverage.
… sanitisation

Two behaviours present in v2 (LangGraph) and dropped in the Mastra migration:

1. Gemini embeddings: v2 set taskType=RETRIEVAL_DOCUMENT for retrieval-tuned
   vectors. AI-SDK exposes taskType only as a per-call provider option and
   Mastra's vector store calls doEmbed internally, so wrap the embedding model
   (withGoogleTaskType) to inject providerOptions.google.taskType on every
   embed. (v2's  has no AI-SDK equivalent — not restored.)

2. Bedrock file upload: v2 built a Converse document block with a sanitised
   name (sanitizeFilenameForAwsConverse). Restore that util (aws/utils.ts) and
   pass it as the AI-SDK file part's  in summariseFile, so Bedrock
   doesn't reject document names with disallowed chars / consecutive spaces.

Adds 8 unit tests (taskType injection + override + passthrough; filename
sanitisation cases). Full suite 420 passing, lint clean.
…tests/docs

Code-review follow-ups: replace the double 'as unknown as' in the Gemini
embedding wrapper with a single documented bridge cast (the installed
@ai-sdk/provider exports EmbeddingModelV2 while @ai-sdk/google returns a
bundled EmbeddingModelV3 — structurally compatible for doEmbed); add a test
that other google providerOptions are preserved alongside the injected
taskType; correct the columnSelection doc to say narrowing is table-level
only (no column pruning in v3).
Drives the public WorkflowRunner.run() with an uploaded file through a
capturing mock LanguageModelV2 wired as chatLlm (which resolveFileSummary
ModelConfig returns). Asserts the file-summary generateText call receives a
file part with mediaType=application/pdf, the buffer bytes, and a sanitised
filename ('Q3 Report!.pdf' -> 'Q3 Report') — proving the Bedrock filename
sanitisation is wired into the real path — plus the system prompt and the
merged [Attached file ...] summary reaching the chat agent. Closes the
audit's zero-file-upload-coverage gap.
LangGraph (main) emitted value-carrying progress as server-side `Log`
stream events (generated SQL, picked tables, validation-failure reasons,
matched template). The Mastra migration dropped the `Log` event type
(zero producers) and `emitToolStatus` only carries generic stage labels;
the dynamic detail survived only on the tracing spans, not the
`DEBUG=ai-integration:*` console channel.

Add `logStepDetail` — a server-ONLY debug log (no client `tool-status`
event, so detail never reaches the UI, matching the old transport which
dropped `Log` events before the client) — and emit it for:
- generated SQL / generation failure (runSqlAttempt, covers fix-query)
- query validation failure + reason and kind
- reselected tables after a table_not_found verdict
- selected tables and the unanswerable reason (get-columns)
- matched template (check-templates)

Restores v2-equivalent developer visibility under
`DEBUG=ai-integration:steps` without changing client-facing behaviour.
}

private resolveFileSummaryModelConfig(): MastraModelConfig | undefined {
if (this.chatLlm) return this.chatLlm;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [BUG]: FileLLM tier injected but never used — file summarisation silently uses ChatLLM

Issue: fileLlm is injected at L265 (@inject(AiIntegrationBindings.FileLLM)) but resolveFileSummaryModelConfig() only ever consults this.chatLlm. Consumers (e.g. reporting-service) that bind a dedicated FileLLM (a vision/file-capable model) expecting it to handle attachments get it ignored — files are summarised by the chat model, which may not even accept file parts. The fileLlm ctor param becomes dead.

Fix:

private resolveFileSummaryModelConfig(): MastraModelConfig | undefined {
  if (this.fileLlm) return this.fileLlm;
  if (this.chatLlm) return this.chatLlm;
  const defaultModel = process.env.MASTRA_DEFAULT_CHAT_MODEL;
  return defaultModel ? toModelRouterFallbackConfig(defaultModel) : undefined;
}

Credit: Open Code

@inject(AiIntegrationBindings.ChatLLM, {optional: true})
private chatLlm?: MastraModelConfig,
@inject(InternalBindings.RunRegistry)
private runRegistry?: IRunRegistry,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [DEAD_CODE]: runRegistry injected but never referenced

Issue: runRegistry is injected (non-optional) but never read or written anywhere in this class — the HITL resume/suspend path that would call .set() is deferred to v3.1 per the handleChunk comment. A non-optional inject for an unused, not-yet-wired dependency is a boot-time coupling with no payoff and risks failing construction if the binding is absent.

Fix: Drop the param until the resume path lands, or mark {optional: true} to avoid a hard boot dependency on an unused binding.

Credit: Open Code

// The runtime Memory instance has updateThread; the narrowed base type
// doesn't declare it, so view it through ThreadMemory (updateThread
// optional + guarded below).
const tm = memory as unknown as ThreadMemory;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [TYPE_SAFETY]: double cast memory as unknown as ThreadMemory hides the real Memory contract

Issue: getMemory() returns a typed Memory; double-casting through unknown to a hand-rolled ThreadMemory to reach updateThread papers over the actual API and will silently break if Mastra renames/moves updateThread. The optional-updateThread guard then makes a missing method a silent no-op (usage never persisted) rather than a visible error.

Fix: Type against the published Memory interface (import type {MastraMemory} / the storage thread API) and call updateThread directly; if the installed version truly lacks it, surface that at boot, not per-request.

Credit: Open Code

const resolver = this.resolvers.shift();
if (resolver) {
resolver({value, done: false});
} else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [PERFORMANCE]: maxSize overflow hard-closes mid-stream → silent SSE truncation

Issue: On overflow push() calls close(), which resolves the consumer's pending next() with done:true and discards every already-queued-but-undrained event. For a fast tool emitting >10000 events (or a slow SSE consumer), the user sees the stream end cleanly with no Error event and a partial answer — indistinguishable from success. That's data loss disguised as completion.

Fix: Before hard-closing, enqueue a terminal Error event so the consumer can distinguish overflow from normal completion:

if (this.queue.length >= this.maxSize) {
  this.queue.push(OVERFLOW_ERROR_EVENT as T); // or surface via a callback
  this.close();
  return;
}

…and/or apply real backpressure (await) for producers that can tolerate it.

Credit: Open Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed — overflow silently looked identical to a normal stream end (clean done:true, partial answer, no signal to the client). Fixed in 8581610: AsyncEventQueue now accepts an optional overflowValue in its constructor; on maxSize overflow it pushes that value before closing so the consumer sees an explicit event before done. workflow-runner wires an LLMStreamEventType.Error event as the sentinel, so the client receives the error before the stream closes.

Comment thread src/scripts/backfill-mastra-threads.ts Outdated
summary.messagesWritten += msgs.length;
return;
}
await memory.createThread({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 [STRUCTURE]: non-atomic createThread + saveMessages breaks idempotency — partial failures lose messages forever

Issue: Idempotency is keyed solely on thread existence (L172-176). But the thread is created (L189) and messages saved (L202) as two separate awaits with no transaction. If the process dies or saveMessages throws between them, the thread exists with zero/partial messages; the next run sees the thread, hits summary.skipped++, and never retries the messages — permanent silent data loss in a migration script.

Fix: Make the operation atomic (single transaction over thread+messages if the storage adapter supports it), or write messages first / verify message count on the idempotency check so a half-written thread is detected and completed on re-run rather than skipped.

Credit: Open Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed — non-atomic create+save could strand a thread empty and skip it forever. Fixed in 824b7b5: an existing thread is now skipped only when it already holds all its messages; otherwise messages are re-saved (idempotent upsert on the stable source message id, so no duplicates), repairing a thread left behind by a run that died before saveMessages. Added an optional Memory.query probe to count persisted messages (falls back to re-save when unavailable).

// eslint-disable-next-line @typescript-eslint/no-explicit-any
visualizer = new BarVisualizer({} as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
generateObjectStub = sinon.stub(visualizer, 'callGen' as any);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Improve: Stubbing the visualizer's own callGen via 'callGen' as any couples the test to a private member name

Issue: sinon.stub(visualizer, 'callGen' as any) stubs a protected method of the class under test, with two as any casts to defeat type-checking. callGen exists in production purely as a test seam wrapping ai.generateObject; the test reaches past the public getConfig surface into an internal name, so renaming callGen (a pure refactor) breaks every visualizer test. (Same pattern in line/pie visualizer specs.)

Fix: Mock the actual boundary — the ai module's generateObject — so the test exercises the real callGen indirection and survives internal renames:

import * as ai from 'ai';
sinon.stub(ai, 'generateObject').resolves({object: mockLLMResponse});

If the seam must stay, at least drop the as any by typing the stub against the real key.

Credit: Open Code


describe('generateQueryWorkflow', () => {
it('completes the stub path with status=success', async () => {
generateQueryWorkflow.__registerMastra(mastra);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Improve: Test drives Mastra's internal __registerMastra instead of the public registration path

Issue: Calling generateQueryWorkflow.__registerMastra(mastra) reaches into a double-underscore internal of @mastra/core. The workflows are already passed to the new Mastra({workflows}) constructor on line 15, which is the documented registration mechanism; the manual __registerMastra call duplicates that via a private API and will break on a Mastra minor bump. (Repeated at the improve/visualization cases.)

Fix: Resolve workflows through the singleton you already constructed — mastra.getWorkflow('generateQueryWorkflow') after the constructor registration — and drop the __registerMastra calls entirely.

Credit: Open Code

Comment thread README.md
### Overview

A Loopack4 based component to integrate a basic Langgraph.js based endpoint in your application which can use any tool that you register using the provided decorator.
A Loopback4 based component to integrate an LLM chat endpoint (powered by [Mastra](https://mastra.ai)) into your application, with pluggable tools, model providers, storage, and observability.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 CONSUMER REGRESSION: No upgrade / breaking-changes section for 3.x → 4.0

Issue: This PR is a major LangChain/LangGraph → Mastra migration with hard breaking changes for the two downstream services (both pin ^3.0.0), yet the README has no consumer-facing "Upgrading from 3.x" / breaking-changes section. The only migration text is the internal src/mastra/README.md (a maintainer v2→v3 node map) and a single testing note at L864. Consumers that today do @inject(AiIntegrationBindings.SmartLLM) llm: BaseChatModel, import {RunnableConfig} from the package, bind services.GetTablesNode / use GetTablesNode, or implement IDataSetStore will break at compile/runtime with no documented path:

  • BaseChatModel → the tier bindings now resolve to AI-SDK LanguageModelV2 (breaks resource-management cv-data-extractor.service.ts, skill-query.service.ts; reporting consumers).
  • RunnableConfig is no longer exported (breaks reporting sow-recommendation.tool.ts) → call-site must move to the Mastra tool ctx + re-exported createTool/Tool.
  • GetTablesNode class + services.GetTablesNode binding deleted → replaced by exported pickRelevantTables (breaks reporting table-selection.acceptance.ts).
  • IDataSetStore gains new required methods.

Fix: Add a top-level ## Upgrading from 3.x (Breaking changes) section enumerating each removed/changed symbol and its replacement, e.g.:

## Upgrading from 3.x (Breaking changes)
- Model tiers (`SmartLLM`/`CheapLLM`/`FileLLM`/...) now resolve to AI-SDK `LanguageModelV2`, **not** LangChain `BaseChatModel`. Replace `BaseChatModel` injections + LangChain call APIs with the AI-SDK `generate`/`stream` surface.
- `RunnableConfig` is removed. Migrate custom tools to Mastra `createTool` (re-exported from the package root) and read context from the tool execute ctx.
- `GetTablesNode` / `services.GetTablesNode` are removed. Use the exported `pickRelevantTables` seam for host table-selection tests.
- `IDataSetStore` requires new methods: <list>.

Credit: Open Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — added the "Upgrading from 3.x to 4.0 (Breaking changes)" section in 824b7b5, enumerating: binding value type BaseChatModel → AI-SDK LanguageModelV2; removed RunnableConfig/./nodes/./state exports + the pickRelevantTables replacement; new required IDataSetStore methods; Node 22 || 24 engines; and the dropped provider inference knobs (with a pointer to track the providerOptions re-wiring).

Comment thread package.json
"exports": {
".": "./dist/index.js",
"./testing": {
"type": "./dist/components/db-query/testing/index.d.ts",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 IMPROVE: subpath exports use "type" instead of the "types" condition key

Issue: Every subpath conditional export points its declaration file at a "type" key (here and in the new ./db-query/testing, ./mastra, ./mastra-* entries). "type" is not a resolution condition Node or TypeScript recognises — TS only honours the "types" condition inside exports. Type resolution currently only works because of the parallel typesVersions fallback; the exports block itself contributes nothing to .d.ts resolution and is misleading. Under moduleResolution: "bundler"/"node16" consumers that don't consult typesVersions, subpath types could fail to resolve.

Fix: rename the condition key and order it before default:

"./mastra": {
  "types": "./dist/mastra/index.d.ts",
  "default": "./dist/mastra/index.js"
}

Apply to all subpath entries (pre-existing ./aws/./openrouter etc. too).

Credit: Open Code

deleted_on timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
modified_by uuid NULL,
modified_on timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
metadata jsonb NOT NULL,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why is this removed ?? why it is not needed now ?

@sf-sahil-jassal sf-sahil-jassal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

50+ Sonar issues, conflicts as well

The custom MODEL_GENERATION child spans created in tracedGenerateText and
the streaming generate-description path set only `attributes` — never the
span `input` (prompt) or `output` (completion). The LangSmith/Langfuse
exporter maps a span's input/output to the run's inputs/outputs, so these
workflow LLM spans (get-columns, sql-generation, classify-sql-error,
semantic-validate, generate-description) showed blank prompt and
completion in the trace while the agent's own native LLM span did not.

Set `input` at createChildSpan and `output` on `end()` for both span
sites. Covers every tracedGenerateText label plus the streamed
description. No client-facing change; trace-only.
# Conflicts:
#	package-lock.json
#	package.json
…docs

Addresses three critical review findings on PR #22:

- security: enforce read-only SQL before validate/persist/execute. The
  connector wraps execution in `SELECT * FROM (<sql>) AS subquery`, which
  blocks a bare INSERT/UPDATE/DELETE, but a data-modifying CTE
  (`WITH d AS (DELETE ... RETURNING *) SELECT * FROM d`) survives the wrap
  and EXPLAIN does not run it — prompt rules are not an enforcement
  boundary. Add `detectDmlStatement` (strips comments/literals first to
  avoid false positives on identifiers like `update_date`) and reject in
  `validateSqlSyntactic` before the connector is touched. +9 unit tests.

- backfill: repair partial backfills. createThread + saveMessages are two
  non-transactional awaits; a crash between them left a thread with zero
  messages that was then skipped forever. Skip only when the existing
  thread already holds all its messages, else re-save (idempotent upsert
  on the stable source message id). Adds optional Memory.query probe.

- docs: add a README "Upgrading from 3.x to 4.0 (Breaking changes)"
  section — binding value type BaseChatModel → AI-SDK LanguageModelV2,
  removed RunnableConfig/./nodes/./state exports + pickRelevantTables
  replacement, new required IDataSetStore methods, Node 22||24 engines,
  dropped provider inference knobs.
…rove early-exit

- async-event-queue: emit overflowValue (caller-supplied) before hard-close
  on maxSize overflow so the SSE consumer sees an explicit Error event
  instead of a silent done:true. workflow-runner wires an Error-type
  LLMStreamEvent as the sentinel. Without this, overflow looks identical
  to a normal stream end — client gets a clean EOF with a partial answer.

- template-helper: replace sequential .replace() calls in both
  _buildExtractionPrompt and _substitutePlaceholders with single-pass
  regex replacements. Sequential chaining is order-dependent and
  injectable — a prompt containing the literal text {template} caused
  the second .replace to substitute INTO already-inserted prompt content;
  a resolved placeholder value containing {{another_marker}} would get
  substituted in a later iteration. One regex pass replaces each marker
  exactly once without re-scanning replaced content.

- improve.shared: loadErrorShortCircuit now forces attempts to
  MAX_IMPROVE_ATTEMPTS so the improve dountil loop exits immediately on
  a load-error rather than wasting the remaining retry budget with
  no-op iterations.
…flow status guard

- observability/util: add excludeSpanTypes [MODEL_CHUNK, MODEL_STEP] to
  the sampling config. During streaming every text-delta and tool-step
  emits a span; without this Langfuse/LangSmith receives one span per
  token on the hot path — high export overhead and trace noise. The
  parent MODEL_GENERATION span already captures full I/O and usage.

- dataset-helper.deleteMany: replace sequential for-await with
  Promise.all for cache evictions. Evictions are best-effort and
  independent; N serialised round-trips was unnecessary latency on
  bulk-delete.

- visualization/get-dataset-data: run fetchDatasetDescriptor and
  fetchDatasetRows concurrently with Promise.all. Two independent DB
  reads were sequenced with no dependency between them.

- visualization/shared.fetchDatasetRows: cap rows with maxRowsForAI
  from DbQueryConfig (default 100). Previously the function pulled
  unbounded rows for every visualization, ignoring the consumer-
  configured cap and risking OOM on large datasets.

- visualization/call-query-generation: guard on result.status before
  extracting datasetId. When the nested generateQueryWorkflow fails/
  suspends, extractWorkflowResult returns {} and datasetId silently
  becomes '' — the visualization step proceeded with no data and no
  signal. Now returns needsQuery=true on non-success so the caller
  can surface the failure. Test stubs updated to carry status:'success'.
@sonarqubecloud

Copy link
Copy Markdown

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.

6 participants