Compare commits

..

2 Commits

Author SHA1 Message Date
BOHA
302a0f8b3f chore(release): v2.4.15 - Odin tool-chip dedupe
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 00:20:44 +02:00
BOHA
78f39e9f82 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>
2026-06-11 00:20:43 +02:00
4 changed files with 64 additions and 4 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "app-ts",
"version": "2.4.14",
"version": "2.4.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "app-ts",
"version": "2.4.14",
"version": "2.4.15",
"license": "ISC",
"dependencies": {
"@anthropic-ai/sdk": "^0.102.0",

View File

@@ -1,6 +1,6 @@
{
"name": "app-ts",
"version": "2.4.14",
"version": "2.4.15",
"description": "",
"main": "dist/server.js",
"scripts": {

View File

@@ -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({

View File

@@ -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 {