@mastra/core
Version:
971 lines (958 loc) • 37.4 kB
JavaScript
import { i as MastraError, n as ErrorDomain, t as ErrorCategory } from "./error-MjDSls8S.js";
import { isStandardSchemaWithJSON, standardSchemaToJSONSchema } from "./schema/index.js";
import { i as isValidationError, r as createTool } from "./tool-qGw4ZhYO.js";
import { f as SandboxFeatureNotSupportedError } from "./errors-B0YGz4tO.js";
import { randomBytes } from "crypto";
import { z } from "zod/v4";
import { tmpdir } from "os";
import { join } from "path";
import { mkdtemp, rm, writeFile } from "fs/promises";
import { pathToFileURL } from "url";
import { lookup } from "dns";
import http from "http";
import https from "https";
import net from "net";
//#region src/tools/code-mode/stub-generator.ts
/** A valid TypeScript identifier? (used to decide quoting of object keys). */
const SAFE_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
/**
* Convert a JSON Schema (draft-07) node into a TypeScript type string.
* Unsupported constructs return `unknown`.
*/
function jsonSchemaToTsString(schema) {
if (schema === void 0) return "unknown";
if (typeof schema === "boolean") return schema ? "unknown" : "never";
if (schema.const !== void 0) return literal(schema.const);
if (Array.isArray(schema.enum)) return schema.enum.length ? schema.enum.map(literal).join(" | ") : "never";
const union = schema.anyOf ?? schema.oneOf;
if (Array.isArray(union) && union.length) return union.map(jsonSchemaToTsString).join(" | ");
const type = normalizeType(schema.type);
if (type === "object" || schema.properties) return objectType(schema);
if (type === "array" || schema.items) return arrayType(schema);
switch (type) {
case "string": return "string";
case "number":
case "integer": return "number";
case "boolean": return "boolean";
case "null": return "null";
default: return "unknown";
}
}
function normalizeType(type) {
if (Array.isArray(type)) return type.find((t) => t !== "null");
return type;
}
function objectType(schema) {
const props = schema.properties ?? {};
const required = new Set(schema.required ?? []);
const keys = Object.keys(props);
if (!keys.length) {
const additional = schema.additionalProperties;
if (additional !== void 0 && additional !== false) return `Record<string, ${typeof additional === "object" ? jsonSchemaToTsString(additional) : "unknown"}>`;
return "Record<string, unknown>";
}
return `{ ${keys.map((key) => {
const optional = !required.has(key) ? "?" : "";
return `${SAFE_IDENT.test(key) ? key : JSON.stringify(key)}${optional}: ${jsonSchemaToTsString(props[key])}`;
}).join("; ")} }`;
}
function arrayType(schema) {
const items = schema.items;
if (Array.isArray(items)) return `[${items.map(jsonSchemaToTsString).join(", ")}]`;
const inner = jsonSchemaToTsString(items);
return isTopLevelUnion(inner) ? `Array<${inner}>` : `${inner}[]`;
}
/** True if `ts` is a union at the top level (a ` | ` not nested in braces/brackets). */
function isTopLevelUnion(ts) {
let depth = 0;
for (let i = 0; i < ts.length; i++) {
const c = ts[i];
if (c === "{" || c === "[" || c === "(" || c === "<") depth++;
else if (c === "}" || c === "]" || c === ")" || c === ">") depth--;
else if (c === "|" && depth === 0) return true;
}
return false;
}
function literal(value) {
if (typeof value === "string") return JSON.stringify(value);
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (value === null) return "null";
return "unknown";
}
function schemaToTs(schema, io) {
if (!isStandardSchemaWithJSON(schema)) return "unknown";
try {
return jsonSchemaToTsString(standardSchemaToJSONSchema(schema, { io }));
} catch {
return "unknown";
}
}
/**
* Strip non-identifier characters so a tool id is a legal function-name suffix.
*
* Transports that map `external_*` names back to tool ids must use this same
* sanitizer so their naming stays identical to the generated stubs.
*/
function sanitizeToolId(id) {
const cleaned = id.replace(/[^A-Za-z0-9_$]/g, "_");
return SAFE_IDENT.test(cleaned) ? cleaned : `_${cleaned}`;
}
/** Generate stubs for every tool in the config. */
function generateStubs(tools) {
const seen = /* @__PURE__ */ new Map();
return Object.entries(tools).map(([key, tool]) => {
const toolId = tool.id ?? key;
const description = tool.description;
const inputType = schemaToTs(tool.inputSchema, "input");
const outputType = schemaToTs(tool.outputSchema, "output");
const externalName = sanitizeToolId(toolId);
const prior = seen.get(externalName);
if (prior !== void 0 && prior !== toolId) throw new Error(`Code Mode tool id collision: "${prior}" and "${toolId}" both map to external_${externalName}`);
seen.set(externalName, toolId);
return {
toolId,
externalName,
declaration: `${description ? `/** ${description.replace(/\*\//g, "* /")} */\n` : ""}declare function external_${externalName}(input: ${inputType}): Promise<${outputType}>;`
};
});
}
const USAGE_CONTRACT = `# Code Mode
You have an \`execute_typescript\` tool. Instead of calling tools one at a time,
write a single TypeScript program that orchestrates them and returns one result.
Rules:
- Call the available tools via the \`external_*\` functions declared below. Each
returns a Promise — \`await\` it.
- Batch independent calls with \`Promise.all\`. Do arithmetic and data shaping in
JavaScript, not in your head.
- End the program by \`return\`-ing the final value (objects/arrays are fine).
- The only supported capabilities are the \`external_*\` functions. Do not rely
on filesystem, network, or process access — depending on the configured
sandbox and transport, the program may run fully isolated with none of those
available.
- Use \`console.log\` for debugging; logs are captured and returned.
Available functions:`;
/** Build the full instructions string (usage contract + stubs). */
function createCodeModeInstructions(config) {
const declarations = generateStubs(config.tools).map((s) => s.declaration).join("\n\n");
return `${USAGE_CONTRACT}\n\n${declarations}`;
}
//#endregion
//#region src/tools/code-mode/runner.ts
/**
* Code Mode — Sandbox runner
*
* Builds the JavaScript program that runs *inside* the sandbox. The runner:
* - defines an `external_<name>` function per allow-listed tool, each of which
* emits a JSON-RPC request on the protocol channel and awaits its response
* (matched by `id`, so `Promise.all` calls resolve independently);
* - wraps the model's program in an async function, captures `console.*`, and
* emits a terminal `done` frame.
*
* Protocol (host <-> runner), newline-delimited JSON on stdout/stdin:
* - Frames the runner emits are prefixed with FRAME_PREFIX so the host can
* tell them apart from any stray output. Forms: `rpc`, `log`, `done`.
* - The host writes `rpc-result` frames to the runner stdin (no prefix).
*/
/** Marks a line on stdout as a Code Mode protocol frame. */
const FRAME_PREFIX = "\0CODEMODE\0";
/**
* Wrap the model's TypeScript program as a default-exported async function
* module. Written to a `.ts` file; Node strips the type annotations at import.
* Top-level `return`, `await`, and `const` work because the body lives inside
* an async function.
*/
function buildProgramModule(program) {
return `export default async function () {\n${program}\n}\n`;
}
/**
* Produce the full runner source to write into the sandbox and run with node.
*/
function buildRunner({ programModule, externals }) {
const SAFE_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
const seen = /* @__PURE__ */ new Map();
for (const { externalName, toolId } of externals) {
if (!SAFE_IDENT.test(externalName)) throw new Error(`Invalid Code Mode external identifier: ${externalName}`);
const existing = seen.get(externalName);
if (existing) throw new Error(`Code Mode external identifier collision: tools "${existing}" and "${toolId}" both map to external_${externalName}`);
seen.set(externalName, toolId);
}
const externalsJson = JSON.stringify(externals.map(({ externalName, toolId }) => ({
externalName,
toolId
})));
return `'use strict';
const FRAME_PREFIX = ${JSON.stringify(FRAME_PREFIX)};
function __emit(frame) {
process.stdout.write(FRAME_PREFIX + JSON.stringify(frame) + '\\n');
}
function __emitDoneAndExit(frame) {
process.stdout.write(FRAME_PREFIX + JSON.stringify(frame) + '\\n', () => process.exit(0));
}
// ---- console capture -------------------------------------------------------
for (const level of ['log', 'info', 'warn', 'error']) {
console[level] = (...args) => {
const message = args
.map((a) => (typeof a === 'string' ? a : safeStringify(a)))
.join(' ');
__emit({ type: 'log', level, message });
};
}
function safeStringify(value) {
try { return JSON.stringify(value); } catch { return String(value); }
}
// ---- RPC bridge ------------------------------------------------------------
let __nextId = 0;
const __pending = new Map();
function __rpc(tool, args) {
const id = __nextId++;
return new Promise((resolve, reject) => {
__pending.set(id, { resolve, reject });
__emit({ type: 'rpc', id, tool, args });
});
}
let __stdinBuffer = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
__stdinBuffer += chunk;
let idx;
while ((idx = __stdinBuffer.indexOf('\\n')) >= 0) {
const line = __stdinBuffer.slice(0, idx);
__stdinBuffer = __stdinBuffer.slice(idx + 1);
if (!line) continue;
let frame;
try { frame = JSON.parse(line); } catch { continue; }
if (frame && frame.type === 'rpc-result') {
const entry = __pending.get(frame.id);
if (!entry) continue;
__pending.delete(frame.id);
if (frame.ok) entry.resolve(frame.result);
else {
const err = new Error(frame.error?.message || 'external tool failed');
if (frame.error?.name) err.name = frame.error.name;
entry.reject(err);
}
}
}
});
// ---- externals -------------------------------------------------------------
for (const { externalName, toolId } of ${externalsJson}) {
globalThis['external_' + externalName] = (input) => __rpc(toolId, input);
}
// ---- user program ----------------------------------------------------------
// The program lives in a sibling .ts module exporting a default async function;
// node strips its TypeScript types natively on import.
async function __main() {
const mod = await import(${JSON.stringify(programModule)});
return await mod.default();
}
__main()
.then((result) => {
__emitDoneAndExit({ type: 'done', ok: true, result });
})
.catch((error) => {
__emitDoneAndExit({
type: 'done',
ok: false,
error: { message: error?.message ?? String(error), name: error?.name },
});
});
`;
}
//#endregion
//#region src/tools/code-mode/transport.ts
/**
* Code Mode — stdio JSON-RPC transport (v1)
*
* Runs the runner inside the sandbox via `sandbox.processes.spawn`, parses
* protocol frames off stdout, dispatches `external_*` calls back to the host,
* and writes results to the runner stdin. Abstracted behind
* {@link CodeModeTransport} so socket/file-queue transports can be added for
* remote sandboxes later.
*/
/**
* Default transport: writes the runner to a temp dir, spawns
* `node <runner>`, and bridges RPC over stdio.
*/
var StdioCodeModeTransport = class {
async run(opts) {
const { sandbox, program, toolIds, dispatch, timeout, abortSignal, onExternalCall, onExternalResult } = opts;
if (!sandbox) throw new Error("StdioCodeModeTransport requires a sandbox");
if (!sandbox.processes) throw new SandboxFeatureNotSupportedError("processes");
const externals = toolIds.map((toolId) => ({
toolId,
externalName: sanitizeToolId(toolId)
}));
const allowList = new Set(toolIds);
const dir = await mkdtemp(join(tmpdir(), "mastra-code-mode-"));
const suffix = randomBytes(4).toString("hex");
const programPath = join(dir, `program-${suffix}.ts`);
await writeFile(programPath, buildProgramModule(program), "utf8");
const runnerSource = buildRunner({
programModule: pathToFileURL(programPath).href,
externals
});
const runnerPath = join(dir, `runner-${suffix}.mjs`);
await writeFile(runnerPath, runnerSource, "utf8");
const logs = [];
let done;
let stdoutBuffer = "";
let resolveDone;
const donePromise = new Promise((resolve) => {
resolveDone = resolve;
});
try {
const handle = await sandbox.processes.spawn(`node --experimental-strip-types ${runnerPath}`, {
cwd: dir,
abortSignal,
onStdout: (chunk) => {
stdoutBuffer += chunk;
let idx;
while ((idx = stdoutBuffer.indexOf("\n")) >= 0) {
const line = stdoutBuffer.slice(0, idx);
stdoutBuffer = stdoutBuffer.slice(idx + 1);
if (!line.startsWith("\0CODEMODE\0")) continue;
let frame;
try {
frame = JSON.parse(line.slice(10));
} catch {
continue;
}
handleFrame(frame);
}
}
});
function handleFrame(frame) {
switch (frame.type) {
case "log":
logs.push(frame.message);
return;
case "done":
done = frame.ok ? {
success: true,
result: frame.result,
logs
} : {
success: false,
error: frame.error,
logs
};
resolveDone();
return;
case "rpc":
serveRpc(frame.id, frame.tool, frame.args).catch(() => {});
return;
}
}
function notifyCall(tool, args) {
try {
onExternalCall?.(tool, args);
} catch {}
}
function notifyResult(tool, durationMs, error) {
try {
onExternalResult?.(tool, durationMs, error);
} catch {}
}
async function serveRpc(id, tool, args) {
const started = Date.now();
notifyCall(tool, args);
if (!allowList.has(tool)) {
notifyResult(tool, Date.now() - started, /* @__PURE__ */ new Error("not allowed"));
await respond(id, false, void 0, {
message: `Tool "${tool}" is not available in Code Mode`,
name: "NotAllowedError"
});
return;
}
try {
const result = await dispatch(tool, args);
notifyResult(tool, Date.now() - started);
await respond(id, true, result);
} catch (error) {
notifyResult(tool, Date.now() - started, error);
await respond(id, false, void 0, {
message: error?.message ?? String(error),
name: error?.name
});
}
}
async function respond(id, ok, result, error) {
await handle.sendStdin(JSON.stringify({
type: "rpc-result",
id,
ok,
result,
error
}) + "\n");
}
let timer;
const timeoutPromise = new Promise((resolve) => {
timer = setTimeout(() => resolve("timeout"), timeout);
});
const exitPromise = handle.wait().then(() => "exited");
const outcome = await Promise.race([
donePromise.then(() => "done"),
exitPromise.catch(() => "exited"),
timeoutPromise
]);
if (timer) clearTimeout(timer);
if (outcome === "timeout") {
await handle.kill().catch(() => {});
return {
success: false,
logs,
error: {
message: `Code Mode execution timed out after ${timeout}ms`,
name: "TimeoutError"
}
};
}
if (!done) await exitPromise.catch(() => {});
return done ?? {
success: false,
logs,
error: {
message: "Program exited without returning a result",
name: "NoResultError"
}
};
} finally {
await rm(dir, {
recursive: true,
force: true
}).catch(() => {});
}
}
};
//#endregion
//#region src/tools/code-mode/code-mode.ts
/**
* Code Mode — tool factory
*
* `createCodeMode(config)` returns the `execute_typescript` tool plus the
* generated `instructions`. The tool transpiles the model's TypeScript to JS,
* runs it in a WorkspaceSandbox via the transport, and bridges each
* `external_*` call back to the real Mastra tool on the host.
*/
const DEFAULT_TIMEOUT = 3e4;
const DEFAULT_TOOL_NAME = "execute_typescript";
const codeModeInputSchema = z.object({ code: z.string().describe("A TypeScript program that orchestrates the available external_* tools and returns a final value. Use Promise.all to batch calls; do arithmetic in JS. End with `return <value>`.") });
const codeModeOutputSchema = z.object({
success: z.boolean(),
result: z.unknown().optional(),
logs: z.array(z.string()).optional(),
error: z.object({
message: z.string(),
name: z.string().optional(),
line: z.number().optional()
}).optional()
});
/** Resolve the tool key -> tool map keyed by the tool's effective id. */
function indexToolsById(config) {
const map = /* @__PURE__ */ new Map();
for (const [key, tool] of Object.entries(config.tools)) {
const id = tool.id ?? key;
map.set(id, tool);
}
return map;
}
/**
* Create only the `execute_typescript` tool. Most callers want
* {@link createCodeMode}, which also returns the matching instructions.
*/
function createCodeModeTool(config, transport = new StdioCodeModeTransport()) {
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
const id = config.id ?? DEFAULT_TOOL_NAME;
const toolsById = indexToolsById(config);
const toolIds = [...toolsById.keys()];
return createTool({
id,
description: "Execute a TypeScript program that orchestrates the available tools in a sandbox. Prefer this over calling tools one at a time when a task needs multiple tool calls, batching, aggregation, or arithmetic.",
inputSchema: codeModeInputSchema,
outputSchema: codeModeOutputSchema,
execute: async ({ code }, ctx) => {
const sandbox = config.sandbox ?? ctx?.workspace?.sandbox;
if (!sandbox && transport.requiresSandbox !== false) throw new Error("Code Mode requires a sandbox to run model-authored code, but none was configured. Pass one to createCodeMode({ tools, sandbox }), or run the agent in a workspace that provides a sandbox. To execute on the host (host privileges — only for trusted/local use), pass `sandbox: new LocalSandbox()`.");
const dispatch = async (toolId, args) => {
const tool = toolsById.get(toolId);
if (!tool?.execute) throw new Error(`Tool "${toolId}" is not available in Code Mode`);
const result = await tool.execute(args, {
mastra: ctx?.mastra,
requestContext: ctx?.requestContext,
abortSignal: ctx?.abortSignal,
workspace: ctx?.workspace
});
if (isValidationError(result)) throw new Error(result.message ?? `Invalid input for tool "${toolId}"`);
return result;
};
return ctx.observe.span(`code-mode:${id}`, () => transport.run({
sandbox,
program: code,
toolIds,
dispatch,
timeout,
abortSignal: ctx?.abortSignal,
onExternalCall: (tool, args) => ctx.observe.log("info", "code-mode external call", {
tool,
args
}),
onExternalResult: (tool, durationMs, error) => ctx.observe.log(error ? "error" : "info", "code-mode external result", {
tool,
durationMs
})
}));
}
});
}
/**
* Create Code Mode: the `execute_typescript` tool plus generated instructions.
*
* @example
* ```ts
* const { tool, instructions } = createCodeMode({ tools: { getTopProducts, getProductRatings } });
* const agent = new Agent({ instructions: ['You are helpful.', instructions], tools: { [tool.id]: tool } });
* ```
*/
function createCodeMode(config, transport) {
return {
tool: createCodeModeTool(config, transport),
instructions: createCodeModeInstructions(config)
};
}
//#endregion
//#region src/tools/builtin/ask-user.ts
const optionSchema = 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")
});
/**
* Converts the resume answer into the text returned to the model after `ask_user`
* resumes. Free-text and single-select prompts already produce a single string,
* while multi-select prompts resume with an array of selected labels that must be
* flattened before the tool result is added back into the generation context.
*
* The formatter keeps the model-facing output compact by joining multi-select
* answers with commas, mirroring the single-answer behavior while still preserving
* every selected option in a readable form.
*/
function formatQuestionAnswer(answer) {
return Array.isArray(answer) ? answer.join(", ") : answer;
}
/**
* Built-in, agent-agnostic tool: ask the user a question and wait for their response.
*
* The tool supports three prompt shapes. Omitting `options` asks an open-ended
* free-text question. Providing `options` without `selectionMode` asks the host to
* render a single-select prompt for backwards compatibility. Providing
* `selectionMode: 'multi_select'` lets the host resume with multiple selected option
* labels as a string array.
*
* Pausing uses the agent-native tool suspension primitive: the tool calls
* `suspend({ question, options, selectionMode })`, which makes the agent emit a
* `tool-call-suspended` event and persist run state. The host renders the question,
* collects the user's answer, and continues the run via `agent.resumeStream(answer)`;
* the tool re-runs with `resumeData` set to the answer and returns it to the model.
*
* When executed without an agent `suspend` (e.g. direct invocation outside an agent
* run), the tool returns a readable fallback prompt so the question and choices are
* still surfaced.
*/
const 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(optionSchema).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.")
}),
suspendSchema: z.object({
question: z.string(),
options: z.array(optionSchema).optional(),
selectionMode: z.enum(["single_select", "multi_select"]).optional()
}),
resumeSchema: z.union([z.string(), z.array(z.string())]),
execute: async ({ question, options, selectionMode }, context) => {
try {
if (selectionMode && !options?.length) return {
content: "Failed to ask user: selectionMode requires options.",
isError: true
};
const resolvedSelectionMode = options?.length ? selectionMode ?? "single_select" : void 0;
const resumeData = context?.agent?.resumeData;
if (resumeData !== void 0) return {
content: `User answered: ${formatQuestionAnswer(resumeData)}`,
isError: false
};
const suspend = context?.agent?.suspend;
if (suspend) {
await suspend({
question,
options,
selectionMode: resolvedSelectionMode
});
return;
}
return {
content: `[Question for user]: ${question}${options?.length ? "\nOptions: " + options.map((o) => o.label).join(", ") : ""}${resolvedSelectionMode ? "\nSelection mode: " + resolvedSelectionMode : ""}`,
isError: false
};
} catch (error) {
return {
content: `Failed to ask user: ${error instanceof Error ? error.message : "Unknown error"}`,
isError: true
};
}
}
});
//#endregion
//#region src/tools/builtin/web-fetch.ts
const MAX_CONTENT_LENGTH = 1e5;
const MAX_REDIRECTS = 5;
const TIMEOUT_MS = 15e3;
var WebFetchError = class extends Error {};
function parseHttpUrl(url) {
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:" ? parsedUrl : void 0;
} catch {
return;
}
}
function isBlockedHostname(hostname) {
const normalizedHostname = hostname.toLowerCase();
return normalizedHostname === "localhost" || normalizedHostname.endsWith(".localhost");
}
function isBlockedIpv4(address) {
const parts = address.split(".").map(Number);
const [first = 0, second = 0] = parts;
return first === 0 || first === 10 || first === 127 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 0 && parts[2] === 0 || first === 192 && second === 0 && parts[2] === 2 || first === 192 && second === 168 || first === 198 && (second === 18 || second === 19) || first === 198 && second === 51 && parts[2] === 100 || first === 203 && second === 0 && parts[2] === 113 || first >= 224;
}
function normalizeHostname(hostname) {
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
}
function parseIpv4MappedGroups(address) {
const ipv4Start = address.lastIndexOf(":");
const ipv4Address = address.slice(ipv4Start + 1);
if (!ipv4Address.includes(".")) return;
const ipv4Parts = ipv4Address.split(".").map(Number);
if (ipv4Parts.length !== 4 || ipv4Parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return;
const [first, second, third, fourth] = ipv4Parts;
return [
...expandIpv6(address.slice(0, ipv4Start), 6),
(first << 8) + second,
(third << 8) + fourth
];
}
function expandIpv6(address, expectedGroups = 8) {
const [left = "", right = ""] = address.split("::");
const leftGroups = left ? left.split(":") : [];
const rightGroups = right ? right.split(":") : [];
const missingGroups = expectedGroups - leftGroups.length - rightGroups.length;
return (address.includes("::") ? [
...leftGroups,
...Array(missingGroups).fill("0"),
...rightGroups
] : leftGroups).map((group) => Number.parseInt(group || "0", 16));
}
function isBlockedIpv6(address) {
const normalizedAddress = normalizeHostname(address).toLowerCase();
const groups = normalizedAddress.includes(".") ? parseIpv4MappedGroups(normalizedAddress) : expandIpv6(normalizedAddress);
if (!groups || groups.length !== 8 || groups.some((group) => Number.isNaN(group))) return false;
const [first, second, third, fourth, fifth, sixth, seventh, eighth] = groups;
const isIpv4Mapped = [
first,
second,
third,
fourth,
fifth
].every((group) => group === 0) && sixth === 65535;
return groups.every((group) => group === 0) || groups.slice(0, 7).every((group) => group === 0) && eighth === 1 || isIpv4Mapped && isBlockedIpv4([
seventh >> 8,
seventh & 255,
eighth >> 8,
eighth & 255
].join(".")) || (first & 65024) === 64512 || (first & 65472) === 65152 || (first & 65280) === 65280;
}
function isBlockedIp(address) {
const normalizedAddress = normalizeHostname(address);
const ipVersion = net.isIP(normalizedAddress);
return ipVersion === 4 ? isBlockedIpv4(normalizedAddress) : ipVersion === 6 ? isBlockedIpv6(normalizedAddress) : false;
}
function assertAllowedUrl(url) {
const hostname = normalizeHostname(url.hostname);
if (isBlockedHostname(hostname) || isBlockedIp(hostname)) throw new WebFetchError("URL resolves to a private or reserved address.");
}
function createLookup() {
return (hostname, options, callback) => {
lookup(hostname, options, (error, address, family) => {
if (error) {
callback(error, address, family);
return;
}
if ((Array.isArray(address) ? address.map((result) => result.address) : [address]).find(isBlockedIp)) {
callback(new WebFetchError("URL resolves to a private or reserved address."), address, family);
return;
}
callback(null, address, family);
});
};
}
async function readBody(response) {
const decoder = new TextDecoder();
let content = "";
let truncated = false;
for await (const chunk of response) {
content += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
if (content.length > MAX_CONTENT_LENGTH) {
content = content.slice(0, MAX_CONTENT_LENGTH);
truncated = true;
response.destroy();
break;
}
}
if (!truncated) content += decoder.decode();
return {
content,
truncated
};
}
async function requestUrl(url, redirectsRemaining = MAX_REDIRECTS) {
assertAllowedUrl(url);
return new Promise((resolve, reject) => {
const request = (url.protocol === "https:" ? https : http).request(url, {
headers: {
"user-agent": "Mastra Web Fetch Tool/1.0",
accept: "text/html,text/plain,application/json,application/xml;q=0.9,*/*;q=0.8"
},
lookup: createLookup(),
timeout: TIMEOUT_MS
}, (response) => {
(async () => {
const location = response.headers.location;
if (location && response.statusCode && response.statusCode >= 300 && response.statusCode < 400) {
response.resume();
if (redirectsRemaining <= 0) throw new WebFetchError(`Too many redirects. Maximum is ${MAX_REDIRECTS}.`);
const nextUrl = parseHttpUrl(new URL(location, url).toString());
if (!nextUrl) throw new WebFetchError("Redirect target must use HTTP or HTTPS.");
resolve(await requestUrl(nextUrl, redirectsRemaining - 1));
return;
}
const { content, truncated } = await readBody(response);
resolve({
content,
truncated,
status: response.statusCode,
statusText: response.statusMessage,
contentType: Array.isArray(response.headers["content-type"]) ? response.headers["content-type"][0] : response.headers["content-type"] ?? null,
url: url.toString(),
ok: response.statusCode ? response.statusCode >= 200 && response.statusCode < 300 : false
});
})().catch(reject);
});
request.on("timeout", () => {
request.destroy(new WebFetchError(`Request timed out after ${TIMEOUT_MS}ms.`));
});
request.on("error", reject);
request.end();
});
}
function getErrorMessage(error) {
if (error instanceof Error) return error.message;
return "Unknown error";
}
const webFetchTool = createTool({
id: "web_fetch",
description: "Fetch a web page by URL and return text content with basic response metadata.",
inputSchema: z.object({ url: z.string().min(1).describe("The fully qualified HTTP or HTTPS URL to fetch.") }),
outputSchema: z.object({
content: z.string(),
truncated: z.boolean().optional(),
status: z.number().optional(),
statusText: z.string().optional(),
contentType: z.string().nullable().optional(),
url: z.string().optional(),
ok: z.boolean().optional(),
isError: z.boolean().optional()
}),
execute: async ({ url }) => {
const parsedUrl = parseHttpUrl(url);
if (!parsedUrl) return {
content: "Failed to fetch URL: only HTTP and HTTPS URLs are supported.",
isError: true
};
try {
return await requestUrl(parsedUrl);
} catch (error) {
return {
content: `Failed to fetch URL: ${getErrorMessage(error)}`,
isError: true
};
}
}
});
//#endregion
//#region src/tools/builtin/web-search.ts
const WEB_SEARCH_TOOL_MARKER = Symbol.for("mastra.tools.webSearchTool");
const webSearchTool = Object.freeze({ [WEB_SEARCH_TOOL_MARKER]: true });
function isWebSearchTool(tool) {
return tool === webSearchTool || typeof tool === "object" && tool !== null && tool[WEB_SEARCH_TOOL_MARKER] === true;
}
function normalizeWebSearchProvider(providerOrModel) {
const provider = getProviderString(providerOrModel);
const supportedProviders = /* @__PURE__ */ new Set([
"openai",
"anthropic",
"google",
"xai"
]);
if (supportedProviders.has(provider)) return provider;
const routerProvider = getRouterProvider(provider);
if (supportedProviders.has(routerProvider)) return routerProvider;
throw new MastraError({
id: "WEB_SEARCH_UNSUPPORTED_PROVIDER",
domain: ErrorDomain.AGENT,
category: ErrorCategory.USER,
details: { provider },
text: `The built-in webSearchTool supports OpenAI, Anthropic, Google, and xAI models. Could not infer a supported provider from "${provider}".`
});
}
function createWebSearchProviderTool(provider) {
const tool = getWebSearchProviderTool(provider);
return {
type: "provider-defined",
id: tool.id,
name: tool.name,
args: {}
};
}
function getProviderString(providerOrModel) {
if (typeof providerOrModel === "string") return providerOrModel;
if (typeof providerOrModel === "object" && providerOrModel !== null) {
const model = providerOrModel;
if (typeof model.provider === "string") {
if (model.provider === "openai-compatible") {
if (typeof model.modelId === "string") return model.modelId;
if (typeof model.id === "string") return model.id;
}
return model.provider;
}
if (typeof model.modelId === "string") return model.modelId;
if (typeof model.id === "string") return model.id;
}
return String(providerOrModel);
}
function getRouterProvider(provider) {
const slashIndex = provider.indexOf("/");
return slashIndex > 0 ? provider.slice(0, slashIndex) : provider;
}
function getWebSearchProviderTool(provider) {
switch (provider) {
case "openai": return {
id: "openai.web_search",
name: "web_search"
};
case "anthropic": return {
id: "anthropic.web_search_20250305",
name: "web_search"
};
case "google": return {
id: "google.google_search",
name: "google_search"
};
case "xai": return {
id: "xai.web_search",
name: "web_search"
};
}
}
//#endregion
//#region src/tools/builtin/submit-plan.ts
const resumeSchema = z.object({
action: z.enum(["approved", "rejected"]),
feedback: z.string().optional(),
path: z.string().optional(),
title: z.string().optional(),
plan: z.string().optional()
});
/**
* Built-in, agent-agnostic tool: submit an implementation plan for user review.
*
* Pausing uses the agent-native tool suspension primitive: the tool calls
* `suspend({ path })`, which makes the agent emit a `tool-call-suspended` event and
* persist run state. The host validates the plan file path, reads it, renders it,
* collects an approve/reject decision, and continues the run via `agent.resumeStream({ action,
* feedback })`; the tool re-runs with `resumeData` set to that decision and reports it
* back to the model.
*
* This tool is deliberately host-agnostic: it does not know about AgentController modes or any
* UI. A plain Agent (e.g. embedded in Studio or a customer app) can use it directly, and
* a AgentController can layer mode-switch behavior on top of the approval in its own response
* handling without the tool needing to change.
*
* The tool takes the plan file `path` — never the plan body. The host reads the plan from
* disk at that path, so more than one plan can exist over time. When executed without an
* agent `suspend` (e.g. direct invocation outside an agent run), the tool returns the path
* as readable text so the submission is still surfaced.
*/
const submitPlanTool = createTool({
id: "submit_plan",
description: "Submit a plan you wrote to a markdown file for review. Pass the `path` to that file (e.g. `.mastracode/plans/add-dark-mode.md`). Write/edit the file first — do not paste the plan contents here. Reuse the same file across revisions; only create a new file for a genuinely new plan. The user can approve, reject, or request changes. On approval, the system automatically switches to the default mode so you can implement.",
inputSchema: z.object({ path: z.string().describe("Path to the plan markdown file on disk (e.g. `.mastracode/plans/add-dark-mode.md`).") }),
suspendSchema: z.object({
path: z.string(),
title: z.string().optional(),
plan: z.string().optional()
}),
resumeSchema,
execute: async ({ path }, context) => {
try {
const resumeData = context?.agent?.resumeData;
if (resumeData !== void 0) {
if (resumeData.action === "approved") return {
content: "Plan approved. Proceed with implementation following the approved plan.",
isError: false,
submittedPlan: {
title: resumeData.title,
path: resumeData.path,
plan: resumeData.plan
}
};
if (resumeData.feedback) return {
content: `Plan was not approved. The user wants revisions.\n\nUser feedback: ${resumeData.feedback}\n\nPlease revise the plan based on the feedback and submit again with submit_plan.`,
isError: false,
submittedPlan: {
title: resumeData.title,
path: resumeData.path,
plan: resumeData.plan
}
};
return {
content: "Plan was not approved. The user will send revision instructions in their next message. Stop now and wait for the user to provide feedback before revising the plan.",
isError: false,
submittedPlan: {
title: resumeData.title,
path: resumeData.path,
plan: resumeData.plan
}
};
}
const suspend = context?.agent?.suspend;
if (suspend) {
await suspend({ path });
return;
}
return {
content: `[Plan submitted for review]\n\nPath: ${path}`,
isError: false
};
} catch (error) {
return {
content: `Failed to submit plan: ${error instanceof Error ? error.message : "Unknown error"}`,
isError: true
};
}
}
});
//#endregion
export { jsonSchemaToTsString as _, webSearchTool as a, formatQuestionAnswer as c, StdioCodeModeTransport as d, FRAME_PREFIX as f, generateStubs as g, createCodeModeInstructions as h, normalizeWebSearchProvider as i, createCodeMode as l, buildRunner as m, createWebSearchProviderTool as n, webFetchTool as o, buildProgramModule as p, isWebSearchTool as r, askUserTool as s, submitPlanTool as t, createCodeModeTool as u, sanitizeToolId as v };
//# sourceMappingURL=tools-DdVMYter.js.map