Skip to content

Commit ca4e589

Browse files
committed
fix: filter undefined historical tools
1 parent e4321fb commit ca4e589

2 files changed

Lines changed: 55 additions & 45 deletions

File tree

worker/src/plaintext.mjs

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -210,40 +210,46 @@ function normalizeTools(tools) {
210210
.filter((t) => t && t.function?.name);
211211
}
212212

213-
function historicalToolNames(messages) {
214-
const names = new Set();
213+
export function filterUnknownToolHistory(messages = [], tools = []) {
214+
const normalizedTools = normalizeTools(tools);
215+
const definedNames = new Set(normalizedTools.map((tool) => tool.function.name));
216+
const droppedToolCallIds = new Set();
217+
const droppedToolNames = new Set();
218+
const filtered = [];
219+
215220
for (const message of Array.isArray(messages) ? messages : []) {
216-
if (message?.role !== "assistant" || !Array.isArray(message.tool_calls)) continue;
217-
for (const toolCall of message.tool_calls) {
218-
const fn = toolCall?.function && typeof toolCall.function === "object" ? toolCall.function : {};
219-
const name = String(fn.name || toolCall?.name || "").trim();
220-
if (name) names.add(name);
221+
if (message?.role === "assistant" && Array.isArray(message.tool_calls)) {
222+
const retainedCalls = [];
223+
for (const toolCall of message.tool_calls) {
224+
const fn = toolCall?.function && typeof toolCall.function === "object" ? toolCall.function : {};
225+
const name = String(fn.name || toolCall?.name || "").trim();
226+
if (name && definedNames.has(name)) {
227+
retainedCalls.push(toolCall);
228+
continue;
229+
}
230+
if (typeof toolCall?.id === "string" && toolCall.id.trim()) {
231+
droppedToolCallIds.add(toolCall.id.trim());
232+
}
233+
if (name) droppedToolNames.add(name);
234+
}
235+
if (!retainedCalls.length) {
236+
if (contentToString(message.content)) filtered.push({ ...message, tool_calls: undefined });
237+
continue;
238+
}
239+
filtered.push({ ...message, tool_calls: retainedCalls });
240+
continue;
241+
}
242+
if (message?.role === "tool" && typeof message.tool_call_id === "string" && droppedToolCallIds.has(message.tool_call_id.trim())) {
243+
continue;
221244
}
245+
filtered.push(message);
222246
}
223-
return names;
224-
}
225247

226-
export function addHistoricalToolDefinitions(tools = [], messages = []) {
227-
const normalized = normalizeTools(tools);
228-
const defined = new Set(normalized.map((tool) => tool.function.name));
229-
const added = [];
230-
for (const name of historicalToolNames(messages)) {
231-
if (defined.has(name)) continue;
232-
normalized.push({
233-
type: "function",
234-
function: {
235-
name,
236-
description: "Compatibility definition for a tool call preserved in conversation history.",
237-
parameters: {
238-
type: "object",
239-
additionalProperties: true,
240-
},
241-
},
242-
});
243-
defined.add(name);
244-
added.push(name);
245-
}
246-
return { tools: normalized, added };
248+
return {
249+
messages: filtered,
250+
droppedToolCallIds: [...droppedToolCallIds],
251+
droppedToolNames: [...droppedToolNames],
252+
};
247253
}
248254

249255
function normalizeToolCall(toolCall, messageIndex, callIndex, usedIds, sourceIds) {
@@ -442,8 +448,8 @@ export function buildPlainChatBody({
442448
const sessionId = crypto.randomUUID();
443449
const mapped = mapModel(model);
444450

445-
const openaiMessages = normalizeMessagesForUpstream(messages || []);
446-
const compatibleTools = addHistoricalToolDefinitions(tools, messages || []);
451+
const filteredHistory = filterUnknownToolHistory(messages || [], tools);
452+
const openaiMessages = normalizeMessagesForUpstream(filteredHistory.messages);
447453

448454
// Chat-API mode:
449455
// - Do NOT inherit capture-template Qoder agent system/tools (they explode multiturn).
@@ -535,7 +541,7 @@ export function buildPlainChatBody({
535541
? [...systemMessages, ...(nonSystem.length ? nonSystem : [{ role: "user", content: "ping" }])]
536542
: [{ role: "user", content: "ping" }],
537543
// Pass through caller tools (OpenAI function tools). Do NOT inherit capture-template tools.
538-
tools: compatibleTools.tools,
544+
tools: normalizeTools(tools),
539545
parameters,
540546
business: {
541547
product: "cli",

worker/test/plaintext.test.mjs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
estimateTokens,
99
normalizeMessagesForUpstream,
1010
diagnoseOpenAIToolHistory,
11-
addHistoricalToolDefinitions,
11+
filterUnknownToolHistory,
1212
} from "../src/plaintext.mjs";
1313

1414
test("maps known display names to upstream keys", () => {
@@ -125,20 +125,24 @@ test("normalizes OpenAI tool history and repairs missing tool ids", () => {
125125
assert.equal("name" in messages[3], false);
126126
});
127127

128-
test("adds compatibility definitions for tools preserved in history", () => {
129-
const result = addHistoricalToolDefinitions(
130-
[{ type: "function", function: { name: "Read", parameters: {} } }],
131-
[{
132-
role: "assistant",
133-
tool_calls: [
128+
test("filters unknown historical tool calls and matching results", () => {
129+
const result = filterUnknownToolHistory(
130+
[
131+
{ role: "assistant", content: null, tool_calls: [
134132
{ id: "call_task", function: { name: "TaskCreate", arguments: "{}" } },
135133
{ id: "call_read", function: { name: "Read", arguments: "{}" } },
136-
],
137-
}],
134+
] },
135+
{ role: "tool", tool_call_id: "call_task", content: "task result" },
136+
{ role: "tool", tool_call_id: "call_read", content: "read result" },
137+
],
138+
[{ type: "function", function: { name: "Read", parameters: {} } }],
138139
);
139-
assert.deepEqual(result.added, ["TaskCreate"]);
140-
assert.deepEqual(result.tools.map((tool) => tool.function.name), ["Read", "TaskCreate"]);
141-
assert.equal(result.tools[1].function.parameters.additionalProperties, true);
140+
assert.deepEqual(result.droppedToolCallIds, ["call_task"]);
141+
assert.deepEqual(result.droppedToolNames, ["TaskCreate"]);
142+
assert.equal(result.messages.length, 2);
143+
assert.equal(result.messages[0].tool_calls.length, 1);
144+
assert.equal(result.messages[0].tool_calls[0].function.name, "Read");
145+
assert.equal(result.messages[1].tool_call_id, "call_read");
142146
});
143147

144148
test("keeps distinct existing tool ids across multiple assistant turns", () => {

0 commit comments

Comments
 (0)