@mastra/core
Version:
1,254 lines (1,249 loc) • 212 kB
JavaScript
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,