@mastra/core
Version:
1,382 lines (1,381 loc) • 205 kB
JavaScript
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();