diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index 4d0fc821de8..92180131f38 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -745,302 +745,4 @@ describe("AnthropicHandler", () => { }) }) }) - - describe("extended thinking with signature capture", () => { - const systemPrompt = "You are a helpful assistant." - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [{ type: "text" as const, text: "Think about this carefully" }], - }, - ] - - it("should capture signature_delta and make it available via getThoughtSignature()", async () => { - mockCreate.mockImplementationOnce(async () => ({ - async *[Symbol.asyncIterator]() { - yield { - type: "message_start", - message: { - usage: { - input_tokens: 100, - output_tokens: 50, - }, - }, - } - yield { - type: "content_block_start", - index: 0, - content_block: { - type: "thinking", - thinking: "Let me think...", - }, - } - yield { - type: "content_block_delta", - index: 0, - delta: { - type: "thinking_delta", - thinking: " about this carefully", - }, - } - yield { - type: "content_block_delta", - index: 0, - delta: { - type: "signature_delta", - signature: "test_signature_123", - }, - } - yield { - type: "content_block_stop", - index: 0, - } - yield { - type: "content_block_start", - index: 1, - content_block: { - type: "text", - text: "Here is my response", - }, - } - yield { - type: "content_block_stop", - index: 1, - } - }, - })) - - const stream = handler.createMessage(systemPrompt, messages, { - taskId: "test-task", - }) - - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - // Verify reasoning chunks were emitted - const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning") - expect(reasoningChunks).toHaveLength(2) - expect(reasoningChunks[0].text).toBe("Let me think...") - expect(reasoningChunks[1].text).toBe(" about this carefully") - - // Verify thinking_complete chunk was emitted with signature - const thinkingCompleteChunk = chunks.find((chunk) => chunk.type === "thinking_complete") - expect(thinkingCompleteChunk).toBeDefined() - expect(thinkingCompleteChunk.signature).toBe("test_signature_123") - - // Verify getThoughtSignature() returns the captured signature - expect(handler.getThoughtSignature()).toBe("test_signature_123") - }) - - it("should reset signature for each new request", async () => { - // First request with signature - mockCreate.mockImplementationOnce(async () => ({ - async *[Symbol.asyncIterator]() { - yield { - type: "message_start", - message: { usage: { input_tokens: 100, output_tokens: 50 } }, - } - yield { - type: "content_block_start", - index: 0, - content_block: { type: "thinking", thinking: "First thinking" }, - } - yield { - type: "content_block_delta", - index: 0, - delta: { type: "signature_delta", signature: "first_signature" }, - } - yield { type: "content_block_stop", index: 0 } - }, - })) - - const stream1 = handler.createMessage(systemPrompt, messages, { taskId: "test-task-1" }) - for await (const _chunk of stream1) { - // consume - } - expect(handler.getThoughtSignature()).toBe("first_signature") - - // Second request without signature - mockCreate.mockImplementationOnce(async () => ({ - async *[Symbol.asyncIterator]() { - yield { - type: "message_start", - message: { usage: { input_tokens: 100, output_tokens: 50 } }, - } - yield { - type: "content_block_start", - index: 0, - content_block: { type: "text", text: "Just text, no thinking" }, - } - yield { type: "content_block_stop", index: 0 } - }, - })) - - const stream2 = handler.createMessage(systemPrompt, messages, { taskId: "test-task-2" }) - for await (const _chunk of stream2) { - // consume - } - - // Signature should be reset (undefined) for the new request - expect(handler.getThoughtSignature()).toBeUndefined() - }) - - it("should accumulate signature_delta chunks (incremental signature)", async () => { - mockCreate.mockImplementationOnce(async () => ({ - async *[Symbol.asyncIterator]() { - yield { - type: "message_start", - message: { usage: { input_tokens: 100, output_tokens: 50 } }, - } - yield { - type: "content_block_start", - index: 0, - content_block: { type: "thinking", thinking: "Thinking..." }, - } - yield { - type: "content_block_delta", - index: 0, - delta: { type: "signature_delta", signature: "sig_part1" }, - } - yield { - type: "content_block_delta", - index: 0, - delta: { type: "signature_delta", signature: "_part2" }, - } - yield { - type: "content_block_delta", - index: 0, - delta: { type: "signature_delta", signature: "_part3" }, - } - yield { type: "content_block_stop", index: 0 } - }, - })) - - const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task" }) - - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - // Verify the accumulated signature - expect(handler.getThoughtSignature()).toBe("sig_part1_part2_part3") - - // Verify thinking_complete has the accumulated signature - const thinkingCompleteChunk = chunks.find((chunk) => chunk.type === "thinking_complete") - expect(thinkingCompleteChunk?.signature).toBe("sig_part1_part2_part3") - }) - - it("should not emit thinking_complete if no signature is captured", async () => { - mockCreate.mockImplementationOnce(async () => ({ - async *[Symbol.asyncIterator]() { - yield { - type: "message_start", - message: { usage: { input_tokens: 100, output_tokens: 50 } }, - } - yield { - type: "content_block_start", - index: 0, - content_block: { type: "thinking", thinking: "Thinking without signature" }, - } - yield { - type: "content_block_delta", - index: 0, - delta: { type: "thinking_delta", thinking: "More thinking" }, - } - // No signature_delta event - yield { type: "content_block_stop", index: 0 } - }, - })) - - const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task" }) - - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - // Verify thinking_complete was NOT emitted (no signature) - const thinkingCompleteChunk = chunks.find((chunk) => chunk.type === "thinking_complete") - expect(thinkingCompleteChunk).toBeUndefined() - - // Verify getThoughtSignature() returns undefined - expect(handler.getThoughtSignature()).toBeUndefined() - }) - - it("should handle interleaved thinking with tool use", async () => { - mockCreate.mockImplementationOnce(async () => ({ - async *[Symbol.asyncIterator]() { - yield { - type: "message_start", - message: { usage: { input_tokens: 100, output_tokens: 50 } }, - } - // First: thinking block - yield { - type: "content_block_start", - index: 0, - content_block: { type: "thinking", thinking: "Let me think about what tool to use" }, - } - yield { - type: "content_block_delta", - index: 0, - delta: { type: "signature_delta", signature: "thinking_signature_abc" }, - } - yield { type: "content_block_stop", index: 0 } - // Second: tool use block - yield { - type: "content_block_start", - index: 1, - content_block: { - type: "tool_use", - id: "toolu_456", - name: "get_weather", - }, - } - yield { - type: "content_block_delta", - index: 1, - delta: { - type: "input_json_delta", - partial_json: '{"location":"Paris"}', - }, - } - yield { type: "content_block_stop", index: 1 } - }, - })) - - const stream = handler.createMessage(systemPrompt, messages, { - taskId: "test-task", - tools: [ - { - type: "function" as const, - function: { - name: "get_weather", - description: "Get weather", - parameters: { type: "object", properties: { location: { type: "string" } } }, - }, - }, - ], - }) - - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - // Verify thinking_complete was emitted for the thinking block - const thinkingCompleteChunk = chunks.find((chunk) => chunk.type === "thinking_complete") - expect(thinkingCompleteChunk).toBeDefined() - expect(thinkingCompleteChunk.signature).toBe("thinking_signature_abc") - - // Verify signature is available for tool use continuation - expect(handler.getThoughtSignature()).toBe("thinking_signature_abc") - - // Verify tool_call_partial was also emitted - const toolChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial") - expect(toolChunks.length).toBeGreaterThan(0) - }) - }) }) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 7d8a49e39bf..b231dda1db9 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -34,12 +34,6 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa private options: ApiHandlerOptions private client: Anthropic private readonly providerName = "Anthropic" - /** - * Store the last thinking block signature for interleaved thinking with tool use. - * This is captured from signature_delta events during streaming and - * must be passed back to the API when providing tool results. - */ - private lastThinkingSignature?: string constructor(options: ApiHandlerOptions) { super() @@ -54,23 +48,11 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa }) } - /** - * Get the thinking signature from the last response. - * Used by Task.addToApiConversationHistory to persist the signature - * so it can be passed back to the API for tool use continuations. - */ - public getThoughtSignature(): string | undefined { - return this.lastThinkingSignature - } - async *createMessage( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], metadata?: ApiHandlerCreateMessageMetadata, ): ApiStream { - // Reset per-request state - this.lastThinkingSignature = undefined - let stream: AnthropicStream const cacheControl: CacheControlEphemeral = { type: "ephemeral" } let { @@ -238,10 +220,6 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa let cacheWriteTokens = 0 let cacheReadTokens = 0 - // Track thinking blocks by index to capture text and signature for interleaved thinking - // This is critical for tool use continuations where thinking blocks must be passed back - const thinkingBlocks: Map = new Map() - for await (const chunk of stream) { switch (chunk.type) { case "message_start": { @@ -284,11 +262,6 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa case "content_block_start": switch (chunk.content_block.type) { case "thinking": - // Initialize thinking block tracking for interleaved thinking - thinkingBlocks.set(chunk.index, { - text: chunk.content_block.thinking || "", - }) - // We may receive multiple text blocks, in which // case just insert a line break between them. if (chunk.index > 0) { @@ -319,37 +292,13 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa } } break - case "content_block_delta": { - const delta = chunk.delta as { - type: string - thinking?: string - signature?: string - text?: string - partial_json?: string - } - switch (delta.type) { - case "thinking_delta": { - // Accumulate thinking text for the block - const block = thinkingBlocks.get(chunk.index) - if (block && delta.thinking) { - block.text += delta.thinking - } - yield { type: "reasoning", text: delta.thinking || "" } + case "content_block_delta": + switch (chunk.delta.type) { + case "thinking_delta": + yield { type: "reasoning", text: chunk.delta.thinking } break - } - case "signature_delta": { - // Capture signature for interleaved thinking with tool use - // This signature must be passed back to the API for tool use continuations - const block = thinkingBlocks.get(chunk.index) - if (block && delta.signature) { - block.signature = (block.signature || "") + delta.signature - // Store the last signature for retrieval via getThoughtSignature() - this.lastThinkingSignature = block.signature - } - break - } case "text_delta": - yield { type: "text", text: delta.text || "" } + yield { type: "text", text: chunk.delta.text } break case "input_json_delta": { // Emit tool call partial chunks as arguments stream in @@ -358,27 +307,19 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa index: chunk.index, id: undefined, name: undefined, - arguments: delta.partial_json, + arguments: chunk.delta.partial_json, } break } } break - } - case "content_block_stop": { - // Emit thinking_complete when a thinking block finishes with its signature - // This is critical for tool use continuations in interleaved thinking - const completedBlock = thinkingBlocks.get(chunk.index) - if (completedBlock?.signature) { - yield { - type: "thinking_complete", - signature: completedBlock.signature, - } - } + case "content_block_stop": + // Block complete - no action needed for now. // NativeToolCallParser handles tool call completion + // Note: Signature for multi-turn thinking would require using stream.finalMessage() + // after iteration completes, which requires restructuring the streaming approach. break - } } } diff --git a/src/api/providers/roo.ts b/src/api/providers/roo.ts index 75542c38957..2be66139a60 100644 --- a/src/api/providers/roo.ts +++ b/src/api/providers/roo.ts @@ -101,13 +101,7 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { model, max_tokens, temperature, - // Enable mergeToolResultText to merge environment_details and other text content - // after tool_results into the last tool message. This prevents reasoning/thinking - // models from dropping reasoning_content when they see a user message after tool results. - messages: [ - { role: "system", content: systemPrompt }, - ...convertToOpenAiMessages(messages, { mergeToolResultText: true }), - ], + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, ...(reasoning && { reasoning }), diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index df6614090dd..a5c3c41bc62 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/src/core/tools/ReadFileTool.ts @@ -124,6 +124,18 @@ export class ReadFileTool extends BaseTool<"read_file"> { return } + // Enforce maxConcurrentFileReads limit + const { maxConcurrentFileReads = 5 } = (await task.providerRef.deref()?.getState()) ?? {} + if (fileEntries.length > maxConcurrentFileReads) { + task.consecutiveMistakeCount++ + task.recordToolError("read_file") + const errorMsg = `Too many files requested. You attempted to read ${fileEntries.length} files, but the concurrent file reads limit is ${maxConcurrentFileReads}. Please read files in batches of ${maxConcurrentFileReads} or fewer.` + await task.say("error", errorMsg) + const errorResult = useNative ? `Error: ${errorMsg}` : `${errorMsg}` + pushToolResult(errorResult) + return + } + const supportsImages = modelInfo.supportsImages ?? false const fileResults: FileResult[] = fileEntries.map((entry) => ({ diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 49f94ca566c..825ee99b90d 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -1779,3 +1779,188 @@ describe("read_file tool with image support", () => { }) }) }) + +describe("read_file tool concurrent file reads limit", () => { + const mockedCountFileLines = vi.mocked(countFileLines) + const mockedIsBinaryFile = vi.mocked(isBinaryFileWithEncodingDetection) + const mockedPathResolve = vi.mocked(path.resolve) + + let mockCline: any + let mockProvider: any + let toolResult: ToolResponse | undefined + + beforeEach(() => { + // Clear specific mocks + mockedCountFileLines.mockClear() + mockedIsBinaryFile.mockClear() + mockedPathResolve.mockClear() + addLineNumbersMock.mockClear() + toolResultMock.mockClear() + + // Use shared mock setup function + const mocks = createMockCline() + mockCline = mocks.mockCline + mockProvider = mocks.mockProvider + + // Disable image support for these tests + setImageSupport(mockCline, false) + + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + mockedIsBinaryFile.mockResolvedValue(false) + mockedCountFileLines.mockResolvedValue(10) + + toolResult = undefined + }) + + async function executeReadFileToolWithLimit( + fileCount: number, + maxConcurrentFileReads: number, + ): Promise { + // Setup provider state with the specified limit + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxConcurrentFileReads, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) + + // Create args with the specified number of files + const files = Array.from({ length: fileCount }, (_, i) => `file${i + 1}.txt`) + const argsContent = files.join("") + + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsContent }, + partial: false, + } + + // Configure mocks for successful file reads + mockReadFileWithTokenBudget.mockResolvedValue({ + content: "test content", + tokenCount: 10, + lineCount: 1, + complete: true, + }) + + await readFileTool.handle(mockCline, toolUse, { + askApproval: mockCline.ask, + handleError: vi.fn(), + pushToolResult: (result: ToolResponse) => { + toolResult = result + }, + removeClosingTag: (_: ToolParamName, content?: string) => content ?? "", + toolProtocol: "xml", + }) + + return toolResult + } + + it("should reject when file count exceeds maxConcurrentFileReads", async () => { + // Try to read 6 files when limit is 5 + const result = await executeReadFileToolWithLimit(6, 5) + + // Verify error result + expect(result).toContain("Error: Too many files requested") + expect(result).toContain("You attempted to read 6 files") + expect(result).toContain("but the concurrent file reads limit is 5") + expect(result).toContain("Please read files in batches of 5 or fewer") + + // Verify error tracking + expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("Too many files requested")) + }) + + it("should allow reading files when count equals maxConcurrentFileReads", async () => { + // Try to read exactly 5 files when limit is 5 + const result = await executeReadFileToolWithLimit(5, 5) + + // Should not contain error + expect(result).not.toContain("Error: Too many files requested") + + // Should contain file results + expect(typeof result === "string" ? result : JSON.stringify(result)).toContain("file1.txt") + }) + + it("should allow reading files when count is below maxConcurrentFileReads", async () => { + // Try to read 3 files when limit is 5 + const result = await executeReadFileToolWithLimit(3, 5) + + // Should not contain error + expect(result).not.toContain("Error: Too many files requested") + + // Should contain file results + expect(typeof result === "string" ? result : JSON.stringify(result)).toContain("file1.txt") + }) + + it("should respect custom maxConcurrentFileReads value of 1", async () => { + // Try to read 2 files when limit is 1 + const result = await executeReadFileToolWithLimit(2, 1) + + // Verify error result with limit of 1 + expect(result).toContain("Error: Too many files requested") + expect(result).toContain("You attempted to read 2 files") + expect(result).toContain("but the concurrent file reads limit is 1") + }) + + it("should allow single file read when maxConcurrentFileReads is 1", async () => { + // Try to read 1 file when limit is 1 + const result = await executeReadFileToolWithLimit(1, 1) + + // Should not contain error + expect(result).not.toContain("Error: Too many files requested") + + // Should contain file result + expect(typeof result === "string" ? result : JSON.stringify(result)).toContain("file1.txt") + }) + + it("should respect higher maxConcurrentFileReads value", async () => { + // Try to read 15 files when limit is 10 + const result = await executeReadFileToolWithLimit(15, 10) + + // Verify error result + expect(result).toContain("Error: Too many files requested") + expect(result).toContain("You attempted to read 15 files") + expect(result).toContain("but the concurrent file reads limit is 10") + }) + + it("should use default value of 5 when maxConcurrentFileReads is not set", async () => { + // Setup provider state without maxConcurrentFileReads + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) + + // Create args with 6 files + const files = Array.from({ length: 6 }, (_, i) => `file${i + 1}.txt`) + const argsContent = files.join("") + + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsContent }, + partial: false, + } + + mockReadFileWithTokenBudget.mockResolvedValue({ + content: "test content", + tokenCount: 10, + lineCount: 1, + complete: true, + }) + + await readFileTool.handle(mockCline, toolUse, { + askApproval: mockCline.ask, + handleError: vi.fn(), + pushToolResult: (result: ToolResponse) => { + toolResult = result + }, + removeClosingTag: (_: ToolParamName, content?: string) => content ?? "", + toolProtocol: "xml", + }) + + // Should use default limit of 5 and reject 6 files + expect(toolResult).toContain("Error: Too many files requested") + expect(toolResult).toContain("but the concurrent file reads limit is 5") + }) +}) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 808270f9c3a..2d1ea9afecc 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -170,7 +170,7 @@ export class ClineProvider public isViewLaunched = false public settingsImportedAt?: number - public readonly latestAnnouncementId = "dec-2025-v3.37.0-minimax-m21-glm47-custom-tools" // v3.37.0 MiniMax M2.1, GLM-4.7, & Experimental Custom Tools + public readonly latestAnnouncementId = "dec-2025-v3.38.0-skills-native-tool-calling" // v3.38.0 Skills & Native Tool Calling Required public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx index f69e18065a6..74883286148 100644 --- a/webview-ui/src/components/chat/Announcement.tsx +++ b/webview-ui/src/components/chat/Announcement.tsx @@ -44,9 +44,24 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {

{t("chat:announcement.release.heading")}

    -
  • {t("chat:announcement.release.minimaxM21")}
  • -
  • {t("chat:announcement.release.glm47")}
  • -
  • {t("chat:announcement.release.customTools")}
  • +
  • + , + }} + /> +
  • +
  • + + ), + }} + /> +
@@ -124,4 +139,15 @@ const CareersLink = ({ children }: { children?: ReactNode }) => ( ) +const BlogLink = ({ href, children }: { href: string; children?: ReactNode }) => ( + { + e.preventDefault() + vscode.postMessage({ type: "openExternal", url: href }) + }}> + {children} + +) + export default memo(Announcement) diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index a33f692a5eb..09251543bce 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -347,9 +347,8 @@ }, "release": { "heading": "What's New:", - "minimaxM21": "Fast and affordable MiniMax M2.1 model now available in Roo Code Cloud, MiniMax, and more", - "glm47": "Z.AI GLM-4.7 model with thinking mode support added to Roo Code Cloud, Z.AI, and more", - "customTools": "Experimental custom tool support for defining your own tools in TypeScript" + "skills": "Roo now supports Agent Skills - reusable packages of prompts, tools, and resources to extend Roo's capabilities.", + "nativeToolCalling": "Native tool calling is now required for all new tasks. Read more about the motivation and what to do if you're encountering issues." }, "cloudAgents": { "heading": "New in the Cloud:", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 546a704cb4f..678c5a4e2cc 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -341,9 +341,8 @@ }, "release": { "heading": "新增功能:", - "minimaxM21": "快速且经济高效的 MiniMax M2.1 模型现在在 Roo Code Cloud、MiniMax 等中可用", - "glm47": "支持思考模式的 Z.AI GLM-4.7 模型已添加到 Roo Code Cloud、Z.AI 等", - "customTools": "用于在 TypeScript 中定义自己的工具的实验性自定义工具支持" + "skills": "Roo 现支持 Agent Skills - 可复用的提示词、工具和资源包,扩展 Roo 的功能。", + "nativeToolCalling": "所有新任务现在需要使用原生工具调用。了解更多相关信息和解决方案。" }, "cloudAgents": { "heading": "云端新功能:", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index a0dd91b0fcb..6382f320883 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -351,9 +351,8 @@ }, "release": { "heading": "新增功能:", - "minimaxM21": "迅速且經濟高效的 MiniMax M2.1 模型現在在 Roo Code Cloud、MiniMax 等中提供", - "glm47": "支援思考模式的 Z.AI GLM-4.7 模型已新增到 Roo Code Cloud、Z.AI 等", - "customTools": "用於在 TypeScript 中定義自己工具的實驗性自訂工具支援" + "skills": "Roo 現已支援 Agent Skills - 可重複使用的提示詞、工具和資源套件,用於擴展 Roo 的功能。", + "nativeToolCalling": "所有新工作現在都需要原生工具呼叫。深入了解相關動機以及遇到問題時的解決方案。" }, "cloudAgents": { "heading": "雲端的新功能:",