@dbx-tools/appkit-mastra 0.9.99 → 0.9.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/agents.ts CHANGED
@@ -34,6 +34,7 @@ import type {
34
34
  ToolsInput,
35
35
  } from "@mastra/core/agent";
36
36
  import { Agent } from "@mastra/core/agent";
37
+ import { createEventedAgent } from "@mastra/core/agent/durable";
37
38
  import { SkillSearchProcessor } from "@mastra/core/processors";
38
39
  import type { OutputProcessor } from "@mastra/core/processors";
39
40
  import { MASTRA_RESOURCE_ID_KEY } from "@mastra/core/request-context";
@@ -409,6 +410,41 @@ export const FALLBACK_AGENT_ID = "default";
409
410
  */
410
411
  export const DEFAULT_AGENT_MAX_STEPS = 25;
411
412
 
413
+ const EVENTED_CONSTRUCTOR_MODEL = {
414
+ specificationVersion: "v3",
415
+ provider: "dbx-tools",
416
+ modelId: "deferred",
417
+ supportedUrls: {},
418
+ async doGenerate() {
419
+ throw new Error("Deferred evented constructor model must not execute");
420
+ },
421
+ async doStream() {
422
+ throw new Error("Deferred evented constructor model must not execute");
423
+ },
424
+ } as never;
425
+
426
+ /**
427
+ * `EventedAgent` delegates model resolution to the wrapped agent at execution
428
+ * time, but its constructor also probes an untyped `__model` slot and otherwise
429
+ * calls `getModel()` with an empty request context. Temporarily filling that
430
+ * probe avoids an eager service-principal lookup while preserving the wrapped
431
+ * agent's request-scoped model resolver for real turns.
432
+ */
433
+ function createBackgroundAgent(agent: Agent, maxSteps: number): Agent {
434
+ const modelSlot = "__model";
435
+ const descriptor = Object.getOwnPropertyDescriptor(agent, modelSlot);
436
+ Object.defineProperty(agent, modelSlot, {
437
+ configurable: true,
438
+ value: EVENTED_CONSTRUCTOR_MODEL,
439
+ });
440
+ try {
441
+ return createEventedAgent({ agent, maxSteps }) as unknown as Agent;
442
+ } finally {
443
+ if (descriptor) Object.defineProperty(agent, modelSlot, descriptor);
444
+ else delete (agent as Agent & { __model?: unknown }).__model;
445
+ }
446
+ }
447
+
412
448
  const FALLBACK_AGENT_INSTRUCTIONS = `You are a data analyst. The user will ask questions about
413
449
  business metrics and may share personal preferences you should remember across turns.
414
450
 
@@ -421,6 +457,13 @@ Rules:
421
457
  3. If you don't have enough information to answer, ask a clarifying
422
458
  question instead of guessing.`;
423
459
 
460
+ /** Tool-call guidance appended to every registered agent. */
461
+ export const DEFAULT_TOOL_WORKFLOW_INSTRUCTIONS = [
462
+ "Tool workflow:",
463
+ "When the active model supports it, call multiple tools in the same turn when their inputs are independent, including multiple calls to the same tool.",
464
+ "Keep calls sequential when a later input depends on an earlier result or when calls mutate the same resource.",
465
+ ].join("\n");
466
+
424
467
  /**
425
468
  * Style guardrails appended to every agent's `instructions` to curb
426
469
  * common LLM-isms (em dashes, emojis, sycophantic openers, excessive
@@ -459,34 +502,36 @@ function resolveStyleInstructions(config: MastraPluginConfig): string | null {
459
502
  return DEFAULT_STYLE_INSTRUCTIONS;
460
503
  }
461
504
 
462
- /** Append the style block without narrowing Mastra's native instruction shapes. */
463
- function appendStyleInstructions(
505
+ /** Append shared workflow and style blocks without narrowing Mastra's instruction shapes. */
506
+ function appendDefaultInstructions(
464
507
  instructions: AgentInstructions,
465
508
  style: string | null,
466
509
  ): AgentInstructions {
467
- if (!style) return instructions;
510
+ const suffix = style
511
+ ? `${DEFAULT_TOOL_WORKFLOW_INSTRUCTIONS}\n\n${style}`
512
+ : DEFAULT_TOOL_WORKFLOW_INSTRUCTIONS;
468
513
  if (typeof instructions === "string") {
469
- return `${instructions.trimEnd()}\n\n${style}`;
514
+ return `${instructions.trimEnd()}\n\n${suffix}`;
470
515
  }
471
516
  if (Array.isArray(instructions)) {
472
517
  if (instructions.every((instruction) => typeof instruction === "string")) {
473
- return [...instructions, style] as AgentInstructions;
518
+ return [...instructions, suffix] as AgentInstructions;
474
519
  }
475
- return [...instructions, { role: "system", content: style }] as AgentInstructions;
520
+ return [...instructions, { role: "system", content: suffix }] as AgentInstructions;
476
521
  }
477
- return [instructions, { role: "system", content: style }] as AgentInstructions;
522
+ return [instructions, { role: "system", content: suffix }] as AgentInstructions;
478
523
  }
479
524
 
480
525
  /**
481
- * Compose static or request-context-aware instructions with the shared style
482
- * policy while preserving Mastra's native generic inference.
526
+ * Compose static or request-context-aware instructions with shared workflow
527
+ * and style guidance while preserving Mastra's native generic inference.
483
528
  */
484
529
  function composeInstructions<TRequestContext extends Record<string, unknown> | unknown>(
485
530
  agentInstructions: AgentConfig<string, ToolsInput, undefined, TRequestContext>["instructions"],
486
531
  style: string | null,
487
532
  ): AgentConfig<string, ToolsInput, undefined, TRequestContext>["instructions"] {
488
533
  if (typeof agentInstructions !== "function") {
489
- return appendStyleInstructions(agentInstructions, style) as AgentConfig<
534
+ return appendDefaultInstructions(agentInstructions, style) as AgentConfig<
490
535
  string,
491
536
  ToolsInput,
492
537
  undefined,
@@ -494,7 +539,7 @@ function composeInstructions<TRequestContext extends Record<string, unknown> | u
494
539
  >["instructions"];
495
540
  }
496
541
  return (async (args) =>
497
- appendStyleInstructions(await agentInstructions(args), style)) as AgentConfig<
542
+ appendDefaultInstructions(await agentInstructions(args), style)) as AgentConfig<
498
543
  string,
499
544
  ToolsInput,
500
545
  undefined,
@@ -593,7 +638,7 @@ export async function buildAgents(opts: {
593
638
  const gated = approvalGatedToolIds(tools);
594
639
  if (gated.length > 0) approvalGatedByAgent.push({ agentId: id, toolIds: gated });
595
640
  const memory = memoryBuilder?.forAgent(id, def);
596
- agents[id] = new Agent({
641
+ const agent = new Agent({
597
642
  id,
598
643
  name: def.name ?? id,
599
644
  // Always carry a non-empty description: it's surfaced to MCP
@@ -617,6 +662,10 @@ export async function buildAgents(opts: {
617
662
  inputProcessors: [...inputProcessors, ...workspaceSkillInputProcessors(workspace, config)],
618
663
  outputProcessors: [toolErrorLoggingProcessor(log)],
619
664
  });
665
+ agents[id] =
666
+ config.backgroundTurns === false
667
+ ? agent
668
+ : createBackgroundAgent(agent, config.agentMaxSteps ?? DEFAULT_AGENT_MAX_STEPS);
620
669
  // Surface the effective default model per agent so operators can
621
670
  // see at a glance which endpoint each agent points at without
622
671
  // having to fire a request and inspect a trace. The value is the
package/src/chat.ts CHANGED
@@ -10,28 +10,120 @@ import {
10
10
  isStaleMastraResumeError,
11
11
  STALE_MASTRA_RESUME_STREAM_TEXT,
12
12
  } from "@dbx-tools/shared-mastra/resume";
13
- import { chatRoute } from "@mastra/ai-sdk";
13
+ import { handleChatStream, toAISdkStream } from "@mastra/ai-sdk";
14
+ import { isEventedAgent } from "@mastra/core/agent/durable";
15
+ import { registerApiRoute } from "@mastra/core/server";
16
+ import { createUIMessageStreamResponse } from "ai";
14
17
 
15
18
  const logger = log.logger("mastra/chat");
16
19
 
17
- /** Register the official AI SDK chat stream plus its approval continuation. */
20
+ /** Client request cancellation is detached only for evented background agents. */
21
+ export function agentAbortSignal(agent: unknown, signal: AbortSignal): AbortSignal | undefined {
22
+ return isEventedAgent(agent) ? undefined : signal;
23
+ }
24
+
25
+ /** Register detached chat, durable observation, and explicit abort routes. */
18
26
  export function agentChatRoutes() {
19
- const path = `${routes.MASTRA_ROUTES.chat}/:agentId` as `${string}:agentId`;
27
+ const chatPath = `${routes.MASTRA_ROUTES.chat}/:agentId` as `${string}:agentId`;
28
+ const runPath =
29
+ `${routes.MASTRA_ROUTES.chat}/:agentId${routes.MASTRA_ROUTES.runs}/:runId` as `${string}:agentId${string}:runId`;
30
+ const abortPath =
31
+ `${runPath}${routes.MASTRA_ROUTES.abort}` as `${string}:agentId${string}:runId${string}`;
20
32
  return [
21
- chatRoute({
22
- path,
23
- sendReasoning: true,
24
- sendSources: true,
25
- onError: serializeChatStreamError,
33
+ registerApiRoute(chatPath, {
34
+ method: "POST",
35
+ handler: async (context) => {
36
+ const params = await context.req.json();
37
+ const mastra = context.get("mastra");
38
+ const agentId = context.req.param("agentId");
39
+ if (!agentId) throw new Error("Agent ID is required");
40
+ const middlewareRequestContext = context.get("requestContext");
41
+ const requestContext = middlewareRequestContext || params.requestContext;
42
+ if (middlewareRequestContext && params.requestContext) {
43
+ mastra
44
+ .getLogger()
45
+ ?.warn('Multiple "requestContext" sources provided. Using middleware context.');
46
+ }
47
+ const versionId = context.req.query("versionId");
48
+ const rawStatus = context.req.query("status");
49
+ if (versionId && rawStatus) {
50
+ throw new Error('Query parameters "versionId" and "status" are mutually exclusive');
51
+ }
52
+ if (rawStatus && rawStatus !== "draft" && rawStatus !== "published") {
53
+ throw new Error('Query parameter "status" must be "draft" or "published"');
54
+ }
55
+ const status = rawStatus as "draft" | "published" | undefined;
56
+ const abortSignal = agentAbortSignal(mastra.getAgentById(agentId), context.req.raw.signal);
57
+ const stream = await handleChatStream({
58
+ mastra,
59
+ agentId,
60
+ ...(versionId
61
+ ? { agentVersion: { versionId } }
62
+ : status
63
+ ? { agentVersion: { status } }
64
+ : {}),
65
+ params: {
66
+ ...params,
67
+ requestContext,
68
+ ...(abortSignal ? { abortSignal } : {}),
69
+ },
70
+ version: "v7",
71
+ sendReasoning: true,
72
+ sendSources: true,
73
+ onError: serializeChatStreamError,
74
+ });
75
+ return createUIMessageStreamResponse({ stream });
76
+ },
77
+ }),
78
+ registerApiRoute(runPath, {
79
+ method: "GET",
80
+ handler: async (context) => {
81
+ const mastra = context.get("mastra");
82
+ const agentId = context.req.param("agentId");
83
+ const runId = context.req.param("runId");
84
+ if (!agentId || !runId) throw new Error("Agent ID and run ID are required");
85
+ const agent = mastra.getAgentById(agentId);
86
+ if (!isEventedAgent(agent)) {
87
+ return Response.json(
88
+ { error: `Agent ${agentId} does not support background turns` },
89
+ { status: 409 },
90
+ );
91
+ }
92
+ const observed = await agent.observe(runId);
93
+ const stream = toAISdkStream(observed.output, {
94
+ from: "agent",
95
+ version: "v7",
96
+ sendReasoning: true,
97
+ sendSources: true,
98
+ onError: serializeChatStreamError,
99
+ });
100
+ return createUIMessageStreamResponse({ stream });
101
+ },
102
+ }),
103
+ registerApiRoute(abortPath, {
104
+ method: "POST",
105
+ handler: async (context) => {
106
+ const mastra = context.get("mastra");
107
+ const agentId = context.req.param("agentId");
108
+ const runId = context.req.param("runId");
109
+ if (!agentId || !runId) throw new Error("Agent ID and run ID are required");
110
+ const agent = mastra.getAgentById(agentId);
111
+ if (!isEventedAgent(agent)) {
112
+ return Response.json(
113
+ { error: `Agent ${agentId} does not support background turns` },
114
+ { status: 409 },
115
+ );
116
+ }
117
+ return Response.json({ aborted: agent.abortRunStream(runId) });
118
+ },
26
119
  }),
27
120
  ];
28
121
  }
29
122
 
30
123
  /**
31
124
  * Keep a late or duplicate resume from failing the UI stream. Mastra's
32
- * `chatRoute` already swallows a couple of resume ids on the v6 approval
33
- * path; this covers the generic workflow errors `consumeStream` still
34
- * surfaces on v5.
125
+ * resume path can surface a workflow error after another client already
126
+ * resolved the same approval.
35
127
  */
36
128
  function serializeChatStreamError(error: unknown): string {
37
129
  if (isStaleMastraResumeError(error)) {
package/src/config.ts CHANGED
@@ -199,7 +199,13 @@ export interface MastraMcpConfig {
199
199
 
200
200
  /** Configuration accepted by the Mastra AppKit plugin. */
201
201
  export interface MastraPluginConfig extends BasePluginConfig {
202
- /** Mastra OpenAI-compatible provider id. Defaults to `"databricks"`; no env fallback. */
202
+ /**
203
+ * Run turns through Mastra's evented durable agent so execution continues
204
+ * after a browser disconnect and can be observed again by run id. Defaults
205
+ * to `true`.
206
+ */
207
+ backgroundTurns?: boolean;
208
+ /** AI SDK provider-options namespace. Defaults to `"openai"`; no env fallback. */
203
209
  providerId?: string;
204
210
  /**
205
211
  * PostgresStore for Mastra threads/messages. `true` reuses the
@@ -641,9 +647,14 @@ export interface MastraPluginConfig extends BasePluginConfig {
641
647
  export const MASTRA_CONFIG_SCHEMA: ConfigSchema = {
642
648
  type: "object",
643
649
  properties: {
650
+ backgroundTurns: {
651
+ type: "boolean",
652
+ description:
653
+ "Run turns through Mastra evented durable execution so they continue after browser disconnects and remain reconnectable by run id. Defaults to true.",
654
+ },
644
655
  providerId: {
645
656
  type: "string",
646
- description: 'Mastra OpenAI-compatible provider id. Defaults to "databricks".',
657
+ description: 'AI SDK provider-options namespace. Defaults to "openai".',
647
658
  },
648
659
  remoteSkills: {
649
660
  type: ["string", "array", "object"],
package/src/model.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Each agent step calls {@link buildModel} with the active
5
5
  * `RequestContext`. The user stamped by `MastraServer` carries an
6
6
  * AppKit `WorkspaceClient`; we ask it for the workspace host and a
7
- * fresh bearer header, then point Mastra's OpenAI-compatible provider
8
- * at `/serving-endpoints` on that host.
7
+ * fresh bearer header, then construct a request-scoped Vercel AI SDK provider
8
+ * for the endpoint's native Databricks serving protocol.
9
9
  *
10
10
  * This module only adds the Mastra-specific glue. The actual model
11
11
  * selection - listing the workspace catalogue and resolving an
@@ -16,26 +16,21 @@
16
16
  * per-request override under {@link MASTRA_MODEL_OVERRIDE_KEY}, the
17
17
  * agent / plugin `modelId`, or `DATABRICKS_SERVING_ENDPOINT_NAME`),
18
18
  * pass the plugin's fuzzy / class / fallback knobs through, and wrap
19
- * the resolved id in the OpenAI-compatible provider config Mastra
20
- * expects. Catalogue fetches fail loud: network / auth errors
19
+ * the resolved id in the ProviderV4 model Mastra accepts. Catalogue fetches fail loud: network / auth errors
21
20
  * propagate so callers see the real SDK message.
22
21
  *
23
22
  * @module
24
23
  */
25
24
 
26
25
  import { getExecutionContext } from "@databricks/appkit";
27
- import { classes, invoke, policy, resolve } from "@dbx-tools/model";
28
- import { functionUtils, json, log, net } from "@dbx-tools/shared-core";
26
+ import { classes, policy, resolve } from "@dbx-tools/model";
27
+ import { createDatabricksLanguageModel } from "@dbx-tools/model-protocol/provider";
28
+ import { log } from "@dbx-tools/shared-core";
29
29
  import { model, type ServingEndpointSummary } from "@dbx-tools/shared-model";
30
30
  import type { MastraModelConfig } from "@mastra/core/llm";
31
31
  import type { RequestContext } from "@mastra/core/request-context";
32
32
 
33
33
  import { MASTRA_USER_KEY, type MastraPluginConfig, type User } from "./config.ts";
34
- import {
35
- rewriteServingRequest,
36
- rewriteServingResponseBody,
37
- rewriteServingResponseStream,
38
- } from "./serving-sanitize.ts";
39
34
  import {
40
35
  MASTRA_MODEL_OVERRIDE_KEY,
41
36
  MASTRA_RESOLVED_MODEL_KEY,
@@ -133,7 +128,6 @@ export async function buildModel(
133
128
  requestContext: RequestContext,
134
129
  overrides: BuildModelOverrides = {},
135
130
  ): Promise<MastraModelConfig> {
136
- void setupFetchInterceptor();
137
131
  // The chat path stamps the AppKit user on the request context via
138
132
  // `MastraServer`. The MCP transport routes don't thread that context
139
133
  // into tool execution, so fall back to the ambient execution context
@@ -145,11 +139,6 @@ export async function buildModel(
145
139
  const host = (await clientConfig.getHost()).toString();
146
140
  const headers = new Headers();
147
141
  await clientConfig.authenticate(headers);
148
- // The OpenAI Node SDK appends paths like `/chat/completions` to whatever
149
- // URL we hand it. Drop the trailing slash so the resulting URL stays
150
- // well-formed (`/serving-endpoints/chat/completions`).
151
- const url = new URL("/serving-endpoints", host).toString().replace(/\/$/, "");
152
-
153
142
  const logger = log.logger(config);
154
143
  const serving = resolveServingConfig(config);
155
144
  const override = serving.allowOverride
@@ -174,98 +163,11 @@ export async function buildModel(
174
163
  requestContext.set(MASTRA_RESOLVED_MODEL_KEY, modelId);
175
164
  recordActiveTraceModel(modelId);
176
165
 
177
- return {
178
- providerId: config.providerId ?? "databricks",
166
+ return createDatabricksLanguageModel({
179
167
  modelId,
180
- url,
168
+ protocol: servingApi(modelId),
169
+ host,
181
170
  headers: Object.fromEntries(headers.entries()),
182
- api: servingApi(modelId),
183
- };
184
- }
185
-
186
- /** Chat Completions route whose provider-specific wire shapes are normalized here. */
187
- const CHAT_COMPLETIONS_PATH = `/${invoke.CHAT_COMPLETIONS_PATH}`;
188
-
189
- /**
190
- * Install a single shared `globalThis.fetch` wrapper for Chat Completions
191
- * requests. The wrapper does three things:
192
- *
193
- * 1. Rewrites outgoing JSON from either `init.body` or a `Request` to repair
194
- * provider-specific request constraints (see {@link rewriteServingRequest}
195
- * in `./serving-sanitize.js`).
196
- * 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
197
- * 4xx debugging doesn't have to fight AI SDK's `[Array]`
198
- * formatter.
199
- * 3. Repairs buffered and streaming responses where a provider returns
200
- * `choices[].message.content` or `choices[].delta.content` as a parts
201
- * array that the AI SDK's OpenAI schema rejects.
202
- *
203
- * Safe to call from any hot path: {@link functionUtils.memoize} ensures
204
- * the wrapper is installed at most once per process, so subsequent
205
- * calls are a no-op even when {@link buildModel} fires on every agent
206
- * step.
207
- */
208
- const setupFetchInterceptor = functionUtils.memoize((): void => {
209
- globalThis.fetch = createServingFetchInterceptor(globalThis.fetch.bind(globalThis));
210
- });
211
-
212
- /** Build the serving fetch wrapper; exported for transport-level regression tests. */
213
- export function createServingFetchInterceptor(original: typeof fetch): typeof fetch {
214
- const logger = log.logger("mastra/llm");
215
- return (async (input, init) => {
216
- const url = net.urlBuilder(input);
217
- const method = init?.method ?? (input instanceof Request ? input.method : "GET");
218
- if (!url || url.pathname !== CHAT_COMPLETIONS_PATH || method.toUpperCase() !== "POST") {
219
- return original(input, init);
220
- }
221
- const rewritten = await rewriteServingRequest(input, init);
222
- const parsed = json.parse<unknown>(rewritten.body);
223
- logger.debug(
224
- "POST",
225
- parsed === undefined
226
- ? { url: url.toString(), bodyType: "non-JSON" }
227
- : { url: url.toString(), body: parsed },
228
- );
229
- const response = await original(rewritten.input, rewritten.init);
230
- return repairServingResponse(response);
231
- }) as typeof globalThis.fetch;
232
- }
233
-
234
- /**
235
- * Rewrite a serving response whose body needs repair, leaving unsupported
236
- * content types byte-identical.
237
- *
238
- * SSE remains a live stream: a TransformStream repairs complete `data:` lines
239
- * as bytes arrive and preserves partial lines across network chunks. JSON is
240
- * buffered through the existing response-body repair. A non-JSON/non-SSE body
241
- * is returned untouched so the caller still sees the original response.
242
- */
243
- async function repairServingResponse(response: Response): Promise<Response> {
244
- const contentType = response.headers.get("content-type") ?? "";
245
- if (contentType.includes("text/event-stream") && response.body) {
246
- return rebuildServingResponse(response, rewriteServingResponseStream(response.body));
247
- }
248
- if (!contentType.includes("application/json")) return response;
249
-
250
- const body = await response.clone().text();
251
- const rewritten = rewriteServingResponseBody(body);
252
- if (rewritten === body) return response;
253
- return rebuildServingResponse(response, rewritten);
254
- }
255
-
256
- /** Rebuild a transformed response without stale byte-length/encoding headers. */
257
- function rebuildServingResponse(
258
- response: Response,
259
- body: string | ReadableStream<Uint8Array>,
260
- ): Response {
261
- // `content-length` / `content-encoding` describe the ORIGINAL bytes, so they
262
- // are dropped: the rewritten body is a different length and already decoded.
263
- const headers = new Headers(response.headers);
264
- headers.delete("content-length");
265
- headers.delete("content-encoding");
266
- return new Response(body, {
267
- status: response.status,
268
- statusText: response.statusText,
269
- headers,
171
+ providerName: config.providerId ?? "openai",
270
172
  });
271
173
  }
@@ -9,15 +9,16 @@
9
9
  * @module
10
10
  */
11
11
 
12
+ import { availableParallelism } from "node:os";
12
13
  import { configUtils } from "@dbx-tools/core";
13
14
  import { errorUtils, functionUtils, log } from "@dbx-tools/shared-core";
14
- import { availableParallelism } from "node:os";
15
- import type {
16
- CommandResult,
17
- ExecuteCommandOptions,
18
- SandboxInfo,
15
+ import {
16
+ MastraSandbox,
17
+ type CommandResult,
18
+ type ExecuteCommandOptions,
19
+ type ProviderStatus,
20
+ type SandboxInfo,
19
21
  } from "@mastra/core/workspace";
20
- import { MastraSandbox, type ProviderStatus } from "@mastra/core/workspace";
21
22
  import type { CheckoutOptions, Monty, MontyCrashedError, MontySession } from "@pydantic/monty";
22
23
 
23
24
  const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
package/src/plugin.ts CHANGED
@@ -458,6 +458,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
458
458
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
459
459
  agents: Object.keys(this.built?.agents ?? {}),
460
460
  feedbackEnabled: this.feedbackEnabled(),
461
+ backgroundTurns: this.config.backgroundTurns !== false,
461
462
  // In `service-principal` mode the chat works for any caller who can open
462
463
  // the app, so a UI that hides chat behind an OBO-token probe should show
463
464
  // it regardless. `"user"` mode keeps the OBO-gated behavior.
@@ -1190,6 +1191,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1190
1191
  this.logger.info("ready", {
1191
1192
  agents: Object.keys(this.built.agents),
1192
1193
  defaultAgent: this.built.defaultAgentId,
1194
+ backgroundTurns: this.config.backgroundTurns !== false,
1193
1195
  apiAccess: this.config.apiAccess ?? "scoped",
1194
1196
  mcp: this.mcp ? `${this.basePath}${this.mcp.httpPath}` : "off",
1195
1197
  lakebase: memoryBuilder !== undefined,