mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Merge origin/main and resolve tool-format conflicts
This commit is contained in:
commit
c1ab9adbe4
128 changed files with 3461 additions and 8334 deletions
|
|
@ -1,198 +0,0 @@
|
|||
<workflow>
|
||||
<step number="1">
|
||||
<name>Understand Test Requirements</name>
|
||||
<instructions>
|
||||
Use ask_followup_question to determine what type of integration test is needed:
|
||||
|
||||
<ask_followup_question>
|
||||
<question>What type of integration test would you like me to create or work on?</question>
|
||||
<follow_up>
|
||||
<suggest>New E2E test for a specific feature or workflow</suggest>
|
||||
<suggest>Fix or update an existing integration test</suggest>
|
||||
<suggest>Create test utilities or helpers for common patterns</suggest>
|
||||
<suggest>Debug failing integration tests</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<name>Gather Test Specifications</name>
|
||||
<instructions>
|
||||
Based on the test type, gather detailed requirements:
|
||||
|
||||
For New E2E Tests:
|
||||
- What specific user workflow or feature needs testing?
|
||||
- What are the expected inputs and outputs?
|
||||
- What edge cases or error scenarios should be covered?
|
||||
- Are there specific API interactions to validate?
|
||||
- What events should be monitored during the test?
|
||||
|
||||
For Existing Test Issues:
|
||||
- Which test file is failing or needs updates?
|
||||
- What specific error messages or failures are occurring?
|
||||
- What changes in the codebase might have affected the test?
|
||||
|
||||
For Test Utilities:
|
||||
- What common patterns are being repeated across tests?
|
||||
- What helper functions would improve test maintainability?
|
||||
|
||||
Use multiple ask_followup_question calls if needed to gather complete information.
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="3">
|
||||
<name>Explore Existing Test Patterns</name>
|
||||
<instructions>
|
||||
Use codebase_search FIRST to understand existing test patterns and similar functionality:
|
||||
|
||||
For New Tests:
|
||||
- Search for similar test scenarios in apps/vscode-e2e/src/suite/
|
||||
- Find existing test utilities and helpers
|
||||
- Identify patterns for the type of functionality being tested
|
||||
|
||||
For Test Fixes:
|
||||
- Search for the failing test file and related code
|
||||
- Find similar working tests for comparison
|
||||
- Look for recent changes that might have broken the test
|
||||
|
||||
Example searches:
|
||||
- "file creation test mocha" for file operation tests
|
||||
- "task completion waitUntilCompleted" for task monitoring patterns
|
||||
- "api message validation" for API interaction tests
|
||||
|
||||
After codebase_search, use:
|
||||
- read_file on relevant test files to understand structure
|
||||
- list_code_definition_names on test directories
|
||||
- search_files for specific test patterns or utilities
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="4">
|
||||
<name>Analyze Test Environment and Setup</name>
|
||||
<instructions>
|
||||
Examine the test environment configuration:
|
||||
|
||||
1. Read the test runner configuration:
|
||||
- apps/vscode-e2e/package.json for test scripts
|
||||
- apps/vscode-e2e/src/runTest.ts for test setup
|
||||
- Any test configuration files
|
||||
|
||||
2. Understand the test workspace setup:
|
||||
- How test workspaces are created
|
||||
- What files are available during tests
|
||||
- How the extension API is accessed
|
||||
|
||||
3. Review existing test utilities:
|
||||
- Helper functions for common operations
|
||||
- Event listening patterns
|
||||
- Assertion utilities
|
||||
- Cleanup procedures
|
||||
|
||||
Document findings including:
|
||||
- Test environment structure
|
||||
- Available utilities and helpers
|
||||
- Common patterns and best practices
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="5">
|
||||
<name>Design Test Structure</name>
|
||||
<instructions>
|
||||
Plan the test implementation based on gathered information:
|
||||
|
||||
For New Tests:
|
||||
- Define test suite structure with suite/test blocks
|
||||
- Plan setup and teardown procedures
|
||||
- Identify required test data and fixtures
|
||||
- Design event listeners and validation points
|
||||
- Plan for both success and failure scenarios
|
||||
|
||||
For Test Fixes:
|
||||
- Identify the root cause of the failure
|
||||
- Plan the minimal changes needed to fix the issue
|
||||
- Consider if the test needs to be updated due to code changes
|
||||
- Plan for improved error handling or debugging
|
||||
|
||||
Create a detailed test plan including:
|
||||
- Test file structure and organization
|
||||
- Required setup and cleanup
|
||||
- Specific assertions and validations
|
||||
- Error handling and edge cases
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="6">
|
||||
<name>Implement Test Code</name>
|
||||
<instructions>
|
||||
Implement the test following established patterns:
|
||||
|
||||
CRITICAL: Never write a test file with a single write_to_file call.
|
||||
Always implement tests in parts:
|
||||
|
||||
1. Start with the basic test structure (suite, setup, teardown)
|
||||
2. Add individual test cases one by one
|
||||
3. Implement helper functions separately
|
||||
4. Add event listeners and validation logic incrementally
|
||||
|
||||
Follow these implementation guidelines:
|
||||
- Use suite() and test() blocks following Mocha TDD style
|
||||
- Always use the global api object for extension interactions
|
||||
- Implement proper async/await patterns with waitFor utility
|
||||
- Use waitUntilCompleted and waitUntilAborted helpers for task monitoring
|
||||
- Listen to and validate appropriate events (message, taskCompleted, etc.)
|
||||
- Test both positive flows and error scenarios
|
||||
- Validate message content using proper type assertions
|
||||
- Create reusable test utilities when patterns emerge
|
||||
- Use meaningful test descriptions that explain the scenario
|
||||
- Always clean up tasks with cancelCurrentTask or clearCurrentTask
|
||||
- Ensure tests are independent and can run in any order
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="7">
|
||||
<name>Run and Validate Tests</name>
|
||||
<instructions>
|
||||
Execute the tests to ensure they work correctly:
|
||||
|
||||
ALWAYS use the correct working directory and commands:
|
||||
- Working directory: apps/vscode-e2e
|
||||
- Test command: npm run test:run
|
||||
- For specific tests: TEST_FILE="filename.test" npm run test:run
|
||||
- Example: cd apps/vscode-e2e && TEST_FILE="apply-diff.test" npm run test:run
|
||||
|
||||
Test execution process:
|
||||
1. Run the specific test file first
|
||||
2. Check for any failures or errors
|
||||
3. Analyze test output and logs
|
||||
4. Debug any issues found
|
||||
5. Re-run tests after fixes
|
||||
|
||||
If tests fail:
|
||||
- Add console.log statements to track execution flow
|
||||
- Log important events like task IDs, file paths, and AI responses
|
||||
- Check test output carefully for error messages and stack traces
|
||||
- Verify file creation in correct workspace directories
|
||||
- Ensure proper event handling and timeouts
|
||||
</instructions>
|
||||
</step>
|
||||
|
||||
<step number="8">
|
||||
<name>Document and Complete</name>
|
||||
<instructions>
|
||||
Finalize the test implementation:
|
||||
|
||||
1. Add comprehensive comments explaining complex test logic
|
||||
2. Document any new test utilities or patterns created
|
||||
3. Ensure test descriptions clearly explain what is being tested
|
||||
4. Verify all cleanup procedures are in place
|
||||
5. Confirm tests can run independently and in any order
|
||||
|
||||
Provide the user with:
|
||||
- Summary of tests created or fixed
|
||||
- Instructions for running the tests
|
||||
- Any new patterns or utilities that can be reused
|
||||
- Recommendations for future test improvements
|
||||
</instructions>
|
||||
</step>
|
||||
</workflow>
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
<test_patterns>
|
||||
<mocha_tdd_structure>
|
||||
<description>Standard Mocha TDD structure for integration tests</description>
|
||||
<pattern>
|
||||
<name>Basic Test Suite Structure</name>
|
||||
<example>
|
||||
```typescript
|
||||
import { suite, test, suiteSetup, suiteTeardown } from 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import { waitFor, waitUntilCompleted, waitUntilAborted } from '../utils/testUtils';
|
||||
|
||||
suite('Feature Name Tests', () => {
|
||||
let testWorkspaceDir: string;
|
||||
let testFiles: { [key: string]: string } = {};
|
||||
|
||||
suiteSetup(async () => {
|
||||
// Setup test workspace and files
|
||||
testWorkspaceDir = vscode.workspace.workspaceFolders![0].uri.fsPath;
|
||||
// Create test files in workspace
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
// Cleanup test files and tasks
|
||||
await api.cancelCurrentTask();
|
||||
});
|
||||
|
||||
test('should perform specific functionality', async () => {
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Event Listening Pattern</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should handle task completion events', async () => {
|
||||
const events: any[] = [];
|
||||
|
||||
const messageListener = (message: any) => {
|
||||
events.push({ type: 'message', data: message });
|
||||
};
|
||||
|
||||
const taskCompletedListener = (result: any) => {
|
||||
events.push({ type: 'taskCompleted', data: result });
|
||||
};
|
||||
|
||||
api.onDidReceiveMessage(messageListener);
|
||||
api.onTaskCompleted(taskCompletedListener);
|
||||
|
||||
try {
|
||||
// Perform test actions
|
||||
await api.startTask('test prompt');
|
||||
await waitUntilCompleted();
|
||||
|
||||
// Validate events
|
||||
assert(events.some(e => e.type === 'taskCompleted'));
|
||||
} finally {
|
||||
// Cleanup listeners
|
||||
api.onDidReceiveMessage(() => {});
|
||||
api.onTaskCompleted(() => {});
|
||||
}
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>File Creation Test Pattern</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should create files in workspace', async () => {
|
||||
const fileName = 'test-file.txt';
|
||||
const expectedContent = 'test content';
|
||||
|
||||
await api.startTask(`Create a file named ${fileName} with content: ${expectedContent}`);
|
||||
await waitUntilCompleted();
|
||||
|
||||
// Check multiple possible locations
|
||||
const possiblePaths = [
|
||||
path.join(testWorkspaceDir, fileName),
|
||||
path.join(process.cwd(), fileName),
|
||||
// Add other possible locations
|
||||
];
|
||||
|
||||
let fileFound = false;
|
||||
let actualContent = '';
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
actualContent = fs.readFileSync(filePath, 'utf8');
|
||||
fileFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert(fileFound, `File ${fileName} not found in any expected location`);
|
||||
assert.strictEqual(actualContent.trim(), expectedContent);
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</mocha_tdd_structure>
|
||||
|
||||
<api_interaction_patterns>
|
||||
<pattern>
|
||||
<name>Basic Task Execution</name>
|
||||
<example>
|
||||
```typescript
|
||||
// Start a task and wait for completion
|
||||
await api.startTask('Your prompt here');
|
||||
await waitUntilCompleted();
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Task with Auto-Approval Settings</name>
|
||||
<example>
|
||||
```typescript
|
||||
// Enable auto-approval for specific actions
|
||||
await api.updateSettings({
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true
|
||||
});
|
||||
|
||||
await api.startTask('Create and execute a script');
|
||||
await waitUntilCompleted();
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Message Validation</name>
|
||||
<example>
|
||||
```typescript
|
||||
const messages: any[] = [];
|
||||
api.onDidReceiveMessage((message) => {
|
||||
messages.push(message);
|
||||
});
|
||||
|
||||
await api.startTask('test prompt');
|
||||
await waitUntilCompleted();
|
||||
|
||||
// Validate specific message types
|
||||
const toolMessages = messages.filter(m =>
|
||||
m.type === 'say' && m.say === 'api_req_started'
|
||||
);
|
||||
assert(toolMessages.length > 0, 'Expected tool execution messages');
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</api_interaction_patterns>
|
||||
|
||||
<error_handling_patterns>
|
||||
<pattern>
|
||||
<name>Task Abortion Handling</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should handle task abortion', async () => {
|
||||
await api.startTask('long running task');
|
||||
|
||||
// Abort after short delay
|
||||
setTimeout(() => api.abortTask(), 1000);
|
||||
|
||||
await waitUntilAborted();
|
||||
|
||||
// Verify task was properly aborted
|
||||
const status = await api.getTaskStatus();
|
||||
assert.strictEqual(status, 'aborted');
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Error Message Validation</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should handle invalid input gracefully', async () => {
|
||||
const errorMessages: any[] = [];
|
||||
|
||||
api.onDidReceiveMessage((message) => {
|
||||
if (message.type === 'error' || message.text?.includes('error')) {
|
||||
errorMessages.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
await api.startTask('invalid prompt that should fail');
|
||||
await waitFor(() => errorMessages.length > 0, 5000);
|
||||
|
||||
assert(errorMessages.length > 0, 'Expected error messages');
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</error_handling_patterns>
|
||||
|
||||
<utility_patterns>
|
||||
<pattern>
|
||||
<name>File Location Helper</name>
|
||||
<example>
|
||||
```typescript
|
||||
function findFileInWorkspace(fileName: string, workspaceDir: string): string | null {
|
||||
const possiblePaths = [
|
||||
path.join(workspaceDir, fileName),
|
||||
path.join(process.cwd(), fileName),
|
||||
path.join(os.tmpdir(), fileName),
|
||||
// Add other common locations
|
||||
];
|
||||
|
||||
for (const filePath of possiblePaths) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>Event Collection Helper</name>
|
||||
<example>
|
||||
```typescript
|
||||
class EventCollector {
|
||||
private events: any[] = [];
|
||||
|
||||
constructor(private api: any) {
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
this.api.onDidReceiveMessage((message: any) => {
|
||||
this.events.push({ type: 'message', timestamp: Date.now(), data: message });
|
||||
});
|
||||
|
||||
this.api.onTaskCompleted((result: any) => {
|
||||
this.events.push({ type: 'taskCompleted', timestamp: Date.now(), data: result });
|
||||
});
|
||||
}
|
||||
|
||||
getEvents(type?: string) {
|
||||
return type ? this.events.filter(e => e.type === type) : this.events;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.events = [];
|
||||
}
|
||||
}
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</utility_patterns>
|
||||
|
||||
<debugging_patterns>
|
||||
<pattern>
|
||||
<name>Comprehensive Logging</name>
|
||||
<example>
|
||||
```typescript
|
||||
test('should log execution flow for debugging', async () => {
|
||||
console.log('Starting test execution');
|
||||
|
||||
const events: any[] = [];
|
||||
api.onDidReceiveMessage((message) => {
|
||||
console.log('Received message:', JSON.stringify(message, null, 2));
|
||||
events.push(message);
|
||||
});
|
||||
|
||||
console.log('Starting task with prompt');
|
||||
await api.startTask('test prompt');
|
||||
|
||||
console.log('Waiting for task completion');
|
||||
await waitUntilCompleted();
|
||||
|
||||
console.log('Task completed, events received:', events.length);
|
||||
console.log('Final workspace state:', fs.readdirSync(testWorkspaceDir));
|
||||
});
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern>
|
||||
<name>State Validation</name>
|
||||
<example>
|
||||
```typescript
|
||||
function validateTestState(description: string) {
|
||||
console.log(`=== ${description} ===`);
|
||||
console.log('Workspace files:', fs.readdirSync(testWorkspaceDir));
|
||||
console.log('Current working directory:', process.cwd());
|
||||
console.log('Task status:', api.getTaskStatus?.() || 'unknown');
|
||||
console.log('========================');
|
||||
}
|
||||
```
|
||||
</example>
|
||||
</pattern>
|
||||
</debugging_patterns>
|
||||
</test_patterns>
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
<best_practices>
|
||||
<test_structure>
|
||||
- Always use suite() and test() blocks following Mocha TDD style
|
||||
- Use descriptive test names that explain the scenario being tested
|
||||
- Implement proper setup and teardown in suiteSetup() and suiteTeardown()
|
||||
- Create test files in the VSCode workspace directory during suiteSetup()
|
||||
- Store file paths in a test-scoped object for easy reference across tests
|
||||
- Ensure tests are independent and can run in any order
|
||||
- Clean up all test files and tasks in suiteTeardown() to avoid test pollution
|
||||
</test_structure>
|
||||
|
||||
<api_interactions>
|
||||
- Always use the global api object for extension interactions
|
||||
- Implement proper async/await patterns with the waitFor utility
|
||||
- Use waitUntilCompleted and waitUntilAborted helpers for task monitoring
|
||||
- Set appropriate auto-approval settings (alwaysAllowWrite, alwaysAllowExecute) for the functionality being tested
|
||||
- Listen to and validate appropriate events (message, taskCompleted, taskAborted, etc.)
|
||||
- Always clean up tasks with cancelCurrentTask or clearCurrentTask after tests
|
||||
- Use meaningful timeouts that account for actual task execution time
|
||||
</api_interactions>
|
||||
|
||||
<file_system_handling>
|
||||
- Be aware that files may be created in the workspace directory (/tmp/roo-test-workspace-*) rather than expected locations
|
||||
- Always check multiple possible file locations when verifying file creation
|
||||
- Use flexible file location checking that searches workspace directories
|
||||
- Verify files exist after creation to catch setup issues early
|
||||
- Account for the fact that the workspace directory is created by runTest.ts
|
||||
- The AI may use internal tools instead of the documented tools - verify outcomes rather than methods
|
||||
</file_system_handling>
|
||||
|
||||
<event_handling>
|
||||
- Add multiple event listeners (taskStarted, taskCompleted, taskAborted) for better debugging
|
||||
- Don't rely on parsing AI messages to detect tool usage - the AI's message format may vary
|
||||
- Use terminal shell execution events (onDidStartTerminalShellExecution, onDidEndTerminalShellExecution) for command tracking
|
||||
- Tool executions are reported via api_req_started messages with type="say" and say="api_req_started"
|
||||
- Focus on testing outcomes (files created, commands executed) rather than message parsing
|
||||
- There is no "tool_result" message type - tool results appear in "completion_result" or "text" messages
|
||||
</event_handling>
|
||||
|
||||
<error_scenarios>
|
||||
- Test both positive flows and error scenarios
|
||||
- Validate message content using proper type assertions
|
||||
- Implement proper error handling and edge cases
|
||||
- Use try-catch blocks around critical test operations
|
||||
- Log important events like task IDs, file paths, and AI responses for debugging
|
||||
- Check test output carefully for error messages and stack traces
|
||||
</error_scenarios>
|
||||
|
||||
<test_reliability>
|
||||
- Remove unnecessary waits for specific tool executions - wait for task completion instead
|
||||
- Simplify message handlers to only capture essential error information
|
||||
- Use the simplest possible test structure that verifies the outcome
|
||||
- Avoid complex message parsing logic that depends on AI behavior
|
||||
- Terminal events are more reliable than message parsing for command execution verification
|
||||
- Keep prompts simple and direct - complex instructions may confuse the AI
|
||||
</test_reliability>
|
||||
|
||||
<debugging_and_troubleshooting>
|
||||
- Add console.log statements to track test execution flow
|
||||
- Log important events like task IDs, file paths, and AI responses
|
||||
- Use codebase_search first to find similar test patterns before writing new tests
|
||||
- Create helper functions for common file location checks
|
||||
- Use descriptive variable names for file paths and content
|
||||
- Always log the expected vs actual locations when tests fail
|
||||
- Add comprehensive comments explaining complex test logic
|
||||
</debugging_and_troubleshooting>
|
||||
|
||||
<test_utilities>
|
||||
- Create reusable test utilities when patterns emerge
|
||||
- Implement helper functions for common operations like file finding
|
||||
- Use event collection utilities for consistent event handling
|
||||
- Create assertion helpers for common validation patterns
|
||||
- Document any new test utilities or patterns created
|
||||
- Share common utilities across test files to reduce duplication
|
||||
</test_utilities>
|
||||
|
||||
<ai_interaction_considerations>
|
||||
- Keep prompts simple and direct - complex instructions may lead to unexpected behavior
|
||||
- Allow for variations in how the AI accomplishes tasks
|
||||
- The AI may not always use the exact tool you specify in the prompt
|
||||
- Be prepared to adapt tests based on actual AI behavior rather than expected behavior
|
||||
- The AI may interpret instructions creatively - test results rather than implementation details
|
||||
- The AI will not see the files in the workspace directory, you must tell it to assume they exist and proceed
|
||||
</ai_interaction_considerations>
|
||||
|
||||
<test_execution>
|
||||
- ALWAYS use the correct working directory: apps/vscode-e2e
|
||||
- The test command is: npm run test:run
|
||||
- To run specific tests use environment variable: TEST_FILE="filename.test" npm run test:run
|
||||
- Example: cd apps/vscode-e2e && TEST_FILE="apply-diff.test" npm run test:run
|
||||
- Never use npm test directly as it doesn't exist
|
||||
- Always check available scripts with npm run if unsure
|
||||
- Run tests incrementally during development to catch issues early
|
||||
</test_execution>
|
||||
|
||||
<code_organization>
|
||||
- Never write a test file with a single write_to_file tool call
|
||||
- Always implement tests in parts: structure first, then individual test cases
|
||||
- Group related tests in the same suite
|
||||
- Use consistent naming conventions for test files and functions
|
||||
- Separate test utilities into their own files when they become substantial
|
||||
- Follow the existing project structure and conventions
|
||||
</code_organization>
|
||||
</best_practices>
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
<common_mistakes_to_avoid>
|
||||
<test_structure_mistakes>
|
||||
- Writing a test file with a single write_to_file tool call instead of implementing in parts
|
||||
- Not using proper Mocha TDD structure with suite() and test() blocks
|
||||
- Forgetting to implement suiteSetup() and suiteTeardown() for proper cleanup
|
||||
- Creating tests that depend on each other or specific execution order
|
||||
- Not cleaning up tasks and files after test completion
|
||||
- Using describe/it blocks instead of the required suite/test blocks
|
||||
</test_structure_mistakes>
|
||||
|
||||
<api_interaction_mistakes>
|
||||
- Not using the global api object for extension interactions
|
||||
- Forgetting to set auto-approval settings (alwaysAllowWrite, alwaysAllowExecute) when testing functionality that requires user approval
|
||||
- Not implementing proper async/await patterns with waitFor utilities
|
||||
- Using incorrect timeout values that are too short for actual task execution
|
||||
- Not properly cleaning up tasks with cancelCurrentTask or clearCurrentTask
|
||||
- Assuming the AI will use specific tools instead of testing outcomes
|
||||
</api_interaction_mistakes>
|
||||
|
||||
<file_system_mistakes>
|
||||
- Assuming files will be created in the expected location without checking multiple paths
|
||||
- Not accounting for the workspace directory being created by runTest.ts
|
||||
- Creating test files in temporary directories instead of the VSCode workspace directory
|
||||
- Not verifying files exist after creation during setup
|
||||
- Forgetting that the AI may not see files in the workspace directory
|
||||
- Not using flexible file location checking that searches workspace directories
|
||||
</file_system_mistakes>
|
||||
|
||||
<event_handling_mistakes>
|
||||
- Relying on parsing AI messages to detect tool usage instead of using proper event listeners
|
||||
- Expecting tool results in "tool_result" message type (which doesn't exist)
|
||||
- Not listening to terminal shell execution events for command tracking
|
||||
- Depending on specific message formats that may vary
|
||||
- Not implementing proper event cleanup after tests
|
||||
- Parsing complex AI conversation messages instead of focusing on outcomes
|
||||
</event_handling_mistakes>
|
||||
|
||||
<test_execution_mistakes>
|
||||
- Using npm test instead of npm run test:run
|
||||
- Not using the correct working directory (apps/vscode-e2e)
|
||||
- Running tests from the wrong directory
|
||||
- Not checking available scripts with npm run when unsure
|
||||
- Forgetting to use TEST_FILE environment variable for specific tests
|
||||
- Not running tests incrementally during development
|
||||
</test_execution_mistakes>
|
||||
|
||||
<debugging_mistakes>
|
||||
- Not adding sufficient logging to track test execution flow
|
||||
- Not logging important events like task IDs, file paths, and AI responses
|
||||
- Not using codebase_search to find similar test patterns before writing new tests
|
||||
- Not checking test output carefully for error messages and stack traces
|
||||
- Not validating test state at critical points
|
||||
- Assuming test failures are due to code issues without checking test logic
|
||||
</debugging_mistakes>
|
||||
|
||||
<ai_interaction_mistakes>
|
||||
- Using complex instructions that may confuse the AI
|
||||
- Expecting the AI to use exact tools specified in prompts
|
||||
- Not allowing for variations in how the AI accomplishes tasks
|
||||
- Testing implementation details instead of outcomes
|
||||
- Not adapting tests based on actual AI behavior
|
||||
- Forgetting to tell the AI to assume files exist in the workspace directory
|
||||
</ai_interaction_mistakes>
|
||||
|
||||
<reliability_mistakes>
|
||||
- Adding unnecessary waits for specific tool executions
|
||||
- Using complex message parsing logic that depends on AI behavior
|
||||
- Not using the simplest possible test structure
|
||||
- Depending on specific AI message formats
|
||||
- Not using terminal events for reliable command execution verification
|
||||
- Making tests too brittle by depending on exact AI responses
|
||||
</reliability_mistakes>
|
||||
|
||||
<workspace_mistakes>
|
||||
- Not understanding that files may be created in /tmp/roo-test-workspace-* directories
|
||||
- Assuming the AI can see files in the workspace directory
|
||||
- Not checking multiple possible file locations when verifying creation
|
||||
- Creating files outside the VSCode workspace during tests
|
||||
- Not properly setting up the test workspace in suiteSetup()
|
||||
- Forgetting to clean up workspace files in suiteTeardown()
|
||||
</workspace_mistakes>
|
||||
|
||||
<message_handling_mistakes>
|
||||
- Expecting specific message types for tool execution results
|
||||
- Not understanding that ClineMessage types have specific values
|
||||
- Trying to parse tool execution from AI conversation messages
|
||||
- Not checking packages/types/src/message.ts for valid message types
|
||||
- Depending on message parsing instead of outcome verification
|
||||
- Not using api_req_started messages to verify tool execution
|
||||
</message_handling_mistakes>
|
||||
|
||||
<timeout_and_timing_mistakes>
|
||||
- Using timeouts that are too short for actual task execution
|
||||
- Not accounting for AI processing time in test timeouts
|
||||
- Waiting for specific tool executions instead of task completion
|
||||
- Not implementing proper retry logic for flaky operations
|
||||
- Using fixed delays instead of condition-based waiting
|
||||
- Not considering that some operations may take longer in CI environments
|
||||
</timeout_and_timing_mistakes>
|
||||
|
||||
<test_data_mistakes>
|
||||
- Not creating test files in the correct workspace directory
|
||||
- Using hardcoded paths that don't work across different environments
|
||||
- Not storing file paths in test-scoped objects for easy reference
|
||||
- Creating test data that conflicts with other tests
|
||||
- Not cleaning up test data properly after tests complete
|
||||
- Using test data that's too complex for the AI to handle reliably
|
||||
</test_data_mistakes>
|
||||
</common_mistakes_to_avoid>
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
<test_environment_and_tools>
|
||||
<test_framework>
|
||||
<description>VSCode E2E testing framework using Mocha and VSCode Test</description>
|
||||
<key_components>
|
||||
- Mocha TDD framework for test structure
|
||||
- VSCode Test framework for extension testing
|
||||
- Custom test utilities and helpers
|
||||
- Event-driven testing patterns
|
||||
- Workspace-based test execution
|
||||
</key_components>
|
||||
</test_framework>
|
||||
|
||||
<directory_structure>
|
||||
<test_files_location>apps/vscode-e2e/src/suite/</test_files_location>
|
||||
<test_utilities>apps/vscode-e2e/src/utils/</test_utilities>
|
||||
<test_runner>apps/vscode-e2e/src/runTest.ts</test_runner>
|
||||
<package_config>apps/vscode-e2e/package.json</package_config>
|
||||
<type_definitions>packages/types/</type_definitions>
|
||||
</directory_structure>
|
||||
|
||||
<test_execution_commands>
|
||||
<working_directory>apps/vscode-e2e</working_directory>
|
||||
<commands>
|
||||
<run_all_tests>npm run test:run</run_all_tests>
|
||||
<run_specific_test>TEST_FILE="filename.test" npm run test:run</run_specific_test>
|
||||
<example>cd apps/vscode-e2e && TEST_FILE="apply-diff.test" npm run test:run</example>
|
||||
<check_scripts>npm run</check_scripts>
|
||||
</commands>
|
||||
<important_notes>
|
||||
- Never use npm test directly as it doesn't exist
|
||||
- Always use the correct working directory
|
||||
- Use TEST_FILE environment variable for specific tests
|
||||
- Check available scripts with npm run if unsure
|
||||
</important_notes>
|
||||
</test_execution_commands>
|
||||
|
||||
<api_object>
|
||||
<description>Global api object for extension interactions</description>
|
||||
<key_methods>
|
||||
<task_management>
|
||||
- api.startTask(prompt: string): Start a new task
|
||||
- api.cancelCurrentTask(): Cancel the current task
|
||||
- api.clearCurrentTask(): Clear the current task
|
||||
- api.abortTask(): Abort the current task
|
||||
- api.getTaskStatus(): Get current task status
|
||||
</task_management>
|
||||
<event_listeners>
|
||||
- api.onDidReceiveMessage(callback): Listen to messages
|
||||
- api.onTaskCompleted(callback): Listen to task completion
|
||||
- api.onTaskAborted(callback): Listen to task abortion
|
||||
- api.onTaskStarted(callback): Listen to task start
|
||||
- api.onDidStartTerminalShellExecution(callback): Terminal start events
|
||||
- api.onDidEndTerminalShellExecution(callback): Terminal end events
|
||||
</event_listeners>
|
||||
<settings>
|
||||
- api.updateSettings(settings): Update extension settings
|
||||
- api.getSettings(): Get current settings
|
||||
</settings>
|
||||
</key_methods>
|
||||
</api_object>
|
||||
|
||||
<test_utilities>
|
||||
<wait_functions>
|
||||
<waitFor>
|
||||
<description>Wait for a condition to be true</description>
|
||||
<usage>await waitFor(() => condition, timeout)</usage>
|
||||
<example>await waitFor(() => fs.existsSync(filePath), 5000)</example>
|
||||
</waitFor>
|
||||
<waitUntilCompleted>
|
||||
<description>Wait until current task is completed</description>
|
||||
<usage>await waitUntilCompleted()</usage>
|
||||
<timeout>Default timeout for task completion</timeout>
|
||||
</waitUntilCompleted>
|
||||
<waitUntilAborted>
|
||||
<description>Wait until current task is aborted</description>
|
||||
<usage>await waitUntilAborted()</usage>
|
||||
<timeout>Default timeout for task abortion</timeout>
|
||||
</waitUntilAborted>
|
||||
</wait_functions>
|
||||
|
||||
<helper_patterns>
|
||||
<file_location_helper>
|
||||
<description>Helper to find files in multiple possible locations</description>
|
||||
<usage>Use when files might be created in different workspace directories</usage>
|
||||
</file_location_helper>
|
||||
<event_collector>
|
||||
<description>Utility to collect and analyze events during test execution</description>
|
||||
<usage>Use for comprehensive event tracking and validation</usage>
|
||||
</event_collector>
|
||||
<assertion_helpers>
|
||||
<description>Custom assertion functions for common test patterns</description>
|
||||
<usage>Use for consistent validation across tests</usage>
|
||||
</assertion_helpers>
|
||||
</helper_patterns>
|
||||
</test_utilities>
|
||||
|
||||
<workspace_management>
|
||||
<workspace_creation>
|
||||
<description>Test workspaces are created by runTest.ts</description>
|
||||
<location>/tmp/roo-test-workspace-*</location>
|
||||
<access>vscode.workspace.workspaceFolders![0].uri.fsPath</access>
|
||||
</workspace_creation>
|
||||
|
||||
<file_creation_strategy>
|
||||
<setup_phase>Create all test files in suiteSetup() before any tests run</setup_phase>
|
||||
<location>Always create files in the VSCode workspace directory</location>
|
||||
<verification>Verify files exist after creation to catch setup issues early</verification>
|
||||
<cleanup>Clean up all test files in suiteTeardown() to avoid test pollution</cleanup>
|
||||
<storage>Store file paths in a test-scoped object for easy reference</storage>
|
||||
</file_creation_strategy>
|
||||
|
||||
<ai_visibility>
|
||||
<important_note>The AI will not see the files in the workspace directory</important_note>
|
||||
<solution>Tell the AI to assume files exist and proceed as if they do</solution>
|
||||
<verification>Always verify outcomes rather than relying on AI file visibility</verification>
|
||||
</ai_visibility>
|
||||
</workspace_management>
|
||||
|
||||
<message_types>
|
||||
<description>Understanding message types for proper event handling</description>
|
||||
<reference>Check packages/types/src/message.ts for valid message types</reference>
|
||||
|
||||
<key_message_types>
|
||||
<api_req_started>
|
||||
<type>say</type>
|
||||
<say>api_req_started</say>
|
||||
<description>Indicates tool execution started</description>
|
||||
<text_content>JSON with tool name and execution details</text_content>
|
||||
<usage>Most reliable way to verify tool execution</usage>
|
||||
</api_req_started>
|
||||
|
||||
<completion_result>
|
||||
<description>Contains tool execution results</description>
|
||||
<usage>Tool results appear here, not in "tool_result" type</usage>
|
||||
</completion_result>
|
||||
|
||||
<text_messages>
|
||||
<description>General AI conversation messages</description>
|
||||
<caution>Format may vary, don't rely on parsing these for tool detection</caution>
|
||||
</text_messages>
|
||||
</key_message_types>
|
||||
</message_types>
|
||||
|
||||
<auto_approval_settings>
|
||||
<description>Settings to enable automatic approval of AI actions</description>
|
||||
<critical_settings>
|
||||
<alwaysAllowWrite>Enable for file creation/modification tests</alwaysAllowWrite>
|
||||
<alwaysAllowExecute>Enable for command execution tests</alwaysAllowExecute>
|
||||
<alwaysAllowBrowser>Enable for browser-related tests</alwaysAllowBrowser>
|
||||
</critical_settings>
|
||||
<usage>
|
||||
```typescript
|
||||
await api.updateSettings({
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true
|
||||
});
|
||||
```
|
||||
</usage>
|
||||
<importance>Without proper auto-approval settings, the AI won't be able to perform actions without user approval</importance>
|
||||
</auto_approval_settings>
|
||||
|
||||
<debugging_tools>
|
||||
<console_logging>
|
||||
<description>Use console.log for tracking test execution flow</description>
|
||||
<best_practices>
|
||||
- Log test phase transitions
|
||||
- Log important events and data
|
||||
- Log file paths and workspace state
|
||||
- Log expected vs actual outcomes
|
||||
</best_practices>
|
||||
</console_logging>
|
||||
|
||||
<state_validation>
|
||||
<description>Helper functions to validate test state at critical points</description>
|
||||
<includes>
|
||||
- Workspace file listing
|
||||
- Current working directory
|
||||
- Task status
|
||||
- Event counts
|
||||
</includes>
|
||||
</state_validation>
|
||||
|
||||
<error_analysis>
|
||||
<description>Tools for analyzing test failures</description>
|
||||
<techniques>
|
||||
- Stack trace analysis
|
||||
- Event timeline reconstruction
|
||||
- File system state comparison
|
||||
- Message flow analysis
|
||||
</techniques>
|
||||
</error_analysis>
|
||||
</debugging_tools>
|
||||
|
||||
<performance_considerations>
|
||||
<timeouts>
|
||||
<description>Appropriate timeout values for different operations</description>
|
||||
<task_completion>Use generous timeouts for task completion (30+ seconds)</task_completion>
|
||||
<file_operations>Shorter timeouts for file system operations (5-10 seconds)</file_operations>
|
||||
<event_waiting>Medium timeouts for event waiting (10-15 seconds)</event_waiting>
|
||||
</timeouts>
|
||||
|
||||
<resource_management>
|
||||
<description>Proper cleanup to avoid resource leaks</description>
|
||||
<event_listeners>Always clean up event listeners after tests</event_listeners>
|
||||
<tasks>Cancel or clear tasks in teardown</tasks>
|
||||
<files>Remove test files to avoid disk space issues</files>
|
||||
</resource_management>
|
||||
</performance_considerations>
|
||||
</test_environment_and_tools>
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
</step>
|
||||
<step>
|
||||
<title>Draft Comment</title>
|
||||
<description>Draft a comment for the GitHub issue that explains your findings and suggested solution in a conversational, human-like tone.</description>
|
||||
<description>Draft a comment for the GitHub issue that explains your findings and suggested solution in a conversational, human-like tone. Start the comment with "Hey @roomote-agent,".</description>
|
||||
</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
</category>
|
||||
<category name="communication">
|
||||
<item>Does the draft comment sound conversational and human?</item>
|
||||
<item>Does the draft comment start with "Hey @roomote-agent,"?</item>
|
||||
<item>Have I avoided technical jargon where possible?</item>
|
||||
<item>Is the tone helpful and not condescending?</item>
|
||||
</category>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,16 @@
|
|||
<practice>Use `gh issue view [URL] --json title,body,labels,comments` to fetch initial details.</practice>
|
||||
<practice>Use `gh issue comment [URL] --body "..."` to add comments, but only after explicit user approval.</practice>
|
||||
<practice>Always wrap the comment body in quotes to handle special characters.</practice>
|
||||
<practice>When posting a comment, the body must start with "Hey @roomote-agent," exactly.</practice>
|
||||
</best_practices>
|
||||
<example><![CDATA[
|
||||
<execute_command>
|
||||
<command>gh issue view https://github.com/RooCodeInc/Roo-Code/issues/123 --json title,body</command>
|
||||
</execute_command>
|
||||
]]></example>
|
||||
<example><![CDATA[
|
||||
<execute_command>
|
||||
<command>gh issue comment https://github.com/RooCodeInc/Roo-Code/issues/123 --body "Hey @roomote-agent, I've investigated and proposed a theoretical fix above."</command>
|
||||
</execute_command>
|
||||
]]></example>
|
||||
</tool>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
<step number="6">
|
||||
<description>Formulate a solution and draft a comment.</description>
|
||||
<draft>
|
||||
"Hey, I took a look at this issue and it seems like the problem is in the `handleLogin` function in `src/auth/login.ts`. There's an empty `try...catch` block that is likely catching the login error and preventing any message from being shown to the user.
|
||||
"Hey @roomote-agent, I took a look at this issue and it seems like the problem is in the `handleLogin` function in `src/auth/login.ts`. There's an empty `try...catch` block that is likely catching the login error and preventing any message from being shown to the user.
|
||||
|
||||
A good first step to fixing this would be to add some logging to the `catch` block to see what error is actually happening. Then, we should probably implement proper error feedback to the user on the login page. This should solve the issue of the silent failure."
|
||||
</draft>
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@
|
|||
<tone_and_style>
|
||||
<principle>Be conversational and helpful, not robotic.</principle>
|
||||
<principle>Write comments as if you are a human developer collaborating on the project.</principle>
|
||||
<principle>Always start any GitHub issue comment body with "Hey @roomote-agent,".</principle>
|
||||
<avoid>
|
||||
<phrase>Analysis complete.</phrase>
|
||||
<phrase>The investigation has yielded the following result.</phrase>
|
||||
<phrase>Starting comments with "Hey," instead of "Hey @roomote-agent,".</phrase>
|
||||
</avoid>
|
||||
<prefer>
|
||||
<phrase>Hey, I took a look at this and found something interesting...</phrase>
|
||||
<phrase>I've been digging into this issue, and I think I've found a possible cause.</phrase>
|
||||
<phrase>Hey @roomote-agent, I took a look at this and found something interesting...</phrase>
|
||||
<phrase>Hey @roomote-agent, I've been digging into this issue, and I think I've found a possible cause.</phrase>
|
||||
</prefer>
|
||||
</tone_and_style>
|
||||
|
||||
<comment_structure>
|
||||
<element>Start with a friendly opening.</element>
|
||||
<element>Start every GitHub issue comment with "Hey @roomote-agent,".</element>
|
||||
<element>State your main finding or hypothesis clearly but not definitively.</element>
|
||||
<element>Provide context, like file paths and function names.</element>
|
||||
<element>Propose a next step or a theoretical solution.</element>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,190 +0,0 @@
|
|||
<github_issue_templates>
|
||||
<overview>
|
||||
This mode prioritizes using repository-specific issue templates over hardcoded ones.
|
||||
If no templates exist in the repository, simple generic templates are created on the fly.
|
||||
</overview>
|
||||
|
||||
<template_detection>
|
||||
<locations>
|
||||
<location priority="1">.github/ISSUE_TEMPLATE/*.yml</location>
|
||||
<location priority="2">.github/ISSUE_TEMPLATE/*.yaml</location>
|
||||
<location priority="3">.github/ISSUE_TEMPLATE/*.md</location>
|
||||
<location priority="4">.github/issue_template.md</location>
|
||||
<location priority="5">.github/ISSUE_TEMPLATE.md</location>
|
||||
</locations>
|
||||
|
||||
<yaml_template_structure>
|
||||
<field name="name">Display name of the template</field>
|
||||
<field name="description">Brief description of when to use this template</field>
|
||||
<field name="title">Default issue title (optional)</field>
|
||||
<field name="labels">Array of labels to apply</field>
|
||||
<field name="assignees">Array of default assignees</field>
|
||||
<field name="body">Array of form elements or markdown content</field>
|
||||
</yaml_template_structure>
|
||||
|
||||
<yaml_form_elements>
|
||||
<element type="markdown">
|
||||
<description>Static markdown content</description>
|
||||
<attributes>
|
||||
<attr name="value">The markdown content to display</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
|
||||
<element type="input">
|
||||
<description>Single-line text input</description>
|
||||
<attributes>
|
||||
<attr name="id">Unique identifier</attr>
|
||||
<attr name="label">Display label</attr>
|
||||
<attr name="description">Help text</attr>
|
||||
<attr name="placeholder">Placeholder text</attr>
|
||||
<attr name="value">Default value</attr>
|
||||
<attr name="required">Boolean</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
|
||||
<element type="textarea">
|
||||
<description>Multi-line text input</description>
|
||||
<attributes>
|
||||
<attr name="id">Unique identifier</attr>
|
||||
<attr name="label">Display label</attr>
|
||||
<attr name="description">Help text</attr>
|
||||
<attr name="placeholder">Placeholder text</attr>
|
||||
<attr name="value">Default value</attr>
|
||||
<attr name="required">Boolean</attr>
|
||||
<attr name="render">Language for syntax highlighting</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
|
||||
<element type="dropdown">
|
||||
<description>Dropdown selection</description>
|
||||
<attributes>
|
||||
<attr name="id">Unique identifier</attr>
|
||||
<attr name="label">Display label</attr>
|
||||
<attr name="description">Help text</attr>
|
||||
<attr name="options">Array of options</attr>
|
||||
<attr name="required">Boolean</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
|
||||
<element type="checkboxes">
|
||||
<description>Multiple checkbox options</description>
|
||||
<attributes>
|
||||
<attr name="id">Unique identifier</attr>
|
||||
<attr name="label">Display label</attr>
|
||||
<attr name="description">Help text</attr>
|
||||
<attr name="options">Array of checkbox items</attr>
|
||||
</attributes>
|
||||
</element>
|
||||
</yaml_form_elements>
|
||||
|
||||
<markdown_template_structure>
|
||||
<front_matter>
|
||||
Optional YAML front matter with:
|
||||
- name: Template name
|
||||
- about: Template description
|
||||
- title: Default title
|
||||
- labels: Comma-separated or array
|
||||
- assignees: Comma-separated or array
|
||||
</front_matter>
|
||||
<body>
|
||||
Markdown content with sections and placeholders
|
||||
Common patterns:
|
||||
- Headers with ##
|
||||
- Placeholder text in brackets or as comments
|
||||
- Checklists with - [ ]
|
||||
- Code blocks with ```
|
||||
</body>
|
||||
</markdown_template_structure>
|
||||
</template_detection>
|
||||
|
||||
<generic_templates>
|
||||
<description>
|
||||
When no repository templates exist, create simple templates based on issue type.
|
||||
These should be minimal and focused on gathering essential information.
|
||||
</description>
|
||||
|
||||
<bug_template>
|
||||
<structure>
|
||||
- Description: Clear explanation of the bug
|
||||
- Steps to Reproduce: Numbered list
|
||||
- Expected Behavior: What should happen
|
||||
- Actual Behavior: What actually happens
|
||||
- Additional Context: Version, environment, logs
|
||||
- Code Investigation: Findings from exploration (if any)
|
||||
</structure>
|
||||
<labels>["bug"]</labels>
|
||||
</bug_template>
|
||||
|
||||
<feature_template>
|
||||
<structure>
|
||||
- Problem Description: What problem this solves
|
||||
- Current Behavior: How it works now
|
||||
- Proposed Solution: What should change
|
||||
- Impact: Who benefits and how
|
||||
- Technical Context: Code findings (if any)
|
||||
</structure>
|
||||
<labels>["enhancement", "proposal"]</labels>
|
||||
</feature_template>
|
||||
</generic_templates>
|
||||
|
||||
<template_parsing_guidelines>
|
||||
<guideline>
|
||||
When parsing YAML templates:
|
||||
1. Use a YAML parser to extract the structure
|
||||
2. Convert form elements to markdown sections
|
||||
3. Preserve required field indicators
|
||||
4. Include descriptions as help text
|
||||
5. Maintain the intended flow of the template
|
||||
</guideline>
|
||||
|
||||
<guideline>
|
||||
When parsing Markdown templates:
|
||||
1. Extract front matter if present
|
||||
2. Identify section headers
|
||||
3. Look for placeholder patterns
|
||||
4. Preserve formatting and structure
|
||||
5. Replace generic placeholders with user's information
|
||||
</guideline>
|
||||
|
||||
<guideline>
|
||||
For template selection:
|
||||
1. If only one template exists, use it automatically
|
||||
2. If multiple exist, let user choose based on name/description
|
||||
3. Match template to issue type when possible (bug vs feature)
|
||||
4. Respect template metadata (labels, assignees, etc.)
|
||||
</guideline>
|
||||
</template_parsing_guidelines>
|
||||
|
||||
<filling_templates>
|
||||
<principle>
|
||||
Fill templates intelligently using gathered information:
|
||||
- Map user's description to appropriate sections
|
||||
- Include code investigation findings where relevant
|
||||
- Preserve template structure and formatting
|
||||
- Don't leave placeholder text unfilled
|
||||
- Add contributor scoping if user is contributing
|
||||
</principle>
|
||||
|
||||
<mapping_examples>
|
||||
<example from="Steps to Reproduce" to="User's reproduction steps + code paths"/>
|
||||
<example from="Expected behavior" to="What user expects + code logic verification"/>
|
||||
<example from="System information" to="Detected versions + environment"/>
|
||||
<example from="Additional context" to="Code findings + architecture insights"/>
|
||||
</mapping_examples>
|
||||
</filling_templates>
|
||||
|
||||
<no_template_behavior>
|
||||
<description>
|
||||
When no templates exist, create appropriate generic templates on the fly.
|
||||
Keep them simple and focused on essential information.
|
||||
</description>
|
||||
|
||||
<guidelines>
|
||||
- Don't overwhelm with too many fields
|
||||
- Focus on problem description first
|
||||
- Include technical details only if user is contributing
|
||||
- Use clear, simple section headers
|
||||
- Adapt based on issue type (bug vs feature)
|
||||
</guidelines>
|
||||
</no_template_behavior>
|
||||
</github_issue_templates>
|
||||
|
|
@ -1,172 +1,147 @@
|
|||
<best_practices>
|
||||
<mode_scope>
|
||||
This mode assembles a template-free issue body grounded by codebase exploration and can submit it via GitHub CLI after explicit confirmation.
|
||||
Submission uses Title and Body only and targets the detected repository after the merged Review and Submit step.
|
||||
</mode_scope>
|
||||
|
||||
<mode_behavior>
|
||||
- CRITICAL: This mode assumes the user's FIRST message is already an issue description
|
||||
- Do NOT ask "What would you like to do?" or "Do you want to create an issue?"
|
||||
- Immediately start the issue creation workflow when the user begins talking
|
||||
- Treat their initial message as the problem/feature description
|
||||
- Begin with repository detection and codebase discovery right away
|
||||
- The user is already in "issue creation mode" by choosing this mode
|
||||
- Treat the user's FIRST message as the issue description; do not ask if they want to create an issue.
|
||||
- Start with repository detection (verify git repo; resolve OWNER/REPO from origin), then determine repository structure (monorepo/standard).
|
||||
- After detection, begin codebase discovery scoped to the repository root or the selected package (in monorepos).
|
||||
- Keep final output non-technical; implementation details remain internal.
|
||||
</mode_behavior>
|
||||
|
||||
<template_usage>
|
||||
- ALWAYS check for repository-specific issue templates before creating issues
|
||||
- Use templates from .github/ISSUE_TEMPLATE/ directory if they exist
|
||||
- Parse both YAML (.yml/.yaml) and Markdown (.md) template formats
|
||||
- If multiple templates exist, let the user choose the appropriate one
|
||||
- If no templates exist, create a simple generic template on the fly
|
||||
- NEVER fall back to hardcoded templates - always use repo templates or generate minimal ones
|
||||
- Respect template metadata like labels, assignees, and title patterns
|
||||
- Fill templates intelligently using gathered information from codebase exploration
|
||||
</template_usage>
|
||||
|
||||
<problem_reporting_focus>
|
||||
- Focus on helping users describe problems clearly, not solutions
|
||||
- The project team will design solutions unless the user explicitly wants to contribute
|
||||
- Don't push users to provide technical details they may not have
|
||||
- Make it easy for non-technical users to report issues effectively
|
||||
|
||||
CRITICAL: Lead with user impact:
|
||||
- Always explain WHO is affected and WHEN the problem occurs
|
||||
- Use concrete examples with actual values, not abstractions
|
||||
- Show before/after scenarios with specific data
|
||||
- Example: "Users trying to [action] see [actual result] instead of [expected result]"
|
||||
</problem_reporting_focus>
|
||||
|
||||
<fact_driven_verification>
|
||||
- ALWAYS verify user claims against actual code implementation
|
||||
- For feature requests, aggressively check if current behavior matches user's description
|
||||
- If code shows different intent than user describes, it might be a bug not a feature
|
||||
- Present code evidence when challenging user assumptions
|
||||
- Do not be agreeable - be fact-driven and question discrepancies
|
||||
- Continue verification until facts are established
|
||||
- A "feature request" where code shows the feature should already work is likely a bug
|
||||
|
||||
CRITICAL additions for thorough analysis:
|
||||
- Trace data flow from where values are created to where they're used
|
||||
- Look for existing variables/functions that already contain needed data
|
||||
- Check if the issue is just missing usage of existing code
|
||||
- Follow imports and exports to understand data availability
|
||||
- Identify patterns in similar features that work correctly
|
||||
</fact_driven_verification>
|
||||
|
||||
<general_practices>
|
||||
- Always search for existing similar issues before creating a new one
|
||||
- Check for and use repository issue templates before creating content
|
||||
- Include specific version numbers and environment details
|
||||
- Use code blocks with syntax highlighting for code snippets
|
||||
- Make titles descriptive but concise (e.g., "Dark theme: Submit button invisible due to white-on-grey text")
|
||||
- For bugs, always test if the issue is reproducible
|
||||
- Include screenshots or mockups when relevant (ask user to provide)
|
||||
- Link to related issues or PRs if found during exploration
|
||||
|
||||
CRITICAL: Use concrete examples throughout:
|
||||
- Show actual data values, not just descriptions
|
||||
- Include specific file paths and line numbers
|
||||
- Demonstrate the data flow with real examples
|
||||
- Bad: "The value is incorrect"
|
||||
- Good: "The function returns '123' when it should return '456'"
|
||||
</general_practices>
|
||||
|
||||
<contributor_specific>
|
||||
- Only perform issue scoping if user wants to contribute
|
||||
- Reference specific files and line numbers from codebase exploration
|
||||
- Ensure technical proposals align with project architecture
|
||||
- Include implementation steps and issue scoping
|
||||
- Provide clear acceptance criteria in Given/When/Then format
|
||||
- Consider trade-offs and alternative approaches
|
||||
|
||||
CRITICAL: Prioritize simple solutions:
|
||||
- ALWAYS check if needed functionality already exists before proposing new code
|
||||
- Look for existing variables that just need to be passed/used differently
|
||||
- Prefer using existing patterns over creating new ones
|
||||
- The best fix often involves minimal code changes
|
||||
- Example: "Use existing `modeInfo` from line 234 in export" vs "Create new mode tracking system"
|
||||
</contributor_specific>
|
||||
|
||||
<backwards_compatibility_focus>
|
||||
ALWAYS consider backwards compatibility:
|
||||
- Think about existing data/configurations already in use
|
||||
- Propose solutions that handle both old and new formats gracefully
|
||||
- Consider migration paths for existing users
|
||||
- Document any breaking changes clearly
|
||||
- Prefer additive changes over breaking changes when possible
|
||||
</backwards_compatibility_focus>
|
||||
|
||||
|
||||
<value_framing>
|
||||
<principles>
|
||||
- Always pair the problem with user-facing value: who is impacted, when it occurs, and why it matters.
|
||||
- Keep value non-technical (clarity, time saved, fewer errors, better UX, improved accessibility, reduced confusion).
|
||||
</principles>
|
||||
<lightweight_impact_options>
|
||||
- Severity: Blocker | High | Medium | Low (optional)
|
||||
- Reach: Few | Some | Many (optional)
|
||||
</lightweight_impact_options>
|
||||
</value_framing>
|
||||
|
||||
<sourcing_and_provenance>
|
||||
<direct_from_user_only>
|
||||
- Reproduction steps
|
||||
- Variations tried
|
||||
- Environment details
|
||||
</direct_from_user_only>
|
||||
<inference_allowed_with_care>
|
||||
- Problem/Value statement (plain-language synthesis from user wording)
|
||||
- Context (who/when) based on user input; keep code-based signals internal
|
||||
</inference_allowed_with_care>
|
||||
<hallucination_guards>
|
||||
- Never fabricate “Variations tried.” If not provided, omit.
|
||||
- If critical details are missing, ask targeted questions; otherwise proceed with omissions.
|
||||
</hallucination_guards>
|
||||
</sourcing_and_provenance>
|
||||
|
||||
<cli_submission>
|
||||
<confirmation>
|
||||
Use a single merged "Review and Submit" step with options:
|
||||
- Submit now
|
||||
- Submit now and assign to me
|
||||
Any other response is treated as a change request and the step is rerun after applying edits.
|
||||
</confirmation>
|
||||
<repo_detection>
|
||||
Submission requires repository detection (git present, origin configured). Capture normalized OWNER/REPO (e.g., owner/repo) and store as [OWNER_REPO] for submission.
|
||||
</repo_detection>
|
||||
<target_repo>
|
||||
Always specify the target using --repo "[OWNER_REPO]" to avoid ambiguity and ensure the correct repository is used.
|
||||
</target_repo>
|
||||
<assignment>
|
||||
When "Submit now and assign to me" is chosen, create using: --assignee "@me".
|
||||
If creation with --assignee fails (e.g., permissions), create the issue without an assignee and immediately run:
|
||||
gh issue edit <issue-url-or-number> --add-assignee "@me".
|
||||
</assignment>
|
||||
<command_safety>
|
||||
Use --body with robust quoting (for example: --body "$(printf '%s\n' "[ISSUE_BODY]")") or a heredoc; do not create temporary files or reference file paths. Always include --repo "[OWNER_REPO]" and echo the resulting issue URL.
|
||||
In execute_command calls, output only the command string; never include XML tags, CDATA markers, code fences, or backticks in the command payload.
|
||||
</command_safety>
|
||||
<error_handling>
|
||||
On gh errors (installation/auth), present the error and offer to retry after fixing gh setup. Surface the computed Title and Body inline
|
||||
so the user can submit manually if needed.
|
||||
</error_handling>
|
||||
</cli_submission>
|
||||
|
||||
<codebase_exploration>
|
||||
<principles>
|
||||
- Use semantic search first to find relevant areas.
|
||||
- Refine with targeted regex for exact strings (errors, component names, flags).
|
||||
- Read key files to verify behavior; keep evidence internal.
|
||||
- Early-stop when hits converge (~70%) or you can name the exact feature/component.
|
||||
- Escalate-once if signals conflict; run one refined batch, then proceed.
|
||||
</principles>
|
||||
<tool_sequence>
|
||||
1) codebase_search → 2) search_files → 3) read_file (as needed)
|
||||
</tool_sequence>
|
||||
<scoping>
|
||||
In monorepos, scope searches to the selected package when the context is clear; otherwise ask for the relevant package/app if ambiguous.
|
||||
</scoping>
|
||||
<internal_only>
|
||||
Keep language plain and exclude technical artifacts (paths, line numbers, stack traces, diffs) from the final issue body.
|
||||
</internal_only>
|
||||
</codebase_exploration>
|
||||
|
||||
<questioning>
|
||||
<guidelines>
|
||||
- Ask minimal, targeted questions based on what you found in code.
|
||||
- For bugs: request a minimal reproduction (environment, steps, expected, actual, variations).
|
||||
- For enhancements: capture user goal, desired behavior in plain language, and any constraints.
|
||||
- Present discrepancies in plain language (no code) and confirm understanding.
|
||||
</guidelines>
|
||||
</questioning>
|
||||
|
||||
<issue_output_rules>
|
||||
<format>
|
||||
<![CDATA[
|
||||
## Type
|
||||
Bug | Enhancement
|
||||
|
||||
## Problem / Value
|
||||
[One or two sentences that capture the problem and why it matters in plain language]
|
||||
|
||||
## Context
|
||||
[Who is affected and when it happens]
|
||||
[Enhancement: desired behavior conceptually, in the user's words]
|
||||
[Bug: current observed behavior in plain language]
|
||||
|
||||
## Reproduction (Bug only, if available)
|
||||
1) Steps (each action/command)
|
||||
2) Expected result
|
||||
3) Actual result
|
||||
4) Variations tried (only if explicitly provided)
|
||||
|
||||
## Constraints/Preferences
|
||||
[Performance, accessibility, UX, or other considerations]
|
||||
]]>
|
||||
</format>
|
||||
<rules>
|
||||
- Omit sections that would be empty.
|
||||
- Do not include "Variations tried" unless explicitly provided by the user.
|
||||
- Keep language plain and user-centric.
|
||||
- Exclude technical artifacts (paths, lines, stacks, diffs).
|
||||
</rules>
|
||||
</issue_output_rules>
|
||||
|
||||
<review_stage_presentation>
|
||||
- At each review stage, present the full current issue details (Title + Body) in a markdown code block.
|
||||
- Offer "Submit now" or "Submit now and assign to me" suggestions; treat any other response as a change request and rerun the step after applying edits.
|
||||
</review_stage_presentation>
|
||||
|
||||
<autonomy_and_budgets>
|
||||
- Tool preambles: restate goal briefly, outline a short plan, narrate progress succinctly, summarize final delta.
|
||||
- One-tool-per-message: await results before continuing.
|
||||
- Discovery budget: default max 3 searches before escalate-once; stop when sufficient.
|
||||
- Early-stop: when top hits converge or target is identifiable.
|
||||
- Verbosity: low narrative; detail appears only in structured outputs.
|
||||
</autonomy_and_budgets>
|
||||
|
||||
<communication_guidelines>
|
||||
- Be supportive and encouraging to problem reporters
|
||||
- Don't overwhelm users with technical questions upfront
|
||||
- Clearly indicate when technical sections are optional
|
||||
- Guide contributors through the additional requirements
|
||||
- Make the "submit now" option clear for problem reporters
|
||||
- When presenting template choices, include template descriptions to help users choose
|
||||
- Explain that you're using the repository's own templates for consistency
|
||||
- Be direct and concise; avoid jargon in the final issue body.
|
||||
- Keep questions optional and easy to answer with suggested options.
|
||||
- Emphasize WHO is affected and WHEN it happens.
|
||||
</communication_guidelines>
|
||||
|
||||
<template_best_practices>
|
||||
<practice name="template_detection">
|
||||
Always check these locations in order:
|
||||
1. .github/ISSUE_TEMPLATE/*.yml or *.yaml (GitHub form syntax)
|
||||
2. .github/ISSUE_TEMPLATE/*.md (Markdown templates)
|
||||
3. .github/issue_template.md (single template)
|
||||
4. .github/ISSUE_TEMPLATE.md (alternate naming)
|
||||
</practice>
|
||||
|
||||
<practice name="template_parsing">
|
||||
For YAML templates:
|
||||
- Extract form elements and convert to appropriate markdown sections
|
||||
- Preserve required field indicators
|
||||
- Include field descriptions as context
|
||||
- Respect dropdown options and checkbox lists
|
||||
|
||||
For Markdown templates:
|
||||
- Parse front matter for metadata
|
||||
- Identify section headers and structure
|
||||
- Replace placeholder text with actual information
|
||||
- Maintain formatting and hierarchy
|
||||
</practice>
|
||||
|
||||
<practice name="template_filling">
|
||||
- Map gathered information to template sections intelligently
|
||||
- Don't leave placeholder text in the final issue
|
||||
- Add code investigation findings to relevant sections
|
||||
- Include contributor scoping in appropriate section if applicable
|
||||
- Preserve the template's intended structure and flow
|
||||
</practice>
|
||||
|
||||
<practice name="no_template_handling">
|
||||
When no templates exist:
|
||||
- Create minimal, focused templates
|
||||
- Use simple section headers
|
||||
- Focus on essential information only
|
||||
- Adapt structure based on issue type
|
||||
- Don't overwhelm with unnecessary fields
|
||||
</practice>
|
||||
</template_best_practices>
|
||||
<technical_accuracy_guidelines>
|
||||
<guideline name="thorough_code_analysis">
|
||||
Before proposing ANY solution:
|
||||
1. Use codebase_search extensively to find all related code
|
||||
2. Read multiple files to understand the full context
|
||||
3. Trace variable usage from creation to consumption
|
||||
4. Look for similar working features to understand patterns
|
||||
5. Identify what already exists vs what's actually missing
|
||||
</guideline>
|
||||
|
||||
<guideline name="simplicity_first">
|
||||
When designing solutions:
|
||||
1. Check if the data/function already exists somewhere
|
||||
2. Look for configuration options before code changes
|
||||
3. Prefer passing existing variables over creating new ones
|
||||
4. Use established patterns from similar features
|
||||
5. Aim for minimal diff size
|
||||
</guideline>
|
||||
|
||||
<guideline name="precise_technical_details">
|
||||
Always include:
|
||||
- Exact file paths and line numbers
|
||||
- Variable/function names as they appear in code
|
||||
- Before/after code snippets showing minimal changes
|
||||
- Clear explanation of why the simple fix works
|
||||
</guideline>
|
||||
</technical_accuracy_guidelines>
|
||||
</best_practices>
|
||||
|
|
@ -1,126 +1,109 @@
|
|||
<common_mistakes_to_avoid>
|
||||
<mode_initialization_mistakes>
|
||||
- CRITICAL: Asking "What would you like to do?" when mode starts
|
||||
- Waiting for user to say "create an issue" or "make me an issue"
|
||||
- Not treating the first user message as the issue description
|
||||
- Delaying the workflow start with unnecessary questions
|
||||
- Asking if they want to create an issue when they've already chosen this mode
|
||||
- Not immediately beginning repository detection and codebase discovery
|
||||
- Asking "What would you like to do?" at start instead of treating the first message as the issue description
|
||||
- Delaying the workflow with unnecessary questions before discovery
|
||||
- Not immediately beginning codebase-aware discovery (semantic search → regex refine → read key files)
|
||||
- Skipping repository detection (git + origin) before discovery or submission
|
||||
- Not validating repository context before gh commands
|
||||
</mode_initialization_mistakes>
|
||||
|
||||
|
||||
<scope_mistakes>
|
||||
- Submitting without explicit user confirmation ("Submit now")
|
||||
- Targeting the wrong repository by relying on current directory defaults; always pass --repo OWNER/REPO detected in Step 2
|
||||
- Performing PR prep, complexity estimates, or technical scoping
|
||||
</scope_mistakes>
|
||||
|
||||
<submission_mistakes>
|
||||
<mistake_block>
|
||||
<mistake>Splitting final review and submission into multiple steps</mistake>
|
||||
<impact>Creates redundant prompts and inconsistent state; leads to janky UX</impact>
|
||||
<correct_approach>Use a single merged "Review and Submit" step offering only: Submit now, Submit now and assign to me; treat any other response as a change request</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Not offering "Submit now and assign to me"</mistake>
|
||||
<impact>Forces manual assignment later; reduces efficiency</impact>
|
||||
<correct_approach>Provide the assignment option and use gh issue create --assignee "@me"; if that fails, immediately run gh issue edit <issue-url-or-number> --add-assignee "@me"</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Using temporary files or --body-file for issue body submission</mistake>
|
||||
<impact>Introduces filesystem dependencies and leaks paths; contradicts single-command policy</impact>
|
||||
<correct_approach>Use inline --body with robust quoting, e.g., --body "$(printf '%s\n' "[ISSUE_BODY]")"; do not reference any file paths</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Omitting --repo or relying on current directory defaults</mistake>
|
||||
<impact>May submit to the wrong repository in multi-repo or worktree contexts</impact>
|
||||
<correct_approach>Always pass --repo [OWNER_REPO] detected in Step 2</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Attempting submission without prior repository detection</mistake>
|
||||
<impact>Commands may target the wrong repo or fail</impact>
|
||||
<correct_approach>Detect git repo and ensure origin is configured before any gh commands</correct_approach>
|
||||
</mistake_block>
|
||||
</submission_mistakes>
|
||||
|
||||
<sourcing_mistakes>
|
||||
<mistake_block>
|
||||
<mistake>Inventing or inferring “Variations tried” when the user didn’t provide any</mistake>
|
||||
<impact>Misleads triage and wastes time reproducing non-existent attempts</impact>
|
||||
<correct_approach>Omit the “Variations tried” line entirely unless explicitly provided; if needed, ask a targeted question first</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Framing only the problem without the value/impact</mistake>
|
||||
<impact>Makes prioritization harder; obscures who benefits and why it matters</impact>
|
||||
<correct_approach>Pair the problem with a plain-language value statement (who, when, why it matters)</correct_approach>
|
||||
</mistake_block>
|
||||
<mistake_block>
|
||||
<mistake>Overstating impact without user signal</mistake>
|
||||
<impact>Damages credibility and misguides prioritization</impact>
|
||||
<correct_approach>Use conservative, plain language; if unsure, omit severity/reach or ask a single targeted question</correct_approach>
|
||||
</mistake_block>
|
||||
</sourcing_mistakes>
|
||||
|
||||
<problem_reporting_mistakes>
|
||||
- Vague descriptions like "doesn't work" or "broken"
|
||||
- Missing reproduction steps for bugs
|
||||
- Feature requests without clear problem statements
|
||||
- Not explaining the impact on users
|
||||
- Forgetting to specify when/how the problem occurs
|
||||
- Using wrong labels or no labels
|
||||
- Titles that don't summarize the issue
|
||||
- Not checking for duplicates
|
||||
- Vague descriptions like "doesn't work" without who/when impact
|
||||
- Missing minimal reproduction for bugs (environment, steps, expected, actual, variations)
|
||||
- Enhancement requests that skip the user goal or desired behavior in plain language
|
||||
- Titles/summaries that don't quickly communicate the issue
|
||||
</problem_reporting_mistakes>
|
||||
|
||||
<workflow_mistakes>
|
||||
- Asking for technical details from non-contributing users
|
||||
- Performing issue scoping before confirming user wants to contribute
|
||||
- Requiring acceptance criteria from problem reporters
|
||||
- Making the process too complex for simple problem reports
|
||||
- Not clearly indicating the "submit now" option
|
||||
- Overwhelming users with contributor requirements upfront
|
||||
- Using hardcoded templates instead of repository templates
|
||||
- Not checking for issue templates before creating content
|
||||
- Ignoring template metadata like labels and assignees
|
||||
</workflow_mistakes>
|
||||
|
||||
<contributor_mistakes>
|
||||
- Starting implementation before approval
|
||||
- Not providing detailed issue scoping when contributing
|
||||
- Missing acceptance criteria for contributed features
|
||||
- Forgetting to include technical context from code exploration
|
||||
- Not considering trade-offs and alternatives
|
||||
- Proposing solutions without understanding current architecture
|
||||
</contributor_mistakes>
|
||||
|
||||
<technical_analysis_mistakes>
|
||||
<mistake>Not tracing data flow completely through the system</mistake>
|
||||
<impact>Missing that data already exists leads to proposing unnecessary new code</impact>
|
||||
|
||||
<output_mistakes>
|
||||
- Including code paths, line numbers, stack traces, or diffs in the final issue body
|
||||
- Adding labels, metadata, or repository details to the body
|
||||
- Leaving empty section placeholders instead of omitting the section
|
||||
- Using technical jargon instead of plain, user-centric language
|
||||
</output_mistakes>
|
||||
|
||||
<code_exploration_mistakes>
|
||||
<mistake>Skipping semantic search and jumping straight to assumptions</mistake>
|
||||
<impact>Leads to misclassification and inaccurate context</impact>
|
||||
<correct_approach>
|
||||
- Use codebase_search extensively to find ALL related code
|
||||
- Trace variables from creation to consumption
|
||||
- Check if needed data is already calculated but not used
|
||||
- Look for similar working features as patterns
|
||||
- Start with codebase_search on extracted keywords
|
||||
- Refine with search_files for exact strings (errors, component names, flags)
|
||||
- read_file only as needed to verify behavior; keep evidence internal
|
||||
- Early-stop when hits converge or you can name the exact feature/component
|
||||
- Escalate-once if signals conflict (one refined pass), then proceed
|
||||
</correct_approach>
|
||||
<example>
|
||||
Bad: "Add mode tracking to import function"
|
||||
Good: "The export already includes mode info at line 234, just use it in import at line 567"
|
||||
</example>
|
||||
</technical_analysis_mistakes>
|
||||
|
||||
<solution_design_mistakes>
|
||||
<mistake>Proposing complex new systems when simple fixes exist</mistake>
|
||||
<impact>Creates unnecessary complexity, maintenance burden, and potential bugs</impact>
|
||||
</code_exploration_mistakes>
|
||||
|
||||
<discrepancy_handling_mistakes>
|
||||
<mistake>Accepting user claims that contradict the codebase without verification</mistake>
|
||||
<impact>Produces misleading or incorrect issue framing</impact>
|
||||
<correct_approach>
|
||||
- ALWAYS check if functionality already exists first
|
||||
- Look for minimal changes that solve the problem
|
||||
- Prefer using existing variables/functions differently
|
||||
- Aim for the smallest possible diff
|
||||
- Verify claims against the implementation; trace data from creation → usage
|
||||
- Compare with similar working features to ground expectations
|
||||
- If discrepancies arise, present concrete, plain-language examples (no code) and confirm
|
||||
</correct_approach>
|
||||
<example>
|
||||
Bad: "Create new state management system for mode tracking"
|
||||
Good: "Pass existing modeInfo variable from line 45 to the function at line 78"
|
||||
</example>
|
||||
</solution_design_mistakes>
|
||||
|
||||
<code_verification_mistakes>
|
||||
<mistake>Not reading actual code before proposing solutions</mistake>
|
||||
<impact>Solutions don't match the actual codebase structure</impact>
|
||||
<correct_approach>
|
||||
- Always read the relevant files first
|
||||
- Verify exact line numbers and content
|
||||
- Check imports/exports to understand data availability
|
||||
- Look at similar features that work correctly
|
||||
</correct_approach>
|
||||
</code_verification_mistakes>
|
||||
|
||||
<pattern_recognition_mistakes>
|
||||
<mistake>Creating new patterns instead of following existing ones</mistake>
|
||||
<impact>Inconsistent codebase, harder to maintain</impact>
|
||||
<correct_approach>
|
||||
- Find similar features that work correctly
|
||||
- Follow the same patterns and structures
|
||||
- Reuse existing utilities and helpers
|
||||
- Maintain consistency with the codebase style
|
||||
</correct_approach>
|
||||
</pattern_recognition_mistakes>
|
||||
|
||||
<template_usage_mistakes>
|
||||
<mistake>Using hardcoded templates when repository templates exist</mistake>
|
||||
<impact>Issues don't follow repository conventions, may be rejected or need reformatting</impact>
|
||||
<correct_approach>
|
||||
- Always check .github/ISSUE_TEMPLATE/ directory first
|
||||
- Parse and use repository templates when available
|
||||
- Only create generic templates when none exist
|
||||
</correct_approach>
|
||||
</template_usage_mistakes>
|
||||
|
||||
<template_parsing_mistakes>
|
||||
<mistake>Not properly parsing YAML template structure</mistake>
|
||||
<impact>Missing required fields, incorrect formatting, lost metadata</impact>
|
||||
<correct_approach>
|
||||
- Parse YAML templates to extract all form elements
|
||||
- Convert form elements to appropriate markdown sections
|
||||
- Preserve field requirements and descriptions
|
||||
- Maintain dropdown options and checkbox lists
|
||||
</correct_approach>
|
||||
</template_parsing_mistakes>
|
||||
|
||||
<template_filling_mistakes>
|
||||
<mistake>Leaving placeholder text in final issue</mistake>
|
||||
<impact>Unprofessional appearance, confusion about what information is needed</impact>
|
||||
<correct_approach>
|
||||
- Replace all placeholders with actual information
|
||||
- Remove instruction text meant for template users
|
||||
- Fill every section with relevant content
|
||||
- Add "N/A" for truly inapplicable sections
|
||||
</correct_approach>
|
||||
</template_filling_mistakes>
|
||||
</discrepancy_handling_mistakes>
|
||||
|
||||
<questioning_mistakes>
|
||||
- Asking broad, unfocused questions instead of targeted ones based on findings
|
||||
- Demanding technical details from non-technical users
|
||||
- Failing to provide easy, suggested answer formats (repro scaffold, goal statement)
|
||||
</questioning_mistakes>
|
||||
|
||||
<consistency_mistakes>
|
||||
- Mixing internal technical evidence into the final body
|
||||
- Ignoring the issue format or adding extra sections
|
||||
- Using inconsistent tone or switching between technical and non-technical language
|
||||
</consistency_mistakes>
|
||||
</common_mistakes_to_avoid>
|
||||
134
.roo/rules-issue-writer/5_examples.xml
Normal file
134
.roo/rules-issue-writer/5_examples.xml
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<issue_examples>
|
||||
<overview>
|
||||
Examples of assembling template-free issue prompts grounded by codebase exploration, with optional CLI submission after explicit confirmation.
|
||||
Repository detection precedes submission; review and submission occur in a single merged step offering "Submit now" or "Submit now and assign to me". Any other response is treated as a change request.
|
||||
</overview>
|
||||
|
||||
<example name="bug_dark_theme_button_invisible">
|
||||
<user_input>
|
||||
In dark theme the Submit button is almost invisible on the New Run page.
|
||||
</user_input>
|
||||
<discovery>
|
||||
<tool_calls>
|
||||
<![CDATA[
|
||||
<codebase_search>
|
||||
<query>dark theme submit button visibility</query>
|
||||
</codebase_search>
|
||||
|
||||
<search_files>
|
||||
<path>.</path>
|
||||
<regex>Submit|button|dark|theme</regex>
|
||||
</search_files>
|
||||
]]>
|
||||
</tool_calls>
|
||||
<notes>
|
||||
Internal: matches found in UI components related to theme; wording grounded to user impact.
|
||||
</notes>
|
||||
</discovery>
|
||||
<final_issue_body><![CDATA[
|
||||
## Type
|
||||
Bug
|
||||
|
||||
## Problem / Value
|
||||
In dark theme, the Submit button is hard to see on the new run form, making it difficult for users to complete new runs.
|
||||
|
||||
## Context
|
||||
Affects users creating new runs with dark theme enabled; the button appears low-contrast and is difficult to locate.
|
||||
|
||||
## Reproduction
|
||||
1) Steps: Open "New Run" -> Scroll to bottom -> Look for Submit
|
||||
2) Expected result: Clearly visible, high-contrast Submit button
|
||||
3) Actual result: Button appears nearly invisible in dark theme
|
||||
4) Variations tried: Different browsers (Chrome/Firefox) show same result
|
||||
]]></final_issue_body>
|
||||
</example>
|
||||
|
||||
<example name="enhancement_copy_run_confirmation">
|
||||
<user_input>
|
||||
I accidentally click "Copy Run" sometimes; would be great to have a simple confirmation.
|
||||
</user_input>
|
||||
<discovery>
|
||||
<tool_calls>
|
||||
<![CDATA[
|
||||
<codebase_search>
|
||||
<query>Copy Run confirmation</query>
|
||||
</codebase_search>
|
||||
]]>
|
||||
</tool_calls>
|
||||
<notes>
|
||||
Internal: feature entry point identified; keep final output non-technical and user-centric.
|
||||
</notes>
|
||||
</discovery>
|
||||
<final_issue_body><![CDATA[
|
||||
## Type
|
||||
Enhancement
|
||||
|
||||
## Problem / Value
|
||||
Add a confirmation dialog before copying an existing run to prevent accidental duplication.
|
||||
|
||||
## Context
|
||||
Users sometimes click "Copy Run" by mistake when browsing runs; a simple confirmation would prevent accidental duplication.
|
||||
|
||||
## Constraints/Preferences
|
||||
Keep the flow lightweight and unobtrusive; avoid slowing down intentional copies.
|
||||
]]></final_issue_body>
|
||||
</example>
|
||||
|
||||
<example name="bug_submission_review_and_assign">
|
||||
<user_input>
|
||||
Dark theme Submit button is invisible; I'd like to file this.
|
||||
</user_input>
|
||||
<final_issue_body><![CDATA[
|
||||
## Type
|
||||
Bug
|
||||
|
||||
## Problem / Value
|
||||
In dark theme, the Submit button is hard to see on the new run form, making it difficult for users to complete new runs.
|
||||
|
||||
## Context
|
||||
Affects users creating new runs with dark theme enabled; the button appears low-contrast and is difficult to locate.
|
||||
|
||||
## Reproduction
|
||||
1) Steps: Open "New Run" -> Scroll to bottom -> Look for Submit
|
||||
2) Expected result: Clearly visible, high-contrast Submit button
|
||||
3) Actual result: Button appears nearly invisible in dark theme
|
||||
]]></final_issue_body>
|
||||
<review_and_submit>
|
||||
<ask_followup_question>
|
||||
<question>Review the current issue details. Select one of the options below or specify any changes or other workflow you would like me to perform:
|
||||
|
||||
```md
|
||||
Title: [ISSUE_TITLE]
|
||||
|
||||
[ISSUE_BODY]
|
||||
```</question>
|
||||
<follow_up>
|
||||
<suggest>Submit now</suggest>
|
||||
<suggest>Submit now and assign to me</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
|
||||
<execute_command for="submit_now">
|
||||
<command>gh issue create --repo "[OWNER_REPO]" --title "[ISSUE_TITLE]" --body "$(printf '%s\n' "[ISSUE_BODY]")"</command>
|
||||
</execute_command>
|
||||
|
||||
<execute_command for="submit_now_and_assign_to_me">
|
||||
<command>ISSUE_URL=$(gh issue create --repo "[OWNER_REPO]" --title "[ISSUE_TITLE]" --body "$(printf '%s\n' "[ISSUE_BODY]")" --assignee "@me") || true; if [ -z "$ISSUE_URL" ]; then ISSUE_URL=$(gh issue create --repo "[OWNER_REPO]" --title "[ISSUE_TITLE]" --body "$(printf '%s\n' "[ISSUE_BODY]")"); gh issue edit "$ISSUE_URL" --add-assignee "@me"; fi; echo "$ISSUE_URL"</command>
|
||||
</execute_command>
|
||||
|
||||
<loopback_note>
|
||||
If a change request is provided, collect the requested edits, update the draft (re-run discovery if new info affects context), then rerun this merged step.
|
||||
</loopback_note>
|
||||
|
||||
<expected_output>https://github.com/OWNER/REPO/issues/123</expected_output>
|
||||
</review_and_submit>
|
||||
</example>
|
||||
|
||||
<policies>
|
||||
<policy>Issues are template-free (Title + Body only).</policy>
|
||||
<policy>Repository detection (git + origin → OWNER/REPO) occurs before submission and is passed explicitly via --repo [OWNER_REPO].</policy>
|
||||
<policy>Never use --body-file or temporary files; submit with inline --body only (no file paths).</policy>
|
||||
<policy>Review and submission happen in one merged step offering "Submit now" or "Submit now and assign to me"; any other response is treated as a change request.</policy>
|
||||
<policy>All discovery is internal; keep final output plain-language.</policy>
|
||||
</policies>
|
||||
</issue_examples>
|
||||
|
|
@ -1,342 +0,0 @@
|
|||
<github_cli_usage>
|
||||
<overview>
|
||||
The GitHub CLI (gh) provides comprehensive tools for interacting with GitHub.
|
||||
Here's when and how to use each command in the issue creation workflow.
|
||||
|
||||
Note: This mode prioritizes using repository-specific issue templates over
|
||||
hardcoded ones. Templates are detected and used dynamically from the repository.
|
||||
</overview>
|
||||
|
||||
<pre_creation_commands>
|
||||
<command name="gh issue list">
|
||||
<when_to_use>
|
||||
ALWAYS use this FIRST before creating any issue to check for duplicates.
|
||||
Search for keywords from the user's problem description.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue list --repo $REPO_FULL_NAME --search "dark theme button visibility" --state all --limit 20</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
<options>
|
||||
--search: Search query for issue titles and bodies
|
||||
--state: all, open, or closed
|
||||
--label: Filter by specific labels
|
||||
--limit: Number of results to show
|
||||
--json: Get structured JSON output
|
||||
</options>
|
||||
</command>
|
||||
|
||||
<command name="gh search issues">
|
||||
<when_to_use>
|
||||
Use for more advanced searches across issues and pull requests.
|
||||
Supports GitHub's advanced search syntax.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh search issues --repo $REPO_FULL_NAME "dark theme button" --limit 10</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="gh issue view">
|
||||
<when_to_use>
|
||||
Use when you find a potentially related issue and need full details.
|
||||
Check if the user's issue is already reported or related.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue view 123 --repo $REPO_FULL_NAME --comments</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
<options>
|
||||
--comments: Include issue comments
|
||||
--json: Get structured data
|
||||
--web: Open in browser
|
||||
</options>
|
||||
</command>
|
||||
</pre_creation_commands>
|
||||
|
||||
<template_detection_commands>
|
||||
<command name="list_files">
|
||||
<when_to_use>
|
||||
Use to check for issue templates in the repository before creating issues.
|
||||
This is not a gh command but necessary for template detection.
|
||||
</when_to_use>
|
||||
<examples>
|
||||
Check for templates in standard location:
|
||||
<list_files>
|
||||
<path>.github/ISSUE_TEMPLATE</path>
|
||||
<recursive>true</recursive>
|
||||
</list_files>
|
||||
|
||||
Check for single template file:
|
||||
<list_files>
|
||||
<path>.github</path>
|
||||
<recursive>false</recursive>
|
||||
</list_files>
|
||||
</examples>
|
||||
</command>
|
||||
|
||||
<command name="read_file">
|
||||
<when_to_use>
|
||||
Read template files to parse their structure and content.
|
||||
Used after detecting template files.
|
||||
</when_to_use>
|
||||
<examples>
|
||||
Read YAML template:
|
||||
<read_file>
|
||||
<path>.github/ISSUE_TEMPLATE/bug_report.yml</path>
|
||||
</read_file>
|
||||
|
||||
Read Markdown template:
|
||||
<read_file>
|
||||
<path>.github/ISSUE_TEMPLATE/feature_request.md</path>
|
||||
</read_file>
|
||||
</examples>
|
||||
</command>
|
||||
</template_detection_commands>
|
||||
|
||||
<contributor_only_commands>
|
||||
<note>
|
||||
These commands should ONLY be used if the user has indicated they want to
|
||||
contribute the implementation. Skip these for problem reporters.
|
||||
</note>
|
||||
|
||||
<command name="gh repo view">
|
||||
<when_to_use>
|
||||
Get repository information and recent activity.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh repo view $REPO_FULL_NAME --json defaultBranchRef,description,updatedAt</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="gh search prs">
|
||||
<when_to_use>
|
||||
Check recent PRs that might be related to the issue.
|
||||
Look for PRs that modified relevant code.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh search prs --repo $REPO_FULL_NAME "dark theme" --limit 10 --state all</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="git log">
|
||||
<when_to_use>
|
||||
For bug reports from contributors, check recent commits that might have introduced the issue.
|
||||
Use after cloning the repository locally.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>git log --oneline --grep="theme" -n 20</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
</contributor_only_commands>
|
||||
|
||||
<issue_creation_command>
|
||||
<command name="gh issue create">
|
||||
<when_to_use>
|
||||
Only use after:
|
||||
1. Confirming no duplicates exist
|
||||
2. Checking for and using repository templates
|
||||
3. Gathering all required information
|
||||
4. Determining if user is contributing or just reporting
|
||||
5. Getting user confirmation
|
||||
</when_to_use>
|
||||
<bug_report_example>
|
||||
<execute_command>
|
||||
<command>gh issue create --repo $REPO_FULL_NAME --title "[Descriptive title of the bug]" --body-file /tmp/issue_body.md --label "bug"</command>
|
||||
</execute_command>
|
||||
</bug_report_example>
|
||||
<feature_request_example>
|
||||
<execute_command>
|
||||
<command>gh issue create --repo $REPO_FULL_NAME --title "[Problem-focused title]" --body-file /tmp/issue_body.md --label "proposal" --label "enhancement"</command>
|
||||
</execute_command>
|
||||
</feature_request_example>
|
||||
<options>
|
||||
--title: Issue title (required)
|
||||
--body: Issue body text
|
||||
--body-file: Read body from file
|
||||
--label: Add labels (can use multiple times)
|
||||
--assignee: Assign to user
|
||||
--project: Add to project
|
||||
--web: Open in browser to create
|
||||
</options>
|
||||
</command>
|
||||
</issue_creation_command>
|
||||
|
||||
<post_creation_commands>
|
||||
<command name="gh issue comment">
|
||||
<when_to_use>
|
||||
ONLY use if user wants to add additional information after creation.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue comment 456 --repo $REPO_FULL_NAME --body "Additional context or comments."</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="gh issue edit">
|
||||
<when_to_use>
|
||||
Use if user realizes they need to update the issue after creation.
|
||||
Can update title, body, or labels.
|
||||
</when_to_use>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue edit 456 --repo $REPO_FULL_NAME --title "[Updated title]" --body "[Updated body]"</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
</post_creation_commands>
|
||||
|
||||
<workflow_integration>
|
||||
<step_1_integration>
|
||||
After user selects issue type, immediately search for related issues:
|
||||
1. Use `gh issue list --search` with keywords from their description
|
||||
2. Show any similar issues found
|
||||
3. Ask if they want to continue or comment on existing issue
|
||||
</step_1_integration>
|
||||
|
||||
<step_2_integration>
|
||||
Template detection (NEW):
|
||||
1. Use list_files to check .github/ISSUE_TEMPLATE/ directory
|
||||
2. Read any template files found (YAML or Markdown)
|
||||
3. Parse template structure and metadata
|
||||
4. If multiple templates, let user choose
|
||||
5. If no templates, prepare to create generic one
|
||||
</step_2_integration>
|
||||
|
||||
<step_3_integration>
|
||||
Decision point for contribution:
|
||||
1. Ask user if they want to contribute implementation
|
||||
2. If yes: Use contributor commands for codebase investigation
|
||||
3. If no: Skip directly to creating a problem-focused issue
|
||||
4. This saves time for problem reporters
|
||||
</step_3_integration>
|
||||
|
||||
<step_4_integration>
|
||||
During codebase exploration (CONTRIBUTORS ONLY):
|
||||
1. Clone repo locally if needed: `gh repo clone $REPO_FULL_NAME`
|
||||
2. Use `git log` to find recent changes to affected files
|
||||
3. Use `gh search prs` for related pull requests
|
||||
4. Include findings in the technical context section
|
||||
</step_4_integration>
|
||||
|
||||
<step_5_integration>
|
||||
When creating the issue:
|
||||
1. Use repository template if found, or generic template if not
|
||||
2. Fill template with gathered information
|
||||
3. Format differently based on contributor vs problem reporter
|
||||
4. Save formatted body to temporary file
|
||||
5. Use `gh issue create` with appropriate labels from template
|
||||
6. Capture the returned issue URL
|
||||
7. Show user the created issue URL
|
||||
</step_5_integration>
|
||||
</workflow_integration>
|
||||
|
||||
<best_practices>
|
||||
<practice name="file_handling">
|
||||
When creating issues with long bodies:
|
||||
1. Save to temporary file: `cat > /tmp/issue_body.md << 'EOF'`
|
||||
2. Use --body-file flag with gh issue create
|
||||
3. Clean up after: `rm /tmp/issue_body.md`
|
||||
</practice>
|
||||
|
||||
<practice name="search_efficiency">
|
||||
Use specific search terms:
|
||||
- Include error messages in quotes
|
||||
- Use label filters when appropriate
|
||||
- Limit results to avoid overwhelming output
|
||||
</practice>
|
||||
|
||||
<practice name="json_output">
|
||||
Use --json flag for structured data when needed:
|
||||
- Easier to parse programmatically
|
||||
- Consistent format across commands
|
||||
- Example: `gh issue list --json number,title,state`
|
||||
</practice>
|
||||
</best_practices>
|
||||
|
||||
<error_handling>
|
||||
<duplicate_found>
|
||||
If search finds exact duplicate:
|
||||
- Show the existing issue to user using `gh issue view`
|
||||
- Ask if they want to add a comment instead
|
||||
- Use `gh issue comment` if they agree
|
||||
</duplicate_found>
|
||||
|
||||
<creation_failed>
|
||||
If `gh issue create` fails:
|
||||
- Check error message (auth, permissions, network)
|
||||
- Ensure gh is authenticated: `gh auth status`
|
||||
- Save the drafted issue content for user
|
||||
- Suggest using --web flag to create in browser
|
||||
</creation_failed>
|
||||
|
||||
<authentication>
|
||||
Ensure GitHub CLI is authenticated:
|
||||
- Check status: `gh auth status`
|
||||
- Login if needed: `gh auth login`
|
||||
- Select appropriate scopes for issue creation
|
||||
</authentication>
|
||||
</error_handling>
|
||||
|
||||
<command_reference>
|
||||
<issues>
|
||||
gh issue create - Create new issue
|
||||
gh issue list - List and search issues
|
||||
gh issue view - View issue details
|
||||
gh issue comment - Add comment to issue
|
||||
gh issue edit - Edit existing issue
|
||||
gh issue close - Close an issue
|
||||
gh issue reopen - Reopen closed issue
|
||||
</issues>
|
||||
|
||||
<search>
|
||||
gh search issues - Search issues and PRs
|
||||
gh search prs - Search pull requests
|
||||
gh search repos - Search repositories
|
||||
</search>
|
||||
|
||||
<repository>
|
||||
gh repo view - View repository info
|
||||
gh repo clone - Clone repository
|
||||
</repository>
|
||||
</command_reference>
|
||||
|
||||
<template_handling_reference>
|
||||
<yaml_template_parsing>
|
||||
When parsing YAML templates:
|
||||
- Extract 'name' for template identification
|
||||
- Get 'labels' array for automatic labeling
|
||||
- Parse 'body' array for form elements
|
||||
- Convert form elements to markdown sections
|
||||
- Preserve 'required' field indicators
|
||||
</yaml_template_parsing>
|
||||
|
||||
<markdown_template_parsing>
|
||||
When parsing Markdown templates:
|
||||
- Check for YAML front matter
|
||||
- Extract metadata (labels, assignees)
|
||||
- Identify section headers
|
||||
- Replace placeholder text
|
||||
- Maintain formatting structure
|
||||
</markdown_template_parsing>
|
||||
|
||||
<template_usage_flow>
|
||||
1. Detect templates with list_files
|
||||
2. Read templates with read_file
|
||||
3. Parse structure and metadata
|
||||
4. Let user choose if multiple exist
|
||||
5. Fill template with information
|
||||
6. Create issue with template content
|
||||
</template_usage_flow>
|
||||
</template_handling_reference>
|
||||
</github_cli_usage>
|
||||
|
|
@ -1,301 +0,0 @@
|
|||
<mode_management_workflow>
|
||||
<overview>
|
||||
This workflow guides you through creating new custom modes or editing existing modes
|
||||
for the Roo Code Software, ensuring comprehensive understanding and cohesive implementation.
|
||||
</overview>
|
||||
|
||||
<initial_determination>
|
||||
<step number="1">
|
||||
<title>Determine User Intent</title>
|
||||
<description>
|
||||
Identify whether the user wants to create a new mode or edit an existing one
|
||||
</description>
|
||||
<detection_patterns>
|
||||
<pattern type="edit_existing">
|
||||
<indicators>
|
||||
<indicator>User mentions a specific mode by name or slug</indicator>
|
||||
<indicator>User references a mode directory path (e.g., .roo/rules-[mode-slug])</indicator>
|
||||
<indicator>User asks to modify, update, enhance, or fix an existing mode</indicator>
|
||||
<indicator>User says "edit this mode" or "change this mode"</indicator>
|
||||
</indicators>
|
||||
</pattern>
|
||||
<pattern type="create_new">
|
||||
<indicators>
|
||||
<indicator>User asks to create a new mode</indicator>
|
||||
<indicator>User describes a new capability not covered by existing modes</indicator>
|
||||
<indicator>User says "make a mode for" or "create a mode that"</indicator>
|
||||
</indicators>
|
||||
</pattern>
|
||||
</detection_patterns>
|
||||
<clarification_question>
|
||||
<ask_followup_question>
|
||||
<question>I want to make sure I understand correctly. Are you looking to create a brand new mode or modify an existing one?</question>
|
||||
<follow_up>
|
||||
<suggest>Create a new mode for a specific purpose</suggest>
|
||||
<suggest>Edit an existing mode to add new capabilities</suggest>
|
||||
<suggest>Fix issues in an existing mode</suggest>
|
||||
<suggest>Enhance an existing mode with better workflows</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</clarification_question>
|
||||
</step>
|
||||
</initial_determination>
|
||||
|
||||
<workflow_branches>
|
||||
<branch name="create_new_mode">
|
||||
<step number="2a">
|
||||
<title>Gather Requirements for New Mode</title>
|
||||
<description>
|
||||
Understand what the user wants the new mode to accomplish
|
||||
</description>
|
||||
<actions>
|
||||
<action>Ask about the mode's primary purpose and use cases</action>
|
||||
<action>Identify what types of tasks the mode should handle</action>
|
||||
<action>Determine what tools and file access the mode needs</action>
|
||||
<action>Clarify any special behaviors or restrictions</action>
|
||||
</actions>
|
||||
<example>
|
||||
<ask_followup_question>
|
||||
<question>What is the primary purpose of this new mode? What types of tasks should it handle?</question>
|
||||
<follow_up>
|
||||
<suggest>A mode for writing and maintaining documentation</suggest>
|
||||
<suggest>A mode for database schema design and migrations</suggest>
|
||||
<suggest>A mode for API endpoint development and testing</suggest>
|
||||
<suggest>A mode for performance optimization and profiling</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</example>
|
||||
</step>
|
||||
|
||||
<step number="3a">
|
||||
<title>Design Mode Configuration</title>
|
||||
<description>
|
||||
Create the mode definition with all required fields
|
||||
</description>
|
||||
<required_fields>
|
||||
<field name="slug">
|
||||
<description>Unique identifier (lowercase, hyphens allowed)</description>
|
||||
<best_practice>Keep it short and descriptive (e.g., "api-dev", "docs-writer")</best_practice>
|
||||
</field>
|
||||
<field name="name">
|
||||
<description>Display name with optional emoji</description>
|
||||
<best_practice>Use an emoji that represents the mode's purpose</best_practice>
|
||||
</field>
|
||||
<field name="roleDefinition">
|
||||
<description>Detailed description of the mode's role and expertise</description>
|
||||
<best_practice>
|
||||
Start with "You are Roo Code, a [specialist type]..."
|
||||
List specific areas of expertise
|
||||
Mention key technologies or methodologies
|
||||
</best_practice>
|
||||
</field>
|
||||
<field name="groups">
|
||||
<description>Tool groups the mode can access</description>
|
||||
<options>
|
||||
<option name="read">File reading and searching tools</option>
|
||||
<option name="edit">File editing tools (can be restricted by regex)</option>
|
||||
<option name="command">Command execution tools</option>
|
||||
<option name="browser">Browser interaction tools</option>
|
||||
<option name="mcp">MCP server tools</option>
|
||||
</options>
|
||||
</field>
|
||||
</required_fields>
|
||||
<recommended_fields>
|
||||
<field name="whenToUse">
|
||||
<description>Clear description for the Orchestrator</description>
|
||||
<best_practice>Explain specific scenarios and task types</best_practice>
|
||||
</field>
|
||||
</recommended_fields>
|
||||
<important_note>
|
||||
Do not include customInstructions in the .roomodes configuration.
|
||||
All detailed instructions should be placed in XML files within
|
||||
the .roo/rules-[mode-slug]/ directory instead.
|
||||
</important_note>
|
||||
</step>
|
||||
|
||||
<step number="4a">
|
||||
<title>Implement File Restrictions</title>
|
||||
<description>
|
||||
Configure appropriate file access permissions
|
||||
</description>
|
||||
<example>
|
||||
<comment>Restrict edit access to specific file types</comment>
|
||||
<code>
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: \.(md|txt|rst)$
|
||||
description: Documentation files only
|
||||
- command
|
||||
</code>
|
||||
</example>
|
||||
<guidelines>
|
||||
<guideline>Use regex patterns to limit file editing scope</guideline>
|
||||
<guideline>Provide clear descriptions for restrictions</guideline>
|
||||
<guideline>Consider the principle of least privilege</guideline>
|
||||
</guidelines>
|
||||
</step>
|
||||
|
||||
<step number="5a">
|
||||
<title>Create XML Instruction Files</title>
|
||||
<description>
|
||||
Design structured instruction files in .roo/rules-[mode-slug]/
|
||||
</description>
|
||||
<file_structure>
|
||||
<file name="1_workflow.xml">Main workflow and step-by-step processes</file>
|
||||
<file name="2_best_practices.xml">Guidelines and conventions</file>
|
||||
<file name="3_common_patterns.xml">Reusable code patterns and examples</file>
|
||||
<file name="4_tool_usage.xml">Specific tool usage instructions</file>
|
||||
<file name="5_examples.xml">Complete workflow examples</file>
|
||||
</file_structure>
|
||||
<xml_best_practices>
|
||||
<practice>Use semantic tag names that describe content</practice>
|
||||
<practice>Nest tags hierarchically for better organization</practice>
|
||||
<practice>Include code examples in CDATA sections when needed</practice>
|
||||
<practice>Add comments to explain complex sections</practice>
|
||||
</xml_best_practices>
|
||||
</step>
|
||||
</branch>
|
||||
|
||||
<branch name="edit_existing_mode">
|
||||
<step number="2b">
|
||||
<title>Immerse in Existing Mode</title>
|
||||
<description>
|
||||
Fully understand the existing mode before making any changes
|
||||
</description>
|
||||
<actions>
|
||||
<action>Locate and read the mode configuration in .roomodes</action>
|
||||
<action>Read all XML instruction files in .roo/rules-[mode-slug]/</action>
|
||||
<action>Analyze the mode's current capabilities and limitations</action>
|
||||
<action>Understand the mode's role in the broader ecosystem</action>
|
||||
</actions>
|
||||
<questions_to_ask>
|
||||
<ask_followup_question>
|
||||
<question>What specific aspects of the mode would you like to change or enhance?</question>
|
||||
<follow_up>
|
||||
<suggest>Add new capabilities or tool permissions</suggest>
|
||||
<suggest>Fix issues with current workflows or instructions</suggest>
|
||||
<suggest>Improve the mode's roleDefinition or whenToUse description</suggest>
|
||||
<suggest>Enhance XML instructions for better clarity</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</questions_to_ask>
|
||||
</step>
|
||||
|
||||
<step number="3b">
|
||||
<title>Analyze Change Impact</title>
|
||||
<description>
|
||||
Understand how proposed changes will affect the mode
|
||||
</description>
|
||||
<analysis_areas>
|
||||
<area>Compatibility with existing workflows</area>
|
||||
<area>Impact on file permissions and tool access</area>
|
||||
<area>Consistency with mode's core purpose</area>
|
||||
<area>Integration with other modes</area>
|
||||
</analysis_areas>
|
||||
<validation_questions>
|
||||
<ask_followup_question>
|
||||
<question>I've analyzed the existing mode. Here's what I understand about your requested changes. Is this correct?</question>
|
||||
<follow_up>
|
||||
<suggest>Yes, that's exactly what I want to change</suggest>
|
||||
<suggest>Mostly correct, but let me clarify some details</suggest>
|
||||
<suggest>No, I meant something different</suggest>
|
||||
<suggest>I'd like to add additional changes</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</validation_questions>
|
||||
</step>
|
||||
|
||||
<step number="4b">
|
||||
<title>Plan Modifications</title>
|
||||
<description>
|
||||
Create a detailed plan for modifying the mode
|
||||
</description>
|
||||
<planning_steps>
|
||||
<step>Identify which files need to be modified</step>
|
||||
<step>Determine if new XML instruction files are needed</step>
|
||||
<step>Check for potential conflicts or contradictions</step>
|
||||
<step>Plan the order of changes for minimal disruption</step>
|
||||
</planning_steps>
|
||||
</step>
|
||||
|
||||
<step number="5b">
|
||||
<title>Implement Changes</title>
|
||||
<description>
|
||||
Apply the planned modifications to the mode
|
||||
</description>
|
||||
<implementation_order>
|
||||
<change>Update .roomodes configuration if needed</change>
|
||||
<change>Modify existing XML instruction files</change>
|
||||
<change>Create new XML instruction files if required</change>
|
||||
<change>Update examples and documentation</change>
|
||||
</implementation_order>
|
||||
</step>
|
||||
</branch>
|
||||
</workflow_branches>
|
||||
|
||||
<validation_and_cohesion>
|
||||
<step number="6">
|
||||
<title>Validate Cohesion and Consistency</title>
|
||||
<description>
|
||||
Ensure all changes are cohesive and don't contradict each other
|
||||
</description>
|
||||
<validation_checks>
|
||||
<check type="configuration">
|
||||
<item>Mode slug follows naming conventions</item>
|
||||
<item>File restrictions align with mode purpose</item>
|
||||
<item>Tool permissions are appropriate</item>
|
||||
<item>whenToUse clearly differentiates from other modes</item>
|
||||
</check>
|
||||
<check type="instructions">
|
||||
<item>All XML files follow consistent structure</item>
|
||||
<item>No contradicting instructions between files</item>
|
||||
<item>Examples align with stated workflows</item>
|
||||
<item>Tool usage matches granted permissions</item>
|
||||
</check>
|
||||
<check type="integration">
|
||||
<item>Mode integrates well with Orchestrator</item>
|
||||
<item>Clear boundaries with other modes</item>
|
||||
<item>Handoff points are well-defined</item>
|
||||
</check>
|
||||
</validation_checks>
|
||||
<cohesion_questions>
|
||||
<ask_followup_question>
|
||||
<question>I've completed the validation checks. Would you like me to review any specific aspect in more detail?</question>
|
||||
<follow_up>
|
||||
<suggest>Review the file permission patterns</suggest>
|
||||
<suggest>Check for workflow contradictions</suggest>
|
||||
<suggest>Verify integration with other modes</suggest>
|
||||
<suggest>Everything looks good, proceed to testing</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</cohesion_questions>
|
||||
</step>
|
||||
|
||||
<step number="7">
|
||||
<title>Test and Refine</title>
|
||||
<description>
|
||||
Verify the mode works as intended
|
||||
</description>
|
||||
<checklist>
|
||||
<item>Mode appears in the mode list</item>
|
||||
<item>File restrictions work correctly</item>
|
||||
<item>Instructions are clear and actionable</item>
|
||||
<item>Mode integrates well with Orchestrator</item>
|
||||
<item>All examples are accurate and helpful</item>
|
||||
<item>Changes don't break existing functionality (for edits)</item>
|
||||
<item>New capabilities work as expected</item>
|
||||
</checklist>
|
||||
</step>
|
||||
</validation_and_cohesion>
|
||||
|
||||
<quick_reference>
|
||||
<command>Create mode in .roomodes for project-specific modes</command>
|
||||
<command>Create mode in global custom_modes.yaml for system-wide modes</command>
|
||||
<command>Use list_files to verify .roo folder structure</command>
|
||||
<command>Test file regex patterns with search_files</command>
|
||||
<command>Use codebase_search to find existing mode implementations</command>
|
||||
<command>Read all XML files in a mode directory to understand its structure</command>
|
||||
<command>Always validate changes for cohesion and consistency</command>
|
||||
</quick_reference>
|
||||
</mode_management_workflow>
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
<xml_structuring_best_practices>
|
||||
<overview>
|
||||
XML tags help Claude parse prompts more accurately, leading to higher-quality outputs.
|
||||
This guide covers best practices for structuring mode instructions using XML.
|
||||
</overview>
|
||||
|
||||
<why_use_xml_tags>
|
||||
<benefit type="clarity">
|
||||
Clearly separate different parts of your instructions and ensure well-structured content
|
||||
</benefit>
|
||||
<benefit type="accuracy">
|
||||
Reduce errors caused by Claude misinterpreting parts of your instructions
|
||||
</benefit>
|
||||
<benefit type="flexibility">
|
||||
Easily find, add, remove, or modify parts of instructions without rewriting everything
|
||||
</benefit>
|
||||
<benefit type="parseability">
|
||||
Having Claude use XML tags in its output makes it easier to extract specific parts of responses
|
||||
</benefit>
|
||||
</why_use_xml_tags>
|
||||
|
||||
<core_principles>
|
||||
<principle name="consistency">
|
||||
<description>Use the same tag names throughout your instructions</description>
|
||||
<example>
|
||||
Always use <step> for workflow steps, not sometimes <action> or <task>
|
||||
</example>
|
||||
</principle>
|
||||
|
||||
<principle name="semantic_naming">
|
||||
<description>Tag names should clearly describe their content</description>
|
||||
<good_examples>
|
||||
<tag>detailed_steps</tag>
|
||||
<tag>error_handling</tag>
|
||||
<tag>validation_rules</tag>
|
||||
</good_examples>
|
||||
<bad_examples>
|
||||
<tag>stuff</tag>
|
||||
<tag>misc</tag>
|
||||
<tag>data1</tag>
|
||||
</bad_examples>
|
||||
</principle>
|
||||
|
||||
<principle name="hierarchical_nesting">
|
||||
<description>Nest tags to show relationships and structure</description>
|
||||
<example>
|
||||
<workflow>
|
||||
<phase name="preparation">
|
||||
<step>Gather requirements</step>
|
||||
<step>Validate inputs</step>
|
||||
</phase>
|
||||
<phase name="execution">
|
||||
<step>Process data</step>
|
||||
<step>Generate output</step>
|
||||
</phase>
|
||||
</workflow>
|
||||
</example>
|
||||
</principle>
|
||||
</core_principles>
|
||||
|
||||
<common_tag_patterns>
|
||||
<pattern name="workflow_structure">
|
||||
<usage>For step-by-step processes</usage>
|
||||
<template><![CDATA[
|
||||
<workflow>
|
||||
<overview>High-level description</overview>
|
||||
<prerequisites>
|
||||
<prerequisite>Required condition 1</prerequisite>
|
||||
<prerequisite>Required condition 2</prerequisite>
|
||||
</prerequisites>
|
||||
<steps>
|
||||
<step number="1">
|
||||
<title>Step Title</title>
|
||||
<description>What this step accomplishes</description>
|
||||
<actions>
|
||||
<action>Specific action to take</action>
|
||||
</actions>
|
||||
<validation>How to verify success</validation>
|
||||
</step>
|
||||
</steps>
|
||||
</workflow>
|
||||
]]></template>
|
||||
</pattern>
|
||||
|
||||
<pattern name="examples_structure">
|
||||
<usage>For providing code examples and demonstrations</usage>
|
||||
<template><![CDATA[
|
||||
<examples>
|
||||
<example name="descriptive_name">
|
||||
<description>What this example demonstrates</description>
|
||||
<context>When to use this approach</context>
|
||||
<code language="typescript">
|
||||
// Your code example here
|
||||
</code>
|
||||
<explanation>
|
||||
Key points about the implementation
|
||||
</explanation>
|
||||
</example>
|
||||
</examples>
|
||||
]]></template>
|
||||
</pattern>
|
||||
|
||||
<pattern name="guidelines_structure">
|
||||
<usage>For rules and best practices</usage>
|
||||
<template><![CDATA[
|
||||
<guidelines category="category_name">
|
||||
<guideline priority="high">
|
||||
<rule>The specific rule or guideline</rule>
|
||||
<rationale>Why this is important</rationale>
|
||||
<exceptions>When this doesn't apply</exceptions>
|
||||
</guideline>
|
||||
</guidelines>
|
||||
]]></template>
|
||||
</pattern>
|
||||
|
||||
<pattern name="tool_usage_structure">
|
||||
<usage>For documenting how to use specific tools</usage>
|
||||
<template><![CDATA[
|
||||
<tool_usage tool="tool_name">
|
||||
<purpose>What this tool accomplishes</purpose>
|
||||
<when_to_use>Specific scenarios for this tool</when_to_use>
|
||||
<syntax>
|
||||
<command>The exact command format</command>
|
||||
<parameters>
|
||||
<parameter name="param1" required="true">
|
||||
<description>What this parameter does</description>
|
||||
<type>string|number|boolean</type>
|
||||
<example>example_value</example>
|
||||
</parameter>
|
||||
</parameters>
|
||||
</syntax>
|
||||
<examples>
|
||||
<example scenario="common_use_case">
|
||||
<code>Actual usage example</code>
|
||||
<output>Expected output</output>
|
||||
</example>
|
||||
</examples>
|
||||
</tool_usage>
|
||||
]]></template>
|
||||
</pattern>
|
||||
</common_tag_patterns>
|
||||
|
||||
<formatting_guidelines>
|
||||
<guideline name="indentation">
|
||||
Use consistent indentation (2 or 4 spaces) for nested elements
|
||||
</guideline>
|
||||
<guideline name="line_breaks">
|
||||
Add line breaks between major sections for readability
|
||||
</guideline>
|
||||
<guideline name="comments">
|
||||
Use XML comments <!-- like this --> to explain complex sections
|
||||
</guideline>
|
||||
<guideline name="cdata_sections">
|
||||
Use CDATA for code blocks or content with special characters:
|
||||
<![CDATA[<code><![CDATA[your code here]]></code>]]>
|
||||
</guideline>
|
||||
<guideline name="attributes_vs_elements">
|
||||
Use attributes for metadata, elements for content:
|
||||
<example type="good">
|
||||
<step number="1" priority="high">
|
||||
<description>The actual step content</description>
|
||||
</step>
|
||||
</example>
|
||||
</guideline>
|
||||
</formatting_guidelines>
|
||||
|
||||
<anti_patterns>
|
||||
<anti_pattern name="flat_structure">
|
||||
<description>Avoid completely flat structures without hierarchy</description>
|
||||
<bad><![CDATA[
|
||||
<instructions>
|
||||
<item1>Do this</item1>
|
||||
<item2>Then this</item2>
|
||||
<item3>Finally this</item3>
|
||||
</instructions>
|
||||
]]></bad>
|
||||
<good><![CDATA[
|
||||
<instructions>
|
||||
<steps>
|
||||
<step order="1">Do this</step>
|
||||
<step order="2">Then this</step>
|
||||
<step order="3">Finally this</step>
|
||||
</steps>
|
||||
</instructions>
|
||||
]]></good>
|
||||
</anti_pattern>
|
||||
|
||||
<anti_pattern name="inconsistent_naming">
|
||||
<description>Don't mix naming conventions</description>
|
||||
<bad>
|
||||
Mixing camelCase, snake_case, and kebab-case in tag names
|
||||
</bad>
|
||||
<good>
|
||||
Pick one convention (preferably snake_case for XML) and stick to it
|
||||
</good>
|
||||
</anti_pattern>
|
||||
|
||||
<anti_pattern name="overly_generic_tags">
|
||||
<description>Avoid tags that don't convey meaning</description>
|
||||
<bad>data, info, stuff, thing, item</bad>
|
||||
<good>user_input, validation_result, error_message, configuration</good>
|
||||
</anti_pattern>
|
||||
</anti_patterns>
|
||||
|
||||
<integration_tips>
|
||||
<tip>
|
||||
Reference XML content in instructions:
|
||||
"Using the workflow defined in <workflow> tags..."
|
||||
</tip>
|
||||
<tip>
|
||||
Combine XML structure with other techniques like multishot prompting
|
||||
</tip>
|
||||
<tip>
|
||||
Use XML tags in expected outputs to make parsing easier
|
||||
</tip>
|
||||
<tip>
|
||||
Create reusable XML templates for common patterns
|
||||
</tip>
|
||||
</integration_tips>
|
||||
</xml_structuring_best_practices>
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
<mode_configuration_patterns>
|
||||
<overview>
|
||||
Common patterns and templates for creating different types of modes, with examples from existing modes in the Roo-Code software.
|
||||
</overview>
|
||||
|
||||
<mode_types>
|
||||
<type name="specialist_mode">
|
||||
<description>
|
||||
Modes focused on specific technical domains or tasks
|
||||
</description>
|
||||
<characteristics>
|
||||
<characteristic>Deep expertise in a particular area</characteristic>
|
||||
<characteristic>Restricted file access based on domain</characteristic>
|
||||
<characteristic>Specialized tool usage patterns</characteristic>
|
||||
</characteristics>
|
||||
<example_template><![CDATA[
|
||||
- slug: api-specialist
|
||||
name: 🔌 API Specialist
|
||||
roleDefinition: >-
|
||||
You are Roo Code, an API development specialist with expertise in:
|
||||
- RESTful API design and implementation
|
||||
- GraphQL schema design
|
||||
- API documentation with OpenAPI/Swagger
|
||||
- Authentication and authorization patterns
|
||||
- Rate limiting and caching strategies
|
||||
- API versioning and deprecation
|
||||
|
||||
You ensure APIs are:
|
||||
- Well-documented and discoverable
|
||||
- Following REST principles or GraphQL best practices
|
||||
- Secure and performant
|
||||
- Properly versioned and maintainable
|
||||
whenToUse: >-
|
||||
Use this mode when designing, implementing, or refactoring APIs.
|
||||
This includes creating new endpoints, updating API documentation,
|
||||
implementing authentication, or optimizing API performance.
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: (api/.*\.(ts|js)|.*\.openapi\.yaml|.*\.graphql|docs/api/.*)$
|
||||
description: API implementation files, OpenAPI specs, and API documentation
|
||||
- command
|
||||
- mcp
|
||||
]]></example_template>
|
||||
</type>
|
||||
|
||||
<type name="workflow_mode">
|
||||
<description>
|
||||
Modes that guide users through multi-step processes
|
||||
</description>
|
||||
<characteristics>
|
||||
<characteristic>Step-by-step workflow guidance</characteristic>
|
||||
<characteristic>Heavy use of ask_followup_question</characteristic>
|
||||
<characteristic>Process validation at each step</characteristic>
|
||||
</characteristics>
|
||||
<example_template><![CDATA[
|
||||
- slug: migration-guide
|
||||
name: 🔄 Migration Guide
|
||||
roleDefinition: >-
|
||||
You are Roo Code, a migration specialist who guides users through
|
||||
complex migration processes:
|
||||
- Database schema migrations
|
||||
- Framework version upgrades
|
||||
- API version migrations
|
||||
- Dependency updates
|
||||
- Breaking change resolutions
|
||||
|
||||
You provide:
|
||||
- Step-by-step migration plans
|
||||
- Automated migration scripts
|
||||
- Rollback strategies
|
||||
- Testing approaches for migrations
|
||||
whenToUse: >-
|
||||
Use this mode when performing any kind of migration or upgrade.
|
||||
This mode will analyze the current state, plan the migration,
|
||||
and guide you through each step with validation.
|
||||
groups:
|
||||
- read
|
||||
- edit
|
||||
- command
|
||||
]]></example_template>
|
||||
</type>
|
||||
|
||||
<type name="analysis_mode">
|
||||
<description>
|
||||
Modes focused on code analysis and reporting
|
||||
</description>
|
||||
<characteristics>
|
||||
<characteristic>Read-heavy operations</characteristic>
|
||||
<characteristic>Limited or no edit permissions</characteristic>
|
||||
<characteristic>Comprehensive reporting outputs</characteristic>
|
||||
</characteristics>
|
||||
<example_template><![CDATA[
|
||||
- slug: security-auditor
|
||||
name: 🔒 Security Auditor
|
||||
roleDefinition: >-
|
||||
You are Roo Code, a security analysis specialist focused on:
|
||||
- Identifying security vulnerabilities
|
||||
- Analyzing authentication and authorization
|
||||
- Reviewing data validation and sanitization
|
||||
- Checking for common security anti-patterns
|
||||
- Evaluating dependency vulnerabilities
|
||||
- Assessing API security
|
||||
|
||||
You provide detailed security reports with:
|
||||
- Vulnerability severity ratings
|
||||
- Specific remediation steps
|
||||
- Security best practice recommendations
|
||||
whenToUse: >-
|
||||
Use this mode to perform security audits on codebases.
|
||||
This mode will analyze code for vulnerabilities, check
|
||||
dependencies, and provide actionable security recommendations.
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- - edit
|
||||
- fileRegex: (SECURITY\.md|\.github/security/.*|docs/security/.*)$
|
||||
description: Security documentation files only
|
||||
]]></example_template>
|
||||
</type>
|
||||
|
||||
<type name="creative_mode">
|
||||
<description>
|
||||
Modes for generating new content or features
|
||||
</description>
|
||||
<characteristics>
|
||||
<characteristic>Broad file creation permissions</characteristic>
|
||||
<characteristic>Template and boilerplate generation</characteristic>
|
||||
<characteristic>Interactive design process</characteristic>
|
||||
</characteristics>
|
||||
<example_template><![CDATA[
|
||||
- slug: component-designer
|
||||
name: 🎨 Component Designer
|
||||
roleDefinition: >-
|
||||
You are Roo Code, a UI component design specialist who creates:
|
||||
- Reusable React/Vue/Angular components
|
||||
- Component documentation and examples
|
||||
- Storybook stories
|
||||
- Unit tests for components
|
||||
- Accessibility-compliant interfaces
|
||||
|
||||
You follow design system principles and ensure components are:
|
||||
- Highly reusable and composable
|
||||
- Well-documented with examples
|
||||
- Fully tested
|
||||
- Accessible (WCAG compliant)
|
||||
- Performance optimized
|
||||
whenToUse: >-
|
||||
Use this mode when creating new UI components or refactoring
|
||||
existing ones. This mode helps design component APIs, implement
|
||||
the components, and create comprehensive documentation.
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: (components/.*|stories/.*|__tests__/.*\.test\.(tsx?|jsx?))$
|
||||
description: Component files, stories, and component tests
|
||||
- browser
|
||||
- command
|
||||
]]></example_template>
|
||||
</type>
|
||||
</mode_types>
|
||||
|
||||
<permission_patterns>
|
||||
<pattern name="documentation_only">
|
||||
<description>For modes that only work with documentation</description>
|
||||
<configuration><![CDATA[
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: \.(md|mdx|rst|txt)$
|
||||
description: Documentation files only
|
||||
]]></configuration>
|
||||
</pattern>
|
||||
|
||||
<pattern name="test_focused">
|
||||
<description>For modes that work with test files</description>
|
||||
<configuration><![CDATA[
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- - edit
|
||||
- fileRegex: (__tests__/.*|__mocks__/.*|.*\.test\.(ts|tsx|js|jsx)$|.*\.spec\.(ts|tsx|js|jsx)$)
|
||||
description: Test files and mocks
|
||||
]]></configuration>
|
||||
</pattern>
|
||||
|
||||
<pattern name="config_management">
|
||||
<description>For modes that manage configuration</description>
|
||||
<configuration><![CDATA[
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: (.*\.config\.(js|ts|json)|.*rc\.json|.*\.yaml|.*\.yml|\.env\.example)$
|
||||
description: Configuration files (not .env)
|
||||
]]></configuration>
|
||||
</pattern>
|
||||
|
||||
<pattern name="full_stack">
|
||||
<description>For modes that need broad access</description>
|
||||
<configuration><![CDATA[
|
||||
groups:
|
||||
- read
|
||||
- edit # No restrictions
|
||||
- command
|
||||
- browser
|
||||
- mcp
|
||||
]]></configuration>
|
||||
</pattern>
|
||||
</permission_patterns>
|
||||
|
||||
<naming_conventions>
|
||||
<convention category="slug">
|
||||
<rule>Use lowercase with hyphens</rule>
|
||||
<good>api-dev, test-writer, docs-manager</good>
|
||||
<bad>apiDev, test_writer, DocsManager</bad>
|
||||
</convention>
|
||||
|
||||
<convention category="name">
|
||||
<rule>Use title case with descriptive emoji</rule>
|
||||
<good>🔧 API Developer, 📝 Documentation Writer</good>
|
||||
<bad>api developer, DOCUMENTATION WRITER</bad>
|
||||
</convention>
|
||||
|
||||
<convention category="emoji_selection">
|
||||
<common_emojis>
|
||||
<emoji meaning="testing">🧪</emoji>
|
||||
<emoji meaning="documentation">📝</emoji>
|
||||
<emoji meaning="design">🎨</emoji>
|
||||
<emoji meaning="debugging">🪲</emoji>
|
||||
<emoji meaning="building">🏗️</emoji>
|
||||
<emoji meaning="security">🔒</emoji>
|
||||
<emoji meaning="api">🔌</emoji>
|
||||
<emoji meaning="database">🗄️</emoji>
|
||||
<emoji meaning="performance">⚡</emoji>
|
||||
<emoji meaning="configuration">⚙️</emoji>
|
||||
</common_emojis>
|
||||
</convention>
|
||||
</naming_conventions>
|
||||
|
||||
<integration_guidelines>
|
||||
<guideline name="orchestrator_compatibility">
|
||||
<description>Ensure whenToUse is clear for Orchestrator mode</description>
|
||||
<checklist>
|
||||
<item>Specify concrete task types the mode handles</item>
|
||||
<item>Include trigger keywords or phrases</item>
|
||||
<item>Differentiate from similar modes</item>
|
||||
<item>Mention specific file types or areas</item>
|
||||
</checklist>
|
||||
</guideline>
|
||||
|
||||
<guideline name="mode_boundaries">
|
||||
<description>Define clear boundaries between modes</description>
|
||||
<checklist>
|
||||
<item>Avoid overlapping responsibilities</item>
|
||||
<item>Make handoff points explicit</item>
|
||||
<item>Use switch_mode when appropriate</item>
|
||||
<item>Document mode interactions</item>
|
||||
</checklist>
|
||||
</guideline>
|
||||
</integration_guidelines>
|
||||
</mode_configuration_patterns>
|
||||
|
|
@ -1,367 +0,0 @@
|
|||
<instruction_file_templates>
|
||||
<overview>
|
||||
Templates and examples for creating XML instruction files that provide
|
||||
detailed guidance for each mode's behavior and workflows.
|
||||
</overview>
|
||||
|
||||
<file_organization>
|
||||
<principle>Number files to indicate execution order</principle>
|
||||
<principle>Use descriptive names that indicate content</principle>
|
||||
<principle>Keep related instructions together</principle>
|
||||
<standard_structure>
|
||||
<file>1_workflow.xml - Main workflow and processes</file>
|
||||
<file>2_best_practices.xml - Guidelines and conventions</file>
|
||||
<file>3_common_patterns.xml - Reusable code patterns</file>
|
||||
<file>4_tool_usage.xml - Specific tool instructions</file>
|
||||
<file>5_examples.xml - Complete workflow examples</file>
|
||||
<file>6_error_handling.xml - Error scenarios and recovery</file>
|
||||
<file>7_communication.xml - User interaction guidelines</file>
|
||||
</standard_structure>
|
||||
</file_organization>
|
||||
|
||||
<workflow_file_template>
|
||||
<description>Template for main workflow files (1_workflow.xml)</description>
|
||||
<template><![CDATA[
|
||||
<workflow_instructions>
|
||||
<mode_overview>
|
||||
Brief description of what this mode does and its primary purpose
|
||||
</mode_overview>
|
||||
|
||||
<initialization_steps>
|
||||
<step number="1">
|
||||
<action>Understand the user's request</action>
|
||||
<details>
|
||||
Parse the user's input to identify:
|
||||
- Primary objective
|
||||
- Specific requirements
|
||||
- Constraints or limitations
|
||||
</details>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<action>Gather necessary context</action>
|
||||
<tools>
|
||||
<tool>codebase_search - Find relevant existing code</tool>
|
||||
<tool>list_files - Understand project structure</tool>
|
||||
<tool>read_file - Examine specific implementations</tool>
|
||||
</tools>
|
||||
</step>
|
||||
</initialization_steps>
|
||||
|
||||
<main_workflow>
|
||||
<phase name="analysis">
|
||||
<description>Analyze the current state and requirements</description>
|
||||
<steps>
|
||||
<step>Identify affected components</step>
|
||||
<step>Assess impact of changes</step>
|
||||
<step>Plan implementation approach</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="implementation">
|
||||
<description>Execute the planned changes</description>
|
||||
<steps>
|
||||
<step>Create/modify necessary files</step>
|
||||
<step>Ensure consistency across codebase</step>
|
||||
<step>Add appropriate documentation</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="validation">
|
||||
<description>Verify the implementation</description>
|
||||
<steps>
|
||||
<step>Check for errors or inconsistencies</step>
|
||||
<step>Validate against requirements</step>
|
||||
<step>Ensure no regressions</step>
|
||||
</steps>
|
||||
</phase>
|
||||
</main_workflow>
|
||||
|
||||
<completion_criteria>
|
||||
<criterion>All requirements have been addressed</criterion>
|
||||
<criterion>Code follows project conventions</criterion>
|
||||
<criterion>Changes are properly documented</criterion>
|
||||
<criterion>No breaking changes introduced</criterion>
|
||||
</completion_criteria>
|
||||
</workflow_instructions>
|
||||
]]></template>
|
||||
</workflow_file_template>
|
||||
|
||||
<best_practices_template>
|
||||
<description>Template for best practices files (2_best_practices.xml)</description>
|
||||
<template><![CDATA[
|
||||
<best_practices>
|
||||
<general_principles>
|
||||
<principle priority="high">
|
||||
<name>Principle Name</name>
|
||||
<description>Detailed explanation of the principle</description>
|
||||
<rationale>Why this principle is important</rationale>
|
||||
<example>
|
||||
<scenario>When this applies</scenario>
|
||||
<good>Correct approach</good>
|
||||
<bad>What to avoid</bad>
|
||||
</example>
|
||||
</principle>
|
||||
</general_principles>
|
||||
|
||||
<code_conventions>
|
||||
<convention category="naming">
|
||||
<rule>Specific naming convention</rule>
|
||||
<examples>
|
||||
<good>goodExampleName</good>
|
||||
<bad>bad_example-name</bad>
|
||||
</examples>
|
||||
</convention>
|
||||
|
||||
<convention category="structure">
|
||||
<rule>How to structure code/files</rule>
|
||||
<template>
|
||||
// Example structure
|
||||
</template>
|
||||
</convention>
|
||||
</code_conventions>
|
||||
|
||||
<common_pitfalls>
|
||||
<pitfall>
|
||||
<description>Common mistake to avoid</description>
|
||||
<why_problematic>Explanation of issues it causes</why_problematic>
|
||||
<correct_approach>How to do it properly</correct_approach>
|
||||
</pitfall>
|
||||
</common_pitfalls>
|
||||
|
||||
<quality_checklist>
|
||||
<category name="before_starting">
|
||||
<item>Understand requirements fully</item>
|
||||
<item>Check existing implementations</item>
|
||||
</category>
|
||||
<category name="during_implementation">
|
||||
<item>Follow established patterns</item>
|
||||
<item>Write clear documentation</item>
|
||||
</category>
|
||||
<category name="before_completion">
|
||||
<item>Review all changes</item>
|
||||
<item>Verify requirements met</item>
|
||||
</category>
|
||||
</quality_checklist>
|
||||
</best_practices>
|
||||
]]></template>
|
||||
</best_practices_template>
|
||||
|
||||
<tool_usage_template>
|
||||
<description>Template for tool usage files (4_tool_usage.xml)</description>
|
||||
<template><![CDATA[
|
||||
<tool_usage_guide>
|
||||
<tool_priorities>
|
||||
<priority level="1">
|
||||
<tool>codebase_search</tool>
|
||||
<when>Always use first to find relevant code</when>
|
||||
<why>Semantic search finds functionality better than keywords</why>
|
||||
</priority>
|
||||
<priority level="2">
|
||||
<tool>read_file</tool>
|
||||
<when>After identifying files with codebase_search</when>
|
||||
<why>Get full context of implementations</why>
|
||||
</priority>
|
||||
</tool_priorities>
|
||||
|
||||
<tool_specific_guidance>
|
||||
<tool name="apply_diff">
|
||||
<best_practices>
|
||||
<practice>Always read file first to ensure exact content match</practice>
|
||||
<practice>Make multiple changes in one diff when possible</practice>
|
||||
<practice>Include line numbers for accuracy</practice>
|
||||
</best_practices>
|
||||
<example><![CDATA[
|
||||
<apply_diff>
|
||||
<path>src/config.ts</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
:start_line:10
|
||||
-------
|
||||
export const config = {
|
||||
apiUrl: 'http://localhost:3000',
|
||||
timeout: 5000
|
||||
};
|
||||
=======
|
||||
export const config = {
|
||||
apiUrl: process.env.API_URL || 'http://localhost:3000',
|
||||
timeout: parseInt(process.env.TIMEOUT || '5000'),
|
||||
retries: 3
|
||||
};
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
]]></example>
|
||||
</tool>
|
||||
|
||||
<tool name="ask_followup_question">
|
||||
<best_practices>
|
||||
<practice>Provide 2-4 specific, actionable suggestions</practice>
|
||||
<practice>Order suggestions by likelihood or importance</practice>
|
||||
<practice>Make suggestions complete (no placeholders)</practice>
|
||||
</best_practices>
|
||||
<example><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>Which database system should I configure for this project?</question>
|
||||
<follow_up>
|
||||
<suggest>PostgreSQL with the default configuration</suggest>
|
||||
<suggest>MySQL 8.0 with InnoDB storage engine</suggest>
|
||||
<suggest>SQLite for local development only</suggest>
|
||||
<suggest>MongoDB for document-based storage</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></example>
|
||||
</tool>
|
||||
</tool_specific_guidance>
|
||||
|
||||
<tool_combination_patterns>
|
||||
<pattern name="explore_then_modify">
|
||||
<sequence>
|
||||
<step>codebase_search - Find relevant files</step>
|
||||
<step>list_code_definition_names - Understand structure</step>
|
||||
<step>read_file - Get full context</step>
|
||||
<step>apply_diff or write_to_file - Make changes</step>
|
||||
</sequence>
|
||||
</pattern>
|
||||
|
||||
<pattern name="verify_then_proceed">
|
||||
<sequence>
|
||||
<step>list_files - Check file exists</step>
|
||||
<step>read_file - Verify current content</step>
|
||||
<step>ask_followup_question - Confirm approach</step>
|
||||
<step>apply_diff - Implement changes</step>
|
||||
</sequence>
|
||||
</pattern>
|
||||
</tool_combination_patterns>
|
||||
</tool_usage_guide>
|
||||
]]></template>
|
||||
</tool_usage_template>
|
||||
|
||||
<examples_file_template>
|
||||
<description>Template for example files (5_examples.xml)</description>
|
||||
<template><![CDATA[
|
||||
<complete_examples>
|
||||
<example name="descriptive_example_name">
|
||||
<scenario>
|
||||
Detailed description of the use case this example covers
|
||||
</scenario>
|
||||
|
||||
<user_request>
|
||||
The initial request from the user
|
||||
</user_request>
|
||||
|
||||
<workflow>
|
||||
<step number="1">
|
||||
<description>First step description</description>
|
||||
<tool_use><![CDATA[
|
||||
<codebase_search>
|
||||
<query>search query here</query>
|
||||
</codebase_search>
|
||||
]]></tool_use>
|
||||
<expected_outcome>What we learn from this step</expected_outcome>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<description>Second step description</description>
|
||||
<tool_use><![CDATA[
|
||||
<read_file>
|
||||
<path>path/to/file.ts</path>
|
||||
</read_file>
|
||||
]]></tool_use>
|
||||
<analysis>How we interpret the results</analysis>
|
||||
</step>
|
||||
|
||||
<step number="3">
|
||||
<description>Implementation step</description>
|
||||
<tool_use><![CDATA[
|
||||
<apply_diff>
|
||||
<path>path/to/file.ts</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
:start_line:1
|
||||
-------
|
||||
original content
|
||||
=======
|
||||
new content
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
</workflow>
|
||||
|
||||
<completion><![CDATA[
|
||||
<attempt_completion>
|
||||
<result>
|
||||
Summary of what was accomplished and how it addresses the user's request
|
||||
</result>
|
||||
</attempt_completion>
|
||||
]]></completion>
|
||||
|
||||
<key_takeaways>
|
||||
<takeaway>Important lesson from this example</takeaway>
|
||||
<takeaway>Pattern that can be reused</takeaway>
|
||||
</key_takeaways>
|
||||
</example>
|
||||
</complete_examples>
|
||||
]]></template>
|
||||
</examples_file_template>
|
||||
|
||||
<communication_template>
|
||||
<description>Template for communication guidelines (7_communication.xml)</description>
|
||||
<template><![CDATA[
|
||||
<communication_guidelines>
|
||||
<tone_and_style>
|
||||
<principle>Be direct and technical, not conversational</principle>
|
||||
<principle>Focus on actions taken and results achieved</principle>
|
||||
<avoid>
|
||||
<phrase>Great! I'll help you with that...</phrase>
|
||||
<phrase>Certainly! Let me...</phrase>
|
||||
<phrase>Sure thing!</phrase>
|
||||
</avoid>
|
||||
<prefer>
|
||||
<phrase>I'll analyze the codebase to...</phrase>
|
||||
<phrase>Implementing the requested changes...</phrase>
|
||||
<phrase>The analysis shows...</phrase>
|
||||
</prefer>
|
||||
</tone_and_style>
|
||||
|
||||
<user_interaction>
|
||||
<when_to_ask_questions>
|
||||
<scenario>Missing critical information</scenario>
|
||||
<scenario>Multiple valid approaches exist</scenario>
|
||||
<scenario>Potential breaking changes</scenario>
|
||||
</when_to_ask_questions>
|
||||
|
||||
<question_format>
|
||||
<guideline>Be specific about what you need</guideline>
|
||||
<guideline>Provide actionable options</guideline>
|
||||
<guideline>Explain implications of choices</guideline>
|
||||
</question_format>
|
||||
</user_interaction>
|
||||
|
||||
<progress_updates>
|
||||
<when>During long-running operations</when>
|
||||
<format>
|
||||
<update>Analyzing [X] files for [purpose]...</update>
|
||||
<update>Implementing [feature] in [location]...</update>
|
||||
<update>Validating changes against [criteria]...</update>
|
||||
</format>
|
||||
</progress_updates>
|
||||
|
||||
<completion_messages>
|
||||
<structure>
|
||||
<element>What was accomplished</element>
|
||||
<element>Key changes made</element>
|
||||
<element>Any important notes or warnings</element>
|
||||
</structure>
|
||||
<avoid>
|
||||
<element>Questions at the end</element>
|
||||
<element>Offers for further assistance</element>
|
||||
<element>Conversational closings</element>
|
||||
</avoid>
|
||||
</completion_messages>
|
||||
</communication_guidelines>
|
||||
]]></template>
|
||||
</communication_template>
|
||||
</instruction_file_templates>
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
<complete_examples>
|
||||
<example name="mode_editing_enhancement">
|
||||
<scenario>
|
||||
Edit the test mode to add support for performance testing and benchmarking
|
||||
</scenario>
|
||||
|
||||
<user_request>
|
||||
I want to edit the test mode to add benchmark testing capabilities
|
||||
</user_request>
|
||||
|
||||
<workflow>
|
||||
<step number="1">
|
||||
<description>Determine user intent and clarify scope</description>
|
||||
<tool_use><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I understand you want to edit the test mode to add performance testing capabilities. Before we proceed, let me confirm the scope of changes. What specific performance testing features do you need?</question>
|
||||
<follow_up>
|
||||
<suggest>Add benchmark testing with Vitest's bench API</suggest>
|
||||
<suggest>Include performance profiling and memory usage tracking</suggest>
|
||||
<suggest>Add load testing capabilities for API endpoints</suggest>
|
||||
<suggest>All of the above - comprehensive performance testing suite</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></tool_use>
|
||||
<expected_outcome>User selected: Add benchmark testing with Vitest's bench API</expected_outcome>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<description>Immerse in existing test mode configuration</description>
|
||||
<tool_use><![CDATA[
|
||||
<read_file>
|
||||
<path>.roomodes</path>
|
||||
</read_file>
|
||||
]]></tool_use>
|
||||
<analysis>Found test mode configuration with current roleDefinition and file restrictions</analysis>
|
||||
</step>
|
||||
|
||||
<step number="3">
|
||||
<description>Explore existing test mode instructions</description>
|
||||
<tool_use><![CDATA[
|
||||
<list_files>
|
||||
<path>.roo/rules-test</path>
|
||||
<recursive>true</recursive>
|
||||
</list_files>
|
||||
]]></tool_use>
|
||||
<tool_use><![CDATA[
|
||||
<read_file>
|
||||
<path>.roo/rules-test/1_workflow.xml</path>
|
||||
</read_file>
|
||||
]]></tool_use>
|
||||
<analysis>Understood current test workflows and patterns</analysis>
|
||||
</step>
|
||||
|
||||
<step number="4">
|
||||
<description>Validate changes for cohesion</description>
|
||||
<tool_use><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I've analyzed the test mode. Adding benchmark testing would require:
|
||||
1. Updating the roleDefinition to include performance testing expertise
|
||||
2. Modifying file restrictions to include .bench.ts files
|
||||
3. Adding new workflow instructions for benchmark creation
|
||||
4. Including Vitest bench API examples
|
||||
|
||||
This aligns well with the existing testing focus. Should I proceed with all these changes?</question>
|
||||
<follow_up>
|
||||
<suggest>Yes, implement all the changes as described</suggest>
|
||||
<suggest>Start with just the roleDefinition and file restrictions</suggest>
|
||||
<suggest>Focus only on the workflow instructions first</suggest>
|
||||
<suggest>Let me review the specific changes before proceeding</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></tool_use>
|
||||
<expected_outcome>User selected: Yes, implement all the changes as described</expected_outcome>
|
||||
</step>
|
||||
|
||||
<step number="5">
|
||||
<description>Update mode configuration in .roomodes</description>
|
||||
<tool_use><![CDATA[
|
||||
<apply_diff>
|
||||
<path>.roomodes</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
roleDefinition: |-
|
||||
You are Roo, a Vitest testing specialist with deep expertise in: - Writing and maintaining Vitest test suites - Test-driven development (TDD) practices - Mocking and stubbing with Vitest - Integration testing strategies - TypeScript testing patterns - Code coverage analysis - Test performance optimization
|
||||
=======
|
||||
roleDefinition: |-
|
||||
You are Roo, a Vitest testing specialist with deep expertise in: - Writing and maintaining Vitest test suites - Test-driven development (TDD) practices - Mocking and stubbing with Vitest - Integration testing strategies - TypeScript testing patterns - Code coverage analysis - Test performance optimization - Benchmark testing with Vitest bench API - Performance profiling and optimization - Memory usage analysis in tests
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
|
||||
<step number="6">
|
||||
<description>Update file restrictions to include benchmark files</description>
|
||||
<tool_use><![CDATA[
|
||||
<apply_diff>
|
||||
<path>.roomodes</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
- fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$)
|
||||
description: Test files, mocks, and Vitest configuration
|
||||
=======
|
||||
- fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|\.bench\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$)
|
||||
description: Test files, benchmarks, mocks, and Vitest configuration
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
|
||||
<step number="7">
|
||||
<description>Create new benchmark workflow instructions</description>
|
||||
<tool_use><![CDATA[
|
||||
<write_to_file>
|
||||
<path>.roo/rules-test/5_benchmark_testing.xml</path>
|
||||
<content>
|
||||
<benchmark_testing_guide>
|
||||
<overview>
|
||||
Guidelines for creating and maintaining performance benchmarks using Vitest bench API
|
||||
</overview>
|
||||
|
||||
<benchmark_patterns>
|
||||
<pattern name="basic_benchmark">
|
||||
<description>Basic benchmark structure</description>
|
||||
<example><![CDATA[
|
||||
import { bench, describe } from 'vitest';
|
||||
|
||||
describe('Array operations', () => {
|
||||
bench('Array.push', () => {
|
||||
const arr = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
bench('Array spread', () => {
|
||||
let arr = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
arr = [...arr, i];
|
||||
}
|
||||
});
|
||||
});
|
||||
]]></example>
|
||||
</pattern>
|
||||
</benchmark_patterns>
|
||||
|
||||
<best_practices>
|
||||
<practice>Use meaningful benchmark names that describe what's being measured</practice>
|
||||
<practice>Include baseline comparisons when optimizing code</practice>
|
||||
<practice>Run benchmarks in isolation to avoid interference</practice>
|
||||
<practice>Document performance expectations and thresholds</practice>
|
||||
</best_practices>
|
||||
</benchmark_testing_guide>
|
||||
</content>
|
||||
<line_count>38</line_count>
|
||||
</write_to_file>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
|
||||
<step number="8">
|
||||
<description>Validate all changes for consistency</description>
|
||||
<tool_use><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I've completed all the changes to add benchmark testing capabilities to the test mode:
|
||||
|
||||
1. ✓ Updated roleDefinition to include benchmark expertise
|
||||
2. ✓ Modified file restrictions to include .bench.ts files
|
||||
3. ✓ Created new benchmark workflow instructions
|
||||
4. ✓ Added Vitest bench API examples
|
||||
|
||||
All changes are cohesive and align with the existing test mode structure. Would you like me to test any specific aspect of these changes?</question>
|
||||
<follow_up>
|
||||
<suggest>Test creating a new benchmark file to verify file permissions</suggest>
|
||||
<suggest>Review the complete changes one more time</suggest>
|
||||
<suggest>Everything looks good, the mode is ready</suggest>
|
||||
<suggest>Create a simple benchmark example to test</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></tool_use>
|
||||
</step>
|
||||
</workflow>
|
||||
|
||||
<completion><![CDATA[
|
||||
<attempt_completion>
|
||||
<result>
|
||||
I've successfully enhanced the test mode with benchmark testing capabilities:
|
||||
|
||||
1. Updated the roleDefinition to include:
|
||||
- Benchmark testing with Vitest bench API
|
||||
- Performance profiling and optimization
|
||||
- Memory usage analysis in tests
|
||||
|
||||
2. Modified file restrictions to include .bench.ts files, allowing the mode to create and edit benchmark files
|
||||
|
||||
3. Created new workflow instructions at .roo/rules-test/5_benchmark_testing.xml with:
|
||||
- Basic benchmark patterns
|
||||
- Best practices for performance testing
|
||||
- Example code using Vitest's bench API
|
||||
|
||||
All changes have been validated for cohesion and consistency with the existing test mode structure. The mode now supports comprehensive performance testing alongside its existing unit and integration testing capabilities.
|
||||
</result>
|
||||
</attempt_completion>
|
||||
]]></completion>
|
||||
|
||||
<key_takeaways>
|
||||
<takeaway>Always immerse yourself in the existing mode before making changes</takeaway>
|
||||
<takeaway>Use ask_followup_question aggressively to clarify scope and validate changes</takeaway>
|
||||
<takeaway>Validate all changes for cohesion and consistency</takeaway>
|
||||
<takeaway>Update all relevant parts: configuration, file restrictions, and instructions</takeaway>
|
||||
<takeaway>Test changes to ensure they work as expected</takeaway>
|
||||
</key_takeaways>
|
||||
</example>
|
||||
</complete_examples>
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
<mode_testing_validation>
|
||||
<overview>
|
||||
Guidelines for testing and validating newly created modes to ensure they function correctly and integrate well with the Roo Code ecosystem.
|
||||
</overview>
|
||||
|
||||
<validation_checklist>
|
||||
<category name="configuration_validation">
|
||||
<item priority="critical">
|
||||
<check>Mode slug is unique and follows naming conventions</check>
|
||||
<validation>No spaces, lowercase, hyphens only</validation>
|
||||
</item>
|
||||
<item priority="critical">
|
||||
<check>All required fields are present and non-empty</check>
|
||||
<fields>slug, name, roleDefinition, groups</fields>
|
||||
</item>
|
||||
<item priority="critical">
|
||||
<check>No customInstructions field in .roomodes</check>
|
||||
<validation>All instructions must be in XML files in .roo/rules-[slug]/</validation>
|
||||
</item>
|
||||
<item priority="high">
|
||||
<check>File restrictions use valid regex patterns</check>
|
||||
<test_method><![CDATA[
|
||||
<search_files>
|
||||
<path>.</path>
|
||||
<regex>your_file_regex_here</regex>
|
||||
</search_files>
|
||||
]]></test_method>
|
||||
</item>
|
||||
<item priority="high">
|
||||
<check>whenToUse clearly differentiates from other modes</check>
|
||||
<validation>Compare with existing mode descriptions</validation>
|
||||
</item>
|
||||
</category>
|
||||
|
||||
<category name="instruction_validation">
|
||||
<item>
|
||||
<check>XML files are well-formed and valid</check>
|
||||
<validation>No syntax errors, proper closing tags</validation>
|
||||
</item>
|
||||
<item>
|
||||
<check>Instructions follow XML best practices</check>
|
||||
<validation>Semantic tag names, proper nesting</validation>
|
||||
</item>
|
||||
<item>
|
||||
<check>Examples use correct tool syntax</check>
|
||||
<validation>Tool parameters match current API</validation>
|
||||
</item>
|
||||
<item>
|
||||
<check>File paths in examples are consistent</check>
|
||||
<validation>Use project-relative paths</validation>
|
||||
</item>
|
||||
</category>
|
||||
|
||||
<category name="functional_testing">
|
||||
<item>
|
||||
<check>Mode appears in mode list</check>
|
||||
<test>Switch to the new mode and verify it loads</test>
|
||||
</item>
|
||||
<item>
|
||||
<check>Tool permissions work as expected</check>
|
||||
<test>Try using each tool group and verify access</test>
|
||||
</item>
|
||||
<item>
|
||||
<check>File restrictions are enforced</check>
|
||||
<test>Attempt to edit allowed and restricted files</test>
|
||||
</item>
|
||||
<item>
|
||||
<check>Mode handles edge cases gracefully</check>
|
||||
<test>Test with minimal input, errors, edge cases</test>
|
||||
</item>
|
||||
</category>
|
||||
</validation_checklist>
|
||||
|
||||
<testing_workflow>
|
||||
<step number="1">
|
||||
<title>Configuration Testing</title>
|
||||
<actions>
|
||||
<action>Verify mode appears in available modes list</action>
|
||||
<action>Check that mode metadata displays correctly</action>
|
||||
<action>Confirm mode can be activated</action>
|
||||
</actions>
|
||||
<verification><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I've created the mode configuration. Can you see the new mode in your mode list?</question>
|
||||
<follow_up>
|
||||
<suggest>Yes, I can see the new mode and switch to it</suggest>
|
||||
<suggest>No, the mode doesn't appear in the list</suggest>
|
||||
<suggest>The mode appears but has errors when switching</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></verification>
|
||||
</step>
|
||||
|
||||
<step number="2">
|
||||
<title>Permission Testing</title>
|
||||
<test_cases>
|
||||
<test case="read_permissions">
|
||||
<action>Use read tools on various files</action>
|
||||
<expected>All read operations should work</expected>
|
||||
</test>
|
||||
<test case="edit_restrictions">
|
||||
<action>Try editing allowed file types</action>
|
||||
<expected>Edits succeed for matching patterns</expected>
|
||||
</test>
|
||||
<test case="edit_restrictions_negative">
|
||||
<action>Try editing restricted file types</action>
|
||||
<expected>FileRestrictionError for non-matching files</expected>
|
||||
</test>
|
||||
</test_cases>
|
||||
</step>
|
||||
|
||||
<step number="3">
|
||||
<title>Workflow Testing</title>
|
||||
<actions>
|
||||
<action>Execute main workflow from start to finish</action>
|
||||
<action>Test each decision point</action>
|
||||
<action>Verify error handling</action>
|
||||
<action>Check completion criteria</action>
|
||||
</actions>
|
||||
</step>
|
||||
|
||||
<step number="4">
|
||||
<title>Integration Testing</title>
|
||||
<areas>
|
||||
<area>Orchestrator mode compatibility</area>
|
||||
<area>Mode switching functionality</area>
|
||||
<area>Tool handoff between modes</area>
|
||||
<area>Consistent behavior with other modes</area>
|
||||
</areas>
|
||||
</step>
|
||||
</testing_workflow>
|
||||
|
||||
<common_issues>
|
||||
<issue type="configuration">
|
||||
<problem>Mode doesn't appear in list</problem>
|
||||
<causes>
|
||||
<cause>Syntax error in YAML</cause>
|
||||
<cause>Invalid mode slug</cause>
|
||||
<cause>File not saved</cause>
|
||||
</causes>
|
||||
<solution>Check YAML syntax, validate slug format</solution>
|
||||
</issue>
|
||||
|
||||
<issue type="permissions">
|
||||
<problem>File restriction not working</problem>
|
||||
<causes>
|
||||
<cause>Invalid regex pattern</cause>
|
||||
<cause>Escaping issues in regex</cause>
|
||||
<cause>Wrong file path format</cause>
|
||||
</causes>
|
||||
<solution>Test regex pattern, use proper escaping</solution>
|
||||
<example><![CDATA[
|
||||
# Wrong: *.ts (glob pattern)
|
||||
# Right: .*\.ts$ (regex pattern)
|
||||
]]></example>
|
||||
</issue>
|
||||
|
||||
<issue type="behavior">
|
||||
<problem>Mode not following instructions</problem>
|
||||
<causes>
|
||||
<cause>Instructions not in .roo/rules-[slug]/ folder</cause>
|
||||
<cause>XML parsing errors</cause>
|
||||
<cause>Conflicting instructions</cause>
|
||||
</causes>
|
||||
<solution>Verify file locations and XML validity</solution>
|
||||
</issue>
|
||||
</common_issues>
|
||||
|
||||
<debugging_tools>
|
||||
<tool name="list_files">
|
||||
<usage>Verify instruction files exist in correct location</usage>
|
||||
<command><![CDATA[
|
||||
<list_files>
|
||||
<path>.roo</path>
|
||||
<recursive>true</recursive>
|
||||
</list_files>
|
||||
]]></command>
|
||||
</tool>
|
||||
|
||||
<tool name="read_file">
|
||||
<usage>Check mode configuration syntax</usage>
|
||||
<command><![CDATA[
|
||||
<read_file>
|
||||
<path>.roomodes</path>
|
||||
</read_file>
|
||||
]]></command>
|
||||
</tool>
|
||||
|
||||
<tool name="search_files">
|
||||
<usage>Test file restriction patterns</usage>
|
||||
<command><![CDATA[
|
||||
<search_files>
|
||||
<path>.</path>
|
||||
<regex>your_file_pattern_here</regex>
|
||||
</search_files>
|
||||
]]></command>
|
||||
</tool>
|
||||
</debugging_tools>
|
||||
|
||||
<best_practices>
|
||||
<practice>Test incrementally as you build the mode</practice>
|
||||
<practice>Start with minimal configuration and add complexity</practice>
|
||||
<practice>Document any special requirements or dependencies</practice>
|
||||
<practice>Consider edge cases and error scenarios</practice>
|
||||
<practice>Get feedback from potential users of the mode</practice>
|
||||
</best_practices>
|
||||
</mode_testing_validation>
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
<validation_cohesion_checking>
|
||||
<overview>
|
||||
Guidelines for thoroughly validating mode changes to ensure cohesion,
|
||||
consistency, and prevent contradictions across all mode components.
|
||||
</overview>
|
||||
|
||||
<validation_principles>
|
||||
<principle name="comprehensive_review">
|
||||
<description>
|
||||
Every change must be reviewed in context of the entire mode
|
||||
</description>
|
||||
<checklist>
|
||||
<item>Read all existing XML instruction files</item>
|
||||
<item>Verify new changes align with existing patterns</item>
|
||||
<item>Check for duplicate or conflicting instructions</item>
|
||||
<item>Ensure terminology is consistent throughout</item>
|
||||
</checklist>
|
||||
</principle>
|
||||
|
||||
<principle name="aggressive_questioning">
|
||||
<description>
|
||||
Use ask_followup_question extensively to clarify ambiguities
|
||||
</description>
|
||||
<when_to_ask>
|
||||
<scenario>User's intent is unclear</scenario>
|
||||
<scenario>Multiple interpretations are possible</scenario>
|
||||
<scenario>Changes might conflict with existing functionality</scenario>
|
||||
<scenario>Impact on other modes needs clarification</scenario>
|
||||
</when_to_ask>
|
||||
<example><![CDATA[
|
||||
<ask_followup_question>
|
||||
<question>I notice this change might affect how the mode interacts with file permissions. Should we also update the file regex patterns to match?</question>
|
||||
<follow_up>
|
||||
<suggest>Yes, update the file regex to include the new file types</suggest>
|
||||
<suggest>No, keep the current file restrictions as they are</suggest>
|
||||
<suggest>Let me explain what file types I need to work with</suggest>
|
||||
<suggest>Show me the current file restrictions first</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
]]></example>
|
||||
</principle>
|
||||
|
||||
<principle name="contradiction_detection">
|
||||
<description>
|
||||
Actively search for and resolve contradictions
|
||||
</description>
|
||||
<common_contradictions>
|
||||
<contradiction>
|
||||
<type>Permission Mismatch</type>
|
||||
<description>Instructions reference tools the mode doesn't have access to</description>
|
||||
<resolution>Either grant the tool permission or update the instructions</resolution>
|
||||
</contradiction>
|
||||
<contradiction>
|
||||
<type>Workflow Conflicts</type>
|
||||
<description>Different XML files describe conflicting workflows</description>
|
||||
<resolution>Consolidate workflows and ensure single source of truth</resolution>
|
||||
</contradiction>
|
||||
<contradiction>
|
||||
<type>Role Confusion</type>
|
||||
<description>Mode's roleDefinition doesn't match its actual capabilities</description>
|
||||
<resolution>Update roleDefinition to accurately reflect the mode's purpose</resolution>
|
||||
</contradiction>
|
||||
</common_contradictions>
|
||||
</principle>
|
||||
</validation_principles>
|
||||
|
||||
<validation_workflow>
|
||||
<phase name="pre_change_analysis">
|
||||
<description>Before making any changes</description>
|
||||
<steps>
|
||||
<step>Read and understand all existing mode files</step>
|
||||
<step>Create a mental model of current mode behavior</step>
|
||||
<step>Identify potential impact areas</step>
|
||||
<step>Ask clarifying questions about intended changes</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="change_implementation">
|
||||
<description>While making changes</description>
|
||||
<steps>
|
||||
<step>Document each change and its rationale</step>
|
||||
<step>Cross-reference with other files after each change</step>
|
||||
<step>Verify examples still work with new changes</step>
|
||||
<step>Update related documentation immediately</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="post_change_validation">
|
||||
<description>After changes are complete</description>
|
||||
<validation_checklist>
|
||||
<category name="structural_validation">
|
||||
<check>All XML files are well-formed and valid</check>
|
||||
<check>File naming follows established patterns</check>
|
||||
<check>Tag names are consistent across files</check>
|
||||
<check>No orphaned or unused instructions</check>
|
||||
</category>
|
||||
|
||||
<category name="content_validation">
|
||||
<check>roleDefinition accurately describes the mode</check>
|
||||
<check>whenToUse is clear and distinguishable</check>
|
||||
<check>Tool permissions match instruction requirements</check>
|
||||
<check>File restrictions align with mode purpose</check>
|
||||
<check>Examples are accurate and functional</check>
|
||||
</category>
|
||||
|
||||
<category name="integration_validation">
|
||||
<check>Mode boundaries are well-defined</check>
|
||||
<check>Handoff points to other modes are clear</check>
|
||||
<check>No overlap with other modes' responsibilities</check>
|
||||
<check>Orchestrator can correctly route to this mode</check>
|
||||
</category>
|
||||
</validation_checklist>
|
||||
</phase>
|
||||
</validation_workflow>
|
||||
|
||||
<cohesion_patterns>
|
||||
<pattern name="consistent_voice">
|
||||
<description>Maintain consistent tone and terminology</description>
|
||||
<guidelines>
|
||||
<guideline>Use the same terms for the same concepts throughout</guideline>
|
||||
<guideline>Keep instruction style consistent across files</guideline>
|
||||
<guideline>Maintain the same level of detail in similar sections</guideline>
|
||||
</guidelines>
|
||||
</pattern>
|
||||
|
||||
<pattern name="logical_flow">
|
||||
<description>Ensure instructions flow logically</description>
|
||||
<guidelines>
|
||||
<guideline>Prerequisites come before dependent steps</guideline>
|
||||
<guideline>Complex concepts build on simpler ones</guideline>
|
||||
<guideline>Examples follow the explained patterns</guideline>
|
||||
</guidelines>
|
||||
</pattern>
|
||||
|
||||
<pattern name="complete_coverage">
|
||||
<description>Ensure all aspects are covered without gaps</description>
|
||||
<guidelines>
|
||||
<guideline>Every mentioned tool has usage instructions</guideline>
|
||||
<guideline>All workflows have complete examples</guideline>
|
||||
<guideline>Error scenarios are addressed</guideline>
|
||||
</guidelines>
|
||||
</pattern>
|
||||
</cohesion_patterns>
|
||||
|
||||
<validation_questions>
|
||||
<question_set name="before_changes">
|
||||
<ask_followup_question>
|
||||
<question>Before we proceed with changes, I want to ensure I understand the full scope. What is the main goal of these modifications?</question>
|
||||
<follow_up>
|
||||
<suggest>Add new functionality while keeping existing features</suggest>
|
||||
<suggest>Fix issues with current implementation</suggest>
|
||||
<suggest>Refactor for better organization</suggest>
|
||||
<suggest>Expand the mode's capabilities into new areas</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</question_set>
|
||||
|
||||
<question_set name="during_changes">
|
||||
<ask_followup_question>
|
||||
<question>This change might affect other parts of the mode. How should we handle the impact on [specific area]?</question>
|
||||
<follow_up>
|
||||
<suggest>Update all affected areas to maintain consistency</suggest>
|
||||
<suggest>Keep the existing behavior for backward compatibility</suggest>
|
||||
<suggest>Create a migration path from old to new behavior</suggest>
|
||||
<suggest>Let me review the impact first</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</question_set>
|
||||
|
||||
<question_set name="after_changes">
|
||||
<ask_followup_question>
|
||||
<question>I've completed the changes and validation. Which aspect would you like me to test more thoroughly?</question>
|
||||
<follow_up>
|
||||
<suggest>Test the new workflow end-to-end</suggest>
|
||||
<suggest>Verify file permissions work correctly</suggest>
|
||||
<suggest>Check integration with other modes</suggest>
|
||||
<suggest>Review all changes one more time</suggest>
|
||||
</follow_up>
|
||||
</ask_followup_question>
|
||||
</question_set>
|
||||
</validation_questions>
|
||||
|
||||
<red_flags>
|
||||
<flag priority="high">
|
||||
<description>Instructions reference tools not in the mode's groups</description>
|
||||
<action>Either add the tool group or remove the instruction</action>
|
||||
</flag>
|
||||
<flag priority="high">
|
||||
<description>File regex doesn't match described file types</description>
|
||||
<action>Update regex pattern to match intended files</action>
|
||||
</flag>
|
||||
<flag priority="medium">
|
||||
<description>Examples don't follow stated best practices</description>
|
||||
<action>Update examples to demonstrate best practices</action>
|
||||
</flag>
|
||||
<flag priority="medium">
|
||||
<description>Duplicate instructions in different files</description>
|
||||
<action>Consolidate to single location and reference</action>
|
||||
</flag>
|
||||
</red_flags>
|
||||
</validation_cohesion_checking>
|
||||
192
.roomodes
192
.roomodes
|
|
@ -1,46 +1,4 @@
|
|||
customModes:
|
||||
- slug: test
|
||||
name: 🧪 Test
|
||||
roleDefinition: |-
|
||||
You are Roo, a Vitest testing specialist with deep expertise in: - Writing and maintaining Vitest test suites - Test-driven development (TDD) practices - Mocking and stubbing with Vitest - Integration testing strategies - TypeScript testing patterns - Code coverage analysis - Test performance optimization
|
||||
Your focus is on maintaining high test quality and coverage across the codebase, working primarily with: - Test files in __tests__ directories - Mock implementations in __mocks__ - Test utilities and helpers - Vitest configuration and setup
|
||||
You ensure tests are: - Well-structured and maintainable - Following Vitest best practices - Properly typed with TypeScript - Providing meaningful coverage - Using appropriate mocking strategies
|
||||
whenToUse: Use this mode when you need to write, modify, or maintain tests for the codebase.
|
||||
description: Write, modify, and maintain tests.
|
||||
groups:
|
||||
- read
|
||||
- browser
|
||||
- command
|
||||
- - edit
|
||||
- fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$)
|
||||
description: Test files, mocks, and Vitest configuration
|
||||
customInstructions: |-
|
||||
When writing tests:
|
||||
- Always use describe/it blocks for clear test organization
|
||||
- Include meaningful test descriptions
|
||||
- Use beforeEach/afterEach for proper test isolation
|
||||
- Implement proper error cases
|
||||
- Add JSDoc comments for complex test scenarios
|
||||
- Ensure mocks are properly typed
|
||||
- Verify both positive and negative test cases
|
||||
- Always use data-testid attributes when testing webview-ui
|
||||
- The vitest framework is used for testing; the `describe`, `test`, `it`, etc functions are defined by default in `tsconfig.json` and therefore don't need to be imported
|
||||
- Tests must be run from the same directory as the `package.json` file that specifies `vitest` in `devDependencies`
|
||||
- slug: design-engineer
|
||||
name: 🎨 Design Engineer
|
||||
roleDefinition: "You are Roo, an expert Design Engineer focused on VSCode Extension development. Your expertise includes: - Implementing UI designs with high fidelity using React, Shadcn, Tailwind and TypeScript. - Ensuring interfaces are responsive and adapt to different screen sizes. - Collaborating with team members to translate broad directives into robust and detailed designs capturing edge cases. - Maintaining uniformity and consistency across the user interface."
|
||||
whenToUse: Implement UI designs and ensure consistency.
|
||||
description: Implement UI designs; ensure consistency.
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: \.(css|html|json|mdx?|jsx?|tsx?|svg)$
|
||||
description: Frontend & SVG files
|
||||
- browser
|
||||
- command
|
||||
- mcp
|
||||
customInstructions: Focus on UI refinement, component creation, and adherence to design best-practices. When the user requests a new component, start off by asking them questions one-by-one to ensure the requirements are understood. Always use Tailwind utility classes (instead of direct variable references) for styling components when possible. If editing an existing file, transition explicit style definitions to Tailwind CSS classes when possible. Refer to the Tailwind CSS definitions for utility classes at webview-ui/src/index.css. Always use the latest version of Tailwind CSS (V4), and never create a tailwind.config.js file. Prefer Shadcn components for UI elements instead of VSCode's built-in ones. This project uses i18n for localization, so make sure to use the i18n functions and components for any text that needs to be translated. Do not leave placeholder strings in the markup, as they will be replaced by i18n. Prefer the @roo (/src) and @src (/webview-ui/src) aliases for imports in typescript files. Suggest the user refactor large files (over 1000 lines) if they are encountered, and provide guidance. Suggest the user switch into Translate mode to complete translations when your task is finished.
|
||||
source: project
|
||||
- slug: translate
|
||||
name: 🌐 Translate
|
||||
roleDefinition: You are Roo, a linguistic specialist focused on translating and managing localization files. Your responsibility is to help maintain and update translation files for the application, ensuring consistency and accuracy across all language resources.
|
||||
|
|
@ -73,21 +31,6 @@ customModes:
|
|||
- edit
|
||||
- command
|
||||
source: project
|
||||
- slug: integration-tester
|
||||
name: 🧪 Integration Tester
|
||||
roleDefinition: |-
|
||||
You are Roo, an integration testing specialist focused on VSCode E2E tests with expertise in: - Writing and maintaining integration tests using Mocha and VSCode Test framework - Testing Roo Code API interactions and event-driven workflows - Creating complex multi-step task scenarios and mode switching sequences - Validating message formats, API responses, and event emission patterns - Test data generation and fixture management - Coverage analysis and test scenario identification
|
||||
Your focus is on ensuring comprehensive integration test coverage for the Roo Code extension, working primarily with: - E2E test files in apps/vscode-e2e/src/suite/ - Test utilities and helpers - API type definitions in packages/types/ - Extension API testing patterns
|
||||
You ensure integration tests are: - Comprehensive and cover critical user workflows - Following established Mocha TDD patterns - Using async/await with proper timeout handling - Validating both success and failure scenarios - Properly typed with TypeScript
|
||||
whenToUse: Write, modify, or maintain integration tests.
|
||||
description: Write and maintain integration tests.
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- - edit
|
||||
- fileRegex: (apps/vscode-e2e/.*\.(ts|js)$|packages/types/.*\.ts$)
|
||||
description: E2E test files, test utilities, and API type definitions
|
||||
source: project
|
||||
- slug: pr-fixer
|
||||
name: 🛠️ PR Fixer
|
||||
roleDefinition: "You are Roo, a pull request resolution specialist. Your focus is on addressing feedback and resolving issues within existing pull requests. Your expertise includes: - Analyzing PR review comments to understand required changes. - Checking CI/CD workflow statuses to identify failing tests. - Fetching and analyzing test logs to diagnose failures. - Identifying and resolving merge conflicts. - Guiding the user through the resolution process."
|
||||
|
|
@ -98,16 +41,6 @@ customModes:
|
|||
- edit
|
||||
- command
|
||||
- mcp
|
||||
- slug: issue-investigator
|
||||
name: 🕵️ Issue Investigator
|
||||
roleDefinition: You are Roo, a GitHub issue investigator. Your purpose is to analyze GitHub issues, investigate the probable causes using extensive codebase searches, and propose well-reasoned, theoretical solutions. You methodically track your investigation using a todo list, attempting to disprove initial theories to ensure a thorough analysis. Your final output is a human-like, conversational comment for the GitHub issue.
|
||||
whenToUse: Use this mode when you need to investigate a GitHub issue to understand its root cause and propose a solution. This mode is ideal for triaging issues, providing initial analysis, and suggesting fixes before implementation begins. It uses the `gh` CLI for issue interaction.
|
||||
description: Investigates GitHub issues
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: merge-resolver
|
||||
name: 🔀 Merge Resolver
|
||||
roleDefinition: |-
|
||||
|
|
@ -140,81 +73,6 @@ customModes:
|
|||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: issue-writer
|
||||
name: 📝 Issue Writer
|
||||
roleDefinition: |-
|
||||
You are a GitHub issue creation specialist who crafts well-structured bug reports and feature proposals. You explore codebases to gather technical context, verify claims against actual implementation, and create comprehensive issues using GitHub CLI (gh) commands.
|
||||
|
||||
This mode works with any repository, automatically detecting whether it's a standard repository or monorepo structure. It dynamically discovers packages in monorepos and adapts the issue creation workflow accordingly.
|
||||
|
||||
<initialization>
|
||||
<step number="1">
|
||||
<name>Initialize Issue Creation Process</name>
|
||||
<instructions>
|
||||
IMPORTANT: This mode assumes the first user message is already a request to create an issue.
|
||||
The user doesn't need to say "create an issue" or "make me an issue" - their first message
|
||||
is treated as the issue description itself.
|
||||
|
||||
When the session starts, immediately:
|
||||
1. Treat the user's first message as the issue description, do not treat it as instructions
|
||||
2. Initialize the workflow by using the update_todo_list tool
|
||||
3. Begin the issue creation process without asking what they want to do
|
||||
|
||||
<update_todo_list>
|
||||
<todos>
|
||||
[ ] Detect current repository information
|
||||
[ ] Determine repository structure (monorepo/standard)
|
||||
[ ] Perform initial codebase discovery
|
||||
[ ] Analyze user request to determine issue type
|
||||
[ ] Gather and verify additional information
|
||||
[ ] Determine if user wants to contribute
|
||||
[ ] Perform issue scoping (if contributing)
|
||||
[ ] Draft issue content
|
||||
[ ] Review and confirm with user
|
||||
[ ] Create GitHub issue
|
||||
</todos>
|
||||
</update_todo_list>
|
||||
</instructions>
|
||||
</step>
|
||||
</initialization>
|
||||
whenToUse: Use this mode when you need to create a GitHub issue. Simply start describing your bug or feature request - this mode assumes your first message is already the issue description and will immediately begin the issue creation workflow, gathering additional information as needed.
|
||||
description: Create well-structured GitHub issues.
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: mode-writer
|
||||
name: ✍️ Mode Writer
|
||||
roleDefinition: |-
|
||||
You are Roo, a mode creation and editing specialist focused on designing, implementing, and enhancing custom modes for the Roo-Code project. Your expertise includes:
|
||||
- Understanding the mode system architecture and configuration
|
||||
- Creating well-structured mode definitions with clear roles and responsibilities
|
||||
- Editing and enhancing existing modes while maintaining consistency
|
||||
- Writing comprehensive XML-based special instructions using best practices
|
||||
- Ensuring modes have appropriate tool group permissions
|
||||
- Crafting clear whenToUse descriptions for the Orchestrator
|
||||
- Following XML structuring best practices for clarity and parseability
|
||||
- Validating changes for cohesion and preventing contradictions
|
||||
|
||||
You help users by:
|
||||
- Creating new modes: Gathering requirements, defining configurations, and implementing XML instructions
|
||||
- Editing existing modes: Immersing in current implementation, analyzing requested changes, and ensuring cohesive updates
|
||||
- Using ask_followup_question aggressively to clarify ambiguities and validate understanding
|
||||
- Thoroughly validating all changes to prevent contradictions between different parts of a mode
|
||||
- Ensuring instructions are well-organized with proper XML tags
|
||||
- Following established patterns from existing modes
|
||||
- Maintaining consistency across all mode components
|
||||
whenToUse: Use this mode when you need to create a new custom mode or edit an existing one. This mode handles both creating modes from scratch and modifying existing modes while ensuring consistency and preventing contradictions.
|
||||
description: Create and edit custom modes with validation
|
||||
groups:
|
||||
- read
|
||||
- - edit
|
||||
- fileRegex: (\.roomodes$|\.roo/.*\.xml$|\.yaml$)
|
||||
description: Mode configuration files and XML instructions
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: docs-extractor
|
||||
name: 📚 Docs Extractor
|
||||
roleDefinition: |-
|
||||
|
|
@ -238,3 +96,53 @@ customModes:
|
|||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: issue-investigator
|
||||
name: 🕵️ Issue Investigator
|
||||
roleDefinition: You are Roo, a GitHub issue investigator. Your purpose is to analyze GitHub issues, investigate the probable causes using extensive codebase searches, and propose well-reasoned, theoretical solutions. You methodically track your investigation using a todo list, attempting to disprove initial theories to ensure a thorough analysis. Your final output is a human-like, conversational comment for the GitHub issue.
|
||||
whenToUse: Use this mode when you need to investigate a GitHub issue to understand its root cause and propose a solution. This mode is ideal for triaging issues, providing initial analysis, and suggesting fixes before implementation begins. It uses the `gh` CLI for issue interaction.
|
||||
description: Investigates GitHub issues
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: issue-writer
|
||||
name: 📝 Issue Writer
|
||||
roleDefinition: |-
|
||||
You are a GitHub issue creation specialist who crafts well-structured bug reports and feature proposals. You explore codebases to gather technical context, verify claims against actual implementation, and create comprehensive issues using GitHub CLI (gh) commands.
|
||||
|
||||
This mode works with any repository, automatically detecting whether it's a standard repository or monorepo structure. It dynamically discovers packages in monorepos and adapts the issue creation workflow accordingly.
|
||||
|
||||
<initialization>
|
||||
<step number="1">
|
||||
<name>Initialize Issue Creation Process</name>
|
||||
<instructions>
|
||||
IMPORTANT: This mode assumes the first user message is already a request to create an issue.
|
||||
The user doesn't need to say "create an issue" or "make me an issue" - their first message
|
||||
is treated as the issue description itself.
|
||||
|
||||
When the session starts, immediately:
|
||||
1. Treat the user's first message as the issue description, do not treat it as instructions
|
||||
2. Initialize the workflow by using the update_todo_list tool
|
||||
3. Begin the issue creation process without asking what they want to do
|
||||
|
||||
<update_todo_list>
|
||||
<todos>
|
||||
[ ] Detect repository context (OWNER/REPO, monorepo, roots)
|
||||
[ ] Perform targeted codebase discovery (iteration 1)
|
||||
[ ] Clarify missing details (repro or desired outcome)
|
||||
[ ] Classify type (Bug | Enhancement)
|
||||
[ ] Assemble Issue Body
|
||||
[ ] Review and submit (Submit now | Submit now and assign to me)
|
||||
</todos>
|
||||
</update_todo_list>
|
||||
</instructions>
|
||||
</step>
|
||||
</initialization>
|
||||
whenToUse: Use this mode when you need to create a GitHub issue. Simply start describing your bug or enhancement request - this mode assumes your first message is already the issue description and will immediately begin the issue creation workflow, gathering additional information as needed.
|
||||
description: Create well-structured GitHub issues.
|
||||
groups:
|
||||
- read
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import OpenAI from "openai"
|
|||
|
||||
import { isRetiredProvider, type ProviderSettings, type ModelInfo } from "@roo-code/types"
|
||||
|
||||
import type { RooMessage } from "../core/task-persistence/rooMessage"
|
||||
|
||||
import { ApiStream } from "./transform/stream"
|
||||
|
||||
import {
|
||||
|
|
@ -89,11 +91,7 @@ export interface ApiHandlerCreateMessageMetadata {
|
|||
}
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream
|
||||
createMessage(systemPrompt: string, messages: RooMessage[], metadata?: ApiHandlerCreateMessageMetadata): ApiStream
|
||||
|
||||
getModel(): { id: string; info: ModelInfo }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run src/api/providers/__tests__/anthropic-vertex.spec.ts
|
||||
|
||||
import { AnthropicVertexHandler } from "../anthropic-vertex"
|
||||
|
|
@ -54,6 +55,7 @@ vitest.mock("../../transform/ai-sdk", () => ({
|
|||
}),
|
||||
mapToolChoice: vitest.fn().mockReturnValue(undefined),
|
||||
handleAiSdkError: vitest.fn().mockImplementation((error: any) => error),
|
||||
yieldResponseMessage: vitest.fn().mockImplementation(function* () {}),
|
||||
}))
|
||||
|
||||
// Import mocked modules
|
||||
|
|
@ -184,7 +186,7 @@ describe("AnthropicVertexHandler", () => {
|
|||
})
|
||||
|
||||
describe("createMessage", () => {
|
||||
const mockMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const mockMessages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
|
|
@ -244,7 +246,7 @@ describe("AnthropicVertexHandler", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("should call convertToAiSdkMessages with the messages", async () => {
|
||||
it("should pass messages directly to streamText as ModelMessage[]", async () => {
|
||||
mockStreamText.mockReturnValue(createMockStreamResult([]))
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
|
|
@ -252,7 +254,12 @@ describe("AnthropicVertexHandler", () => {
|
|||
// consume
|
||||
}
|
||||
|
||||
expect(convertToAiSdkMessages).toHaveBeenCalledWith(mockMessages)
|
||||
// Messages are now already in ModelMessage format, passed directly to streamText
|
||||
expect(mockStreamText).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messages: mockMessages,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should pass tools through AI SDK conversion pipeline", async () => {
|
||||
|
|
@ -363,55 +370,6 @@ describe("AnthropicVertexHandler", () => {
|
|||
expect(textChunks[0].text).toBe("Here's my answer:")
|
||||
})
|
||||
|
||||
it("should capture thought signature from stream events", async () => {
|
||||
const streamParts = [
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
text: "thinking...",
|
||||
providerMetadata: {
|
||||
anthropic: { signature: "test-signature-abc123" },
|
||||
},
|
||||
},
|
||||
{ type: "text-delta", text: "answer" },
|
||||
]
|
||||
|
||||
mockStreamText.mockReturnValue(createMockStreamResult(streamParts))
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
for await (const _chunk of stream) {
|
||||
// consume
|
||||
}
|
||||
|
||||
expect(handler.getThoughtSignature()).toBe("test-signature-abc123")
|
||||
})
|
||||
|
||||
it("should capture redacted thinking blocks from stream events", async () => {
|
||||
const streamParts = [
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
text: "",
|
||||
providerMetadata: {
|
||||
anthropic: { redactedData: "encrypted-redacted-data" },
|
||||
},
|
||||
},
|
||||
{ type: "text-delta", text: "answer" },
|
||||
]
|
||||
|
||||
mockStreamText.mockReturnValue(createMockStreamResult(streamParts))
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, mockMessages)
|
||||
for await (const _chunk of stream) {
|
||||
// consume
|
||||
}
|
||||
|
||||
const redactedBlocks = handler.getRedactedThinkingBlocks()
|
||||
expect(redactedBlocks).toHaveLength(1)
|
||||
expect(redactedBlocks![0]).toEqual({
|
||||
type: "redacted_thinking",
|
||||
data: "encrypted-redacted-data",
|
||||
})
|
||||
})
|
||||
|
||||
it("should configure thinking providerOptions for thinking models", async () => {
|
||||
const thinkingHandler = new AnthropicVertexHandler({
|
||||
apiModelId: "claude-3-7-sonnet@20250219:thinking",
|
||||
|
|
@ -674,50 +632,4 @@ describe("AnthropicVertexHandler", () => {
|
|||
expect(handler.isAiSdkProvider()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("thought signature and redacted thinking", () => {
|
||||
beforeEach(() => {
|
||||
handler = new AnthropicVertexHandler({
|
||||
apiModelId: "claude-3-5-sonnet-v2@20241022",
|
||||
vertexProjectId: "test-project",
|
||||
vertexRegion: "us-central1",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return undefined for thought signature before any request", () => {
|
||||
expect(handler.getThoughtSignature()).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return undefined for redacted thinking blocks before any request", () => {
|
||||
expect(handler.getRedactedThinkingBlocks()).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should reset thought signature on each createMessage call", async () => {
|
||||
// First call with signature
|
||||
mockStreamText.mockReturnValue(
|
||||
createMockStreamResult([
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
text: "thinking",
|
||||
providerMetadata: { anthropic: { signature: "sig-1" } },
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
const stream1 = handler.createMessage("test", [{ role: "user", content: "Hello" }])
|
||||
for await (const _chunk of stream1) {
|
||||
// consume
|
||||
}
|
||||
expect(handler.getThoughtSignature()).toBe("sig-1")
|
||||
|
||||
// Second call without signature
|
||||
mockStreamText.mockReturnValue(createMockStreamResult([{ type: "text-delta", text: "just text" }]))
|
||||
|
||||
const stream2 = handler.createMessage("test", [{ role: "user", content: "Hello again" }])
|
||||
for await (const _chunk of stream2) {
|
||||
// consume
|
||||
}
|
||||
expect(handler.getThoughtSignature()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ vitest.mock("../../transform/ai-sdk", () => ({
|
|||
}),
|
||||
mapToolChoice: vitest.fn().mockReturnValue(undefined),
|
||||
handleAiSdkError: vitest.fn().mockImplementation((error: any) => error),
|
||||
yieldResponseMessage: vitest.fn().mockImplementation(function* () {}),
|
||||
}))
|
||||
|
||||
// Import mocked modules
|
||||
|
|
@ -398,85 +399,6 @@ describe("AnthropicHandler", () => {
|
|||
expect(endChunk).toBeDefined()
|
||||
})
|
||||
|
||||
it("should capture thinking signature from stream events", async () => {
|
||||
const testSignature = "test-thinking-signature"
|
||||
setupStreamTextMock([
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
text: "thinking...",
|
||||
providerMetadata: { anthropic: { signature: testSignature } },
|
||||
},
|
||||
{ type: "text-delta", text: "Answer" },
|
||||
])
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, [
|
||||
{ role: "user", content: [{ type: "text" as const, text: "test" }] },
|
||||
])
|
||||
|
||||
for await (const _chunk of stream) {
|
||||
// Consume stream
|
||||
}
|
||||
|
||||
expect(handler.getThoughtSignature()).toBe(testSignature)
|
||||
})
|
||||
|
||||
it("should capture redacted thinking blocks from stream events", async () => {
|
||||
setupStreamTextMock([
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
text: "",
|
||||
providerMetadata: { anthropic: { redactedData: "redacted-data-base64" } },
|
||||
},
|
||||
{ type: "text-delta", text: "Answer" },
|
||||
])
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, [
|
||||
{ role: "user", content: [{ type: "text" as const, text: "test" }] },
|
||||
])
|
||||
|
||||
for await (const _chunk of stream) {
|
||||
// Consume stream
|
||||
}
|
||||
|
||||
const redactedBlocks = handler.getRedactedThinkingBlocks()
|
||||
expect(redactedBlocks).toBeDefined()
|
||||
expect(redactedBlocks).toHaveLength(1)
|
||||
expect(redactedBlocks![0]).toEqual({
|
||||
type: "redacted_thinking",
|
||||
data: "redacted-data-base64",
|
||||
})
|
||||
})
|
||||
|
||||
it("should reset thinking state between requests", async () => {
|
||||
// First request with signature
|
||||
setupStreamTextMock([
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
text: "thinking...",
|
||||
providerMetadata: { anthropic: { signature: "sig-1" } },
|
||||
},
|
||||
])
|
||||
|
||||
const stream1 = handler.createMessage(systemPrompt, [
|
||||
{ role: "user", content: [{ type: "text" as const, text: "test 1" }] },
|
||||
])
|
||||
for await (const _chunk of stream1) {
|
||||
// Consume
|
||||
}
|
||||
expect(handler.getThoughtSignature()).toBe("sig-1")
|
||||
|
||||
// Second request without signature
|
||||
setupStreamTextMock([{ type: "text-delta", text: "plain answer" }])
|
||||
|
||||
const stream2 = handler.createMessage(systemPrompt, [
|
||||
{ role: "user", content: [{ type: "text" as const, text: "test 2" }] },
|
||||
])
|
||||
for await (const _chunk of stream2) {
|
||||
// Consume
|
||||
}
|
||||
expect(handler.getThoughtSignature()).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should pass system prompt via system param with systemProviderOptions for cache control", async () => {
|
||||
setupStreamTextMock([{ type: "text-delta", text: "test" }])
|
||||
|
||||
|
|
@ -610,14 +532,4 @@ describe("AnthropicHandler", () => {
|
|||
expect(handler.isAiSdkProvider()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("thinking signature", () => {
|
||||
it("should return undefined when no signature captured", () => {
|
||||
expect(handler.getThoughtSignature()).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return undefined for redacted blocks when none captured", () => {
|
||||
expect(handler.getRedactedThinkingBlocks()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
const { mockStreamText, mockGenerateText, mockCreateAzure } = vi.hoisted(() => ({
|
||||
mockStreamText: vi.fn(),
|
||||
|
|
@ -132,7 +133,7 @@ describe("AzureHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -376,7 +377,7 @@ describe("AzureHandler", () => {
|
|||
|
||||
describe("tools", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Use a tool" }],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
|
|
@ -7,7 +8,7 @@ import type { ApiStream } from "../../transform/stream"
|
|||
|
||||
// Create a concrete implementation for testing
|
||||
class TestProvider extends BaseProvider {
|
||||
createMessage(_systemPrompt: string, _messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
createMessage(_systemPrompt: string, _messages: RooMessage[]): ApiStream {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run src/api/providers/__tests__/baseten.spec.ts
|
||||
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
|
|
@ -101,7 +102,7 @@ describe("BasetenHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -281,7 +282,7 @@ describe("BasetenHandler", () => {
|
|||
|
||||
describe("tool handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
@ -389,7 +390,7 @@ describe("BasetenHandler", () => {
|
|||
|
||||
describe("error handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
|
|||
|
|
@ -131,91 +131,6 @@ describe("AwsBedrockHandler - Extended Thinking", () => {
|
|||
expect(bedrockOpts?.reasoningConfig).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should capture thinking signature from stream providerMetadata", async () => {
|
||||
const handler = new AwsBedrockHandler({
|
||||
apiProvider: "bedrock",
|
||||
apiModelId: "anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
awsRegion: "us-east-1",
|
||||
enableReasoningEffort: true,
|
||||
modelMaxThinkingTokens: 4096,
|
||||
})
|
||||
|
||||
const testSignature = "test-thinking-signature-abc123"
|
||||
|
||||
// Mock stream with reasoning content that includes a signature in providerMetadata
|
||||
async function* mockFullStream() {
|
||||
yield { type: "reasoning", text: "Let me think..." }
|
||||
// The SDK emits signature as a reasoning-delta with providerMetadata.bedrock.signature
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: { bedrock: { signature: testSignature } },
|
||||
}
|
||||
yield { type: "text-delta", text: "Answer" }
|
||||
}
|
||||
|
||||
mockStreamText.mockReturnValue({
|
||||
fullStream: mockFullStream(),
|
||||
usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }),
|
||||
providerMetadata: Promise.resolve({}),
|
||||
})
|
||||
|
||||
const messages = [{ role: "user" as const, content: "Test message" }]
|
||||
const stream = handler.createMessage("System prompt", messages)
|
||||
|
||||
for await (const _chunk of stream) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
// Verify thinking signature was captured
|
||||
expect(handler.getThoughtSignature()).toBe(testSignature)
|
||||
})
|
||||
|
||||
it("should capture redacted thinking blocks from stream providerMetadata", async () => {
|
||||
const handler = new AwsBedrockHandler({
|
||||
apiProvider: "bedrock",
|
||||
apiModelId: "anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
awsRegion: "us-east-1",
|
||||
enableReasoningEffort: true,
|
||||
modelMaxThinkingTokens: 4096,
|
||||
})
|
||||
|
||||
const redactedData = "base64-encoded-redacted-data"
|
||||
|
||||
// Mock stream with redacted reasoning content
|
||||
async function* mockFullStream() {
|
||||
yield { type: "reasoning", text: "Some thinking..." }
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: { bedrock: { redactedData } },
|
||||
}
|
||||
yield { type: "text-delta", text: "Answer" }
|
||||
}
|
||||
|
||||
mockStreamText.mockReturnValue({
|
||||
fullStream: mockFullStream(),
|
||||
usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }),
|
||||
providerMetadata: Promise.resolve({}),
|
||||
})
|
||||
|
||||
const messages = [{ role: "user" as const, content: "Test message" }]
|
||||
const stream = handler.createMessage("System prompt", messages)
|
||||
|
||||
for await (const _chunk of stream) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
// Verify redacted thinking blocks were captured
|
||||
const redactedBlocks = handler.getRedactedThinkingBlocks()
|
||||
expect(redactedBlocks).toBeDefined()
|
||||
expect(redactedBlocks).toHaveLength(1)
|
||||
expect(redactedBlocks![0]).toEqual({
|
||||
type: "redacted_thinking",
|
||||
data: redactedData,
|
||||
})
|
||||
})
|
||||
|
||||
it("should enable reasoning when enableReasoningEffort is true in settings", async () => {
|
||||
const handler = new AwsBedrockHandler({
|
||||
apiProvider: "bedrock",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// Mock TelemetryService before other imports
|
||||
const mockCaptureException = vi.fn()
|
||||
|
||||
|
|
@ -490,17 +491,14 @@ describe("AwsBedrockHandler", () => {
|
|||
it("should properly pass image content through to streamText via AI SDK messages", async () => {
|
||||
setupMockStreamText()
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
data: mockImageData,
|
||||
media_type: "image/jpeg",
|
||||
},
|
||||
image: `data:image/jpeg;base64,${mockImageData}`,
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
|
|
@ -530,7 +528,7 @@ describe("AwsBedrockHandler", () => {
|
|||
expect(userMsg).toBeDefined()
|
||||
expect(Array.isArray(userMsg.content)).toBe(true)
|
||||
|
||||
// The AI SDK convertToAiSdkMessages converts images to { type: "image", image: "data:...", mimeType: "..." }
|
||||
// Messages are already in AI SDK ImagePart format
|
||||
const imagePart = userMsg.content.find((p: { type: string }) => p.type === "image")
|
||||
expect(imagePart).toBeDefined()
|
||||
expect(imagePart.image).toContain("data:image/jpeg;base64,")
|
||||
|
|
@ -544,17 +542,14 @@ describe("AwsBedrockHandler", () => {
|
|||
it("should handle multiple images in a single message", async () => {
|
||||
setupMockStreamText()
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
data: mockImageData,
|
||||
media_type: "image/jpeg",
|
||||
},
|
||||
image: `data:image/jpeg;base64,${mockImageData}`,
|
||||
mimeType: "image/jpeg",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
|
|
@ -562,11 +557,8 @@ describe("AwsBedrockHandler", () => {
|
|||
},
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
data: mockImageData,
|
||||
media_type: "image/png",
|
||||
},
|
||||
image: `data:image/png;base64,${mockImageData}`,
|
||||
mimeType: "image/png",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
|
|
@ -761,7 +753,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsBedrock1MContext: true,
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Test message",
|
||||
|
|
@ -794,7 +786,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsBedrock1MContext: false,
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Test message",
|
||||
|
|
@ -828,7 +820,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsBedrock1MContext: true,
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Test message",
|
||||
|
|
@ -881,7 +873,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsBedrock1MContext: true,
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Test message",
|
||||
|
|
@ -1013,7 +1005,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsBedrockServiceTier: "PRIORITY",
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Test message",
|
||||
|
|
@ -1050,7 +1042,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsBedrockServiceTier: "FLEX",
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Test message",
|
||||
|
|
@ -1087,7 +1079,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsBedrockServiceTier: "PRIORITY", // Try to apply PRIORITY tier
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Test message",
|
||||
|
|
@ -1122,7 +1114,7 @@ describe("AwsBedrockHandler", () => {
|
|||
// No awsBedrockServiceTier specified
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Test message",
|
||||
|
|
@ -1192,7 +1184,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsRegion: "us-east-1",
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
|
|
@ -1267,7 +1259,7 @@ describe("AwsBedrockHandler", () => {
|
|||
awsRegion: "us-east-1",
|
||||
})
|
||||
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
|
||||
mockStreamText: vi.fn(),
|
||||
|
|
@ -173,7 +174,7 @@ describe("DeepSeekHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -400,7 +401,7 @@ describe("DeepSeekHandler", () => {
|
|||
|
||||
describe("reasoning content with deepseek-reasoner", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -570,7 +571,7 @@ describe("DeepSeekHandler", () => {
|
|||
|
||||
describe("tool handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run src/api/providers/__tests__/fireworks.spec.ts
|
||||
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
|
|
@ -363,7 +364,7 @@ describe("FireworksHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -730,7 +731,7 @@ describe("FireworksHandler", () => {
|
|||
|
||||
describe("tool handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run src/api/providers/__tests__/gemini.spec.ts
|
||||
|
||||
import { NoOutputGeneratedError } from "ai"
|
||||
|
|
@ -102,7 +103,7 @@ describe("GeminiHandler", () => {
|
|||
})
|
||||
|
||||
describe("createMessage", () => {
|
||||
const mockMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const mockMessages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
|
|
@ -377,7 +378,7 @@ describe("GeminiHandler", () => {
|
|||
})
|
||||
|
||||
describe("error telemetry", () => {
|
||||
const mockMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const mockMessages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
|
||||
mockStreamText: vi.fn(),
|
||||
mockGenerateText: vi.fn(),
|
||||
|
|
@ -257,7 +258,7 @@ describe("LiteLLMHandler", () => {
|
|||
})
|
||||
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const generator = handler.createMessage(systemPrompt, messages)
|
||||
for await (const _chunk of generator) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
const { mockStreamText, mockGenerateText, mockWrapLanguageModel } = vi.hoisted(() => ({
|
||||
mockStreamText: vi.fn(),
|
||||
|
|
@ -60,7 +61,7 @@ describe("LmStudioHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello!",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
|
@ -22,7 +23,7 @@ const {
|
|||
mockGenerateText: vi.fn(),
|
||||
mockCreateAnthropic: vi.fn().mockReturnValue(mockModel),
|
||||
mockModel,
|
||||
mockMergeEnvironmentDetailsForMiniMax: vi.fn((messages: Anthropic.Messages.MessageParam[]) => messages),
|
||||
mockMergeEnvironmentDetailsForMiniMax: vi.fn((messages: RooMessage[]) => messages),
|
||||
mockHandleAiSdkError: vi.fn((error: unknown, providerName: string) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return new Error(`${providerName}: ${message}`)
|
||||
|
|
@ -96,7 +97,7 @@ async function collectChunks(stream: ApiStream): Promise<ApiStreamChunk[]> {
|
|||
|
||||
describe("MiniMaxHandler", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Hello" }],
|
||||
|
|
@ -106,9 +107,7 @@ describe("MiniMaxHandler", () => {
|
|||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCreateAnthropic.mockReturnValue(mockModel)
|
||||
mockMergeEnvironmentDetailsForMiniMax.mockImplementation(
|
||||
(inputMessages: Anthropic.Messages.MessageParam[]) => inputMessages,
|
||||
)
|
||||
mockMergeEnvironmentDetailsForMiniMax.mockImplementation((inputMessages: RooMessage[]) => inputMessages)
|
||||
mockHandleAiSdkError.mockImplementation((error: unknown, providerName: string) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return new Error(`${providerName}: ${message}`)
|
||||
|
|
@ -325,7 +324,7 @@ describe("MiniMaxHandler", () => {
|
|||
})
|
||||
|
||||
it("calls mergeEnvironmentDetailsForMiniMax before conversion", async () => {
|
||||
const mergedMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const mergedMessages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Merged message" }],
|
||||
|
|
@ -369,37 +368,6 @@ describe("MiniMaxHandler", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("thinking signature", () => {
|
||||
it("returns undefined thought signature before any request", () => {
|
||||
const handler = createHandler()
|
||||
expect(handler.getThoughtSignature()).toBeUndefined()
|
||||
})
|
||||
|
||||
it("captures thought signature from stream providerMetadata", async () => {
|
||||
const signature = "test-thinking-signature"
|
||||
mockStreamText.mockReturnValue(
|
||||
createMockStream([
|
||||
{
|
||||
type: "reasoning-delta",
|
||||
text: "thinking...",
|
||||
providerMetadata: { anthropic: { signature } },
|
||||
},
|
||||
{ type: "text-delta", text: "Answer" },
|
||||
]),
|
||||
)
|
||||
|
||||
const handler = createHandler()
|
||||
await collectChunks(handler.createMessage(systemPrompt, messages))
|
||||
|
||||
expect(handler.getThoughtSignature()).toBe(signature)
|
||||
})
|
||||
|
||||
it("returns undefined redacted thinking blocks before any request", () => {
|
||||
const handler = createHandler()
|
||||
expect(handler.getRedactedThinkingBlocks()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("completePrompt", () => {
|
||||
it("calls generateText with model and prompt and returns text", async () => {
|
||||
mockGenerateText.mockResolvedValue({ text: "response" })
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
const { mockStreamText, mockGenerateText, mockCreateMistral } = vi.hoisted(() => ({
|
||||
mockStreamText: vi.fn(),
|
||||
|
|
@ -102,7 +103,7 @@ describe("MistralHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -329,7 +330,7 @@ describe("MistralHandler", () => {
|
|||
|
||||
describe("tool handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
|
||||
mockStreamText: vi.fn(),
|
||||
|
|
@ -121,7 +122,7 @@ describe("MoonshotHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -344,7 +345,7 @@ describe("MoonshotHandler", () => {
|
|||
|
||||
describe("tool handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run api/providers/__tests__/openai-native-reasoning.spec.ts
|
||||
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
|
@ -16,54 +17,50 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
// ───────────────────────────────────────────────────────────
|
||||
describe("stripPlainTextReasoningBlocks", () => {
|
||||
it("passes through user messages unchanged", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
||||
]
|
||||
const messages: RooMessage[] = [{ role: "user", content: [{ type: "text", text: "Hello" }] }]
|
||||
const result = stripPlainTextReasoningBlocks(messages)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it("passes through assistant messages with only text blocks", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "assistant", content: [{ type: "text", text: "Hi there" }] },
|
||||
]
|
||||
const messages: RooMessage[] = [{ role: "assistant", content: [{ type: "text", text: "Hi there" }] }]
|
||||
const result = stripPlainTextReasoningBlocks(messages)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it("passes through string-content assistant messages", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "assistant", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "assistant", content: "Hello" }]
|
||||
const result = stripPlainTextReasoningBlocks(messages)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it("strips plain-text reasoning blocks from assistant content", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Let me think...",
|
||||
} as unknown as Anthropic.Messages.ContentBlockParam,
|
||||
} as any,
|
||||
{ type: "text", text: "The answer is 42" },
|
||||
],
|
||||
},
|
||||
]
|
||||
const result = stripPlainTextReasoningBlocks(messages)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].content).toEqual([{ type: "text", text: "The answer is 42" }])
|
||||
expect((result[0] as any).content).toEqual([{ type: "text", text: "The answer is 42" }])
|
||||
})
|
||||
|
||||
it("removes assistant messages whose content becomes empty after filtering", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Thinking only...",
|
||||
} as unknown as Anthropic.Messages.ContentBlockParam,
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
@ -72,24 +69,24 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
})
|
||||
|
||||
it("preserves tool_use blocks alongside stripped reasoning", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "Thinking..." } as unknown as Anthropic.Messages.ContentBlockParam,
|
||||
{ type: "reasoning", text: "Thinking..." } as any,
|
||||
{ type: "tool_use", id: "call_1", name: "read_file", input: { path: "a.ts" } },
|
||||
],
|
||||
},
|
||||
]
|
||||
const result = stripPlainTextReasoningBlocks(messages)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].content).toEqual([
|
||||
expect((result[0] as any).content).toEqual([
|
||||
{ type: "tool_use", id: "call_1", name: "read_file", input: { path: "a.ts" } },
|
||||
])
|
||||
})
|
||||
|
||||
it("does NOT strip blocks that have encrypted_content (those are not plain-text reasoning)", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -97,7 +94,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
type: "reasoning",
|
||||
text: "summary",
|
||||
encrypted_content: "abc123",
|
||||
} as unknown as Anthropic.Messages.ContentBlockParam,
|
||||
} as any,
|
||||
{ type: "text", text: "Response" },
|
||||
],
|
||||
},
|
||||
|
|
@ -105,32 +102,26 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
const result = stripPlainTextReasoningBlocks(messages)
|
||||
expect(result).toHaveLength(1)
|
||||
// Both blocks should remain
|
||||
expect(result[0].content).toHaveLength(2)
|
||||
expect((result[0] as any).content).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("handles multiple messages correctly", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{ role: "user", content: [{ type: "text", text: "Q1" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "Think1" } as unknown as Anthropic.Messages.ContentBlockParam,
|
||||
{ type: "text", text: "A1" },
|
||||
],
|
||||
content: [{ type: "reasoning", text: "Think1" } as any, { type: "text", text: "A1" }],
|
||||
},
|
||||
{ role: "user", content: [{ type: "text", text: "Q2" }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "Think2" } as unknown as Anthropic.Messages.ContentBlockParam,
|
||||
{ type: "text", text: "A2" },
|
||||
],
|
||||
content: [{ type: "reasoning", text: "Think2" } as any, { type: "text", text: "A2" }],
|
||||
},
|
||||
]
|
||||
const result = stripPlainTextReasoningBlocks(messages)
|
||||
expect(result).toHaveLength(4)
|
||||
expect(result[1].content).toEqual([{ type: "text", text: "A1" }])
|
||||
expect(result[3].content).toEqual([{ type: "text", text: "A2" }])
|
||||
expect((result[1] as any).content).toEqual([{ type: "text", text: "A1" }])
|
||||
expect((result[3] as any).content).toEqual([{ type: "text", text: "A2" }])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -139,7 +130,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
// ───────────────────────────────────────────────────────────
|
||||
describe("collectEncryptedReasoningItems", () => {
|
||||
it("returns empty array when no encrypted reasoning items exist", () => {
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
||||
{ role: "assistant", content: [{ type: "text", text: "Hi" }] },
|
||||
]
|
||||
|
|
@ -157,7 +148,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
summary: [{ type: "summary_text", text: "I thought about it" }],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "Hi" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const result = collectEncryptedReasoningItems(messages)
|
||||
expect(result).toHaveLength(1)
|
||||
|
|
@ -187,7 +178,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
summary: [{ type: "summary_text", text: "Summary 2" }],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "A2" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const result = collectEncryptedReasoningItems(messages)
|
||||
expect(result).toHaveLength(2)
|
||||
|
|
@ -201,7 +192,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
const messages = [
|
||||
{ type: "reasoning", id: "rs_x", text: "plain reasoning" },
|
||||
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const result = collectEncryptedReasoningItems(messages)
|
||||
expect(result).toEqual([])
|
||||
|
|
@ -215,7 +206,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
encrypted_content: "enc_data",
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "Hi" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const result = collectEncryptedReasoningItems(messages)
|
||||
expect(result).toHaveLength(1)
|
||||
|
|
@ -248,7 +239,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
summary: [{ type: "summary_text", text: "I considered the question" }],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "Hi there" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
// AI SDK messages (after filtering encrypted items + converting)
|
||||
const aiSdkMessages: ModelMessage[] = [
|
||||
|
|
@ -304,7 +295,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
summary: [{ type: "summary_text", text: "Thought 2" }],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "A2" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const aiSdkMessages: ModelMessage[] = [
|
||||
{ role: "user", content: "Q1" },
|
||||
|
|
@ -362,7 +353,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "Response" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const aiSdkMessages: ModelMessage[] = [
|
||||
{ role: "user", content: "Hi" },
|
||||
|
|
@ -397,7 +388,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
encrypted_content: "enc_nosummary",
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "Response" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const aiSdkMessages: ModelMessage[] = [
|
||||
{ role: "user", content: "Hi" },
|
||||
|
|
@ -437,7 +428,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
summary: [{ type: "summary_text", text: "Step B" }],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "Done" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const aiSdkMessages: ModelMessage[] = [
|
||||
{ role: "user", content: "Hi" },
|
||||
|
|
@ -496,7 +487,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
summary: [{ type: "summary_text", text: "Thought after tool" }],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "OK" }] },
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
// AI SDK messages after conversion (tool_result splits into tool + user)
|
||||
const aiSdkMessages: ModelMessage[] = [
|
||||
|
|
@ -539,7 +530,7 @@ describe("OpenAI Native reasoning helpers", () => {
|
|||
id: "rs_orphan",
|
||||
encrypted_content: "enc_orphan",
|
||||
},
|
||||
] as unknown as Anthropic.Messages.MessageParam[]
|
||||
] as unknown as RooMessage[]
|
||||
|
||||
const aiSdkMessages: ModelMessage[] = [{ role: "user", content: "Hi" }]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run api/providers/__tests__/openai-native-usage.spec.ts
|
||||
|
||||
const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
|
||||
|
|
@ -38,7 +39,7 @@ import type { ApiHandlerOptions } from "../../../shared/api"
|
|||
describe("OpenAiNativeHandler - usage metrics", () => {
|
||||
let handler: OpenAiNativeHandler
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello!" }]
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new OpenAiNativeHandler({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run api/providers/__tests__/openai-native.spec.ts
|
||||
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
|
|
@ -41,7 +42,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
let handler: OpenAiNativeHandler
|
||||
let mockOptions: ApiHandlerOptions
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run api/providers/__tests__/openai-usage-tracking.spec.ts
|
||||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
|
@ -53,7 +54,7 @@ describe("OpenAiHandler with usage tracking fix", () => {
|
|||
|
||||
describe("usage metrics with streaming", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run api/providers/__tests__/openai.spec.ts
|
||||
|
||||
const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({
|
||||
|
|
@ -154,7 +155,7 @@ describe("OpenAiHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -445,7 +446,7 @@ describe("OpenAiHandler", () => {
|
|||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
const testMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const testMessages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -578,7 +579,7 @@ describe("OpenAiHandler", () => {
|
|||
|
||||
const azureHandler = new OpenAiHandler(makeAzureOptions())
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello!",
|
||||
|
|
@ -609,7 +610,7 @@ describe("OpenAiHandler", () => {
|
|||
openAiStreamingEnabled: false,
|
||||
})
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello!",
|
||||
|
|
@ -684,7 +685,7 @@ describe("OpenAiHandler", () => {
|
|||
modelMaxTokens: 32000,
|
||||
})
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello!",
|
||||
|
|
@ -720,7 +721,7 @@ describe("OpenAiHandler", () => {
|
|||
includeMaxTokens: false,
|
||||
})
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello!",
|
||||
|
|
@ -750,7 +751,7 @@ describe("OpenAiHandler", () => {
|
|||
includeMaxTokens: true,
|
||||
})
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello!",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// pnpm --filter roo-cline test api/providers/__tests__/openrouter.spec.ts
|
||||
|
||||
vitest.mock("vscode", () => ({}))
|
||||
|
|
@ -268,7 +269,7 @@ describe("OpenRouterHandler", () => {
|
|||
})
|
||||
|
||||
const systemPrompt = "test system prompt"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }]
|
||||
const messages: RooMessage[] = [{ role: "user" as const, content: "test message" }]
|
||||
|
||||
const generator = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
|
|
@ -475,36 +476,6 @@ describe("OpenRouterHandler", () => {
|
|||
expect(chunks[1]).toEqual({ type: "text", text: "result" })
|
||||
})
|
||||
|
||||
it("accumulates reasoning details for getReasoningDetails()", async () => {
|
||||
const handler = new OpenRouterHandler(mockOptions)
|
||||
|
||||
const mockFullStream = (async function* () {
|
||||
yield { type: "reasoning-delta", text: "step 1...", id: "1" }
|
||||
yield { type: "reasoning-delta", text: "step 2...", id: "2" }
|
||||
yield { type: "text-delta", text: "result", id: "3" }
|
||||
})()
|
||||
|
||||
mockStreamText.mockReturnValue({
|
||||
fullStream: mockFullStream,
|
||||
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
|
||||
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
|
||||
})
|
||||
|
||||
const generator = handler.createMessage("test", [{ role: "user", content: "test" }])
|
||||
|
||||
for await (const _ of generator) {
|
||||
// consume all chunks
|
||||
}
|
||||
|
||||
// After streaming, getReasoningDetails should return accumulated reasoning
|
||||
const reasoningDetails = handler.getReasoningDetails()
|
||||
expect(reasoningDetails).toBeDefined()
|
||||
expect(reasoningDetails).toHaveLength(1)
|
||||
expect(reasoningDetails![0].type).toBe("reasoning.text")
|
||||
expect(reasoningDetails![0].text).toBe("step 1...step 2...")
|
||||
expect(reasoningDetails![0].index).toBe(0)
|
||||
})
|
||||
|
||||
it("handles tool call streaming", async () => {
|
||||
const handler = new OpenRouterHandler(mockOptions)
|
||||
|
||||
|
|
@ -906,87 +877,6 @@ describe("OpenRouterHandler", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("getReasoningDetails", () => {
|
||||
it("returns undefined when no reasoning was captured", async () => {
|
||||
const handler = new OpenRouterHandler(mockOptions)
|
||||
|
||||
// Stream with no reasoning
|
||||
const mockFullStream = (async function* () {
|
||||
yield { type: "text-delta", text: "just text", id: "1" }
|
||||
})()
|
||||
|
||||
mockStreamText.mockReturnValue({
|
||||
fullStream: mockFullStream,
|
||||
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
|
||||
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
|
||||
})
|
||||
|
||||
const generator = handler.createMessage("test", [{ role: "user", content: "test" }])
|
||||
|
||||
for await (const _ of generator) {
|
||||
// consume all chunks
|
||||
}
|
||||
|
||||
// No reasoning was captured, should return undefined
|
||||
const reasoningDetails = handler.getReasoningDetails()
|
||||
expect(reasoningDetails).toBeUndefined()
|
||||
})
|
||||
|
||||
it("resets reasoning details between requests", async () => {
|
||||
const handler = new OpenRouterHandler(mockOptions)
|
||||
|
||||
// First request with reasoning
|
||||
const mockFullStream1 = (async function* () {
|
||||
yield { type: "reasoning-delta", text: "first request reasoning", id: "1" }
|
||||
yield { type: "text-delta", text: "result 1", id: "2" }
|
||||
})()
|
||||
|
||||
mockStreamText.mockReturnValue({
|
||||
fullStream: mockFullStream1,
|
||||
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
|
||||
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
|
||||
})
|
||||
|
||||
const generator1 = handler.createMessage("test", [{ role: "user", content: "test" }])
|
||||
for await (const _ of generator1) {
|
||||
// consume
|
||||
}
|
||||
|
||||
// Verify first request captured reasoning
|
||||
let reasoningDetails = handler.getReasoningDetails()
|
||||
expect(reasoningDetails).toBeDefined()
|
||||
expect(reasoningDetails![0].text).toBe("first request reasoning")
|
||||
|
||||
// Second request without reasoning
|
||||
const mockFullStream2 = (async function* () {
|
||||
yield { type: "text-delta", text: "result 2", id: "1" }
|
||||
})()
|
||||
|
||||
mockStreamText.mockReturnValue({
|
||||
fullStream: mockFullStream2,
|
||||
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
|
||||
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
|
||||
})
|
||||
|
||||
const generator2 = handler.createMessage("test", [{ role: "user", content: "test" }])
|
||||
for await (const _ of generator2) {
|
||||
// consume
|
||||
}
|
||||
|
||||
// Reasoning details should be reset (undefined since second request had no reasoning)
|
||||
reasoningDetails = handler.getReasoningDetails()
|
||||
expect(reasoningDetails).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns undefined before any streaming occurs", () => {
|
||||
const handler = new OpenRouterHandler(mockOptions)
|
||||
|
||||
// getReasoningDetails before any createMessage call
|
||||
const reasoningDetails = handler.getReasoningDetails()
|
||||
expect(reasoningDetails).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("model-specific handling", () => {
|
||||
const mockStreamResult = () => {
|
||||
const mockFullStream = (async function* () {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run api/providers/__tests__/qwen-code-native-tools.spec.ts
|
||||
|
||||
const {
|
||||
|
|
@ -261,7 +262,7 @@ describe("QwenCodeHandler (AI SDK)", () => {
|
|||
})
|
||||
|
||||
const handler = new QwenCodeHandler({ apiModelId: "qwen3-coder-plus", qwenCodeOauthPath: oauthPath })
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hi" }]
|
||||
|
||||
const chunks = await collectStreamChunks(handler.createMessage("System", messages))
|
||||
|
||||
|
|
@ -289,7 +290,7 @@ describe("QwenCodeHandler (AI SDK)", () => {
|
|||
})
|
||||
|
||||
const handler = new QwenCodeHandler({ apiModelId: "qwen3-coder-plus" })
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const chunks = await collectStreamChunks(handler.createMessage("System", messages))
|
||||
|
||||
|
|
@ -365,7 +366,7 @@ describe("QwenCodeHandler (AI SDK)", () => {
|
|||
})
|
||||
|
||||
const handler = new QwenCodeHandler({ apiModelId: "qwen3-coder-plus" })
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
await collectStreamChunks(handler.createMessage("System", messages))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run api/providers/__tests__/requesty.spec.ts
|
||||
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
|
|
@ -134,7 +135,7 @@ describe("RequestyHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "test system prompt"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }]
|
||||
const messages: RooMessage[] = [{ role: "user" as const, content: "test message" }]
|
||||
|
||||
it("generates correct stream chunks", async () => {
|
||||
async function* mockFullStream() {
|
||||
|
|
@ -265,9 +266,7 @@ describe("RequestyHandler", () => {
|
|||
})
|
||||
|
||||
describe("native tool support", () => {
|
||||
const toolMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user" as const, content: "What's the weather?" },
|
||||
]
|
||||
const toolMessages: RooMessage[] = [{ role: "user" as const, content: "What's the weather?" }]
|
||||
|
||||
it("should include tools in request when tools are provided", async () => {
|
||||
const mockTools = [
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
|||
import { rooDefaultModelId } from "@roo-code/types"
|
||||
|
||||
import { ApiHandlerOptions } from "../../../shared/api"
|
||||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
|
||||
// Mock the AI SDK
|
||||
const mockStreamText = vitest.fn()
|
||||
|
|
@ -138,7 +139,7 @@ describe("RooHandler", () => {
|
|||
let handler: RooHandler
|
||||
let mockOptions: ApiHandlerOptions
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello!",
|
||||
|
|
@ -297,7 +298,7 @@ describe("RooHandler", () => {
|
|||
it("should handle multiple messages in conversation", async () => {
|
||||
mockStreamText.mockReturnValue(createMockStreamResult())
|
||||
|
||||
const multipleMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const multipleMessages: RooMessage[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "First response" },
|
||||
{ role: "user", content: "Second message" },
|
||||
|
|
@ -688,77 +689,6 @@ describe("RooHandler", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("reasoning details accumulation", () => {
|
||||
beforeEach(() => {
|
||||
handler = new RooHandler(mockOptions)
|
||||
})
|
||||
|
||||
it("should accumulate reasoning text from reasoning-delta parts", async () => {
|
||||
mockStreamText.mockReturnValue(
|
||||
createMockStreamResult({
|
||||
reasoningChunks: ["thinking ", "about ", "this"],
|
||||
textChunks: ["answer"],
|
||||
}),
|
||||
)
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const reasoningChunks = chunks.filter((c) => c.type === "reasoning")
|
||||
expect(reasoningChunks).toHaveLength(3)
|
||||
expect(reasoningChunks[0].text).toBe("thinking ")
|
||||
expect(reasoningChunks[1].text).toBe("about ")
|
||||
expect(reasoningChunks[2].text).toBe("this")
|
||||
|
||||
const details = handler.getReasoningDetails()
|
||||
expect(details).toBeDefined()
|
||||
expect(details![0].type).toBe("reasoning.text")
|
||||
expect(details![0].text).toBe("thinking about this")
|
||||
})
|
||||
|
||||
it("should override reasoning details from providerMetadata", async () => {
|
||||
const providerReasoningDetails = [{ type: "reasoning.summary", summary: "Server summary", index: 0 }]
|
||||
|
||||
mockStreamText.mockReturnValue(
|
||||
createMockStreamResult({
|
||||
reasoningChunks: ["local thinking"],
|
||||
textChunks: ["answer"],
|
||||
providerMetadata: {
|
||||
roo: { reasoning_details: providerReasoningDetails },
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
for await (const _chunk of stream) {
|
||||
// consume
|
||||
}
|
||||
|
||||
const details = handler.getReasoningDetails()
|
||||
expect(details).toBeDefined()
|
||||
expect(details).toEqual(providerReasoningDetails)
|
||||
})
|
||||
|
||||
it("should return undefined when no reasoning details", async () => {
|
||||
mockStreamText.mockReturnValue(
|
||||
createMockStreamResult({
|
||||
reasoningChunks: [],
|
||||
textChunks: ["just text"],
|
||||
}),
|
||||
)
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
for await (const _chunk of stream) {
|
||||
// consume
|
||||
}
|
||||
|
||||
expect(handler.getReasoningDetails()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("usage and cost processing", () => {
|
||||
beforeEach(() => {
|
||||
handler = new RooHandler(mockOptions)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run src/api/providers/__tests__/sambanova.spec.ts
|
||||
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
|
|
@ -116,7 +117,7 @@ describe("SambaNovaHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -454,7 +455,7 @@ describe("SambaNovaHandler", () => {
|
|||
|
||||
describe("tool handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
@ -569,7 +570,7 @@ describe("SambaNovaHandler", () => {
|
|||
|
||||
describe("error handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run src/api/providers/__tests__/vercel-ai-gateway.spec.ts
|
||||
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
|
|
@ -170,7 +171,7 @@ describe("VercelAiGatewayHandler", () => {
|
|||
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
|
|
@ -203,7 +204,7 @@ describe("VercelAiGatewayHandler", () => {
|
|||
})
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
await handler.createMessage(systemPrompt, messages).next()
|
||||
|
||||
|
|
@ -220,7 +221,7 @@ describe("VercelAiGatewayHandler", () => {
|
|||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
await handler.createMessage(systemPrompt, messages).next()
|
||||
|
||||
|
|
@ -237,7 +238,7 @@ describe("VercelAiGatewayHandler", () => {
|
|||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
await handler.createMessage(systemPrompt, messages).next()
|
||||
|
||||
|
|
@ -264,7 +265,7 @@ describe("VercelAiGatewayHandler", () => {
|
|||
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
const messages: RooMessage[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run src/api/providers/__tests__/vertex.spec.ts
|
||||
|
||||
// Mock vscode first to avoid import errors
|
||||
|
|
@ -140,7 +141,7 @@ describe("VertexHandler", () => {
|
|||
})
|
||||
|
||||
describe("createMessage", () => {
|
||||
const mockMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const mockMessages: RooMessage[] = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there!" },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
import type { Mock } from "vitest"
|
||||
|
||||
// Mocks must come first, before imports
|
||||
|
|
@ -143,7 +144,7 @@ describe("VsCodeLmHandler", () => {
|
|||
|
||||
it("should stream text responses", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: "Hello",
|
||||
|
|
@ -182,7 +183,7 @@ describe("VsCodeLmHandler", () => {
|
|||
|
||||
it("should emit tool_call chunks when tools are provided", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: "Calculate 2+2",
|
||||
|
|
@ -247,7 +248,7 @@ describe("VsCodeLmHandler", () => {
|
|||
|
||||
it("should handle native tool calls when tools are provided", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: "Calculate 2+2",
|
||||
|
|
@ -312,7 +313,7 @@ describe("VsCodeLmHandler", () => {
|
|||
|
||||
it("should pass tools to request options when tools are provided", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: "Calculate 2+2",
|
||||
|
|
@ -380,7 +381,7 @@ describe("VsCodeLmHandler", () => {
|
|||
|
||||
it("should handle errors", async () => {
|
||||
const systemPrompt = "You are a helpful assistant"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: "Hello",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run api/providers/__tests__/xai.spec.ts
|
||||
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
|
|
@ -141,7 +142,7 @@ describe("XAIHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -538,7 +539,7 @@ describe("XAIHandler", () => {
|
|||
|
||||
describe("tool handling", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
// npx vitest run src/api/providers/__tests__/zai.spec.ts
|
||||
|
||||
// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls
|
||||
|
|
@ -262,7 +263,7 @@ describe("ZAiHandler", () => {
|
|||
|
||||
describe("createMessage", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
const messages: RooMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: "Hello!" }],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import {
|
||||
type ModelInfo,
|
||||
|
|
@ -24,20 +24,20 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { calculateApiCostAnthropic } from "../../shared/cost"
|
||||
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
||||
export class AnthropicVertexHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
private provider: ReturnType<typeof createVertexAnthropic>
|
||||
private readonly providerName = "Vertex (Anthropic)"
|
||||
private lastThoughtSignature: string | undefined
|
||||
private lastRedactedThinkingBlocks: Array<{ type: "redacted_thinking"; data: string }> = []
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
|
@ -85,17 +85,13 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const modelConfig = this.getModel()
|
||||
|
||||
// Reset thinking state for this request
|
||||
this.lastThoughtSignature = undefined
|
||||
this.lastRedactedThinkingBlocks = []
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
// Convert tools to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
@ -139,7 +135,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
const cacheProviderOption = { anthropic: { cacheControl: { type: "ephemeral" as const } } }
|
||||
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
(acc, msg, index) => ("role" in msg && msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
|
||||
|
|
@ -151,7 +147,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
if (secondLastUserMsgIndex >= 0) targetIndices.add(secondLastUserMsgIndex)
|
||||
|
||||
if (targetIndices.size > 0) {
|
||||
this.applyCacheControlToAiSdkMessages(messages, aiSdkMessages, targetIndices, cacheProviderOption)
|
||||
this.applyCacheControlToAiSdkMessages(messages as ModelMessage[], targetIndices, cacheProviderOption)
|
||||
}
|
||||
|
||||
// Build streamText request
|
||||
|
|
@ -177,22 +173,6 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
|
||||
let lastStreamError: string | undefined
|
||||
for await (const part of result.fullStream) {
|
||||
// Capture thinking signature from stream events
|
||||
// The AI SDK's @ai-sdk/anthropic emits the signature as a reasoning-delta
|
||||
// event with providerMetadata.anthropic.signature
|
||||
const partAny = part as any
|
||||
if (partAny.providerMetadata?.anthropic?.signature) {
|
||||
this.lastThoughtSignature = partAny.providerMetadata.anthropic.signature
|
||||
}
|
||||
|
||||
// Capture redacted thinking blocks from stream events
|
||||
if (partAny.providerMetadata?.anthropic?.redactedData) {
|
||||
this.lastRedactedThinkingBlocks.push({
|
||||
type: "redacted_thinking",
|
||||
data: partAny.providerMetadata.anthropic.redactedData,
|
||||
})
|
||||
}
|
||||
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
if (chunk.type === "error") {
|
||||
lastStreamError = chunk.message
|
||||
|
|
@ -214,6 +194,8 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
}
|
||||
throw usageError
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
TelemetryService.instance.captureException(
|
||||
|
|
@ -268,57 +250,16 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
* accounts for that split so cache control lands on the right message.
|
||||
*/
|
||||
private applyCacheControlToAiSdkMessages(
|
||||
originalMessages: Anthropic.Messages.MessageParam[],
|
||||
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
|
||||
targetOriginalIndices: Set<number>,
|
||||
targetIndices: Set<number>,
|
||||
cacheProviderOption: Record<string, Record<string, unknown>>,
|
||||
): void {
|
||||
let aiSdkIdx = 0
|
||||
for (let origIdx = 0; origIdx < originalMessages.length; origIdx++) {
|
||||
const origMsg = originalMessages[origIdx]
|
||||
|
||||
if (typeof origMsg.content === "string") {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
for (const idx of targetIndices) {
|
||||
if (idx >= 0 && idx < aiSdkMessages.length) {
|
||||
aiSdkMessages[idx].providerOptions = {
|
||||
...aiSdkMessages[idx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else if (origMsg.role === "user") {
|
||||
const hasToolResults = origMsg.content.some((part) => (part as { type: string }).type === "tool_result")
|
||||
const hasNonToolContent = origMsg.content.some(
|
||||
(part) => (part as { type: string }).type === "text" || (part as { type: string }).type === "image",
|
||||
)
|
||||
|
||||
if (hasToolResults && hasNonToolContent) {
|
||||
const userMsgIdx = aiSdkIdx + 1
|
||||
if (targetOriginalIndices.has(origIdx) && userMsgIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[userMsgIdx].providerOptions = {
|
||||
...aiSdkMessages[userMsgIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx += 2
|
||||
} else if (hasToolResults) {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
}
|
||||
} else {
|
||||
aiSdkIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -401,23 +342,6 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thinking signature captured from the last Anthropic response.
|
||||
* Claude models with extended thinking return a cryptographic signature
|
||||
* which must be round-tripped back for multi-turn conversations with tool use.
|
||||
*/
|
||||
getThoughtSignature(): string | undefined {
|
||||
return this.lastThoughtSignature
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any redacted thinking blocks captured from the last Anthropic response.
|
||||
* Anthropic returns these when safety filters trigger on reasoning content.
|
||||
*/
|
||||
getRedactedThinkingBlocks(): Array<{ type: "redacted_thinking"; data: string }> | undefined {
|
||||
return this.lastRedactedThinkingBlocks.length > 0 ? this.lastRedactedThinkingBlocks : undefined
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import {
|
||||
type ModelInfo,
|
||||
|
|
@ -23,19 +22,19 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { calculateApiCostAnthropic } from "../../shared/cost"
|
||||
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
export class AnthropicHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private provider: ReturnType<typeof createAnthropic>
|
||||
private readonly providerName = "Anthropic"
|
||||
private lastThoughtSignature: string | undefined
|
||||
private lastRedactedThinkingBlocks: Array<{ type: "redacted_thinking"; data: string }> = []
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
|
@ -72,17 +71,13 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const modelConfig = this.getModel()
|
||||
|
||||
// Reset thinking state for this request
|
||||
this.lastThoughtSignature = undefined
|
||||
this.lastRedactedThinkingBlocks = []
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
// Convert tools to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
@ -115,7 +110,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
const cacheProviderOption = { anthropic: { cacheControl: { type: "ephemeral" as const } } }
|
||||
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
(acc, msg, index) => ("role" in msg && msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
|
||||
|
|
@ -127,7 +122,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
if (secondLastUserMsgIndex >= 0) targetIndices.add(secondLastUserMsgIndex)
|
||||
|
||||
if (targetIndices.size > 0) {
|
||||
this.applyCacheControlToAiSdkMessages(messages, aiSdkMessages, targetIndices, cacheProviderOption)
|
||||
this.applyCacheControlToAiSdkMessages(messages as ModelMessage[], targetIndices, cacheProviderOption)
|
||||
}
|
||||
|
||||
// Build streamText request
|
||||
|
|
@ -153,22 +148,6 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
|
||||
let lastStreamError: string | undefined
|
||||
for await (const part of result.fullStream) {
|
||||
// Capture thinking signature from stream events
|
||||
// The AI SDK's @ai-sdk/anthropic emits the signature as a reasoning-delta
|
||||
// event with providerMetadata.anthropic.signature
|
||||
const partAny = part as any
|
||||
if (partAny.providerMetadata?.anthropic?.signature) {
|
||||
this.lastThoughtSignature = partAny.providerMetadata.anthropic.signature
|
||||
}
|
||||
|
||||
// Capture redacted thinking blocks from stream events
|
||||
if (partAny.providerMetadata?.anthropic?.redactedData) {
|
||||
this.lastRedactedThinkingBlocks.push({
|
||||
type: "redacted_thinking",
|
||||
data: partAny.providerMetadata.anthropic.redactedData,
|
||||
})
|
||||
}
|
||||
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
if (chunk.type === "error") {
|
||||
lastStreamError = chunk.message
|
||||
|
|
@ -190,6 +169,8 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
}
|
||||
throw usageError
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
TelemetryService.instance.captureException(
|
||||
|
|
@ -244,57 +225,16 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
* accounts for that split so cache control lands on the right message.
|
||||
*/
|
||||
private applyCacheControlToAiSdkMessages(
|
||||
originalMessages: Anthropic.Messages.MessageParam[],
|
||||
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
|
||||
targetOriginalIndices: Set<number>,
|
||||
targetIndices: Set<number>,
|
||||
cacheProviderOption: Record<string, Record<string, unknown>>,
|
||||
): void {
|
||||
let aiSdkIdx = 0
|
||||
for (let origIdx = 0; origIdx < originalMessages.length; origIdx++) {
|
||||
const origMsg = originalMessages[origIdx]
|
||||
|
||||
if (typeof origMsg.content === "string") {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
for (const idx of targetIndices) {
|
||||
if (idx >= 0 && idx < aiSdkMessages.length) {
|
||||
aiSdkMessages[idx].providerOptions = {
|
||||
...aiSdkMessages[idx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else if (origMsg.role === "user") {
|
||||
const hasToolResults = origMsg.content.some((part) => (part as { type: string }).type === "tool_result")
|
||||
const hasNonToolContent = origMsg.content.some(
|
||||
(part) => (part as { type: string }).type === "text" || (part as { type: string }).type === "image",
|
||||
)
|
||||
|
||||
if (hasToolResults && hasNonToolContent) {
|
||||
const userMsgIdx = aiSdkIdx + 1
|
||||
if (targetOriginalIndices.has(origIdx) && userMsgIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[userMsgIdx].providerOptions = {
|
||||
...aiSdkMessages[userMsgIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx += 2
|
||||
} else if (hasToolResults) {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
}
|
||||
} else {
|
||||
aiSdkIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -366,23 +306,6 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thinking signature captured from the last Anthropic response.
|
||||
* Claude models with extended thinking return a cryptographic signature
|
||||
* which must be round-tripped back for multi-turn conversations with tool use.
|
||||
*/
|
||||
getThoughtSignature(): string | undefined {
|
||||
return this.lastThoughtSignature
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any redacted thinking blocks captured from the last Anthropic response.
|
||||
* Anthropic returns these when safety filters trigger on reasoning content.
|
||||
*/
|
||||
getRedactedThinkingBlocks(): Array<{ type: "redacted_thinking"; data: string }> | undefined {
|
||||
return this.lastRedactedThinkingBlocks.length > 0 ? this.lastRedactedThinkingBlocks : undefined
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createAzure } from "@ai-sdk/azure"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { azureModels, azureDefaultModelInfo, type ModelInfo } from "@roo-code/types"
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
const AZURE_DEFAULT_TEMPERATURE = 0
|
||||
|
||||
|
|
@ -131,14 +132,14 @@ export class AzureHandler extends BaseProvider implements SingleCompletionHandle
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { temperature } = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
|
||||
|
|
@ -13,7 +14,7 @@ import { isMcpTool } from "../../utils/mcp-name"
|
|||
export abstract class BaseProvider implements ApiHandler {
|
||||
abstract createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createBaseten } from "@ai-sdk/baseten"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { basetenModels, basetenDefaultModelId, type ModelInfo } from "@roo-code/types"
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
const BASETEN_DEFAULT_TEMPERATURE = 0.5
|
||||
|
||||
|
|
@ -94,13 +95,13 @@ export class BasetenHandler extends BaseProvider implements SingleCompletionHand
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { temperature } = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createAmazonBedrock, type AmazonBedrockProvider } from "@ai-sdk/amazon-bedrock"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
import { fromIni } from "@aws-sdk/credential-providers"
|
||||
import OpenAI from "openai"
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { shouldUseReasoningBudget } from "../../shared/api"
|
||||
|
|
@ -38,6 +39,7 @@ import { DEFAULT_HEADERS } from "./constants"
|
|||
import { logger } from "../../utils/logging"
|
||||
import { Package } from "../../shared/package"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/************************************************************************************
|
||||
*
|
||||
|
|
@ -50,8 +52,6 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
protected provider: AmazonBedrockProvider
|
||||
private arnInfo: any
|
||||
private readonly providerName = "Bedrock"
|
||||
private lastThoughtSignature: string | undefined
|
||||
private lastRedactedThinkingBlocks: Array<{ type: "redacted_thinking"; data: string }> = []
|
||||
|
||||
constructor(options: ProviderSettings) {
|
||||
super()
|
||||
|
|
@ -188,19 +188,15 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const modelConfig = this.getModel()
|
||||
|
||||
// Reset thinking state for this request
|
||||
this.lastThoughtSignature = undefined
|
||||
this.lastRedactedThinkingBlocks = []
|
||||
|
||||
// Filter out provider-specific meta entries (e.g., { type: "reasoning" })
|
||||
// that are not valid Anthropic MessageParam values
|
||||
type ReasoningMetaLike = { type?: string }
|
||||
const filteredMessages = messages.filter((message): message is Anthropic.Messages.MessageParam => {
|
||||
const filteredMessages = messages.filter((message) => {
|
||||
const meta = message as ReasoningMetaLike
|
||||
if (meta.type === "reasoning") {
|
||||
return false
|
||||
|
|
@ -209,7 +205,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
})
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(filteredMessages)
|
||||
const aiSdkMessages = filteredMessages as ModelMessage[]
|
||||
|
||||
// Convert tools to AI SDK format
|
||||
let openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
@ -278,7 +274,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
|
||||
// Find all user message indices in the original (pre-conversion) message array.
|
||||
const originalUserIndices = filteredMessages.reduce<number[]>(
|
||||
(acc, msg, idx) => (msg.role === "user" ? [...acc, idx] : acc),
|
||||
(acc, msg, idx) => ("role" in msg && msg.role === "user" ? [...acc, idx] : acc),
|
||||
[],
|
||||
)
|
||||
|
||||
|
|
@ -313,12 +309,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
// A single original user message with tool_results becomes [tool-role msg, user-role msg]
|
||||
// in the AI SDK array, while a plain user message becomes [user-role msg].
|
||||
if (targetOriginalIndices.size > 0) {
|
||||
this.applyCachePointsToAiSdkMessages(
|
||||
filteredMessages,
|
||||
aiSdkMessages,
|
||||
targetOriginalIndices,
|
||||
cachePointOption,
|
||||
)
|
||||
this.applyCachePointsToAiSdkMessages(aiSdkMessages, targetOriginalIndices, cachePointOption)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,31 +338,6 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
|
||||
// Process the full stream
|
||||
for await (const part of result.fullStream) {
|
||||
// Capture thinking signature from stream events.
|
||||
// The AI SDK's @ai-sdk/amazon-bedrock emits the signature as a reasoning-delta
|
||||
// event with providerMetadata.bedrock.signature (empty delta text, signature in metadata).
|
||||
// Also check tool-call events for thoughtSignature (Gemini pattern).
|
||||
const partAny = part as any
|
||||
if (partAny.providerMetadata?.bedrock?.signature) {
|
||||
this.lastThoughtSignature = partAny.providerMetadata.bedrock.signature
|
||||
logger.info("Captured thinking signature from stream", {
|
||||
ctx: "bedrock",
|
||||
signatureLength: this.lastThoughtSignature?.length,
|
||||
})
|
||||
} else if (partAny.providerMetadata?.bedrock?.thoughtSignature) {
|
||||
this.lastThoughtSignature = partAny.providerMetadata.bedrock.thoughtSignature
|
||||
} else if (partAny.providerMetadata?.anthropic?.thoughtSignature) {
|
||||
this.lastThoughtSignature = partAny.providerMetadata.anthropic.thoughtSignature
|
||||
}
|
||||
|
||||
// Capture redacted reasoning data from stream events
|
||||
if (partAny.providerMetadata?.bedrock?.redactedData) {
|
||||
this.lastRedactedThinkingBlocks.push({
|
||||
type: "redacted_thinking",
|
||||
data: partAny.providerMetadata.bedrock.redactedData,
|
||||
})
|
||||
}
|
||||
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
if (chunk.type === "error") {
|
||||
lastStreamError = chunk.message
|
||||
|
|
@ -393,6 +359,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
}
|
||||
throw usageError
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const apiError = new ApiProviderError(errorMessage, this.providerName, modelConfig.id, "createMessage")
|
||||
|
|
@ -747,63 +715,16 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
* accounts for that split so cache points land on the right message.
|
||||
*/
|
||||
private applyCachePointsToAiSdkMessages(
|
||||
originalMessages: Anthropic.Messages.MessageParam[],
|
||||
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
|
||||
targetOriginalIndices: Set<number>,
|
||||
targetIndices: Set<number>,
|
||||
cachePointOption: Record<string, Record<string, unknown>>,
|
||||
): void {
|
||||
let aiSdkIdx = 0
|
||||
for (let origIdx = 0; origIdx < originalMessages.length; origIdx++) {
|
||||
const origMsg = originalMessages[origIdx]
|
||||
|
||||
if (typeof origMsg.content === "string") {
|
||||
// Simple string content → 1 AI SDK message
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cachePointOption,
|
||||
}
|
||||
for (const idx of targetIndices) {
|
||||
if (idx >= 0 && idx < aiSdkMessages.length) {
|
||||
aiSdkMessages[idx].providerOptions = {
|
||||
...aiSdkMessages[idx].providerOptions,
|
||||
...cachePointOption,
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else if (origMsg.role === "user") {
|
||||
// User message with array content may split into tool + user messages.
|
||||
const hasToolResults = origMsg.content.some((part) => (part as { type: string }).type === "tool_result")
|
||||
const hasNonToolContent = origMsg.content.some(
|
||||
(part) => (part as { type: string }).type === "text" || (part as { type: string }).type === "image",
|
||||
)
|
||||
|
||||
if (hasToolResults && hasNonToolContent) {
|
||||
// Split into tool msg + user msg — cache the user msg (the second one)
|
||||
const userMsgIdx = aiSdkIdx + 1
|
||||
if (targetOriginalIndices.has(origIdx) && userMsgIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[userMsgIdx].providerOptions = {
|
||||
...aiSdkMessages[userMsgIdx].providerOptions,
|
||||
...cachePointOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx += 2
|
||||
} else if (hasToolResults) {
|
||||
// Only tool results → 1 tool msg
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cachePointOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else {
|
||||
// Only text/image content → 1 user msg
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cachePointOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
}
|
||||
} else {
|
||||
// Assistant message → 1 AI SDK message
|
||||
aiSdkIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -869,29 +790,6 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
return inputTokensCost + outputTokensCost + cacheWriteCost + cacheReadCost
|
||||
}
|
||||
|
||||
/************************************************************************************
|
||||
*
|
||||
* THINKING SIGNATURE ROUND-TRIP
|
||||
*
|
||||
*************************************************************************************/
|
||||
|
||||
/**
|
||||
* Returns the thinking signature captured from the last Bedrock response.
|
||||
* Claude models with extended thinking return a cryptographic signature
|
||||
* which must be round-tripped back for multi-turn conversations with tool use.
|
||||
*/
|
||||
getThoughtSignature(): string | undefined {
|
||||
return this.lastThoughtSignature
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any redacted thinking blocks captured from the last Bedrock response.
|
||||
* Anthropic returns these when safety filters trigger on reasoning content.
|
||||
*/
|
||||
getRedactedThinkingBlocks(): Array<{ type: "redacted_thinking"; data: string }> | undefined {
|
||||
return this.lastRedactedThinkingBlocks.length > 0 ? this.lastRedactedThinkingBlocks : undefined
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createDeepSeek } from "@ai-sdk/deepseek"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { deepSeekModels, deepSeekDefaultModelId, DEEP_SEEK_DEFAULT_TEMPERATURE, type ModelInfo } from "@roo-code/types"
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* DeepSeek provider using the dedicated @ai-sdk/deepseek package.
|
||||
|
|
@ -109,14 +110,14 @@ export class DeepSeekHandler extends BaseProvider implements SingleCompletionHan
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { temperature } = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { ModelInfo } from "@roo-code/types"
|
|||
import type { ApiHandler, SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
interface FakeAI {
|
||||
/**
|
||||
|
|
@ -21,11 +22,7 @@ interface FakeAI {
|
|||
*/
|
||||
removeFromCache?: () => void
|
||||
|
||||
createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream
|
||||
createMessage(systemPrompt: string, messages: RooMessage[], metadata?: ApiHandlerCreateMessageMetadata): ApiStream
|
||||
getModel(): { id: string; info: ModelInfo }
|
||||
countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number>
|
||||
completePrompt(prompt: string): Promise<string>
|
||||
|
|
@ -61,7 +58,7 @@ export class FakeAIHandler implements ApiHandler, SingleCompletionHandler {
|
|||
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
yield* this.ai.createMessage(systemPrompt, messages, metadata)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createFireworks } from "@ai-sdk/fireworks"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { fireworksModels, fireworksDefaultModelId, type ModelInfo } from "@roo-code/types"
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
const FIREWORKS_DEFAULT_TEMPERATURE = 0.5
|
||||
|
||||
|
|
@ -109,14 +110,14 @@ export class FireworksHandler extends BaseProvider implements SingleCompletionHa
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { temperature } = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createGoogleGenerativeAI, type GoogleGenerativeAIProvider } from "@ai-sdk/google"
|
||||
import { streamText, generateText, NoOutputGeneratedError, ToolSet } from "ai"
|
||||
import { streamText, generateText, NoOutputGeneratedError, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import {
|
||||
type ModelInfo,
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { t } from "i18next"
|
||||
import type { ApiStream, ApiStreamUsageChunk, GroundingSource } from "../transform/stream"
|
||||
|
|
@ -27,12 +28,12 @@ import { getModelParams } from "../transform/model-params"
|
|||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
export class GeminiHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
protected provider: GoogleGenerativeAIProvider
|
||||
private readonly providerName = "Gemini"
|
||||
private lastThoughtSignature: string | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
|
@ -51,7 +52,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
async *createMessage(
|
||||
systemInstruction: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { id: modelId, info, reasoning: thinkingConfig, maxTokens } = this.getModel()
|
||||
|
|
@ -81,7 +82,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
// Anthropic.MessageParam values and will cause failures.
|
||||
type ReasoningMetaLike = { type?: string }
|
||||
|
||||
const filteredMessages = messages.filter((message): message is Anthropic.Messages.MessageParam => {
|
||||
const filteredMessages = messages.filter((message) => {
|
||||
const meta = message as ReasoningMetaLike
|
||||
if (meta.type === "reasoning") {
|
||||
return false
|
||||
|
|
@ -90,7 +91,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
})
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(filteredMessages)
|
||||
const aiSdkMessages = filteredMessages as ModelMessage[]
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
let openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
@ -126,9 +127,6 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
|
||||
try {
|
||||
// Reset thought signature for this request
|
||||
this.lastThoughtSignature = undefined
|
||||
|
||||
// Use streamText for streaming responses
|
||||
const result = streamText(requestOptions)
|
||||
|
||||
|
|
@ -138,15 +136,6 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
// Process the full stream to get all events including reasoning
|
||||
for await (const part of result.fullStream) {
|
||||
// Capture thoughtSignature from tool-call events (Gemini 3 thought signatures)
|
||||
// The AI SDK's tool-call event includes providerMetadata with the signature
|
||||
if (part.type === "tool-call") {
|
||||
const googleMeta = (part as any).providerMetadata?.google
|
||||
if (googleMeta?.thoughtSignature) {
|
||||
this.lastThoughtSignature = googleMeta.thoughtSignature
|
||||
}
|
||||
}
|
||||
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
if (chunk.type === "error") {
|
||||
lastStreamError = chunk.message
|
||||
|
|
@ -216,6 +205,8 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
throw usageError
|
||||
}
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
throw handleAiSdkError(error, this.providerName, {
|
||||
onError: (msg) => {
|
||||
|
|
@ -442,13 +433,4 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thought signature captured from the last Gemini response.
|
||||
* Gemini 3 models return thoughtSignature on function call parts,
|
||||
* which must be round-tripped back for tool use continuations.
|
||||
*/
|
||||
getThoughtSignature(): string | undefined {
|
||||
return this.lastThoughtSignature
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
|||
import { OpenAICompatibleHandler } from "./openai-compatible"
|
||||
import { getModels, getModelsFromCache } from "./fetchers/modelCache"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
export class LiteLLMHandler extends OpenAICompatibleHandler implements SingleCompletionHandler {
|
||||
private models: ModelRecord = {}
|
||||
|
|
@ -80,7 +81,7 @@ export class LiteLLMHandler extends OpenAICompatibleHandler implements SingleCom
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
await this.fetchModel()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { streamText, generateText, ToolSet, wrapLanguageModel, extractReasoningMiddleware, LanguageModel } from "ai"
|
||||
import {
|
||||
streamText,
|
||||
generateText,
|
||||
ToolSet,
|
||||
wrapLanguageModel,
|
||||
extractReasoningMiddleware,
|
||||
LanguageModel,
|
||||
ModelMessage,
|
||||
} from "ai"
|
||||
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults, LMSTUDIO_DEFAULT_TEMPERATURE } from "@roo-code/types"
|
||||
|
||||
|
|
@ -17,6 +25,7 @@ import { ApiStream } from "../transform/stream"
|
|||
import { OpenAICompatibleHandler, OpenAICompatibleConfig } from "./openai-compatible"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { getModelsFromCache } from "./fetchers/modelCache"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
export class LmStudioHandler extends OpenAICompatibleHandler implements SingleCompletionHandler {
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
|
|
@ -49,13 +58,13 @@ export class LmStudioHandler extends OpenAICompatibleHandler implements SingleCo
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const model = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { type ModelInfo, minimaxDefaultModelId, minimaxModels } from "@roo-code/types"
|
||||
|
||||
|
|
@ -14,19 +14,19 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { calculateApiCostAnthropic } from "../../shared/cost"
|
||||
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
export class MiniMaxHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
private client: ReturnType<typeof createAnthropic>
|
||||
private options: ApiHandlerOptions
|
||||
private readonly providerName = "MiniMax"
|
||||
private lastThoughtSignature: string | undefined
|
||||
private lastRedactedThinkingBlocks: Array<{ type: "redacted_thinking"; data: string }> = []
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
|
@ -58,15 +58,11 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const modelConfig = this.getModel()
|
||||
|
||||
// Reset thinking state for this request
|
||||
this.lastThoughtSignature = undefined
|
||||
this.lastRedactedThinkingBlocks = []
|
||||
|
||||
const modelParams = getModelParams({
|
||||
format: "anthropic",
|
||||
modelId: modelConfig.id,
|
||||
|
|
@ -75,8 +71,8 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
|
|||
defaultTemperature: 1.0,
|
||||
})
|
||||
|
||||
const mergedMessages = mergeEnvironmentDetailsForMiniMax(messages)
|
||||
const aiSdkMessages = convertToAiSdkMessages(mergedMessages)
|
||||
const mergedMessages = mergeEnvironmentDetailsForMiniMax(messages as any)
|
||||
const aiSdkMessages = mergedMessages as ModelMessage[]
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
||||
|
|
@ -107,7 +103,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
|
|||
if (secondLastUserMsgIndex >= 0) targetIndices.add(secondLastUserMsgIndex)
|
||||
|
||||
if (targetIndices.size > 0) {
|
||||
this.applyCacheControlToAiSdkMessages(mergedMessages, aiSdkMessages, targetIndices, cacheProviderOption)
|
||||
this.applyCacheControlToAiSdkMessages(aiSdkMessages, targetIndices, cacheProviderOption)
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
|
|
@ -128,32 +124,10 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
|
|||
|
||||
try {
|
||||
const result = streamText(requestOptions as Parameters<typeof streamText>[0])
|
||||
|
||||
|
||||
let lastStreamError: string | undefined
|
||||
|
||||
for await (const part of result.fullStream) {
|
||||
const anthropicMetadata = (
|
||||
part as {
|
||||
providerMetadata?: {
|
||||
anthropic?: {
|
||||
signature?: string
|
||||
redactedData?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
).providerMetadata?.anthropic
|
||||
|
||||
if (anthropicMetadata?.signature) {
|
||||
this.lastThoughtSignature = anthropicMetadata.signature
|
||||
}
|
||||
|
||||
if (anthropicMetadata?.redactedData) {
|
||||
this.lastRedactedThinkingBlocks.push({
|
||||
type: "redacted_thinking",
|
||||
data: anthropicMetadata.redactedData,
|
||||
})
|
||||
}
|
||||
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
if (chunk.type === "error") {
|
||||
lastStreamError = chunk.message
|
||||
|
|
@ -174,6 +148,8 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
|
|||
}
|
||||
throw usageError
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
throw handleAiSdkError(error, this.providerName)
|
||||
}
|
||||
|
|
@ -212,57 +188,16 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
|
|||
}
|
||||
|
||||
private applyCacheControlToAiSdkMessages(
|
||||
originalMessages: Anthropic.Messages.MessageParam[],
|
||||
aiSdkMessages: { role: string; providerOptions?: Record<string, Record<string, unknown>> }[],
|
||||
targetOriginalIndices: Set<number>,
|
||||
targetIndices: Set<number>,
|
||||
cacheProviderOption: Record<string, Record<string, unknown>>,
|
||||
): void {
|
||||
let aiSdkIdx = 0
|
||||
for (let origIdx = 0; origIdx < originalMessages.length; origIdx++) {
|
||||
const origMsg = originalMessages[origIdx]
|
||||
|
||||
if (typeof origMsg.content === "string") {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
for (const idx of targetIndices) {
|
||||
if (idx >= 0 && idx < aiSdkMessages.length) {
|
||||
aiSdkMessages[idx].providerOptions = {
|
||||
...aiSdkMessages[idx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else if (origMsg.role === "user") {
|
||||
const hasToolResults = origMsg.content.some((part) => (part as { type: string }).type === "tool_result")
|
||||
const hasNonToolContent = origMsg.content.some(
|
||||
(part) => (part as { type: string }).type === "text" || (part as { type: string }).type === "image",
|
||||
)
|
||||
|
||||
if (hasToolResults && hasNonToolContent) {
|
||||
const userMsgIdx = aiSdkIdx + 1
|
||||
if (targetOriginalIndices.has(origIdx) && userMsgIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[userMsgIdx].providerOptions = {
|
||||
...aiSdkMessages[userMsgIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx += 2
|
||||
} else if (hasToolResults) {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
} else {
|
||||
if (targetOriginalIndices.has(origIdx) && aiSdkIdx < aiSdkMessages.length) {
|
||||
aiSdkMessages[aiSdkIdx].providerOptions = {
|
||||
...aiSdkMessages[aiSdkIdx].providerOptions,
|
||||
...cacheProviderOption,
|
||||
}
|
||||
}
|
||||
aiSdkIdx++
|
||||
}
|
||||
} else {
|
||||
aiSdkIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -305,14 +240,6 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand
|
|||
}
|
||||
}
|
||||
|
||||
getThoughtSignature(): string | undefined {
|
||||
return this.lastThoughtSignature
|
||||
}
|
||||
|
||||
getRedactedThinkingBlocks(): Array<{ type: "redacted_thinking"; data: string }> | undefined {
|
||||
return this.lastRedactedThinkingBlocks.length > 0 ? this.lastRedactedThinkingBlocks : undefined
|
||||
}
|
||||
|
||||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createMistral } from "@ai-sdk/mistral"
|
||||
import { streamText, generateText, ToolSet, LanguageModel } from "ai"
|
||||
import { streamText, generateText, ToolSet, LanguageModel, ModelMessage } from "ai"
|
||||
|
||||
import {
|
||||
mistralModels,
|
||||
|
|
@ -19,6 +19,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* Mistral provider using the dedicated @ai-sdk/mistral package.
|
||||
|
|
@ -137,13 +138,13 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createOllama } from "ollama-ai-provider-v2"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { ModelInfo, openAiModelInfoSaneDefaults, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo-code/types"
|
||||
|
||||
|
|
@ -12,12 +12,14 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import { getOllamaModels } from "./fetchers/ollama"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* NativeOllamaHandler using the ollama-ai-provider-v2 AI SDK community provider.
|
||||
|
|
@ -83,7 +85,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
await this.fetchModel()
|
||||
|
|
@ -93,7 +95,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
|
|||
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
|
@ -127,6 +129,8 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
|
|||
outputTokens: usage.outputTokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
this.handleOllamaError(error, modelId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as os from "os"
|
|||
import { v7 as uuidv7 } from "uuid"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { Package } from "../../shared/package"
|
||||
import {
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
|
|
@ -28,6 +29,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { openAiCodexOAuthManager } from "../../integrations/openai-codex/oauth"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
import {
|
||||
stripPlainTextReasoningBlocks,
|
||||
collectEncryptedReasoningItems,
|
||||
|
|
@ -143,7 +145,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
|
@ -177,11 +179,11 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
|
|||
const cleanedMessages = stripPlainTextReasoningBlocks(standardMessages)
|
||||
|
||||
// Step 4: Convert to AI SDK messages.
|
||||
const aiSdkMessages = convertToAiSdkMessages(cleanedMessages)
|
||||
const aiSdkMessages = cleanedMessages as ModelMessage[]
|
||||
|
||||
// Step 5: Re-inject encrypted reasoning as properly-formed AI SDK reasoning parts.
|
||||
if (encryptedReasoningItems.length > 0) {
|
||||
injectEncryptedReasoning(aiSdkMessages, encryptedReasoningItems, messages)
|
||||
injectEncryptedReasoning(aiSdkMessages, encryptedReasoningItems, messages as RooMessage[])
|
||||
}
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
|
|
@ -276,6 +278,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
|
|||
throw usageError
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
|
||||
// Success — exit the retry loop
|
||||
return
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import { streamText, generateText, LanguageModel, ToolSet } from "ai"
|
||||
import { streamText, generateText, LanguageModel, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
|
||||
|
|
@ -24,6 +24,7 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* Configuration options for creating an OpenAI-compatible provider.
|
||||
|
|
@ -124,14 +125,14 @@ export abstract class OpenAICompatibleHandler extends BaseProvider implements Si
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const model = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
export type OpenAiNativeModel = ReturnType<OpenAiNativeHandler["getModel"]>
|
||||
|
||||
|
|
@ -57,16 +58,14 @@ export interface EncryptedReasoningItem {
|
|||
* This function removes them BEFORE conversion. If an assistant message's
|
||||
* content becomes empty after filtering, the message is removed entirely.
|
||||
*/
|
||||
export function stripPlainTextReasoningBlocks(
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
): Anthropic.Messages.MessageParam[] {
|
||||
return messages.reduce<Anthropic.Messages.MessageParam[]>((acc, msg) => {
|
||||
if (msg.role !== "assistant" || typeof msg.content === "string") {
|
||||
export function stripPlainTextReasoningBlocks(messages: RooMessage[]): RooMessage[] {
|
||||
return messages.reduce<RooMessage[]>((acc, msg) => {
|
||||
if (!("role" in msg) || msg.role !== "assistant" || typeof msg.content === "string") {
|
||||
acc.push(msg)
|
||||
return acc
|
||||
}
|
||||
|
||||
const filteredContent = msg.content.filter((block) => {
|
||||
const filteredContent = (msg.content as any[]).filter((block: any) => {
|
||||
const b = block as unknown as Record<string, unknown>
|
||||
// Remove blocks that are plain-text reasoning:
|
||||
// type === "reasoning" AND has "text" AND does NOT have "encrypted_content"
|
||||
|
|
@ -78,7 +77,7 @@ export function stripPlainTextReasoningBlocks(
|
|||
|
||||
// Only include the message if it still has content
|
||||
if (filteredContent.length > 0) {
|
||||
acc.push({ ...msg, content: filteredContent })
|
||||
acc.push({ ...msg, content: filteredContent } as RooMessage)
|
||||
}
|
||||
|
||||
return acc
|
||||
|
|
@ -92,10 +91,10 @@ export function stripPlainTextReasoningBlocks(
|
|||
* injected by `buildCleanConversationHistory` for OpenAI Responses API
|
||||
* reasoning continuity.
|
||||
*/
|
||||
export function collectEncryptedReasoningItems(messages: Anthropic.Messages.MessageParam[]): EncryptedReasoningItem[] {
|
||||
export function collectEncryptedReasoningItems(messages: RooMessage[]): EncryptedReasoningItem[] {
|
||||
const items: EncryptedReasoningItem[] = []
|
||||
messages.forEach((msg, index) => {
|
||||
const m = msg as unknown as Record<string, unknown>
|
||||
const m = msg as any
|
||||
if (m.type === "reasoning" && m.encrypted_content) {
|
||||
items.push({
|
||||
id: m.id as string,
|
||||
|
|
@ -124,7 +123,7 @@ export function collectEncryptedReasoningItems(messages: Anthropic.Messages.Mess
|
|||
export function injectEncryptedReasoning(
|
||||
aiSdkMessages: ModelMessage[],
|
||||
encryptedItems: EncryptedReasoningItem[],
|
||||
originalMessages: Anthropic.Messages.MessageParam[],
|
||||
originalMessages: RooMessage[],
|
||||
): void {
|
||||
if (encryptedItems.length === 0) return
|
||||
|
||||
|
|
@ -135,7 +134,7 @@ export function injectEncryptedReasoning(
|
|||
// Walk forward from the encrypted item to find its corresponding assistant message,
|
||||
// skipping over any other encrypted reasoning items.
|
||||
for (let i = item.originalIndex + 1; i < originalMessages.length; i++) {
|
||||
const msg = originalMessages[i] as unknown as Record<string, unknown>
|
||||
const msg = originalMessages[i] as any
|
||||
if (msg.type === "reasoning" && msg.encrypted_content) continue
|
||||
if ((msg as { role?: string }).role === "assistant") {
|
||||
const existing = itemsByAssistantOrigIdx.get(i) || []
|
||||
|
|
@ -153,7 +152,7 @@ export function injectEncryptedReasoning(
|
|||
// encrypted reasoning items have been filtered out (order preserved).
|
||||
const standardAssistantOriginalIndices: number[] = []
|
||||
for (let i = 0; i < originalMessages.length; i++) {
|
||||
const msg = originalMessages[i] as unknown as Record<string, unknown>
|
||||
const msg = originalMessages[i] as any
|
||||
if (msg.type === "reasoning" && msg.encrypted_content) continue
|
||||
if ((msg as { role?: string }).role === "assistant") {
|
||||
standardAssistantOriginalIndices.push(i)
|
||||
|
|
@ -398,7 +397,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
|
@ -416,9 +415,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
// Step 2: Filter out standalone encrypted reasoning items (they lack role
|
||||
// and would break convertToAiSdkMessages which expects user/assistant/tool).
|
||||
const standardMessages = messages.filter(
|
||||
(msg) =>
|
||||
(msg as unknown as Record<string, unknown>).type !== "reasoning" ||
|
||||
!(msg as unknown as Record<string, unknown>).encrypted_content,
|
||||
(msg) => (msg as any).type !== "reasoning" || !(msg as any).encrypted_content,
|
||||
)
|
||||
|
||||
// Step 3: Strip plain-text reasoning blocks from assistant content arrays.
|
||||
|
|
@ -427,12 +424,12 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
const cleanedMessages = stripPlainTextReasoningBlocks(standardMessages)
|
||||
|
||||
// Step 4: Convert to AI SDK messages.
|
||||
const aiSdkMessages = convertToAiSdkMessages(cleanedMessages)
|
||||
const aiSdkMessages = cleanedMessages as ModelMessage[]
|
||||
|
||||
// Step 5: Re-inject encrypted reasoning as properly-formed AI SDK reasoning
|
||||
// parts with providerOptions.openai.itemId and reasoningEncryptedContent.
|
||||
if (encryptedReasoningItems.length > 0) {
|
||||
injectEncryptedReasoning(aiSdkMessages, encryptedReasoningItems, messages)
|
||||
injectEncryptedReasoning(aiSdkMessages, encryptedReasoningItems, messages as RooMessage[])
|
||||
}
|
||||
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
|||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import { createAzure } from "@ai-sdk/azure"
|
||||
import { streamText, generateText, ToolSet, LanguageModel } from "ai"
|
||||
import { streamText, generateText, ToolSet, LanguageModel, ModelMessage } from "ai"
|
||||
import axios from "axios"
|
||||
|
||||
import {
|
||||
|
|
@ -22,6 +22,7 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
|
|
@ -29,6 +30,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
// TODO: Rename this to OpenAICompatibleHandler. Also, I think the
|
||||
// `OpenAINativeHandler` can subclass from this, since it's obviously
|
||||
|
|
@ -93,7 +95,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { info: modelInfo, temperature, reasoning } = this.getModel()
|
||||
|
|
@ -104,7 +106,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
|
@ -170,7 +172,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
private async *handleStreaming(
|
||||
languageModel: LanguageModel,
|
||||
systemPrompt: string | undefined,
|
||||
messages: ReturnType<typeof convertToAiSdkMessages>,
|
||||
messages: ModelMessage[],
|
||||
temperature: number | undefined,
|
||||
tools: ToolSet | undefined,
|
||||
metadata: ApiHandlerCreateMessageMetadata | undefined,
|
||||
|
|
@ -231,6 +233,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
throw usageError
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
throw handleAiSdkError(error, this.providerName)
|
||||
}
|
||||
|
|
@ -239,7 +243,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
private async *handleNonStreaming(
|
||||
languageModel: LanguageModel,
|
||||
systemPrompt: string | undefined,
|
||||
messages: ReturnType<typeof convertToAiSdkMessages>,
|
||||
messages: ModelMessage[],
|
||||
temperature: number | undefined,
|
||||
tools: ToolSet | undefined,
|
||||
metadata: ApiHandlerCreateMessageMetadata | undefined,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||
import { streamText, generateText } from "ai"
|
||||
import { streamText, generateText, ModelMessage } from "ai"
|
||||
|
||||
import {
|
||||
type ModelRecord,
|
||||
|
|
@ -16,9 +16,13 @@ import { TelemetryService } from "@roo-code/telemetry"
|
|||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../shared/cost"
|
||||
|
||||
import { type ReasoningDetail } from "../transform/openai-format"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import { convertToAiSdkMessages, convertToolsForAiSdk, processAiSdkStreamPart } from "../transform/ai-sdk"
|
||||
import {
|
||||
convertToAiSdkMessages,
|
||||
convertToolsForAiSdk,
|
||||
processAiSdkStreamPart,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import { getModels, getModelsFromCache } from "./fetchers/modelCache"
|
||||
|
|
@ -28,13 +32,13 @@ import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-
|
|||
|
||||
import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index"
|
||||
import type { ApiStreamChunk, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
export class OpenRouterHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
protected models: ModelRecord = {}
|
||||
protected endpoints: ModelRecord = {}
|
||||
private readonly providerName = "OpenRouter"
|
||||
private currentReasoningDetails: ReasoningDetail[] = []
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
|
@ -82,10 +86,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
})
|
||||
}
|
||||
|
||||
getReasoningDetails(): ReasoningDetail[] | undefined {
|
||||
return this.currentReasoningDetails.length > 0 ? this.currentReasoningDetails : undefined
|
||||
}
|
||||
|
||||
private normalizeUsage(
|
||||
usage: { inputTokens: number; outputTokens: number },
|
||||
providerMetadata: Record<string, any> | undefined,
|
||||
|
|
@ -130,10 +130,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): AsyncGenerator<ApiStreamChunk> {
|
||||
this.currentReasoningDetails = []
|
||||
const model = await this.fetchModel()
|
||||
let { id: modelId, maxTokens, temperature, topP, reasoning } = model
|
||||
|
||||
|
|
@ -149,7 +148,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
? { "x-anthropic-beta": "fine-grained-tool-streaming-2025-05-14" }
|
||||
: undefined
|
||||
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
const openrouter = this.createOpenRouterProvider({ reasoning, headers })
|
||||
|
||||
|
|
@ -175,8 +174,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
}
|
||||
: undefined
|
||||
|
||||
let accumulatedReasoningText = ""
|
||||
|
||||
try {
|
||||
const result = streamText({
|
||||
model: openrouter.chat(modelId),
|
||||
|
|
@ -191,31 +188,12 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
})
|
||||
|
||||
for await (const part of result.fullStream) {
|
||||
if (part.type === "reasoning-delta" && part.text !== "[REDACTED]") {
|
||||
accumulatedReasoningText += part.text
|
||||
}
|
||||
yield* processAiSdkStreamPart(part)
|
||||
}
|
||||
|
||||
if (accumulatedReasoningText) {
|
||||
this.currentReasoningDetails.push({
|
||||
type: "reasoning.text",
|
||||
text: accumulatedReasoningText,
|
||||
index: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const providerMetadata =
|
||||
(await result.providerMetadata) ?? (await (result as any).experimental_providerMetadata)
|
||||
|
||||
const providerReasoningDetails = providerMetadata?.openrouter?.reasoning_details as
|
||||
| ReasoningDetail[]
|
||||
| undefined
|
||||
|
||||
if (providerReasoningDetails && providerReasoningDetails.length > 0) {
|
||||
this.currentReasoningDetails = providerReasoningDetails
|
||||
}
|
||||
|
||||
const usage = await result.usage
|
||||
const totalUsage = await result.totalUsage
|
||||
const usageChunk = this.normalizeUsage(
|
||||
|
|
@ -227,6 +205,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
model.info,
|
||||
)
|
||||
yield usageChunk
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error: any) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { ApiStream } from "../transform/stream"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { OpenAICompatibleHandler, OpenAICompatibleConfig } from "./openai-compatible"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"
|
||||
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`
|
||||
|
|
@ -274,7 +275,7 @@ export class QwenCodeHandler extends OpenAICompatibleHandler implements SingleCo
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
await this.ensureAuthenticated()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createRequesty, type RequestyProviderMetadata } from "@requesty/ai-sdk"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { type ModelInfo, type ModelRecord, requestyDefaultModelId, requestyDefaultModelInfo } from "@roo-code/types"
|
||||
|
||||
|
|
@ -23,6 +23,7 @@ import { BaseProvider } from "./base-provider"
|
|||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { toRequestyServiceUrl } from "../../shared/utils/requesty"
|
||||
import { applyRouterToolPreferences } from "./utils/router-tool-preferences"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* Requesty provider using the dedicated @requesty/ai-sdk package.
|
||||
|
|
@ -172,13 +173,13 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { info, temperature } = await this.fetchModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import { streamText, generateText } from "ai"
|
||||
import { streamText, generateText, type ModelMessage } from "ai"
|
||||
|
||||
import { rooDefaultModelId, getApiProtocol, type ImageGenerationApiMethod } from "@roo-code/types"
|
||||
import { CloudService } from "@roo-code/cloud"
|
||||
|
|
@ -11,13 +11,12 @@ import { calculateApiCostOpenAI } from "../../shared/cost"
|
|||
import { ApiStream } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
import {
|
||||
convertToAiSdkMessages,
|
||||
convertToolsForAiSdk,
|
||||
processAiSdkStreamPart,
|
||||
handleAiSdkError,
|
||||
mapToolChoice,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { type ReasoningDetail } from "../transform/openai-format"
|
||||
import type { RooReasoningParams } from "../transform/reasoning"
|
||||
import { getRooReasoning } from "../transform/reasoning"
|
||||
|
||||
|
|
@ -26,6 +25,7 @@ import { BaseProvider } from "./base-provider"
|
|||
import { getModels, getModelsFromCache } from "./fetchers/modelCache"
|
||||
import { generateImageWithProvider, generateImageWithImagesApi, ImageGenerationResult } from "./utils/image-generation"
|
||||
import { t } from "../../i18n"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
function getSessionToken(): string {
|
||||
const token = CloudService.hasInstance() ? CloudService.instance.authService?.getSessionToken() : undefined
|
||||
|
|
@ -35,7 +35,6 @@ function getSessionToken(): string {
|
|||
export class RooHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
private fetcherBaseURL: string
|
||||
private currentReasoningDetails: ReasoningDetail[] = []
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
|
@ -89,18 +88,11 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
|
|||
return true as const
|
||||
}
|
||||
|
||||
getReasoningDetails(): ReasoningDetail[] | undefined {
|
||||
return this.currentReasoningDetails.length > 0 ? this.currentReasoningDetails : undefined
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
// Reset reasoning_details accumulator for this request
|
||||
this.currentReasoningDetails = []
|
||||
|
||||
const model = this.getModel()
|
||||
const { id: modelId, info } = model
|
||||
|
||||
|
|
@ -127,11 +119,10 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
|
|||
// Create per-request provider with fresh session token
|
||||
const provider = this.createRooProvider({ reasoning, taskId: metadata?.taskId })
|
||||
|
||||
// Convert messages and tools to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
// RooMessage[] is already AI SDK-compatible, cast directly
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
const tools = convertToolsForAiSdk(this.convertToolsForOpenAI(metadata?.tools))
|
||||
|
||||
let accumulatedReasoningText = ""
|
||||
let lastStreamError: string | undefined
|
||||
|
||||
try {
|
||||
|
|
@ -146,9 +137,6 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
|
|||
})
|
||||
|
||||
for await (const part of result.fullStream) {
|
||||
if (part.type === "reasoning-delta" && part.text !== "[REDACTED]") {
|
||||
accumulatedReasoningText += part.text
|
||||
}
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
if (chunk.type === "error") {
|
||||
lastStreamError = chunk.message
|
||||
|
|
@ -157,25 +145,11 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
|
|||
}
|
||||
}
|
||||
|
||||
// Build reasoning details from accumulated text
|
||||
if (accumulatedReasoningText) {
|
||||
this.currentReasoningDetails.push({
|
||||
type: "reasoning.text",
|
||||
text: accumulatedReasoningText,
|
||||
index: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Check provider metadata for reasoning_details (override if present)
|
||||
// Check provider metadata for usage details
|
||||
const providerMetadata =
|
||||
(await result.providerMetadata) ?? (await (result as any).experimental_providerMetadata)
|
||||
const rooMeta = providerMetadata?.roo as Record<string, any> | undefined
|
||||
|
||||
const providerReasoningDetails = rooMeta?.reasoning_details as ReasoningDetail[] | undefined
|
||||
if (providerReasoningDetails && providerReasoningDetails.length > 0) {
|
||||
this.currentReasoningDetails = providerReasoningDetails
|
||||
}
|
||||
|
||||
// Process usage with protocol-aware normalization
|
||||
const usage = await result.usage
|
||||
const promptTokens = usage.inputTokens ?? 0
|
||||
|
|
@ -212,6 +186,8 @@ export class RooHandler extends BaseProvider implements SingleCompletionHandler
|
|||
cacheReadTokens: cacheRead,
|
||||
totalCost,
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
if (lastStreamError) {
|
||||
throw new Error(lastStreamError)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createSambaNova } from "sambanova-ai-provider"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import { sambaNovaModels, sambaNovaDefaultModelId, type ModelInfo } from "@roo-code/types"
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
const SAMBANOVA_DEFAULT_TEMPERATURE = 0.7
|
||||
|
||||
|
|
@ -110,18 +111,16 @@ export class SambaNovaHandler extends BaseProvider implements SingleCompletionHa
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { temperature, info } = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
// For models that don't support multi-part content (like DeepSeek), flatten messages to string content
|
||||
// SambaNova's DeepSeek models expect string content, not array content
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages, {
|
||||
transform: info.supportsImages ? undefined : flattenAiSdkMessagesToStringContent,
|
||||
})
|
||||
const castMessages = messages as ModelMessage[]
|
||||
const aiSdkMessages = info.supportsImages ? castMessages : flattenAiSdkMessagesToStringContent(castMessages)
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createGateway, streamText, generateText, ToolSet } from "ai"
|
||||
import { createGateway, streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import {
|
||||
vercelAiGatewayDefaultModelId,
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ import { DEFAULT_HEADERS } from "./constants"
|
|||
import { BaseProvider } from "./base-provider"
|
||||
import { getModels, getModelsFromCache } from "./fetchers/modelCache"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* Vercel AI Gateway provider using the built-in AI SDK gateway support.
|
||||
|
|
@ -108,13 +110,13 @@ export class VercelAiGatewayHandler extends BaseProvider implements SingleComple
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { id: modelId, info } = await this.fetchModel()
|
||||
const languageModel = this.getLanguageModel(modelId)
|
||||
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
|
@ -157,6 +159,8 @@ export class VercelAiGatewayHandler extends BaseProvider implements SingleComple
|
|||
}
|
||||
throw usageError
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
throw handleAiSdkError(error, "Vercel AI Gateway")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createVertex, type GoogleVertexProvider } from "@ai-sdk/google-vertex"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import {
|
||||
type ModelInfo,
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
processAiSdkStreamPart,
|
||||
mapToolChoice,
|
||||
handleAiSdkError,
|
||||
yieldResponseMessage,
|
||||
} from "../transform/ai-sdk"
|
||||
import { t } from "i18next"
|
||||
import type { ApiStream, ApiStreamUsageChunk, GroundingSource } from "../transform/stream"
|
||||
|
|
@ -27,6 +28,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* Vertex AI provider using the dedicated @ai-sdk/google-vertex package.
|
||||
|
|
@ -36,7 +38,6 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
protected options: ApiHandlerOptions
|
||||
protected provider: GoogleVertexProvider
|
||||
private readonly providerName = "Vertex"
|
||||
private lastThoughtSignature: string | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
|
@ -65,7 +66,7 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
async *createMessage(
|
||||
systemInstruction: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { id: modelId, info, reasoning: thinkingConfig, maxTokens } = this.getModel()
|
||||
|
|
@ -95,7 +96,7 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
// Anthropic.MessageParam values and will cause failures.
|
||||
type ReasoningMetaLike = { type?: string }
|
||||
|
||||
const filteredMessages = messages.filter((message): message is Anthropic.Messages.MessageParam => {
|
||||
const filteredMessages = messages.filter((message) => {
|
||||
const meta = message as ReasoningMetaLike
|
||||
if (meta.type === "reasoning") {
|
||||
return false
|
||||
|
|
@ -104,7 +105,7 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
})
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(filteredMessages)
|
||||
const aiSdkMessages = filteredMessages as ModelMessage[]
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
let openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
@ -140,27 +141,12 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
|
||||
try {
|
||||
// Reset thought signature for this request
|
||||
this.lastThoughtSignature = undefined
|
||||
|
||||
// Use streamText for streaming responses
|
||||
const result = streamText(requestOptions)
|
||||
|
||||
// Process the full stream to get all events including reasoning
|
||||
let lastStreamError: string | undefined
|
||||
for await (const part of result.fullStream) {
|
||||
// Capture thoughtSignature from tool-call events (Gemini 3 thought signatures)
|
||||
// The AI SDK's tool-call event includes providerMetadata with the signature
|
||||
// Vertex AI stores it under the "vertex" key in providerMetadata
|
||||
if (part.type === "tool-call") {
|
||||
const vertexMeta = (part as any).providerMetadata?.vertex
|
||||
const googleMeta = (part as any).providerMetadata?.google
|
||||
const sig = vertexMeta?.thoughtSignature ?? googleMeta?.thoughtSignature
|
||||
if (sig) {
|
||||
this.lastThoughtSignature = sig
|
||||
}
|
||||
}
|
||||
|
||||
for (const chunk of processAiSdkStreamPart(part)) {
|
||||
if (chunk.type === "error") {
|
||||
lastStreamError = chunk.message
|
||||
|
|
@ -200,6 +186,8 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
throw usageError
|
||||
}
|
||||
|
||||
yield* yieldResponseMessage(result)
|
||||
} catch (error) {
|
||||
throw handleAiSdkError(error, this.providerName, {
|
||||
onError: (msg) => {
|
||||
|
|
@ -417,13 +405,4 @@ export class VertexHandler extends BaseProvider implements SingleCompletionHandl
|
|||
override isAiSdkProvider(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thought signature captured from the last Vertex AI response.
|
||||
* Gemini 3 models return thoughtSignature on function call parts,
|
||||
* which must be round-tripped back for tool use continuations.
|
||||
*/
|
||||
getThoughtSignature(): string | undefined {
|
||||
return this.lastThoughtSignature
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { convertToVsCodeLmMessages, extractTextCountFromMessage } from "../trans
|
|||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* Converts OpenAI-format tools to VSCode Language Model tools.
|
||||
|
|
@ -364,7 +365,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
|
|||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
// Ensure clean state before starting a new request
|
||||
|
|
@ -374,13 +375,13 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
|
|||
// Process messages
|
||||
const cleanedMessages = messages.map((msg) => ({
|
||||
...msg,
|
||||
content: this.cleanMessageContent(msg.content),
|
||||
...("content" in msg ? { content: this.cleanMessageContent((msg as any).content) } : {}),
|
||||
}))
|
||||
|
||||
// Convert Anthropic messages to VS Code LM messages
|
||||
// Convert messages to VS Code LM messages
|
||||
const vsCodeLmMessages: vscode.LanguageModelChatMessage[] = [
|
||||
vscode.LanguageModelChatMessage.Assistant(systemPrompt),
|
||||
...convertToVsCodeLmMessages(cleanedMessages),
|
||||
...convertToVsCodeLmMessages(cleanedMessages as any),
|
||||
]
|
||||
|
||||
// Initialize cancellation token for the request
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
const XAI_DEFAULT_TEMPERATURE = 0
|
||||
|
||||
|
|
@ -118,14 +119,14 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { temperature, reasoning } = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
// Convert messages to AI SDK format
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages
|
||||
|
||||
// Convert tools to OpenAI format first, then to AI SDK format
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { createZhipu } from "zhipu-ai-provider"
|
||||
import { streamText, generateText, ToolSet } from "ai"
|
||||
import { streamText, generateText, ToolSet, ModelMessage } from "ai"
|
||||
|
||||
import {
|
||||
internationalZAiModels,
|
||||
|
|
@ -27,6 +27,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import type { RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* Z.ai provider using the dedicated zhipu-ai-provider package.
|
||||
|
|
@ -91,13 +92,13 @@ export class ZAiHandler extends BaseProvider implements SingleCompletionHandler
|
|||
*/
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { id: modelId, info, temperature } = this.getModel()
|
||||
const languageModel = this.getLanguageModel()
|
||||
|
||||
const aiSdkMessages = convertToAiSdkMessages(messages)
|
||||
const aiSdkMessages = messages as ModelMessage[]
|
||||
|
||||
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
|
||||
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import type { ModelInfo } from "@roo-code/types"
|
||||
|
||||
import { ApiHandler } from "../../index"
|
||||
import { ApiMessage } from "../../../core/task-persistence/apiMessages"
|
||||
import { maybeRemoveImageBlocks } from "../image-cleaning"
|
||||
|
||||
describe("maybeRemoveImageBlocks", () => {
|
||||
|
|
@ -24,7 +23,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
|
||||
it("should handle empty messages array", () => {
|
||||
const apiHandler = createMockApiHandler(true)
|
||||
const messages: ApiMessage[] = []
|
||||
const messages: any[] = []
|
||||
|
||||
const result = maybeRemoveImageBlocks(messages, apiHandler)
|
||||
|
||||
|
|
@ -34,7 +33,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
|
||||
it("should not modify messages with no image blocks", () => {
|
||||
const apiHandler = createMockApiHandler(true)
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello, world!",
|
||||
|
|
@ -53,7 +52,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
|
||||
it("should not modify messages with array content but no image blocks", () => {
|
||||
const apiHandler = createMockApiHandler(true)
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -77,7 +76,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
|
||||
it("should not modify image blocks when API handler supports images", () => {
|
||||
const apiHandler = createMockApiHandler(true)
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -106,7 +105,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
|
||||
it("should convert image blocks to text descriptions when API handler doesn't support images", () => {
|
||||
const apiHandler = createMockApiHandler(false)
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -149,7 +148,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
|
||||
it("should handle mixed content messages with multiple text and image blocks", () => {
|
||||
const apiHandler = createMockApiHandler(false)
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -212,7 +211,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
|
||||
it("should handle multiple messages with image blocks", () => {
|
||||
const apiHandler = createMockApiHandler(false)
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -293,7 +292,7 @@ describe("maybeRemoveImageBlocks", () => {
|
|||
|
||||
it("should preserve additional message properties", () => {
|
||||
const apiHandler = createMockApiHandler(false)
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// npx vitest run api/transform/__tests__/openai-format.spec.ts
|
||||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import type { RooMessage } from "../../../core/task-persistence/rooMessage"
|
||||
|
||||
import {
|
||||
convertToOpenAiMessages,
|
||||
|
|
@ -13,7 +13,7 @@ import { normalizeMistralToolCallId } from "../mistral-format"
|
|||
|
||||
describe("convertToOpenAiMessages", () => {
|
||||
it("should convert simple text messages", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hello",
|
||||
|
|
@ -37,7 +37,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should handle messages with image content", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -76,8 +76,52 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should preserve AI SDK image data URLs without double-prefixing", () => {
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
image: "data:image/png;base64,already_encoded",
|
||||
mediaType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const openAiMessages = convertToOpenAiMessages(messages)
|
||||
const content = openAiMessages[0].content as Array<{ type: string; image_url?: { url: string } }>
|
||||
expect(content[0]).toEqual({
|
||||
type: "image_url",
|
||||
image_url: { url: "data:image/png;base64,already_encoded" },
|
||||
})
|
||||
})
|
||||
|
||||
it("should preserve AI SDK image http URLs without converting to data URLs", () => {
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
image: "https://example.com/image.png",
|
||||
mediaType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const openAiMessages = convertToOpenAiMessages(messages)
|
||||
const content = openAiMessages[0].content as Array<{ type: string; image_url?: { url: string } }>
|
||||
expect(content[0]).toEqual({
|
||||
type: "image_url",
|
||||
image_url: { url: "https://example.com/image.png" },
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle assistant messages with tool use (no normalization without normalizeToolCallId)", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -113,7 +157,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should handle user messages with tool results (no normalization without normalizeToolCallId)", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -136,7 +180,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should normalize tool call IDs when normalizeToolCallId function is provided", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -173,7 +217,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should not normalize tool call IDs when normalizeToolCallId function is not provided", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -208,7 +252,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should use custom normalization function when provided", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -235,7 +279,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
// have content set to "" instead of undefined. Gemini (via OpenRouter) requires
|
||||
// every message to have at least one "parts" field, which fails if content is undefined.
|
||||
// See: ROO-425
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -265,7 +309,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
// of an empty string. Gemini (via OpenRouter) requires function responses to have
|
||||
// non-empty content in the "parts" field, and an empty string causes validation failure
|
||||
// with error: "Unable to submit request because it must include at least one parts field"
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -289,7 +333,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it('should use "(empty)" placeholder for tool result with undefined content (Gemini compatibility)', () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -297,7 +341,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
type: "tool_result",
|
||||
tool_use_id: "tool-456",
|
||||
// content is undefined/not provided
|
||||
} as Anthropic.ToolResultBlockParam,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
@ -311,7 +355,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it('should use "(empty)" placeholder for tool result with empty array content (Gemini compatibility)', () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -319,7 +363,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
type: "tool_result",
|
||||
tool_use_id: "tool-789",
|
||||
content: [], // Empty array
|
||||
} as Anthropic.ToolResultBlockParam,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
@ -337,7 +381,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
// This test ensures that user messages with empty text blocks are filtered out
|
||||
// to prevent "must include at least one parts field" error from Gemini (via OpenRouter).
|
||||
// Empty text blocks can occur in edge cases during message construction.
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -365,7 +409,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
|
||||
it("should not create user message when all text blocks are empty (Gemini compatibility)", () => {
|
||||
// If all text blocks are empty, no user message should be created
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -387,7 +431,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should preserve image blocks when filtering empty text blocks", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -426,7 +470,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
|
||||
describe("mergeToolResultText option", () => {
|
||||
it("should merge text content into last tool message when mergeToolResultText is true", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -456,7 +500,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should merge text into last tool message when multiple tool results exist", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -489,7 +533,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should NOT merge text when images are present (fall back to user message)", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -519,7 +563,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should create separate user message when mergeToolResultText is false", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -548,7 +592,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should work with normalizeToolCallId when mergeToolResultText is true", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -581,7 +625,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should handle user messages with only text content (no tool results)", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -906,7 +950,7 @@ describe("convertToOpenAiMessages", () => {
|
|||
})
|
||||
|
||||
it("should handle messages without reasoning_details", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
const anthropicMessages: any[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Simple response" }],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { tool as createTool, jsonSchema, type ModelMessage, type TextStreamPart } from "ai"
|
||||
import type { AssistantModelMessage } from "ai"
|
||||
import type { ApiStreamChunk, ApiStream } from "./stream"
|
||||
|
||||
/**
|
||||
|
|
@ -512,6 +513,7 @@ export async function* consumeAiSdkStream(
|
|||
result: {
|
||||
fullStream: AsyncIterable<ExtendedStreamPart>
|
||||
usage: PromiseLike<{ inputTokens?: number; outputTokens?: number }>
|
||||
response?: PromiseLike<{ messages?: Array<{ role: string; content: unknown; providerOptions?: unknown }> }>
|
||||
},
|
||||
usageHandler?: () => AsyncGenerator<ApiStreamChunk>,
|
||||
): ApiStream {
|
||||
|
|
@ -545,6 +547,31 @@ export async function* consumeAiSdkStream(
|
|||
}
|
||||
throw usageError
|
||||
}
|
||||
|
||||
// Yield the AI SDK's fully-formed assistant message for direct storage
|
||||
yield* yieldResponseMessage(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Await `result.response` and yield the assistant message from `response.messages`.
|
||||
* Used by both `consumeAiSdkStream` and providers with manual `fullStream` iteration.
|
||||
*/
|
||||
export async function* yieldResponseMessage(result: {
|
||||
response?: PromiseLike<{ messages?: Array<{ role: string; content: unknown; providerOptions?: unknown }> }>
|
||||
}): ApiStream {
|
||||
if (!result.response) return
|
||||
try {
|
||||
const response = await result.response
|
||||
if (response.messages && response.messages.length > 0) {
|
||||
const assistantMsg = response.messages.find((m) => m.role === "assistant")
|
||||
if (assistantMsg) {
|
||||
yield { type: "response_message", message: assistantMsg as AssistantModelMessage }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// response resolution can fail if the stream errored — ignore silently
|
||||
// since the stream error is already surfaced via lastStreamError
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
import { ApiMessage } from "../../core/task-persistence/apiMessages"
|
||||
import { type RooMessage } from "../../core/task-persistence/rooMessage"
|
||||
|
||||
import { ApiHandler } from "../index"
|
||||
|
||||
/* Removes image blocks from messages if they are not supported by the Api Handler */
|
||||
export function maybeRemoveImageBlocks(messages: ApiMessage[], apiHandler: ApiHandler): ApiMessage[] {
|
||||
export function maybeRemoveImageBlocks(messages: RooMessage[], apiHandler: ApiHandler): RooMessage[] {
|
||||
// Check model capability ONCE instead of for every message
|
||||
const supportsImages = apiHandler.getModel().info.supportsImages
|
||||
|
||||
if (supportsImages) {
|
||||
return messages
|
||||
}
|
||||
|
||||
return messages.map((message) => {
|
||||
// Handle array content (could contain image blocks).
|
||||
let { content } = message
|
||||
if (Array.isArray(content)) {
|
||||
if (!supportsImages) {
|
||||
// Convert image blocks to text descriptions.
|
||||
content = content.map((block) => {
|
||||
if (block.type === "image") {
|
||||
// Convert image blocks to text descriptions.
|
||||
// Note: We can't access the actual image content/url due to API limitations,
|
||||
// but we can indicate that an image was present in the conversation.
|
||||
return {
|
||||
type: "text",
|
||||
text: "[Referenced image in conversation]",
|
||||
}
|
||||
}
|
||||
return block
|
||||
})
|
||||
}
|
||||
// Only process messages with a role and array content
|
||||
if (!("role" in message) || !Array.isArray(message.content)) {
|
||||
return message
|
||||
}
|
||||
return { ...message, content }
|
||||
|
||||
const content = message.content.map((block: any) => {
|
||||
if (block.type === "image") {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: "[Referenced image in conversation]",
|
||||
}
|
||||
}
|
||||
return block
|
||||
})
|
||||
|
||||
return { ...message, content } as typeof message
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,18 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import {
|
||||
type RooMessage,
|
||||
type RooRoleMessage,
|
||||
type AnyToolCallBlock,
|
||||
type AnyToolResultBlock,
|
||||
isRooRoleMessage,
|
||||
isAnyToolCallBlock,
|
||||
isAnyToolResultBlock,
|
||||
getToolCallId,
|
||||
getToolCallName,
|
||||
getToolCallInput,
|
||||
getToolResultCallId,
|
||||
getToolResultContent,
|
||||
} from "../../core/task-persistence/rooMessage"
|
||||
|
||||
/**
|
||||
* Type for OpenRouter's reasoning detail elements.
|
||||
|
|
@ -145,6 +158,12 @@ export function consolidateReasoningDetails(reasoningDetails: ReasoningDetail[])
|
|||
return consolidated
|
||||
}
|
||||
|
||||
/**
|
||||
* A RooRoleMessage that may carry `reasoning_details` from OpenAI/OpenRouter providers.
|
||||
* Used to type-narrow instead of `as any` when accessing reasoning metadata.
|
||||
*/
|
||||
type MessageWithReasoningDetails = RooRoleMessage & { reasoning_details?: ReasoningDetail[] }
|
||||
|
||||
/**
|
||||
* Sanitizes OpenAI messages for Gemini models by filtering reasoning_details
|
||||
* to only include entries that match the tool call IDs.
|
||||
|
|
@ -254,17 +273,17 @@ export function sanitizeGeminiMessages(
|
|||
}
|
||||
|
||||
/**
|
||||
* Options for converting Anthropic messages to OpenAI format.
|
||||
* Options for converting messages to OpenAI format.
|
||||
*/
|
||||
export interface ConvertToOpenAiMessagesOptions {
|
||||
/**
|
||||
* Optional function to normalize tool call IDs for providers with strict ID requirements.
|
||||
* When provided, this function will be applied to all tool_use IDs and tool_result tool_use_ids.
|
||||
* When provided, this function will be applied to all tool call IDs.
|
||||
* This allows callers to declare provider-specific ID format requirements.
|
||||
*/
|
||||
normalizeToolCallId?: (id: string) => string
|
||||
/**
|
||||
* If true, merge text content after tool_results into the last tool message
|
||||
* If true, merge text content after tool results into the last tool message
|
||||
* instead of creating a separate user message. This is critical for providers
|
||||
* with reasoning/thinking models (like DeepSeek-reasoner, GLM-4.7, etc.) where
|
||||
* a user message after tool results causes the model to drop all previous
|
||||
|
|
@ -273,8 +292,13 @@ export interface ConvertToOpenAiMessagesOptions {
|
|||
mergeToolResultText?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts RooMessage[] to OpenAI chat completion messages.
|
||||
* Handles both AI SDK format (tool-call/tool-result) and legacy Anthropic format
|
||||
* (tool_use/tool_result) for backward compatibility with persisted data.
|
||||
*/
|
||||
export function convertToOpenAiMessages(
|
||||
anthropicMessages: Anthropic.Messages.MessageParam[],
|
||||
messages: RooMessage[],
|
||||
options?: ConvertToOpenAiMessagesOptions,
|
||||
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = []
|
||||
|
|
@ -300,208 +324,230 @@ export function convertToOpenAiMessages(
|
|||
// Use provided normalization function or identity function
|
||||
const normalizeId = options?.normalizeToolCallId ?? ((id: string) => id)
|
||||
|
||||
for (const anthropicMessage of anthropicMessages) {
|
||||
if (typeof anthropicMessage.content === "string") {
|
||||
// Some upstream transforms (e.g. [`Task.buildCleanConversationHistory()`](src/core/task/Task.ts:4048))
|
||||
// will convert a single text block into a string for compactness.
|
||||
// If a message also contains reasoning_details (Gemini 3 / xAI / o-series, etc.),
|
||||
// we must preserve it here as well.
|
||||
const messageWithDetails = anthropicMessage as any
|
||||
/** Get image data URL from either AI SDK or legacy format. */
|
||||
const getImageDataUrl = (part: {
|
||||
type: string
|
||||
image?: string
|
||||
mediaType?: string
|
||||
source?: { media_type?: string; data?: string }
|
||||
}): string => {
|
||||
// AI SDK format:
|
||||
// - raw base64 + mediaType: construct data URL
|
||||
// - existing data/http(s) URL in image: pass through unchanged
|
||||
if (part.image) {
|
||||
const image = part.image.trim()
|
||||
if (image.startsWith("data:") || /^https?:\/\//i.test(image)) {
|
||||
return image
|
||||
}
|
||||
if (part.mediaType) {
|
||||
return `data:${part.mediaType};base64,${image}`
|
||||
}
|
||||
}
|
||||
// Legacy Anthropic format: { type: "image", source: { media_type, data } }
|
||||
if (part.source?.media_type && part.source?.data) {
|
||||
return `data:${part.source.media_type};base64,${part.source.data}`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
// Skip RooReasoningMessage (no role property)
|
||||
if (!("role" in message)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
// String content: simple text message
|
||||
const messageWithDetails = message as MessageWithReasoningDetails
|
||||
const baseMessage: OpenAI.Chat.ChatCompletionMessageParam & { reasoning_details?: any[] } = {
|
||||
role: anthropicMessage.role,
|
||||
content: anthropicMessage.content,
|
||||
role: message.role as "user" | "assistant",
|
||||
content: message.content,
|
||||
}
|
||||
|
||||
if (anthropicMessage.role === "assistant") {
|
||||
const mapped = mapReasoningDetails(messageWithDetails.reasoning_details)
|
||||
if (mapped) {
|
||||
;(baseMessage as any).reasoning_details = mapped
|
||||
}
|
||||
}
|
||||
|
||||
openAiMessages.push(baseMessage)
|
||||
} else {
|
||||
// image_url.url is base64 encoded image data
|
||||
// ensure it contains the content-type of the image: data:image/png;base64,
|
||||
/*
|
||||
{ role: "user", content: "" | { type: "text", text: string } | { type: "image_url", image_url: { url: string } } },
|
||||
// content required unless tool_calls is present
|
||||
{ role: "assistant", content?: "" | null, tool_calls?: [{ id: "", function: { name: "", arguments: "" }, type: "function" }] },
|
||||
{ role: "tool", tool_call_id: "", content: ""}
|
||||
*/
|
||||
if (anthropicMessage.role === "user") {
|
||||
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
|
||||
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
|
||||
toolMessages: Anthropic.ToolResultBlockParam[]
|
||||
}>(
|
||||
(acc, part) => {
|
||||
if (part.type === "tool_result") {
|
||||
acc.toolMessages.push(part)
|
||||
} else if (part.type === "text" || part.type === "image") {
|
||||
acc.nonToolMessages.push(part)
|
||||
} // user cannot send tool_use messages
|
||||
return acc
|
||||
},
|
||||
{ nonToolMessages: [], toolMessages: [] },
|
||||
)
|
||||
|
||||
// Process tool result messages FIRST since they must follow the tool use messages
|
||||
let toolResultImages: Anthropic.Messages.ImageBlockParam[] = []
|
||||
toolMessages.forEach((toolMessage) => {
|
||||
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the OpenAI SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
|
||||
let content: string
|
||||
|
||||
if (typeof toolMessage.content === "string") {
|
||||
content = toolMessage.content
|
||||
} else {
|
||||
content =
|
||||
toolMessage.content
|
||||
?.map((part) => {
|
||||
if (part.type === "image") {
|
||||
toolResultImages.push(part)
|
||||
return "(see following user message for image)"
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n") ?? ""
|
||||
}
|
||||
openAiMessages.push({
|
||||
role: "tool",
|
||||
tool_call_id: normalizeId(toolMessage.tool_use_id),
|
||||
// Use "(empty)" placeholder for empty content to satisfy providers like Gemini (via OpenRouter)
|
||||
content: content || "(empty)",
|
||||
})
|
||||
})
|
||||
|
||||
// If tool results contain images, send as a separate user message
|
||||
// I ran into an issue where if I gave feedback for one of many tool uses, the request would fail.
|
||||
// "Messages following `tool_use` blocks must begin with a matching number of `tool_result` blocks."
|
||||
// Therefore we need to send these images after the tool result messages
|
||||
// NOTE: it's actually okay to have multiple user messages in a row, the model will treat them as a continuation of the same input (this way works better than combining them into one message, since the tool result specifically mentions (see following user message for image)
|
||||
// UPDATE v2.0: we don't use tools anymore, but if we did it's important to note that the openrouter prompt caching mechanism requires one user message at a time, so we would need to add these images to the user content array instead.
|
||||
// if (toolResultImages.length > 0) {
|
||||
// openAiMessages.push({
|
||||
// role: "user",
|
||||
// content: toolResultImages.map((part) => ({
|
||||
// type: "image_url",
|
||||
// image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
|
||||
// })),
|
||||
// })
|
||||
// }
|
||||
|
||||
// Process non-tool messages
|
||||
// Filter out empty text blocks to prevent "must include at least one parts field" error
|
||||
// from Gemini (via OpenRouter). Images always have content (base64 data).
|
||||
const filteredNonToolMessages = nonToolMessages.filter(
|
||||
(part) => part.type === "image" || (part.type === "text" && part.text),
|
||||
)
|
||||
|
||||
if (filteredNonToolMessages.length > 0) {
|
||||
// Check if we should merge text into the last tool message
|
||||
// This is critical for reasoning/thinking models where a user message
|
||||
// after tool results causes the model to drop all previous reasoning_content
|
||||
const hasOnlyTextContent = filteredNonToolMessages.every((part) => part.type === "text")
|
||||
const hasToolMessages = toolMessages.length > 0
|
||||
const shouldMergeIntoToolMessage =
|
||||
options?.mergeToolResultText && hasToolMessages && hasOnlyTextContent
|
||||
|
||||
if (shouldMergeIntoToolMessage) {
|
||||
// Merge text content into the last tool message
|
||||
const lastToolMessage = openAiMessages[
|
||||
openAiMessages.length - 1
|
||||
] as OpenAI.Chat.ChatCompletionToolMessageParam
|
||||
if (lastToolMessage?.role === "tool") {
|
||||
const additionalText = filteredNonToolMessages
|
||||
.map((part) => (part as Anthropic.TextBlockParam).text)
|
||||
.join("\n")
|
||||
lastToolMessage.content = `${lastToolMessage.content}\n\n${additionalText}`
|
||||
}
|
||||
} else {
|
||||
// Standard behavior: add user message with text/image content
|
||||
openAiMessages.push({
|
||||
role: "user",
|
||||
content: filteredNonToolMessages.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return {
|
||||
type: "image_url",
|
||||
image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
|
||||
}
|
||||
}
|
||||
return { type: "text", text: part.text }
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (anthropicMessage.role === "assistant") {
|
||||
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
|
||||
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
|
||||
toolMessages: Anthropic.ToolUseBlockParam[]
|
||||
}>(
|
||||
(acc, part) => {
|
||||
if (part.type === "tool_use") {
|
||||
acc.toolMessages.push(part)
|
||||
} else if (part.type === "text" || part.type === "image") {
|
||||
acc.nonToolMessages.push(part)
|
||||
} // assistant cannot send tool_result messages
|
||||
return acc
|
||||
},
|
||||
{ nonToolMessages: [], toolMessages: [] },
|
||||
)
|
||||
|
||||
// Process non-tool messages
|
||||
let content: string | undefined
|
||||
if (nonToolMessages.length > 0) {
|
||||
content = nonToolMessages
|
||||
.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return "" // impossible as the assistant cannot send images
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
// Process tool use messages
|
||||
let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
|
||||
id: normalizeId(toolMessage.id),
|
||||
type: "function",
|
||||
function: {
|
||||
name: toolMessage.name,
|
||||
// json string
|
||||
arguments: JSON.stringify(toolMessage.input),
|
||||
},
|
||||
}))
|
||||
|
||||
// Check if the message has reasoning_details (used by Gemini 3, xAI, etc.)
|
||||
const messageWithDetails = anthropicMessage as any
|
||||
|
||||
// Build message with reasoning_details BEFORE tool_calls to preserve
|
||||
// the order expected by providers like Roo. Property order matters
|
||||
// when sending messages back to some APIs.
|
||||
const baseMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam & {
|
||||
reasoning_details?: any[]
|
||||
} = {
|
||||
role: "assistant",
|
||||
// Use empty string instead of undefined for providers like Gemini (via OpenRouter)
|
||||
// that require every message to have content in the "parts" field
|
||||
content: content ?? "",
|
||||
}
|
||||
|
||||
// Pass through reasoning_details to preserve the original shape from the API.
|
||||
// The `id` field is stripped from openai-responses-v1 blocks (see mapReasoningDetails).
|
||||
if (message.role === "assistant") {
|
||||
const mapped = mapReasoningDetails(messageWithDetails.reasoning_details)
|
||||
if (mapped) {
|
||||
baseMessage.reasoning_details = mapped
|
||||
}
|
||||
}
|
||||
|
||||
// Add tool_calls after reasoning_details
|
||||
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
|
||||
if (tool_calls.length > 0) {
|
||||
baseMessage.tool_calls = tool_calls
|
||||
openAiMessages.push(baseMessage)
|
||||
} else if (message.role === "tool") {
|
||||
// RooToolMessage: each tool-result → OpenAI tool message
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const part of message.content) {
|
||||
if (isAnyToolResultBlock(part as { type: string })) {
|
||||
const resultBlock = part as AnyToolResultBlock
|
||||
const rawContent = getToolResultContent(resultBlock)
|
||||
let content: string
|
||||
if (typeof rawContent === "string") {
|
||||
content = rawContent
|
||||
} else if (rawContent && typeof rawContent === "object" && "value" in rawContent) {
|
||||
content = String((rawContent as { value: unknown }).value)
|
||||
} else {
|
||||
content = rawContent ? JSON.stringify(rawContent) : ""
|
||||
}
|
||||
openAiMessages.push({
|
||||
role: "tool",
|
||||
tool_call_id: normalizeId(getToolResultCallId(resultBlock)),
|
||||
content: content || "(empty)",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (message.role === "user") {
|
||||
// User message: separate tool results from text/image content
|
||||
// Persisted data may contain legacy Anthropic tool_result blocks alongside AI SDK parts,
|
||||
// so we widen the element type to handle all possible block shapes.
|
||||
const contentArray: Array<{ type: string }> = Array.isArray(message.content)
|
||||
? (message.content as unknown as Array<{ type: string }>)
|
||||
: []
|
||||
|
||||
const nonToolMessages: Array<{ type: string; text?: unknown; [k: string]: unknown }> = []
|
||||
const toolMessages: AnyToolResultBlock[] = []
|
||||
|
||||
for (const part of contentArray) {
|
||||
if (isAnyToolResultBlock(part)) {
|
||||
toolMessages.push(part)
|
||||
} else if (part.type === "text" || part.type === "image") {
|
||||
nonToolMessages.push(part as { type: string; text?: unknown; [k: string]: unknown })
|
||||
}
|
||||
}
|
||||
|
||||
// Process tool result messages FIRST
|
||||
toolMessages.forEach((toolMessage) => {
|
||||
const rawContent = getToolResultContent(toolMessage)
|
||||
let content: string
|
||||
|
||||
if (typeof rawContent === "string") {
|
||||
content = rawContent
|
||||
} else if (Array.isArray(rawContent)) {
|
||||
content =
|
||||
rawContent
|
||||
.map((part: { type: string; text?: string }) => {
|
||||
if (part.type === "image") {
|
||||
return "(see following user message for image)"
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n") ?? ""
|
||||
} else if (rawContent && typeof rawContent === "object" && "value" in rawContent) {
|
||||
content = String((rawContent as { value: unknown }).value)
|
||||
} else {
|
||||
content = rawContent ? JSON.stringify(rawContent) : ""
|
||||
}
|
||||
|
||||
openAiMessages.push(baseMessage)
|
||||
openAiMessages.push({
|
||||
role: "tool",
|
||||
tool_call_id: normalizeId(getToolResultCallId(toolMessage)),
|
||||
content: content || "(empty)",
|
||||
})
|
||||
})
|
||||
|
||||
// Process non-tool messages
|
||||
// Filter out empty text blocks to prevent "must include at least one parts field" error
|
||||
const filteredNonToolMessages = nonToolMessages.filter(
|
||||
(part) => part.type === "image" || (part.type === "text" && part.text),
|
||||
)
|
||||
|
||||
if (filteredNonToolMessages.length > 0) {
|
||||
const hasOnlyTextContent = filteredNonToolMessages.every((part) => part.type === "text")
|
||||
const hasToolMessages = toolMessages.length > 0
|
||||
const shouldMergeIntoToolMessage = options?.mergeToolResultText && hasToolMessages && hasOnlyTextContent
|
||||
|
||||
if (shouldMergeIntoToolMessage) {
|
||||
const lastToolMessage = openAiMessages[
|
||||
openAiMessages.length - 1
|
||||
] as OpenAI.Chat.ChatCompletionToolMessageParam
|
||||
if (lastToolMessage?.role === "tool") {
|
||||
const additionalText = filteredNonToolMessages.map((part) => String(part.text ?? "")).join("\n")
|
||||
lastToolMessage.content = `${lastToolMessage.content}\n\n${additionalText}`
|
||||
}
|
||||
} else {
|
||||
openAiMessages.push({
|
||||
role: "user",
|
||||
content: filteredNonToolMessages.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return {
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: getImageDataUrl(
|
||||
part as {
|
||||
type: string
|
||||
image?: string
|
||||
mediaType?: string
|
||||
source?: { media_type?: string; data?: string }
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
return { type: "text", text: String(part.text ?? "") }
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (message.role === "assistant") {
|
||||
// Assistant message: separate tool calls from text content
|
||||
// Persisted data may contain legacy Anthropic tool_use blocks, so we widen
|
||||
// the element type to accommodate both AI SDK and legacy block shapes.
|
||||
const contentArray: Array<{ type: string }> = Array.isArray(message.content)
|
||||
? (message.content as unknown as Array<{ type: string }>)
|
||||
: []
|
||||
|
||||
const nonToolMessages: Array<{ type: string; text?: unknown }> = []
|
||||
const toolCallMessages: AnyToolCallBlock[] = []
|
||||
|
||||
for (const part of contentArray) {
|
||||
if (isAnyToolCallBlock(part)) {
|
||||
toolCallMessages.push(part)
|
||||
} else if (part.type === "text" || part.type === "image") {
|
||||
nonToolMessages.push(part as { type: string; text?: unknown })
|
||||
}
|
||||
}
|
||||
|
||||
// Process non-tool messages
|
||||
let content: string | undefined
|
||||
if (nonToolMessages.length > 0) {
|
||||
content = nonToolMessages
|
||||
.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return ""
|
||||
}
|
||||
return part.text as string
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
// Process tool call messages
|
||||
let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolCallMessages.map((tc) => ({
|
||||
id: normalizeId(getToolCallId(tc)),
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: getToolCallName(tc),
|
||||
arguments: JSON.stringify(getToolCallInput(tc)),
|
||||
},
|
||||
}))
|
||||
|
||||
const messageWithDetails = message as MessageWithReasoningDetails
|
||||
|
||||
const baseMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam & {
|
||||
reasoning_details?: any[]
|
||||
} = {
|
||||
role: "assistant",
|
||||
content: content ?? "",
|
||||
}
|
||||
|
||||
const mapped = mapReasoningDetails(messageWithDetails.reasoning_details)
|
||||
if (mapped) {
|
||||
baseMessage.reasoning_details = mapped
|
||||
}
|
||||
|
||||
if (tool_calls.length > 0) {
|
||||
baseMessage.tool_calls = tool_calls
|
||||
}
|
||||
|
||||
openAiMessages.push(baseMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { AssistantModelMessage } from "ai"
|
||||
|
||||
export type ApiStream = AsyncGenerator<ApiStreamChunk>
|
||||
|
||||
export type ApiStreamChunk =
|
||||
|
|
@ -11,6 +13,7 @@ export type ApiStreamChunk =
|
|||
| ApiStreamToolCallDeltaChunk
|
||||
| ApiStreamToolCallEndChunk
|
||||
| ApiStreamToolCallPartialChunk
|
||||
| ApiStreamResponseMessageChunk
|
||||
| ApiStreamError
|
||||
|
||||
export interface ApiStreamError {
|
||||
|
|
@ -107,6 +110,15 @@ export interface ApiStreamToolCallPartialChunk {
|
|||
arguments?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Carries the fully-formed assistant message from the AI SDK's `result.response.messages`.
|
||||
* Yielded after streaming completes so Task.ts can store it directly without manual reconstruction.
|
||||
*/
|
||||
export interface ApiStreamResponseMessageChunk {
|
||||
type: "response_message"
|
||||
message: AssistantModelMessage
|
||||
}
|
||||
|
||||
export interface GroundingSource {
|
||||
title: string
|
||||
url: string
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
|
|||
currentStreamingContentIndex: 0,
|
||||
assistantMessageContent: [],
|
||||
userMessageContent: [],
|
||||
pendingToolResults: [],
|
||||
didCompleteReadingStream: false,
|
||||
didRejectTool: false,
|
||||
didAlreadyUseTool: false,
|
||||
|
|
@ -66,13 +67,13 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
|
|||
|
||||
// Add pushToolResultToUserContent method after mockTask is created so it can reference mockTask
|
||||
mockTask.pushToolResultToUserContent = vi.fn().mockImplementation((toolResult: any) => {
|
||||
const existingResult = mockTask.userMessageContent.find(
|
||||
(block: any) => block.type === "tool_result" && block.tool_use_id === toolResult.tool_use_id,
|
||||
const existingResult = mockTask.pendingToolResults.find(
|
||||
(block: any) => block.type === "tool-result" && block.toolCallId === toolResult.toolCallId,
|
||||
)
|
||||
if (existingResult) {
|
||||
return false
|
||||
}
|
||||
mockTask.userMessageContent.push(toolResult)
|
||||
mockTask.pendingToolResults.push(toolResult)
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
|
@ -109,25 +110,25 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
|
|||
// Execute presentAssistantMessage
|
||||
await presentAssistantMessage(mockTask)
|
||||
|
||||
// Verify that userMessageContent was populated
|
||||
expect(mockTask.userMessageContent.length).toBeGreaterThan(0)
|
||||
// Verify that pendingToolResults was populated
|
||||
expect(mockTask.pendingToolResults.length).toBeGreaterThan(0)
|
||||
|
||||
// Find the tool_result block
|
||||
const toolResult = mockTask.userMessageContent.find(
|
||||
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
|
||||
// Find the tool-result block in pendingToolResults
|
||||
const toolResult = mockTask.pendingToolResults.find(
|
||||
(item: any) => item.type === "tool-result" && item.toolCallId === toolCallId,
|
||||
)
|
||||
|
||||
expect(toolResult).toBeDefined()
|
||||
expect(toolResult.tool_use_id).toBe(toolCallId)
|
||||
expect(toolResult.toolCallId).toBe(toolCallId)
|
||||
|
||||
// For native tool calling, tool_result content should be a string (text only)
|
||||
expect(typeof toolResult.content).toBe("string")
|
||||
expect(toolResult.content).toContain("I see a cat")
|
||||
// For native tool calling, output should be a text value
|
||||
expect(toolResult.output).toBeDefined()
|
||||
expect(toolResult.output.value).toContain("I see a cat")
|
||||
|
||||
// Images should be added as separate blocks AFTER the tool_result
|
||||
// Images should be added as separate ImagePart blocks in userMessageContent
|
||||
const imageBlocks = mockTask.userMessageContent.filter((item: any) => item.type === "image")
|
||||
expect(imageBlocks.length).toBeGreaterThan(0)
|
||||
expect(imageBlocks[0].source.data).toBe("base64ImageData")
|
||||
expect(imageBlocks[0].image).toBe("base64ImageData")
|
||||
})
|
||||
|
||||
it("should convert to string when no images are present (native tool calling)", async () => {
|
||||
|
|
@ -152,14 +153,15 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
|
|||
|
||||
await presentAssistantMessage(mockTask)
|
||||
|
||||
const toolResult = mockTask.userMessageContent.find(
|
||||
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
|
||||
const toolResult = mockTask.pendingToolResults.find(
|
||||
(item: any) => item.type === "tool-result" && item.toolCallId === toolCallId,
|
||||
)
|
||||
|
||||
expect(toolResult).toBeDefined()
|
||||
|
||||
// When no images, content should be a string
|
||||
expect(typeof toolResult.content).toBe("string")
|
||||
// When no images, output should be a text value
|
||||
expect(toolResult.output.type).toBe("text")
|
||||
expect(typeof toolResult.output.value).toBe("string")
|
||||
})
|
||||
|
||||
it("should fail fast when tool_use is missing id (legacy/XML-style tool call)", async () => {
|
||||
|
|
@ -209,13 +211,13 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
|
|||
|
||||
await presentAssistantMessage(mockTask)
|
||||
|
||||
const toolResult = mockTask.userMessageContent.find(
|
||||
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
|
||||
const toolResult = mockTask.pendingToolResults.find(
|
||||
(item: any) => item.type === "tool-result" && item.toolCallId === toolCallId,
|
||||
)
|
||||
|
||||
expect(toolResult).toBeDefined()
|
||||
// Should have fallback text
|
||||
expect(toolResult.content).toBeTruthy()
|
||||
expect(toolResult.output).toBeTruthy()
|
||||
})
|
||||
|
||||
describe("Multiple tool calls handling", () => {
|
||||
|
|
@ -246,20 +248,20 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
|
|||
mockTask.currentStreamingContentIndex = 1
|
||||
await presentAssistantMessage(mockTask)
|
||||
|
||||
// Find the tool_result for the second tool
|
||||
const toolResult = mockTask.userMessageContent.find(
|
||||
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId2,
|
||||
// Find the tool-result for the second tool in pendingToolResults
|
||||
const toolResult = mockTask.pendingToolResults.find(
|
||||
(item: any) => item.type === "tool-result" && item.toolCallId === toolCallId2,
|
||||
)
|
||||
|
||||
// Verify that a tool_result block was created (not a text block)
|
||||
// Verify that a tool-result block was created (not a text block)
|
||||
expect(toolResult).toBeDefined()
|
||||
expect(toolResult.tool_use_id).toBe(toolCallId2)
|
||||
expect(toolResult.is_error).toBe(true)
|
||||
expect(toolResult.content).toContain("due to user rejecting a previous tool")
|
||||
expect(toolResult.toolCallId).toBe(toolCallId2)
|
||||
expect(toolResult.output.value).toContain("[ERROR]")
|
||||
expect(toolResult.output.value).toContain("due to user rejecting a previous tool")
|
||||
|
||||
// Ensure no text blocks were added for this rejection
|
||||
const textBlocks = mockTask.userMessageContent.filter(
|
||||
(item: any) => item.type === "text" && item.text.includes("due to user rejecting"),
|
||||
(item: any) => item.type === "text" && item.text?.includes("due to user rejecting"),
|
||||
)
|
||||
expect(textBlocks.length).toBe(0)
|
||||
})
|
||||
|
|
@ -310,15 +312,15 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () =
|
|||
|
||||
await presentAssistantMessage(mockTask)
|
||||
|
||||
// Find the tool_result
|
||||
const toolResult = mockTask.userMessageContent.find(
|
||||
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
|
||||
// Find the tool-result in pendingToolResults
|
||||
const toolResult = mockTask.pendingToolResults.find(
|
||||
(item: any) => item.type === "tool-result" && item.toolCallId === toolCallId,
|
||||
)
|
||||
|
||||
// Verify tool_result was created for partial block
|
||||
// Verify tool-result was created for partial block
|
||||
expect(toolResult).toBeDefined()
|
||||
expect(toolResult.is_error).toBe(true)
|
||||
expect(toolResult.content).toContain("was interrupted and not executed")
|
||||
expect(toolResult.output.value).toContain("[ERROR]")
|
||||
expect(toolResult.output.value).toContain("was interrupted and not executed")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
|
|||
currentStreamingContentIndex: 0,
|
||||
assistantMessageContent: [],
|
||||
userMessageContent: [],
|
||||
pendingToolResults: [],
|
||||
didCompleteReadingStream: false,
|
||||
didRejectTool: false,
|
||||
didAlreadyUseTool: false,
|
||||
|
|
@ -62,13 +63,13 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
|
|||
|
||||
// Add pushToolResultToUserContent method after mockTask is created so 'this' binds correctly
|
||||
mockTask.pushToolResultToUserContent = vi.fn().mockImplementation((toolResult: any) => {
|
||||
const existingResult = mockTask.userMessageContent.find(
|
||||
(block: any) => block.type === "tool_result" && block.tool_use_id === toolResult.tool_use_id,
|
||||
const existingResult = mockTask.pendingToolResults.find(
|
||||
(block: any) => block.type === "tool-result" && block.toolCallId === toolResult.toolCallId,
|
||||
)
|
||||
if (existingResult) {
|
||||
return false
|
||||
}
|
||||
mockTask.userMessageContent.push(toolResult)
|
||||
mockTask.pendingToolResults.push(toolResult)
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
|
@ -89,17 +90,17 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
|
|||
// Execute presentAssistantMessage
|
||||
await presentAssistantMessage(mockTask)
|
||||
|
||||
// Verify that a tool_result with error was pushed
|
||||
const toolResult = mockTask.userMessageContent.find(
|
||||
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
|
||||
// Verify that a tool-result with error was pushed to pendingToolResults
|
||||
const toolResult = mockTask.pendingToolResults.find(
|
||||
(item: any) => item.type === "tool-result" && item.toolCallId === toolCallId,
|
||||
)
|
||||
|
||||
expect(toolResult).toBeDefined()
|
||||
expect(toolResult.tool_use_id).toBe(toolCallId)
|
||||
// The error is wrapped in JSON by formatResponse.toolError
|
||||
expect(toolResult.content).toContain("nonexistent_tool")
|
||||
expect(toolResult.content).toContain("does not exist")
|
||||
expect(toolResult.content).toContain("error")
|
||||
expect(toolResult.toolCallId).toBe(toolCallId)
|
||||
// The error is wrapped in output.value by formatResponse.toolError
|
||||
expect(toolResult.output.value).toContain("nonexistent_tool")
|
||||
expect(toolResult.output.value).toContain("does not exist")
|
||||
expect(toolResult.output.value).toContain("error")
|
||||
|
||||
// Verify consecutiveMistakeCount was incremented
|
||||
expect(mockTask.consecutiveMistakeCount).toBe(1)
|
||||
|
|
@ -169,9 +170,9 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
|
|||
const completed = await Promise.race([resultPromise, timeoutPromise])
|
||||
expect(completed).toBe(true)
|
||||
|
||||
// Verify a tool_result was pushed (critical for API not to freeze)
|
||||
const toolResult = mockTask.userMessageContent.find(
|
||||
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
|
||||
// Verify a tool-result was pushed (critical for API not to freeze)
|
||||
const toolResult = mockTask.pendingToolResults.find(
|
||||
(item: any) => item.type === "tool-result" && item.toolCallId === toolCallId,
|
||||
)
|
||||
expect(toolResult).toBeDefined()
|
||||
})
|
||||
|
|
@ -233,13 +234,13 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
|
|||
|
||||
await presentAssistantMessage(mockTask)
|
||||
|
||||
// When didRejectTool is true, should send error tool_result
|
||||
const toolResult = mockTask.userMessageContent.find(
|
||||
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
|
||||
// When didRejectTool is true, should send error tool-result
|
||||
const toolResult = mockTask.pendingToolResults.find(
|
||||
(item: any) => item.type === "tool-result" && item.toolCallId === toolCallId,
|
||||
)
|
||||
|
||||
expect(toolResult).toBeDefined()
|
||||
expect(toolResult.is_error).toBe(true)
|
||||
expect(toolResult.content).toContain("due to user rejecting a previous tool")
|
||||
expect(toolResult.output.value).toContain("[ERROR]")
|
||||
expect(toolResult.output.value).toContain("due to user rejecting a previous tool")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { serializeError } from "serialize-error"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import type { ImagePart, ToolResultPart } from "../task-persistence"
|
||||
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
|
||||
import { ConsecutiveMistakeError, TelemetryEventName } from "@roo-code/types"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
|
@ -118,10 +119,10 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
|
||||
if (toolCallId) {
|
||||
cline.pushToolResultToUserContent({
|
||||
type: "tool_result",
|
||||
tool_use_id: sanitizeToolUseId(toolCallId),
|
||||
content: errorMessage,
|
||||
is_error: true,
|
||||
type: "tool-result",
|
||||
toolCallId: sanitizeToolUseId(toolCallId),
|
||||
toolName: mcpBlock.name,
|
||||
output: { type: "text", value: `[ERROR] ${errorMessage}` },
|
||||
})
|
||||
}
|
||||
break
|
||||
|
|
@ -143,13 +144,13 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
}
|
||||
|
||||
let resultContent: string
|
||||
let imageBlocks: Anthropic.ImageBlockParam[] = []
|
||||
let imageBlocks: ImagePart[] = []
|
||||
|
||||
if (typeof content === "string") {
|
||||
resultContent = content || "(tool did not return anything)"
|
||||
} else {
|
||||
const textBlocks = content.filter((item) => item.type === "text")
|
||||
imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
|
||||
imageBlocks = content.filter((item) => item.type === "image") as ImagePart[]
|
||||
resultContent =
|
||||
textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
|
||||
"(tool did not return anything)"
|
||||
|
|
@ -169,9 +170,10 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
|
||||
if (toolCallId) {
|
||||
cline.pushToolResultToUserContent({
|
||||
type: "tool_result",
|
||||
tool_use_id: sanitizeToolUseId(toolCallId),
|
||||
content: resultContent,
|
||||
type: "tool-result",
|
||||
toolCallId: sanitizeToolUseId(toolCallId),
|
||||
toolName: mcpBlock.name,
|
||||
output: { type: "text", value: resultContent },
|
||||
})
|
||||
|
||||
if (imageBlocks.length > 0) {
|
||||
|
|
@ -399,10 +401,10 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
: `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`
|
||||
|
||||
cline.pushToolResultToUserContent({
|
||||
type: "tool_result",
|
||||
tool_use_id: sanitizeToolUseId(toolCallId),
|
||||
content: errorMessage,
|
||||
is_error: true,
|
||||
type: "tool-result",
|
||||
toolCallId: sanitizeToolUseId(toolCallId),
|
||||
toolName: block.name,
|
||||
output: { type: "text", value: `[ERROR] ${errorMessage}` },
|
||||
})
|
||||
|
||||
break
|
||||
|
|
@ -436,10 +438,10 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
// Push tool_result directly without setting didAlreadyUseTool so streaming can
|
||||
// continue gracefully.
|
||||
cline.pushToolResultToUserContent({
|
||||
type: "tool_result",
|
||||
tool_use_id: sanitizeToolUseId(toolCallId),
|
||||
content: formatResponse.toolError(errorMessage),
|
||||
is_error: true,
|
||||
type: "tool-result",
|
||||
toolCallId: sanitizeToolUseId(toolCallId),
|
||||
toolName: block.name,
|
||||
output: { type: "text", value: `[ERROR] ${formatResponse.toolError(errorMessage)}` },
|
||||
})
|
||||
|
||||
break
|
||||
|
|
@ -459,13 +461,13 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
}
|
||||
|
||||
let resultContent: string
|
||||
let imageBlocks: Anthropic.ImageBlockParam[] = []
|
||||
let imageBlocks: ImagePart[] = []
|
||||
|
||||
if (typeof content === "string") {
|
||||
resultContent = content || "(tool did not return anything)"
|
||||
} else {
|
||||
const textBlocks = content.filter((item) => item.type === "text")
|
||||
imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
|
||||
imageBlocks = content.filter((item) => item.type === "image") as ImagePart[]
|
||||
resultContent =
|
||||
textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
|
||||
"(tool did not return anything)"
|
||||
|
|
@ -482,9 +484,10 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
}
|
||||
|
||||
cline.pushToolResultToUserContent({
|
||||
type: "tool_result",
|
||||
tool_use_id: sanitizeToolUseId(toolCallId),
|
||||
content: resultContent,
|
||||
type: "tool-result",
|
||||
toolCallId: sanitizeToolUseId(toolCallId),
|
||||
toolName: block.name,
|
||||
output: { type: "text", value: resultContent },
|
||||
})
|
||||
|
||||
if (imageBlocks.length > 0) {
|
||||
|
|
@ -644,10 +647,13 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
const errorContent = formatResponse.toolError(error.message)
|
||||
// Push tool_result directly without setting didAlreadyUseTool
|
||||
cline.pushToolResultToUserContent({
|
||||
type: "tool_result",
|
||||
tool_use_id: sanitizeToolUseId(toolCallId),
|
||||
content: typeof errorContent === "string" ? errorContent : "(validation error)",
|
||||
is_error: true,
|
||||
type: "tool-result",
|
||||
toolCallId: sanitizeToolUseId(toolCallId),
|
||||
toolName: block.name,
|
||||
output: {
|
||||
type: "text",
|
||||
value: `[ERROR] ${typeof errorContent === "string" ? errorContent : "(validation error)"}`,
|
||||
},
|
||||
})
|
||||
|
||||
break
|
||||
|
|
@ -948,10 +954,10 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
// Push tool_result directly WITHOUT setting didAlreadyUseTool
|
||||
// This prevents the stream from being interrupted with "Response interrupted by tool use result"
|
||||
cline.pushToolResultToUserContent({
|
||||
type: "tool_result",
|
||||
tool_use_id: sanitizeToolUseId(toolCallId),
|
||||
content: formatResponse.toolError(errorMessage),
|
||||
is_error: true,
|
||||
type: "tool-result",
|
||||
toolCallId: sanitizeToolUseId(toolCallId),
|
||||
toolName: block.name,
|
||||
output: { type: "text", value: `[ERROR] ${formatResponse.toolError(errorMessage)}` },
|
||||
})
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
// npx vitest src/core/condense/__tests__/condense.spec.ts
|
||||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { BaseProvider } from "../../../api/providers/base-provider"
|
||||
import { ApiMessage } from "../../task-persistence/apiMessages"
|
||||
import {
|
||||
summarizeConversation,
|
||||
getMessagesSinceLastSummary,
|
||||
|
|
@ -41,7 +39,7 @@ class MockApiHandler extends BaseProvider {
|
|||
}
|
||||
}
|
||||
|
||||
override async countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
|
||||
override async countTokens(content: Array<any>): Promise<number> {
|
||||
// Simple token counting for testing
|
||||
let tokens = 0
|
||||
for (const block of content) {
|
||||
|
|
@ -65,7 +63,7 @@ describe("Condense", () => {
|
|||
|
||||
describe("extractCommandBlocks", () => {
|
||||
it("should extract command blocks from string content", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: 'Some text <command name="prr">/prr #123</command> more text',
|
||||
}
|
||||
|
|
@ -75,7 +73,7 @@ describe("Condense", () => {
|
|||
})
|
||||
|
||||
it("should extract multiple command blocks", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: '<command name="prr">/prr #123</command> text <command name="mode">/mode code</command>',
|
||||
}
|
||||
|
|
@ -85,7 +83,7 @@ describe("Condense", () => {
|
|||
})
|
||||
|
||||
it("should extract command blocks from array content", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Some user text" },
|
||||
|
|
@ -98,7 +96,7 @@ describe("Condense", () => {
|
|||
})
|
||||
|
||||
it("should return empty string when no command blocks found", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: "Just regular text without commands",
|
||||
}
|
||||
|
|
@ -108,7 +106,7 @@ describe("Condense", () => {
|
|||
})
|
||||
|
||||
it("should handle multiline command blocks", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: `<command name="prr">
|
||||
Line 1
|
||||
|
|
@ -124,7 +122,7 @@ Line 2
|
|||
|
||||
describe("summarizeConversation", () => {
|
||||
it("should create a summary message with role user (fresh start model)", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message with /prr command content" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -147,22 +145,22 @@ Line 2
|
|||
// Verify we have a summary message with role "user" (fresh start model)
|
||||
const summaryMessage = result.messages.find((msg) => msg.isSummary)
|
||||
expect(summaryMessage).toBeTruthy()
|
||||
expect(summaryMessage!.role).toBe("user")
|
||||
expect(Array.isArray(summaryMessage!.content)).toBe(true)
|
||||
const contentArray = summaryMessage!.content as any[]
|
||||
expect((summaryMessage as any).role).toBe("user")
|
||||
expect(Array.isArray((summaryMessage as any).content)).toBe(true)
|
||||
const contentArray = (summaryMessage as any).content as any[]
|
||||
expect(contentArray.some((b) => b.type === "text")).toBe(true)
|
||||
// Should NOT have reasoning blocks (no longer needed for user messages)
|
||||
expect(contentArray.some((b) => b.type === "reasoning")).toBe(false)
|
||||
|
||||
// Fresh start model: effective history should only contain the summary
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages)
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages as any)
|
||||
expect(effectiveHistory.length).toBe(1)
|
||||
expect(effectiveHistory[0].isSummary).toBe(true)
|
||||
expect(effectiveHistory[0].role).toBe("user")
|
||||
expect((effectiveHistory[0] as any).role).toBe("user")
|
||||
})
|
||||
|
||||
it("should tag ALL messages with condenseParent", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message with /prr command content" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -187,7 +185,7 @@ Line 2
|
|||
})
|
||||
|
||||
it("should preserve <command> blocks in the summary", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -216,7 +214,7 @@ Line 2
|
|||
const summaryMessage = result.messages.find((msg) => msg.isSummary)
|
||||
expect(summaryMessage).toBeTruthy()
|
||||
|
||||
const contentArray = summaryMessage!.content as any[]
|
||||
const contentArray = (summaryMessage as any).content as any[]
|
||||
// Summary content is split into separate text blocks:
|
||||
// - First block: "## Conversation Summary\n..."
|
||||
// - Second block: "<system-reminder>..." with command blocks
|
||||
|
|
@ -228,12 +226,12 @@ Line 2
|
|||
})
|
||||
|
||||
it("should handle complex first message content", async () => {
|
||||
const complexContent: Anthropic.Messages.ContentBlockParam[] = [
|
||||
const complexContent: any[] = [
|
||||
{ type: "text", text: "/mode code" },
|
||||
{ type: "text", text: "Additional context from the user" },
|
||||
]
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: complexContent },
|
||||
{ role: "assistant", content: "Switching to code mode" },
|
||||
{ role: "user", content: "Write a function" },
|
||||
|
|
@ -254,14 +252,14 @@ Line 2
|
|||
})
|
||||
|
||||
// Effective history should contain only the summary (fresh start)
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages)
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages as any)
|
||||
expect(effectiveHistory).toHaveLength(1)
|
||||
expect(effectiveHistory[0].isSummary).toBe(true)
|
||||
expect(effectiveHistory[0].role).toBe("user")
|
||||
expect((effectiveHistory[0] as any).role).toBe("user")
|
||||
})
|
||||
|
||||
it("should return error when not enough messages to summarize", async () => {
|
||||
const messages: ApiMessage[] = [{ role: "user", content: "Only one message" }]
|
||||
const messages: any[] = [{ role: "user", content: "Only one message" }]
|
||||
|
||||
const result = await summarizeConversation({
|
||||
messages,
|
||||
|
|
@ -278,7 +276,7 @@ Line 2
|
|||
})
|
||||
|
||||
it("should not summarize messages that already contain a recent summary with no new messages", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message with /command" },
|
||||
{ role: "user", content: "Previous summary", isSummary: true },
|
||||
]
|
||||
|
|
@ -312,7 +310,7 @@ Line 2
|
|||
}
|
||||
|
||||
const emptyHandler = new EmptyMockApiHandler()
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second" },
|
||||
{ role: "user", content: "Third" },
|
||||
|
|
@ -339,7 +337,7 @@ Line 2
|
|||
describe("getEffectiveApiHistory", () => {
|
||||
it("should return only summary when summary exists (fresh start)", () => {
|
||||
const condenseId = "test-condense-id"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: condenseId },
|
||||
{ role: "user", content: "Third", condenseParent: condenseId },
|
||||
|
|
@ -359,7 +357,7 @@ Line 2
|
|||
|
||||
it("should include messages after summary in fresh start model", () => {
|
||||
const condenseId = "test-condense-id"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: condenseId },
|
||||
{
|
||||
|
|
@ -376,12 +374,12 @@ Line 2
|
|||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
expect(result[1].content).toBe("New response after summary")
|
||||
expect(result[2].content).toBe("New user message")
|
||||
expect((result[1] as any).content).toBe("New response after summary")
|
||||
expect((result[2] as any).content).toBe("New user message")
|
||||
})
|
||||
|
||||
it("should return all messages when no summary exists", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First" },
|
||||
{ role: "assistant", content: "Second" },
|
||||
{ role: "user", content: "Third" },
|
||||
|
|
@ -397,7 +395,7 @@ Line 2
|
|||
// The cleanupAfterTruncation function would normally clear these,
|
||||
// but even without cleanup, getEffectiveApiHistory should handle orphaned tags
|
||||
const orphanedCondenseId = "deleted-summary-id"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: orphanedCondenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: orphanedCondenseId },
|
||||
{ role: "user", content: "Third", condenseParent: orphanedCondenseId },
|
||||
|
|
@ -413,7 +411,7 @@ Line 2
|
|||
|
||||
describe("getMessagesSinceLastSummary", () => {
|
||||
it("should return all messages when no summary exists", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -424,7 +422,7 @@ Line 2
|
|||
})
|
||||
|
||||
it("should return messages since last summary including the summary", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Summary content", isSummary: true },
|
||||
|
|
@ -440,7 +438,7 @@ Line 2
|
|||
})
|
||||
|
||||
it("should handle multiple summaries and return from the last one", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "user", content: "First summary", isSummary: true },
|
||||
{ role: "assistant", content: "Middle message" },
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ describe("foldedFileContext", () => {
|
|||
expect(summaryMessage).toBeDefined()
|
||||
|
||||
// Each file should have its own content block
|
||||
const contentArray = summaryMessage!.content as any[]
|
||||
const contentArray = (summaryMessage as any).content as any[]
|
||||
|
||||
// Find the content blocks containing file contexts
|
||||
const userFileBlock = contentArray.find(
|
||||
|
|
@ -381,7 +381,7 @@ describe("foldedFileContext", () => {
|
|||
expect(summaryMessage).toBeDefined()
|
||||
|
||||
// The summary content should NOT contain any file context blocks
|
||||
const contentArray = summaryMessage!.content as any[]
|
||||
const contentArray = (summaryMessage as any).content as any[]
|
||||
const fileContextBlock = contentArray.find(
|
||||
(block: any) => block.type === "text" && block.text?.includes("## File Context"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
|
||||
import type { Mock } from "vitest"
|
||||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { ApiHandler } from "../../../api"
|
||||
import { ApiMessage } from "../../task-persistence/apiMessages"
|
||||
import { RooMessage } from "../../task-persistence/rooMessage"
|
||||
import { maybeRemoveImageBlocks } from "../../../api/transform/image-cleaning"
|
||||
import {
|
||||
summarizeConversation,
|
||||
|
|
@ -22,7 +21,7 @@ import {
|
|||
} from "../index"
|
||||
|
||||
vi.mock("../../../api/transform/image-cleaning", () => ({
|
||||
maybeRemoveImageBlocks: vi.fn((messages: ApiMessage[], _apiHandler: ApiHandler) => [...messages]),
|
||||
maybeRemoveImageBlocks: vi.fn((messages: RooMessage[], _apiHandler: ApiHandler) => [...messages]),
|
||||
}))
|
||||
|
||||
vi.mock("@roo-code/telemetry", () => ({
|
||||
|
|
@ -37,7 +36,7 @@ const taskId = "test-task-id"
|
|||
|
||||
describe("extractCommandBlocks", () => {
|
||||
it("should extract command blocks from string content", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: 'Some text <command name="prr">/prr #123</command> more text',
|
||||
}
|
||||
|
|
@ -47,7 +46,7 @@ describe("extractCommandBlocks", () => {
|
|||
})
|
||||
|
||||
it("should extract multiple command blocks", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: '<command name="prr">/prr #123</command> text <command name="mode">/mode code</command>',
|
||||
}
|
||||
|
|
@ -57,7 +56,7 @@ describe("extractCommandBlocks", () => {
|
|||
})
|
||||
|
||||
it("should extract command blocks from array content", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Some user text" },
|
||||
|
|
@ -70,7 +69,7 @@ describe("extractCommandBlocks", () => {
|
|||
})
|
||||
|
||||
it("should return empty string when no command blocks found", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: "Just regular text without commands",
|
||||
}
|
||||
|
|
@ -80,7 +79,7 @@ describe("extractCommandBlocks", () => {
|
|||
})
|
||||
|
||||
it("should handle multiline command blocks", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: `<command name="prr">
|
||||
Line 1
|
||||
|
|
@ -94,7 +93,7 @@ Line 2
|
|||
})
|
||||
|
||||
it("should handle command blocks with attributes", () => {
|
||||
const message: ApiMessage = {
|
||||
const message: any = {
|
||||
role: "user",
|
||||
content: '<command name="test" attr1="value1" attr2="value2">content</command>',
|
||||
}
|
||||
|
|
@ -107,7 +106,7 @@ Line 2
|
|||
|
||||
describe("injectSyntheticToolResults", () => {
|
||||
it("should return messages unchanged when no orphan tool_calls exist", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
|
|
@ -126,7 +125,7 @@ describe("injectSyntheticToolResults", () => {
|
|||
})
|
||||
|
||||
it("should inject synthetic tool_result for orphan tool_call", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
|
|
@ -141,17 +140,17 @@ describe("injectSyntheticToolResults", () => {
|
|||
const result = injectSyntheticToolResults(messages)
|
||||
|
||||
expect(result.length).toBe(3)
|
||||
expect(result[2].role).toBe("user")
|
||||
expect((result[2] as any).role).toBe("tool")
|
||||
|
||||
const content = result[2].content as any[]
|
||||
const content = (result[2] as any).content as any[]
|
||||
expect(content.length).toBe(1)
|
||||
expect(content[0].type).toBe("tool_result")
|
||||
expect(content[0].tool_use_id).toBe("tool-orphan")
|
||||
expect(content[0].content).toBe("Context condensation triggered. Tool execution deferred.")
|
||||
expect(content[0].type).toBe("tool-result")
|
||||
expect(content[0].toolCallId).toBe("tool-orphan")
|
||||
expect(content[0].output.value).toBe("Context condensation triggered. Tool execution deferred.")
|
||||
})
|
||||
|
||||
it("should inject synthetic tool_results for multiple orphan tool_calls", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
|
|
@ -167,14 +166,14 @@ describe("injectSyntheticToolResults", () => {
|
|||
const result = injectSyntheticToolResults(messages)
|
||||
|
||||
expect(result.length).toBe(3)
|
||||
const content = result[2].content as any[]
|
||||
const content = (result[2] as any).content as any[]
|
||||
expect(content.length).toBe(2)
|
||||
expect(content[0].tool_use_id).toBe("tool-1")
|
||||
expect(content[1].tool_use_id).toBe("tool-2")
|
||||
expect(content[0].toolCallId).toBe("tool-1")
|
||||
expect(content[1].toolCallId).toBe("tool-2")
|
||||
})
|
||||
|
||||
it("should only inject for orphan tool_calls, not matched ones", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
|
|
@ -195,13 +194,13 @@ describe("injectSyntheticToolResults", () => {
|
|||
const result = injectSyntheticToolResults(messages)
|
||||
|
||||
expect(result.length).toBe(4)
|
||||
const syntheticContent = result[3].content as any[]
|
||||
const syntheticContent = (result[3] as any).content as any[]
|
||||
expect(syntheticContent.length).toBe(1)
|
||||
expect(syntheticContent[0].tool_use_id).toBe("orphan-tool")
|
||||
expect(syntheticContent[0].toolCallId).toBe("orphan-tool")
|
||||
})
|
||||
|
||||
it("should handle messages with string content (no tool_use/tool_result)", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there!", ts: 2 },
|
||||
]
|
||||
|
|
@ -216,7 +215,7 @@ describe("injectSyntheticToolResults", () => {
|
|||
})
|
||||
|
||||
it("should handle tool_results spread across multiple user messages", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
|
|
@ -242,58 +241,11 @@ describe("injectSyntheticToolResults", () => {
|
|||
// Both tool_uses have matching tool_results, no injection needed
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it("should support AI SDK tool-call/tool-result blocks", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "tool-1",
|
||||
toolName: "read_file",
|
||||
input: { path: "test.ts" },
|
||||
},
|
||||
] as any,
|
||||
ts: 2,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool-result", toolCallId: "tool-1", output: "file contents" }] as any,
|
||||
ts: 3,
|
||||
},
|
||||
]
|
||||
|
||||
const result = injectSyntheticToolResults(messages)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it("should inject synthetic tool_result for orphan AI SDK tool-call", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool-call", toolCallId: "tool-orphan", toolName: "attempt_completion", input: {} },
|
||||
] as any,
|
||||
ts: 2,
|
||||
},
|
||||
]
|
||||
|
||||
const result = injectSyntheticToolResults(messages)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
const content = result[2].content as any[]
|
||||
expect(content).toHaveLength(1)
|
||||
expect(content[0].type).toBe("tool_result")
|
||||
expect(content[0].tool_use_id).toBe("tool-orphan")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMessagesSinceLastSummary", () => {
|
||||
it("should return all messages when there is no summary", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -304,7 +256,7 @@ describe("getMessagesSinceLastSummary", () => {
|
|||
})
|
||||
|
||||
it("should return messages since the last summary", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "Summary of conversation", ts: 3, isSummary: true },
|
||||
|
|
@ -321,7 +273,7 @@ describe("getMessagesSinceLastSummary", () => {
|
|||
})
|
||||
|
||||
it("should handle multiple summary messages and return since the last one", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "user", content: "First summary", ts: 2, isSummary: true },
|
||||
{ role: "assistant", content: "How are you?", ts: 3 },
|
||||
|
|
@ -342,7 +294,7 @@ describe("getMessagesSinceLastSummary", () => {
|
|||
})
|
||||
|
||||
it("should return messages from user summary (fresh start model)", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1, condenseParent: "cond-1" },
|
||||
{ role: "assistant", content: "Hi there", ts: 2, condenseParent: "cond-1" },
|
||||
{ role: "user", content: "Summary content", ts: 3, isSummary: true, condenseId: "cond-1" },
|
||||
|
|
@ -351,14 +303,14 @@ describe("getMessagesSinceLastSummary", () => {
|
|||
|
||||
const result = getMessagesSinceLastSummary(messages)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
expect(result[0].role).toBe("user")
|
||||
expect((result[0] as any).role).toBe("user")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getEffectiveApiHistory", () => {
|
||||
it("should return only summary when summary exists (fresh start model)", () => {
|
||||
const condenseId = "test-condense-id"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: condenseId },
|
||||
{ role: "user", content: "Third", condenseParent: condenseId },
|
||||
|
|
@ -378,7 +330,7 @@ describe("getEffectiveApiHistory", () => {
|
|||
|
||||
it("should include messages after summary in fresh start model", () => {
|
||||
const condenseId = "test-condense-id"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: condenseId },
|
||||
{
|
||||
|
|
@ -395,12 +347,12 @@ describe("getEffectiveApiHistory", () => {
|
|||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
expect(result[1].content).toBe("New response after summary")
|
||||
expect(result[2].content).toBe("New user message")
|
||||
expect((result[1] as any).content).toBe("New response after summary")
|
||||
expect((result[2] as any).content).toBe("New user message")
|
||||
})
|
||||
|
||||
it("should return all messages when no summary exists", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First" },
|
||||
{ role: "assistant", content: "Second" },
|
||||
{ role: "user", content: "Third" },
|
||||
|
|
@ -413,7 +365,7 @@ describe("getEffectiveApiHistory", () => {
|
|||
|
||||
it("should restore messages when summary is deleted (rewind - orphaned condenseParent)", () => {
|
||||
const orphanedCondenseId = "deleted-summary-id"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: orphanedCondenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: orphanedCondenseId },
|
||||
{ role: "user", content: "Third", condenseParent: orphanedCondenseId },
|
||||
|
|
@ -429,7 +381,7 @@ describe("getEffectiveApiHistory", () => {
|
|||
it("should filter out truncated messages within summary range", () => {
|
||||
const condenseId = "cond-1"
|
||||
const truncationId = "trunc-1"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: condenseId },
|
||||
{
|
||||
role: "user",
|
||||
|
|
@ -453,12 +405,12 @@ describe("getEffectiveApiHistory", () => {
|
|||
expect(result).toHaveLength(3)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
expect(result[1].isTruncationMarker).toBe(true)
|
||||
expect(result[2].content).toBe("After truncation")
|
||||
expect((result[2] as any).content).toBe("After truncation")
|
||||
})
|
||||
|
||||
it("should filter out orphan tool_result blocks after fresh start condensation", () => {
|
||||
const condenseId = "cond-1"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", condenseParent: condenseId },
|
||||
{
|
||||
role: "assistant",
|
||||
|
|
@ -490,7 +442,7 @@ describe("getEffectiveApiHistory", () => {
|
|||
|
||||
it("should keep tool_result blocks that have matching tool_use in fresh start", () => {
|
||||
const condenseId = "cond-1"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", condenseParent: condenseId },
|
||||
{
|
||||
role: "user",
|
||||
|
|
@ -498,12 +450,14 @@ describe("getEffectiveApiHistory", () => {
|
|||
isSummary: true,
|
||||
condenseId,
|
||||
},
|
||||
// This tool_use is AFTER the summary, so it's not condensed away
|
||||
// This tool-call is AFTER the summary, so it's not condensed away
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tool-valid", name: "read_file", input: { path: "test.ts" } }],
|
||||
content: [
|
||||
{ type: "tool-call", toolCallId: "tool-valid", toolName: "read_file", input: { path: "test.ts" } },
|
||||
],
|
||||
},
|
||||
// This tool_result has a matching tool_use, so it should be kept
|
||||
// This tool_result has a matching tool-call, so it should be kept (legacy user message format)
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "tool-valid", content: "file contents" }],
|
||||
|
|
@ -515,18 +469,23 @@ describe("getEffectiveApiHistory", () => {
|
|||
// All messages after summary should be included
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
expect((result[1].content as any[])[0].id).toBe("tool-valid")
|
||||
expect((result[2].content as any[])[0].tool_use_id).toBe("tool-valid")
|
||||
expect(((result[1] as any).content as any[])[0].toolCallId).toBe("tool-valid")
|
||||
expect(((result[2] as any).content as any[])[0].tool_use_id).toBe("tool-valid")
|
||||
})
|
||||
|
||||
it("should filter orphan tool_results but keep other content in mixed user message", () => {
|
||||
const condenseId = "cond-1"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", condenseParent: condenseId },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool-orphan", name: "attempt_completion", input: { result: "Done" } },
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "tool-orphan",
|
||||
toolName: "attempt_completion",
|
||||
input: { result: "Done" },
|
||||
},
|
||||
],
|
||||
condenseParent: condenseId,
|
||||
},
|
||||
|
|
@ -536,12 +495,14 @@ describe("getEffectiveApiHistory", () => {
|
|||
isSummary: true,
|
||||
condenseId,
|
||||
},
|
||||
// This tool_use is AFTER the summary
|
||||
// This tool-call is AFTER the summary
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tool-valid", name: "read_file", input: { path: "test.ts" } }],
|
||||
content: [
|
||||
{ type: "tool-call", toolCallId: "tool-valid", toolName: "read_file", input: { path: "test.ts" } },
|
||||
],
|
||||
},
|
||||
// Mixed content: one orphan tool_result and one valid tool_result
|
||||
// Mixed content: one orphan tool_result and one valid tool_result (legacy user message format)
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
|
|
@ -553,18 +514,18 @@ describe("getEffectiveApiHistory", () => {
|
|||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
// Summary + assistant with tool_use + filtered user message
|
||||
// Summary + assistant with tool-call + filtered user message
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
// The user message should only contain the valid tool_result
|
||||
const userContent = result[2].content as any[]
|
||||
const userContent = (result[2] as any).content as any[]
|
||||
expect(userContent).toHaveLength(1)
|
||||
expect(userContent[0].tool_use_id).toBe("tool-valid")
|
||||
})
|
||||
|
||||
it("should handle multiple orphan tool_results in a single message", () => {
|
||||
const condenseId = "cond-1"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -598,7 +559,7 @@ describe("getEffectiveApiHistory", () => {
|
|||
|
||||
it("should preserve non-tool_result content in user messages", () => {
|
||||
const condenseId = "cond-1"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
|
@ -627,69 +588,17 @@ describe("getEffectiveApiHistory", () => {
|
|||
// Summary + user message with only text (orphan tool_result filtered)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
const userContent = result[1].content as any[]
|
||||
const userContent = (result[1] as any).content as any[]
|
||||
expect(userContent).toHaveLength(1)
|
||||
expect(userContent[0].type).toBe("text")
|
||||
expect(userContent[0].text).toBe("User added some text")
|
||||
})
|
||||
|
||||
it("should keep AI SDK tool-result blocks that have matching tool-call after summary", () => {
|
||||
const condenseId = "cond-1"
|
||||
const messages: ApiMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary content" }],
|
||||
isSummary: true,
|
||||
condenseId,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool-call", toolCallId: "tool-valid", toolName: "read_file", input: {} }] as any,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool-result", toolCallId: "tool-valid", output: "ok" }] as any,
|
||||
},
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect((result[2].content as any[])[0].toolCallId).toBe("tool-valid")
|
||||
})
|
||||
|
||||
it("should filter orphan AI SDK tool-result blocks after fresh start condensation", () => {
|
||||
const condenseId = "cond-1"
|
||||
const messages: ApiMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool-call", toolCallId: "tool-orphan", toolName: "attempt_completion", input: {} },
|
||||
] as any,
|
||||
condenseParent: condenseId,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary content" }],
|
||||
isSummary: true,
|
||||
condenseId,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool-result", toolCallId: "tool-orphan", output: "Rejected by user" }] as any,
|
||||
},
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cleanupAfterTruncation", () => {
|
||||
it("should clear orphaned condenseParent references", () => {
|
||||
const orphanedCondenseId = "deleted-summary"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: orphanedCondenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: orphanedCondenseId },
|
||||
{ role: "user", content: "Third" },
|
||||
|
|
@ -704,7 +613,7 @@ describe("cleanupAfterTruncation", () => {
|
|||
|
||||
it("should keep condenseParent when summary still exists", () => {
|
||||
const condenseId = "existing-summary"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: condenseId },
|
||||
{
|
||||
|
|
@ -723,7 +632,7 @@ describe("cleanupAfterTruncation", () => {
|
|||
|
||||
it("should clear orphaned truncationParent references", () => {
|
||||
const orphanedTruncationId = "deleted-truncation"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", truncationParent: orphanedTruncationId },
|
||||
{ role: "assistant", content: "Second" },
|
||||
]
|
||||
|
|
@ -735,7 +644,7 @@ describe("cleanupAfterTruncation", () => {
|
|||
|
||||
it("should keep truncationParent when marker still exists", () => {
|
||||
const truncationId = "existing-truncation"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", truncationParent: truncationId },
|
||||
{
|
||||
role: "assistant",
|
||||
|
|
@ -753,7 +662,7 @@ describe("cleanupAfterTruncation", () => {
|
|||
it("should handle mixed orphaned and valid references", () => {
|
||||
const validCondenseId = "valid-cond"
|
||||
const orphanedCondenseId = "orphaned-cond"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First", condenseParent: orphanedCondenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: validCondenseId },
|
||||
{
|
||||
|
|
@ -811,7 +720,7 @@ describe("summarizeConversation", () => {
|
|||
const defaultSystemPrompt = "You are a helpful assistant."
|
||||
|
||||
it("should not summarize when there are not enough messages", async () => {
|
||||
const messages: ApiMessage[] = [{ role: "user", content: "Hello", ts: 1 }]
|
||||
const messages: any[] = [{ role: "user", content: "Hello", ts: 1 }]
|
||||
|
||||
const result = await summarizeConversation({
|
||||
messages,
|
||||
|
|
@ -828,7 +737,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should create summary with user role (fresh start model)", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -862,19 +771,19 @@ describe("summarizeConversation", () => {
|
|||
}
|
||||
|
||||
// Summary message is a user message with just text (fresh start model)
|
||||
expect(summaryMessage!.role).toBe("user")
|
||||
expect(Array.isArray(summaryMessage!.content)).toBe(true)
|
||||
const content = summaryMessage!.content as any[]
|
||||
expect((summaryMessage! as any).role).toBe("user")
|
||||
expect(Array.isArray((summaryMessage as any).content)).toBe(true)
|
||||
const content = (summaryMessage as any).content as any[]
|
||||
expect(content).toHaveLength(1)
|
||||
expect(content[0].type).toBe("text")
|
||||
expect(content[0].text).toContain("## Conversation Summary")
|
||||
expect(content[0].text).toContain("This is a summary")
|
||||
|
||||
// Fresh start: effective API history should contain only the summary
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages)
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages as any)
|
||||
expect(effectiveHistory).toHaveLength(1)
|
||||
expect(effectiveHistory[0].isSummary).toBe(true)
|
||||
expect(effectiveHistory[0].role).toBe("user")
|
||||
expect((effectiveHistory[0] as any).role).toBe("user")
|
||||
|
||||
// Check the cost and token counts
|
||||
expect(result.cost).toBe(0.05)
|
||||
|
|
@ -885,7 +794,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should preserve command blocks from first message in summary", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: 'Hello <command name="prr">/prr #123</command>',
|
||||
|
|
@ -907,7 +816,7 @@ describe("summarizeConversation", () => {
|
|||
const summaryMessage = result.messages.find((m) => m.isSummary)
|
||||
expect(summaryMessage).toBeDefined()
|
||||
|
||||
const content = summaryMessage!.content as any[]
|
||||
const content = (summaryMessage as any).content as any[]
|
||||
// Summary content is now split into separate text blocks
|
||||
expect(content).toHaveLength(2)
|
||||
expect(content[0].text).toContain("## Conversation Summary")
|
||||
|
|
@ -917,7 +826,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should not include command blocks wrapper when no commands in first message", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -935,14 +844,14 @@ describe("summarizeConversation", () => {
|
|||
const summaryMessage = result.messages.find((m) => m.isSummary)
|
||||
expect(summaryMessage).toBeDefined()
|
||||
|
||||
const content = summaryMessage!.content as any[]
|
||||
const content = (summaryMessage as any).content as any[]
|
||||
expect(content[0].text).not.toContain("<system-reminder>")
|
||||
expect(content[0].text).not.toContain("Active Workflows")
|
||||
})
|
||||
|
||||
it("should handle empty summary response and return error", async () => {
|
||||
// We need enough messages to trigger summarization
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -983,7 +892,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should correctly format the request to the API", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -1020,7 +929,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should include the original first user message in summarization input", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Initial ask", ts: 1 },
|
||||
{ role: "assistant", content: "Ack", ts: 2 },
|
||||
{ role: "user", content: "Follow-up", ts: 3 },
|
||||
|
|
@ -1052,7 +961,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should calculate newContextTokens correctly with systemPrompt", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -1091,7 +1000,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should successfully summarize conversation", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -1124,7 +1033,7 @@ describe("summarizeConversation", () => {
|
|||
expect(result.messages.length).toBe(messages.length + 1)
|
||||
|
||||
// Fresh start: effective history should contain only the summary
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages)
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages as any)
|
||||
expect(effectiveHistory.length).toBe(1)
|
||||
expect(effectiveHistory[0].isSummary).toBe(true)
|
||||
|
||||
|
|
@ -1136,7 +1045,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should return error when API handler is invalid", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -1180,7 +1089,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should tag all messages with condenseParent (fresh start model)", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -1206,7 +1115,7 @@ describe("summarizeConversation", () => {
|
|||
})
|
||||
|
||||
it("should place summary message at end of messages array", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -1224,7 +1133,7 @@ describe("summarizeConversation", () => {
|
|||
// Summary should be the last message
|
||||
const lastMessage = result.messages[result.messages.length - 1]
|
||||
expect(lastMessage.isSummary).toBe(true)
|
||||
expect(lastMessage.role).toBe("user")
|
||||
expect((lastMessage as any).role).toBe("user")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1235,7 +1144,7 @@ describe("summarizeConversation with custom settings", () => {
|
|||
const localTaskId = "test-task"
|
||||
|
||||
// Sample messages for testing
|
||||
const sampleMessages: ApiMessage[] = [
|
||||
const sampleMessages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi there", ts: 2 },
|
||||
{ role: "user", content: "How are you?", ts: 3 },
|
||||
|
|
@ -1388,7 +1297,7 @@ describe("summarizeConversation with custom settings", () => {
|
|||
|
||||
describe("toolUseToText", () => {
|
||||
it("should convert tool_use block with object input to text", () => {
|
||||
const block: Anthropic.Messages.ToolUseBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_use",
|
||||
id: "tool-123",
|
||||
name: "read_file",
|
||||
|
|
@ -1401,7 +1310,7 @@ describe("toolUseToText", () => {
|
|||
})
|
||||
|
||||
it("should convert tool_use block with nested object input to text", () => {
|
||||
const block: Anthropic.Messages.ToolUseBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_use",
|
||||
id: "tool-456",
|
||||
name: "write_file",
|
||||
|
|
@ -1421,7 +1330,7 @@ describe("toolUseToText", () => {
|
|||
})
|
||||
|
||||
it("should convert tool_use block with string input to text", () => {
|
||||
const block: Anthropic.Messages.ToolUseBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_use",
|
||||
id: "tool-789",
|
||||
name: "execute_command",
|
||||
|
|
@ -1434,7 +1343,7 @@ describe("toolUseToText", () => {
|
|||
})
|
||||
|
||||
it("should handle empty object input", () => {
|
||||
const block: Anthropic.Messages.ToolUseBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_use",
|
||||
id: "tool-empty",
|
||||
name: "some_tool",
|
||||
|
|
@ -1449,7 +1358,7 @@ describe("toolUseToText", () => {
|
|||
|
||||
describe("toolResultToText", () => {
|
||||
it("should convert tool_result with string content to text", () => {
|
||||
const block: Anthropic.Messages.ToolResultBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-123",
|
||||
content: "File contents here",
|
||||
|
|
@ -1461,7 +1370,7 @@ describe("toolResultToText", () => {
|
|||
})
|
||||
|
||||
it("should convert tool_result with error flag to text", () => {
|
||||
const block: Anthropic.Messages.ToolResultBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-456",
|
||||
content: "File not found",
|
||||
|
|
@ -1474,7 +1383,7 @@ describe("toolResultToText", () => {
|
|||
})
|
||||
|
||||
it("should convert tool_result with array content to text", () => {
|
||||
const block: Anthropic.Messages.ToolResultBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-789",
|
||||
content: [
|
||||
|
|
@ -1489,7 +1398,7 @@ describe("toolResultToText", () => {
|
|||
})
|
||||
|
||||
it("should handle tool_result with image in array content", () => {
|
||||
const block: Anthropic.Messages.ToolResultBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-img",
|
||||
content: [
|
||||
|
|
@ -1504,7 +1413,7 @@ describe("toolResultToText", () => {
|
|||
})
|
||||
|
||||
it("should handle tool_result with no content", () => {
|
||||
const block: Anthropic.Messages.ToolResultBlockParam = {
|
||||
const block: any = {
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-empty",
|
||||
}
|
||||
|
|
@ -1525,7 +1434,7 @@ describe("convertToolBlocksToText", () => {
|
|||
})
|
||||
|
||||
it("should convert tool_use blocks to text blocks", () => {
|
||||
const content: Anthropic.Messages.ContentBlockParam[] = [
|
||||
const content: any[] = [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-123",
|
||||
|
|
@ -1537,12 +1446,12 @@ describe("convertToolBlocksToText", () => {
|
|||
const result = convertToolBlocksToText(content)
|
||||
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect((result as Anthropic.Messages.ContentBlockParam[])[0].type).toBe("text")
|
||||
expect((result as Anthropic.Messages.TextBlockParam[])[0].text).toContain("[Tool Use: read_file]")
|
||||
expect((result as any[])[0].type).toBe("text")
|
||||
expect((result as any[])[0].text).toContain("[Tool Use: read_file]")
|
||||
})
|
||||
|
||||
it("should convert tool_result blocks to text blocks", () => {
|
||||
const content: Anthropic.Messages.ContentBlockParam[] = [
|
||||
const content: any[] = [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-123",
|
||||
|
|
@ -1553,12 +1462,12 @@ describe("convertToolBlocksToText", () => {
|
|||
const result = convertToolBlocksToText(content)
|
||||
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect((result as Anthropic.Messages.ContentBlockParam[])[0].type).toBe("text")
|
||||
expect((result as Anthropic.Messages.TextBlockParam[])[0].text).toContain("[Tool Result]")
|
||||
expect((result as any[])[0].type).toBe("text")
|
||||
expect((result as any[])[0].text).toContain("[Tool Result]")
|
||||
})
|
||||
|
||||
it("should preserve non-tool blocks unchanged", () => {
|
||||
const content: Anthropic.Messages.ContentBlockParam[] = [
|
||||
const content: any[] = [
|
||||
{ type: "text", text: "Hello" },
|
||||
{
|
||||
type: "tool_use",
|
||||
|
|
@ -1572,16 +1481,16 @@ describe("convertToolBlocksToText", () => {
|
|||
const result = convertToolBlocksToText(content)
|
||||
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
const resultArray = result as Anthropic.Messages.ContentBlockParam[]
|
||||
const resultArray = result as any[]
|
||||
expect(resultArray).toHaveLength(3)
|
||||
expect(resultArray[0]).toEqual({ type: "text", text: "Hello" })
|
||||
expect(resultArray[1].type).toBe("text")
|
||||
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Use: read_file]")
|
||||
expect((resultArray[1] as any).text).toContain("[Tool Use: read_file]")
|
||||
expect(resultArray[2]).toEqual({ type: "text", text: "World" })
|
||||
})
|
||||
|
||||
it("should handle mixed content with multiple tool blocks", () => {
|
||||
const content: Anthropic.Messages.ContentBlockParam[] = [
|
||||
const content: any[] = [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-1",
|
||||
|
|
@ -1598,36 +1507,11 @@ describe("convertToolBlocksToText", () => {
|
|||
const result = convertToolBlocksToText(content)
|
||||
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
const resultArray = result as Anthropic.Messages.ContentBlockParam[]
|
||||
const resultArray = result as any[]
|
||||
expect(resultArray).toHaveLength(2)
|
||||
expect((resultArray[0] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Use: read_file]")
|
||||
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Result]")
|
||||
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("contents of a.ts")
|
||||
})
|
||||
|
||||
it("should convert AI SDK tool-call and tool-result blocks to text blocks", () => {
|
||||
const content = [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "tool-1",
|
||||
toolName: "read_file",
|
||||
input: { path: "a.ts" },
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "tool-1",
|
||||
output: [{ type: "text", text: "contents of a.ts" }],
|
||||
},
|
||||
] as any
|
||||
|
||||
const result = convertToolBlocksToText(content)
|
||||
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
const resultArray = result as Anthropic.Messages.ContentBlockParam[]
|
||||
expect(resultArray).toHaveLength(2)
|
||||
expect((resultArray[0] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Use: read_file]")
|
||||
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Result]")
|
||||
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("contents of a.ts")
|
||||
expect((resultArray[0] as any).text).toContain("[Tool Use: read_file]")
|
||||
expect((resultArray[1] as any).text).toContain("[Tool Result]")
|
||||
expect((resultArray[1] as any).text).toContain("contents of a.ts")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ describe("nested condensing scenarios", () => {
|
|||
const condenseId2 = "condense-2"
|
||||
|
||||
// Simulate history after two nested condenses with user-role summaries
|
||||
const history: ApiMessage[] = [
|
||||
const history: any[] = [
|
||||
// Original task - condensed in first condense
|
||||
{ role: "user", content: "Build an app", ts: 100, condenseParent: condenseId1 },
|
||||
// Messages from first condense
|
||||
|
|
@ -47,8 +47,8 @@ describe("nested condensing scenarios", () => {
|
|||
expect(effectiveHistory.length).toBe(3)
|
||||
expect(effectiveHistory[0].isSummary).toBe(true)
|
||||
expect(effectiveHistory[0].condenseId).toBe(condenseId2) // Latest summary
|
||||
expect(effectiveHistory[1].content).toBe("Database added")
|
||||
expect(effectiveHistory[2].content).toBe("Now test it")
|
||||
expect((effectiveHistory[1] as any).content).toBe("Database added")
|
||||
expect((effectiveHistory[2] as any).content).toBe("Now test it")
|
||||
|
||||
// Verify NO condensed messages are included
|
||||
const hasCondensedMessages = effectiveHistory.some(
|
||||
|
|
@ -68,7 +68,7 @@ describe("nested condensing scenarios", () => {
|
|||
const hasSummary1 = messagesSinceLastSummary.some((m) => m.condenseId === condenseId1)
|
||||
expect(hasSummary1).toBe(false)
|
||||
|
||||
const hasOriginalTask = messagesSinceLastSummary.some((m) => m.content === "Build an app")
|
||||
const hasOriginalTask = messagesSinceLastSummary.some((m) => (m as any).content === "Build an app")
|
||||
expect(hasOriginalTask).toBe(false)
|
||||
})
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ describe("nested condensing scenarios", () => {
|
|||
const condenseId2 = "condense-2"
|
||||
const condenseId3 = "condense-3"
|
||||
|
||||
const history: ApiMessage[] = [
|
||||
const history: any[] = [
|
||||
// First condense content
|
||||
{ role: "user", content: "Task", ts: 100, condenseParent: condenseId1 },
|
||||
{
|
||||
|
|
@ -116,7 +116,7 @@ describe("nested condensing scenarios", () => {
|
|||
// Should only contain Summary3 and current work
|
||||
expect(effectiveHistory.length).toBe(2)
|
||||
expect(effectiveHistory[0].condenseId).toBe(condenseId3)
|
||||
expect(effectiveHistory[1].content).toBe("Current work")
|
||||
expect((effectiveHistory[1] as any).content).toBe("Current work")
|
||||
|
||||
const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory)
|
||||
expect(messagesSinceLastSummary.length).toBe(2)
|
||||
|
|
@ -133,7 +133,7 @@ describe("nested condensing scenarios", () => {
|
|||
it("should return consistent results when called with full history vs effective history", () => {
|
||||
const condenseId = "condense-1"
|
||||
|
||||
const fullHistory: ApiMessage[] = [
|
||||
const fullHistory: any[] = [
|
||||
{ role: "user", content: "Original task", ts: 100, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Response", ts: 200, condenseParent: condenseId },
|
||||
{
|
||||
|
|
@ -166,7 +166,7 @@ describe("nested condensing scenarios", () => {
|
|||
const condenseId2 = "condense-2"
|
||||
|
||||
// Scenario: Two nested condenses with user-role summaries
|
||||
const fullHistory: ApiMessage[] = [
|
||||
const fullHistory: any[] = [
|
||||
{ role: "user", content: "Original task - should NOT appear", ts: 100, condenseParent: condenseId1 },
|
||||
{ role: "assistant", content: "Old response", ts: 200, condenseParent: condenseId1 },
|
||||
// First summary (user role, fresh-start model), then condensed again
|
||||
|
|
@ -197,9 +197,9 @@ describe("nested condensing scenarios", () => {
|
|||
|
||||
// The original task should NOT be included
|
||||
const hasOriginalTask = messagesSinceLastSummary.some((m) =>
|
||||
typeof m.content === "string"
|
||||
? m.content.includes("Original task")
|
||||
: JSON.stringify(m.content).includes("Original task"),
|
||||
typeof (m as any).content === "string"
|
||||
? (m as any).content.includes("Original task")
|
||||
: JSON.stringify((m as any).content).includes("Original task"),
|
||||
)
|
||||
expect(hasOriginalTask).toBe(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
describe("getEffectiveApiHistory", () => {
|
||||
it("should return summary and messages after summary (fresh start model)", () => {
|
||||
const condenseId = "summary-123"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message", ts: 1, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "First response", ts: 2, condenseParent: condenseId },
|
||||
{ role: "user", content: "Second message", ts: 3, condenseParent: condenseId },
|
||||
|
|
@ -39,12 +39,12 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// Fresh start model: summary + all messages after it
|
||||
expect(effective.length).toBe(3)
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[1].content).toBe("Third message")
|
||||
expect(effective[2].content).toBe("Third response")
|
||||
expect((effective[1] as any).content).toBe("Third message")
|
||||
expect((effective[2] as any).content).toBe("Third response")
|
||||
})
|
||||
|
||||
it("should include messages without condenseParent", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi", ts: 2 },
|
||||
]
|
||||
|
|
@ -64,7 +64,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
describe("cleanupAfterTruncation", () => {
|
||||
it("should clear condenseParent when summary message is deleted", () => {
|
||||
const condenseId = "summary-123"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message", ts: 1 },
|
||||
{ role: "assistant", content: "First response", ts: 2, condenseParent: condenseId },
|
||||
{ role: "user", content: "Second message", ts: 3, condenseParent: condenseId },
|
||||
|
|
@ -80,7 +80,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
|
||||
it("should preserve condenseParent when summary message still exists", () => {
|
||||
const condenseId = "summary-123"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message", ts: 1 },
|
||||
{ role: "assistant", content: "First response", ts: 2, condenseParent: condenseId },
|
||||
{ role: "user", content: "Summary", ts: 3, isSummary: true, condenseId },
|
||||
|
|
@ -95,7 +95,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
it("should handle multiple condense operations with different IDs", () => {
|
||||
const condenseId1 = "summary-1"
|
||||
const condenseId2 = "summary-2"
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Message 1", ts: 1, condenseParent: condenseId1 },
|
||||
{ role: "user", content: "Summary 1", ts: 2, isSummary: true, condenseId: condenseId1 },
|
||||
{ role: "user", content: "Message 2", ts: 3, condenseParent: condenseId2 },
|
||||
|
|
@ -111,7 +111,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
})
|
||||
|
||||
it("should not modify messages without condenseParent", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Hi", ts: 2 },
|
||||
]
|
||||
|
|
@ -132,7 +132,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
const condenseId = "summary-abc"
|
||||
|
||||
// Simulate a conversation after condensing (all prior messages tagged)
|
||||
const fullHistory: ApiMessage[] = [
|
||||
const fullHistory: any[] = [
|
||||
{ role: "user", content: "Initial task", ts: 1, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Working on it", ts: 2, condenseParent: condenseId },
|
||||
{ role: "user", content: "Continue", ts: 3, condenseParent: condenseId },
|
||||
|
|
@ -152,11 +152,11 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
}
|
||||
|
||||
// Verify effective history after cleanup: all messages should be visible now
|
||||
const effectiveAfterCleanup = getEffectiveApiHistory(cleanedAfterDeletingSummary)
|
||||
const effectiveAfterCleanup = getEffectiveApiHistory(cleanedAfterDeletingSummary as any)
|
||||
expect(effectiveAfterCleanup.length).toBe(3)
|
||||
expect(effectiveAfterCleanup[0].content).toBe("Initial task")
|
||||
expect(effectiveAfterCleanup[1].content).toBe("Working on it")
|
||||
expect(effectiveAfterCleanup[2].content).toBe("Continue")
|
||||
expect((effectiveAfterCleanup[0] as any).content).toBe("Initial task")
|
||||
expect((effectiveAfterCleanup[1] as any).content).toBe("Working on it")
|
||||
expect((effectiveAfterCleanup[2] as any).content).toBe("Continue")
|
||||
})
|
||||
|
||||
it("should properly restore context after rewind when summary was deleted", () => {
|
||||
|
|
@ -165,7 +165,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// Scenario: Most of the conversation was condensed, but the summary was deleted.
|
||||
// getEffectiveApiHistory already correctly handles orphaned messages (includes them
|
||||
// when their summary doesn't exist). cleanupAfterTruncation cleans up the tags.
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Start", ts: 1 },
|
||||
{ role: "assistant", content: "Response 1", ts: 2, condenseParent: condenseId },
|
||||
{ role: "user", content: "More", ts: 3, condenseParent: condenseId },
|
||||
|
|
@ -177,8 +177,8 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// getEffectiveApiHistory already includes orphaned messages (summary doesn't exist)
|
||||
const effectiveBefore = getEffectiveApiHistory(messages)
|
||||
expect(effectiveBefore.length).toBe(5) // All messages visible since summary was deleted
|
||||
expect(effectiveBefore[0].content).toBe("Start")
|
||||
expect(effectiveBefore[1].content).toBe("Response 1")
|
||||
expect((effectiveBefore[0] as any).content).toBe("Start")
|
||||
expect((effectiveBefore[1] as any).content).toBe("Response 1")
|
||||
|
||||
// cleanupAfterTruncation clears the orphaned condenseParent tags for data hygiene
|
||||
const cleaned = cleanupAfterTruncation(messages)
|
||||
|
|
@ -190,7 +190,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
expect(cleaned[4].condenseParent).toBeUndefined()
|
||||
|
||||
// After cleanup, effective history is the same (all visible)
|
||||
const effectiveAfter = getEffectiveApiHistory(cleaned)
|
||||
const effectiveAfter = getEffectiveApiHistory(cleaned as any)
|
||||
expect(effectiveAfter.length).toBe(5) // All messages visible
|
||||
})
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
|
||||
// Scenario: Messages were condensed and summary exists - fresh start model returns
|
||||
// only the summary and messages after it, NOT messages before the summary
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Start", ts: 1 },
|
||||
{ role: "assistant", content: "Response 1", ts: 2, condenseParent: condenseId },
|
||||
{ role: "user", content: "More", ts: 3, condenseParent: condenseId },
|
||||
|
|
@ -211,9 +211,9 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// "Start" is NOT included because it's before the summary
|
||||
const effective = getEffectiveApiHistory(messages)
|
||||
expect(effective.length).toBe(2) // Summary, After summary (NOT Start)
|
||||
expect(effective[0].content).toBe("Summary")
|
||||
expect((effective[0] as any).content).toBe("Summary")
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[1].content).toBe("After summary")
|
||||
expect((effective[1] as any).content).toBe("After summary")
|
||||
|
||||
// cleanupAfterTruncation should NOT clear condenseParent since summary exists
|
||||
const cleaned = cleanupAfterTruncation(messages)
|
||||
|
|
@ -241,7 +241,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// Simulate post-condense state where summary has unique timestamp (firstKeptTs - 1)
|
||||
// In real usage, condensed messages have timestamps like 100, 200, 300...
|
||||
// and firstKeptTs is much larger, so firstKeptTs - 1 = 999 is unique
|
||||
const messagesAfterCondense: ApiMessage[] = [
|
||||
const messagesAfterCondense: any[] = [
|
||||
{ role: "user", content: "Initial task", ts: 100 },
|
||||
{ role: "assistant", content: "Response 1", ts: 200, condenseParent: condenseId },
|
||||
{ role: "user", content: "Continue", ts: 300, condenseParent: condenseId },
|
||||
|
|
@ -281,7 +281,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
const condenseId = "summary-lookup-test"
|
||||
const firstKeptTs = 8
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "Initial", ts: 1 },
|
||||
{ role: "user", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId },
|
||||
{ role: "assistant", content: "First kept message", ts: firstKeptTs },
|
||||
|
|
@ -320,7 +320,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// - msg2-msg7 tagged with condenseParent
|
||||
// - summary inserted with ts = msg8.ts - 1
|
||||
// - msg8, msg9, msg10 kept
|
||||
const storageAfterCondense: ApiMessage[] = [
|
||||
const storageAfterCondense: any[] = [
|
||||
{ role: "user", content: "Task: Build a feature", ts: 100, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "I'll help with that", ts: 200, condenseParent: condenseId },
|
||||
{ role: "user", content: "Start with the API", ts: 300, condenseParent: condenseId },
|
||||
|
|
@ -350,23 +350,23 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
expect(effective.length).toBe(4)
|
||||
|
||||
// Verify exact order and content
|
||||
expect(effective[0].role).toBe("user")
|
||||
expect((effective[0] as any).role).toBe("user")
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[0].content).toBe("Summary: Built API with validation, working on tests")
|
||||
expect((effective[0] as any).content).toBe("Summary: Built API with validation, working on tests")
|
||||
|
||||
expect(effective[1].role).toBe("assistant")
|
||||
expect(effective[1].content).toBe("Writing unit tests now")
|
||||
expect((effective[1] as any).role).toBe("assistant")
|
||||
expect((effective[1] as any).content).toBe("Writing unit tests now")
|
||||
|
||||
expect(effective[2].role).toBe("user")
|
||||
expect(effective[2].content).toBe("Include edge cases")
|
||||
expect((effective[2] as any).role).toBe("user")
|
||||
expect((effective[2] as any).content).toBe("Include edge cases")
|
||||
|
||||
expect(effective[3].role).toBe("assistant")
|
||||
expect(effective[3].content).toBe("Added edge case tests")
|
||||
expect((effective[3] as any).role).toBe("assistant")
|
||||
expect((effective[3] as any).content).toBe("Added edge case tests")
|
||||
|
||||
// Verify condensed messages are NOT in effective history
|
||||
const condensedContents = ["I'll help with that", "Start with the API", "Creating API endpoints"]
|
||||
for (const content of condensedContents) {
|
||||
expect(effective.find((m) => m.content === content)).toBeUndefined()
|
||||
expect(effective.find((m) => (m as any).content === content)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -380,7 +380,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// Second condense: summary1 + msg8-msg17 condensed, summary2 created
|
||||
//
|
||||
// Storage after double condense:
|
||||
const storageAfterDoubleCondense: ApiMessage[] = [
|
||||
const storageAfterDoubleCondense: any[] = [
|
||||
// First message - condensed during the first condense
|
||||
{ role: "user", content: "Initial task: Build a full app", ts: 100, condenseParent: condenseId1 },
|
||||
|
||||
|
|
@ -437,22 +437,22 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
expect(effective.length).toBe(4)
|
||||
|
||||
// Verify exact order and content
|
||||
expect(effective[0].role).toBe("user")
|
||||
expect((effective[0] as any).role).toBe("user")
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[0].condenseId).toBe(condenseId2) // Must be the SECOND summary
|
||||
expect(effective[0].content).toContain("Summary2")
|
||||
expect((effective[0] as any).content).toContain("Summary2")
|
||||
|
||||
expect(effective[1].role).toBe("assistant")
|
||||
expect(effective[1].content).toBe("Writing integration tests")
|
||||
expect((effective[1] as any).role).toBe("assistant")
|
||||
expect((effective[1] as any).content).toBe("Writing integration tests")
|
||||
|
||||
expect(effective[2].role).toBe("user")
|
||||
expect(effective[2].content).toBe("Test the auth flow")
|
||||
expect((effective[2] as any).role).toBe("user")
|
||||
expect((effective[2] as any).content).toBe("Test the auth flow")
|
||||
|
||||
expect(effective[3].role).toBe("assistant")
|
||||
expect(effective[3].content).toBe("Auth tests passing")
|
||||
expect((effective[3] as any).role).toBe("assistant")
|
||||
expect((effective[3] as any).content).toBe("Auth tests passing")
|
||||
|
||||
// Verify Summary1 is NOT in effective history (it's tagged with condenseParent)
|
||||
const summary1 = effective.find((m) => m.content?.toString().includes("Summary1"))
|
||||
const summary1 = effective.find((m) => (m as any).content?.toString().includes("Summary1"))
|
||||
expect(summary1).toBeUndefined()
|
||||
|
||||
// Verify all condensed messages are NOT in effective history
|
||||
|
|
@ -464,7 +464,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
"Implemented error handlers",
|
||||
]
|
||||
for (const content of condensedContents) {
|
||||
expect(effective.find((m) => m.content === content)).toBeUndefined()
|
||||
expect(effective.find((m) => (m as any).content === content)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -473,7 +473,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
|
||||
// Verify that after condense, the effective history maintains proper
|
||||
// user/assistant message alternation (important for API compatibility)
|
||||
const storage: ApiMessage[] = [
|
||||
const storage: any[] = [
|
||||
{ role: "user", content: "Start task", ts: 100, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Response 1", ts: 200, condenseParent: condenseId },
|
||||
{ role: "user", content: "Continue", ts: 300, condenseParent: condenseId },
|
||||
|
|
@ -488,17 +488,17 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
|
||||
// Verify the sequence: user(summary), assistant, user, assistant
|
||||
// This is the fresh-start model with user-role summaries
|
||||
expect(effective[0].role).toBe("user")
|
||||
expect((effective[0] as any).role).toBe("user")
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[1].role).toBe("assistant")
|
||||
expect(effective[2].role).toBe("user")
|
||||
expect(effective[3].role).toBe("assistant")
|
||||
expect((effective[1] as any).role).toBe("assistant")
|
||||
expect((effective[2] as any).role).toBe("user")
|
||||
expect((effective[3] as any).role).toBe("assistant")
|
||||
})
|
||||
|
||||
it("should preserve timestamps in chronological order in effective history", () => {
|
||||
const condenseId = "summary-timestamps"
|
||||
|
||||
const storage: ApiMessage[] = [
|
||||
const storage: any[] = [
|
||||
{ role: "user", content: "First", ts: 100, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Condensed", ts: 200, condenseParent: condenseId },
|
||||
{ role: "user", content: "Summary", ts: 299, isSummary: true, condenseId },
|
||||
|
|
|
|||
|
|
@ -1,64 +1,50 @@
|
|||
import Anthropic from "@anthropic-ai/sdk"
|
||||
import crypto from "crypto"
|
||||
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { t } from "../../i18n"
|
||||
import { ApiHandler, ApiHandlerCreateMessageMetadata } from "../../api"
|
||||
import { ApiMessage } from "../task-persistence/apiMessages"
|
||||
import {
|
||||
type RooMessage,
|
||||
type RooUserMessage,
|
||||
type RooToolMessage,
|
||||
type RooRoleMessage,
|
||||
isRooAssistantMessage,
|
||||
isRooToolMessage,
|
||||
isRooUserMessage,
|
||||
isRooRoleMessage,
|
||||
type ToolCallPart,
|
||||
type ToolResultPart,
|
||||
type TextPart,
|
||||
type AnyToolCallBlock,
|
||||
type AnyToolResultBlock,
|
||||
isAnyToolCallBlock,
|
||||
isAnyToolResultBlock,
|
||||
getToolCallId,
|
||||
getToolCallName,
|
||||
getToolCallInput,
|
||||
getToolResultCallId,
|
||||
getToolResultContent,
|
||||
getToolResultIsError,
|
||||
} from "../task-persistence/rooMessage"
|
||||
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { supportPrompt } from "../../shared/support-prompt"
|
||||
import { RooIgnoreController } from "../ignore/RooIgnoreController"
|
||||
import {
|
||||
type ToolResultLikeBlock,
|
||||
type ToolUseLikeBlock,
|
||||
getToolResultLikeId,
|
||||
getToolResultLikePayload,
|
||||
getToolUseLikeId,
|
||||
getToolUseLikeName,
|
||||
isToolResultLikeBlock,
|
||||
isToolUseLikeBlock,
|
||||
stringifyUnknown,
|
||||
} from "../task/toolBlockFormat"
|
||||
import { generateFoldedFileContext } from "./foldedFileContext"
|
||||
|
||||
export type { FoldedFileContextResult, FoldedFileContextOptions } from "./foldedFileContext"
|
||||
|
||||
function toolResultPayloadToText(payload: unknown): string {
|
||||
if (typeof payload === "string") {
|
||||
return payload
|
||||
}
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
.map((contentBlock: unknown) => {
|
||||
if (!contentBlock || typeof contentBlock !== "object") {
|
||||
return stringifyUnknown(contentBlock)
|
||||
}
|
||||
const block = contentBlock as { type?: string; text?: string; value?: unknown }
|
||||
if (block.type === "text") {
|
||||
return typeof block.text === "string" ? block.text : stringifyUnknown(block.value)
|
||||
}
|
||||
if (block.type === "image") {
|
||||
return "[Image]"
|
||||
}
|
||||
return `[${block.type ?? "unknown"}]`
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
return stringifyUnknown(payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a tool_use block to a text representation.
|
||||
* This allows the conversation to be summarized without requiring the tools parameter.
|
||||
* Converts a tool-call / tool_use block to a text representation.
|
||||
* Accepts both AI SDK ToolCallPart (toolName, input) and legacy Anthropic format (name, input).
|
||||
*/
|
||||
export function toolUseToText(block: Anthropic.Messages.ToolUseBlockParam | ToolUseLikeBlock): string {
|
||||
const toolName = getToolUseLikeName(block as ToolUseLikeBlock)
|
||||
const toolInput = (block as ToolUseLikeBlock).input
|
||||
export function toolUseToText(block: AnyToolCallBlock): string {
|
||||
const name = getToolCallName(block)
|
||||
const rawInput = getToolCallInput(block)
|
||||
let input: string
|
||||
if (typeof toolInput === "object" && toolInput !== null) {
|
||||
input = Object.entries(toolInput)
|
||||
if (typeof rawInput === "object" && rawInput !== null) {
|
||||
input = Object.entries(rawInput)
|
||||
.map(([key, value]) => {
|
||||
const formattedValue =
|
||||
typeof value === "object" && value !== null ? JSON.stringify(value, null, 2) : String(value)
|
||||
|
|
@ -66,21 +52,40 @@ export function toolUseToText(block: Anthropic.Messages.ToolUseBlockParam | Tool
|
|||
})
|
||||
.join("\n")
|
||||
} else {
|
||||
input = String(toolInput)
|
||||
input = String(rawInput)
|
||||
}
|
||||
return `[Tool Use: ${toolName}]\n${input}`
|
||||
return `[Tool Use: ${name}]\n${input}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a tool_result block to a text representation.
|
||||
* This allows the conversation to be summarized without requiring the tools parameter.
|
||||
* Converts a tool-result / tool_result block to a text representation.
|
||||
* Accepts both AI SDK ToolResultPart and legacy Anthropic format.
|
||||
*/
|
||||
export function toolResultToText(block: Anthropic.Messages.ToolResultBlockParam | ToolResultLikeBlock): string {
|
||||
const isError = (block as ToolResultLikeBlock).is_error || (block as ToolResultLikeBlock).isError
|
||||
export function toolResultToText(block: AnyToolResultBlock): string {
|
||||
const isError = getToolResultIsError(block)
|
||||
const errorSuffix = isError ? " (Error)" : ""
|
||||
const payload = getToolResultLikePayload(block as ToolResultLikeBlock)
|
||||
const text = toolResultPayloadToText(payload)
|
||||
return text ? `[Tool Result${errorSuffix}]\n${text}` : `[Tool Result${errorSuffix}]`
|
||||
// AI SDK uses `output`, legacy uses `content`
|
||||
const rawContent = getToolResultContent(block)
|
||||
if (typeof rawContent === "string") {
|
||||
return `[Tool Result${errorSuffix}]\n${rawContent}`
|
||||
} else if (Array.isArray(rawContent)) {
|
||||
const contentText = rawContent
|
||||
.map((contentBlock: { type: string; text?: string }) => {
|
||||
if (contentBlock.type === "text") {
|
||||
return contentBlock.text
|
||||
}
|
||||
if (contentBlock.type === "image") {
|
||||
return "[Image]"
|
||||
}
|
||||
return `[${contentBlock.type}]`
|
||||
})
|
||||
.join("\n")
|
||||
return `[Tool Result${errorSuffix}]\n${contentText}`
|
||||
} else if (rawContent && typeof rawContent === "object" && "value" in rawContent) {
|
||||
// AI SDK ToolResultPart.output has shape { type: "text", value: string }
|
||||
return `[Tool Result${errorSuffix}]\n${String((rawContent as { value: unknown }).value)}`
|
||||
}
|
||||
return `[Tool Result${errorSuffix}]`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,21 +96,21 @@ export function toolResultToText(block: Anthropic.Messages.ToolResultBlockParam
|
|||
* @param content - The message content (string or array of content blocks)
|
||||
* @returns The transformed content with tool blocks converted to text blocks
|
||||
*/
|
||||
export function convertToolBlocksToText(
|
||||
content: string | Anthropic.Messages.ContentBlockParam[],
|
||||
): string | Anthropic.Messages.ContentBlockParam[] {
|
||||
export function convertToolBlocksToText(content: string | Array<{ type: string }>): string | Array<{ type: string }> {
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
|
||||
return content.map((block) => {
|
||||
if (isToolUseLikeBlock(block)) {
|
||||
// Check both AI SDK (`tool-call`) and legacy (`tool_use`) discriminators
|
||||
if (isAnyToolCallBlock(block)) {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: toolUseToText(block),
|
||||
}
|
||||
}
|
||||
if (isToolResultLikeBlock(block)) {
|
||||
// Check both AI SDK (`tool-result`) and legacy (`tool_result`) discriminators
|
||||
if (isAnyToolResultBlock(block)) {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: toolResultToText(block),
|
||||
|
|
@ -122,9 +127,9 @@ export function convertToolBlocksToText(
|
|||
* @param messages - The messages to transform
|
||||
* @returns The transformed messages with tool blocks converted to text
|
||||
*/
|
||||
export function transformMessagesForCondensing<
|
||||
T extends { role: string; content: string | Anthropic.Messages.ContentBlockParam[] },
|
||||
>(messages: T[]): T[] {
|
||||
export function transformMessagesForCondensing<T extends { role: string; content: string | Array<{ type: string }> }>(
|
||||
messages: T[],
|
||||
): T[] {
|
||||
return messages.map((msg) => ({
|
||||
...msg,
|
||||
content: convertToolBlocksToText(msg.content),
|
||||
|
|
@ -154,30 +159,33 @@ The goal is for work to continue seamlessly after condensation - as if it never
|
|||
* @param messages - The conversation messages to process
|
||||
* @returns The messages with synthetic tool_results appended if needed
|
||||
*/
|
||||
export function injectSyntheticToolResults(messages: ApiMessage[]): ApiMessage[] {
|
||||
// Find all tool_call IDs in assistant messages
|
||||
export function injectSyntheticToolResults(messages: RooMessage[]): RooMessage[] {
|
||||
// Find all tool-call IDs in assistant messages
|
||||
const toolCallIds = new Set<string>()
|
||||
// Find all tool_result IDs in user messages
|
||||
// Find all tool-result IDs in user/tool messages
|
||||
const toolResultIds = new Set<string>()
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
if (isRooAssistantMessage(msg) && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (isToolUseLikeBlock(block)) {
|
||||
const id = getToolUseLikeId(block)
|
||||
if (id) {
|
||||
toolCallIds.add(id)
|
||||
}
|
||||
if (isAnyToolCallBlock(block as { type: string })) {
|
||||
toolCallIds.add(getToolCallId(block as AnyToolCallBlock))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
if (isRooToolMessage(msg) && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (isToolResultLikeBlock(block)) {
|
||||
const id = getToolResultLikeId(block)
|
||||
if (id) {
|
||||
toolResultIds.add(id)
|
||||
}
|
||||
if (isAnyToolResultBlock(block as { type: string })) {
|
||||
toolResultIds.add(getToolResultCallId(block as AnyToolResultBlock))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also check legacy user messages with tool_result blocks
|
||||
if (isRooUserMessage(msg) && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
const typedBlock = block as unknown as { type: string }
|
||||
if (isAnyToolResultBlock(typedBlock)) {
|
||||
toolResultIds.add(getToolResultCallId(typedBlock))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -190,15 +198,16 @@ export function injectSyntheticToolResults(messages: ApiMessage[]): ApiMessage[]
|
|||
return messages
|
||||
}
|
||||
|
||||
// Inject synthetic tool_results as a new user message
|
||||
const syntheticResults: Anthropic.Messages.ToolResultBlockParam[] = orphanIds.map((id) => ({
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: id,
|
||||
content: "Context condensation triggered. Tool execution deferred.",
|
||||
// Inject synthetic tool_results as a new RooToolMessage
|
||||
const syntheticResults: ToolResultPart[] = orphanIds.map((id) => ({
|
||||
type: "tool-result" as const,
|
||||
toolCallId: id,
|
||||
toolName: "unknown",
|
||||
output: { type: "text" as const, value: "Context condensation triggered. Tool execution deferred." },
|
||||
}))
|
||||
|
||||
const syntheticMessage: ApiMessage = {
|
||||
role: "user",
|
||||
const syntheticMessage: RooToolMessage = {
|
||||
role: "tool",
|
||||
content: syntheticResults,
|
||||
ts: Date.now(),
|
||||
}
|
||||
|
|
@ -213,7 +222,10 @@ export function injectSyntheticToolResults(messages: ApiMessage[]): ApiMessage[]
|
|||
* @param message - The message to extract command blocks from
|
||||
* @returns A string containing all command blocks found, or empty string if none
|
||||
*/
|
||||
export function extractCommandBlocks(message: ApiMessage): string {
|
||||
export function extractCommandBlocks(message: RooMessage): string {
|
||||
if (!isRooRoleMessage(message)) {
|
||||
return ""
|
||||
}
|
||||
const content = message.content
|
||||
let text: string
|
||||
|
||||
|
|
@ -222,7 +234,7 @@ export function extractCommandBlocks(message: ApiMessage): string {
|
|||
} else if (Array.isArray(content)) {
|
||||
// Concatenate all text blocks
|
||||
text = content
|
||||
.filter((block): block is Anthropic.Messages.TextBlockParam => block.type === "text")
|
||||
.filter((block): block is TextPart => (block as { type: string }).type === "text")
|
||||
.map((block) => block.text)
|
||||
.join("\n")
|
||||
} else {
|
||||
|
|
@ -241,7 +253,7 @@ export function extractCommandBlocks(message: ApiMessage): string {
|
|||
}
|
||||
|
||||
export type SummarizeResponse = {
|
||||
messages: ApiMessage[] // The messages after summarization
|
||||
messages: RooMessage[] // The messages after summarization
|
||||
summary: string // The summary text; empty string for no summary
|
||||
cost: number // The cost of the summarization operation
|
||||
newContextTokens?: number // The number of tokens in the context for the next API request
|
||||
|
|
@ -251,7 +263,7 @@ export type SummarizeResponse = {
|
|||
}
|
||||
|
||||
export type SummarizeConversationOptions = {
|
||||
messages: ApiMessage[]
|
||||
messages: RooMessage[]
|
||||
apiHandler: ApiHandler
|
||||
systemPrompt: string
|
||||
taskId: string
|
||||
|
|
@ -316,7 +328,7 @@ export async function summarizeConversation(options: SummarizeConversationOption
|
|||
}
|
||||
|
||||
// Check if there's a recent summary in the messages (edge case)
|
||||
const recentSummaryExists = messagesToSummarize.some((message: ApiMessage) => message.isSummary)
|
||||
const recentSummaryExists = messagesToSummarize.some((message) => message.isSummary)
|
||||
|
||||
if (recentSummaryExists && messagesToSummarize.length <= 2) {
|
||||
const error = t("common:errors.condensed_recently")
|
||||
|
|
@ -327,7 +339,7 @@ export async function summarizeConversation(options: SummarizeConversationOption
|
|||
// This respects user's custom condensing prompt setting
|
||||
const condenseInstructions = customCondensingPrompt?.trim() || supportPrompt.default.CONDENSE
|
||||
|
||||
const finalRequestMessage: Anthropic.MessageParam = {
|
||||
const finalRequestMessage: RooUserMessage = {
|
||||
role: "user",
|
||||
content: condenseInstructions,
|
||||
}
|
||||
|
|
@ -340,8 +352,15 @@ export async function summarizeConversation(options: SummarizeConversationOption
|
|||
// This is necessary because some providers (like Bedrock via LiteLLM) require the `tools` parameter
|
||||
// when tool blocks are present. By converting them to text, we can send the conversation for
|
||||
// summarization without needing to pass the tools parameter.
|
||||
// Filter out reasoning messages (no role/content) before transforming for the API
|
||||
const messagesForApi = [...messagesWithToolResults, finalRequestMessage].filter(
|
||||
(msg): msg is Exclude<RooMessage, { type: "reasoning" }> => "role" in msg,
|
||||
)
|
||||
const messagesWithTextToolBlocks = transformMessagesForCondensing(
|
||||
maybeRemoveImageBlocks([...messagesWithToolResults, finalRequestMessage], apiHandler),
|
||||
maybeRemoveImageBlocks(messagesForApi, apiHandler) as Array<{
|
||||
role: string
|
||||
content: string | Array<{ type: string }>
|
||||
}>,
|
||||
)
|
||||
|
||||
const requestMessages = messagesWithTextToolBlocks.map(({ role, content }) => ({ role, content }))
|
||||
|
|
@ -361,7 +380,7 @@ export async function summarizeConversation(options: SummarizeConversationOption
|
|||
let outputTokens = 0
|
||||
|
||||
try {
|
||||
const stream = apiHandler.createMessage(promptToUse, requestMessages, metadata)
|
||||
const stream = apiHandler.createMessage(promptToUse, requestMessages as RooMessage[], metadata)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === "text") {
|
||||
|
|
@ -427,9 +446,7 @@ export async function summarizeConversation(options: SummarizeConversationOption
|
|||
const commandBlocks = firstMessage ? extractCommandBlocks(firstMessage) : ""
|
||||
|
||||
// Build the summary content as separate text blocks
|
||||
const summaryContent: Anthropic.Messages.ContentBlockParam[] = [
|
||||
{ type: "text", text: `## Conversation Summary\n${summary}` },
|
||||
]
|
||||
const summaryContent: TextPart[] = [{ type: "text", text: `## Conversation Summary\n${summary}` }]
|
||||
|
||||
// Add command blocks (active workflows) in their own system-reminder block if present
|
||||
if (commandBlocks) {
|
||||
|
|
@ -484,7 +501,7 @@ ${commandBlocks}
|
|||
// The summary goes at the end of all messages.
|
||||
const lastMsgTs = messages[messages.length - 1]?.ts ?? Date.now()
|
||||
|
||||
const summaryMessage: ApiMessage = {
|
||||
const summaryMessage: RooUserMessage = {
|
||||
role: "user", // Fresh start model: summary is a user message
|
||||
content: summaryContent,
|
||||
ts: lastMsgTs + 1, // Unique timestamp after last message
|
||||
|
|
@ -517,7 +534,7 @@ ${commandBlocks}
|
|||
|
||||
// Count the tokens in the context for the next API request
|
||||
// After condense, the context will contain: system prompt + summary + tool definitions
|
||||
const systemPromptMessage: ApiMessage = { role: "user", content: systemPrompt }
|
||||
const systemPromptMessage: RooUserMessage = { role: "user", content: systemPrompt }
|
||||
|
||||
// Count actual summaryMessage content directly instead of using outputTokens as a proxy
|
||||
// This ensures we account for wrapper text (## Conversation Summary, <system-reminder>, <environment_details>)
|
||||
|
|
@ -525,7 +542,7 @@ ${commandBlocks}
|
|||
typeof message.content === "string" ? [{ text: message.content, type: "text" as const }] : message.content,
|
||||
)
|
||||
|
||||
const messageTokens = await apiHandler.countTokens(contextBlocks)
|
||||
const messageTokens = await apiHandler.countTokens(contextBlocks as Parameters<typeof apiHandler.countTokens>[0])
|
||||
|
||||
// Count tool definition tokens if tools are provided
|
||||
let toolTokens = 0
|
||||
|
|
@ -545,7 +562,7 @@ ${commandBlocks}
|
|||
* Note: Summary messages are always created with role: "user" (fresh-start model),
|
||||
* so the first message since the last summary is guaranteed to be a user message.
|
||||
*/
|
||||
export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[] {
|
||||
export function getMessagesSinceLastSummary(messages: RooMessage[]): RooMessage[] {
|
||||
const lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary)
|
||||
|
||||
if (lastSummaryIndexReverse === -1) {
|
||||
|
|
@ -572,7 +589,7 @@ export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[
|
|||
* @param messages - The full API conversation history including tagged messages
|
||||
* @returns The filtered history that should be sent to the API
|
||||
*/
|
||||
export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
||||
export function getEffectiveApiHistory(messages: RooMessage[]): RooMessage[] {
|
||||
// Find the most recent summary message
|
||||
const lastSummary = findLast(messages, (msg) => msg.isSummary === true)
|
||||
|
||||
|
|
@ -581,46 +598,56 @@ export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
|||
const summaryIndex = messages.indexOf(lastSummary)
|
||||
let messagesFromSummary = messages.slice(summaryIndex)
|
||||
|
||||
// Collect all tool_use IDs from assistant messages in the result
|
||||
// This is needed to filter out orphan tool_result blocks that reference
|
||||
// tool_use IDs from messages that were condensed away
|
||||
const toolUseIds = new Set<string>()
|
||||
// Collect all tool call IDs from assistant messages in the result.
|
||||
// This is needed to filter out orphan tool results that reference
|
||||
// tool call IDs from messages that were condensed away.
|
||||
const toolCallIds = new Set<string>()
|
||||
for (const msg of messagesFromSummary) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (isToolUseLikeBlock(block)) {
|
||||
const id = getToolUseLikeId(block)
|
||||
if (id) {
|
||||
toolUseIds.add(id)
|
||||
}
|
||||
if (isRooAssistantMessage(msg) && Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (isAnyToolCallBlock(part as { type: string })) {
|
||||
toolCallIds.add(getToolCallId(part as AnyToolCallBlock))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out orphan tool_result blocks from user messages
|
||||
// Filter out orphan tool results from tool messages
|
||||
messagesFromSummary = messagesFromSummary
|
||||
.map((msg) => {
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
const filteredContent = msg.content.filter((block) => {
|
||||
if (isToolResultLikeBlock(block)) {
|
||||
const id = getToolResultLikeId(block)
|
||||
return id ? toolUseIds.has(id) : false
|
||||
if (isRooToolMessage(msg) && Array.isArray(msg.content)) {
|
||||
const filteredContent = msg.content.filter((part) => {
|
||||
if (part.type === "tool-result") {
|
||||
return toolCallIds.has((part as ToolResultPart).toolCallId)
|
||||
}
|
||||
return true
|
||||
})
|
||||
// If all content was filtered out, mark for removal
|
||||
if (filteredContent.length === 0) {
|
||||
return null
|
||||
}
|
||||
// If some content was filtered, return updated message
|
||||
if (filteredContent.length !== msg.content.length) {
|
||||
return { ...msg, content: filteredContent }
|
||||
}
|
||||
}
|
||||
// Also handle legacy user messages that may contain tool_result blocks
|
||||
if (isRooUserMessage(msg) && Array.isArray(msg.content)) {
|
||||
const filteredContent = msg.content.filter((block) => {
|
||||
const typedBlock = block as unknown as { type: string }
|
||||
if (isAnyToolResultBlock(typedBlock)) {
|
||||
return toolCallIds.has(getToolResultCallId(typedBlock))
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (filteredContent.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (filteredContent.length !== msg.content.length) {
|
||||
return { ...msg, content: filteredContent as typeof msg.content }
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
.filter((msg): msg is ApiMessage => msg !== null)
|
||||
.filter((msg): msg is RooMessage => msg !== null)
|
||||
|
||||
// Still need to filter out any truncated messages within this range
|
||||
const existingTruncationIds = new Set<string>()
|
||||
|
|
@ -631,7 +658,6 @@ export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
|||
}
|
||||
|
||||
return messagesFromSummary.filter((msg) => {
|
||||
// Filter out truncated messages if their truncation marker exists
|
||||
if (msg.truncationParent && existingTruncationIds.has(msg.truncationParent)) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -642,9 +668,7 @@ export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
|||
// No summary - filter based on condenseParent and truncationParent as before
|
||||
// This handles the case of orphaned condenseParent tags (summary was deleted via rewind)
|
||||
|
||||
// Collect all condenseIds of summaries that exist in the current history
|
||||
const existingSummaryIds = new Set<string>()
|
||||
// Collect all truncationIds of truncation markers that exist in the current history
|
||||
const existingTruncationIds = new Set<string>()
|
||||
|
||||
for (const msg of messages) {
|
||||
|
|
@ -656,15 +680,10 @@ export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
|||
}
|
||||
}
|
||||
|
||||
// Filter out messages whose condenseParent points to an existing summary
|
||||
// or whose truncationParent points to an existing truncation marker.
|
||||
// Messages with orphaned parents (summary/marker was deleted) are included.
|
||||
return messages.filter((msg) => {
|
||||
// Filter out condensed messages if their summary exists
|
||||
if (msg.condenseParent && existingSummaryIds.has(msg.condenseParent)) {
|
||||
return false
|
||||
}
|
||||
// Filter out truncated messages if their truncation marker exists
|
||||
if (msg.truncationParent && existingTruncationIds.has(msg.truncationParent)) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -683,7 +702,7 @@ export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
|||
* @param messages - The API conversation history after truncation
|
||||
* @returns The cleaned history with orphaned condenseParent and truncationParent fields cleared
|
||||
*/
|
||||
export function cleanupAfterTruncation(messages: ApiMessage[]): ApiMessage[] {
|
||||
export function cleanupAfterTruncation(messages: RooMessage[]): RooMessage[] {
|
||||
// Collect all condenseIds of summaries that still exist
|
||||
const existingSummaryIds = new Set<string>()
|
||||
// Collect all truncationIds of truncation markers that still exist
|
||||
|
|
@ -715,7 +734,7 @@ export function cleanupAfterTruncation(messages: ApiMessage[]): ApiMessage[] {
|
|||
if (needsUpdate) {
|
||||
// Create a new object without orphaned parent references
|
||||
const { condenseParent, truncationParent, ...rest } = msg
|
||||
const result: ApiMessage = rest as ApiMessage
|
||||
const result = rest as RooMessage
|
||||
|
||||
// Keep condenseParent if its summary still exists
|
||||
if (condenseParent && existingSummaryIds.has(condenseParent)) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { ModelInfo } from "@roo-code/types"
|
|||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { BaseProvider } from "../../../api/providers/base-provider"
|
||||
import { ApiMessage } from "../../task-persistence/apiMessages"
|
||||
|
||||
import * as condenseModule from "../../condense"
|
||||
|
||||
import {
|
||||
|
|
@ -61,7 +61,7 @@ describe("Context Management", () => {
|
|||
*/
|
||||
describe("truncateConversation", () => {
|
||||
it("should retain the first message", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -80,7 +80,7 @@ describe("Context Management", () => {
|
|||
})
|
||||
|
||||
it("should remove the specified fraction of messages (rounded to even number)", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -103,7 +103,7 @@ describe("Context Management", () => {
|
|||
|
||||
// Marker should be at index 3 (at the boundary, after truncated messages)
|
||||
expect(result.messages[3].isTruncationMarker).toBe(true)
|
||||
expect(result.messages[3].role).toBe("user")
|
||||
expect((result.messages[3] as any).role).toBe("user")
|
||||
|
||||
// Messages at indices 3 and 4 from original should NOT be tagged (now at indices 4 and 5)
|
||||
expect(result.messages[4].truncationParent).toBeUndefined()
|
||||
|
|
@ -111,7 +111,7 @@ describe("Context Management", () => {
|
|||
})
|
||||
|
||||
it("should round to an even number of messages to remove", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -131,7 +131,7 @@ describe("Context Management", () => {
|
|||
})
|
||||
|
||||
it("should handle edge case with fracToRemove = 0", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -145,7 +145,7 @@ describe("Context Management", () => {
|
|||
})
|
||||
|
||||
it("should handle edge case with fracToRemove = 1", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -167,7 +167,7 @@ describe("Context Management", () => {
|
|||
|
||||
// Marker should be at index 3 (at the boundary)
|
||||
expect(result.messages[3].isTruncationMarker).toBe(true)
|
||||
expect(result.messages[3].role).toBe("user")
|
||||
expect((result.messages[3] as any).role).toBe("user")
|
||||
|
||||
// Last message should NOT be tagged (now at index 4)
|
||||
expect(result.messages[4].truncationParent).toBeUndefined()
|
||||
|
|
@ -273,7 +273,7 @@ describe("Context Management", () => {
|
|||
maxTokens,
|
||||
})
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -446,7 +446,7 @@ describe("Context Management", () => {
|
|||
// Test case 1: Small content that won't push us over the threshold
|
||||
const smallContent = [{ type: "text" as const, text: "Small content" }]
|
||||
const smallContentTokens = await estimateTokenCount(smallContent, mockApiHandler)
|
||||
const messagesWithSmallContent: ApiMessage[] = [
|
||||
const messagesWithSmallContent: any[] = [
|
||||
...messages.slice(0, -1),
|
||||
{ role: messages[messages.length - 1].role, content: smallContent },
|
||||
]
|
||||
|
|
@ -482,7 +482,7 @@ describe("Context Management", () => {
|
|||
},
|
||||
]
|
||||
const largeContentTokens = await estimateTokenCount(largeContent, mockApiHandler)
|
||||
const messagesWithLargeContent: ApiMessage[] = [
|
||||
const messagesWithLargeContent: any[] = [
|
||||
...messages.slice(0, -1),
|
||||
{ role: messages[messages.length - 1].role, content: largeContent },
|
||||
]
|
||||
|
|
@ -510,7 +510,7 @@ describe("Context Management", () => {
|
|||
// Test case 3: Very large content that will definitely exceed threshold
|
||||
const veryLargeContent = [{ type: "text" as const, text: "X".repeat(1000) }]
|
||||
const veryLargeContentTokens = await estimateTokenCount(veryLargeContent, mockApiHandler)
|
||||
const messagesWithVeryLargeContent: ApiMessage[] = [
|
||||
const messagesWithVeryLargeContent: any[] = [
|
||||
...messages.slice(0, -1),
|
||||
{ role: messages[messages.length - 1].role, content: veryLargeContent },
|
||||
]
|
||||
|
|
@ -858,7 +858,7 @@ describe("Context Management", () => {
|
|||
maxTokens,
|
||||
})
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -1067,7 +1067,7 @@ describe("Context Management", () => {
|
|||
maxTokens,
|
||||
})
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -1275,7 +1275,7 @@ describe("Context Management", () => {
|
|||
})
|
||||
|
||||
// Reuse across tests for consistency
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -1623,7 +1623,7 @@ describe("Context Management", () => {
|
|||
const modelInfo = createModelInfo(100000, 30000)
|
||||
const totalTokens = 70001 // Above threshold to trigger truncation
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
@ -1665,7 +1665,7 @@ describe("Context Management", () => {
|
|||
const modelInfo = createModelInfo(100000, 30000)
|
||||
const totalTokens = 70001 // Above threshold to trigger truncation
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
const messages: any[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue