UNPKG

@mastra/core

Version:
1,254 lines (1,249 loc) 212 kB
import { Workspace, Mastra, Agent, taskCheckTool, taskCompleteTool, taskUpdateTool, taskWriteTool, submitPlanTool, askUserTool, createWorkspaceTools, getServerSideFallbackInfo, summarizeTaskCheck } from './chunk-OE4IEL7C.js'; import { ModelRouterLanguageModel } from './chunk-AR6WPTSV.js'; import { GatewayManager, defaultGateways } from './chunk-J7CHY2GW.js'; import { safeStringify } from './chunk-R5KSSZYP.js'; import { createTool } from './chunk-O5QBSZXE.js'; import { toStandardSchema } from './chunk-6SRTDZ7S.js'; import { createSignal, mastraDBMessageToSignal, getTransformedToolPayload, hasTransformedToolPayload } from './chunk-WEN4YCAX.js'; import { RequestContext } from './chunk-W4DLMY73.js'; import { getErrorFromUnknown } from './chunk-M7RBQNFP.js'; import { z } from 'zod/v4'; import { randomUUID } from 'crypto'; // src/agent-controller/types.ts function createEmptyTokenUsage() { return { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedInputTokens: 0, cacheCreationInputTokens: 0 }; } function defaultDisplayState() { return { isRunning: false, currentMessage: null, queuedFollowUps: 0, tokenUsage: createEmptyTokenUsage(), activeTools: /* @__PURE__ */ new Map(), toolInputBuffers: /* @__PURE__ */ new Map(), pendingApproval: null, pendingSuspensions: /* @__PURE__ */ new Map(), activeSubagents: /* @__PURE__ */ new Map(), omProgress: defaultOMProgressState(), bufferingMessages: false, bufferingObservations: false, modifiedFiles: /* @__PURE__ */ new Map(), tasks: [], previousTasks: [] }; } function defaultOMProgressState() { return { status: "idle", pendingTokens: 0, threshold: 3e4, thresholdPercent: 0, observationTokens: 0, reflectionThreshold: 4e4, reflectionThresholdPercent: 0, buffered: { observations: { status: "idle", chunks: 0, messageTokens: 0, projectedMessageRemoval: 0, observationTokens: 0 }, reflection: { status: "idle", inputObservationTokens: 0, observationTokens: 0 } }, generationCount: 0, stepNumber: 0, preReflectionTokens: 0 }; } // src/agent-controller/stream-content.ts function getDisplayTransform(metadata, phase, fallback) { const transform = getTransformedToolPayload(metadata, "display", phase); return hasTransformedToolPayload(transform) ? transform.transformed : fallback; } function getStringValue(value) { return typeof value === "string" ? value : void 0; } function getRecordValue(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : void 0; } function signalContentsToControllerContent(contents) { if (typeof contents === "string") return [{ type: "text", text: contents }]; return contents.flatMap((part) => { if (part.type === "text") { return [{ type: "text", text: part.text }]; } if (typeof part.data !== "string") return []; if (part.mediaType.startsWith("image/")) { return [{ type: "image", data: part.data, mimeType: part.mediaType }]; } return [ { type: "file", data: part.data, mediaType: part.mediaType, filename: part.filename } ]; }); } function toSystemReminderContent(payload) { const attributes = getRecordValue(payload.attributes); const metadata = getRecordValue(payload.metadata); const message = signalContentsToText(payload.contents); if (!message) return void 0; return { type: "system_reminder", message, reminderType: getStringValue(payload.reminderType) ?? getStringValue(attributes?.type) ?? getStringValue(payload.type), path: getStringValue(payload.path) ?? getStringValue(attributes?.path), precedesMessageId: getStringValue(payload.precedesMessageId) ?? getStringValue(attributes?.precedesMessageId), gapText: getStringValue(payload.gapText) ?? getStringValue(attributes?.gapText), gapMs: typeof payload.gapMs === "number" ? payload.gapMs : typeof attributes?.gapMs === "number" ? attributes.gapMs : void 0, timestamp: getStringValue(payload.timestamp) ?? getStringValue(attributes?.timestamp), goalMaxTurns: typeof payload.goalMaxTurns === "number" ? payload.goalMaxTurns : typeof metadata?.goalMaxTurns === "number" ? metadata.goalMaxTurns : void 0, judgeModelId: getStringValue(payload.judgeModelId) ?? getStringValue(metadata?.judgeModelId), goalEvaluation: getRecordValue(metadata?.goalEvaluation) }; } function toUserSignalMessage(payload) { const id = getStringValue(payload.id); const rawContents = payload.contents; if (!id || rawContents === void 0) return void 0; const signal = createSignal({ id, type: "user", tagName: "user", contents: rawContents, attributes: getRecordValue(payload.attributes), createdAt: getStringValue(payload.createdAt) }); const content = signalContentsToControllerContent(signal.contents); if (content.length === 0) return void 0; return { id: signal.id, role: "user", content, createdAt: signal.createdAt, attributes: signal.attributes }; } function signalContentsToText(contents) { if (typeof contents === "string") return contents; if (!Array.isArray(contents)) return ""; return contents.filter((part) => getRecordValue(part)?.type === "text").map((part) => part.text).join("\n"); } function toStateSignalContent(payload) { const stateMetadata = getRecordValue(getRecordValue(payload.metadata)?.state); const stateId = getStringValue(stateMetadata?.id) ?? getStringValue(payload.tagName) ?? "state"; return { type: "state_signal", id: getStringValue(payload.id), stateId, mode: stateMetadata?.mode === "delta" ? "delta" : "snapshot", cacheKey: getStringValue(stateMetadata?.cacheKey), version: typeof stateMetadata?.version === "number" ? stateMetadata.version : void 0, message: signalContentsToText(payload.contents) }; } function toNotificationSummaryContent(payload) { const metadataSummary = getRecordValue(getRecordValue(payload.metadata)?.notificationSummary); const bySource = getRecordValue(metadataSummary?.bySource) ?? {}; const byPriority = getRecordValue(metadataSummary?.byPriority) ?? {}; const notificationIds = Array.isArray(metadataSummary?.notificationIds) ? metadataSummary.notificationIds.filter((id) => typeof id === "string") : []; const pending = typeof metadataSummary?.pending === "number" ? metadataSummary.pending : void 0; return { type: "notification_summary", id: getStringValue(payload.id), message: signalContentsToText(payload.contents), pending: pending ?? notificationIds.length, bySource: Object.fromEntries( Object.entries(bySource).filter((entry) => typeof entry[1] === "number") ), byPriority: Object.fromEntries( Object.entries(byPriority).filter((entry) => typeof entry[1] === "number") ), notificationIds }; } function toReactiveSignalContent(payload) { const tagName = getStringValue(payload.tagName); if (!tagName) return void 0; return { type: "reactive_signal", id: getStringValue(payload.id), tagName, message: signalContentsToText(payload.contents), attributes: getRecordValue(payload.attributes), metadata: getRecordValue(payload.metadata) }; } function toNotificationContent(payload) { const attributes = getRecordValue(payload.attributes) ?? {}; const metadata = getRecordValue(payload.metadata) ?? {}; const notificationMetadata = getRecordValue(metadata.notification); const message = signalContentsToText(payload.contents); if (!message) return void 0; return { type: "notification", id: getStringValue(payload.id), notificationId: getStringValue(attributes.id) ?? getStringValue(notificationMetadata?.recordId), message, source: getStringValue(attributes.source) ?? getStringValue(notificationMetadata?.source), kind: getStringValue(attributes.kind) ?? getStringValue(attributes.type) ?? getStringValue(notificationMetadata?.kind), priority: getStringValue(attributes.priority) ?? getStringValue(notificationMetadata?.priority), status: getStringValue(attributes.status) ?? getStringValue(notificationMetadata?.status), attributes, metadata }; } function describeNonSuccessFinishReason(reason, providerMetadata) { switch (reason) { case "content-filter": { const stopDetails = providerMetadata?.anthropic?.stopDetails; const explanation = stopDetails && typeof stopDetails.explanation === "string" ? stopDetails.explanation : void 0; const category = stopDetails && typeof stopDetails.category === "string" ? stopDetails.category : void 0; const detail = explanation ?? (category ? `category: ${category}` : void 0); return detail ? `The model stopped on a content filter (${detail}).` : "The model stopped on a content filter."; } case "error": return "The model stream ended with an error before producing a final response."; case "length": return "The model stopped because it reached its maximum output length before finishing."; default: return void 0; } } function describeServerSideFallback(providerMetadata) { const fallback = getServerSideFallbackInfo(providerMetadata); if (!fallback) { return void 0; } return fallback.model ? `The selected model declined this turn; the response was generated by fallback model ${fallback.model}.` : "The selected model declined this turn; the response was generated by a fallback model."; } function getUsageNumber(usage, key) { const value = usage[key]; if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string" && value.trim() !== "") { const numericValue = Number(value); if (Number.isFinite(numericValue)) { return numericValue; } } return void 0; } function addOptionalUsageField(usage, key, value) { if (value !== void 0) { usage[key] = (usage[key] ?? 0) + value; } } // src/agent-controller/session-run-engine.ts function formatToolProgressOutput(progress) { if (typeof progress === "string") return progress.endsWith("\n") ? progress : `${progress} `; if (typeof progress !== "object" || progress === null) return `${String(progress)} `; const record = progress; const parts = [record.status, record.detail].filter( (part) => typeof part === "string" && part.length > 0 ); return parts.length > 0 ? `${parts.join(": ")} ` : `${JSON.stringify(progress)} `; } var SessionRunEngine = class { #session; #machinery; constructor(session, machinery) { this.#session = session; this.#machinery = machinery; } createEmptyAssistantMessage() { return { id: this.#machinery.generateId(), role: "assistant", content: [], createdAt: /* @__PURE__ */ new Date() }; } hasCurrentMessageContent(state) { return state.currentMessage.content.length > 0; } finishCurrentMessageAndRotate(state) { if (!this.hasCurrentMessageContent(state)) return; state.currentMessage.stopReason ??= "complete"; this.#session.emit({ type: "message_end", message: state.currentMessage }); state.lastFinishedMessage = state.currentMessage; state.currentMessage = this.createEmptyAssistantMessage(); state.textContentById.clear(); state.thinkingContentById.clear(); } createStreamState() { return { currentMessage: this.createEmptyAssistantMessage(), isSuspended: false, textContentById: /* @__PURE__ */ new Map(), thinkingContentById: /* @__PURE__ */ new Map() }; } abortForOmFailure({ operationType, stage, error }) { this.#session.emit({ type: "error", error: new Error(`Observational memory ${operationType} ${stage} failed: ${error}`) }); this.#session.abortRun(); } /** * Process a stream response (shared between sendMessage and tool approval). */ async processStream(response, requestContextInput) { const state = this.createStreamState(); const requestContext = await this.#machinery.buildRequestContext(requestContextInput); this.#session.run.nextOperation(); this.#session.emit({ type: "agent_start" }); let result; let error = false; let aborted = false; for await (const chunk of response.fullStream) { result = await this.processStreamChunk(state, chunk, requestContext); if (chunk.type === "error") { error = true; } if (chunk.type === "abort") { aborted = true; } if (result || chunk.type === "finish" || chunk.type === "error" || chunk.type === "abort" || chunk.type === "tool-call-suspended" || this.#session.run.isAbortRequested()) { result ??= this.finishStreamState(state); break; } } result ??= this.finishStreamState(state); if (state.terminalError && !error && !aborted && !this.#session.run.isAbortRequested() && !result.suspended) { error = true; this.#session.emit({ type: "error", error: new Error(state.terminalError) }); } this.#session.emit({ type: "agent_end", reason: error ? "error" : result.suspended ? "suspended" : aborted || this.#session.run.isAbortRequested() ? "aborted" : "complete" }); this.#session.run.reset(); await this.#session.drainFollowUpQueue(); return result; } async processStreamChunk(state, chunk, requestContext) { if ("runId" in chunk && chunk.runId) { this.#session.run.setRunId({ runId: chunk.runId }); } switch (chunk.type) { case "text-start": { const textIndex = state.currentMessage.content.length; state.currentMessage.content.push({ type: "text", text: "" }); state.textContentById.set(chunk.payload.id, { index: textIndex, text: "" }); this.#session.emit({ type: "message_start", message: { ...state.currentMessage } }); break; } case "text-delta": { const textState = state.textContentById.get(chunk.payload.id); if (textState) { textState.text += chunk.payload.text; const textContent = state.currentMessage.content[textState.index]; if (textContent && textContent.type === "text") { textContent.text = textState.text; } this.#session.emit({ type: "message_update", message: { ...state.currentMessage } }); } break; } case "reasoning-start": { const thinkingIndex = state.currentMessage.content.length; state.currentMessage.content.push({ type: "thinking", thinking: "" }); state.thinkingContentById.set(chunk.payload.id, { index: thinkingIndex, text: "" }); this.#session.emit({ type: "message_update", message: { ...state.currentMessage } }); break; } case "reasoning-delta": { const thinkingState = state.thinkingContentById.get(chunk.payload.id); if (thinkingState) { thinkingState.text += chunk.payload.text; const thinkingContent = state.currentMessage.content[thinkingState.index]; if (thinkingContent && thinkingContent.type === "thinking") { thinkingContent.thinking = thinkingState.text; } this.#session.emit({ type: "message_update", message: { ...state.currentMessage } }); } break; } case "tool-call-input-streaming-start": { const { toolCallId, toolName } = chunk.payload; this.#session.emit({ type: "tool_input_start", toolCallId, toolName }); break; } case "tool-call-delta": { const { toolCallId, argsTextDelta, toolName } = chunk.payload; const transform = getTransformedToolPayload(chunk.metadata, "display", "input-delta"); if (!transform?.suppress) { this.#session.emit({ type: "tool_input_delta", toolCallId, argsTextDelta: hasTransformedToolPayload(transform) ? transform.transformed : argsTextDelta, toolName }); } break; } case "tool-call-input-streaming-end": { const { toolCallId } = chunk.payload; this.#session.emit({ type: "tool_input_end", toolCallId }); break; } case "tool-call": { const toolCall = chunk.payload; const args = getDisplayTransform(chunk.metadata, "input-available", toolCall.args); state.currentMessage.content.push({ type: "tool_call", id: toolCall.toolCallId, name: toolCall.toolName, args }); this.#session.emit({ type: "tool_start", toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, args }); this.#session.emit({ type: "message_update", message: { ...state.currentMessage } }); break; } case "tool-result": { const toolResult = chunk.payload; const providerMetadata = toolResult.providerMetadata; const result = getDisplayTransform(chunk.metadata, "output-available", toolResult.result); state.currentMessage.content.push({ type: "tool_result", id: toolResult.toolCallId, name: toolResult.toolName, result, isError: toolResult.isError ?? false, ...providerMetadata ? { providerMetadata } : {} }); this.#session.emit({ type: "tool_end", toolCallId: toolResult.toolCallId, result, isError: toolResult.isError ?? false, ...providerMetadata ? { providerMetadata } : {} }); this.#session.emit({ type: "message_update", message: { ...state.currentMessage } }); break; } case "tool-error": { const toolError = chunk.payload; this.#session.emit({ type: "tool_end", toolCallId: toolError.toolCallId, result: getDisplayTransform(chunk.metadata, "error", toolError.error), isError: true }); break; } case "tool-call-approval": { const toolCallId = chunk.payload.toolCallId; const toolName = chunk.payload.toolName; const approvalTransform = getTransformedToolPayload(chunk.metadata, "display", "approval"); const toolArgs = hasTransformedToolPayload(approvalTransform) ? approvalTransform.transformed : getDisplayTransform(chunk.metadata, "input-available", chunk.payload.args); const policy = this.#session.resolveToolApproval(toolName); if (policy === "allow") { await this.#session.approveToolCall({ toolCallId, requestContext }); break; } if (policy === "deny") { await this.#session.declineToolCall({ toolCallId, requestContext }); break; } const approvalPromise = this.#session.approval.arm({ toolName, toolCallId }); this.#session.emit({ type: "tool_approval_required", toolCallId, toolName, args: toolArgs }); const approval = await approvalPromise; this.#session.approval.clearToolName(); if (approval.decision === "approve") { await this.#session.approveToolCall({ toolCallId, requestContext: approval.requestContext ?? requestContext }); } else { await this.#session.declineToolCall({ toolCallId, requestContext: approval.requestContext ?? requestContext, declineContext: approval.declineContext }); } break; } case "tool-call-suspended": { const suspToolCallId = chunk.payload.toolCallId; const suspToolName = chunk.payload.toolName; const suspArgs = getDisplayTransform(chunk.metadata, "input-available", chunk.payload.args); const suspPayload = getDisplayTransform(chunk.metadata, "suspend", chunk.payload.suspendPayload); const suspResumeSchema = chunk.payload.resumeSchema; const suspRunId = this.#session.run.getRunId(); if (suspRunId) { this.#session.suspensions.register({ toolCallId: suspToolCallId, runId: suspRunId, toolName: suspToolName }); } state.isSuspended = true; this.#session.emit({ type: "tool_suspended", toolCallId: suspToolCallId, toolName: suspToolName, args: suspArgs, suspendPayload: suspPayload, resumeSchema: suspResumeSchema }); break; } case "error": { const streamError = getErrorFromUnknown(chunk.payload.error); this.#session.emit({ type: "error", error: streamError }); break; } case "step-finish": { const usage = chunk.payload?.output?.usage; if (usage) { const usageRecord = usage; const promptTokens = getUsageNumber(usageRecord, "promptTokens") ?? getUsageNumber(usageRecord, "inputTokens") ?? 0; const completionTokens = getUsageNumber(usageRecord, "completionTokens") ?? getUsageNumber(usageRecord, "outputTokens") ?? 0; const totalTokens = getUsageNumber(usageRecord, "totalTokens") ?? promptTokens + completionTokens; const stepUsage = { promptTokens, completionTokens, totalTokens }; addOptionalUsageField(stepUsage, "reasoningTokens", getUsageNumber(usageRecord, "reasoningTokens")); addOptionalUsageField(stepUsage, "cachedInputTokens", getUsageNumber(usageRecord, "cachedInputTokens")); addOptionalUsageField( stepUsage, "cacheCreationInputTokens", getUsageNumber(usageRecord, "cacheCreationInputTokens") ); if (usageRecord.raw !== void 0) { stepUsage.raw = usageRecord.raw; } this.#session.addUsage(stepUsage); this.#machinery.persistTokenUsage().catch(() => { }); this.#session.emit({ type: "usage_update", usage: stepUsage }); } break; } case "finish": { const finishReason = chunk.payload.stepResult?.reason; const finishProviderMetadata = chunk.payload?.metadata?.providerMetadata ?? chunk.payload?.providerMetadata; const fallbackNotice = describeServerSideFallback(finishProviderMetadata); if (fallbackNotice) { this.#session.emit({ type: "info", message: fallbackNotice }); } if (finishReason === "stop" || finishReason === "end-turn") { state.currentMessage.stopReason = "complete"; } else if (finishReason === "tool-calls") { state.currentMessage.stopReason = "tool_use"; } else { const errorMessage = describeNonSuccessFinishReason(finishReason, finishProviderMetadata); if (errorMessage) { state.currentMessage.stopReason = "error"; state.currentMessage.errorMessage = errorMessage; state.terminalError = errorMessage; } else { state.currentMessage.stopReason = "complete"; } } break; } case "goal": { this.finishCurrentMessageAndRotate(state); this.#session.emit({ type: "goal_evaluation", payload: chunk.payload }); break; } // Observational Memory data parts // NOTE: OM data parts arrive as { type, data: { ... } } — NOT { type, payload } case "data-om-status": { const d = chunk.data; if (d?.windows) { const w = d.windows; const active = w.active ?? {}; const msgs = active.messages ?? {}; const obs = active.observations ?? {}; const buffObs = w.buffered?.observations ?? {}; const buffRef = w.buffered?.reflection ?? {}; this.#session.emit({ type: "om_status", windows: { active: { messages: { tokens: msgs.tokens ?? 0, threshold: msgs.threshold ?? 0 }, observations: { tokens: obs.tokens ?? 0, threshold: obs.threshold ?? 0 } }, buffered: { observations: { status: buffObs.status ?? "idle", chunks: buffObs.chunks ?? 0, messageTokens: buffObs.messageTokens ?? 0, projectedMessageRemoval: buffObs.projectedMessageRemoval ?? 0, observationTokens: buffObs.observationTokens ?? 0 }, reflection: { status: buffRef.status ?? "idle", inputObservationTokens: buffRef.inputObservationTokens ?? 0, observationTokens: buffRef.observationTokens ?? 0 } } }, recordId: d.recordId ?? "", threadId: d.threadId ?? "", stepNumber: d.stepNumber ?? 0, generationCount: d.generationCount ?? 0 }); } break; } case "data-om-observation-start": { const payload = chunk.data; if (payload && payload.cycleId) { if (payload.operationType === "observation") { this.#session.emit({ type: "om_observation_start", cycleId: payload.cycleId, operationType: payload.operationType, tokensToObserve: payload.tokensToObserve ?? 0 }); } else if (payload.operationType === "reflection") { this.#session.emit({ type: "om_reflection_start", cycleId: payload.cycleId, tokensToReflect: payload.tokensToObserve ?? 0 }); } } break; } case "data-om-observation-end": { const payload = chunk.data; if (payload && payload.cycleId) { if (payload.operationType === "reflection") { this.#session.emit({ type: "om_reflection_end", cycleId: payload.cycleId, durationMs: payload.durationMs ?? 0, compressedTokens: payload.observationTokens ?? 0, observations: payload.observations }); } else { this.#session.emit({ type: "om_observation_end", cycleId: payload.cycleId, durationMs: payload.durationMs ?? 0, tokensObserved: payload.tokensObserved ?? 0, observationTokens: payload.observationTokens ?? 0, observations: payload.observations, currentTask: payload.currentTask, suggestedResponse: payload.suggestedResponse }); } } break; } case "data-om-observation-failed": { const payload = chunk.data; if (payload) { const operationType = payload.operationType === "reflection" ? "reflection" : "observation"; const error = payload.error ?? "Unknown error"; if (operationType === "reflection") { this.#session.emit({ type: "om_reflection_failed", cycleId: payload.cycleId ?? "unknown", error, durationMs: payload.durationMs ?? 0 }); } else { this.#session.emit({ type: "om_observation_failed", cycleId: payload.cycleId ?? "unknown", error, durationMs: payload.durationMs ?? 0 }); } this.abortForOmFailure({ operationType, stage: "run", error }); return { message: state.currentMessage }; } break; } // Async buffering lifecycle case "data-om-buffering-start": { const payload = chunk.data; if (payload && payload.cycleId) { this.#session.emit({ type: "om_buffering_start", cycleId: payload.cycleId, operationType: payload.operationType ?? "observation", tokensToBuffer: payload.tokensToBuffer ?? 0 }); } break; } case "data-om-buffering-end": { const payload = chunk.data; if (payload && payload.cycleId) { this.#session.emit({ type: "om_buffering_end", cycleId: payload.cycleId, operationType: payload.operationType ?? "observation", tokensBuffered: payload.tokensBuffered ?? 0, bufferedTokens: payload.bufferedTokens ?? 0, observations: payload.observations }); } break; } case "data-om-buffering-failed": { const payload = chunk.data; if (payload) { const operationType = payload.operationType ?? "observation"; const error = payload.error ?? "Unknown error"; this.#session.emit({ type: "om_buffering_failed", cycleId: payload.cycleId, operationType, error }); this.abortForOmFailure({ operationType, stage: "buffering", error }); return { message: state.currentMessage }; } break; } case "data-signal": { const payload = chunk.data; if (payload?.type === "state") { const stateSignal = toStateSignalContent(payload); if (stateSignal) { state.currentMessage.content.push(stateSignal); this.#session.emit({ type: "message_update", message: state.currentMessage }); } } else if (payload?.type === "reactive" && payload.tagName === "system-reminder") { const reminder = toSystemReminderContent(payload); if (reminder) { state.currentMessage.content.push(reminder); this.#session.emit({ type: "message_update", message: state.currentMessage }); } } else if (payload?.type === "notification" && payload.tagName === "notification-summary") { const notificationSummary = toNotificationSummaryContent(payload); if (notificationSummary) { state.currentMessage.content.push(notificationSummary); this.#session.emit({ type: "message_update", message: state.currentMessage }); } } else if (payload?.type === "notification" && payload.tagName === "notification") { const notification = toNotificationContent(payload); if (notification) { state.currentMessage.content.push(notification); this.#session.emit({ type: "message_update", message: state.currentMessage }); } } else if (payload?.type === "reactive") { const reactiveSignal = toReactiveSignalContent(payload); if (reactiveSignal) { state.currentMessage.content.push(reactiveSignal); this.#session.emit({ type: "message_update", message: state.currentMessage }); } } break; } case "data-user-message": { const payload = chunk.data; const message = payload ? toUserSignalMessage(payload) : void 0; if (message) { if (state.currentMessage.content.length > 0) { state.currentMessage.stopReason ??= "complete"; this.#session.emit({ type: "message_end", message: { ...state.currentMessage } }); state.currentMessage = { id: this.#machinery.generateId(), role: "assistant", content: [], createdAt: /* @__PURE__ */ new Date() }; state.textContentById.clear(); state.thinkingContentById.clear(); } this.#session.emit({ type: "message_start", message }); this.#session.emit({ type: "message_end", message }); } break; } // Back-compat: persisted streams may still contain data-system-reminder parts case "data-system-reminder": { const payload = chunk.data; const reminder = payload ? toSystemReminderContent(payload) : void 0; if (reminder) { state.currentMessage.content.push(reminder); this.#session.emit({ type: "message_update", message: state.currentMessage }); } break; } case "data-om-activation": { const payload = chunk.data; if (payload && payload.cycleId) { this.#session.emit({ type: "om_activation", cycleId: payload.cycleId, operationType: payload.operationType ?? "observation", chunksActivated: payload.chunksActivated ?? 0, tokensActivated: payload.tokensActivated ?? 0, observationTokens: payload.observationTokens ?? 0, messagesActivated: payload.messagesActivated ?? 0, generationCount: payload.generationCount ?? 0, triggeredBy: payload.triggeredBy, lastActivityAt: payload.lastActivityAt, ttlExpiredMs: payload.ttlExpiredMs, activateAfterIdle: typeof payload.config?.activateAfterIdle === "number" ? payload.config.activateAfterIdle : void 0, previousModel: payload.previousModel, currentModel: payload.currentModel }); } break; } case "data-om-thread-update": { const payload = chunk.data; if (payload && payload.newTitle) { this.#session.emit({ type: "om_thread_title_updated", cycleId: payload.cycleId ?? "unknown", threadId: payload.threadId ?? this.#session.thread.getId() ?? "unknown", oldTitle: payload.oldTitle, newTitle: payload.newTitle }); } break; } case "data-mastracode-tool-progress": { const d = chunk.data; if (d?.toolCallId && d?.progress !== void 0) { this.#session.emit({ type: "tool_update", toolCallId: d.toolCallId, partialResult: d.progress }); const output = formatToolProgressOutput(d.progress); if (output) { this.#session.emit({ type: "shell_output", toolCallId: d.toolCallId, output, stream: "stdout" }); } } break; } // Sandbox streaming data chunks (from workspace execute_command tool) case "data-sandbox-stdout": { const d = chunk.data; if (d?.output && d?.toolCallId) { this.#session.emit({ type: "shell_output", toolCallId: d.toolCallId, output: d.output, stream: "stdout" }); } break; } case "data-sandbox-stderr": { const d = chunk.data; if (d?.output && d?.toolCallId) { this.#session.emit({ type: "shell_output", toolCallId: d.toolCallId, output: d.output, stream: "stderr" }); } break; } } } finishStreamState(state) { if (this.hasCurrentMessageContent(state) || !state.lastFinishedMessage) { this.#session.emit({ type: "message_end", message: state.currentMessage }); return { message: state.currentMessage, suspended: state.isSuspended || void 0 }; } return { message: state.lastFinishedMessage, suspended: state.isSuspended || void 0 }; } async finishSubscribedStreamRun({ suspended, error, aborted }) { const reason = error ? "error" : suspended ? "suspended" : aborted || this.#session.run.isAbortRequested() ? "aborted" : "complete"; this.#session.emit({ type: "agent_end", reason }); this.#session.run.reset(); await this.#session.drainFollowUpQueue(); } async handleSubscribedStreamError(error) { if (error instanceof Error && error.name === "AbortError") { this.#session.emit({ type: "agent_end", reason: "aborted" }); } else { this.#session.emit({ type: "error", error: getErrorFromUnknown(error) }); this.#session.emit({ type: "agent_end", reason: "error" }); } this.#session.stream.detach(); this.#session.run.reset(); await this.#session.drainFollowUpQueue(); } async processSubscribedThreadStream(subscription) { const requestContext = await this.#machinery.buildRequestContext(); let currentRun; try { for await (const chunk of subscription.stream) { if (!this.#session.stream.isCurrent({ subscription })) { subscription.unsubscribe(); break; } if (!currentRun) { currentRun = this.createStreamState(); this.#session.run.nextOperation(); this.#session.run.ensureAbortController(); this.#session.run.setRunId({ runId: subscription.activeRunId() ?? ("runId" in chunk ? chunk.runId : null) }); this.#session.run.setTraceId({ traceId: null }); this.#session.emit({ type: "agent_start" }); } if (chunk.type === "start") { continue; } try { const streamResult = await this.processStreamChunk(currentRun, chunk, requestContext); if (streamResult || chunk.type === "finish" || chunk.type === "error" || chunk.type === "abort" || chunk.type === "tool-call-suspended") { const suspended = chunk.type === "tool-call-suspended" || (streamResult ?? this.finishStreamState(currentRun)).suspended || void 0; const aborted = chunk.type === "abort"; let isError = chunk.type === "error"; if (currentRun.terminalError && !isError && !aborted && !this.#session.run.isAbortRequested() && !suspended) { isError = true; this.#session.emit({ type: "error", error: new Error(currentRun.terminalError) }); } await this.finishSubscribedStreamRun({ suspended, error: isError, aborted }); currentRun = void 0; if (aborted) { this.#session.stream.detach(); break; } } } catch (error) { await this.handleSubscribedStreamError(error); currentRun = void 0; } } if (currentRun && this.#session.stream.isCurrent({ subscription })) { const streamResult = this.finishStreamState(currentRun); await this.finishSubscribedStreamRun({ suspended: streamResult.suspended }); currentRun = void 0; } } catch (error) { if (this.#session.stream.isCurrent({ subscription })) { await this.handleSubscribedStreamError(error); } } } }; // src/agent-controller/session.ts function addOptionalUsageField2(usage, key, value) { if (value !== void 0) { usage[key] = (usage[key] ?? 0) + value; } } var MODE_ID_KEY = "currentModeId"; var modeModelKey = (modeId) => `modeModelId_${modeId}`; function isReservedThreadMetadataKey(key) { return key === "currentModelId" || key === MODE_ID_KEY || key === "observerModelId" || key === "reflectorModelId" || key === "observationThreshold" || key === "reflectionThreshold" || key === "tokenUsage" || key.startsWith("modeModelId_"); } var SessionIdentity = class { /** The memory resourceId the session currently reads/writes under. */ #resourceId; /** The resourceId the session started with, retained across resource switches. */ #defaultResourceId; /** Stable session identifier (mirrors SessionRecord.id in storage). */ #id; /** Stable session owner (mirrors SessionRecord.ownerId in storage). */ #ownerId; constructor({ resourceId, id, ownerId }) { this.#resourceId = resourceId; this.#defaultResourceId = resourceId; this.#id = id; this.#ownerId = ownerId; } /** The resourceId the session currently reads/writes under. */ getResourceId() { return this.#resourceId; } /** The resourceId the session started with. */ getDefaultResourceId() { return this.#defaultResourceId; } /** The stable session identifier for this session. */ getId() { return this.#id; } /** The stable owner identifier for this session. */ getOwnerId() { return this.#ownerId; } /** Point the session at a different resourceId (the default is unchanged). */ setResourceId({ resourceId }) { this.#resourceId = resourceId; } }; var SessionThread = class { /** The active thread id, or null when the session is not bound to a thread. */ #threadId = null; /** Gateway to the host's shared thread storage, injected via {@link connect}. */ #store; /** Reads the session's current resourceId (sibling identity state). */ #getResourceId; /** * The owning session, injected via {@link connect}. Thread lifecycle * transitions (create/switch/clone/delete) orchestrate sibling session * subsystems (model/mode/om/state/stream/run/usage/event bus) plus rebind the * agent subscription, so the thread domain reaches its peers through this * back-reference. Host-owned primitives (storage, lock, clone) stay behind the * injected {@link ThreadDataStore}. */ #session; constructor(getResourceId) { this.#getResourceId = getResourceId; } /** * Attach the shared-host storage gateway the thread domain reads/writes * through and the owning session whose subsystems lifecycle transitions * orchestrate. The AgentController calls this once during wiring; without a store the * data methods degrade gracefully. */ connect(store, session) { this.#store = store; this.#session = session; } /** The owning session, throwing when accessed before {@link connect}. */ get #owner() { if (!this.#session) { throw new Error("SessionThread has not been connected to its session"); } return this.#session; } /** The active thread id, or null when the session is not bound to a thread. */ getId() { return this.#threadId; } /** Whether the session is currently bound to a thread. */ isSet() { return this.#threadId !== null; } /** The active thread id, throwing when the session is not bound to a thread. */ requireId() { if (this.#threadId === null) { throw new Error("No active thread on this session"); } return this.#threadId; } /** Bind the session to a thread. */ set({ threadId }) { this.#threadId = threadId; } /** Clear the session's thread binding. */ clear() { this.#threadId = null; } /** Clear the session's thread binding and release its lock when one is held. */ async clearAndReleaseLock() { const threadId = this.#threadId; this.#threadId = null; if (threadId) { await this.#store?.releaseLock(threadId); } } // --------------------------------------------------------------------------- // Data domain: reads/queries scoped to this session, backed by host storage. // --------------------------------------------------------------------------- /** List this session's threads (its own resource by default, or all resources). */ async list(options) { if (!this.#store) { return []; } const resourceId = options?.allResources ? void 0 : this.#getResourceId(); const threads = await this.#store.listThreads({ resourceId, includeForkedSubagents: options?.includeForkedSubagents, metadata: options?.metadata }); return threads; } /** Fetch a single thread by id, or null when it doesn't exist / no storage. */ async getById({ threadId }) { if (!this.#store) return null; return this.#store.getById({ threadId }); } /** Clone a detected cross-resource project thread into this session's resource. */ async cloneToCurrentResource({ threadId, expectedResourceId, expectedProjectPath }) { if (!this.#store?.hasStorage()) { throw new Error("Memory is not configured on this AgentController"); } const thread = await this.#store.getById({ threadId }); if (!thread || thread.resourceId !== expectedResourceId || thread.metadata?.projectPath !== expectedProjectPath || expectedResourceId === this.#getResourceId()) { throw new Error(`Thread not found: ${threadId}`); } return this.#cloneThread({ sourceThreadId: thread.id, resourceId: this.#getResourceId(), title: thread.title, metadata: thread.metadata }); } /** * Load a thread and verify it belongs to this session's resourceId before * allowing access. Threads owned by another resource are treated as missing * so a session can never read, switch to, rename, or delete a thread it does * not own (the thread id is otherwise an unguessable but unscoped key). Throws * `Thread not found: <id>` when the thread is absent or owned by someone else. */ async #requireOwnedThread({ threadId }) { const thread = await this.#store?.getById({ threadId }); if (!thread || thread.resourceId !== this.#getResourceId()) { throw new Error(`Thread not found: ${threadId}`); } return thread; } /** List messages for a thread (newest-`limit`, returned oldest-first), or all. */ async listMessages({ threadId, limit }) { if (!this.#store) return []; await this.#requireOwnedThread({ threadId }); return this.#store.listMessages({ threadId, limit }); } /** List messages for the session's active thread (empty when not bound). */ async listActiveMessages({ limit } = {}) { if (this.#threadId === null) return []; return this.listMessages({ threadId: this.#threadId, limit }); } /** The first user message for a single thread, or null. */ async firstUserMessage({ threadId }) { const messages = await this.firstUserMessages({ threadIds: [threadId] }); return messages.get(threadId) ?? null; } /** The first user message for each given thread id. */ async firstUserMessages({ threadIds }) { if (!this.#store || threadIds.length === 0) return /* @__PURE__ */ new Map(); return this.#store.firstUserMessages({ threadIds }); } /** Read a setting (metadata value) for the active thread. */ async getSetting({ key }) { if (!this.#store || this.#threadId === null) return void 0; return this.#store.getMetadata({ threadId: this.#threadId, key }); } /** Persist a setting (metadata value) for the active thread. */ async setSetting({ key, value }) { if (!this.#store || this.#threadId === null) return; await this.#store.setMetadata({ threadId: this.#threadId, key, value }); } /** Delete a setting (metadata value) for the active thread. */ async deleteSetting({ key }) { if (!this.#store || this.#threadId === null) return; await this.#store.deleteMetadata({ threadId: this.#threadId, key }); } // --------------------------------------------------------------------------- // Lifecycle: transitions that bind/rebind this session to a thread. These // orchestrate sibling subsystems (model/mode/om/state/usage/event bus) and the // agent subscription via the owning session, and reach host storage/lock/clone // through the injected gateway. // --------------------------------------------------------------------------- /** Tear down the current agent subscription and reset the run tracker. */ cleanupSubscription() { this.#owner.stream.cleanup(); this.#owner.run.reset(); } /** * Ensure the session is subscribed to the given agent/thread stream, opening a * fresh subscription (and driving its run loop) when the binding changed. */ async ensureSubscription(threadId) { const session = this.#owner; const agent = session.machinery.getAgent(); const resourceId = this.#getResourceId(); const key = SessionStream.keyFor({ agent, resourceId, threadId }); if (session.stream.matches({ key })) return; this.cleanupSubscription(); const subscription = await session.machinery.subscribeToThread({ resourceId, threadId }); session.stream.attach({ subscription, key }); void session.processSubscribedThreadStream(subscription); } /** Ensure a subscription for the session's active thread (no-op when unbound). */ async ensureCurrentSubscription() { if (this.#threadId === null) return; await this.ensureSubscription(this.#threadId); } /** Detach from the current thread: abort the run and tear down the subscription. */ detachFromCurrent() { this.#owner.abort(); this.cleanupSubscription(); } /** Create a new thread, bind the session to it, and rebind the agent stream. */ async create({ title } = {}) { const session = this.#owner; const store = this.#store; this.cleanupSubscription(); const now = /* @__PURE__ */ new Date(); const thread = { id: session.machinery.generateId(), resourceId: session.identity.getResourceId(), title: title || "", createdAt: now, updatedAt: now }; const currentStateModel = session.model.get(); const currentMode = session.mode.resolve(); const modelId = currentStateModel || currentMode.defaultModelId; const metadata = {}; if (modelId) { metadata.currentModelId = modelId; metadata[`modeModelId_${session.mode.get()}`] = modelId; } const tags = session.getTags(); if (Object.keys(tags).length > 0) { for (const [key, value] of Object.entries(tags)) { if (!isReservedThreadMetadataKey(key)) metadata[key] = value; } } else { const projectPath = session.state.get().projectPath; if (projectPath) { metadata.projectPath = projectPath; } } const oldThreadId = this.#threadId; if (store) { try { await store.acquireLock(thread.id); } catch (err) { if (oldThreadId) { try { await store.acquireLock(oldThreadId); } catch { } } throw err; } if (oldThreadId) { try { await store.releaseLock(oldThreadId); } catch { } } } if (store?.hasStorage()) { try { await store.saveThread({ thread: { id: thread.id, resourceId: thread.resourceId, title: thread.title,