Skip to content

Commit e7545cb

Browse files
committed
feat: wire fetch_web_content into execution dispatcher
1 parent cff217c commit e7545cb

3 files changed

Lines changed: 153 additions & 0 deletions

File tree

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,15 @@ export class NativeToolCallParser {
637637
}
638638
break
639639

640+
case "fetch_web_content":
641+
if (partialArgs.url !== undefined) {
642+
nativeArgs = {
643+
url: partialArgs.url,
644+
prompt: partialArgs.prompt,
645+
}
646+
}
647+
break
648+
640649
default:
641650
break
642651
}
@@ -992,6 +1001,15 @@ export class NativeToolCallParser {
9921001
}
9931002
break
9941003

1004+
case "fetch_web_content":
1005+
if (args.url !== undefined) {
1006+
nativeArgs = {
1007+
url: args.url,
1008+
prompt: args.prompt,
1009+
} as NativeArgsFor<TName>
1010+
}
1011+
break
1012+
9951013
default:
9961014
if (customToolRegistry.has(resolvedName)) {
9971015
nativeArgs = args as NativeArgsFor<TName>

src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,67 @@ describe("NativeToolCallParser", () => {
291291
})
292292
})
293293
})
294+
295+
describe("fetch_web_content tool", () => {
296+
it("should parse fetch_web_content with url and prompt", () => {
297+
const toolCall = {
298+
id: "toolu_fetch_1",
299+
name: "fetch_web_content" as const,
300+
arguments: JSON.stringify({
301+
url: "https://example.com",
302+
prompt: "Find the main heading",
303+
}),
304+
}
305+
306+
const result = NativeToolCallParser.parseToolCall(toolCall)
307+
308+
expect(result).not.toBeNull()
309+
expect(result?.type).toBe("tool_use")
310+
if (result?.type === "tool_use") {
311+
expect(result.nativeArgs).toBeDefined()
312+
const nativeArgs = result.nativeArgs as { url: string; prompt?: string }
313+
expect(nativeArgs.url).toBe("https://example.com")
314+
expect(nativeArgs.prompt).toBe("Find the main heading")
315+
}
316+
})
317+
318+
it("should parse fetch_web_content with url only (no prompt)", () => {
319+
const toolCall = {
320+
id: "toolu_fetch_2",
321+
name: "fetch_web_content" as const,
322+
arguments: JSON.stringify({
323+
url: "https://api.example.com/status",
324+
prompt: null,
325+
}),
326+
}
327+
328+
const result = NativeToolCallParser.parseToolCall(toolCall)
329+
330+
expect(result).not.toBeNull()
331+
expect(result?.type).toBe("tool_use")
332+
if (result?.type === "tool_use") {
333+
expect(result.nativeArgs).toBeDefined()
334+
const nativeArgs = result.nativeArgs as { url: string; prompt?: string | null }
335+
expect(nativeArgs.url).toBe("https://api.example.com/status")
336+
expect(nativeArgs.prompt).toBeNull()
337+
}
338+
})
339+
340+
it("should return null when url is missing", () => {
341+
const toolCall = {
342+
id: "toolu_fetch_3",
343+
name: "fetch_web_content" as const,
344+
arguments: JSON.stringify({
345+
prompt: "some prompt",
346+
}),
347+
}
348+
349+
const result = NativeToolCallParser.parseToolCall(toolCall)
350+
351+
// Should return null because nativeArgs can't be constructed without url
352+
expect(result).toBeNull()
353+
})
354+
})
294355
})
295356

296357
describe("processStreamingChunk", () => {
@@ -311,6 +372,22 @@ describe("NativeToolCallParser", () => {
311372
expect(nativeArgs.path).toBe("src/test.ts")
312373
})
313374
})
375+
376+
describe("fetch_web_content tool", () => {
377+
it("should emit a partial ToolUse with nativeArgs.url during streaming", () => {
378+
const id = "toolu_streaming_fetch_1"
379+
NativeToolCallParser.startStreamingToolCall(id, "fetch_web_content")
380+
381+
const fullArgs = JSON.stringify({ url: "https://example.com", prompt: "Find info" })
382+
const result = NativeToolCallParser.processStreamingChunk(id, fullArgs)
383+
384+
expect(result).not.toBeNull()
385+
expect(result?.nativeArgs).toBeDefined()
386+
const nativeArgs = result?.nativeArgs as { url: string; prompt?: string }
387+
expect(nativeArgs.url).toBe("https://example.com")
388+
expect(nativeArgs.prompt).toBe("Find info")
389+
})
390+
})
314391
})
315392

316393
describe("finalizeStreamingToolCall", () => {
@@ -342,5 +419,53 @@ describe("NativeToolCallParser", () => {
342419
}
343420
})
344421
})
422+
423+
describe("fetch_web_content tool", () => {
424+
it("should parse fetch_web_content args on finalize", () => {
425+
const id = "toolu_finalize_fetch_1"
426+
NativeToolCallParser.startStreamingToolCall(id, "fetch_web_content")
427+
428+
NativeToolCallParser.processStreamingChunk(
429+
id,
430+
JSON.stringify({
431+
url: "https://docs.example.com/api",
432+
prompt: "Find authentication methods",
433+
}),
434+
)
435+
436+
const result = NativeToolCallParser.finalizeStreamingToolCall(id)
437+
438+
expect(result).not.toBeNull()
439+
expect(result?.type).toBe("tool_use")
440+
if (result?.type === "tool_use") {
441+
const nativeArgs = result.nativeArgs as { url: string; prompt?: string }
442+
expect(nativeArgs.url).toBe("https://docs.example.com/api")
443+
expect(nativeArgs.prompt).toBe("Find authentication methods")
444+
}
445+
})
446+
447+
it("should parse fetch_web_content with null prompt on finalize", () => {
448+
const id = "toolu_finalize_fetch_2"
449+
NativeToolCallParser.startStreamingToolCall(id, "fetch_web_content")
450+
451+
NativeToolCallParser.processStreamingChunk(
452+
id,
453+
JSON.stringify({
454+
url: "https://api.example.com/status",
455+
prompt: null,
456+
}),
457+
)
458+
459+
const result = NativeToolCallParser.finalizeStreamingToolCall(id)
460+
461+
expect(result).not.toBeNull()
462+
expect(result?.type).toBe("tool_use")
463+
if (result?.type === "tool_use") {
464+
const nativeArgs = result.nativeArgs as { url: string; prompt?: string | null }
465+
expect(nativeArgs.url).toBe("https://api.example.com/status")
466+
expect(nativeArgs.prompt).toBeNull()
467+
}
468+
})
469+
})
345470
})
346471
})

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { generateImageTool } from "../tools/GenerateImageTool"
3737
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
3838
import { isValidToolName, validateToolUse } from "../tools/validateToolUse"
3939
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
40+
import { fetchWebContentTool } from "../tools/FetchWebContentTool"
4041

4142
import { formatResponse } from "../prompts/responses"
4243
import { sanitizeToolUseId } from "../../utils/tool-id"
@@ -383,6 +384,8 @@ export async function presentAssistantMessage(cline: Task) {
383384
return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
384385
case "generate_image":
385386
return `[${block.name} for '${block.params.path}']`
387+
case "fetch_web_content":
388+
return `[${block.name} for '${block.params.url}']`
386389
default:
387390
return `[${block.name}]`
388391
}
@@ -849,6 +852,13 @@ export async function presentAssistantMessage(cline: Task) {
849852
pushToolResult,
850853
})
851854
break
855+
case "fetch_web_content":
856+
await fetchWebContentTool.handle(cline, block as ToolUse<"fetch_web_content">, {
857+
askApproval,
858+
handleError,
859+
pushToolResult,
860+
})
861+
break
852862
default: {
853863
// Handle unknown/invalid tool names OR custom tools
854864
// This is critical for native tool calling where every tool_use MUST have a tool_result

0 commit comments

Comments
 (0)