mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Revert "fix: capture extended thinking signatures for tool use continuations" (#10360)
This commit is contained in:
parent
c61dd7ad31
commit
25f76ce25e
2 changed files with 10 additions and 367 deletions
|
|
@ -746,302 +746,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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<Anthropic.Messages.RawMessageStreamEvent>
|
||||
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<number, { text: string; signature?: string }> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue