diff --git a/packages/ai/src/tools/index.ts b/packages/ai/src/tools/index.ts index cd910e734..148a77f16 100644 --- a/packages/ai/src/tools/index.ts +++ b/packages/ai/src/tools/index.ts @@ -17,3 +17,5 @@ export { lsFiles } from './file-tools/ls-files-tool/ls-files-tool'; export { grepSearch } from './file-tools/grep-search-tool/grep-search-tool'; export { bashExecute } from './file-tools'; export { deleteFiles } from './file-tools/delete-files-tool/delete-files-tool'; +export { checkOffTodoList } from './planning-thinking-tools/check-off-todo-list-tool/check-off-todo-list-tool'; +export { updateClarificationsFile } from './planning-thinking-tools/update-clarifications-file-tool/update-clarifications-file-tool'; diff --git a/packages/ai/src/tools/planning-thinking-tools/check-off-todo-list-tool/check-off-todo-list-tool.test.ts b/packages/ai/src/tools/planning-thinking-tools/check-off-todo-list-tool/check-off-todo-list-tool.test.ts new file mode 100644 index 000000000..8040cb05d --- /dev/null +++ b/packages/ai/src/tools/planning-thinking-tools/check-off-todo-list-tool/check-off-todo-list-tool.test.ts @@ -0,0 +1,134 @@ +import { RuntimeContext } from '@mastra/core/runtime-context'; +import { beforeEach, describe, expect, it } from 'vitest'; +import type { DocsAgentContext } from '../../../context/docs-agent-context'; +import { checkOffTodoList } from './check-off-todo-list-tool'; + +describe('checkOffTodoList', () => { + let runtimeContext: RuntimeContext; + + beforeEach(() => { + runtimeContext = new RuntimeContext(); + }); + + it('should check off a todo item successfully', async () => { + const initialTodoList = `## Todo List +- [ ] Write unit tests +- [ ] Implement feature +- [ ] Review code`; + + runtimeContext.set('todoList', initialTodoList); + + const result = await checkOffTodoList.execute({ + context: { todoItem: 'Write unit tests' }, + runtimeContext, + }); + + expect(result.success).toBe(true); + expect(result.updatedTodoList).toContain('- [x] Write unit tests'); + expect(result.updatedTodoList).toContain('- [ ] Implement feature'); + expect(result.updatedTodoList).toContain('- [ ] Review code'); + expect(result.message).toBe('Successfully checked off: "Write unit tests"'); + + // Verify context was updated + const updatedContext = runtimeContext.get('todoList'); + expect(updatedContext).toBe(result.updatedTodoList); + }); + + it('should return error when todo list is not found in context', async () => { + const result = await checkOffTodoList.execute({ + context: { todoItem: 'Some task' }, + runtimeContext, + }); + + expect(result.success).toBe(false); + expect(result.updatedTodoList).toBe(''); + expect(result.message).toBe('No todo list found in context'); + }); + + it('should return error when todo item is not found', async () => { + const todoList = `## Todo List +- [ ] Write unit tests +- [ ] Implement feature`; + + runtimeContext.set('todoList', todoList); + + const result = await checkOffTodoList.execute({ + context: { todoItem: 'Non-existent task' }, + runtimeContext, + }); + + expect(result.success).toBe(false); + expect(result.updatedTodoList).toBe(todoList); + expect(result.message).toBe( + 'Todo item "Non-existent task" not found in the list or already checked off' + ); + }); + + it('should not check off an already checked item', async () => { + const todoList = `## Todo List +- [x] Write unit tests +- [ ] Implement feature`; + + runtimeContext.set('todoList', todoList); + + const result = await checkOffTodoList.execute({ + context: { todoItem: 'Write unit tests' }, + runtimeContext, + }); + + expect(result.success).toBe(false); + expect(result.updatedTodoList).toBe(todoList); + expect(result.message).toBe( + 'Todo item "Write unit tests" not found in the list or already checked off' + ); + }); + + it('should handle first occurrence when there are duplicates', async () => { + const todoList = `## Todo List +- [ ] Write unit tests for feature A +- [ ] Write unit tests`; + + runtimeContext.set('todoList', todoList); + + const result = await checkOffTodoList.execute({ + context: { todoItem: 'Write unit tests for feature A' }, + runtimeContext, + }); + + expect(result.success).toBe(true); + expect(result.updatedTodoList).toBe(`## Todo List +- [x] Write unit tests for feature A +- [ ] Write unit tests`); + }); + + it('should validate input schema', () => { + const validInput = { todoItem: 'Test task' }; + const parsed = checkOffTodoList.inputSchema.parse(validInput); + expect(parsed).toEqual(validInput); + + expect(() => { + checkOffTodoList.inputSchema.parse({ todoItem: 123 }); + }).toThrow(); + + expect(() => { + checkOffTodoList.inputSchema.parse({}); + }).toThrow(); + }); + + it('should validate output schema', () => { + const validOutput = { + success: true, + updatedTodoList: '- [x] Done', + message: 'Success', + }; + const parsed = checkOffTodoList.outputSchema.parse(validOutput); + expect(parsed).toEqual(validOutput); + + const minimalOutput = { + success: false, + updatedTodoList: '', + }; + const minimalParsed = checkOffTodoList.outputSchema.parse(minimalOutput); + expect(minimalParsed).toEqual(minimalOutput); + }); +}); diff --git a/packages/ai/src/tools/planning-thinking-tools/check-off-todo-list-tool/check-off-todo-list-tool.ts b/packages/ai/src/tools/planning-thinking-tools/check-off-todo-list-tool/check-off-todo-list-tool.ts new file mode 100644 index 000000000..d81e931c0 --- /dev/null +++ b/packages/ai/src/tools/planning-thinking-tools/check-off-todo-list-tool/check-off-todo-list-tool.ts @@ -0,0 +1,84 @@ +import type { RuntimeContext } from '@mastra/core/runtime-context'; +import { createTool } from '@mastra/core/tools'; +import { wrapTraced } from 'braintrust'; +import { z } from 'zod'; +import { type DocsAgentContext, DocsAgentContextKey } from '../../../context/docs-agent-context'; + +const checkOffTodoListInputSchema = z.object({ + todoItem: z.string().describe('The exact text of the todo item to check off in the list'), +}); + +const checkOffTodoListOutputSchema = z.object({ + success: z.boolean(), + updatedTodoList: z.string().describe('The updated todo list with the item checked off'), + message: z.string().optional(), +}); + +const checkOffTodoListExecution = wrapTraced( + async ( + params: z.infer, + runtimeContext: RuntimeContext + ): Promise> => { + const { todoItem } = params; + + try { + // Get the current todo list from context + const currentTodoList = runtimeContext.get('todoList'); + + if (!currentTodoList) { + return { + success: false, + updatedTodoList: '', + message: 'No todo list found in context', + }; + } + + // Check if the item exists in the list (not already checked off) + if (!currentTodoList.includes(`- [ ] ${todoItem}`)) { + return { + success: false, + updatedTodoList: currentTodoList, + message: `Todo item "${todoItem}" not found in the list or already checked off`, + }; + } + + // Replace the unchecked item with a checked version + const updatedTodoList = currentTodoList.replace(`- [ ] ${todoItem}`, `- [x] ${todoItem}`); + + // Update the context with the new todo list + runtimeContext.set('todoList', updatedTodoList); + + return { + success: true, + updatedTodoList, + message: `Successfully checked off: "${todoItem}"`, + }; + } catch (error) { + return { + success: false, + updatedTodoList: '', + message: `Error checking off todo item: ${error instanceof Error ? error.message : 'Unknown error'}`, + }; + } + }, + { name: 'check-off-todo-list' } +); + +export const checkOffTodoList = createTool({ + id: 'check-off-todo-list', + description: + 'Check off a todo item in the todo list by replacing "- [ ]" with "- [x]". The todo list is maintained as a string in the runtime context.', + inputSchema: checkOffTodoListInputSchema, + outputSchema: checkOffTodoListOutputSchema, + execute: async ({ + context, + runtimeContext, + }: { + context: z.infer; + runtimeContext: RuntimeContext; + }) => { + return await checkOffTodoListExecution(context, runtimeContext); + }, +}); + +export default checkOffTodoList; diff --git a/packages/ai/src/tools/planning-thinking-tools/update-clarifications-file-tool/update-clarifications-file-tool.test.ts b/packages/ai/src/tools/planning-thinking-tools/update-clarifications-file-tool/update-clarifications-file-tool.test.ts new file mode 100644 index 000000000..a0f834dcd --- /dev/null +++ b/packages/ai/src/tools/planning-thinking-tools/update-clarifications-file-tool/update-clarifications-file-tool.test.ts @@ -0,0 +1,187 @@ +import { RuntimeContext } from '@mastra/core/runtime-context'; +import { beforeEach, describe, expect, it } from 'vitest'; +import type { DocsAgentContext } from '../../../context/docs-agent-context'; +import { updateClarificationsFile } from './update-clarifications-file-tool'; + +describe('updateClarificationsFile', () => { + let runtimeContext: RuntimeContext; + + beforeEach(() => { + runtimeContext = new RuntimeContext(); + }); + + it('should add a clarification question successfully', async () => { + const result = await updateClarificationsFile.execute({ + context: { + issue: 'Database connection configuration', + context: + 'The user mentioned they need to connect to a database but did not specify which type', + clarificationQuestion: + 'Which type of database are you using? (PostgreSQL, MySQL, MongoDB, etc.)', + }, + runtimeContext, + }); + + expect(result.success).toBe(true); + expect(result.clarification).toEqual({ + issue: 'Database connection configuration', + context: + 'The user mentioned they need to connect to a database but did not specify which type', + clarificationQuestion: + 'Which type of database are you using? (PostgreSQL, MySQL, MongoDB, etc.)', + }); + expect(result.message).toBe('Successfully added clarification question'); + + // Verify context was updated + const savedClarification = runtimeContext.get('clarificationQuestion'); + expect(savedClarification).toEqual(result.clarification); + }); + + it('should overwrite previous clarification when adding new one', async () => { + // Add first clarification + await updateClarificationsFile.execute({ + context: { + issue: 'First issue', + context: 'First context', + clarificationQuestion: 'First question?', + }, + runtimeContext, + }); + + // Add second clarification + const result = await updateClarificationsFile.execute({ + context: { + issue: 'Second issue', + context: 'Second context', + clarificationQuestion: 'Second question?', + }, + runtimeContext, + }); + + expect(result.success).toBe(true); + expect(result.clarification).toEqual({ + issue: 'Second issue', + context: 'Second context', + clarificationQuestion: 'Second question?', + }); + + // Verify only the second clarification is stored + const savedClarification = runtimeContext.get('clarificationQuestion'); + expect(savedClarification).toEqual(result.clarification); + }); + + it('should handle very long clarification content', async () => { + const longText = 'A'.repeat(1000); + + const result = await updateClarificationsFile.execute({ + context: { + issue: longText, + context: longText, + clarificationQuestion: `${longText}?`, + }, + runtimeContext, + }); + + expect(result.success).toBe(true); + expect(result.clarification?.issue).toBe(longText); + expect(result.clarification?.context).toBe(longText); + expect(result.clarification?.clarificationQuestion).toBe(`${longText}?`); + }); + + it('should validate input schema', () => { + const validInput = { + issue: 'Test issue', + context: 'Test context', + clarificationQuestion: 'Test question?', + }; + const parsed = updateClarificationsFile.inputSchema.parse(validInput); + expect(parsed).toEqual(validInput); + + // Missing required fields + expect(() => { + updateClarificationsFile.inputSchema.parse({ + issue: 'Test issue', + context: 'Test context', + }); + }).toThrow(); + + expect(() => { + updateClarificationsFile.inputSchema.parse({ + issue: 'Test issue', + clarificationQuestion: 'Test question?', + }); + }).toThrow(); + + expect(() => { + updateClarificationsFile.inputSchema.parse({ + context: 'Test context', + clarificationQuestion: 'Test question?', + }); + }).toThrow(); + + // Wrong types + expect(() => { + updateClarificationsFile.inputSchema.parse({ + issue: 123, + context: 'Test context', + clarificationQuestion: 'Test question?', + }); + }).toThrow(); + }); + + it('should validate output schema', () => { + const validOutput = { + success: true, + clarification: { + issue: 'Test issue', + context: 'Test context', + clarificationQuestion: 'Test question?', + }, + message: 'Success', + }; + const parsed = updateClarificationsFile.outputSchema.parse(validOutput); + expect(parsed).toEqual(validOutput); + + const minimalOutput = { + success: false, + }; + const minimalParsed = updateClarificationsFile.outputSchema.parse(minimalOutput); + expect(minimalParsed).toEqual(minimalOutput); + }); + + it('should handle empty strings', async () => { + const result = await updateClarificationsFile.execute({ + context: { + issue: '', + context: '', + clarificationQuestion: '', + }, + runtimeContext, + }); + + expect(result.success).toBe(true); + expect(result.clarification).toEqual({ + issue: '', + context: '', + clarificationQuestion: '', + }); + }); + + it('should handle special characters in clarification content', async () => { + const result = await updateClarificationsFile.execute({ + context: { + issue: 'Issue with "quotes" and \'apostrophes\'', + context: 'Context with\nnewlines\tand\ttabs', + clarificationQuestion: 'Question with émojis 🤔 and special chars: <>?/@#$%', + }, + runtimeContext, + }); + + expect(result.success).toBe(true); + expect(result.clarification).toEqual({ + issue: 'Issue with "quotes" and \'apostrophes\'', + context: 'Context with\nnewlines\tand\ttabs', + clarificationQuestion: 'Question with émojis 🤔 and special chars: <>?/@#$%', + }); + }); +}); diff --git a/packages/ai/src/tools/planning-thinking-tools/update-clarifications-file-tool/update-clarifications-file-tool.ts b/packages/ai/src/tools/planning-thinking-tools/update-clarifications-file-tool/update-clarifications-file-tool.ts new file mode 100644 index 000000000..71505fdb3 --- /dev/null +++ b/packages/ai/src/tools/planning-thinking-tools/update-clarifications-file-tool/update-clarifications-file-tool.ts @@ -0,0 +1,88 @@ +import type { RuntimeContext } from '@mastra/core/runtime-context'; +import { createTool } from '@mastra/core/tools'; +import { wrapTraced } from 'braintrust'; +import { z } from 'zod'; +import { + ClarifyingQuestionSchema, + type DocsAgentContext, + DocsAgentContextKey, + type MessageUserClarifyingQuestion, +} from '../../../context/docs-agent-context'; + +const updateClarificationsInputSchema = z.object({ + issue: z.string().describe('The issue or problem that needs clarification'), + context: z + .string() + .describe('The context around the issue to help understand what clarification is needed'), + clarificationQuestion: z + .string() + .describe('The specific question to ask the user for clarification'), +}); + +const updateClarificationsOutputSchema = z.object({ + success: z.boolean(), + clarification: ClarifyingQuestionSchema.optional(), + message: z.string().optional(), +}); + +const updateClarificationsExecution = wrapTraced( + async ( + params: z.infer, + runtimeContext: RuntimeContext + ): Promise> => { + const { issue, context, clarificationQuestion } = params; + + try { + // Create the new clarification question + const newClarification: MessageUserClarifyingQuestion = { + issue, + context, + clarificationQuestion, + }; + + // Validate the clarification against the schema + const validatedClarification = ClarifyingQuestionSchema.parse(newClarification); + + // Update the context with the new clarification + runtimeContext.set('clarificationQuestion', validatedClarification); + + return { + success: true, + clarification: validatedClarification, + message: 'Successfully added clarification question', + }; + } catch (error) { + if (error instanceof z.ZodError) { + return { + success: false, + message: `Validation error: ${error.errors.map((e) => e.message).join(', ')}`, + }; + } + + return { + success: false, + message: `Error adding clarification: ${error instanceof Error ? error.message : 'Unknown error'}`, + }; + } + }, + { name: 'update-clarifications-file' } +); + +export const updateClarificationsFile = createTool({ + id: 'update-clarifications-file', + description: + 'Add a new clarification question to the context. This tool helps agents request clarification from users when they encounter ambiguous or unclear requirements.', + inputSchema: updateClarificationsInputSchema, + outputSchema: updateClarificationsOutputSchema, + execute: async ({ + context, + runtimeContext, + }: { + context: z.infer; + runtimeContext: RuntimeContext; + }) => { + return await updateClarificationsExecution(context, runtimeContext); + }, +}); + +export default updateClarificationsFile;