Conversation
- Introduced `text-analysis.tool.ts` for analyzing text with operations such as word count, sentence count, readability, sentiment analysis, language detection, and summarization. - Implemented `text-processing.tool.ts` for processing and transforming text with operations like lowercase, uppercase, trim, and extracting information (numbers, emails, URLs). - Added helper functions for text analysis including counting words, sentences, paragraphs, calculating readability, analyzing sentiment, detecting language, generating summaries, and extracting various data types. - Created `url-tool.ts` which includes `urlValidationTool` for validating, parsing, normalizing, and analyzing URLs with operations like checking reachability and fetching metadata. - Added `urlManipulationTool` for manipulating URLs by adding, removing, or updating query parameters, paths, and fragments. - Implemented context schemas using Zod for both tools to define default settings and options. - Enhanced logging for input and output stages of both tools to improve traceability and debugging.
Learn moreAll Green is an AI agent that automatically: ✅ Addresses code review comments ✅ Fixes failing CI checks ✅ Resolves merge conflicts |
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
🤖 Hi @ssdeanx, I've received your request, and I'm working on it now! You can track my progress in the logs for more details. |
|
Caution Review failedThe pull request is closed. Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings. WalkthroughThis PR introduces comprehensive streaming and cancellation support across the Mastra tools ecosystem, adding lifecycle hooks (onInputDelta, onInputStart, onInputAvailable, onOutput) for input/output observability, implementing AbortSignal-based cancellation handling, integrating OpenTelemetry tracing with progress events, and adding four new tool implementations (calculator, datetime, git operations, text analysis). It also bumps several package dependencies to newer patch versions. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (43)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR implements comprehensive code formatting and standardization improvements across the codebase, with a focus on consistent semicolon usage, quote style normalization, and the addition of onInputDelta lifecycle hooks to existing tools.
- Standardized all code to use single quotes and semicolons consistently
- Added
onInputDeltahooks to multiple tool implementations for enhanced streaming support - Introduced new tool modules including URL validation, text analysis, random generation, datetime operations, and local Git operations
- Added cancellation handling with
abortSignalchecks across multiple tools
Reviewed changes
Copilot reviewed 43 out of 44 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| ui/sidebar.tsx | Added constant declarations for sidebar configuration values |
| tests/test-results/test-results.json | Updated test execution timestamp |
| src/mastra/tools/write-note.ts | Reformatted with consistent quotes/semicolons and added onInputDelta hook |
| src/mastra/tools/web-scraper-tool.ts | Added abort signal handling and onInputDelta hook |
| src/mastra/tools/weather-tool.ts | Enhanced cancellation handling and added import type modifier |
| src/mastra/tools/url-tool.ts | New file implementing URL validation and manipulation tools |
| src/mastra/tools/text-analysis.tool.ts | New file implementing text analysis and processing tools |
| src/mastra/tools/test-generator.tool.ts | Reformatted and added onInputDelta hook |
| src/mastra/tools/serpapi-*.tool.ts | Added onInputDelta hooks and abort signal logging |
| src/mastra/tools/random-generator.tool.ts | New file implementing random data generation tool |
| src/mastra/tools/polygon-tools.ts | Added abort signal handling and error handling improvements |
| src/mastra/tools/pdf*.ts | Reformatted with consistent style and added onInputDelta hooks |
| src/mastra/tools/multi-string-edit.tool.ts | Reformatted and added onInputDelta hook |
| src/mastra/tools/jwt-auth.tool.ts | Added cancellation handling and onInputDelta hook |
| src/mastra/tools/json-to-csv.tool.ts | Added abort signal checks and onInputDelta hook |
| src/mastra/tools/index.ts | Added exports for new tools (calculator, datetime, git-local, random, text-analysis, url) |
| src/mastra/tools/github.ts | Added cancellation handling and onInputDelta hook |
| src/mastra/tools/git-local.tool.ts | New file implementing comprehensive local Git operations |
| src/mastra/tools/fs.ts | Added abort signal handling and onInputDelta hook |
| src/mastra/tools/find-*.tool.ts | Reformatted with consistent style and added onInputDelta hooks |
| src/mastra/tools/extract*.ts | Reformatted with consistent semicolons/quotes and added onInputDelta hooks |
| src/mastra/tools/execa-tool.ts | Reformatted and added onInputDelta hook |
| src/mastra/tools/editor-agent-tool.ts | Reformatted with consistent style and added onInputDelta hook |
| src/mastra/tools/diff-review.tool.ts | Reformatted and added onInputDelta hook |
| src/mastra/tools/datetime.tool.ts | New file implementing date/time manipulation and timezone conversion |
| src/mastra/tools/data-validator.tool.ts | Reformatted with consistent style and added onInputDelta hook |
| src/mastra/tools/csv-to-json.tool.ts | Added abort signal handling and onInputDelta hook |
|
🤖 I'm sorry @ssdeanx, but I was unable to process your request. Please see the logs for more details. |
Summary of ChangesHello @ssdeanx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a substantial expansion of the agent's functional toolkit, adding new capabilities for advanced mathematical computations, date and time management, local Git repository interactions, random data generation, and sophisticated text analysis. Concurrently, it significantly enhances the robustness and user experience of existing tools by implementing detailed input streaming feedback and cancellation mechanisms, ensuring smoother and more controllable long-running operations. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request primarily focuses on enhancing tool lifecycle observability by adding onInputDelta hooks and abortSignal handling to numerous existing tools, including arxiv.tool.ts, browser-tool.ts, code-search.tool.ts, csv-to-json.tool.ts, fs.ts, jwt-auth.tool.ts, polygon-tools.ts, serpapi-academic-local.tool.ts, serpapi-news-trends.tool.ts, and serpapi-search.tool.ts. It also introduces several new tools: a comprehensive calculator.tool.ts with expression evaluation, unit conversion, and matrix operations; datetime.tool.ts for date/time manipulation and timezone conversions; git-local.tool.ts offering advanced Git operations like status, diff, commit, log, branch, stash, and config management; random-generator.tool.ts for generating various random data types; text-analysis.tool.ts for text statistics and processing; and url-tool.ts for URL validation and manipulation. Dependency versions in package.json were updated, and the AGENTS.md documentation was revised to reflect the complete implementation of lifecycle hooks across all tools. Review comments highlight a security vulnerability in calculator.tool.ts due to new Function() usage, inaccuracies in datetime.tool.ts's date difference and timezone conversion logic, missing abortSignal propagation in execaTool.ts and copywriter-agent-tool.ts, an incorrect error message in polygon-tools.ts's AbortError handling, and a request to re-include startIndex and maxResults in serpapi-academic-local.tool.ts's onOutput logging.
| function evaluateExpression( | ||
| expression: string, | ||
| variables: Record<string, number> = {} | ||
| ): number { | ||
| // Remove whitespace | ||
| const cleanExpr = expression.replace(/\s+/g, '') | ||
|
|
||
| // Basic security checks | ||
| if (/[^0-9+\-*/().\s,a-zA-Z_]/.test(cleanExpr)) { | ||
| throw new Error('Invalid characters in expression') | ||
| } | ||
|
|
||
| if ( | ||
| cleanExpr.includes('__proto__') || | ||
| cleanExpr.includes('prototype') || | ||
| cleanExpr.includes('constructor') | ||
| ) { | ||
| throw new Error('Potentially unsafe expression') | ||
| } | ||
|
|
||
| // Create function with safe context and variables | ||
| const context = { ...createSafeContext(), ...variables } | ||
| const func = new Function(...Object.keys(context), `return ${cleanExpr}`) | ||
|
|
||
| try { | ||
| const result = func(...Object.values(context)) | ||
| if (typeof result !== 'number' || !isFinite(result)) { | ||
| throw new Error('Expression did not evaluate to a valid number') | ||
| } | ||
| return result | ||
| } catch (error) { | ||
| throw new Error( | ||
| `Evaluation failed: ${error instanceof Error ? error.message : 'Unknown error'}` | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
The evaluateExpression function uses new Function(...) to evaluate a string as code. This is a significant security vulnerability as it can allow for arbitrary code execution, even with the existing sanitization attempts. A malicious expression could bypass the checks. Please use a dedicated and safe math expression parser library (like math.js) to prevent potential remote code execution (RCE) vulnerabilities.
| function calculateDateDiff(from: Date, to: Date) { | ||
| const diffMs = to.getTime() - from.getTime() | ||
|
|
||
| const years = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 365.25)) | ||
| const months = Math.floor( | ||
| (diffMs % (1000 * 60 * 60 * 24 * 365.25)) / | ||
| (1000 * 60 * 60 * 24 * 30.44) | ||
| ) | ||
| const days = Math.floor( | ||
| (diffMs % (1000 * 60 * 60 * 24 * 30.44)) / (1000 * 60 * 60 * 24) | ||
| ) | ||
| const hours = Math.floor( | ||
| (diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) | ||
| ) | ||
| const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)) | ||
| const seconds = Math.floor((diffMs % (1000 * 60)) / 1000) | ||
|
|
||
| return { | ||
| years: years || undefined, | ||
| months: months || undefined, | ||
| days: days || undefined, | ||
| hours: hours || undefined, | ||
| minutes: minutes || undefined, | ||
| seconds: seconds || undefined, | ||
| } | ||
| } |
There was a problem hiding this comment.
| function convertTimezone(date: Date, fromTz: string, toTz: string): string { | ||
| // For now, we'll use a simple approach. In a real implementation, | ||
| // you'd want to use a proper timezone library like 'date-fns-tz' or 'luxon' | ||
| // This is a simplified version that assumes UTC offsets | ||
|
|
||
| // Get timezone offsets (this is approximate and not production-ready) | ||
| const fromOffset = getTimezoneOffset(fromTz) | ||
| const toOffset = getTimezoneOffset(toTz) | ||
|
|
||
| const utcTime = date.getTime() - fromOffset * 60 * 1000 | ||
| const targetTime = utcTime + toOffset * 60 * 1000 | ||
|
|
||
| return new Date(targetTime).toISOString() | ||
| } |
There was a problem hiding this comment.
The convertTimezone function uses a simplified, hardcoded map of timezone offsets and does not account for Daylight Saving Time (DST). This will produce incorrect results for many timezones and dates. The comment "not production-ready" highlights this. Please replace this implementation with a proper timezone library like date-fns-tz or luxon to ensure correct timezone conversions.
| execute: async (inputData, context) => { | ||
| const writer = context?.writer | ||
| const requestContext = context?.requestContext | ||
|
|
||
| const tracer = trace.getTracer('execa-tool', '1.0.0'); | ||
| const span = tracer.startSpan('execa-tool', { | ||
| attributes: { | ||
| 'tool.id': 'execa-tool', | ||
| 'tool.input.command': inputData.command, | ||
| 'tool.input.args': inputData.args.join(' '), | ||
| } | ||
| }); | ||
| const tracer = trace.getTracer('execa-tool', '1.0.0') | ||
| const span = tracer.startSpan('execa-tool', { | ||
| attributes: { | ||
| 'tool.id': 'execa-tool', | ||
| 'tool.input.command': inputData.command, | ||
| 'tool.input.args': inputData.args.join(' '), | ||
| }, | ||
| }) | ||
|
|
||
| const { command, args, cwd, timeout, env } = inputData | ||
| await writer?.custom({ type: 'data-tool-progress', data: { status: 'in-progress', message: `💻 Executing command: ${command} ${args.join(' ')}`, stage: 'execaTool' }, id: 'execaTool' }); | ||
| try { | ||
| log.info( | ||
| chalk.green(`Running command: ${command} ${args.join(' ')}`) | ||
| ) | ||
| const optionsEnv: NodeJS.ProcessEnv = { ...process.env, ...(env ?? {}) }; | ||
| const result = await execa(command, args, { | ||
| all: true, | ||
| stdio: 'pipe', | ||
| cwd, | ||
| timeout, | ||
| env: optionsEnv, | ||
| }) | ||
| const output = result.all ?? '' | ||
| await writer?.custom({ type: 'data-tool-progress', data: { status: 'done', message: '✅ Command executed successfully', stage: 'execaTool' }, id: 'execaTool' }); | ||
| span.setAttributes({ | ||
| 'tool.output.success': true, | ||
| 'tool.output.outputLength': output.length, | ||
| }); | ||
| span.end(); | ||
| return { message: chalk.green(output) } | ||
| } catch (e) { | ||
| const errorMsg = e instanceof Error ? e.message : String(e); | ||
| log.error(errorMsg) | ||
| span.recordException(e instanceof Error ? e : new Error(errorMsg)); | ||
| span.setStatus({ code: 2, message: errorMsg }); | ||
| span.end(); | ||
| const execaErr = e as ExecaErrorType; | ||
| if (e instanceof Error && 'all' in e) { | ||
| return { message: execaErr.all ?? execaErr.message ?? 'Command failed' } | ||
| } | ||
| return { message: errorMsg || 'Error' } | ||
| } | ||
| }, | ||
| const { command, args, cwd, timeout, env } = inputData | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'in-progress', | ||
| message: `💻 Executing command: ${command} ${args.join(' ')}`, | ||
| stage: 'execaTool', | ||
| }, | ||
| id: 'execaTool', | ||
| }) | ||
| try { | ||
| log.info( | ||
| chalk.green(`Running command: ${command} ${args.join(' ')}`) | ||
| ) | ||
| const optionsEnv: NodeJS.ProcessEnv = { | ||
| ...process.env, | ||
| ...(env ?? {}), | ||
| } | ||
| const result = await execa(command, args, { | ||
| all: true, | ||
| stdio: 'pipe', | ||
| cwd, | ||
| timeout, | ||
| env: optionsEnv, | ||
| }) | ||
| const output = result.all ?? '' | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'done', | ||
| message: '✅ Command executed successfully', | ||
| stage: 'execaTool', | ||
| }, | ||
| id: 'execaTool', | ||
| }) | ||
| span.setAttributes({ | ||
| 'tool.output.success': true, | ||
| 'tool.output.outputLength': output.length, | ||
| }) | ||
| span.end() | ||
| return { message: chalk.green(output) } | ||
| } catch (e) { | ||
| const errorMsg = e instanceof Error ? e.message : String(e) | ||
| log.error(errorMsg) | ||
| span.recordException(e instanceof Error ? e : new Error(errorMsg)) | ||
| span.setStatus({ code: 2, message: errorMsg }) | ||
| span.end() | ||
| const execaErr = e as ExecaErrorType | ||
| if (e instanceof Error && 'all' in e) { | ||
| return { | ||
| message: | ||
| execaErr.all ?? execaErr.message ?? 'Command failed', | ||
| } | ||
| } | ||
| return { message: errorMsg || 'Error' } | ||
| } | ||
| }, | ||
| }) | ||
|
|
||
| export type ExecaUITool = InferUITool<typeof execaTool>; | ||
| export type ExecaUITool = InferUITool<typeof execaTool> |
There was a problem hiding this comment.
| onOutput: ({ output, toolCallId, toolName, abortSignal }) => { | ||
| log.info('ArXiv search completed', { | ||
| toolCallId, | ||
| toolName, | ||
| abortSignal: abortSignal?.aborted, | ||
| papersFound: output.papers.length, | ||
| totalResults: output.total_results, | ||
| hook: 'onOutput', | ||
| }) |
| // Handle AbortError specifically | ||
| if (e instanceof Error && e.name === 'AbortError') { | ||
| const cancelMessage = `Browser operation cancelled for ${inputData.url}` | ||
| span.setStatus({ code: 2, message: cancelMessage }) | ||
| span.end() | ||
|
|
||
| await context?.writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'done', | ||
| message: `🛑 ${cancelMessage}`, | ||
| stage: 'browserTool', | ||
| }, | ||
| id: 'browserTool', | ||
| }) | ||
|
|
||
| log.warn(cancelMessage) | ||
| return { message: `Error: ${cancelMessage}` } | ||
| } |
There was a problem hiding this comment.
The AbortError is caught, but instead of re-throwing an error to signal failure, it returns a success-like object with an error message: { message: Error: ${cancelMessage} }. The execute function should throw an exception on failure to ensure the calling agent handles it as an error. Returning an error message in a success payload can lead to unexpected behavior.
| if (error instanceof Error && error.name === 'AbortError') { | ||
| const cancelMessage = `Polygon stock fundamentals cancelled for ${inputData.symbol}` | ||
| const totalDuration = Date.now() - startTime | ||
| rootSpan.setStatus({ code: 2, message: cancelMessage }) | ||
| rootSpan.end() | ||
|
|
||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'done', | ||
| message: `🛑 ${cancelMessage}`, | ||
| stage: 'polygon-stock-fundamentals', | ||
| }, | ||
| id: 'polygon-stock-fundamentals', | ||
| }) | ||
|
|
||
| log.warn(cancelMessage) | ||
| throw new Error(cancelMessage) | ||
| } |
There was a problem hiding this comment.
| execute: async (inputData, context) => { | ||
| const writer = context?.writer | ||
| const mastra = context?.mastra | ||
|
|
||
| const userIdVal = context?.requestContext?.get('userId') | ||
| const userId = typeof userIdVal === 'string' && userIdVal.trim().length > 0 ? userIdVal : 'anonymous' | ||
| const { | ||
| topic, | ||
| contentType = 'blog', | ||
| targetAudience, | ||
| tone, | ||
| length = 'medium', | ||
| specificRequirements, | ||
| } = inputData | ||
| const userIdVal = context?.requestContext?.get('userId') | ||
| const userId = | ||
| typeof userIdVal === 'string' && userIdVal.trim().length > 0 | ||
| ? userIdVal | ||
| : 'anonymous' | ||
| const { | ||
| topic, | ||
| contentType = 'blog', | ||
| targetAudience, | ||
| tone, | ||
| length = 'medium', | ||
| specificRequirements, | ||
| } = inputData | ||
|
|
||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'in-progress', | ||
| message: `✍️ Starting copywriter agent for ${contentType} about "${topic}"`, | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }); | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'in-progress', | ||
| message: `✍️ Starting copywriter agent for ${contentType} about "${topic}"`, | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
|
|
||
| const span = trace.getTracer('copywriter-agent-tool', '1.0.0').startSpan('copywriter-generate', { | ||
| attributes: { | ||
| 'tool.id': 'copywriter-agent', | ||
| 'tool.input.topic': topic, | ||
| 'tool.input.contentType': contentType, | ||
| 'tool.input.targetAudience': targetAudience, | ||
| 'tool.input.tone': tone, | ||
| 'tool.input.length': length, | ||
| } | ||
| }); | ||
| const span = trace | ||
| .getTracer('copywriter-agent-tool', '1.0.0') | ||
| .startSpan('copywriter-generate', { | ||
| attributes: { | ||
| 'tool.id': 'copywriter-agent', | ||
| 'tool.input.topic': topic, | ||
| 'tool.input.contentType': contentType, | ||
| 'tool.input.targetAudience': targetAudience, | ||
| 'tool.input.tone': tone, | ||
| 'tool.input.length': length, | ||
| }, | ||
| }) | ||
|
|
||
| try { | ||
| const agent = mastra?.getAgent?.('copywriterAgent') ?? copywriterAgent | ||
| try { | ||
| const agent = | ||
| mastra?.getAgent?.('copywriterAgent') ?? copywriterAgent | ||
|
|
||
| // Validate agent has an invocation method (generate or stream). | ||
| if (typeof agent.generate !== 'function' && typeof agent.stream !== 'function') { | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'done', | ||
| message: 'Copywriter agent is not available in this runtime.', | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
| // Validate agent has an invocation method (generate or stream). | ||
| if ( | ||
| typeof agent.generate !== 'function' && | ||
| typeof agent.stream !== 'function' | ||
| ) { | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'done', | ||
| message: | ||
| 'Copywriter agent is not available in this runtime.', | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
|
|
||
| return { | ||
| content: `Unable to generate content: copywriterAgent is not available.`, | ||
| contentType, | ||
| title: undefined, | ||
| summary: undefined, | ||
| keyPoints: [], | ||
| wordCount: 0, | ||
| } | ||
| } | ||
| return { | ||
| content: `Unable to generate content: copywriterAgent is not available.`, | ||
| contentType, | ||
| title: undefined, | ||
| summary: undefined, | ||
| keyPoints: [], | ||
| wordCount: 0, | ||
| } | ||
| } | ||
|
|
||
| // Build the prompt with context | ||
| let prompt = `Create ${length} ${contentType} content about: ${topic}` | ||
| // Build the prompt with context | ||
| let prompt = `Create ${length} ${contentType} content about: ${topic}` | ||
|
|
||
| if (userId !== undefined) { | ||
| prompt += `\n\nUser: ${userId}` | ||
| } | ||
| if (userId !== undefined) { | ||
| prompt += `\n\nUser: ${userId}` | ||
| } | ||
|
|
||
| if (typeof targetAudience === 'string' && targetAudience.trim().length > 0) { | ||
| prompt += `\n\nTarget audience: ${targetAudience}` | ||
| } | ||
| if ( | ||
| typeof targetAudience === 'string' && | ||
| targetAudience.trim().length > 0 | ||
| ) { | ||
| prompt += `\n\nTarget audience: ${targetAudience}` | ||
| } | ||
|
|
||
| if (typeof tone === 'string' && tone.trim().length > 0) { | ||
| prompt += `\n\nDesired tone: ${tone}` | ||
| } | ||
| if (typeof tone === 'string' && tone.trim().length > 0) { | ||
| prompt += `\n\nDesired tone: ${tone}` | ||
| } | ||
|
|
||
| if ( | ||
| typeof specificRequirements === 'string' && | ||
| specificRequirements.trim().length > 0 | ||
| ) { | ||
| prompt += `\n\nSpecific requirements: ${specificRequirements}` | ||
| } | ||
| if ( | ||
| typeof specificRequirements === 'string' && | ||
| specificRequirements.trim().length > 0 | ||
| ) { | ||
| prompt += `\n\nSpecific requirements: ${specificRequirements}` | ||
| } | ||
|
|
||
| // Add content type specific guidance | ||
| switch (contentType) { | ||
| case 'blog': | ||
| prompt += | ||
| '\n\nCreate a well-structured blog post with engaging introduction, body sections, and conclusion.' | ||
| break | ||
| case 'marketing': | ||
| prompt += | ||
| '\n\nCreate persuasive marketing copy that highlights benefits and includes clear calls-to-action.' | ||
| break | ||
| case 'social': | ||
| prompt += | ||
| '\n\nCreate concise, engaging social media content optimized for sharing and engagement.' | ||
| break | ||
| case 'technical': | ||
| prompt += | ||
| '\n\nCreate clear, accurate technical content with proper explanations and examples.' | ||
| break | ||
| case 'business': | ||
| prompt += | ||
| '\n\nCreate professional business communication with clear objectives and actionable content.' | ||
| break | ||
| case 'creative': | ||
| prompt += | ||
| '\n\nCreate engaging creative content with storytelling elements and vivid language.' | ||
| break | ||
| case 'general': { | ||
| throw new Error('Not implemented yet: "general" case') | ||
| } | ||
| } | ||
| // Add content type specific guidance | ||
| switch (contentType) { | ||
| case 'blog': | ||
| prompt += | ||
| '\n\nCreate a well-structured blog post with engaging introduction, body sections, and conclusion.' | ||
| break | ||
| case 'marketing': | ||
| prompt += | ||
| '\n\nCreate persuasive marketing copy that highlights benefits and includes clear calls-to-action.' | ||
| break | ||
| case 'social': | ||
| prompt += | ||
| '\n\nCreate concise, engaging social media content optimized for sharing and engagement.' | ||
| break | ||
| case 'technical': | ||
| prompt += | ||
| '\n\nCreate clear, accurate technical content with proper explanations and examples.' | ||
| break | ||
| case 'business': | ||
| prompt += | ||
| '\n\nCreate professional business communication with clear objectives and actionable content.' | ||
| break | ||
| case 'creative': | ||
| prompt += | ||
| '\n\nCreate engaging creative content with storytelling elements and vivid language.' | ||
| break | ||
| case 'general': { | ||
| throw new Error('Not implemented yet: "general" case') | ||
| } | ||
| } | ||
|
|
||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'in-progress', | ||
| message: '🤖 Generating content...', | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'in-progress', | ||
| message: '🤖 Generating content...', | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
|
|
||
| let contentText = '' | ||
| if (typeof agent.stream === 'function') { | ||
| try { | ||
| await writer?.custom({ type: 'data-tool-progress', data: { status: 'in-progress', message: '🔁 Streaming content from copywriter agent', stage: 'copywriter-agent' }, id: 'copywriter-agent' }); | ||
| const stream = await agent.stream(prompt) as MastraModelOutput | undefined | ||
| let contentText = '' | ||
| if (typeof agent.stream === 'function') { | ||
| try { | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'in-progress', | ||
| message: | ||
| '🔁 Streaming content from copywriter agent', | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
| const stream = (await agent.stream(prompt)) as | ||
| | MastraModelOutput | ||
| | undefined | ||
|
|
||
| if (stream?.textStream && writer) { | ||
| await writer?.custom({ type: 'data-tool-progress', data: { status: 'in-progress', message: '🔁 Streaming text from copywriter agent', stage: 'copywriter-agent' }, id: 'copywriter-agent' }); | ||
| await stream.textStream.pipeTo(writer as unknown as WritableStream) | ||
| } else if (stream?.fullStream && writer) { | ||
| await writer?.custom({ type: 'data-tool-progress', data: { status: 'in-progress', message: '🔁 Streaming from copywriter agent (full stream)', stage: 'copywriter-agent' }, id: 'copywriter-agent' }); | ||
| await stream.fullStream.pipeTo(writer as unknown as WritableStream) | ||
| } | ||
| if (stream?.textStream && writer) { | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'in-progress', | ||
| message: | ||
| '🔁 Streaming text from copywriter agent', | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
| await stream.textStream.pipeTo( | ||
| writer as unknown as WritableStream | ||
| ) | ||
| } else if (stream?.fullStream && writer) { | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'in-progress', | ||
| message: | ||
| '🔁 Streaming from copywriter agent (full stream)', | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
| await stream.fullStream.pipeTo( | ||
| writer as unknown as WritableStream | ||
| ) | ||
| } | ||
|
|
||
| const text = (await stream?.text) ?? '' | ||
| const responseObject = stream?.object ?? (() => { try { return JSON.parse(text) } catch { return {} } })() | ||
| const text = (await stream?.text) ?? '' | ||
| const responseObject = | ||
| stream?.object ?? | ||
| (() => { | ||
| try { | ||
| return JSON.parse(text) | ||
| } catch { | ||
| return {} | ||
| } | ||
| })() | ||
|
|
||
| if ((Boolean(responseObject)) && typeof responseObject === 'object') { | ||
| const obj = responseObject as Record<string, unknown> | ||
| const contentVal = obj.content | ||
| if (typeof contentVal === 'string') { | ||
| contentText = contentVal | ||
| if ( | ||
| Boolean(responseObject) && | ||
| typeof responseObject === 'object' | ||
| ) { | ||
| const obj = responseObject as Record<string, unknown> | ||
| const contentVal = obj.content | ||
| if (typeof contentVal === 'string') { | ||
| contentText = contentVal | ||
| } else { | ||
| contentText = text | ||
| } | ||
| } else { | ||
| contentText = text | ||
| } | ||
| } catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err) | ||
| span.recordException( | ||
| err instanceof Error ? err : new Error(msg) | ||
| ) | ||
| try { | ||
| span.setStatus({ | ||
| code: SpanStatusCode.ERROR, | ||
| message: msg, | ||
| }) | ||
| } catch { | ||
| /* ignore */ | ||
| } | ||
| span.end() | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'done', | ||
| message: `❌ Error generating content: ${msg}`, | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
| throw err | ||
| } | ||
| } else { | ||
| contentText = text | ||
| const response = await agent.generate(prompt) | ||
| const responseObject = | ||
| response.object ?? | ||
| (() => { | ||
| try { | ||
| return JSON.parse(response.text) | ||
| } catch { | ||
| return undefined | ||
| } | ||
| })() | ||
| const obj = responseObject as | ||
| | Record<string, unknown> | ||
| | undefined | ||
| if (obj && typeof obj.content === 'string') { | ||
| contentText = obj.content | ||
| } else { | ||
| contentText = response.text | ||
| } | ||
| } | ||
| } else { | ||
| contentText = text | ||
| } | ||
| } catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err) | ||
| span.recordException(err instanceof Error ? err : new Error(msg)) | ||
| try { span.setStatus({ code: SpanStatusCode.ERROR, message: msg }) } catch { /* ignore */ } | ||
| span.end() | ||
| await writer?.custom({ type: 'data-tool-progress', data: { status: 'done', message: `❌ Error generating content: ${msg}`, stage: 'copywriter-agent' }, id: 'copywriter-agent' }); | ||
| throw err | ||
| } | ||
| } else { | ||
| const response = await agent.generate(prompt) | ||
| const responseObject = response.object ?? (() => { try { return JSON.parse(response.text) } catch { return undefined } })() | ||
| const obj = responseObject as Record<string, unknown> | undefined | ||
| if (obj && typeof obj.content === 'string') { | ||
| contentText = obj.content | ||
| } else { | ||
| contentText = response.text | ||
| } | ||
| } | ||
|
|
||
| // Final progress 'done' event | ||
| await writer?.custom({ type: 'data-tool-progress', data: { status: 'done', message: '✅ Content generated', stage: 'copywriter-agent' }, id: 'copywriter-agent' }); | ||
| // Final progress 'done' event | ||
| await writer?.custom({ | ||
| type: 'data-tool-progress', | ||
| data: { | ||
| status: 'done', | ||
| message: '✅ Content generated', | ||
| stage: 'copywriter-agent', | ||
| }, | ||
| id: 'copywriter-agent', | ||
| }) | ||
|
|
||
| // Parse and structure the response | ||
| const content = contentText | ||
| const wordCount = content.split(/\s+/).length | ||
| // Parse and structure the response | ||
| const content = contentText | ||
| const wordCount = content.split(/\s+/).length | ||
|
|
||
| // Extract title if present (look for # or ## at start) | ||
| const titleMatch = /^#{1,2}\s+(.+)$/m.exec(content) | ||
| const title = titleMatch ? titleMatch[1] : undefined | ||
| // Extract title if present (look for # or ## at start) | ||
| const titleMatch = /^#{1,2}\s+(.+)$/m.exec(content) | ||
| const title = titleMatch ? titleMatch[1] : undefined | ||
|
|
||
| // Create a simple summary from the first paragraph or first few sentences | ||
| const firstParagraph = | ||
| content.split('\n\n')[0] ?? content.split('\n')[0] ?? '' | ||
| const summary = | ||
| firstParagraph.length > 200 | ||
| ? firstParagraph.substring(0, 200) + '...' | ||
| : firstParagraph | ||
| // Create a simple summary from the first paragraph or first few sentences | ||
| const firstParagraph = | ||
| content.split('\n\n')[0] ?? content.split('\n')[0] ?? '' | ||
| const summary = | ||
| firstParagraph.length > 200 | ||
| ? firstParagraph.substring(0, 200) + '...' | ||
| : firstParagraph | ||
|
|
||
| span.setAttribute('tool.output.success', true); | ||
| span.setAttribute('tool.output.wordCount', wordCount); | ||
| span.setAttribute('tool.output.contentLength', content.length); | ||
| span.end(); | ||
| span.setAttribute('tool.output.success', true) | ||
| span.setAttribute('tool.output.wordCount', wordCount) | ||
| span.setAttribute('tool.output.contentLength', content.length) | ||
| span.end() | ||
|
|
||
| return { | ||
| content, | ||
| contentType, | ||
| title, | ||
| summary, | ||
| keyPoints: [], // Could be enhanced to extract key points | ||
| wordCount, | ||
| } | ||
| } catch (error) { | ||
| const errorMsg = | ||
| error instanceof Error ? error.message : 'Unknown error' | ||
| log.error('Copywriter agent tool error:', { | ||
| error: errorMsg, | ||
| topic, | ||
| contentType, | ||
| }) | ||
| span?.setAttribute('tool.output.success', false); | ||
| span?.setAttribute('tool.output.error', errorMsg); | ||
| span?.end(); | ||
| throw new Error(`Failed to generate content: ${errorMsg}`) | ||
| } | ||
| }, | ||
| return { | ||
| content, | ||
| contentType, | ||
| title, | ||
| summary, | ||
| keyPoints: [], // Could be enhanced to extract key points | ||
| wordCount, | ||
| } | ||
| } catch (error) { | ||
| const errorMsg = | ||
| error instanceof Error ? error.message : 'Unknown error' | ||
| log.error('Copywriter agent tool error:', { | ||
| error: errorMsg, | ||
| topic, | ||
| contentType, | ||
| }) | ||
| span?.setAttribute('tool.output.success', false) | ||
| span?.setAttribute('tool.output.error', errorMsg) | ||
| span?.end() | ||
| throw new Error(`Failed to generate content: ${errorMsg}`) | ||
| } | ||
| }, | ||
| }) |
There was a problem hiding this comment.
This tool calls another agent, which can be a long-running operation. However, it does not implement abortSignal handling. The agent.stream or agent.generate calls should ideally accept an abortSignal from the tool context to allow for cancellation. Please consider propagating the abortSignal to the agent call.

No description provided.