feat(mastra): migrate v2 LangGraph chat extension to Mastra v3#22
feat(mastra): migrate v2 LangGraph chat extension to Mastra v3#22rohit-sourcefuse wants to merge 142 commits into
Conversation
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.
There was a problem hiding this comment.
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-scopedWorkflowRunner.run()that streams Mastraagent.stream().fullStreaminto 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.
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; |
There was a problem hiding this comment.
🟠 [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, |
There was a problem hiding this comment.
🟡 [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; |
There was a problem hiding this comment.
🟡 [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 { |
There was a problem hiding this comment.
🟠 [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
There was a problem hiding this comment.
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.
| summary.messagesWritten += msgs.length; | ||
| return; | ||
| } | ||
| await memory.createThread({ |
There was a problem hiding this comment.
🔴 [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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
🟡 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); |
There was a problem hiding this comment.
🟡 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
| ### 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. |
There was a problem hiding this comment.
🔴 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-SDKLanguageModelV2(breaks resource-managementcv-data-extractor.service.ts,skill-query.service.ts; reporting consumers).RunnableConfigis no longer exported (breaks reportingsow-recommendation.tool.ts) → call-site must move to the Mastra tool ctx + re-exportedcreateTool/Tool.GetTablesNodeclass +services.GetTablesNodebinding deleted → replaced by exportedpickRelevantTables(breaks reportingtable-selection.acceptance.ts).IDataSetStoregains 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
There was a problem hiding this comment.
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).
| "exports": { | ||
| ".": "./dist/index.js", | ||
| "./testing": { | ||
| "type": "./dist/components/db-query/testing/index.d.ts", |
There was a problem hiding this comment.
🟡 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, |
There was a problem hiding this comment.
why is this removed ?? why it is not needed now ?
sf-sahil-jassal
left a comment
There was a problem hiding this comment.
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'.
SonarQube reviewer guide
|



Migrate
lb4-llm-chat-componentfrom LangGraph + LangChain to Mastra v3Replaces the LangGraph/LangChain runtime with Mastra v3 (
Agent+Memory+Workflowprimitives). 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.111commits, +18.6k / −13.1k LOC across223files. 372 mocha tests pass; build + eslint + prettier + SonarCloud quality gate all green on HEAD9649101.Runtime
mastra.getAgent('chatAgent')(per-request model / tools / instructions resolved fromRequestContext), so every/replyis a single root trace. No detached per-requestnew Agent().WorkflowRunner(REQUEST-scoped) bridges LB4 controllers toagent.stream().fullStream. A singleAsyncEventQueueenforces total event order across pre-processing, the fullStream pump, and tool-sideeventWritercalls.Mastrainstance (SINGLETON) holds storage pools, vector clients, observability exporters; per-request DI flows throughagent.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.ContextCompressionNodeported to a Mastra input processor (MAX_TOKEN_COUNT).Workflows (fully wired — no stub steps)
Three workflows registered on
MastraProvider, callable viamastra.getWorkflow(id).createRun().start():generateQueryWorkflow—parallel(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).improveQueryWorkflow—load-existing → dountil(fix-query) → branch(save-improved | failed).visualizationWorkflow—select-visualisation → call-query-generation → get-dataset-data → render-visualization.Behaviour restored to v2 parity (each covered by tests):
table_not_foundreclassification — a missing-table/column error re-selects and widens the allowed table set for the next dountil iteration.get-columnsdetects 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.nodes.sqlGenerationNode.useSmartLLMForSingleTableQueries).nodes.generateChecklistNode.enabledand skips on ≤2 tables, eliding a planning LLM call per query.Bindings (BREAKING vs the pre-Mastra names)
ChatLLM/FileLLM/CheapLLM/SmartLLM/SmartNonThinkingLLM(theMastra*aliases were removed).MastraInternalBindings(Mastra,Storage,Tools,Observability,RunRegistry,ResourceId).IGraphTool,ToolStore,GetDataAsDatasetTool/ImproveDatasetTool/AskAboutDatasetTool/GenerateVisualizationTool; files*.tool.ts; LB4 keysservices.<X>Tool.MastraProvider.LLM tiers + call-time settings
Cheap / Smart / SmartNonThinking tiers wired for cost parity;
CLAUDE_THINKING(_BUDGET), per-provider temperatures, OpenRouter reasoning, andMAX_TOKEN_COUNTall threaded at call time. Providers are AI-SDK-native (@ai-sdk/*+@openrouter/ai-sdk-provider+ollama-ai-provider);create{OpenAI,OpenRouter}Modelfactories.Memory + storage
semanticRecallis opt-in (MASTRA_SEMANTIC_RECALL, default OFF) — leaving it on whenever a vector store is bound caused progressive per-request latency.generateTitlelikewise opt-in (MASTRA_GENERATE_TITLE).PostgresStorageProvider(@mastra/pg) alongside the LibSQL default; opt-inMastraVector(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 ARCChatController/ChatStore). Responses match the v2Chat/Messageshape consumers (e.g. BizBook) depend on:tenantId/userId,title(first prompt;New Chatfallback), top-levelinputTokens/outputTokens(persisted to thread metadata per run),createdOn/modifiedOn. Each Mastra message is flattened intouser/ai/toolmessages; thetoolmessage carriesmetadata.{toolName, args, existingDatasetId, status}— and for the visualization toolmetadata.{visualization, config}— so a consumer can re-run / Load Dataset / re-render a chart from history.resourceIdderivation (${tenantId}:${principalId}) is shared by the writer (WorkflowRunner) and readers (ChatController) so scopes never drift.Observability
MastraMultiObservabilityfans every agent / workflow / tool span to Langfuse and LangSmith at once (whichever env keys are present); workflow tools forward tracing context so each/replyis one root trace. Env:LANGFUSE_*,LANGSMITH_*(LangChain-style names also honoured),OTEL_SAMPLE_RATE.Developer ergonomics
debugchannel:DEBUG=ai-integration:*(orai-integration:steps) shows step progress on the server console (restores v2 step-log visibility; off by default, noconsole.*).MASTRA_STREAM_TOKENS=trueemits onemessageSSE event per text delta for progressive rendering. Default OFF preserves the single-event contract (and v2 parity — v2 never streamed). A consumer must appendmessageevents to use it; the bundled sandbox does. (The current BizBook UI renders a new bubble permessageevent, so it stays OFF until that UI appends.)lb4-llm-chat-component/testing(accuracy harness) andlb4-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
generateQueryWorkflow(AsIs / FromTemplate / Continue / unanswerable / disliked-regenerate).WorkflowRunnerunit (Init / Message / coalesced-vs-streamed tokens / Tool / ToolStatus / Error / TokenCount / Status / ResourceId).LibSQLStore(:memory:)driven bycreateMockModel; catchesfullStreamchunk-shape shifts between Mastra minor versions../testingexport) for prompt→SQL golden cases.Test plan
npm run build— clean.npm test— 372 pass, eslint + prettier clean.npm ls @langchain/langgraph— empty.LibSQLStore(:memory:)+createMockModel.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 thefullStreamswitch matches the SSE wire contract.MastraProvider(src/providers/mastra/mastra.provider.ts) — workflow + agent registration, memory/semantic-recall/title env gating.src/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.git show <sha>^:...pointers in step files for behaviour-delta review.Known follow-ups (not blocking this PR)
ApprovalController+RunRegistryconsumer) lands in v3.1 — tools emitAwaitingApprovalbut there is no resume flow yet.messageevents instead of rendering one bubble per event.