Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion core/tools/systemMessageTools/buildToolsSystemMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const generateToolsSystemMessage = (
`\nAlso, these additional tool definitions show other tools you can call with the same syntax:`,
);

for (const tool of tools) {
for (const tool of withDynamicMessage) {
try {
const definition = framework.toolToSystemToolDefinition(tool);
instructions.push(`\n${definition}`);
Expand Down
13 changes: 7 additions & 6 deletions core/tools/systemMessageTools/interceptSystemToolCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function* interceptSystemToolCalls(
return result.value;
} else {
for await (const message of result.value) {
if (abortController.signal.aborted || parseState?.done) {
if (abortController.signal.aborted) {
break;
}
// Skip non-assistant messages or messages with native tool calls
Expand Down Expand Up @@ -96,12 +96,13 @@ export async function* interceptSystemToolCalls(
},
];
}
} else {
// Prevent content after tool calls for now
if (parseState) {
continue;
// Completed tool calls should not terminate parsing for subsequent
// chunks/messages; reset state so normal content (or another tool
// call) can be handled.
if (parseState.done) {
parseState = undefined;
}

} else {
// Yield normal assistant message
yield [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ describe("generateToolsSystemMessage", () => {

const hasDynamicToolsSection = /additional tool definitions/i.test(result);
expect(hasDynamicToolsSection).toBe(true);

// Dynamic definitions should not duplicate tools that already have predefined messages.
expect(result.match(/TOOL_NAME: tool_with_description/g)?.length ?? 0).toBe(
1,
);
});

it("includes example tool definition and call", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,15 +242,15 @@ describe("interceptSystemToolCalls", () => {
).toBe("}");
});

it("ignores content after a tool call", async () => {
it("preserves content after a tool call", async () => {
const messages: ChatMessage[][] = [
[{ role: "assistant", content: "```tool\n" }],
[{ role: "assistant", content: "TOOL_NAME: test_tool\n" }],
[{ role: "assistant", content: "BEGIN_ARG: arg1\n" }],
[{ role: "assistant", content: "value1\n" }],
[{ role: "assistant", content: "END_ARG\n" }],
[{ role: "assistant", content: "```\n" }],
[{ role: "assistant", content: "This content should be ignored" }],
[{ role: "assistant", content: "This content should be preserved" }],
];

const generator = interceptSystemToolCalls(
Expand All @@ -260,14 +260,102 @@ describe("interceptSystemToolCalls", () => {
);

let result;
// Process through all the tool call parts
for (let i = 0; i < 6; i++) {
// Process through all the tool call deltas (name, arg prefix, arg value, closing brace)
for (let i = 0; i < 4; i++) {
result = await generator.next();
}

// The content after the tool call should be ignored
// The trailing newline from "```\n" is yielded as text after the tool call ends
result = await generator.next();
expect(result.value).toBeUndefined();
expect(result.value).toEqual([
{
role: "assistant",
content: [{ type: "text", text: "\n" }],
},
]);

// The content after the tool call should be preserved
result = await generator.next();
expect(result.value).toEqual([
{
role: "assistant",
content: [{ type: "text", text: "This content should be preserved" }],
},
]);
});

it("parses a tool call that appears mid-message and preserves trailing content", async () => {
const messages: ChatMessage[][] = [
[
{
role: "assistant",
content:
"Before tool\n```tool\nTOOL_NAME: test_tool\nBEGIN_ARG: arg1\nvalue1\nEND_ARG\n```\nAfter tool",
},
],
];

const generator = interceptSystemToolCalls(
createAsyncGenerator(messages),
abortController,
framework,
);

let result = await generator.next();
expect(result.value).toEqual([
{
role: "assistant",
content: [{ type: "text", text: "Before tool" }],
},
]);

result = await generator.next();
expect(result.value).toEqual([
{
role: "assistant",
content: [{ type: "text", text: "\n" }],
},
]);

result = await generator.next();
expect(
(result.value as AssistantChatMessage[])[0].toolCalls?.[0].function?.name,
).toBe("test_tool");

result = await generator.next();
expect(
(result.value as AssistantChatMessage[])[0].toolCalls?.[0].function
?.arguments,
).toContain('{"arg1":');

result = await generator.next();
expect(
(result.value as AssistantChatMessage[])[0].toolCalls?.[0].function
?.arguments,
).toBe('"value1"');

result = await generator.next();
expect(
(result.value as AssistantChatMessage[])[0].toolCalls?.[0].function
?.arguments,
).toBe("}");

// The newline between the closing ``` and "After tool" is a separate chunk
result = await generator.next();
expect(result.value).toEqual([
{
role: "assistant",
content: [{ type: "text", text: "\n" }],
},
]);

result = await generator.next();
expect(result.value).toEqual([
{
role: "assistant",
content: [{ type: "text", text: "After tool" }],
},
]);
});

it("stops processing when aborted", async () => {
Expand Down
Loading