@mastra/core
Version:
1,082 lines (1,069 loc) • 40 kB
JavaScript
const require_rolldown_runtime = require("./rolldown-runtime-uwYp4b74.cjs");
const require_error = require("./error-B-e62x-A.cjs");
const require_tool = require("./tool-d85xHVkl.cjs");
const require_errors = require("./errors-Bfv4VhVf.cjs");
let crypto = require("crypto");
let zod_v4 = require("zod/v4");
let os = require("os");
let path = require("path");
let fs_promises = require("fs/promises");
let url = require("url");
let dns = require("dns");
let http = require("http");
http = require_rolldown_runtime.__toESM(http, 1);
let https = require("https");
https = require_rolldown_runtime.__toESM(https, 1);
let net = require("net");
net = require_rolldown_runtime.__toESM(net, 1);
let _mastra_schema_compat_schema = require("@mastra/schema-compat/schema");
//#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 (!(0, _mastra_schema_compat_schema.isStandardSchemaWithJSON)(schema)) return "unknown";
try {
return jsonSchemaToTsString((0, _mastra_schema_compat_schema.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 require_errors.SandboxFeatureNotSupportedError("processes");
const externals = toolIds.map((toolId) => ({
toolId,
externalName: sanitizeToolId(toolId)
}));
const allowList = new Set(toolIds);
const dir = await (0, fs_promises.mkdtemp)((0, path.join)((0, os.tmpdir)(), "mastra-code-mode-"));
const suffix = (0, crypto.randomBytes)(4).toString("hex");
const programPath = (0, path.join)(dir, `program-${suffix}.ts`);
await (0, fs_promises.writeFile)(programPath, buildProgramModule(program), "utf8");
const runnerSource = buildRunner({
programModule: (0, url.pathToFileURL)(programPath).href,
externals
});
const runnerPath = (0, path.join)(dir, `runner-${suffix}.mjs`);
await (0, fs_promises.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 (0, fs_promises.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 = zod_v4.z.object({ code: zod_v4.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 = zod_v4.z.object({
success: zod_v4.z.boolean(),
result: zod_v4.z.unknown().optional(),
logs: zod_v4.z.array(zod_v4.z.string()).optional(),
error: zod_v4.z.object({
message: zod_v4.z.string(),
name: zod_v4.z.string().optional(),
line: zod_v4.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 require_tool.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 (require_tool.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 = zod_v4.z.object({
label: zod_v4.z.string().describe("Short display text for this option (1-5 words)"),
description: zod_v4.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 = require_tool.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: zod_v4.z.object({
question: zod_v4.z.string().min(1).describe("The question to ask the user. Should be clear and specific."),
options: zod_v4.z.array(optionSchema).optional().describe("Optional choices. If provided, shows a selection list. If omitted, shows a free-text input."),
selectionMode: zod_v4.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: zod_v4.z.object({
question: zod_v4.z.string(),
options: zod_v4.z.array(optionSchema).optional(),
selectionMode: zod_v4.z.enum(["single_select", "multi_select"]).optional()
}),
resumeSchema: zod_v4.z.union([zod_v4.z.string(), zod_v4.z.array(zod_v4.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.default.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) => {
(0, dns.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.default : http.default).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 = require_tool.createTool({
id: "web_fetch",
description: "Fetch a web page by URL and return text content with basic response metadata.",
inputSchema: zod_v4.z.object({ url: zod_v4.z.string().min(1).describe("The fully qualified HTTP or HTTPS URL to fetch.") }),
outputSchema: zod_v4.z.object({
content: zod_v4.z.string(),
truncated: zod_v4.z.boolean().optional(),
status: zod_v4.z.number().optional(),
statusText: zod_v4.z.string().optional(),
contentType: zod_v4.z.string().nullable().optional(),
url: zod_v4.z.string().optional(),
ok: zod_v4.z.boolean().optional(),
isError: zod_v4.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 require_error.MastraError({
id: "WEB_SEARCH_UNSUPPORTED_PROVIDER",
domain: require_error.ErrorDomain.AGENT,
category: require_error.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 = zod_v4.z.object({
action: zod_v4.z.enum(["approved", "rejected"]),
feedback: zod_v4.z.string().optional(),
path: zod_v4.z.string().optional(),
title: zod_v4.z.string().optional(),
plan: zod_v4.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 = require_tool.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: zod_v4.z.object({ path: zod_v4.z.string().describe("Path to the plan markdown file on disk (e.g. `.mastracode/plans/add-dark-mode.md`).") }),
suspendSchema: zod_v4.z.object({
path: zod_v4.z.string(),
title: zod_v4.z.string().optional(),
plan: zod_v4.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
Object.defineProperty(exports, "FRAME_PREFIX", {
enumerable: true,
get: function() {
return FRAME_PREFIX;
}
});
Object.defineProperty(exports, "StdioCodeModeTransport", {
enumerable: true,
get: function() {
return StdioCodeModeTransport;
}
});
Object.defineProperty(exports, "askUserTool", {
enumerable: true,
get: function() {
return askUserTool;
}
});
Object.defineProperty(exports, "buildProgramModule", {
enumerable: true,
get: function() {
return buildProgramModule;
}
});
Object.defineProperty(exports, "buildRunner", {
enumerable: true,
get: function() {
return buildRunner;
}
});
Object.defineProperty(exports, "createCodeMode", {
enumerable: true,
get: function() {
return createCodeMode;
}
});
Object.defineProperty(exports, "createCodeModeInstructions", {
enumerable: true,
get: function() {
return createCodeModeInstructions;
}
});
Object.defineProperty(exports, "createCodeModeTool", {
enumerable: true,
get: function() {
return createCodeModeTool;
}
});
Object.defineProperty(exports, "createWebSearchProviderTool", {
enumerable: true,
get: function() {
return createWebSearchProviderTool;
}
});
Object.defineProperty(exports, "formatQuestionAnswer", {
enumerable: true,
get: function() {
return formatQuestionAnswer;
}
});
Object.defineProperty(exports, "generateStubs", {
enumerable: true,
get: function() {
return generateStubs;
}
});
Object.defineProperty(exports, "isWebSearchTool", {
enumerable: true,
get: function() {
return isWebSearchTool;
}
});
Object.defineProperty(exports, "jsonSchemaToTsString", {
enumerable: true,
get: function() {
return jsonSchemaToTsString;
}
});
Object.defineProperty(exports, "normalizeWebSearchProvider", {
enumerable: true,
get: function() {
return normalizeWebSearchProvider;
}
});
Object.defineProperty(exports, "sanitizeToolId", {
enumerable: true,
get: function() {
return sanitizeToolId;
}
});
Object.defineProperty(exports, "submitPlanTool", {
enumerable: true,
get: function() {
return submitPlanTool;
}
});
Object.defineProperty(exports, "webFetchTool", {
enumerable: true,
get: function() {
return webFetchTool;
}
});
Object.defineProperty(exports, "webSearchTool", {
enumerable: true,
get: function() {
return webSearchTool;
}
});
//# sourceMappingURL=tools-B-JXNZhs.cjs.map