UNPKG

@mastra/core

Version:
1,082 lines (1,069 loc) 40 kB
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