Skip to content

feat: Implement parallel tool calling functionality #6524

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e34d71e
Add initial parallel tool calls support to core data structures
Patrick-Erichsen Jul 8, 2025
ab5be7b
Fix parallel tool calls test format to use OpenAI toolCalls structure
Patrick-Erichsen Jul 8, 2025
082a73e
Refactor utility functions for parallel tool call support
Patrick-Erichsen Jul 8, 2025
7cb78fe
Rename callCurrentTool to callToolById for clarity
Patrick-Erichsen Jul 8, 2025
aa50f54
Update streamNormalInput to handle multiple tool calls
Patrick-Erichsen Jul 8, 2025
e3d6d16
Update PendingToolCallToolbar for parallel tool calls
Patrick-Erichsen Jul 8, 2025
c48eeb4
Update Redux state management for parallel tool calls
Patrick-Erichsen Jul 8, 2025
41a3106
Update message construction for parallel tool calls
Patrick-Erichsen Jul 8, 2025
bcf0969
Fix parallel tool calls utility functions and remove backward compati…
Patrick-Erichsen Jul 8, 2025
c49f841
Revert streaming fixes and preserve parallel tool calls progress
Patrick-Erichsen Jul 8, 2025
148d84f
Remove backward compatibility check in addToolCallDeltaToState
Patrick-Erichsen Jul 8, 2025
8bdffaf
Fix parallel tool calling functionality
Patrick-Erichsen Jul 8, 2025
a9e3a56
Fix parallel tool calls core processing and Claude API compatibility
Patrick-Erichsen Jul 9, 2025
cd30d42
fix: countTokens bug
Patrick-Erichsen Jul 9, 2025
4da6a68
remove unneeded fn
Patrick-Erichsen Jul 9, 2025
1d72ef6
Update countTokens.ts
Patrick-Erichsen Jul 9, 2025
9c8ea71
countTokens testing
Patrick-Erichsen Jul 9, 2025
6fd0135
Update countTokens.ts
Patrick-Erichsen Jul 9, 2025
d1de549
nuke flattenMessages
Patrick-Erichsen Jul 9, 2025
f573597
Fix parallel tool calls accept/reject issue and refactor selectors
Patrick-Erichsen Jul 10, 2025
77a1482
UI polish
Patrick-Erichsen Jul 10, 2025
e100d9f
improve tool group ui
Patrick-Erichsen Jul 10, 2025
7da112b
merge main
Patrick-Erichsen Jul 11, 2025
6d1db0a
Update launch.json
Patrick-Erichsen Jul 11, 2025
daf6cfb
Fix tool state persistence after tool calls complete
Patrick-Erichsen Jul 11, 2025
fc27add
fix streaming bugs
Patrick-Erichsen Jul 11, 2025
e3a5582
Update Button.tsx
Patrick-Erichsen Jul 11, 2025
326440f
cleanup group header
Patrick-Erichsen Jul 11, 2025
815e51c
cleanup history page
Patrick-Erichsen Jul 11, 2025
0274041
Merge branch 'main' into pe/parallel-tool-calling
Patrick-Erichsen Jul 12, 2025
70a5365
Update GeneratingIndicator.tsx
Patrick-Erichsen Jul 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { BuiltInToolNames } from "../../../tools/builtIn";
import { callBuiltInTool } from "../../../tools/callTool";
import { globSearchTool } from "../../../tools/definitions/globSearch";
import { grepSearchTool } from "../../../tools/definitions/grepSearch";
import { lsTool } from "../../../tools/definitions/lsTool";
import { lsTool } from "../../../tools/definitions/ls";
import { readFileTool } from "../../../tools/definitions/readFile";
import { viewRepoMapTool } from "../../../tools/definitions/viewRepoMap";
import { viewSubdirectoryTool } from "../../../tools/definitions/viewSubdirectory";
Expand Down
2 changes: 1 addition & 1 deletion core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export interface ChatHistoryItem {
editorState?: any;
modifiers?: InputModifiers;
promptLogs?: PromptLog[];
toolCallState?: ToolCallState;
toolCallStates?: ToolCallState[];
isGatheringContext?: boolean;
reasoning?: Reasoning;
appliedRules?: RuleWithSource[];
Expand Down
188 changes: 187 additions & 1 deletion core/llm/countTokens.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @ts-nocheck
// Generated by continue
import { ChatMessage, MessagePart } from "../index.js";
import { ChatMessage, MessagePart, ToolCall } from "../index.js";
import {
compileChatMessages,
countTokens,
countTokensAsync,
extractToolSequence,
pruneLinesFromBottom,
pruneLinesFromTop,
pruneRawPromptFromTop,
Expand Down Expand Up @@ -174,3 +175,188 @@ describe.skip("compileChatMessages", () => {
expect(compiled.length).toBe(1);
});
});

describe("extractToolSequence", () => {
// Helper function to create mock messages
const createUserMessage = (content: string): ChatMessage => ({
role: "user",
content,
});

const createAssistantMessage = (
content: string,
toolCalls?: ToolCall[],
): ChatMessage => ({
role: "assistant",
content,
toolCalls,
});

const createToolMessage = (
content: string,
toolCallId: string,
): ChatMessage => ({
role: "tool",
content,
toolCallId,
});

const createToolCall = (
id: string,
name: string,
args: object,
): ToolCall => ({
id,
type: "function",
function: {
name,
arguments: JSON.stringify(args),
},
});

test("extractToolSequence should handle a single user message", () => {
const messages: ChatMessage[] = [createUserMessage("Hello world!")];
const sequence = extractToolSequence(messages);

expect(sequence).toHaveLength(1);
expect(sequence[0].role).toBe("user");
expect(sequence[0].content).toBe("Hello world!");
expect(messages).toHaveLength(0); // Original array should be modified
});

test("extractToolSequence should handle a single tool message with assistant", () => {
const toolCall = createToolCall("tc_1", "read_file", { path: "test.txt" });
const messages: ChatMessage[] = [
createAssistantMessage("I'll read the file for you.", [toolCall]),
createToolMessage("File contents here", "tc_1"),
];
const sequence = extractToolSequence(messages);

expect(sequence).toHaveLength(2);
expect(sequence[0].role).toBe("assistant");
expect(sequence[1].role).toBe("tool");
expect(sequence[1].toolCallId).toBe("tc_1");
expect(messages).toHaveLength(0);
});

test("extractToolSequence should handle multiple consecutive tool messages", () => {
const toolCall1 = createToolCall("tc_1", "read_file", { path: "test.txt" });
const toolCall2 = createToolCall("tc_2", "write_file", {
path: "output.txt",
content: "data",
});
const messages: ChatMessage[] = [
createAssistantMessage("I'll read and write files.", [
toolCall1,
toolCall2,
]),
createToolMessage("File contents 1", "tc_1"),
createToolMessage("File written successfully", "tc_2"),
];
const sequence = extractToolSequence(messages);

expect(sequence).toHaveLength(3);
expect(sequence[0].role).toBe("assistant");
expect(sequence[1].role).toBe("tool");
expect(sequence[1].toolCallId).toBe("tc_1");
expect(sequence[2].role).toBe("tool");
expect(sequence[2].toolCallId).toBe("tc_2");
expect(messages).toHaveLength(0);
});

test("extractToolSequence should handle tool sequence with previous context", () => {
const toolCall = createToolCall("tc_1", "search", { query: "test" });
const messages: ChatMessage[] = [
createUserMessage("What's in the codebase?"),
createAssistantMessage("Let me search for you.", [toolCall]),
createToolMessage("Search results here", "tc_1"),
];
const sequence = extractToolSequence(messages);

expect(sequence).toHaveLength(2);
expect(sequence[0].role).toBe("assistant");
expect(sequence[1].role).toBe("tool");
expect(messages).toHaveLength(1); // User message should remain
expect(messages[0].role).toBe("user");
});

test("extractToolSequence should throw error when tool message has no matching tool call", () => {
const toolCall = createToolCall("tc_1", "read_file", { path: "test.txt" });
const messages: ChatMessage[] = [
createAssistantMessage("I'll read the file for you.", [toolCall]),
createToolMessage("File contents here", "tc_2"), // Wrong tool call ID
];

expect(() => extractToolSequence(messages)).toThrow(
'Error parsing chat history: no tool call found to match tool output for id "tc_2"',
);
});

test("extractToolSequence should throw error when last message is not user or tool", () => {
const messages: ChatMessage[] = [
createUserMessage("Hello"),
createAssistantMessage("Hi there!"),
];

expect(() => extractToolSequence(messages)).toThrow(
"Error parsing chat history: no user/tool message found",
);
});

test("extractToolSequence should throw error when messages array is empty", () => {
const messages: ChatMessage[] = [];

expect(() => extractToolSequence(messages)).toThrow(
"Error parsing chat history: no user/tool message found",
);
});

test("extractToolSequence should handle tool sequence without assistant message", () => {
const messages: ChatMessage[] = [
createToolMessage("Orphaned tool message", "tc_1"),
];
const sequence = extractToolSequence(messages);

expect(sequence).toHaveLength(1);
expect(sequence[0].role).toBe("tool");
expect(sequence[0].toolCallId).toBe("tc_1");
expect(messages).toHaveLength(0);
});

test("extractToolSequence should handle complex tool sequence with multiple tool calls", () => {
const toolCall1 = createToolCall("tc_1", "read_file", {
path: "file1.txt",
});
const toolCall2 = createToolCall("tc_2", "read_file", {
path: "file2.txt",
});
const toolCall3 = createToolCall("tc_3", "write_file", {
path: "output.txt",
content: "merged",
});

const messages: ChatMessage[] = [
createUserMessage("Merge these files"),
createAssistantMessage("I'll read both files and merge them.", [
toolCall1,
toolCall2,
toolCall3,
]),
createToolMessage("Contents of file1", "tc_1"),
createToolMessage("Contents of file2", "tc_2"),
createToolMessage("Files merged successfully", "tc_3"),
];

const sequence = extractToolSequence(messages);

expect(sequence).toHaveLength(4);
expect(sequence[0].role).toBe("assistant");
expect(sequence[1].role).toBe("tool");
expect(sequence[1].toolCallId).toBe("tc_1");
expect(sequence[2].role).toBe("tool");
expect(sequence[2].toolCallId).toBe("tc_2");
expect(sequence[3].role).toBe("tool");
expect(sequence[3].toolCallId).toBe("tc_3");
expect(messages).toHaveLength(1); // User message should remain
});
});
Loading
Loading