UNPKG

@mastra/core

Version:
1,382 lines (1,381 loc) 205 kB
import { o as getErrorFromUnknown } from "./error-MjDSls8S.js"; import { a as RequestContext } from "./request-context-p_Tq-4EM.js"; import { a as ModelRouterLanguageModel, c as defaultGateways, o as GatewayManager } from "./llm-DntEbB3j.js"; import { toStandardSchema } from "./schema/index.js"; import { r as createTool } from "./tool-qGw4ZhYO.js"; import { safeStringify } from "./utils/safe-stringify.js"; import { H as CHAT_CHANNEL_RENDER_CONTEXT_KEY, l as getServerSideFallbackInfo, t as Agent } from "./agent-Dj30gJa3.js"; import { n as hasTransformedToolPayload, t as getTransformedToolPayload } from "./payload-transform-C4k4-WlM.js"; import { n as createSignal } from "./signals-DTzJ08gd.js"; import { d as taskCheckTool, f as taskCompleteTool, m as taskWriteTool, p as taskUpdateTool, u as summarizeTaskCheck } from "./task-state-processor-C9agcUfw.js"; import { t as createWorkspaceTools, x as Workspace } from "./workspace-CvZ9jmZN.js"; import { s as askUserTool, t as submitPlanTool } from "./tools-DdVMYter.js"; import { t as AgentControllerChannels } from "./channels-BQCMfPzU.js"; import { t as Mastra } from "./mastra-Qdk2LI3v.js"; import { randomUUID } from "crypto"; import { z } from "zod/v4"; //#region src/agent-controller/stream-content.ts /** * Pure transforms shared by the run engine for folding raw agent-stream chunk * payloads into the `MastraDBMessage` shape a Session renders, plus terminal * finish-reason and server-side-fallback diagnostics. They hold no * AgentController or Session state, so they live in their own module. */ function getDisplayTransform(metadata, phase, fallback) { const transform = getTransformedToolPayload(metadata, "display", phase); return hasTransformedToolPayload(transform) ? transform.transformed : fallback; } function isRecord$1(value) { return typeof value === "object" && value !== null; } function getAnthropicStopDetails(providerMetadata) { if (!isRecord$1(providerMetadata)) return; const anthropic = providerMetadata.anthropic; if (!isRecord$1(anthropic)) return; const stopDetails = anthropic.stopDetails; return isRecord$1(stopDetails) ? stopDetails : void 0; } /** * Map a non-success terminal finish reason (content-filter, error, length) to a * user-facing message, or `undefined` for success reasons. A non-success finish * must become an explicit terminal error rather than a silent `complete`. */ function describeNonSuccessFinishReason(reason, providerMetadata) { switch (reason) { case "content-filter": { const stopDetails = getAnthropicStopDetails(providerMetadata); 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; } } /** * Build a user-facing notice when a turn was served by an Anthropic server-side * fallback model instead of the primary model. Without a notice the user has no * way to tell the response did not come from the model they selected. */ function describeServerSideFallback(providerMetadata) { const fallback = getServerSideFallbackInfo(providerMetadata); if (!fallback) return; 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."; } /** Coerce a usage field to a finite number, accepting numeric strings. */ 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; } } /** Fold an optional usage field into a tally when present. */ function addOptionalUsageField$1(usage, key, value) { if (value !== void 0) usage[key] = (usage[key] ?? 0) + value; } //#endregion //#region src/agent-controller/session-run-engine.ts function isRecord(value) { return typeof value === "object" && value !== null; } function isProviderMetadata(value) { return isRecord(value); } function getString(value) { return typeof value === "string" ? value : void 0; } function getNumber(value, fallback) { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } function getOptionalNumber(value) { return typeof value === "number" && Number.isFinite(value) ? value : void 0; } function getBoolean(value, fallback) { return typeof value === "boolean" ? value : fallback; } function getRecord(value) { return isRecord(value) ? value : void 0; } function getPayload(chunk) { return "payload" in chunk ? getRecord(chunk.payload) ?? {} : {}; } function getDataRecord(chunk) { return "data" in chunk ? getRecord(chunk.data) : void 0; } function getNestedRecord(record, key) { return record ? getRecord(record[key]) : void 0; } function isGoalEvaluationPayload(value) { const record = getRecord(value); return Boolean(record && typeof record.objective === "string" && typeof record.iteration === "number" && typeof record.maxRuns === "number" && typeof record.passed === "boolean" && (record.status === "active" || record.status === "paused" || record.status === "done") && Array.isArray(record.results) && typeof record.duration === "number" && typeof record.timedOut === "boolean" && typeof record.maxRunsReached === "boolean" && typeof record.suppressFeedback === "boolean"); } function getOperationType(value) { return value === "reflection" ? "reflection" : "observation"; } function getActivationTrigger(value) { if (value === "ttl" || value === "threshold" || value === "provider_change") return value; } function getOmStatus(value) { if (value === "running" || value === "complete") return value; return "idle"; } function formatToolProgressOutput(progress) { if (typeof progress === "string") return progress.endsWith("\n") ? progress : `${progress}\n`; if (typeof progress !== "object" || progress === null) return `${String(progress)}\n`; const record = progress; const parts = [record.status, record.detail].filter((part) => typeof part === "string" && part.length > 0); return parts.length > 0 ? `${parts.join(": ")}\n` : `${JSON.stringify(progress)}\n`; } /** * The per-session agent run engine: it consumes an agent's event stream, folds * each chunk into the session's display messages and token usage, drives tool * approval/suspension, and finalizes the run. In the multi-user host the run * loop, run state, and thread stream are per-session and cannot be shared, so * they live on the Session — this engine is owned by exactly one Session. * * It reaches the host only through the narrow {@link SessionMachinery} it is * constructed with (resolve the agent, build run/stream options, persist usage, * drive tool approval/resume, drain follow-ups). It never reaches back into the * AgentController or another session: all per-run state is read and written on its own * {@link Session}. */ var SessionRunEngine = class { #session; #machinery; #requestContext; constructor(session, machinery) { this.#session = session; this.#machinery = machinery; } setRequestContext(requestContext) { if (requestContext) this.#requestContext = requestContext; } createEmptyAssistantMessage() { return { id: this.#machinery.generateId(), role: "assistant", content: { format: 2, parts: [] }, createdAt: /* @__PURE__ */ new Date() }; } /** * Build a DB-native signal message from a streamed `data-signal` / * `data-user-message` / `data-system-reminder` chunk. The raw data-part is * carried verbatim on `content.parts` and the signal identity is preserved on * `content.metadata.signal` so consumers read the native shape (no flattening). */ createSignalMessage(partType, payload) { const part = { type: partType, data: payload }; const signalId = typeof payload.id === "string" ? payload.id : this.#machinery.generateId(); const createdAt = typeof payload.createdAt === "string" && !Number.isNaN(Date.parse(payload.createdAt)) ? new Date(payload.createdAt) : /* @__PURE__ */ new Date(); return { id: signalId, role: "signal", content: { format: 2, parts: [part], metadata: { signal: payload } }, createdAt }; } hasCurrentMessageContent(state) { return state.currentMessage.content.parts.length > 0; } /** * Snapshot a message for emission. The engine mutates parts in place * (text/reasoning deltas, tool-invocation upgrades) and `setStopReason` / * `setErrorMessage` mutate `content.metadata`, so emitted snapshots must * deep-clone the content or later mutations rewrite earlier snapshots. */ cloneMessage(message) { return { ...message, content: structuredClone(message.content) }; } setStopReason(message, stopReason, force = false) { message.content.metadata ??= {}; const metadata = message.content.metadata; if (force) metadata.stopReason = stopReason; else metadata.stopReason ??= stopReason; } setErrorMessage(message, errorMessage) { message.content.metadata ??= {}; message.content.metadata.errorMessage = errorMessage; } finishCurrentMessageAndRotate(state) { if (!this.hasCurrentMessageContent(state)) return; this.setStopReason(state.currentMessage, "complete"); this.#session.emit({ type: "message_end", message: state.currentMessage }); state.lastFinishedMessage = state.currentMessage; state.currentMessage = this.createEmptyAssistantMessage(); state.textContentById.clear(); state.thinkingContentById.clear(); state.toolPartById.clear(); } createStreamState() { return { currentMessage: this.createEmptyAssistantMessage(), isSuspended: false, textContentById: /* @__PURE__ */ new Map(), thinkingContentById: /* @__PURE__ */ new Map(), toolPartById: /* @__PURE__ */ new Map() }; } abortForOmFailure({ operationType, stage, error }) { this.#session.emit({ type: "error", error: /* @__PURE__ */ 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.parts.length; state.currentMessage.content.parts.push({ type: "text", text: "" }); state.textContentById.set(getString(getPayload(chunk).id) ?? "", { index: textIndex, text: "" }); this.#session.emit({ type: "message_start", message: this.cloneMessage(state.currentMessage) }); break; } case "text-delta": { const textState = state.textContentById.get(getString(getPayload(chunk).id) ?? ""); if (textState) { textState.text += getString(getPayload(chunk).text) ?? ""; const textContent = state.currentMessage.content.parts[textState.index]; if (textContent && textContent.type === "text") textContent.text = textState.text; this.#session.emit({ type: "message_update", message: this.cloneMessage(state.currentMessage) }); } break; } case "reasoning-start": { const thinkingIndex = state.currentMessage.content.parts.length; state.currentMessage.content.parts.push({ type: "reasoning", reasoning: "", details: [] }); state.thinkingContentById.set(getString(getPayload(chunk).id) ?? "", { index: thinkingIndex, text: "" }); this.#session.emit({ type: "message_update", message: this.cloneMessage(state.currentMessage) }); break; } case "reasoning-delta": { const thinkingState = state.thinkingContentById.get(getString(getPayload(chunk).id) ?? ""); if (thinkingState) { thinkingState.text += getString(getPayload(chunk).text) ?? ""; const thinkingContent = state.currentMessage.content.parts[thinkingState.index]; if (thinkingContent && thinkingContent.type === "reasoning") { thinkingContent.reasoning = thinkingState.text; thinkingContent.details = [{ type: "text", text: thinkingState.text }]; } this.#session.emit({ type: "message_update", message: this.cloneMessage(state.currentMessage) }); } break; } case "tool-call-input-streaming-start": { const payload = getPayload(chunk); const toolCallId = getString(payload.toolCallId) ?? ""; const toolName = getString(payload.toolName) ?? ""; this.#session.emit({ type: "tool_input_start", toolCallId, toolName }); break; } case "tool-call-delta": { const payload = getPayload(chunk); const toolCallId = getString(payload.toolCallId) ?? ""; const argsTextDelta = getString(payload.argsTextDelta) ?? ""; const toolName = getString(payload.toolName); 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 = getString(getPayload(chunk).toolCallId) ?? ""; this.#session.emit({ type: "tool_input_end", toolCallId }); break; } case "tool-call": { const toolCall = getPayload(chunk); const toolCallId = getString(toolCall.toolCallId) ?? ""; const toolName = getString(toolCall.toolName) ?? ""; const args = getDisplayTransform(chunk.metadata, "input-available", toolCall.args); const toolIndex = state.currentMessage.content.parts.length; state.currentMessage.content.parts.push({ type: "tool-invocation", toolInvocation: { state: "call", toolCallId, toolName, args } }); state.toolPartById.set(toolCallId, toolIndex); this.#session.emit({ type: "tool_start", toolCallId, toolName, args }); this.#session.emit({ type: "message_update", message: this.cloneMessage(state.currentMessage) }); break; } case "tool-result": { const toolResult = getPayload(chunk); const toolCallId = getString(toolResult.toolCallId) ?? ""; const toolName = getString(toolResult.toolName) ?? ""; const providerMetadata = isProviderMetadata(toolResult.providerMetadata) ? toolResult.providerMetadata : void 0; const result = getDisplayTransform(chunk.metadata, "output-available", toolResult.result); const isError = getBoolean(toolResult.isError, false); const toolIndex = state.toolPartById.get(toolCallId); const existing = toolIndex !== void 0 ? state.currentMessage.content.parts[toolIndex] : void 0; if (existing && existing.type === "tool-invocation") { existing.toolInvocation = Object.assign(existing.toolInvocation, { state: "result", result, isError }); if (providerMetadata) existing.providerMetadata = providerMetadata; } else { const toolInvocationPart = { type: "tool-invocation", toolInvocation: Object.assign({ state: "result", toolCallId, toolName, args: {}, result }, { isError }) }; if (providerMetadata) toolInvocationPart.providerMetadata = providerMetadata; state.currentMessage.content.parts.push(toolInvocationPart); } this.#session.emit({ type: "tool_end", toolCallId, result, isError, ...providerMetadata ? { providerMetadata } : {} }); this.#session.emit({ type: "message_update", message: this.cloneMessage(state.currentMessage) }); break; } case "tool-error": { const toolError = getPayload(chunk); const toolCallId = getString(toolError.toolCallId) ?? ""; this.#session.emit({ type: "tool_end", toolCallId, result: getDisplayTransform(chunk.metadata, "error", toolError.error), isError: true }); break; } case "tool-call-approval": { const toolCallId = getString(getPayload(chunk).toolCallId) ?? ""; const toolName = getString(getPayload(chunk).toolName) ?? ""; const approvalTransform = getTransformedToolPayload(chunk.metadata, "display", "approval"); const toolArgs = hasTransformedToolPayload(approvalTransform) ? approvalTransform.transformed : getDisplayTransform(chunk.metadata, "input-available", getPayload(chunk).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 = getString(getPayload(chunk).toolCallId) ?? ""; const suspToolName = getString(getPayload(chunk).toolName) ?? ""; const suspArgs = getDisplayTransform(chunk.metadata, "input-available", getPayload(chunk).args); const suspPayload = getDisplayTransform(chunk.metadata, "suspend", getPayload(chunk).suspendPayload); const suspResumeSchema = getString(getPayload(chunk).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(getPayload(chunk).error); this.#session.emit({ type: "error", error: streamError }); const failedRunId = chunk.runId ?? this.#session.run.getRunId(); if (failedRunId) for (const { toolCallId, toolName } of this.#session.suspensions.deleteForRun({ runId: failedRunId })) this.#session.emit({ type: "tool_suspension_cancelled", toolCallId, toolName, reason: streamError.message }); break; } case "step-finish": { const usage = getRecord(getPayload(chunk).output)?.usage; const usageRecord = getRecord(usage); if (usageRecord) { const promptTokens = getUsageNumber(usageRecord, "promptTokens") ?? getUsageNumber(usageRecord, "inputTokens") ?? 0; const completionTokens = getUsageNumber(usageRecord, "completionTokens") ?? getUsageNumber(usageRecord, "outputTokens") ?? 0; const stepUsage = { promptTokens, completionTokens, totalTokens: getUsageNumber(usageRecord, "totalTokens") ?? promptTokens + completionTokens }; addOptionalUsageField$1(stepUsage, "reasoningTokens", getUsageNumber(usageRecord, "reasoningTokens")); addOptionalUsageField$1(stepUsage, "cachedInputTokens", getUsageNumber(usageRecord, "cachedInputTokens")); addOptionalUsageField$1(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 = getString(getRecord(getPayload(chunk).stepResult)?.reason) ?? ""; const finishProviderMetadata = getRecord(getPayload(chunk).metadata)?.providerMetadata ?? getPayload(chunk).providerMetadata; const fallbackNotice = describeServerSideFallback(finishProviderMetadata); if (fallbackNotice) this.#session.emit({ type: "info", message: fallbackNotice }); if (finishReason === "stop" || finishReason === "end-turn") this.setStopReason(state.currentMessage, "complete", true); else if (finishReason === "tool-calls") this.setStopReason(state.currentMessage, "tool_use", true); else { const errorMessage = describeNonSuccessFinishReason(finishReason, finishProviderMetadata); if (errorMessage) { this.setStopReason(state.currentMessage, "error", true); this.setErrorMessage(state.currentMessage, errorMessage); state.terminalError = errorMessage; } else this.setStopReason(state.currentMessage, "complete", true); } break; } case "goal": { this.finishCurrentMessageAndRotate(state); const goalPayload = getPayload(chunk); if (isGoalEvaluationPayload(goalPayload)) this.#session.emit({ type: "goal_evaluation", payload: goalPayload }); break; } case "data-om-status": { const d = getDataRecord(chunk); const w = getRecord(d?.windows); if (d && w) { const active = getNestedRecord(w, "active"); const msgs = getNestedRecord(active, "messages"); const obs = getNestedRecord(active, "observations"); const buffered = getNestedRecord(w, "buffered"); const buffObs = getNestedRecord(buffered, "observations"); const buffRef = getNestedRecord(buffered, "reflection"); this.#session.emit({ type: "om_status", windows: { active: { messages: { tokens: getNumber(msgs?.tokens, 0), threshold: getNumber(msgs?.threshold, 0) }, observations: { tokens: getNumber(obs?.tokens, 0), threshold: getNumber(obs?.threshold, 0) } }, buffered: { observations: { status: getOmStatus(buffObs?.status), chunks: getNumber(buffObs?.chunks, 0), messageTokens: getNumber(buffObs?.messageTokens, 0), projectedMessageRemoval: getNumber(buffObs?.projectedMessageRemoval, 0), observationTokens: getNumber(buffObs?.observationTokens, 0) }, reflection: { status: getOmStatus(buffRef?.status), inputObservationTokens: getNumber(buffRef?.inputObservationTokens, 0), observationTokens: getNumber(buffRef?.observationTokens, 0) } } }, recordId: getString(d.recordId) ?? "", threadId: getString(d.threadId) ?? "", stepNumber: getNumber(d.stepNumber, 0), generationCount: getNumber(d.generationCount, 0) }); } break; } case "data-om-observation-start": { const payload = getDataRecord(chunk); const cycleId = getString(payload?.cycleId); if (payload && cycleId) { const operationType = getOperationType(payload.operationType); if (operationType === "observation") this.#session.emit({ type: "om_observation_start", cycleId, operationType, tokensToObserve: getNumber(payload.tokensToObserve, 0) }); else this.#session.emit({ type: "om_reflection_start", cycleId, tokensToReflect: getNumber(payload.tokensToObserve, 0) }); } break; } case "data-om-observation-end": { const payload = getDataRecord(chunk); const cycleId = getString(payload?.cycleId); if (payload && cycleId) if (payload.operationType === "reflection") this.#session.emit({ type: "om_reflection_end", cycleId, durationMs: getNumber(payload.durationMs, 0), compressedTokens: getNumber(payload.observationTokens, 0), observations: getString(payload.observations) }); else this.#session.emit({ type: "om_observation_end", cycleId, durationMs: getNumber(payload.durationMs, 0), tokensObserved: getNumber(payload.tokensObserved, 0), observationTokens: getNumber(payload.observationTokens, 0), observations: getString(payload.observations), currentTask: getString(payload.currentTask), suggestedResponse: getString(payload.suggestedResponse) }); break; } case "data-om-observation-failed": { const payload = getDataRecord(chunk); if (payload) { const operationType = getOperationType(payload.operationType); const error = getString(payload.error) ?? "Unknown error"; if (operationType === "reflection") this.#session.emit({ type: "om_reflection_failed", cycleId: getString(payload.cycleId) ?? "unknown", error, durationMs: getNumber(payload.durationMs, 0) }); else this.#session.emit({ type: "om_observation_failed", cycleId: getString(payload.cycleId) ?? "unknown", error, durationMs: getNumber(payload.durationMs, 0) }); this.abortForOmFailure({ operationType, stage: "run", error }); return { message: state.currentMessage }; } break; } case "data-om-buffering-start": { const payload = getDataRecord(chunk); const cycleId = getString(payload?.cycleId); if (payload && cycleId) this.#session.emit({ type: "om_buffering_start", cycleId, operationType: getOperationType(payload.operationType), tokensToBuffer: getNumber(payload.tokensToBuffer, 0) }); break; } case "data-om-buffering-end": { const payload = getDataRecord(chunk); const cycleId = getString(payload?.cycleId); if (payload && cycleId) this.#session.emit({ type: "om_buffering_end", cycleId, operationType: getOperationType(payload.operationType), tokensBuffered: getNumber(payload.tokensBuffered, 0), bufferedTokens: getNumber(payload.bufferedTokens, 0), observations: getString(payload.observations) }); break; } case "data-om-buffering-failed": { const payload = getDataRecord(chunk); if (payload) { const operationType = getOperationType(payload.operationType); const error = getString(payload.error) ?? "Unknown error"; this.#session.emit({ type: "om_buffering_failed", cycleId: getString(payload.cycleId) ?? "unknown", operationType, error }); this.abortForOmFailure({ operationType, stage: "buffering", error }); return { message: state.currentMessage }; } break; } case "data-signal": { const payload = getDataRecord(chunk); if (payload) { const message = this.createSignalMessage("data-signal", payload); this.#session.emit({ type: "message_start", message }); this.#session.emit({ type: "message_end", message }); } break; } case "data-user-message": { const payload = getDataRecord(chunk); if (payload) { this.finishCurrentMessageAndRotate(state); const message = this.createSignalMessage("data-user-message", payload); this.#session.emit({ type: "message_start", message }); this.#session.emit({ type: "message_end", message }); } break; } case "data-system-reminder": { const payload = getDataRecord(chunk); if (payload) { const message = this.createSignalMessage("data-system-reminder", payload); this.#session.emit({ type: "message_start", message }); this.#session.emit({ type: "message_end", message }); } break; } case "data-om-activation": { const payload = getDataRecord(chunk); const cycleId = getString(payload?.cycleId); if (payload && cycleId) this.#session.emit({ type: "om_activation", cycleId, operationType: getOperationType(payload.operationType), chunksActivated: getNumber(payload.chunksActivated, 0), tokensActivated: getNumber(payload.tokensActivated, 0), observationTokens: getNumber(payload.observationTokens, 0), messagesActivated: getNumber(payload.messagesActivated, 0), generationCount: getNumber(payload.generationCount, 0), triggeredBy: getActivationTrigger(payload.triggeredBy), lastActivityAt: getOptionalNumber(payload.lastActivityAt), ttlExpiredMs: getOptionalNumber(payload.ttlExpiredMs), activateAfterIdle: getOptionalNumber(getRecord(payload.config)?.activateAfterIdle), previousModel: getString(payload.previousModel), currentModel: getString(payload.currentModel) }); break; } case "data-om-thread-update": { const payload = getDataRecord(chunk); const newTitle = getString(payload?.newTitle); if (payload && newTitle) this.#session.emit({ type: "om_thread_title_updated", cycleId: getString(payload.cycleId) ?? "unknown", threadId: getString(payload.threadId) ?? this.#session.thread.getId() ?? "unknown", oldTitle: getString(payload.oldTitle), 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; } case "data-sandbox-stdout": { const d = getDataRecord(chunk); const output = getString(d?.output); const toolCallId = getString(d?.toolCallId); if (output && toolCallId) this.#session.emit({ type: "shell_output", toolCallId, output, stream: "stdout" }); break; } case "data-sandbox-stderr": { const d = getDataRecord(chunk); const output = getString(d?.output); const toolCallId = getString(d?.toolCallId); if (output && toolCallId) this.#session.emit({ type: "shell_output", toolCallId, output, stream: "stderr" }); break; } default: 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) { 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 requestContext = await this.#machinery.buildRequestContext(this.#requestContext); 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); } } }; //#endregion //#region src/agent-controller/types.ts /** Creates a zero-initialized TokenUsage object. */ function createEmptyTokenUsage() { return { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedInputTokens: 0, cacheCreationInputTokens: 0 }; } /** * Creates the default/initial `AgentControllerDisplayState`. */ 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: [] }; } /** * Creates the default OM progress state. */ 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 }; } //#endregion //#region src/agent-controller/session.ts function addOptionalUsageField(usage, key, value) { if (value !== void 0) usage[key] = (usage[key] ?? 0) + value; } /** Persisted thread-setting key for the currently-selected mode. */ const MODE_ID_KEY = "currentModeId"; /** Persisted thread-setting key prefix for a mode's last-used model. */ const modeModelKey = (modeId) => `modeModelId_${modeId}`; /** * Internal thread-metadata keys used by `Session.loadMetadata()` to persist * runtime bookkeeping (selected model/mode, observer/reflector config, token * usage). These share the flat thread `metadata` bag with user-provided * session scoping tags, so they must never be treated as tags: they are * skipped when stamping tags onto a thread and excluded when reading tags * back out of thread metadata. */ function isReservedThreadMetadataKey(key) { return key === "currentModelId" || key === MODE_ID_KEY || key === "observerModelId" || key === "reflectorModelId" || key === "observationThreshold" || key === "reflectionThreshold" || key === "tokenUsage" || key.startsWith("modeModelId_"); } /** * Owns the session's identity: the memory `resourceId` and the active * `threadId` this session reads and writes under. Together they form the memory * binding (`{ thread, resource }`) every run uses. In a multi-user host one * AgentController serves many sessions, so this identity — "whose session is this, and * which thread is it on" — belongs to the Session, not the AgentController. * * `defaultResourceId` is the resourceId the session started with; switching to a * different resource (e.g. impersonation, or browsing another user's threads) * updates the current resourceId while the default is retained so the session * can return to its own identity. * * `id` is the stable identifier for this session (mirrors `SessionRecord.id` in * storage) and `ownerId` is the owner of this session (mirrors * `SessionRecord.ownerId`). Both are stable for the life of the session and do * not change when the resourceId is switched. * * The active thread the session is bound to lives on {@link SessionThread}, not * here — identity is the stable "who", the thread is the navigational "where". */ 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; } }; /** * Owns the session's thread domain: the navigational binding (which thread the * session is currently on) plus the data reads/queries scoped to it. `null` * until the session is bound (a thread is created, switched to, or reacquired on * startup); switching/deleting updates it. * * In the multi-user model each session has its own current thread and reads its * own threads, while the AgentController host shares storage, the thread lock, and the * event bus. So the binding + data queries are per-session and live here; the * session leverages the host's storage via an injected {@link ThreadDataStore}. * Lifecycle *transitions* (create/switch/clone/delete) remain host machinery * because they drive the shared event bus and rebind the shared agent stream. */ 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); } /** 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(); return await this.#store.listThreads({ resourceId, includeForkedSubagents: options?.includeForkedSubagents, metadata: options?.metadata }); } /** 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 }) { return (await this.firstUserMessages({ threadIds: [threadId] })).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 }); } /** 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 }); 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, id } = {}) { const session = this.#owner; const store = this.#store; this.cleanupSubscription(); const now = /* @__PURE__ */ new Date(); const thread = { id: 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; } Object.assign(metadata, session.getThreadScope()); 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, createdAt: thread.createdAt, updatedAt: thread.updatedAt, metadata: Object.keys(metadata).length > 0 ? metadata : void 0 } }); } catch (err) { let reacquired = false; try { await store.releaseLock(thread.id); } catch {} if (oldThreadId) try { await store.acquireLock(oldThreadId); reacquired = true; } catch {} if (reacquired && oldThreadId) this.set({ threadId: oldThreadId }); else this.clear();