Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 77050ab

Browse files
committed
Add call/response to chat
1 parent 33bb838 commit 77050ab

26 files changed

Lines changed: 503 additions & 8 deletions

File tree

packages/types/src/message.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ export const clineSays = [
170170
"user_edit_todos",
171171
"too_many_tools_warning",
172172
"tool",
173+
"use_advisor_tool",
174+
"advisor_tool_result",
173175
] as const
174176

175177
export const clineSaySchema = z.enum(clineSays)

packages/types/src/vscode-extension-host.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,12 @@ export interface ClineSayTool {
842842
skill?: string
843843
}
844844

845+
export interface ClineAskUseAdvisorTool {
846+
toolUseId: string
847+
name: string
848+
input: string // JSON-serialized input
849+
}
850+
845851
export interface ClineAskUseMcpServer {
846852
serverName: string
847853
type: "use_mcp_tool" | "access_mcp_resource"

src/api/providers/__tests__/anthropic.spec.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,5 +922,151 @@ describe("AnthropicHandler", () => {
922922
arguments: '"London"}',
923923
})
924924
})
925+
926+
it("should emit advisor_tool_use chunk with id from server_tool_use block", async () => {
927+
mockCreate.mockImplementationOnce(async () => ({
928+
async *[Symbol.asyncIterator]() {
929+
yield {
930+
type: "message_start",
931+
message: { usage: { input_tokens: 100, output_tokens: 50 } },
932+
}
933+
yield {
934+
type: "content_block_start",
935+
index: 0,
936+
content_block: {
937+
type: "server_tool_use",
938+
id: "srvtoolu_abc123",
939+
name: "advisor",
940+
input: {},
941+
},
942+
}
943+
},
944+
}))
945+
946+
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
947+
taskId: "test-task",
948+
})
949+
950+
const chunks: any[] = []
951+
for await (const chunk of stream) {
952+
chunks.push(chunk)
953+
}
954+
955+
const advisorUseChunk = chunks.find((c) => c.type === "advisor_tool_use")
956+
expect(advisorUseChunk).toBeDefined()
957+
expect(advisorUseChunk.id).toBe("srvtoolu_abc123")
958+
expect(advisorUseChunk.name).toBe("advisor")
959+
})
960+
961+
it("should emit advisor_tool_result chunk with tool_use_id and text content", async () => {
962+
mockCreate.mockImplementationOnce(async () => ({
963+
async *[Symbol.asyncIterator]() {
964+
yield {
965+
type: "message_start",
966+
message: { usage: { input_tokens: 100, output_tokens: 50 } },
967+
}
968+
yield {
969+
type: "content_block_start",
970+
index: 1,
971+
content_block: {
972+
type: "advisor_tool_result",
973+
tool_use_id: "srvtoolu_abc123",
974+
content: { type: "advisor_result", text: "Use channel-based coordination." },
975+
},
976+
}
977+
},
978+
}))
979+
980+
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
981+
taskId: "test-task",
982+
})
983+
984+
const chunks: any[] = []
985+
for await (const chunk of stream) {
986+
chunks.push(chunk)
987+
}
988+
989+
const resultChunk = chunks.find((c) => c.type === "advisor_tool_result")
990+
expect(resultChunk).toBeDefined()
991+
expect(resultChunk.tool_use_id).toBe("srvtoolu_abc123")
992+
expect(resultChunk.content).toBe("Use channel-based coordination.")
993+
// rawContent must carry the verbatim object for round-tripping to the API
994+
expect(resultChunk.rawContent).toEqual({ type: "advisor_result", text: "Use channel-based coordination." })
995+
})
996+
997+
it("should extract text from advisor_result object content shape", async () => {
998+
mockCreate.mockImplementationOnce(async () => ({
999+
async *[Symbol.asyncIterator]() {
1000+
yield {
1001+
type: "message_start",
1002+
message: { usage: { input_tokens: 100, output_tokens: 50 } },
1003+
}
1004+
yield {
1005+
type: "content_block_start",
1006+
index: 0,
1007+
content_block: {
1008+
type: "advisor_tool_result",
1009+
tool_use_id: "srvtoolu_xyz",
1010+
content: { type: "advisor_result", text: "Plan: do X then Y." },
1011+
},
1012+
}
1013+
},
1014+
}))
1015+
1016+
const stream = handler.createMessage("system", [{ role: "user", content: "Plan?" }], {
1017+
taskId: "test-task",
1018+
})
1019+
1020+
const chunks: any[] = []
1021+
for await (const chunk of stream) {
1022+
chunks.push(chunk)
1023+
}
1024+
1025+
const resultChunk = chunks.find((c) => c.type === "advisor_tool_result")
1026+
expect(resultChunk).toBeDefined()
1027+
expect(resultChunk.content).toBe("Plan: do X then Y.")
1028+
// rawContent must be the verbatim object, not just the extracted text
1029+
expect(resultChunk.rawContent).toEqual({ type: "advisor_result", text: "Plan: do X then Y." })
1030+
})
1031+
1032+
it("should emit empty string for encrypted advisor_redacted_result content", async () => {
1033+
mockCreate.mockImplementationOnce(async () => ({
1034+
async *[Symbol.asyncIterator]() {
1035+
yield {
1036+
type: "message_start",
1037+
message: { usage: { input_tokens: 100, output_tokens: 50 } },
1038+
}
1039+
yield {
1040+
type: "content_block_start",
1041+
index: 0,
1042+
content_block: {
1043+
type: "advisor_tool_result",
1044+
tool_use_id: "srvtoolu_redacted",
1045+
content: { type: "advisor_redacted_result", encrypted_content: "opaque-blob" },
1046+
},
1047+
}
1048+
},
1049+
}))
1050+
1051+
const stream = handler.createMessage("system", [{ role: "user", content: "Plan?" }], {
1052+
taskId: "test-task",
1053+
})
1054+
1055+
const chunks: any[] = []
1056+
for await (const chunk of stream) {
1057+
chunks.push(chunk)
1058+
}
1059+
1060+
const resultChunk = chunks.find((c) => c.type === "advisor_tool_result")
1061+
expect(resultChunk).toBeDefined()
1062+
expect(resultChunk.tool_use_id).toBe("srvtoolu_redacted")
1063+
// Encrypted content has no text field — content should be empty string
1064+
expect(resultChunk.content).toBe("")
1065+
// rawContent must carry the verbatim encrypted object for round-tripping
1066+
expect(resultChunk.rawContent).toEqual({
1067+
type: "advisor_redacted_result",
1068+
encrypted_content: "opaque-blob",
1069+
})
1070+
})
9251071
})
9261072
})

src/api/providers/anthropic.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,16 +272,17 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
272272
case "message_stop":
273273
// No usage data, just an indicator that the message is done.
274274
break
275-
case "content_block_start":
276-
switch (chunk.content_block.type) {
275+
case "content_block_start": {
276+
const contentBlock = chunk.content_block as any
277+
switch (contentBlock.type) {
277278
case "thinking":
278279
// We may receive multiple text blocks, in which
279280
// case just insert a line break between them.
280281
if (chunk.index > 0) {
281282
yield { type: "reasoning", text: "\n" }
282283
}
283284

284-
yield { type: "reasoning", text: chunk.content_block.thinking }
285+
yield { type: "reasoning", text: contentBlock.thinking }
285286
break
286287
case "text":
287288
// We may receive multiple text blocks, in which
@@ -290,21 +291,67 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
290291
yield { type: "text", text: "\n" }
291292
}
292293

293-
yield { type: "text", text: chunk.content_block.text }
294+
yield { type: "text", text: contentBlock.text }
294295
break
295296
case "tool_use": {
296297
// Emit initial tool call partial with id and name
297298
yield {
298299
type: "tool_call_partial",
299300
index: chunk.index,
300-
id: chunk.content_block.id,
301-
name: chunk.content_block.name,
301+
id: contentBlock.id,
302+
name: contentBlock.name,
302303
arguments: undefined,
303304
}
304305
break
305306
}
307+
case "server_tool_use": {
308+
const { id, name, input } = contentBlock
309+
yield {
310+
type: "advisor_tool_use",
311+
id,
312+
name,
313+
input: typeof input === "object" ? JSON.stringify(input) : String(input ?? "{}"),
314+
}
315+
break
316+
}
317+
case "advisor_tool_result": {
318+
const block = contentBlock as {
319+
type: "advisor_tool_result"
320+
tool_use_id: string
321+
content:
322+
| string
323+
| { type: string; text?: string }
324+
| Array<{ type: string; text?: string }>
325+
| undefined
326+
}
327+
const rawContent = block.content
328+
let text: string
329+
if (typeof rawContent === "string") {
330+
text = rawContent
331+
} else if (Array.isArray(rawContent)) {
332+
text = rawContent
333+
.filter((b) => b.type === "text")
334+
.map((b) => b.text ?? "")
335+
.join("\n")
336+
} else if (rawContent && typeof rawContent === "object" && "text" in rawContent) {
337+
// advisor_result shape: { type: "advisor_result", text: "..." }
338+
text = (rawContent as { text?: string }).text ?? ""
339+
} else {
340+
text = ""
341+
}
342+
yield {
343+
type: "advisor_tool_result",
344+
tool_use_id: block.tool_use_id,
345+
content: text,
346+
// Pass through the verbatim content object so it can be
347+
// round-tripped on subsequent turns as the Anthropic API requires.
348+
rawContent: rawContent,
349+
}
350+
break
351+
}
306352
}
307353
break
354+
}
308355
case "content_block_delta":
309356
switch (chunk.delta.type) {
310357
case "thinking_delta":

src/api/transform/stream.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export type ApiStreamChunk =
1111
| ApiStreamToolCallDeltaChunk
1212
| ApiStreamToolCallEndChunk
1313
| ApiStreamToolCallPartialChunk
14+
| ApiStreamAdvisorToolUseChunk
15+
| ApiStreamAdvisorToolResultChunk
1416
| ApiStreamError
1517

1618
export interface ApiStreamError {
@@ -107,6 +109,22 @@ export interface ApiStreamToolCallPartialChunk {
107109
arguments?: string
108110
}
109111

112+
export interface ApiStreamAdvisorToolUseChunk {
113+
type: "advisor_tool_use"
114+
id: string
115+
name: string
116+
input: string // JSON-serialized
117+
}
118+
119+
export interface ApiStreamAdvisorToolResultChunk {
120+
type: "advisor_tool_result"
121+
tool_use_id: string
122+
/** Extracted text for display purposes */
123+
content: string
124+
/** Verbatim original content object from the API, for round-tripping on subsequent turns */
125+
rawContent: unknown
126+
}
127+
110128
export interface GroundingSource {
111129
title: string
112130
url: string

src/core/task/Task.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
type ModelInfo,
3636
type ClineApiReqCancelReason,
3737
type ClineApiReqInfo,
38+
type ClineAskUseAdvisorTool,
3839
RooCodeEventName,
3940
TelemetryEventName,
4041
TaskStatus,
@@ -2792,6 +2793,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
27922793
let assistantMessage = ""
27932794
let reasoningMessage = ""
27942795
let pendingGroundingSources: GroundingSource[] = []
2796+
let hasAdvisorEvents = false
2797+
// Accumulate advisor blocks in arrival order so they can be saved to API history.
2798+
// Anthropic requires the full assistant content (including server_tool_use /
2799+
// advisor_tool_result blocks) to be round-tripped on subsequent turns.
2800+
const pendingAdvisorBlocks: Array<{
2801+
type: "server_tool_use" | "advisor_tool_result"
2802+
id?: string
2803+
name?: string
2804+
input?: Record<string, unknown>
2805+
tool_use_id?: string
2806+
content?: unknown
2807+
}> = []
27952808
this.isStreaming = true
27962809

27972810
try {
@@ -3014,6 +3027,37 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
30143027
presentAssistantMessage(this)
30153028
break
30163029
}
3030+
case "advisor_tool_use": {
3031+
hasAdvisorEvents = true
3032+
pendingAdvisorBlocks.push({
3033+
type: "server_tool_use",
3034+
id: chunk.id,
3035+
name: chunk.name,
3036+
input:
3037+
typeof chunk.input === "string" ? {} : (chunk.input as Record<string, unknown>),
3038+
})
3039+
const payload: ClineAskUseAdvisorTool = {
3040+
toolUseId: chunk.id,
3041+
name: chunk.name,
3042+
input: chunk.input,
3043+
}
3044+
await this.say("use_advisor_tool", JSON.stringify(payload))
3045+
break
3046+
}
3047+
case "advisor_tool_result": {
3048+
hasAdvisorEvents = true
3049+
pendingAdvisorBlocks.push({
3050+
type: "advisor_tool_result",
3051+
tool_use_id: chunk.tool_use_id,
3052+
// Use rawContent (the verbatim original object from the API) for
3053+
// round-tripping to Anthropic. The API requires content to be the
3054+
// original discriminated union object (e.g. { type: "advisor_result", text: "..." })
3055+
// not a plain string. chunk.content is the extracted text for display only.
3056+
content: chunk.rawContent,
3057+
})
3058+
await this.say("advisor_tool_result", chunk.content)
3059+
break
3060+
}
30173061
case "text": {
30183062
assistantMessage += chunk.text
30193063

@@ -3410,14 +3454,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
34103454
// the assistant message is already in history. Otherwise, tool_result blocks would appear
34113455
// BEFORE their corresponding tool_use blocks, causing API errors.
34123456

3413-
// Check if we have any content to process (text or tool uses)
3457+
// Check if we have any content to process (text, tool uses, or advisor interactions)
34143458
const hasTextContent = assistantMessage.length > 0
34153459

34163460
const hasToolUses = this.assistantMessageContent.some(
34173461
(block) => block.type === "tool_use" || block.type === "mcp_tool_use",
34183462
)
34193463

3420-
if (hasTextContent || hasToolUses) {
3464+
if (hasTextContent || hasToolUses || hasAdvisorEvents) {
34213465
// Reset counter when we get a successful response with content
34223466
this.consecutiveNoAssistantMessagesCount = 0
34233467
// Display grounding sources to the user if they exist
@@ -3505,6 +3549,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
35053549
}
35063550
}
35073551

3552+
// Append advisor blocks (server_tool_use / advisor_tool_result) that arrived during
3553+
// this stream. The Anthropic API requires these to be round-tripped verbatim on
3554+
// subsequent turns; omitting them causes a 400 invalid_request_error.
3555+
for (const advisorBlock of pendingAdvisorBlocks) {
3556+
assistantContent.push(advisorBlock as unknown as Anthropic.ToolUseBlockParam)
3557+
}
3558+
35083559
// Enforce new_task isolation: if new_task is called alongside other tools,
35093560
// truncate any tools that come after it and inject error tool_results.
35103561
// This prevents orphaned tools when delegation disposes the parent task.

0 commit comments

Comments
 (0)