fix(odin): dedupe tool-trace chips — one per tool, warning only if all attempts failed
A failed call followed by a successful retry showed two identical chips (one orange). Retries are loop mechanics: the trace now carries one entry per tool with ok = delivered-at-least-once, for live chips and persisted history alike (also keeps meta.tools under its 30-entry cap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -534,6 +534,55 @@ describe("agenticChat loop (SDK mocked)", () => {
|
||||
expect(usageRows.length).toBe(2);
|
||||
});
|
||||
|
||||
it("dedupes the tool trace: failed attempt + successful retry = one ok chip", async () => {
|
||||
const self: AiAuthCtx = {
|
||||
userId: 999999996,
|
||||
roleName: "viewer",
|
||||
permissions: ["attendance.record"],
|
||||
};
|
||||
createMock.mockReset();
|
||||
createMock
|
||||
.mockResolvedValueOnce({
|
||||
stop_reason: "tool_use",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_a",
|
||||
name: "get_attendance_summary",
|
||||
input: { user_id: 1 }, // denied: someone else without manage
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 10, output_tokens: 5 },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stop_reason: "tool_use",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_b",
|
||||
name: "get_attendance_summary",
|
||||
input: {}, // retry: own data, succeeds
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 10, output_tokens: 5 },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stop_reason: "end_turn",
|
||||
content: [{ type: "text", text: "Hotovo." }],
|
||||
usage: { input_tokens: 10, output_tokens: 5 },
|
||||
});
|
||||
const res = await agenticChat(
|
||||
[{ role: "user", content: "Moje docházka?" }],
|
||||
self,
|
||||
);
|
||||
expect(res.toolTrace).toEqual([
|
||||
{ name: "get_attendance_summary", label: "Docházka", ok: true },
|
||||
]);
|
||||
await prisma.ai_usage.deleteMany({
|
||||
where: { user_id: self.userId, kind: "agent" },
|
||||
});
|
||||
});
|
||||
|
||||
it("injects the caller's identity into the system prompt", async () => {
|
||||
createMock.mockReset();
|
||||
createMock.mockResolvedValueOnce({
|
||||
|
||||
@@ -381,6 +381,17 @@ export async function agenticChat(
|
||||
}
|
||||
}
|
||||
|
||||
// One chip per tool: a failed attempt followed by a successful retry is
|
||||
// loop mechanics, not information for the user. ok = the tool delivered
|
||||
// data at least once; orange stays only when every attempt failed. Also
|
||||
// keeps the persisted meta.tools comfortably under its 30-entry cap.
|
||||
const dedupedTrace: ToolTraceEntry[] = [];
|
||||
for (const t of trace) {
|
||||
const seen = dedupedTrace.find((d) => d.name === t.name);
|
||||
if (!seen) dedupedTrace.push({ ...t });
|
||||
else seen.ok = seen.ok || t.ok;
|
||||
}
|
||||
|
||||
const text = (res?.content ?? [])
|
||||
.filter((b): b is Anthropic.TextBlock => b.type === "text")
|
||||
.map((b) => b.text)
|
||||
@@ -399,7 +410,7 @@ export async function agenticChat(
|
||||
} else if (!reply) {
|
||||
reply = "Nepodařilo se získat odpověď, zkuste to prosím znovu.";
|
||||
}
|
||||
return { reply, toolTrace: trace };
|
||||
return { reply, toolTrace: dedupedTrace };
|
||||
}
|
||||
|
||||
export interface ExtractedInvoice {
|
||||
|
||||
Reference in New Issue
Block a user