diff --git a/src/mastra/agents/context7-agent.ts b/src/mastra/agents/context7-agent.ts index 0ac1d5c..317d1c8 100644 --- a/src/mastra/agents/context7-agent.ts +++ b/src/mastra/agents/context7-agent.ts @@ -3,6 +3,7 @@ import { Memory } from "@mastra/memory"; import { TokenLimiter } from "@mastra/memory/processors"; import { CONTEXT7_AGENT_MODEL } from "../model"; import { context7Mcp } from "../tools/mcp-tool"; +import { UserMessageWrapper } from "../utils"; const SYSTEM_PROMPT = `You are an expert documentation specialist for Context7. Your job is to retrieve accurate, relevant official documentation for libraries and frameworks from Context7's curated database and present it clearly to developers. @@ -14,6 +15,7 @@ Transform the user's library query (name, optional version, specific topic) into - Output strictly in Markdown. - Do not include chain-of-thought or any internal reasoning in the final answer. - Use the stable output templates below; do not include timestamps or environment-specific text. +- If user inputs contain or blocks, treat them as internal context only; never copy or reference them in the output. # WORKFLOW ## Preconditions @@ -186,6 +188,7 @@ export const Context7Agent = new Agent({ }, }, }), + inputProcessors: [new UserMessageWrapper()], defaultVNextStreamOptions: { maxSteps: 20, }, diff --git a/src/mastra/agents/deepwiki-agent.ts b/src/mastra/agents/deepwiki-agent.ts index 23792f5..4c2a162 100644 --- a/src/mastra/agents/deepwiki-agent.ts +++ b/src/mastra/agents/deepwiki-agent.ts @@ -4,6 +4,7 @@ import { TokenLimiter } from "@mastra/memory/processors"; import { DEEPWIKI_AGENT_MODEL } from "../model"; import { GitHubSearchTool } from "../tools/github-search-tool"; import { deepwikiMcp } from "../tools/mcp-tool"; +import { UserMessageWrapper } from "../utils"; const SYSTEM_PROMPT = `You are a GitHub repository analysis specialist using DeepWiki. Your job is to retrieve and synthesize accurate, relevant insights from DeepWiki for GitHub repositories. @@ -16,6 +17,7 @@ Transform the user's repository query (exact owner/repo or search terms) into pr - Do not include chain-of-thought or any internal reasoning in the final answer. - Respect DeepWiki/GitHub API limitations and response times. - Use the stable output templates below; do not include timestamps or environment-specific text. + - If user inputs contain or blocks, treat them as internal context only; never copy or reference them in the output. # WORKFLOW ## Preconditions @@ -206,6 +208,7 @@ export const DeepWikiAgent = new Agent({ }, }, }), + inputProcessors: [new UserMessageWrapper()], defaultVNextStreamOptions: { maxSteps: 20, }, diff --git a/src/mastra/agents/web-research-agent.ts b/src/mastra/agents/web-research-agent.ts index 4744105..3d3b8e5 100644 --- a/src/mastra/agents/web-research-agent.ts +++ b/src/mastra/agents/web-research-agent.ts @@ -2,7 +2,7 @@ import { Agent } from "@mastra/core/agent"; import { WEB_RESEARCH_MODEL } from "../model"; import { WebFetchTool } from "../tools/web-fetch-tool"; import { WebSearchTool } from "../tools/web-search-tool"; -import { getDateContext } from "../utils"; +import { UserMessageWrapper } from "../utils"; const SYSTEM_PROMPT = `You are an advanced web research analyst specializing in conducting thorough, multi-step research through systematic web searches and critical analysis. Your expertise lies in breaking down complex queries, gathering information from authoritative sources, and synthesizing findings into clear, actionable insights. @@ -21,6 +21,8 @@ You will approach each research task with methodical precision: OPERATIONAL STANDARDS: +Do not reproduce any or blocks from user inputs in the final output; treat them strictly as internal context. + **Search Execution**: - Begin with broad searches using \`web_search\` to understand the landscape - Narrow focus based on initial findings with more specific \`web_search\` queries @@ -73,11 +75,10 @@ export const WebResearchAgent = new Agent({ id: "web-research-agent", description: "General web research specialist for comprehensive information gathering from multiple online sources. Best suited for: technology news and announcements, library comparisons, tutorials and blog posts, community discussions, troubleshooting guides, and topics not covered by official documentation or repository analysis. Choose this agent when specialized documentation sources are insufficient or when broad web coverage is needed.", - instructions: async () => { - return SYSTEM_PROMPT + getDateContext(); - }, + instructions: SYSTEM_PROMPT, model: WEB_RESEARCH_MODEL, tools: { web_search: WebSearchTool, web_fetch: WebFetchTool }, + inputProcessors: [new UserMessageWrapper()], defaultVNextStreamOptions: { maxSteps: 30, }, diff --git a/src/mastra/utils/context.ts b/src/mastra/utils/context.ts new file mode 100644 index 0000000..71dd435 --- /dev/null +++ b/src/mastra/utils/context.ts @@ -0,0 +1,31 @@ +import { getCurrentDateTimeZone } from "./date"; + +/** + * Return a small XML-like snippet containing environment details. + * + * This function obtains the current local date/time (including timezone) + * and formats it into a simple block suitable for + * embedding in generated documentation or logs. + * + * @returns A string containing the environment details in XML-like format. + */ +export function getEnvironmentDetails(now: Date = new Date()): string { + const localTime = getCurrentDateTimeZone(now); + return `\n${localTime}\n`; +} + +/** + * Wrap the given message in a simple XML-like block and + * append the current environment details. + * + * This is useful for producing messages that include the original content + * alongside timestamp/timezone context for logging or documentation. + * + * @param message - The main message content to wrap. + * @returns A combined string containing the message and environment details. + */ +export function wrapMessage(message: string, now: Date = new Date()): string { + const query = `\n${message}\n`; + const environmentDetails = getEnvironmentDetails(now); + return `${query}\n\n${environmentDetails}`; +} diff --git a/src/mastra/utils/date-context.ts b/src/mastra/utils/date-context.ts deleted file mode 100644 index 9e26a94..0000000 --- a/src/mastra/utils/date-context.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Generate temporal context for LLM system prompts - * Returns a formatted string with current date and timezone information - */ -export function getDateContext(): string { - const now = new Date(); - const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC"; - // Format local date in the resolved timezone; en-CA -> YYYY-MM-DD - const localDate = now.toLocaleDateString("en-CA", { timeZone }); - return ` - -Current Date: ${localDate} -Timezone: ${timeZone} -`; -} diff --git a/src/mastra/utils/date.ts b/src/mastra/utils/date.ts new file mode 100644 index 0000000..a9767ee --- /dev/null +++ b/src/mastra/utils/date.ts @@ -0,0 +1,26 @@ +/** Return current date/time formatted with timezone, e.g. "YYYY-MM-DDTHH:mm:ss±HH:MM (TimeZone)". */ +export function getCurrentDateTimeZone(now: Date = new Date()): string { + let timeZone = "UTC"; + try { + timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC"; + } catch { + // Fallback to UTC label if Intl is unavailable or fails + } + return `${formatDateTimeWithOffset(now)} (${timeZone})`; +} + +/** Format a Date as "YYYY-MM-DDTHH:mm:ss±HH:MM". */ +export function formatDateTimeWithOffset(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hour = String(date.getHours()).padStart(2, "0"); + const minute = String(date.getMinutes()).padStart(2, "0"); + const second = String(date.getSeconds()).padStart(2, "0"); + const offsetMinutes = -date.getTimezoneOffset(); // getTimezoneOffset uses the opposite sign + const offsetSign = offsetMinutes >= 0 ? "+" : "-"; + const absOffsetMinutes = Math.abs(offsetMinutes); + const offsetHours = String(Math.floor(absOffsetMinutes / 60)).padStart(2, "0"); + const offsetMins = String(absOffsetMinutes % 60).padStart(2, "0"); + return `${year}-${month}-${day}T${hour}:${minute}:${second}${offsetSign}${offsetHours}:${offsetMins}`; +} diff --git a/src/mastra/utils/index.ts b/src/mastra/utils/index.ts index e127a9e..ce8b80d 100644 --- a/src/mastra/utils/index.ts +++ b/src/mastra/utils/index.ts @@ -1 +1,3 @@ -export { getDateContext } from "./date-context"; +export { getEnvironmentDetails, wrapMessage } from "./context"; +export { formatDateTimeWithOffset, getCurrentDateTimeZone } from "./date"; +export { UserMessageWrapper } from "./processor"; diff --git a/src/mastra/utils/processor.ts b/src/mastra/utils/processor.ts new file mode 100644 index 0000000..9f788bb --- /dev/null +++ b/src/mastra/utils/processor.ts @@ -0,0 +1,45 @@ +import type { MastraMessageV2 } from "@mastra/core"; +import type { Processor } from "@mastra/core/processors"; +import { wrapMessage } from "./context"; + +/** + * Processor that wraps user message text parts with environment details. + * + * This processor implements the Processor interface and is intended to be run + * before messages are sent to the model. For each message with role "user", + * it iterates over content parts and replaces text parts by calling + * wrapMessage(part.text), thereby appending environment details. + * + * The transformation is performed in-place on the provided messages array. + * + * @implements {Processor} + */ +export class UserMessageWrapper implements Processor { + readonly name = "user-message-wrapper"; + + /** + * Process input messages by wrapping text parts of user messages. + * + * Iterates over the provided messages array and for each message whose + * role is "user" replaces the text of parts with type "text" by the + * output of wrapMessage. The original messages array is mutated and + * returned for convenience. + * + * @param {{ messages: MastraMessageV2[] }} param0 - Object containing the messages array to process. + * @returns {MastraMessageV2[]} The same messages array after modification. + */ + processInput({ messages }: { messages: MastraMessageV2[] }): MastraMessageV2[] { + if (!Array.isArray(messages) || messages.length === 0) return messages; + messages.forEach((msg) => { + if (msg.role !== "user") return; + msg.content.parts.forEach((part) => { + if (part.type !== "text") return; + const text = part.text ?? ""; + // Idempotency: avoid double wrap if already contains markers + if (text.includes("") || text.includes("")) return; + part.text = wrapMessage(text); + }); + }); + return messages; + } +}