From 5544d81bc5c8e0936a2b6c3c38bba41cb36cce95 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:32:36 -0400 Subject: [PATCH] Save latest assistant response before tool use; fix task resumption conversation reconstruction --- src/ClaudeDev.ts | 81 ++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/src/ClaudeDev.ts b/src/ClaudeDev.ts index 50543d0..90fc385 100644 --- a/src/ClaudeDev.ts +++ b/src/ClaudeDev.ts @@ -507,16 +507,12 @@ export class ClaudeDev { } const { response, text, images } = await this.ask(askType) // calls poststatetowebview - - let newUserContent: UserContent = [] + let responseText: string | undefined + let responseImages: string[] | undefined if (response === "messageResponse") { await this.say("user_feedback", text, images) - if (images && images.length > 0) { - newUserContent.push(...this.formatImagesIntoBlocks(images)) - } - if (text) { - newUserContent.push({ type: "text", text }) - } + responseText = text + responseImages = images } // need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with claude messages @@ -529,8 +525,8 @@ export class ClaudeDev { const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await this.getSavedApiConversationHistory() - let modifiedOldUserContent: UserContent - let modifiedApiConversationHistory: Anthropic.Messages.MessageParam[] + let modifiedOldUserContent: UserContent // either the last message if its user message, or the user message before the last (assistant) message + let modifiedApiConversationHistory: Anthropic.Messages.MessageParam[] // need to remove the last user message to replace with new modified user message if (existingApiConversationHistory.length > 0) { const lastMessage = existingApiConversationHistory[existingApiConversationHistory.length - 1] @@ -556,7 +552,7 @@ export class ClaudeDev { modifiedOldUserContent = [] } } else if (lastMessage.role === "user") { - const previousAssistantMessage = + const previousAssistantMessage: Anthropic.Messages.MessageParam | undefined = existingApiConversationHistory[existingApiConversationHistory.length - 2] const existingUserContent: UserContent = Array.isArray(lastMessage.content) @@ -586,7 +582,7 @@ export class ClaudeDev { content: "Task was interrupted before this tool call could be completed.", })) - modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1) + modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1) // removes the last user message modifiedOldUserContent = [...existingUserContent, ...missingToolResponses] } else { modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1) @@ -603,10 +599,8 @@ export class ClaudeDev { throw new Error("Unexpected: No existing API conversation history") } - // now we have newUserContent which is user's current message, and the modifiedOldUserContent which is the old message with tool responses filled in - // we need to combine them while ensuring there is only one text block - const modifiedOldUserContentText = modifiedOldUserContent.find((block) => block.type === "text")?.text - const newUserContentText = newUserContent.find((block) => block.type === "text")?.text + let newUserContent: UserContent = [...modifiedOldUserContent] + const agoText = (() => { const timestamp = lastClaudeMessage?.ts ?? Date.now() const now = Date.now() @@ -627,22 +621,21 @@ export class ClaudeDev { return "just now" })() - const combinedText = - `Task resumption: This autonomous coding task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now ${cwd}. If the task has not been completed, retry the last step before interruption and proceed with completing the task.` + - (modifiedOldUserContentText - ? `\n\nLast recorded user input before interruption:\n\n${modifiedOldUserContentText}\n` - : "") + - (newUserContentText - ? `\n\nNew instructions for task continuation:\n\n${newUserContentText}\n` - : "") + newUserContent.push({ + type: "text", + text: + `Task resumption: This autonomous coding task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.` + + (responseText + ? `\n\nNew instructions for task continuation:\n\n${responseText}\n` + : ""), + }) - const newUserContentImages = newUserContent.filter((block) => block.type === "image") - const combinedModifiedOldUserContentWithNewUserContent: UserContent = ( - modifiedOldUserContent.filter((block) => block.type !== "text") as UserContent - ).concat([{ type: "text", text: combinedText }, ...newUserContentImages]) + if (responseImages && responseImages.length > 0) { + newUserContent.push(...this.formatImagesIntoBlocks(responseImages)) + } await this.overwriteApiConversationHistory(modifiedApiConversationHistory) - await this.initiateTaskLoop(combinedModifiedOldUserContentWithNewUserContent) + await this.initiateTaskLoop(newUserContent) } private async initiateTaskLoop(userContent: UserContent): Promise { @@ -1649,17 +1642,34 @@ ${this.customInstructions.trim()} // A response always returns text content blocks (it's just that before we were iterating over the completion_attempt response before we could append text response, resulting in bug) for (const contentBlock of response.content) { + // type can only be text or tool_use if (contentBlock.type === "text") { assistantResponses.push(contentBlock) await this.say("text", contentBlock.text) + } else if (contentBlock.type === "tool_use") { + assistantResponses.push(contentBlock) } } + // need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response + if (assistantResponses.length > 0) { + await this.addToApiConversationHistory({ role: "assistant", content: assistantResponses }) + } else { + // this should never happen! it there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error + await this.say( + "error", + "Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output." + ) + await this.addToApiConversationHistory({ + role: "assistant", + content: [{ type: "text", text: "Failure: I did not provide a response." }], + }) + } + let toolResults: Anthropic.ToolResultBlockParam[] = [] let attemptCompletionBlock: Anthropic.Messages.ToolUseBlock | undefined for (const contentBlock of response.content) { if (contentBlock.type === "tool_use") { - assistantResponses.push(contentBlock) const toolName = contentBlock.name as ToolName const toolInput = contentBlock.input const toolUseId = contentBlock.id @@ -1677,17 +1687,6 @@ ${this.customInstructions.trim()} } } - if (assistantResponses.length > 0) { - await this.addToApiConversationHistory({ role: "assistant", content: assistantResponses }) - } else { - // this should never happen! it there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error - await this.say("error", "Unexpected Error: No assistant messages were found in the API response") - await this.addToApiConversationHistory({ - role: "assistant", - content: [{ type: "text", text: "Failure: I did not have a response to provide." }], - }) - } - let didEndLoop = false // attempt_completion is always done last, since there might have been other tools that needed to be called first before the job is finished