Feat/add skill tool (#11051)

* feat: add SkillTool component and integrate into agent tools

- Introduced SkillTool component for rendering skill-related functionality.
- Updated MessageAgentTools to include SkillTool in the tool renderers.
- Enhanced MessageTool to recognize 'Skill' as a valid agent tool type.
- Modified handleUserMessage to conditionally handle text blocks based on skill inclusion.
- Added SkillToolInput and SkillToolOutput types for better type safety.

* feat: implement command tag filtering in message handling

- Added filterCommandTags function to remove command-* tags from text content, ensuring internal command messages do not appear in the user-facing UI.
- Updated handleUserMessage to utilize the new filtering logic, enhancing the handling of text blocks and improving user experience by preventing unwanted command messages from being displayed.

* refactor: rename tool prefix constants for clarity

- Updated variable names for tool prefixes in MessageTool and SkillTool components to enhance code readability.
- Changed `prefix` to `builtinToolsPrefix` and `agentPrefix` to `agentMcpToolsPrefix` for better understanding of their purpose.
This commit is contained in:
MyPrototypeWhat 2025-10-31 16:31:50 +08:00 committed by GitHub
parent 68e0d8b0f1
commit b6dcf2f5fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 80 additions and 45 deletions

View File

@ -73,6 +73,15 @@ const emptyUsage: LanguageModelUsage = {
*/ */
const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}` const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}`
/**
* Filters out command-* tags from text content to prevent internal command
* messages from appearing in the user-facing UI.
* Removes tags like <command-message>...</command-message> and <command-name>...</command-name>
*/
const filterCommandTags = (text: string): string => {
return text.replace(/<command-[^>]+>.*?<\/command-[^>]+>/gs, '').trim()
}
/** /**
* Extracts provider metadata from the raw Claude message so we can surface it * Extracts provider metadata from the raw Claude message so we can surface it
* on every emitted stream part for observability and debugging purposes. * on every emitted stream part for observability and debugging purposes.
@ -270,12 +279,17 @@ function handleUserMessage(
const chunks: AgentStreamPart[] = [] const chunks: AgentStreamPart[] = []
const providerMetadata = sdkMessageToProviderMetadata(message) const providerMetadata = sdkMessageToProviderMetadata(message)
const content = message.message.content const content = message.message.content
const isSynthetic = message.isSynthetic ?? false
if (typeof content === 'string') { if (typeof content === 'string') {
if (!content) { if (!content) {
return chunks return chunks
} }
const filteredContent = filterCommandTags(content)
if (!filteredContent) {
return chunks
}
const id = message.uuid?.toString() || generateMessageId() const id = message.uuid?.toString() || generateMessageId()
chunks.push({ chunks.push({
type: 'text-start', type: 'text-start',
@ -285,7 +299,7 @@ function handleUserMessage(
chunks.push({ chunks.push({
type: 'text-delta', type: 'text-delta',
id, id,
text: content, text: filteredContent,
providerMetadata providerMetadata
}) })
chunks.push({ chunks.push({
@ -323,24 +337,30 @@ function handleUserMessage(
providerExecuted: true providerExecuted: true
}) })
} }
} else if (block.type === 'text') { } else if (block.type === 'text' && !isSynthetic) {
const id = message.uuid?.toString() || generateMessageId() const rawText = (block as { text: string }).text
chunks.push({ const filteredText = filterCommandTags(rawText)
type: 'text-start',
id, // Only push text chunks if there's content after filtering
providerMetadata if (filteredText) {
}) const id = message.uuid?.toString() || generateMessageId()
chunks.push({ chunks.push({
type: 'text-delta', type: 'text-start',
id, id,
text: (block as { text: string }).text, providerMetadata
providerMetadata })
}) chunks.push({
chunks.push({ type: 'text-delta',
type: 'text-end', id,
id, text: filteredText,
providerMetadata providerMetadata
}) })
chunks.push({
type: 'text-end',
id,
providerMetadata
})
}
} else { } else {
logger.warn('Unhandled user content block', { type: (block as any).type }) logger.warn('Unhandled user content block', { type: (block as any).type })
} }

View File

@ -0,0 +1,16 @@
import { AccordionItem } from '@heroui/react'
import { PencilRuler } from 'lucide-react'
import { ToolTitle } from './GenericTools'
import type { SkillToolInput, SkillToolOutput } from './types'
export function SkillTool({ input, output }: { input: SkillToolInput; output?: SkillToolOutput }) {
return (
<AccordionItem
key="tool"
aria-label="Skill Tool"
title={<ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />}>
{output}
</AccordionItem>
)
}

View File

@ -17,6 +17,7 @@ import { MultiEditTool } from './MultiEditTool'
import { NotebookEditTool } from './NotebookEditTool' import { NotebookEditTool } from './NotebookEditTool'
import { ReadTool } from './ReadTool' import { ReadTool } from './ReadTool'
import { SearchTool } from './SearchTool' import { SearchTool } from './SearchTool'
import { SkillTool } from './SkillTool'
import { TaskTool } from './TaskTool' import { TaskTool } from './TaskTool'
import { TodoWriteTool } from './TodoWriteTool' import { TodoWriteTool } from './TodoWriteTool'
import { AgentToolsType, ToolInput, ToolOutput } from './types' import { AgentToolsType, ToolInput, ToolOutput } from './types'
@ -24,6 +25,7 @@ import { UnknownToolRenderer } from './UnknownToolRenderer'
import { WebFetchTool } from './WebFetchTool' import { WebFetchTool } from './WebFetchTool'
import { WebSearchTool } from './WebSearchTool' import { WebSearchTool } from './WebSearchTool'
import { WriteTool } from './WriteTool' import { WriteTool } from './WriteTool'
const logger = loggerService.withContext('MessageAgentTools') const logger = loggerService.withContext('MessageAgentTools')
// 创建工具渲染器映射,这样就实现了完全的类型安全 // 创建工具渲染器映射,这样就实现了完全的类型安全
@ -42,7 +44,8 @@ export const toolRenderers = {
[AgentToolsType.MultiEdit]: MultiEditTool, [AgentToolsType.MultiEdit]: MultiEditTool,
[AgentToolsType.BashOutput]: BashOutputTool, [AgentToolsType.BashOutput]: BashOutputTool,
[AgentToolsType.NotebookEdit]: NotebookEditTool, [AgentToolsType.NotebookEdit]: NotebookEditTool,
[AgentToolsType.ExitPlanMode]: ExitPlanModeTool [AgentToolsType.ExitPlanMode]: ExitPlanModeTool,
[AgentToolsType.Skill]: SkillTool
} as const } as const
// 类型守卫函数 // 类型守卫函数

View File

@ -1,4 +1,5 @@
export enum AgentToolsType { export enum AgentToolsType {
Skill = 'Skill',
Read = 'Read', Read = 'Read',
Task = 'Task', Task = 'Task',
Bash = 'Bash', Bash = 'Bash',
@ -22,6 +23,15 @@ export type TextOutput = {
} }
// Read 工具的类型定义 // Read 工具的类型定义
export interface SkillToolInput {
/**
* The skill to use
*/
command: string
}
export type SkillToolOutput = string
export interface ReadToolInput { export interface ReadToolInput {
/** /**
* The absolute path to the file to read * The absolute path to the file to read

View File

@ -2,6 +2,7 @@ import { NormalToolResponse } from '@renderer/types'
import type { ToolMessageBlock } from '@renderer/types/newMessage' import type { ToolMessageBlock } from '@renderer/types/newMessage'
import { MessageAgentTools } from './MessageAgentTools' import { MessageAgentTools } from './MessageAgentTools'
import { AgentToolsType } from './MessageAgentTools/types'
import { MessageKnowledgeSearchToolTitle } from './MessageKnowledgeSearch' import { MessageKnowledgeSearchToolTitle } from './MessageKnowledgeSearch'
import { MessageMemorySearchToolTitle } from './MessageMemorySearch' import { MessageMemorySearchToolTitle } from './MessageMemorySearch'
import { MessageWebSearchToolTitle } from './MessageWebSearch' import { MessageWebSearchToolTitle } from './MessageWebSearch'
@ -9,27 +10,12 @@ import { MessageWebSearchToolTitle } from './MessageWebSearch'
interface Props { interface Props {
block: ToolMessageBlock block: ToolMessageBlock
} }
const prefix = 'builtin_' const builtinToolsPrefix = 'builtin_'
const agentPrefix = 'mcp__' const agentMcpToolsPrefix = 'mcp__'
const agentTools = [ const agentTools = Object.values(AgentToolsType)
'Read',
'Task', const isAgentTool = (toolName: AgentToolsType) => {
'Bash', if (agentTools.includes(toolName) || toolName.startsWith(agentMcpToolsPrefix)) {
'Search',
'Glob',
'TodoWrite',
'WebSearch',
'Grep',
'Write',
'WebFetch',
'Edit',
'MultiEdit',
'BashOutput',
'NotebookEdit',
'ExitPlanMode'
]
const isAgentTool = (toolName: string) => {
if (agentTools.includes(toolName) || toolName.startsWith(agentPrefix)) {
return true return true
} }
return false return false
@ -38,8 +24,8 @@ const isAgentTool = (toolName: string) => {
const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => { const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => {
let toolName = toolResponse.tool.name let toolName = toolResponse.tool.name
const toolType = toolResponse.tool.type const toolType = toolResponse.tool.type
if (toolName.startsWith(prefix)) { if (toolName.startsWith(builtinToolsPrefix)) {
toolName = toolName.slice(prefix.length) toolName = toolName.slice(builtinToolsPrefix.length)
switch (toolName) { switch (toolName) {
case 'web_search': case 'web_search':
case 'web_search_preview': case 'web_search_preview':
@ -51,7 +37,7 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null =>
default: default:
return null return null
} }
} else if (isAgentTool(toolName)) { } else if (isAgentTool(toolName as AgentToolsType)) {
return <MessageAgentTools toolResponse={toolResponse} /> return <MessageAgentTools toolResponse={toolResponse} />
} }
return null return null