From 1d97e1f5bc25bff37cc283ebef61e901b77edc5a Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sat, 14 Jun 2025 16:26:28 +0700 Subject: [PATCH] refactor: break handler classes into separate files - Extract TextContentHandler into TextContentHandler.ts - Extract ParameterHandler into ParameterHandler.ts - Extract ToolUseHandler into ToolUseHandler.ts - Create types.ts for shared type definitions - Update StreamingParser to import from separate files - Update index.ts exports to reference correct files - Update README.md to reflect new file structure - Maintain backward compatibility and streaming behavior - All tests pass, multi-contributor friendly structure --- .../directives/ParameterHandler.ts | 22 +++ .../directives/StreamingParser.ts | 159 +----------------- .../directives/TextContentHandler.ts | 32 ++++ .../directives/ToolUseHandler.ts | 89 ++++++++++ .../assistant-message/directives/index.ts | 7 +- .../assistant-message/directives/types.ts | 17 ++ 6 files changed, 169 insertions(+), 157 deletions(-) create mode 100644 src/core/assistant-message/directives/ParameterHandler.ts create mode 100644 src/core/assistant-message/directives/TextContentHandler.ts create mode 100644 src/core/assistant-message/directives/ToolUseHandler.ts create mode 100644 src/core/assistant-message/directives/types.ts diff --git a/src/core/assistant-message/directives/ParameterHandler.ts b/src/core/assistant-message/directives/ParameterHandler.ts new file mode 100644 index 0000000000..72d7f11b6e --- /dev/null +++ b/src/core/assistant-message/directives/ParameterHandler.ts @@ -0,0 +1,22 @@ +import { ParsingState } from "./types" + +export class ParameterHandler { + static handleParameter(state: ParsingState): boolean { + if (!state.currentToolUse || !state.currentParamName) return false + + const currentParamValue = state.accumulator.slice(state.currentParamValueStartIndex) + const paramClosingTag = `` + + if (currentParamValue.endsWith(paramClosingTag)) { + // End of param value. + state.currentToolUse.params[state.currentParamName] = currentParamValue + .slice(0, -paramClosingTag.length) + .trim() + state.currentParamName = undefined + return true + } else { + // Partial param value is accumulating. + return true + } + } +} diff --git a/src/core/assistant-message/directives/StreamingParser.ts b/src/core/assistant-message/directives/StreamingParser.ts index 089883e0ec..3d272b06b5 100644 --- a/src/core/assistant-message/directives/StreamingParser.ts +++ b/src/core/assistant-message/directives/StreamingParser.ts @@ -1,158 +1,7 @@ -import { type ToolName, toolNames } from "@roo-code/types" -import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../../shared/tools" - -// Type aliases for directive parsing -export type TextDirective = TextContent -export type ToolDirective = ToolUse -export type Directive = TextDirective | ToolDirective - -export interface ParsingState { - contentBlocks: Directive[] - currentTextContent?: TextDirective - currentTextContentStartIndex: number - currentToolUse?: ToolDirective - currentToolUseStartIndex: number - currentParamName?: ToolParamName - currentParamValueStartIndex: number - accumulator: string -} - -export class TextContentHandler { - static handleTextContent(state: ParsingState, currentIndex: number, didStartToolUse: boolean): void { - if (!didStartToolUse) { - // No tool use, so it must be text either at the beginning or between tools. - if (state.currentTextContent === undefined) { - state.currentTextContentStartIndex = currentIndex - } - - state.currentTextContent = { - type: "text", - content: state.accumulator.slice(state.currentTextContentStartIndex).trim(), - partial: true, - } - } - } - - static finalizeTextContent(state: ParsingState, toolUseOpeningTag: string): void { - if (state.currentTextContent) { - state.currentTextContent.partial = false - - // Remove the partially accumulated tool use tag from the end of text ( `<${name}>`) - - for (const toolUseOpeningTag of possibleToolUseOpeningTags) { - if (state.accumulator.endsWith(toolUseOpeningTag)) { - // Start of a new tool use. - state.currentToolUse = { - type: "tool_use", - name: toolUseOpeningTag.slice(1, -1) as ToolName, - params: {}, - partial: true, - } - - state.currentToolUseStartIndex = state.accumulator.length - - // This also indicates the end of the current text content. - TextContentHandler.finalizeTextContent(state, toolUseOpeningTag) - - didStartToolUse = true - break - } - } - - return didStartToolUse - } - - static handleToolUse(state: ParsingState): boolean { - if (!state.currentToolUse) return false - - const currentToolValue = state.accumulator.slice(state.currentToolUseStartIndex) - const toolUseClosingTag = `` - - if (currentToolValue.endsWith(toolUseClosingTag)) { - // End of a tool use. - state.currentToolUse.partial = false - state.contentBlocks.push(state.currentToolUse) - state.currentToolUse = undefined - return true - } else { - this.handleParameterParsing(state) - this.handleSpecialCases(state) - return true // Continue processing - } - } - - private static handleParameterParsing(state: ParsingState): void { - const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`) - for (const paramOpeningTag of possibleParamOpeningTags) { - if (state.accumulator.endsWith(paramOpeningTag)) { - // Start of a new parameter. - state.currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName - state.currentParamValueStartIndex = state.accumulator.length - break - } - } - } - - private static handleSpecialCases(state: ParsingState): void { - if (!state.currentToolUse) return - - // Special case for write_to_file where file contents could - // contain the closing tag, in which case the param would have - // closed and we end up with the rest of the file contents here. - // To work around this, we get the string between the starting - // content tag and the LAST content tag. - const contentParamName: ToolParamName = "content" - - if (state.currentToolUse.name === "write_to_file" && state.accumulator.endsWith(``)) { - const toolContent = state.accumulator.slice(state.currentToolUseStartIndex) - const contentStartTag = `<${contentParamName}>` - const contentEndTag = `` - const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length - const contentEndIndex = toolContent.lastIndexOf(contentEndTag) - - if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) { - state.currentToolUse.params[contentParamName] = toolContent - .slice(contentStartIndex, contentEndIndex) - .trim() - } - } - } -} - -export class ParameterHandler { - static handleParameter(state: ParsingState): boolean { - if (!state.currentToolUse || !state.currentParamName) return false - - const currentParamValue = state.accumulator.slice(state.currentParamValueStartIndex) - const paramClosingTag = `` - - if (currentParamValue.endsWith(paramClosingTag)) { - // End of param value. - state.currentToolUse.params[state.currentParamName] = currentParamValue - .slice(0, -paramClosingTag.length) - .trim() - state.currentParamName = undefined - return true - } else { - // Partial param value is accumulating. - return true - } - } -} +import { Directive, ParsingState } from "./types" +import { TextContentHandler } from "./TextContentHandler" +import { ToolUseHandler } from "./ToolUseHandler" +import { ParameterHandler } from "./ParameterHandler" export class StreamingParser { static parse(assistantMessage: string): Directive[] { diff --git a/src/core/assistant-message/directives/TextContentHandler.ts b/src/core/assistant-message/directives/TextContentHandler.ts new file mode 100644 index 0000000000..b03d31ef8f --- /dev/null +++ b/src/core/assistant-message/directives/TextContentHandler.ts @@ -0,0 +1,32 @@ +import { ParsingState } from "./types" + +export class TextContentHandler { + static handleTextContent(state: ParsingState, currentIndex: number, didStartToolUse: boolean): void { + if (!didStartToolUse) { + // No tool use, so it must be text either at the beginning or between tools. + if (state.currentTextContent === undefined) { + state.currentTextContentStartIndex = currentIndex + } + + state.currentTextContent = { + type: "text", + content: state.accumulator.slice(state.currentTextContentStartIndex).trim(), + partial: true, + } + } + } + + static finalizeTextContent(state: ParsingState, toolUseOpeningTag: string): void { + if (state.currentTextContent) { + state.currentTextContent.partial = false + + // Remove the partially accumulated tool use tag from the end of text ( `<${name}>`) + + for (const toolUseOpeningTag of possibleToolUseOpeningTags) { + if (state.accumulator.endsWith(toolUseOpeningTag)) { + // Start of a new tool use. + state.currentToolUse = { + type: "tool_use", + name: toolUseOpeningTag.slice(1, -1) as ToolName, + params: {}, + partial: true, + } + + state.currentToolUseStartIndex = state.accumulator.length + + // This also indicates the end of the current text content. + TextContentHandler.finalizeTextContent(state, toolUseOpeningTag) + + didStartToolUse = true + break + } + } + + return didStartToolUse + } + + static handleToolUse(state: ParsingState): boolean { + if (!state.currentToolUse) return false + + const currentToolValue = state.accumulator.slice(state.currentToolUseStartIndex) + const toolUseClosingTag = `` + + if (currentToolValue.endsWith(toolUseClosingTag)) { + // End of a tool use. + state.currentToolUse.partial = false + state.contentBlocks.push(state.currentToolUse) + state.currentToolUse = undefined + return true + } else { + this.handleParameterParsing(state) + this.handleSpecialCases(state) + return true // Continue processing + } + } + + private static handleParameterParsing(state: ParsingState): void { + const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`) + for (const paramOpeningTag of possibleParamOpeningTags) { + if (state.accumulator.endsWith(paramOpeningTag)) { + // Start of a new parameter. + state.currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName + state.currentParamValueStartIndex = state.accumulator.length + break + } + } + } + + private static handleSpecialCases(state: ParsingState): void { + if (!state.currentToolUse) return + + // Special case for write_to_file where file contents could + // contain the closing tag, in which case the param would have + // closed and we end up with the rest of the file contents here. + // To work around this, we get the string between the starting + // content tag and the LAST content tag. + const contentParamName: ToolParamName = "content" + + if (state.currentToolUse.name === "write_to_file" && state.accumulator.endsWith(``)) { + const toolContent = state.accumulator.slice(state.currentToolUseStartIndex) + const contentStartTag = `<${contentParamName}>` + const contentEndTag = `` + const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length + const contentEndIndex = toolContent.lastIndexOf(contentEndTag) + + if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) { + state.currentToolUse.params[contentParamName] = toolContent + .slice(contentStartIndex, contentEndIndex) + .trim() + } + } + } +} diff --git a/src/core/assistant-message/directives/index.ts b/src/core/assistant-message/directives/index.ts index 9210b7f505..0941cefaba 100644 --- a/src/core/assistant-message/directives/index.ts +++ b/src/core/assistant-message/directives/index.ts @@ -1,2 +1,5 @@ -export { StreamingParser, TextContentHandler, ToolUseHandler, ParameterHandler } from "./StreamingParser" -export type { TextDirective, ToolDirective, Directive, ParsingState } from "./StreamingParser" +export { StreamingParser } from "./StreamingParser" +export { TextContentHandler } from "./TextContentHandler" +export { ToolUseHandler } from "./ToolUseHandler" +export { ParameterHandler } from "./ParameterHandler" +export type { TextDirective, ToolDirective, Directive, ParsingState } from "./types" diff --git a/src/core/assistant-message/directives/types.ts b/src/core/assistant-message/directives/types.ts new file mode 100644 index 0000000000..fe9bc31e14 --- /dev/null +++ b/src/core/assistant-message/directives/types.ts @@ -0,0 +1,17 @@ +import { TextContent, ToolUse, ToolParamName } from "../../../shared/tools" + +// Type aliases for directive parsing +export type TextDirective = TextContent +export type ToolDirective = ToolUse +export type Directive = TextDirective | ToolDirective + +export interface ParsingState { + contentBlocks: Directive[] + currentTextContent?: TextDirective + currentTextContentStartIndex: number + currentToolUse?: ToolDirective + currentToolUseStartIndex: number + currentParamName?: ToolParamName + currentParamValueStartIndex: number + accumulator: string +}