@mastra/core
Version:
Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.
1,351 lines (1,328 loc) • 161 kB
JavaScript
import { Mastra } from '../chunk-FIJEBQM6.js';
import { Agent } from '../chunk-AM3IOVFX.js';
import { Workspace, createWorkspaceTools } from '../chunk-THS5TQZD.js';
import { safeStringify } from '../chunk-K3E3M5U5.js';
import { createTool } from '../chunk-6FFXBNBE.js';
import { toStandardSchema } from '../chunk-6SRTDZ7S.js';
import { createSignal, mastraDBMessageToSignal } from '../chunk-NRWJGKOK.js';
import { getTransformedToolPayload, hasTransformedToolPayload } from '../chunk-GNP47JBD.js';
import { RequestContext } from '../chunk-BBVL3KAA.js';
import { randomUUID } from 'crypto';
import { z } from 'zod/v4';
// src/harness/display-state-scheduler.ts
var DEFAULT_DISPLAY_STATE_SUBSCRIPTION_OPTIONS = {
windowMs: 250,
maxWaitMs: 500
};
var CRITICAL_DISPLAY_STATE_EVENT_TYPES = /* @__PURE__ */ new Set([
"agent_start",
"agent_end",
"error",
"tool_approval_required",
"tool_suspended",
"ask_question",
"plan_approval_required",
"plan_approved",
"thread_changed",
"thread_created",
"thread_deleted",
"mode_changed",
"model_changed",
"subagent_model_changed",
"state_changed",
"tool_input_end",
"tool_end",
"subagent_end"
]);
function cloneValue(value, seen = /* @__PURE__ */ new WeakMap()) {
if (value === null || typeof value !== "object") {
return value;
}
if (value instanceof Date) {
return new Date(value.getTime());
}
if (seen.has(value)) {
return seen.get(value);
}
if (Array.isArray(value)) {
const cloned2 = [];
seen.set(value, cloned2);
for (const item of value) {
cloned2.push(cloneValue(item, seen));
}
return cloned2;
}
if (value instanceof Map) {
const cloned2 = /* @__PURE__ */ new Map();
seen.set(value, cloned2);
for (const [key, mapValue] of value) {
cloned2.set(cloneValue(key, seen), cloneValue(mapValue, seen));
}
return cloned2;
}
if (value instanceof Set) {
const cloned2 = /* @__PURE__ */ new Set();
seen.set(value, cloned2);
for (const item of value) {
cloned2.add(cloneValue(item, seen));
}
return cloned2;
}
const cloned = {};
seen.set(value, cloned);
for (const key of Reflect.ownKeys(value)) {
cloned[key] = cloneValue(value[key], seen);
}
return cloned;
}
function cloneUnknown(value) {
return cloneValue(value);
}
function cloneDisplayState(state) {
return {
...state,
currentMessage: state.currentMessage ? {
...state.currentMessage,
createdAt: new Date(state.currentMessage.createdAt.getTime()),
content: state.currentMessage.content.map((part) => cloneUnknown(part))
} : null,
tokenUsage: { ...state.tokenUsage },
activeTools: new Map(
Array.from(state.activeTools, ([id, tool]) => [
id,
{
...tool,
args: cloneUnknown(tool.args),
result: cloneUnknown(tool.result)
}
])
),
toolInputBuffers: new Map(Array.from(state.toolInputBuffers, ([id, buffer]) => [id, { ...buffer }])),
pendingApproval: state.pendingApproval ? { ...state.pendingApproval, args: cloneUnknown(state.pendingApproval.args) } : null,
pendingSuspension: state.pendingSuspension ? {
...state.pendingSuspension,
args: cloneUnknown(state.pendingSuspension.args),
suspendPayload: cloneUnknown(state.pendingSuspension.suspendPayload)
} : null,
pendingQuestion: state.pendingQuestion ? {
...state.pendingQuestion,
options: state.pendingQuestion.options?.map((option) => cloneUnknown(option))
} : null,
pendingPlanApproval: state.pendingPlanApproval ? { ...state.pendingPlanApproval } : null,
activeSubagents: new Map(
Array.from(state.activeSubagents, ([id, subagent]) => [
id,
{
...subagent,
toolCalls: subagent.toolCalls.map((toolCall) => cloneUnknown(toolCall))
}
])
),
omProgress: {
...state.omProgress,
buffered: {
observations: { ...state.omProgress.buffered.observations },
reflection: { ...state.omProgress.buffered.reflection }
}
},
modifiedFiles: new Map(
Array.from(state.modifiedFiles, ([path, modifiedFile]) => [
path,
{
...modifiedFile,
firstModified: new Date(modifiedFile.firstModified.getTime()),
operations: [...modifiedFile.operations]
}
])
),
tasks: state.tasks.map((task) => cloneUnknown(task)),
previousTasks: state.previousTasks.map((task) => cloneUnknown(task))
};
}
var DisplayStateScheduler = class {
constructor(listener, windowMs, maxWaitMs) {
this.listener = listener;
this.windowMs = windowMs;
this.maxWaitMs = maxWaitMs;
}
listener;
windowMs;
maxWaitMs;
disposed = false;
pendingState = null;
windowTimer = null;
maxWaitTimer = null;
notify(state, isCritical) {
if (this.disposed) return;
if (isCritical) {
this.flush(state);
return;
}
this.pendingState = state;
if (this.windowTimer) {
clearTimeout(this.windowTimer);
}
this.windowTimer = setTimeout(() => this.flushPending(), this.windowMs);
if (!this.maxWaitTimer) {
this.maxWaitTimer = setTimeout(() => this.flushPending(), this.maxWaitMs);
}
}
dispose() {
this.disposed = true;
this.pendingState = null;
this.clearTimers();
}
flushPending() {
if (!this.pendingState) return;
this.flush(this.pendingState);
}
flush(state) {
if (this.disposed) return;
this.pendingState = null;
this.clearTimers();
try {
const result = this.listener(cloneDisplayState(state));
if (result && typeof result === "object" && "catch" in result && typeof result.catch === "function") {
result.catch((err) => console.error("Error in harness display state listener:", err));
}
} catch (err) {
console.error("Error in harness display state listener:", err);
}
}
clearTimers() {
if (this.windowTimer) {
clearTimeout(this.windowTimer);
}
if (this.maxWaitTimer) {
clearTimeout(this.maxWaitTimer);
}
this.windowTimer = null;
this.maxWaitTimer = null;
}
};
var questionCounter = 0;
var planCounter = 0;
var FORKED_SUBAGENT_NESTING_NOTICE = "Do not call the `subagent` tool. You are currently running inside a forked subagent, and this is the maximum allowed subagent nesting level. Further subagent calls will return an error. Answer the task directly using the conversation history and the other tools available to you.";
var FORKED_SUBAGENT_TASK_NOTICE = "Do not call `task_write`, `task_update`, `task_complete`, or `task_check` inside a forked subagent. Forked subagents keep the parent task-tool schemas for prompt-cache stability, but the parent agent owns the visible task list. Track forked subagent work in your final answer instead.";
function formatQuestionAnswer(answer) {
return Array.isArray(answer) ? answer.join(", ") : answer;
}
var askUserTool = createTool({
id: "ask_user",
description: "Ask the user a question and wait for their response. Use this when you need clarification, want to validate assumptions, or need the user to make a decision between options. Provide options for structured choices (2-4 options), or omit them for open-ended questions. Use selectionMode to choose whether the user can pick one option or multiple options.",
inputSchema: z.object({
question: z.string().min(1).describe("The question to ask the user. Should be clear and specific."),
options: z.array(
z.object({
label: z.string().describe("Short display text for this option (1-5 words)"),
description: z.string().optional().describe("Explanation of what this option means")
})
).optional().describe("Optional choices. If provided, shows a selection list. If omitted, shows a free-text input."),
selectionMode: z.enum(["single_select", "multi_select"]).optional().describe(
"Controls how many provided options the user can select. Defaults to single_select when options are provided. Requires options."
)
}),
execute: async ({ question, options, selectionMode }, context) => {
try {
const harnessCtx = context?.requestContext?.get("harness");
const resolvedSelectionMode = options?.length ? selectionMode ?? "single_select" : void 0;
if (selectionMode && !options?.length) {
return {
content: "Failed to ask user: selectionMode requires options.",
isError: true
};
}
if (!harnessCtx?.emitEvent || !harnessCtx?.registerQuestion) {
return {
content: `[Question for user]: ${question}${options?.length ? "\nOptions: " + options.map((o) => o.label).join(", ") : ""}${resolvedSelectionMode ? "\nSelection mode: " + resolvedSelectionMode : ""}`,
isError: false
};
}
const questionId = `q_${++questionCounter}_${Date.now()}`;
const answer = await new Promise((resolve, reject) => {
const signal = harnessCtx.abortSignal;
if (signal?.aborted) {
reject(new DOMException("Aborted", "AbortError"));
return;
}
const onAbort = () => reject(new DOMException("Aborted", "AbortError"));
signal?.addEventListener("abort", onAbort, { once: true });
harnessCtx.registerQuestion({
questionId,
resolve: (answer2) => {
signal?.removeEventListener("abort", onAbort);
resolve(answer2);
}
});
harnessCtx.emitEvent({
type: "ask_question",
questionId,
question,
options,
selectionMode: resolvedSelectionMode
});
});
return { content: `User answered: ${formatQuestionAnswer(answer)}`, isError: false };
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
return { content: `Failed to ask user: ${msg}`, isError: true };
}
}
});
var submitPlanTool = createTool({
id: "submit_plan",
description: "Submit a completed implementation plan for user review. The plan will be rendered as markdown and the user can approve, reject, or request changes. Use this when your exploration is complete and you have a concrete plan ready for review. On approval, the system automatically switches to the default mode so you can implement.",
inputSchema: z.object({
title: z.string().optional().describe("Short title for the plan (e.g., 'Add dark mode toggle')"),
plan: z.string().min(1).describe("The full plan content in markdown format. Should include Overview, Steps, and Verification sections.")
}),
execute: async ({ title, plan }, context) => {
try {
const harnessCtx = context?.requestContext?.get("harness");
if (!harnessCtx?.emitEvent || !harnessCtx?.registerPlanApproval) {
return {
content: `[Plan submitted for review]
Title: ${title || "Implementation Plan"}
${plan}`,
isError: false
};
}
const planId = `plan_${++planCounter}_${Date.now()}`;
const result = await new Promise((resolve, reject) => {
const signal = harnessCtx.abortSignal;
if (signal?.aborted) {
reject(new DOMException("Aborted", "AbortError"));
return;
}
const onAbort = () => reject(new DOMException("Aborted", "AbortError"));
signal?.addEventListener("abort", onAbort, { once: true });
harnessCtx.registerPlanApproval({
planId,
resolve: (res) => {
signal?.removeEventListener("abort", onAbort);
resolve(res);
}
});
harnessCtx.emitEvent({
type: "plan_approval_required",
planId,
title: title || "Implementation Plan",
plan
});
});
if (result.action === "approved") {
return {
content: "Plan approved. Proceed with implementation following the approved plan.",
isError: false
};
}
const feedback = result.feedback ? `
User feedback: ${result.feedback}` : "";
return {
content: `Plan was not approved. The user wants revisions.${feedback}
Please revise the plan based on the feedback and submit again with submit_plan.`,
isError: false
};
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
return { content: `Failed to submit plan: ${msg}`, isError: true };
}
}
});
var taskIdSchema = z.string().min(1).describe("Stable task identifier (for example, 'task_investigate_tests'). Keep this unchanged across updates.");
var taskItemInputSchema = z.object({
id: taskIdSchema.optional(),
content: z.string().min(1).describe("Task description in imperative form (e.g., 'Fix authentication bug')"),
status: z.enum(["pending", "in_progress", "completed"]).describe("Current task status"),
activeForm: z.string().min(1).describe("Present continuous form shown during execution (e.g., 'Fixing authentication bug')")
});
var taskItemSchema = taskItemInputSchema.extend({
id: taskIdSchema
});
var taskToolResultSchema = z.object({
content: z.string(),
tasks: z.array(taskItemSchema),
isError: z.boolean()
});
var taskCheckSummarySchema = z.object({
total: z.number().int().nonnegative(),
completed: z.number().int().nonnegative(),
inProgress: z.number().int().nonnegative(),
pending: z.number().int().nonnegative(),
incomplete: z.number().int().nonnegative(),
hasTasks: z.boolean(),
allCompleted: z.boolean()
});
var taskCheckResultSchema = taskToolResultSchema.extend({
summary: taskCheckSummarySchema,
incompleteTasks: z.array(taskItemSchema)
});
var TASK_ID_SLUG_MAX_LENGTH = 48;
function slugifyTaskContent(content) {
let slug = "";
let pendingSeparator = false;
for (const char of content.toLowerCase()) {
const code = char.charCodeAt(0);
const isAsciiLetter = code >= 97 && code <= 122;
const isDigit = code >= 48 && code <= 57;
if (isAsciiLetter || isDigit) {
if (pendingSeparator && slug.length > 0 && slug.length < TASK_ID_SLUG_MAX_LENGTH) {
slug += "_";
}
if (slug.length >= TASK_ID_SLUG_MAX_LENGTH) break;
slug += char;
pendingSeparator = false;
continue;
}
pendingSeparator = slug.length > 0;
}
return slug;
}
function createDeterministicTaskId(task, occurrence) {
const slug = slugifyTaskContent(task.content);
const suffix = occurrence > 1 ? `_${occurrence}` : "";
return `task_${slug || "item"}${suffix}`;
}
function makeUniqueTaskId(id, usedIds, reservedIds = /* @__PURE__ */ new Set()) {
if (!usedIds.has(id) && !reservedIds.has(id)) return id;
let suffix = 2;
let nextId = `${id}_${suffix}`;
while (usedIds.has(nextId) || reservedIds.has(nextId)) {
suffix += 1;
nextId = `${id}_${suffix}`;
}
return nextId;
}
function assignTaskIds(tasks, previousTasks = []) {
const usedIds = /* @__PURE__ */ new Set();
const contentOccurrences = /* @__PURE__ */ new Map();
const omittedContentCounts = /* @__PURE__ */ new Map();
const explicitTaskIds = new Set(tasks.map((task) => task.id).filter((id) => Boolean(id)));
const reusablePreviousIds = /* @__PURE__ */ new Map();
for (const task of tasks) {
if (!task.id) {
omittedContentCounts.set(task.content, (omittedContentCounts.get(task.content) ?? 0) + 1);
}
}
tasks.forEach((task, index) => {
if (task.id || omittedContentCounts.get(task.content) !== 1) return;
const previousMatches = previousTasks.filter(
(previous) => previous.content === task.content && !explicitTaskIds.has(previous.id)
);
if (previousMatches.length === 1) {
reusablePreviousIds.set(index, previousMatches[0].id);
}
});
const reservedIds = /* @__PURE__ */ new Set([...explicitTaskIds, ...reusablePreviousIds.values()]);
return tasks.map((task, index) => {
const contentOccurrence = (contentOccurrences.get(task.content) ?? 0) + 1;
contentOccurrences.set(task.content, contentOccurrence);
const fallbackId = createDeterministicTaskId(task, contentOccurrence);
const reusablePreviousId = reusablePreviousIds.get(index);
const requestedId = task.id && !usedIds.has(task.id) ? task.id : void 0;
const id = requestedId ?? (reusablePreviousId && !usedIds.has(reusablePreviousId) ? reusablePreviousId : makeUniqueTaskId(fallbackId, usedIds, reservedIds));
usedIds.add(id);
return {
id,
content: task.content,
status: task.status,
activeForm: task.activeForm
};
});
}
function getTasksFromState(state) {
const typedState = state;
return assignTaskIds(typedState?.tasks || []);
}
function getCurrentTasks(harnessCtx) {
const state = harnessCtx?.getState ? harnessCtx.getState() : harnessCtx?.state;
return getTasksFromState(state);
}
async function readTasks(harnessCtx) {
if (harnessCtx.updateState) {
return harnessCtx.updateState((state) => ({
result: getTasksFromState(state)
}));
}
return getCurrentTasks(harnessCtx);
}
function formatTaskListResult(tasks) {
const completed = tasks.filter((t) => t.status === "completed").length;
const inProgress = tasks.find((t) => t.status === "in_progress");
const total = tasks.length;
let summary = `Tasks updated: [${completed}/${total} completed]`;
if (inProgress) {
summary += `
Currently: ${inProgress.activeForm} (${inProgress.id})`;
}
if (tasks.length > 0) {
summary += `
Task IDs:
${tasks.map((t) => `- ${t.id}: ${t.content} (${t.status})`).join("\n")}`;
}
return summary;
}
function summarizeTaskCheck(tasks) {
const completedTasks = tasks.filter((task) => task.status === "completed");
const inProgressTasks = tasks.filter((task) => task.status === "in_progress");
const pendingTasks = tasks.filter((task) => task.status === "pending");
const incompleteTasks = [...inProgressTasks, ...pendingTasks];
return {
summary: {
total: tasks.length,
completed: completedTasks.length,
inProgress: inProgressTasks.length,
pending: pendingTasks.length,
incomplete: incompleteTasks.length,
hasTasks: tasks.length > 0,
allCompleted: tasks.length > 0 && incompleteTasks.length === 0
},
inProgressTasks,
pendingTasks,
incompleteTasks
};
}
function formatTaskCheckResult(taskCheck) {
const { summary, inProgressTasks, pendingTasks } = taskCheck;
if (!summary.hasTasks) {
return "No tasks found. Consider using task_write to create a task list for complex work.";
}
let response = `Task Status: [${summary.completed}/${summary.total} completed]
`;
response += `- Completed: ${summary.completed}
`;
response += `- In Progress: ${summary.inProgress}
`;
response += `- Pending: ${summary.pending}
`;
response += `
All tasks completed: ${summary.allCompleted ? "YES" : "NO"}`;
if (!summary.allCompleted) {
response += "\n\nIncomplete tasks:";
if (inProgressTasks.length > 0) {
response += "\n\nIn Progress:";
inProgressTasks.forEach((t) => {
response += `
- ${t.id}: ${t.content}`;
});
}
if (pendingTasks.length > 0) {
response += "\n\nPending:";
pendingTasks.forEach((t) => {
response += `
- ${t.id}: ${t.content}`;
});
}
response += "\n\nContinue working on these tasks before ending.";
}
return response;
}
function hasMultipleInProgress(tasks) {
return tasks.filter((task) => task.status === "in_progress").length > 1;
}
function multipleInProgressError(tasks) {
return {
content: "Only one task can be in_progress at a time.",
tasks,
isError: true
};
}
function demoteExtraInProgress(tasks, preferredIndex) {
const inProgressIndices = tasks.reduce((acc, t, i) => {
if (t.status === "in_progress") acc.push(i);
return acc;
}, []);
if (inProgressIndices.length <= 1) return tasks;
const keepIndex = preferredIndex !== void 0 && inProgressIndices.includes(preferredIndex) ? preferredIndex : inProgressIndices[inProgressIndices.length - 1];
return tasks.map(
(t, i) => t.status === "in_progress" && i !== keepIndex ? { ...t, status: "pending" } : t
);
}
async function writeTasks(harnessCtx, tasks) {
if (!harnessCtx) return;
await harnessCtx.setState({ tasks });
harnessCtx.emitEvent?.({
type: "task_updated",
tasks
});
}
async function mutateTasks(harnessCtx, mutation) {
if (harnessCtx.updateState) {
return harnessCtx.updateState((state) => {
const result2 = mutation(getTasksFromState(state));
return {
result: result2,
updates: result2.isError ? void 0 : { tasks: result2.tasks },
events: result2.isError ? void 0 : [
{
type: "task_updated",
tasks: result2.tasks
}
]
};
});
}
const result = mutation(getCurrentTasks(harnessCtx));
if (!result.isError) {
await writeTasks(harnessCtx, result.tasks);
}
return result;
}
function formatAvailableTaskIds(tasks) {
if (tasks.length === 0) return "No tasks are currently tracked.";
return `Available task IDs:
${tasks.map((t) => `- ${t.id}: ${t.content} (${t.status})`).join("\n")}`;
}
var taskWriteTool = createTool({
id: "task_write",
description: `Create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
Usage:
- Use this to create the initial task list or replace the whole list after replanning
- Pass the FULL task list each time this tool is called (replaces the previous list)
- Each task has: id (stable identifier), content (imperative), status (pending, in_progress, or completed), activeForm (present continuous)
- IDs must be unique. If duplicate explicit IDs are provided, the duplicate task is returned with a generated fallback ID
- Keep task IDs stable across updates. If omitted, IDs are generated and returned in the tool result
- When an ID is omitted while rewriting an existing list, one unambiguous matching task may reuse an existing ID
- Prefer single-task update tools when they are available
- Mark tasks in_progress BEFORE starting work (only ONE at a time)
- Mark tasks completed IMMEDIATELY after finishing
- Use this for multi-step tasks requiring 3+ distinct actions
States:
- pending: Not yet started
- in_progress: Currently working on (limit to ONE)
- completed: Finished successfully`,
inputSchema: z.object({
tasks: z.array(taskItemInputSchema).describe("The complete updated task list")
}),
outputSchema: taskToolResultSchema,
execute: async ({ tasks }, context) => {
try {
const harnessCtx = context?.requestContext?.get("harness");
if (!harnessCtx) {
return {
content: "Unable to update task list (no harness context)",
tasks: [],
isError: true
};
}
return await mutateTasks(harnessCtx, (currentTasks) => {
const normalizedTasks = assignTaskIds(tasks, currentTasks);
if (hasMultipleInProgress(normalizedTasks)) {
return multipleInProgressError(currentTasks);
}
return {
content: formatTaskListResult(normalizedTasks),
tasks: normalizedTasks,
isError: false
};
});
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
return {
content: `Failed to update tasks: ${msg}`,
tasks: [],
isError: true
};
}
}
});
var taskUpdateTool = createTool({
id: "task_update",
description: `Update one task in the current task list by stable ID. Use this for targeted changes to one existing task.
Usage:
- Provide the task ID returned by the task-list tools
- Include only the fields that changed
- Use status to move a task between pending, in_progress, and completed
- Use task_complete when only marking a task completed
- If the ID is unknown, the tool returns an error with available task IDs`,
inputSchema: z.object({
id: taskIdSchema,
content: z.string().min(1).optional().describe("New task description in imperative form"),
status: z.enum(["pending", "in_progress", "completed"]).optional().describe("New task status"),
activeForm: z.string().min(1).optional().describe("New present continuous form shown during execution")
}).refine((input) => input.content !== void 0 || input.status !== void 0 || input.activeForm !== void 0, {
message: "Provide at least one field to update."
}),
outputSchema: taskToolResultSchema,
execute: async ({ id, content, status, activeForm }, context) => {
try {
const harnessCtx = context?.requestContext?.get("harness");
if (!harnessCtx) {
return {
content: "Unable to update task list (no harness context)",
tasks: [],
isError: true
};
}
return await mutateTasks(harnessCtx, (tasks) => {
const taskIndex = tasks.findIndex((task) => task.id === id);
if (taskIndex === -1) {
return {
content: `Task not found: ${id}
${formatAvailableTaskIds(tasks)}`,
tasks,
isError: true
};
}
const updatedTasks = demoteExtraInProgress(
tasks.map(
(task, index) => index === taskIndex ? {
...task,
...content !== void 0 ? { content } : {},
...status !== void 0 ? { status } : {},
...activeForm !== void 0 ? { activeForm } : {}
} : task
),
taskIndex
);
return {
content: formatTaskListResult(updatedTasks),
tasks: updatedTasks,
isError: false
};
});
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
return {
content: `Failed to update task: ${msg}`,
tasks: [],
isError: true
};
}
}
});
var taskCompleteTool = createTool({
id: "task_complete",
description: `Mark one task completed by stable ID. Use this when one tracked task is finished.
Usage:
- Provide the task ID returned by the task-list tools
- If the ID is unknown, the tool returns an error with available task IDs`,
inputSchema: z.object({
id: taskIdSchema
}),
outputSchema: taskToolResultSchema,
execute: async ({ id }, context) => {
try {
const harnessCtx = context?.requestContext?.get("harness");
if (!harnessCtx) {
return {
content: "Unable to update task list (no harness context)",
tasks: [],
isError: true
};
}
return await mutateTasks(harnessCtx, (tasks) => {
const taskIndex = tasks.findIndex((task) => task.id === id);
if (taskIndex === -1) {
return {
content: `Task not found: ${id}
${formatAvailableTaskIds(tasks)}`,
tasks,
isError: true
};
}
const updatedTasks = tasks.map(
(task, index) => index === taskIndex ? {
...task,
status: "completed"
} : task
);
return {
content: formatTaskListResult(updatedTasks),
tasks: updatedTasks,
isError: false
};
});
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
return {
content: `Failed to complete task: ${msg}`,
tasks: [],
isError: true
};
}
}
});
var taskCheckTool = createTool({
id: "task_check",
description: `Check the completion status of your current task list. Use this before finishing tracked work to ensure all tasks are completed.
Returns:
- Human-readable content summary with task counts and incomplete task IDs
- Structured task list snapshot with stable IDs
- summary object with total, completed, inProgress, pending, incomplete, hasTasks, and allCompleted
- incompleteTasks array for tasks that still need work
summary.allCompleted is true only when at least one tracked task exists and every tracked task is completed. If no tasks exist, summary.hasTasks is false and summary.allCompleted is false.`,
inputSchema: z.object({}),
// No input needed
outputSchema: taskCheckResultSchema,
execute: async ({}, context) => {
try {
const harnessCtx = context?.requestContext?.get("harness");
if (!harnessCtx) {
const emptyCheck = summarizeTaskCheck([]);
return {
content: "Unable to access task list (no harness context)",
tasks: [],
summary: emptyCheck.summary,
incompleteTasks: emptyCheck.incompleteTasks,
isError: true
};
}
const tasks = await readTasks(harnessCtx);
const taskCheck = summarizeTaskCheck(tasks);
return {
content: formatTaskCheckResult(taskCheck),
tasks,
summary: taskCheck.summary,
incompleteTasks: taskCheck.incompleteTasks,
isError: false
};
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
const emptyCheck = summarizeTaskCheck([]);
return {
content: `Failed to check tasks: ${msg}`,
tasks: [],
summary: emptyCheck.summary,
incompleteTasks: emptyCheck.incompleteTasks,
isError: true
};
}
}
});
function createSubagentTool(opts) {
const { subagents, resolveModel, harnessTools, fallbackModelId } = opts;
const subagentIds = subagents.map((s) => s.id);
const typeDescriptions = subagents.map((s) => `- **${s.id}** (${s.name}): ${s.description}`).join("\n");
return createTool({
id: "subagent",
description: `Delegate a focused task to a specialized subagent. The subagent runs independently with a constrained toolset, then returns its findings as text.
Available agent types:
${typeDescriptions}
By default the subagent runs in its own context \u2014 it does NOT see the parent conversation history. Write a clear, self-contained task description.
Set \`forked: true\` for context-dependent parallel work that needs the parent conversation, prior tool results, or the parent tool environment. Omit it for self-contained delegation. A forked subagent reuses the parent agent's instructions and tools so the prompt prefix stays cache-friendly.
Use this tool when:
- You want to run multiple investigations in parallel
- The task is self-contained and can be delegated`,
inputSchema: z.object({
agentType: z.enum(subagentIds).describe("Type of subagent to spawn"),
task: z.string().describe(
"Clear, self-contained description of what the subagent should do. For non-forked subagents include all relevant context \u2014 the subagent cannot see the parent conversation."
),
modelId: z.string().optional().describe(
"Optional model ID override for this task. Ignored when `forked: true` (the parent agent's model is used)."
),
forked: z.boolean().optional().describe(
"If true, fork the parent conversation: clone the parent thread and run with the parent agent's instructions/tools so prompt cache is preserved. Requires memory to be configured on the Harness. Defaults to the subagent definition's `forked` setting."
)
}),
execute: async (input, context) => {
const { agentType, modelId, forked } = input;
let { task } = input;
const displayTask = task;
const definition = subagents.find((s) => s.id === agentType);
if (!definition) {
return {
content: `Unknown agent type: ${agentType}. Valid types: ${subagentIds.join(", ")}`,
isError: true
};
}
const harnessCtx = context?.requestContext?.get("harness");
const emitEvent = harnessCtx?.emitEvent;
const abortSignal = harnessCtx?.abortSignal;
const toolCallId = context?.agent?.toolCallId ?? "unknown";
const workspace = context?.workspace;
const runAsForked = forked ?? definition.forked ?? false;
let subagentToRun;
let resolvedModelId;
let subagentRequestContext;
let streamMemory;
let streamMaxSteps;
let streamStopWhen;
let streamPrepareStep;
let forkedToolsets;
if (runAsForked) {
const parentAgent = opts.getParentAgent?.();
if (!parentAgent) {
return {
content: "Forked subagent requires a parent agent. None is configured on this Harness.",
isError: true
};
}
const parentThreadId = harnessCtx?.threadId;
if (!parentThreadId) {
return {
content: "Forked subagent requires an active parent thread; none is set on the Harness.",
isError: true
};
}
if (!opts.cloneThreadForFork) {
return {
content: "Forked subagent requires memory to be configured on the Harness so the parent thread can be cloned.",
isError: true
};
}
await context?.agent?.flushMessages?.().catch(() => {
});
let forkedThread;
try {
forkedThread = await opts.cloneThreadForFork({
sourceThreadId: parentThreadId,
resourceId: harnessCtx?.resourceId,
title: `Fork: ${definition.name} subagent`
});
} catch (err) {
return {
content: `Failed to clone parent thread for forked subagent: ${err instanceof Error ? err.message : String(err)}`,
isError: true
};
}
subagentToRun = parentAgent;
resolvedModelId = opts.getParentModelId?.() || "parent-agent";
task = `${task}
${FORKED_SUBAGENT_NESTING_NOTICE}
${FORKED_SUBAGENT_TASK_NOTICE}`;
streamMemory = { thread: forkedThread.id, resource: forkedThread.resourceId };
streamMaxSteps = 1e3;
streamStopWhen = void 0;
streamPrepareStep = void 0;
if (context?.requestContext) {
subagentRequestContext = new RequestContext(context.requestContext.entries());
if (harnessCtx) {
subagentRequestContext.set("harness", {
...harnessCtx,
threadId: forkedThread.id,
resourceId: forkedThread.resourceId
});
}
}
const inheritedToolsets = await opts.getParentToolsets?.(subagentRequestContext);
if (inheritedToolsets) {
forkedToolsets = {};
for (const [setName, setTools] of Object.entries(inheritedToolsets)) {
const patched = {};
for (const [toolId, tool] of Object.entries(setTools)) {
if (toolId === "subagent") {
patched[toolId] = patchSubagentToolForFork(tool);
} else if (isForkedTaskToolId(toolId)) {
patched[toolId] = patchTaskToolForFork(tool, toolId);
} else {
patched[toolId] = tool;
}
}
forkedToolsets[setName] = patched;
}
}
} else {
const mergedTools = { ...definition.tools };
if (definition.allowedHarnessTools && harnessTools) {
for (const toolId of definition.allowedHarnessTools) {
if (harnessTools[toolId] && !mergedTools[toolId]) {
mergedTools[toolId] = harnessTools[toolId];
}
}
}
const harnessModelId = harnessCtx?.getSubagentModelId?.({ agentType }) ?? void 0;
const maybeModelId = modelId ?? harnessModelId ?? definition.defaultModelId ?? fallbackModelId;
if (!maybeModelId) {
return { content: "No model ID available for subagent. Configure defaultModelId.", isError: true };
}
resolvedModelId = maybeModelId;
let model;
try {
model = resolveModel(resolvedModelId);
} catch (err) {
return {
content: `Failed to resolve model "${resolvedModelId}": ${err instanceof Error ? err.message : String(err)}`,
isError: true
};
}
subagentToRun = new Agent({
id: `subagent-${definition.id}`,
name: `${definition.name} Subagent`,
instructions: definition.instructions,
model,
tools: mergedTools,
workspace
});
const allowedWs = definition.allowedWorkspaceTools ? new Set(definition.allowedWorkspaceTools) : void 0;
const allWorkspaceToolNames = workspace && allowedWs ? new Set(
Object.keys(
await createWorkspaceTools(workspace, {
requestContext: context?.requestContext ?? {},
workspace
})
)
) : void 0;
streamMaxSteps = definition.maxSteps ?? (definition.stopWhen ? void 0 : 50);
streamStopWhen = definition.stopWhen;
streamPrepareStep = allowedWs && allWorkspaceToolNames ? ({ tools }) => ({
activeTools: Object.keys(tools ?? {}).filter((k) => !allWorkspaceToolNames.has(k) || allowedWs.has(k))
}) : void 0;
if (context?.requestContext) {
subagentRequestContext = new RequestContext(context.requestContext.entries());
if (harnessCtx) {
subagentRequestContext.set("harness", { ...harnessCtx, threadId: null, resourceId: "" });
}
}
}
const startTime = Date.now();
emitEvent?.({
type: "subagent_start",
toolCallId,
agentType,
task: displayTask,
modelId: resolvedModelId,
forked: runAsForked
});
let partialText = "";
try {
const response = await subagentToRun.stream(task, {
maxSteps: streamMaxSteps,
stopWhen: streamStopWhen,
abortSignal,
requireToolApproval: false,
requestContext: subagentRequestContext,
...streamMemory && { memory: streamMemory },
...forkedToolsets && { toolsets: forkedToolsets },
...context?.tracingContext && { tracingContext: context.tracingContext },
prepareStep: streamPrepareStep
});
for await (const chunk of response.fullStream) {
switch (chunk.type) {
case "text-delta":
partialText += chunk.payload.text;
emitEvent?.({
type: "subagent_text_delta",
toolCallId,
agentType,
textDelta: chunk.payload.text
});
break;
case "tool-call":
if (!(runAsForked && chunk.payload.toolName === "subagent")) {
emitEvent?.({
type: "subagent_tool_start",
toolCallId,
agentType,
subToolName: chunk.payload.toolName,
subToolArgs: chunk.payload.args
});
}
break;
case "tool-result": {
const isErr = chunk.payload.isError ?? false;
if (!(runAsForked && chunk.payload.toolName === "subagent")) {
emitEvent?.({
type: "subagent_tool_end",
toolCallId,
agentType,
subToolName: chunk.payload.toolName,
subToolResult: chunk.payload.result,
isError: isErr
});
}
break;
}
}
}
if (abortSignal?.aborted) {
const durationMs2 = Date.now() - startTime;
const abortResult = partialText ? `[Aborted by user]
Partial output:
${partialText}` : "[Aborted by user]";
emitEvent?.({ type: "subagent_end", toolCallId, agentType, result: abortResult, isError: false, durationMs: durationMs2 });
return { content: abortResult, isError: false };
}
const fullOutput = await response.getFullOutput();
const resultText = fullOutput.text || partialText;
const durationMs = Date.now() - startTime;
emitEvent?.({ type: "subagent_end", toolCallId, agentType, result: resultText, isError: false, durationMs });
return { content: resultText, isError: false };
} catch (err) {
const isAbort = err instanceof Error && (err.name === "AbortError" || err.message?.includes("abort") || err.message?.includes("cancel"));
const durationMs = Date.now() - startTime;
if (isAbort) {
const abortResult = partialText ? `[Aborted by user]
Partial output:
${partialText}` : "[Aborted by user]";
emitEvent?.({ type: "subagent_end", toolCallId, agentType, result: abortResult, isError: false, durationMs });
return { content: abortResult, isError: false };
}
const message = err instanceof Error ? err.message : String(err);
emitEvent?.({ type: "subagent_end", toolCallId, agentType, result: message, isError: true, durationMs });
return { content: `Subagent "${definition.name}" failed: ${message}`, isError: true };
}
}
});
}
function patchSubagentToolForFork(tool) {
const stubExecute = async () => ({
content: FORKED_SUBAGENT_NESTING_NOTICE,
isError: true
});
return Object.assign({}, tool, { execute: stubExecute });
}
function isForkedTaskToolId(toolId) {
return toolId === "task_write" || toolId === "task_update" || toolId === "task_complete" || toolId === "task_check";
}
function patchTaskToolForFork(tool, toolId) {
const stubExecute = async () => {
const baseResult = {
content: FORKED_SUBAGENT_TASK_NOTICE,
tasks: [],
isError: true
};
if (toolId === "task_check") {
const taskCheck = summarizeTaskCheck([]);
return {
...baseResult,
summary: taskCheck.summary,
incompleteTasks: taskCheck.incompleteTasks
};
}
return baseResult;
};
return Object.assign({}, tool, { execute: stubExecute });
}
function parseSubagentMeta(content) {
const match = content.match(/\n<subagent-meta modelId="([^"]*)" durationMs="(\d+)" tools="([^"]*)" \/>$/);
if (!match) return { text: content };
const text = content.slice(0, match.index);
const modelId = match[1];
const durationMs = parseInt(match[2], 10);
const toolCalls = match[3] ? match[3].split(",").filter(Boolean).map((entry) => {
const [name, status] = entry.split(":");
return { name, isError: status === "err" };
}) : [];
return { text, modelId, durationMs, toolCalls };
}
// src/harness/types.ts
function createEmptyTokenUsage() {
return {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
cachedInputTokens: 0,
cacheCreationInputTokens: 0
};
}
function defaultDisplayState() {
return {
isRunning: false,
currentMessage: null,
tokenUsage: createEmptyTokenUsage(),
activeTools: /* @__PURE__ */ new Map(),
toolInputBuffers: /* @__PURE__ */ new Map(),
pendingApproval: null,
pendingSuspension: null,
pendingQuestion: null,
pendingPlanApproval: null,
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/harness/harness.ts
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;
}
}
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 signalContentsToHarnessContent(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 = getStringValue(payload.contents);
if (message === void 0) 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)
};
}
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-message",
contents: rawContents,
createdAt: getStringValue(payload.createdAt)
});
const content = signalContentsToHarnessContent(signal.contents);
if (content.length === 0) return void 0;
return {
id: signal.id,
role: "user",
content,
createdAt: signal.createdAt
};
}
var Harness = class {
id;
config;
stateSchema;
state;
currentModeId;
currentThreadId = null;
resourceId;
defaultResourceId;
listeners = [];
displayStateSchedulers = /* @__PURE__ */ new Set();
abortController = null;
abortRequested = false;
currentRunId = null;
currentTraceId = null;
currentOperationId = 0;
agentThreadSubscription = null;
agentThreadSubscriptionKey = null;
followUpQueue = [];
pendingApprovalResolve = null;
pendingApprovalToolName = null;
pendingSuspensionRunId = null;
pendingSuspensionToolCallId = null;
pendingQuestions = /* @__PURE__ */ new Map();
pendingPlanApprovals = /* @__PURE__ */ new Map();
workspace = void 0;
workspaceFn = void 0;
workspaceInitialized = false;
browser = void 0;
browserFn = void 0;
heartbeatTimers = /* @__PURE__ */ new Map();
tokenUsage = createEmptyTokenUsage();
sessionGrantedCategories = /* @__PURE__ */ new Set();
sessionGrantedTools = /* @__PURE__ */ new Set();
displayState = defaultDisplayState();
stateUpdateQueue = Promise.resolve();
#internalMastra = void 0;
constructor(config) {
this.id = config.id;
this.config = config;
this.resourceId = config.resourceId ?? config.id;
this.defaultResourceId = this.resourceId;
this.stateSchema = config.stateSchema ? toStandardSchema(config.stateSchema) : void 0;
this.state = {
...this.getSchemaDefaults(),
...config.initialState
};
const defaultMode = config.modes.find((m) => m.default) ?? config.modes[0];
if (!defaultMode) {
throw new Error("Harness requires at least one agent mode");
}
this.currentModeId = defaultMode.id;
if (config.workspace instanceof Workspace) {
this.workspace = config.workspace;
} else if (typeof config.workspace === "function") {
this.workspaceFn = config.workspace;
}
if (config.browser && typeof config.browser !== "function") {
this.browser = config.browser;
} else if (typeof config.browser === "function") {
this.browserFn = config.browser;
}
const currentModel = this.state.currentModelId;
if (!currentModel && defaultMode.defaultModelId) {
void this.setState({ currentModelId: defaultMode.defaultModelId });
}
}
// ===========================================================================
// Accessors
// ===========================================================================
/**
* Access the internal Mastra instance.
* Available after `init()` when storage is configured.
* Useful for scorer registration, observability access, and eval tooling.
*/
getMastra() {
return this.#internalMastra;
}
/**
* Sets or updates the harness-level browser and propagates it to static mode agents.
*/
setBrowser(browser) {
this.browser = browser;
this.browserFn = void 0;
for (const mode of this