mirror of https://github.com/buster-so/buster.git
feat: add new tools for managing todo lists and clarifications
- Export checkOffTodoList and updateClarificationsFile from their respective modules in the planning-thinking-tools directory.
This commit is contained in:
parent
7337e11b25
commit
a621f74910
|
@ -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 { grepSearch } from './file-tools/grep-search-tool/grep-search-tool';
|
||||||
export { bashExecute } from './file-tools';
|
export { bashExecute } from './file-tools';
|
||||||
export { deleteFiles } from './file-tools/delete-files-tool/delete-files-tool';
|
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';
|
||||||
|
|
|
@ -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<DocsAgentContext>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
runtimeContext = new RuntimeContext<DocsAgentContext>();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<typeof checkOffTodoListInputSchema>,
|
||||||
|
runtimeContext: RuntimeContext<DocsAgentContext>
|
||||||
|
): Promise<z.infer<typeof checkOffTodoListOutputSchema>> => {
|
||||||
|
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<typeof checkOffTodoListInputSchema>;
|
||||||
|
runtimeContext: RuntimeContext<DocsAgentContext>;
|
||||||
|
}) => {
|
||||||
|
return await checkOffTodoListExecution(context, runtimeContext);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default checkOffTodoList;
|
|
@ -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<DocsAgentContext>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
runtimeContext = new RuntimeContext<DocsAgentContext>();
|
||||||
|
});
|
||||||
|
|
||||||
|
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: <>?/@#$%',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<typeof updateClarificationsInputSchema>,
|
||||||
|
runtimeContext: RuntimeContext<DocsAgentContext>
|
||||||
|
): Promise<z.infer<typeof updateClarificationsOutputSchema>> => {
|
||||||
|
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<typeof updateClarificationsInputSchema>;
|
||||||
|
runtimeContext: RuntimeContext<DocsAgentContext>;
|
||||||
|
}) => {
|
||||||
|
return await updateClarificationsExecution(context, runtimeContext);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updateClarificationsFile;
|
Loading…
Reference in New Issue