@mastra/core
Version:
1 lines • 437 kB
Source Map (JSON)
{"version":3,"file":"agent-controller-Y3190eVv.cjs","names":["getTransformedToolPayload","hasTransformedToolPayload","isRecord","getServerSideFallbackInfo","addOptionalUsageField","#session","#machinery","#requestContext","getTransformedToolPayload","hasTransformedToolPayload","getErrorFromUnknown","#defaultResourceId","#id","#ownerId","#resourceId","#getResourceId","#store","#session","#owner","#threadId","#cloneThread","#requireOwnedThread","#teardownWaiters","#key","#subscription","#notifyTeardown","#pending","#queue","#toolName","#toolCallId","#resolve","#runId","#traceId","#abortController","#abortRequested","#operationId","#bus","#getCurrentModeId","#trackModelUse","#model","#resolveMode","#switchVersion","#config","#getState","#setState","#setSetting","#omConfig","#gateways","ModelRouterLanguageModel","#schema","#state","#updateQueue","safeStringify","#listeners","#displayState","#lastWorkspaceEvents","#dispatch","#grantedCategories","#grantedTools","#tags","#workspace","#resolveSubagentName","Workspace","#resolveCategory","#machinery","#engine","createSignal","getErrorFromUnknown","#tokenUsage","createTool","z","RequestContext","CHAT_CHANNEL_RENDER_CONTEXT_KEY","Agent","createWorkspaceTools","summarizeTaskCheck","#defaultMode","#sessionsByResource","#sessionScopes","#instructions","#channels","AgentControllerChannels","#gatewayManager","GatewayManager","defaultGateways","#createSessionForResource","RequestContext","#wireSession","Workspace","getErrorFromUnknown","#externalMastra","#internalMastra","Mastra","#resolveStorage","#legacyAgentMode","Agent","#dropSessionFromRegistry","askUserTool","submitPlanTool","taskWriteTool","taskUpdateTool","taskCompleteTool","taskCheckTool"],"sources":["../src/agent-controller/stream-content.ts","../src/agent-controller/session-run-engine.ts","../src/agent-controller/types.ts","../src/agent-controller/session.ts","../src/agent-controller/tools.ts","../src/agent-controller/agent-controller.ts"],"sourcesContent":["import { getServerSideFallbackInfo } from '../llm/model/server-side-fallback';\nimport { getTransformedToolPayload, hasTransformedToolPayload } from '../tools/payload-transform';\nimport type { ToolPayloadTransformPhase } from '../tools/types';\nimport type { TokenUsage } from './types';\n\n/**\n * Pure transforms shared by the run engine for folding raw agent-stream chunk\n * payloads into the `MastraDBMessage` shape a Session renders, plus terminal\n * finish-reason and server-side-fallback diagnostics. They hold no\n * AgentController or Session state, so they live in their own module.\n */\n\nexport function getDisplayTransform(metadata: unknown, phase: ToolPayloadTransformPhase, fallback: unknown) {\n const transform = getTransformedToolPayload(metadata, 'display', phase);\n return hasTransformedToolPayload(transform) ? transform.transformed : fallback;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction getAnthropicStopDetails(providerMetadata: unknown): Record<string, unknown> | undefined {\n if (!isRecord(providerMetadata)) {\n return undefined;\n }\n const anthropic = providerMetadata.anthropic;\n if (!isRecord(anthropic)) {\n return undefined;\n }\n const stopDetails = anthropic.stopDetails;\n return isRecord(stopDetails) ? stopDetails : undefined;\n}\n\n/**\n * Map a non-success terminal finish reason (content-filter, error, length) to a\n * user-facing message, or `undefined` for success reasons. A non-success finish\n * must become an explicit terminal error rather than a silent `complete`.\n */\nexport function describeNonSuccessFinishReason(reason: string, providerMetadata: unknown): string | undefined {\n switch (reason) {\n case 'content-filter': {\n const stopDetails = getAnthropicStopDetails(providerMetadata);\n const explanation =\n stopDetails && typeof stopDetails.explanation === 'string' ? stopDetails.explanation : undefined;\n const category = stopDetails && typeof stopDetails.category === 'string' ? stopDetails.category : undefined;\n const detail = explanation ?? (category ? `category: ${category}` : undefined);\n return detail ? `The model stopped on a content filter (${detail}).` : 'The model stopped on a content filter.';\n }\n case 'error':\n return 'The model stream ended with an error before producing a final response.';\n case 'length':\n return 'The model stopped because it reached its maximum output length before finishing.';\n default:\n return undefined;\n }\n}\n\n/**\n * Build a user-facing notice when a turn was served by an Anthropic server-side\n * fallback model instead of the primary model. Without a notice the user has no\n * way to tell the response did not come from the model they selected.\n */\nexport function describeServerSideFallback(providerMetadata: unknown): string | undefined {\n const fallback = getServerSideFallbackInfo(providerMetadata);\n if (!fallback) {\n return undefined;\n }\n return fallback.model\n ? `The selected model declined this turn; the response was generated by fallback model ${fallback.model}.`\n : 'The selected model declined this turn; the response was generated by a fallback model.';\n}\n\n/** Coerce a usage field to a finite number, accepting numeric strings. */\nexport function getUsageNumber(usage: Record<string, unknown>, key: string): number | undefined {\n const value = usage[key];\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === 'string' && value.trim() !== '') {\n const numericValue = Number(value);\n if (Number.isFinite(numericValue)) {\n return numericValue;\n }\n }\n return undefined;\n}\n\n/** Fold an optional usage field into a tally when present. */\nexport function addOptionalUsageField(\n usage: TokenUsage,\n key: keyof Pick<TokenUsage, 'reasoningTokens' | 'cachedInputTokens' | 'cacheCreationInputTokens'>,\n value: number | undefined,\n): void {\n if (value !== undefined) {\n usage[key] = (usage[key] ?? 0) + value;\n }\n}\n","import type {\n MastraDBMessage,\n MastraMessagePart,\n MastraProviderMetadata,\n MastraToolInvocationPart,\n} from '../agent/message-list/state/types';\nimport type { AgentThreadSubscription } from '../agent/types';\nimport { getErrorFromUnknown } from '../error';\nimport type { RequestContext } from '../request-context';\nimport type { GoalEvaluationPayload } from '../stream/types';\nimport { getTransformedToolPayload, hasTransformedToolPayload } from '../tools/payload-transform';\nimport type { Session, SessionMachinery } from './session';\nimport {\n addOptionalUsageField,\n describeNonSuccessFinishReason,\n describeServerSideFallback,\n getDisplayTransform,\n getUsageNumber,\n} from './stream-content';\nimport type { TokenUsage } from './types';\n\n/**\n * The transient state of a single in-flight agent stream: the assistant message\n * being assembled, content indices for streaming deltas, and suspend/terminal\n * flags. One per run; recreated per run within a subscribed thread stream.\n */\ntype StreamDataPart = MastraMessagePart & { type: `data-${string}`; data: unknown };\ntype StreamChunkPayload = Record<string, unknown>;\ntype StreamChunkBase<TType extends string> = {\n type: TType;\n runId?: string | null;\n metadata?: unknown;\n};\ntype StreamPayloadChunk<TType extends string> = StreamChunkBase<TType> & { payload?: unknown };\ntype StreamObjectChunk<TType extends string> = StreamChunkBase<TType> & { object?: unknown };\ntype StreamDataChunk<TType extends `data-${string}`> = StreamChunkBase<TType> & { data?: unknown };\ntype StreamIgnoredChunk =\n | StreamPayloadChunk<'start'>\n | StreamPayloadChunk<'abort'>\n | StreamPayloadChunk<'response-metadata'>\n | StreamPayloadChunk<'text-end'>\n | StreamPayloadChunk<'reasoning-end'>\n | StreamPayloadChunk<'reasoning-signature'>\n | StreamPayloadChunk<'redacted-reasoning'>\n | StreamPayloadChunk<'source'>\n | StreamPayloadChunk<'file'>\n | StreamPayloadChunk<'raw'>\n | StreamPayloadChunk<'step-start'>\n | StreamPayloadChunk<'tool-output'>\n | StreamPayloadChunk<'step-output'>\n | StreamPayloadChunk<'watch'>\n | StreamPayloadChunk<'tripwire'>\n | StreamPayloadChunk<'is-task-complete'>\n | StreamPayloadChunk<'background-task-started'>\n | StreamPayloadChunk<'background-task-completed'>\n | StreamPayloadChunk<'background-task-failed'>\n | StreamPayloadChunk<'background-task-progress'>\n | StreamPayloadChunk<'background-task-running'>\n | StreamPayloadChunk<'background-task-cancelled'>\n | StreamPayloadChunk<'background-task-output'>\n | StreamPayloadChunk<'background-task-suspended'>\n | StreamPayloadChunk<'background-task-resumed'>\n | StreamObjectChunk<'object'>\n | StreamObjectChunk<'object-result'>;\ntype StreamChunk =\n | StreamIgnoredChunk\n | StreamPayloadChunk<'text-start'>\n | StreamPayloadChunk<'text-delta'>\n | StreamPayloadChunk<'reasoning-start'>\n | StreamPayloadChunk<'reasoning-delta'>\n | StreamPayloadChunk<'tool-call-input-streaming-start'>\n | StreamPayloadChunk<'tool-call-delta'>\n | StreamPayloadChunk<'tool-call-input-streaming-end'>\n | StreamPayloadChunk<'tool-call'>\n | StreamPayloadChunk<'tool-result'>\n | StreamPayloadChunk<'tool-error'>\n | StreamPayloadChunk<'tool-call-approval'>\n | StreamPayloadChunk<'tool-call-suspended'>\n | StreamPayloadChunk<'error'>\n | StreamPayloadChunk<'step-finish'>\n | StreamPayloadChunk<'finish'>\n | StreamPayloadChunk<'goal'>\n | StreamDataChunk<'data-om-status'>\n | StreamDataChunk<'data-om-observation-start'>\n | StreamDataChunk<'data-om-observation-end'>\n | StreamDataChunk<'data-om-observation-failed'>\n | StreamDataChunk<'data-om-buffering-start'>\n | StreamDataChunk<'data-om-buffering-end'>\n | StreamDataChunk<'data-om-buffering-failed'>\n | StreamDataChunk<'data-signal'>\n | StreamDataChunk<'data-user-message'>\n | StreamDataChunk<'data-system-reminder'>\n | StreamDataChunk<'data-om-activation'>\n | StreamDataChunk<'data-om-thread-update'>\n | StreamDataChunk<'data-mastracode-tool-progress'>\n | StreamDataChunk<'data-sandbox-stdout'>\n | StreamDataChunk<'data-sandbox-stderr'>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isProviderMetadata(value: unknown): value is MastraProviderMetadata {\n return isRecord(value);\n}\n\nfunction getString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction getNumber(value: unknown, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : fallback;\n}\n\nfunction getOptionalNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction getBoolean(value: unknown, fallback: boolean): boolean {\n return typeof value === 'boolean' ? value : fallback;\n}\n\nfunction getRecord(value: unknown): Record<string, unknown> | undefined {\n return isRecord(value) ? value : undefined;\n}\n\nfunction getPayload(chunk: StreamChunk): StreamChunkPayload {\n return 'payload' in chunk ? (getRecord(chunk.payload) ?? {}) : {};\n}\n\nfunction getDataRecord(chunk: StreamChunk): Record<string, unknown> | undefined {\n return 'data' in chunk ? getRecord(chunk.data) : undefined;\n}\n\nfunction getNestedRecord(\n record: Record<string, unknown> | undefined,\n key: string,\n): Record<string, unknown> | undefined {\n return record ? getRecord(record[key]) : undefined;\n}\n\nfunction isGoalEvaluationPayload(value: unknown): value is GoalEvaluationPayload {\n const record = getRecord(value);\n return Boolean(\n record &&\n typeof record.objective === 'string' &&\n typeof record.iteration === 'number' &&\n typeof record.maxRuns === 'number' &&\n typeof record.passed === 'boolean' &&\n (record.status === 'active' || record.status === 'paused' || record.status === 'done') &&\n Array.isArray(record.results) &&\n typeof record.duration === 'number' &&\n typeof record.timedOut === 'boolean' &&\n typeof record.maxRunsReached === 'boolean' &&\n typeof record.suppressFeedback === 'boolean',\n );\n}\n\nfunction getOperationType(value: unknown): 'observation' | 'reflection' {\n return value === 'reflection' ? 'reflection' : 'observation';\n}\n\nfunction getActivationTrigger(value: unknown): 'ttl' | 'threshold' | 'provider_change' | undefined {\n if (value === 'ttl' || value === 'threshold' || value === 'provider_change') return value;\n return undefined;\n}\n\nfunction getOmStatus(value: unknown): 'idle' | 'running' | 'complete' {\n if (value === 'running' || value === 'complete') return value;\n return 'idle';\n}\n\nfunction formatToolProgressOutput(progress: unknown): string {\n if (typeof progress === 'string') return progress.endsWith('\\n') ? progress : `${progress}\\n`;\n if (typeof progress !== 'object' || progress === null) return `${String(progress)}\\n`;\n\n const record = progress as { status?: unknown; detail?: unknown };\n const parts = [record.status, record.detail].filter(\n (part): part is string => typeof part === 'string' && part.length > 0,\n );\n return parts.length > 0 ? `${parts.join(': ')}\\n` : `${JSON.stringify(progress)}\\n`;\n}\n\ntype StreamState = {\n currentMessage: MastraDBMessage;\n lastFinishedMessage?: MastraDBMessage;\n isSuspended: boolean;\n textContentById: Map<string, { index: number; text: string }>;\n thinkingContentById: Map<string, { index: number; text: string }>;\n toolPartById: Map<string, number>;\n /**\n * Set when a stream ends on a non-success finish reason (e.g. `content-filter`,\n * `error`, `length`). Carries the user-facing message so the run finalizes\n * into an explicit terminal error state instead of silently completing.\n */\n terminalError?: string;\n};\n\n/**\n * The per-session agent run engine: it consumes an agent's event stream, folds\n * each chunk into the session's display messages and token usage, drives tool\n * approval/suspension, and finalizes the run. In the multi-user host the run\n * loop, run state, and thread stream are per-session and cannot be shared, so\n * they live on the Session — this engine is owned by exactly one Session.\n *\n * It reaches the host only through the narrow {@link SessionMachinery} it is\n * constructed with (resolve the agent, build run/stream options, persist usage,\n * drive tool approval/resume, drain follow-ups). It never reaches back into the\n * AgentController or another session: all per-run state is read and written on its own\n * {@link Session}.\n */\nexport class SessionRunEngine {\n readonly #session: Session;\n readonly #machinery: SessionMachinery;\n #requestContext?: RequestContext;\n\n constructor(session: Session, machinery: SessionMachinery) {\n this.#session = session;\n this.#machinery = machinery;\n }\n\n setRequestContext(requestContext?: RequestContext): void {\n if (requestContext) this.#requestContext = requestContext;\n }\n\n private createEmptyAssistantMessage(): MastraDBMessage {\n return {\n id: this.#machinery.generateId(),\n role: 'assistant',\n content: { format: 2, parts: [] },\n createdAt: new Date(),\n };\n }\n\n /**\n * Build a DB-native signal message from a streamed `data-signal` /\n * `data-user-message` / `data-system-reminder` chunk. The raw data-part is\n * carried verbatim on `content.parts` and the signal identity is preserved on\n * `content.metadata.signal` so consumers read the native shape (no flattening).\n */\n private createSignalMessage(partType: `data-${string}`, payload: Record<string, unknown>): MastraDBMessage {\n const part: StreamDataPart = { type: partType, data: payload };\n const signalId = typeof payload.id === 'string' ? payload.id : this.#machinery.generateId();\n const createdAt =\n typeof payload.createdAt === 'string' && !Number.isNaN(Date.parse(payload.createdAt))\n ? new Date(payload.createdAt)\n : new Date();\n return {\n id: signalId,\n role: 'signal',\n content: {\n format: 2,\n parts: [part],\n metadata: { signal: payload },\n },\n createdAt,\n };\n }\n\n private hasCurrentMessageContent(state: StreamState): boolean {\n return state.currentMessage.content.parts.length > 0;\n }\n\n /**\n * Snapshot a message for emission. The engine mutates parts in place\n * (text/reasoning deltas, tool-invocation upgrades) and `setStopReason` /\n * `setErrorMessage` mutate `content.metadata`, so emitted snapshots must\n * deep-clone the content or later mutations rewrite earlier snapshots.\n */\n private cloneMessage(message: MastraDBMessage): MastraDBMessage {\n return { ...message, content: structuredClone(message.content) };\n }\n\n private setStopReason(message: MastraDBMessage, stopReason: string, force = false): void {\n message.content.metadata ??= {};\n const metadata = message.content.metadata;\n if (force) {\n metadata.stopReason = stopReason;\n } else {\n metadata.stopReason ??= stopReason;\n }\n }\n\n private setErrorMessage(message: MastraDBMessage, errorMessage: string): void {\n message.content.metadata ??= {};\n message.content.metadata.errorMessage = errorMessage;\n }\n\n private finishCurrentMessageAndRotate(state: StreamState): void {\n if (!this.hasCurrentMessageContent(state)) return;\n this.setStopReason(state.currentMessage, 'complete');\n this.#session.emit({ type: 'message_end', message: state.currentMessage });\n state.lastFinishedMessage = state.currentMessage;\n state.currentMessage = this.createEmptyAssistantMessage();\n state.textContentById.clear();\n state.thinkingContentById.clear();\n state.toolPartById.clear();\n }\n\n createStreamState(): StreamState {\n return {\n currentMessage: this.createEmptyAssistantMessage(),\n isSuspended: false,\n textContentById: new Map<string, { index: number; text: string }>(),\n thinkingContentById: new Map<string, { index: number; text: string }>(),\n toolPartById: new Map<string, number>(),\n };\n }\n\n private abortForOmFailure({ operationType, stage, error }: { operationType: string; stage: string; error: string }) {\n this.#session.emit({\n type: 'error',\n error: new Error(`Observational memory ${operationType} ${stage} failed: ${error}`),\n });\n this.#session.abortRun();\n }\n\n /**\n * Process a stream response (shared between sendMessage and tool approval).\n */\n async processStream(\n response: { fullStream: AsyncIterable<StreamChunk> },\n requestContextInput?: RequestContext,\n ): Promise<{ message: MastraDBMessage; suspended?: boolean } | undefined> {\n const state = this.createStreamState();\n const requestContext = await this.#machinery.buildRequestContext(requestContextInput);\n this.#session.run.nextOperation();\n this.#session.emit({ type: 'agent_start' });\n\n let result: { message: MastraDBMessage; suspended?: boolean } | undefined;\n let error = false;\n let aborted = false;\n\n for await (const chunk of response.fullStream) {\n result = await this.processStreamChunk(state, chunk, requestContext);\n if (chunk.type === 'error') {\n error = true;\n }\n if (chunk.type === 'abort') {\n aborted = true;\n }\n if (\n result ||\n chunk.type === 'finish' ||\n chunk.type === 'error' ||\n chunk.type === 'abort' ||\n chunk.type === 'tool-call-suspended' ||\n this.#session.run.isAbortRequested()\n ) {\n result ??= this.finishStreamState(state);\n break;\n }\n }\n\n result ??= this.finishStreamState(state);\n\n // A non-success terminal finish reason (e.g. a `claude-fable-5`\n // content-filter refusal) becomes an explicit error so the run never\n // silently stops without a visible terminal state.\n if (state.terminalError && !error && !aborted && !this.#session.run.isAbortRequested() && !result.suspended) {\n error = true;\n this.#session.emit({ type: 'error', error: new Error(state.terminalError) });\n }\n\n this.#session.emit({\n type: 'agent_end',\n reason: error\n ? 'error'\n : result.suspended\n ? 'suspended'\n : aborted || this.#session.run.isAbortRequested()\n ? 'aborted'\n : 'complete',\n });\n\n this.#session.run.reset();\n await this.#session.drainFollowUpQueue();\n\n return result;\n }\n\n async processStreamChunk(\n state: StreamState,\n chunk: StreamChunk,\n requestContext: RequestContext,\n ): Promise<{ message: MastraDBMessage; suspended?: boolean } | undefined> {\n if ('runId' in chunk && chunk.runId) {\n this.#session.run.setRunId({ runId: chunk.runId });\n }\n\n switch (chunk.type) {\n case 'text-start': {\n const textIndex = state.currentMessage.content.parts.length;\n state.currentMessage.content.parts.push({ type: 'text', text: '' });\n state.textContentById.set(getString(getPayload(chunk).id) ?? '', { index: textIndex, text: '' });\n this.#session.emit({ type: 'message_start', message: this.cloneMessage(state.currentMessage) });\n break;\n }\n\n case 'text-delta': {\n const textState = state.textContentById.get(getString(getPayload(chunk).id) ?? '');\n if (textState) {\n textState.text += getString(getPayload(chunk).text) ?? '';\n const textContent = state.currentMessage.content.parts[textState.index];\n if (textContent && textContent.type === 'text') {\n textContent.text = textState.text;\n }\n this.#session.emit({ type: 'message_update', message: this.cloneMessage(state.currentMessage) });\n }\n break;\n }\n\n case 'reasoning-start': {\n const thinkingIndex = state.currentMessage.content.parts.length;\n state.currentMessage.content.parts.push({ type: 'reasoning', reasoning: '', details: [] });\n state.thinkingContentById.set(getString(getPayload(chunk).id) ?? '', { index: thinkingIndex, text: '' });\n this.#session.emit({ type: 'message_update', message: this.cloneMessage(state.currentMessage) });\n break;\n }\n\n case 'reasoning-delta': {\n const thinkingState = state.thinkingContentById.get(getString(getPayload(chunk).id) ?? '');\n if (thinkingState) {\n thinkingState.text += getString(getPayload(chunk).text) ?? '';\n const thinkingContent = state.currentMessage.content.parts[thinkingState.index];\n if (thinkingContent && thinkingContent.type === 'reasoning') {\n thinkingContent.reasoning = thinkingState.text;\n thinkingContent.details = [{ type: 'text', text: thinkingState.text }];\n }\n this.#session.emit({ type: 'message_update', message: this.cloneMessage(state.currentMessage) });\n }\n break;\n }\n\n case 'tool-call-input-streaming-start': {\n const payload = getPayload(chunk);\n const toolCallId = getString(payload.toolCallId) ?? '';\n const toolName = getString(payload.toolName) ?? '';\n this.#session.emit({ type: 'tool_input_start', toolCallId, toolName });\n break;\n }\n\n case 'tool-call-delta': {\n const payload = getPayload(chunk);\n const toolCallId = getString(payload.toolCallId) ?? '';\n const argsTextDelta = getString(payload.argsTextDelta) ?? '';\n const toolName = getString(payload.toolName);\n const transform = getTransformedToolPayload(chunk.metadata, 'display', 'input-delta');\n if (!transform?.suppress) {\n this.#session.emit({\n type: 'tool_input_delta',\n toolCallId,\n argsTextDelta: hasTransformedToolPayload(transform) ? transform.transformed : argsTextDelta,\n toolName,\n });\n }\n break;\n }\n\n case 'tool-call-input-streaming-end': {\n const toolCallId = getString(getPayload(chunk).toolCallId) ?? '';\n this.#session.emit({ type: 'tool_input_end', toolCallId });\n break;\n }\n\n case 'tool-call': {\n const toolCall = getPayload(chunk);\n const toolCallId = getString(toolCall.toolCallId) ?? '';\n const toolName = getString(toolCall.toolName) ?? '';\n const args = getDisplayTransform(chunk.metadata, 'input-available', toolCall.args);\n const toolIndex = state.currentMessage.content.parts.length;\n state.currentMessage.content.parts.push({\n type: 'tool-invocation',\n toolInvocation: {\n state: 'call',\n toolCallId,\n toolName,\n args,\n },\n });\n state.toolPartById.set(toolCallId, toolIndex);\n this.#session.emit({\n type: 'tool_start',\n toolCallId,\n toolName,\n args,\n });\n this.#session.emit({ type: 'message_update', message: this.cloneMessage(state.currentMessage) });\n break;\n }\n\n case 'tool-result': {\n const toolResult = getPayload(chunk);\n const toolCallId = getString(toolResult.toolCallId) ?? '';\n const toolName = getString(toolResult.toolName) ?? '';\n const providerMetadata = isProviderMetadata(toolResult.providerMetadata)\n ? toolResult.providerMetadata\n : undefined;\n const result = getDisplayTransform(chunk.metadata, 'output-available', toolResult.result);\n const isError = getBoolean(toolResult.isError, false);\n const toolIndex = state.toolPartById.get(toolCallId);\n const existing = toolIndex !== undefined ? state.currentMessage.content.parts[toolIndex] : undefined;\n if (existing && existing.type === 'tool-invocation') {\n existing.toolInvocation = Object.assign(existing.toolInvocation, {\n state: 'result' as const,\n result,\n isError,\n });\n if (providerMetadata) {\n existing.providerMetadata = providerMetadata;\n }\n } else {\n const toolInvocationPart: MastraToolInvocationPart = {\n type: 'tool-invocation',\n toolInvocation: Object.assign(\n {\n state: 'result' as const,\n toolCallId,\n toolName,\n args: {},\n result,\n },\n { isError },\n ),\n };\n if (providerMetadata) {\n toolInvocationPart.providerMetadata = providerMetadata;\n }\n state.currentMessage.content.parts.push(toolInvocationPart);\n }\n this.#session.emit({\n type: 'tool_end',\n toolCallId,\n result,\n isError,\n ...(providerMetadata ? { providerMetadata } : {}),\n });\n this.#session.emit({ type: 'message_update', message: this.cloneMessage(state.currentMessage) });\n break;\n }\n\n case 'tool-error': {\n const toolError = getPayload(chunk);\n const toolCallId = getString(toolError.toolCallId) ?? '';\n this.#session.emit({\n type: 'tool_end',\n toolCallId,\n result: getDisplayTransform(chunk.metadata, 'error', toolError.error),\n isError: true,\n });\n break;\n }\n\n case 'tool-call-approval': {\n const toolCallId = getString(getPayload(chunk).toolCallId) ?? '';\n const toolName = getString(getPayload(chunk).toolName) ?? '';\n const approvalTransform = getTransformedToolPayload(chunk.metadata, 'display', 'approval');\n const toolArgs = hasTransformedToolPayload(approvalTransform)\n ? approvalTransform.transformed\n : getDisplayTransform(chunk.metadata, 'input-available', getPayload(chunk).args);\n\n const policy = this.#session.resolveToolApproval(toolName);\n\n if (policy === 'allow') {\n await this.#session.approveToolCall({ toolCallId, requestContext });\n break;\n }\n\n if (policy === 'deny') {\n await this.#session.declineToolCall({ toolCallId, requestContext });\n break;\n }\n\n const approvalPromise = this.#session.approval.arm({ toolName, toolCallId });\n this.#session.emit({ type: 'tool_approval_required', toolCallId, toolName, args: toolArgs });\n\n const approval = await approvalPromise;\n this.#session.approval.clearToolName();\n\n if (approval.decision === 'approve') {\n await this.#session.approveToolCall({\n toolCallId,\n requestContext: approval.requestContext ?? requestContext,\n });\n } else {\n await this.#session.declineToolCall({\n toolCallId,\n requestContext: approval.requestContext ?? requestContext,\n declineContext: approval.declineContext,\n });\n }\n break;\n }\n\n case 'tool-call-suspended': {\n const suspToolCallId = getString(getPayload(chunk).toolCallId) ?? '';\n const suspToolName = getString(getPayload(chunk).toolName) ?? '';\n const suspArgs = getDisplayTransform(chunk.metadata, 'input-available', getPayload(chunk).args);\n const suspPayload = getDisplayTransform(chunk.metadata, 'suspend', getPayload(chunk).suspendPayload);\n const suspResumeSchema = getString(getPayload(chunk).resumeSchema);\n\n const suspRunId = this.#session.run.getRunId();\n if (suspRunId) {\n this.#session.suspensions.register({\n toolCallId: suspToolCallId,\n runId: suspRunId,\n toolName: suspToolName,\n });\n }\n state.isSuspended = true;\n\n this.#session.emit({\n type: 'tool_suspended',\n toolCallId: suspToolCallId,\n toolName: suspToolName,\n args: suspArgs,\n suspendPayload: suspPayload,\n resumeSchema: suspResumeSchema,\n });\n\n break;\n }\n\n case 'error': {\n const streamError = getErrorFromUnknown(getPayload(chunk).error);\n this.#session.emit({ type: 'error', error: streamError });\n\n // A run that dies after emitting `tool_suspended` (e.g. persisting the\n // suspended snapshot failed) leaves its parked suspensions unresumable:\n // answering them would fail with a misleading \"could not find a\n // suspended run\" error that masks this primary failure. Retract them so\n // the UI dismisses the prompts and the user sees the real error.\n const failedRunId = chunk.runId ?? this.#session.run.getRunId();\n if (failedRunId) {\n for (const { toolCallId, toolName } of this.#session.suspensions.deleteForRun({ runId: failedRunId })) {\n this.#session.emit({\n type: 'tool_suspension_cancelled',\n toolCallId,\n toolName,\n reason: streamError.message,\n });\n }\n }\n break;\n }\n\n case 'step-finish': {\n const usage = getRecord(getPayload(chunk).output)?.usage;\n const usageRecord = getRecord(usage);\n if (usageRecord) {\n const promptTokens =\n getUsageNumber(usageRecord, 'promptTokens') ?? getUsageNumber(usageRecord, 'inputTokens') ?? 0;\n const completionTokens =\n getUsageNumber(usageRecord, 'completionTokens') ?? getUsageNumber(usageRecord, 'outputTokens') ?? 0;\n const totalTokens = getUsageNumber(usageRecord, 'totalTokens') ?? promptTokens + completionTokens;\n const stepUsage: TokenUsage = {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n addOptionalUsageField(stepUsage, 'reasoningTokens', getUsageNumber(usageRecord, 'reasoningTokens'));\n addOptionalUsageField(stepUsage, 'cachedInputTokens', getUsageNumber(usageRecord, 'cachedInputTokens'));\n addOptionalUsageField(\n stepUsage,\n 'cacheCreationInputTokens',\n getUsageNumber(usageRecord, 'cacheCreationInputTokens'),\n );\n if (usageRecord.raw !== undefined) {\n stepUsage.raw = usageRecord.raw;\n }\n\n this.#session.addUsage(stepUsage);\n\n this.#machinery.persistTokenUsage().catch(() => {});\n this.#session.emit({ type: 'usage_update', usage: stepUsage });\n }\n break;\n }\n\n case 'finish': {\n const finishReason = getString(getRecord(getPayload(chunk).stepResult)?.reason) ?? '';\n const finishProviderMetadata =\n getRecord(getPayload(chunk).metadata)?.providerMetadata ?? getPayload(chunk).providerMetadata;\n // A server-side fallback means the turn was answered by a different\n // model than the one the user selected (e.g. fable-5 declined and the\n // fallback served the response). Surface that, otherwise the\n // substitution is invisible.\n const fallbackNotice = describeServerSideFallback(finishProviderMetadata);\n if (fallbackNotice) {\n this.#session.emit({ type: 'info', message: fallbackNotice });\n }\n if (finishReason === 'stop' || finishReason === 'end-turn') {\n this.setStopReason(state.currentMessage, 'complete', true);\n } else if (finishReason === 'tool-calls') {\n this.setStopReason(state.currentMessage, 'tool_use', true);\n } else {\n // Non-success terminal reasons (e.g. `content-filter` from a\n // `claude-fable-5` refusal, `error`, or `length`) must become an\n // explicit terminal error rather than a silent `complete`. Otherwise\n // the run ends with no final message and no error, leaving the user\n // unable to tell whether it completed, failed, or is still active.\n const errorMessage = describeNonSuccessFinishReason(finishReason, finishProviderMetadata);\n if (errorMessage) {\n this.setStopReason(state.currentMessage, 'error', true);\n this.setErrorMessage(state.currentMessage, errorMessage);\n state.terminalError = errorMessage;\n } else {\n this.setStopReason(state.currentMessage, 'complete', true);\n }\n }\n break;\n }\n\n case 'goal': {\n // In-loop goal evaluation marks a boundary between assistant attempts.\n // Close the current assistant message before rendering the judge result\n // so a continuation starts a fresh message instead of overwriting the\n // previous attempt in streaming UIs.\n this.finishCurrentMessageAndRotate(state);\n // Forward the payload so consumers (the TUI's judge display) can render\n // judge progress and the decision.\n const goalPayload = getPayload(chunk);\n if (isGoalEvaluationPayload(goalPayload)) {\n this.#session.emit({ type: 'goal_evaluation', payload: goalPayload });\n }\n break;\n }\n\n // Observational Memory data parts\n // NOTE: OM data parts arrive in { type, data: { ... } } form — NOT { type, payload }\n case 'data-om-status': {\n const d = getDataRecord(chunk);\n const w = getRecord(d?.windows);\n if (d && w) {\n const active = getNestedRecord(w, 'active');\n const msgs = getNestedRecord(active, 'messages');\n const obs = getNestedRecord(active, 'observations');\n const buffered = getNestedRecord(w, 'buffered');\n const buffObs = getNestedRecord(buffered, 'observations');\n const buffRef = getNestedRecord(buffered, 'reflection');\n\n this.#session.emit({\n type: 'om_status',\n windows: {\n active: {\n messages: { tokens: getNumber(msgs?.tokens, 0), threshold: getNumber(msgs?.threshold, 0) },\n observations: { tokens: getNumber(obs?.tokens, 0), threshold: getNumber(obs?.threshold, 0) },\n },\n buffered: {\n observations: {\n status: getOmStatus(buffObs?.status),\n chunks: getNumber(buffObs?.chunks, 0),\n messageTokens: getNumber(buffObs?.messageTokens, 0),\n projectedMessageRemoval: getNumber(buffObs?.projectedMessageRemoval, 0),\n observationTokens: getNumber(buffObs?.observationTokens, 0),\n },\n reflection: {\n status: getOmStatus(buffRef?.status),\n inputObservationTokens: getNumber(buffRef?.inputObservationTokens, 0),\n observationTokens: getNumber(buffRef?.observationTokens, 0),\n },\n },\n },\n recordId: getString(d.recordId) ?? '',\n threadId: getString(d.threadId) ?? '',\n stepNumber: getNumber(d.stepNumber, 0),\n generationCount: getNumber(d.generationCount, 0),\n });\n }\n break;\n }\n case 'data-om-observation-start': {\n const payload = getDataRecord(chunk);\n const cycleId = getString(payload?.cycleId);\n if (payload && cycleId) {\n const operationType = getOperationType(payload.operationType);\n if (operationType === 'observation') {\n this.#session.emit({\n type: 'om_observation_start',\n cycleId,\n operationType,\n tokensToObserve: getNumber(payload.tokensToObserve, 0),\n });\n } else {\n this.#session.emit({\n type: 'om_reflection_start',\n cycleId,\n tokensToReflect: getNumber(payload.tokensToObserve, 0),\n });\n }\n }\n break;\n }\n case 'data-om-observation-end': {\n const payload = getDataRecord(chunk);\n const cycleId = getString(payload?.cycleId);\n if (payload && cycleId) {\n if (payload.operationType === 'reflection') {\n this.#session.emit({\n type: 'om_reflection_end',\n cycleId,\n durationMs: getNumber(payload.durationMs, 0),\n compressedTokens: getNumber(payload.observationTokens, 0),\n observations: getString(payload.observations),\n });\n } else {\n this.#session.emit({\n type: 'om_observation_end',\n cycleId,\n durationMs: getNumber(payload.durationMs, 0),\n tokensObserved: getNumber(payload.tokensObserved, 0),\n observationTokens: getNumber(payload.observationTokens, 0),\n observations: getString(payload.observations),\n currentTask: getString(payload.currentTask),\n suggestedResponse: getString(payload.suggestedResponse),\n });\n }\n }\n break;\n }\n case 'data-om-observation-failed': {\n const payload = getDataRecord(chunk);\n if (payload) {\n const operationType = getOperationType(payload.operationType);\n const error = getString(payload.error) ?? 'Unknown error';\n\n if (operationType === 'reflection') {\n this.#session.emit({\n type: 'om_reflection_failed',\n cycleId: getString(payload.cycleId) ?? 'unknown',\n error,\n durationMs: getNumber(payload.durationMs, 0),\n });\n } else {\n this.#session.emit({\n type: 'om_observation_failed',\n cycleId: getString(payload.cycleId) ?? 'unknown',\n error,\n durationMs: getNumber(payload.durationMs, 0),\n });\n }\n\n this.abortForOmFailure({ operationType, stage: 'run', error });\n return { message: state.currentMessage };\n }\n break;\n }\n // Async buffering lifecycle\n case 'data-om-buffering-start': {\n const payload = getDataRecord(chunk);\n const cycleId = getString(payload?.cycleId);\n if (payload && cycleId) {\n this.#session.emit({\n type: 'om_buffering_start',\n cycleId,\n operationType: getOperationType(payload.operationType),\n tokensToBuffer: getNumber(payload.tokensToBuffer, 0),\n });\n }\n break;\n }\n case 'data-om-buffering-end': {\n const payload = getDataRecord(chunk);\n const cycleId = getString(payload?.cycleId);\n if (payload && cycleId) {\n this.#session.emit({\n type: 'om_buffering_end',\n cycleId,\n operationType: getOperationType(payload.operationType),\n tokensBuffered: getNumber(payload.tokensBuffered, 0),\n bufferedTokens: getNumber(payload.bufferedTokens, 0),\n observations: getString(payload.observations),\n });\n }\n break;\n }\n case 'data-om-buffering-failed': {\n const payload = getDataRecord(chunk);\n if (payload) {\n const operationType = getOperationType(payload.operationType);\n const error = getString(payload.error) ?? 'Unknown error';\n\n this.#session.emit({\n type: 'om_buffering_failed',\n cycleId: getString(payload.cycleId) ?? 'unknown',\n operationType,\n error,\n });\n\n this.abortForOmFailure({ operationType, stage: 'buffering', error });\n return { message: state.currentMessage };\n }\n break;\n }\n case 'data-signal': {\n const payload = getDataRecord(chunk);\n if (payload) {\n const message = this.createSignalMessage('data-signal', payload);\n this.#session.emit({ type: 'message_start', message });\n this.#session.emit({ type: 'message_end', message });\n }\n break;\n }\n case 'data-user-message': {\n const payload = getDataRecord(chunk);\n if (payload) {\n this.finishCurrentMessageAndRotate(state);\n const message = this.createSignalMessage('data-user-message', payload);\n this.#session.emit({ type: 'message_start', message });\n this.#session.emit({ type: 'message_end', message });\n }\n break;\n }\n // Back-compat: persisted streams may still contain data-system-reminder parts\n case 'data-system-reminder': {\n const payload = getDataRecord(chunk);\n if (payload) {\n const message = this.createSignalMessage('data-system-reminder', payload);\n this.#session.emit({ type: 'message_start', message });\n this.#session.emit({ type: 'message_end', message });\n }\n break;\n }\n case 'data-om-activation': {\n const payload = getDataRecord(chunk);\n const cycleId = getString(payload?.cycleId);\n if (payload && cycleId) {\n this.#session.emit({\n type: 'om_activation',\n cycleId,\n operationType: getOperationType(payload.operationType),\n chunksActivated: getNumber(payload.chunksActivated, 0),\n tokensActivated: getNumber(payload.tokensActivated, 0),\n observationTokens: getNumber(payload.observationTokens, 0),\n messagesActivated: getNumber(payload.messagesActivated, 0),\n generationCount: getNumber(payload.generationCount, 0),\n triggeredBy: getActivationTrigger(payload.triggeredBy),\n lastActivityAt: getOptionalNumber(payload.lastActivityAt),\n ttlExpiredMs: getOptionalNumber(payload.ttlExpiredMs),\n activateAfterIdle: getOptionalNumber(getRecord(payload.config)?.activateAfterIdle),\n previousModel: getString(payload.previousModel),\n currentModel: getString(payload.currentModel),\n });\n }\n break;\n }\n case 'data-om-thread-update': {\n const payload = getDataRecord(chunk);\n const newTitle = getString(payload?.newTitle);\n if (payload && newTitle) {\n this.#session.emit({\n type: 'om_thread_title_updated',\n cycleId: getString(payload.cycleId) ?? 'unknown',\n threadId: getString(payload.threadId) ?? this.#session.thread.getId() ?? 'unknown',\n oldTitle: getString(payload.oldTitle),\n newTitle,\n });\n }\n break;\n }\n\n case 'data-mastracode-tool-progress': {\n const d = (chunk as any).data as Record<string, any> | undefined;\n if (d?.toolCallId && d?.progress !== undefined) {\n this.#session.emit({ type: 'tool_update', toolCallId: d.toolCallId, partialResult: d.progress });\n const output = formatToolProgressOutput(d.progress);\n if (output) {\n this.#session.emit({ type: 'shell_output', toolCallId: d.toolCallId, output, stream: 'stdout' });\n }\n }\n break;\n }\n\n // Sandbox streaming data chunks (from workspace execute_command tool)\n case 'data-sandbox-stdout': {\n const d = getDataRecord(chunk);\n const output = getString(d?.output);\n const toolCallId = getString(d?.toolCallId);\n if (output && toolCallId) {\n this.#session.emit({ type: 'shell_output', toolCallId, output, stream: 'stdout' });\n }\n break;\n }\n case 'data-sandbox-stderr': {\n const d = getDataRecord(chunk);\n const output = getString(d?.output);\n const toolCallId = getString(d?.toolCallId);\n if (output && toolCallId) {\n this.#session.emit({ type: 'shell_output', toolCallId, output, stream: 'stderr' });\n }\n break;\n }\n\n default:\n break;\n }\n }\n\n private finishStreamState(state: StreamState): { message: MastraDBMessage; suspended?: boolean } {\n if (this.hasCurrentMessageContent(state) || !state.lastFinishedMessage) {\n this.#session.emit({ type: 'message_end', message: state.currentMessage });\n return { message: state.currentMessage, suspended: state.isSuspended || undefined };\n }\n\n return { message: state.lastFinishedMessage, suspended: state.isSuspended || undefined };\n }\n\n private async finishSubscribedStreamRun({\n suspended,\n error,\n aborted,\n }: {\n suspended?: boolean;\n error?: boolean;\n aborted?: boolean;\n }): Promise<void> {\n const reason = error\n ? 'error'\n : suspended\n ? 'suspended'\n : aborted || this.#session.run.isAbortRequested()\n ? 'aborted'\n : 'complete';\n this.#session.emit({ type: 'agent_end', reason });\n this.#session.run.reset();\n await this.#session.drainFollowUpQueue();\n }\n\n private async handleSubscribedStreamError(error: unknown): Promise<void> {\n if (error instanceof Error && error.name === 'AbortError') {\n this.#session.emit({ type: 'agent_end', reason: 'aborted' });\n } else {\n this.#session.emit({ type: 'error', error: getErrorFromUnknown(error) });\n this.#session.emit({ type: 'agent_end', reason: 'error' });\n }\n this.#session.stream.detach();\n this.#session.run.reset();\n await this.#session.drainFollowUpQueue();\n }\n\n async processSubscribedThreadStream(subscription: AgentThreadSubscription<StreamChunk>): Promise<void> {\n let currentRun: StreamState | undefined;\n\n try {\n for await (const chunk of subscription.stream) {\n if (!this.#session.stream.isCurrent({ subscription })) {\n subscription.unsubscribe();\n break;\n }\n\n if (!currentRun) {\n currentRun = this.createStreamState();\n this.#session.run.nextOperation();\n this.#session.run.ensureAbortController();\n this.#session.run.setRunId({ runId: subscription.activeRunId() ?? ('runId' in chunk ? chunk.runId : null) });\n this.#session.run.setTraceId({ traceId: null });\n this.#session.emit({ type: 'agent_start' });\n }\n\n if (chunk.type === 'start') {\n continue;\n }\n\n try {\n const requestContext = await this.#machinery.buildRequestContext(this.#requestContext);\n const streamResult = await this.processStreamChunk(currentRun, chunk, requestContext);\n if (\n streamResult ||\n chunk.type === 'finish' ||\n chunk.type === 'error' ||\n chunk.type === 'abort' ||\n chunk.type === 'tool-call-suspended'\n ) {\n const suspended =\n chunk.type === 'tool-call-suspended' ||\n (streamResult ?? this.finishStreamState(currentRun)).suspended ||\n undefined;\n const aborted = chunk.type === 'abort';\n // A non-success terminal finish reason (e.g. a `claude-fable-5`\n // content-filter refusal) becomes an explicit error so the\n // run never silently stops without a visible terminal state.\n let isError = chunk.type === 'error';\n if (\n currentRun.terminalError &&\n !isError &&\n !aborted &&\n !this.#session.run.isAbortRequested() &&\n !s