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
9 changes: 6 additions & 3 deletions app/src/features/account/TokensByBot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { useEffect, useState } from "react";
import { useT } from "../../i18n";
import { errorText } from "../../lib/api";
import { tokens, usedTokens } from "../../lib/format";
import { reloadedTokens, tokens, usedTokens } from "../../lib/format";
import type { BotTokens } from "../../lib/protocol.gen";
import { useApi } from "../../store/context";
import { Callout } from "../../ui/Callout";
Expand Down Expand Up @@ -99,7 +99,7 @@ function Row({ bot, most }: { bot: BotTokens; most: number }) {
/>
</div>
<span className="text-muted text-xs">
{u.detail(bot.turns, tokens(bot.tokens.cacheRead))}
{u.detail(bot.turns, tokens(bot.tokens.cacheRead), reloadedTokens(bot.tokens.reloaded))}
</span>
</div>
</li>
Expand All @@ -111,13 +111,16 @@ function Total({ bots }: { bots: BotTokens[] }) {
const used = bots.reduce((sum, bot) => sum + usedTokens(bot.tokens), 0);
const turns = bots.reduce((sum, bot) => sum + bot.turns, 0);
const reread = bots.reduce((sum, bot) => sum + bot.tokens.cacheRead, 0);
const reloaded = bots.reduce((sum, bot) => sum + bot.tokens.reloaded, 0);
return (
<li className="flex flex-col gap-1 border-line border-t pt-3">
<div className="flex items-baseline justify-between gap-3 font-semibold">
<span>{u.total}</span>
<span className="tabular-nums">{tokens(used)}</span>
</div>
<span className="text-muted text-xs">{u.detail(turns, tokens(reread))}</span>
<span className="text-muted text-xs">
{u.detail(turns, tokens(reread), reloadedTokens(reloaded))}
</span>
</li>
);
}
22 changes: 22 additions & 0 deletions app/src/features/account/tokens.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ describe("tokens by bot", () => {
expect(dialog.textContent).toContain("All bots9.8k");
});

test("the conversation sent again after a pause counts apart", async () => {
const fake = new FakeBotloft();
const ops = fake.addCrew("Ops");
const scout = fake.addBot(ops.id, "Scout", "Finds sources");
fake.chat.turn(scout.id, null, {
input: 2,
cacheWrite: 72_636,
reloaded: 72_000,
cacheRead: 0,
output: 983,
});
fake.chat.turn(scout.id);

const dialog = await openUsage(fake);
const list = await within(dialog).findByRole("list", { name: "Tokens by bot" });
expect(
within(list)
.getAllByRole("listitem")
.map((row) => row.textContent),
).toEqual(["Scout4.1kWorked 2 times · 24k reread · 72k reloaded"]);
});

test("a period with no work says so", async () => {
const dialog = await openUsage(new FakeBotloft());
expect(await within(dialog).findByText("No bot worked in this period.")).toBeDefined();
Expand Down
8 changes: 5 additions & 3 deletions app/src/features/chat/BotRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { CircleAlert } from "lucide-react";
import { memo, type ReactNode } from "react";
import { useT } from "../../i18n";
import { duration, tokens, usedTokens, when } from "../../lib/format";
import { duration, reloadedTokens, tokens, usedTokens, when } from "../../lib/format";
import type { Bot, ChatItem, TurnItem } from "../../lib/protocol.gen";
import { useArrival } from "../../ui/motion";
import { BotAvatar, moodOf } from "../bots/BotAvatar";
Expand Down Expand Up @@ -33,14 +33,16 @@ function TurnEnd({ turn }: { turn: TurnItem }) {
}
const time = duration(turn.durationMs);
const used = turn.tokens;
const reloaded = used && reloadedTokens(used.reloaded);
const detail = used && {
read: tokens(used.input + used.cacheWrite),
read: tokens(used.input + used.cacheWrite - used.reloaded),
wrote: tokens(used.output),
reread: tokens(used.cacheRead),
reloaded,
};
return (
<p className="text-muted text-xs" title={t.chat.run.took(time, detail)}>
{t.chat.run.done(time, used && tokens(usedTokens(used)))}
{t.chat.run.done(time, used && tokens(usedTokens(used)), reloaded)}
</p>
);
}
Expand Down
32 changes: 32 additions & 0 deletions app/src/features/chat/context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,38 @@ function open() {
}

describe("conversation space", () => {
test("a turn after a pause tells the conversation it sent again apart", async () => {
const { fake, scout } = await openScout();
act(() => {
// The turn seen in spec 19: almost all of it was the conversation again.
fake.chat.turn(scout.id, null, {
input: 2,
cacheWrite: 72_636,
reloaded: 72_000,
cacheRead: 0,
output: 983,
});
});
const done = screen.getByText("Done in 4.2 s · 1.6k tokens · reloaded the conversation (72k)");
expect(done.title).toBe(
"Took 4.2 s. Read 638 new tokens and wrote 983; reread 0 of the conversation, which weighs much less. It also sent 72k of the conversation again, as the model no longer had it after a pause.",
);
});

test("a little sent again is not worth a mention", async () => {
const { fake, scout } = await openScout();
act(() => {
fake.chat.turn(scout.id, null, {
input: 2,
cacheWrite: 1_400,
reloaded: 300,
cacheRead: 40_000,
output: 100,
});
});
expect(screen.getByText("Done in 4.2 s · 1.2k tokens")).toBeDefined();
});

test("token counts read short", () => {
expect(tokens(850)).toBe("850");
expect(tokens(24_340)).toBe("24.3k");
Expand Down
8 changes: 5 additions & 3 deletions app/src/i18n/en/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ export const account = {
tokens: {
title: "Tokens by bot",
intro:
"Tokens are the pieces of text a bot reads and writes. The count adds the new text read and the replies written; the conversation reread each time shows apart, as it weighs much less.",
"Tokens are the pieces of text a bot reads and writes. The count adds the new text read and the replies written. The conversation reread each time shows apart, as it weighs much less, and so does the conversation sent again after a pause.",
period: "Period",
periods: { today: "Today", week: "7 days", month: "30 days", all: "All time" },
empty: "No bot worked in this period.",
failed: "Could not load the tokens",
archived: "archived",
detail: (times: number, reread: string) =>
`Worked ${times === 1 ? "once" : `${times} times`} · ${reread} reread`,
detail: (times: number, reread: string, reloaded: string | null) =>
`Worked ${times === 1 ? "once" : `${times} times`} · ${reread} reread${
reloaded === null ? "" : ` · ${reloaded} reloaded`
}`,
total: "All bots",
},
},
Expand Down
14 changes: 11 additions & 3 deletions app/src/i18n/en/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface TurnTokens {
read: string;
wrote: string;
reread: string;
/** The conversation sent again after a pause, if it was worth telling. */
reloaded: string | null;
}

export const chat = {
Expand Down Expand Up @@ -54,13 +56,19 @@ export const chat = {
},
run: {
working: "Working",
done: (time: string, tokens: string | null) =>
`Done in ${time}${tokens === null ? "" : ` · ${tokens} tokens`}`,
done: (time: string, tokens: string | null, reloaded: string | null) =>
`Done in ${time}${tokens === null ? "" : ` · ${tokens} tokens`}${
reloaded === null ? "" : ` · reloaded the conversation (${reloaded})`
}`,
took: (time: string, tokens: TurnTokens | null) =>
`Took ${time}${
tokens === null
? ""
: `. Read ${tokens.read} new tokens and wrote ${tokens.wrote}; reread ${tokens.reread} of the conversation, which weighs much less.`
: `. Read ${tokens.read} new tokens and wrote ${tokens.wrote}; reread ${tokens.reread} of the conversation, which weighs much less.${
tokens.reloaded === null
? ""
: ` It also sent ${tokens.reloaded} of the conversation again, as the model no longer had it after a pause.`
}`
}`,
stopped: (reason: string) => `The bot stopped working on this: ${reason}`,
},
Expand Down
8 changes: 5 additions & 3 deletions app/src/i18n/es/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ export const account: Messages["account"] = {
tokens: {
title: "Tokens por bot",
intro:
"Los tokens son los trozos de texto que un bot lee y escribe. La cuenta suma el texto nuevo leído y las respuestas escritas; la conversación releída cada vez aparece aparte, porque pesa mucho menos.",
"Los tokens son los trozos de texto que un bot lee y escribe. La cuenta suma el texto nuevo leído y las respuestas escritas. La conversación releída cada vez aparece aparte, porque pesa mucho menos, y también la conversación enviada de nuevo tras una pausa.",
period: "Período",
periods: { today: "Hoy", week: "7 días", month: "30 días", all: "Todo" },
empty: "Ningún bot trabajó en este período.",
failed: "No se pudieron cargar los tokens",
archived: "archivado",
detail: (times: number, reread: string) =>
`Trabajó ${times === 1 ? "1 vez" : `${times} veces`} · ${reread} releídos`,
detail: (times: number, reread: string, reloaded: string | null) =>
`Trabajó ${times === 1 ? "1 vez" : `${times} veces`} · ${reread} releídos${
reloaded === null ? "" : ` · ${reloaded} recargados`
}`,
total: "Todos los bots",
},
},
Expand Down
12 changes: 9 additions & 3 deletions app/src/i18n/es/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,19 @@ export const chat: Messages["chat"] = {
},
run: {
working: "Trabajando",
done: (time: string, tokens: string | null) =>
`Terminado en ${time}${tokens === null ? "" : ` · ${tokens} tokens`}`,
done: (time: string, tokens: string | null, reloaded: string | null) =>
`Terminado en ${time}${tokens === null ? "" : ` · ${tokens} tokens`}${
reloaded === null ? "" : ` · recargó la conversación (${reloaded})`
}`,
took: (time: string, tokens: TurnTokens | null) =>
`Tardó ${time}${
tokens === null
? ""
: `. Leyó ${tokens.read} tokens nuevos y escribió ${tokens.wrote}; releyó ${tokens.reread} de la conversación, que pesa mucho menos.`
: `. Leyó ${tokens.read} tokens nuevos y escribió ${tokens.wrote}; releyó ${tokens.reread} de la conversación, que pesa mucho menos.${
tokens.reloaded === null
? ""
: ` También volvió a enviar ${tokens.reloaded} de la conversación, que el modelo ya no tenía tras una pausa.`
}`
}`,
stopped: (reason: string) => `El bot dejó de trabajar en esto: ${reason}`,
},
Expand Down
8 changes: 5 additions & 3 deletions app/src/i18n/pt-BR/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ export const account: Messages["account"] = {
tokens: {
title: "Tokens por bot",
intro:
"Tokens são os pedaços de texto que um bot lê e escreve. A conta soma o texto novo lido e as respostas escritas; a conversa relida a cada vez aparece à parte, porque pesa bem menos.",
"Tokens são os pedaços de texto que um bot lê e escreve. A conta soma o texto novo lido e as respostas escritas. A conversa relida a cada vez aparece à parte, porque pesa bem menos, e a conversa mandada de novo depois de uma pausa também.",
period: "Período",
periods: { today: "Hoje", week: "7 dias", month: "30 dias", all: "Tudo" },
empty: "Nenhum bot trabalhou nesse período.",
failed: "Não deu para carregar os tokens",
archived: "arquivado",
detail: (times: number, reread: string) =>
`Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`} · ${reread} relidos`,
detail: (times: number, reread: string, reloaded: string | null) =>
`Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`} · ${reread} relidos${
reloaded === null ? "" : ` · ${reloaded} recarregados`
}`,
total: "Todos os bots",
},
},
Expand Down
12 changes: 9 additions & 3 deletions app/src/i18n/pt-BR/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,19 @@ export const chat: Messages["chat"] = {
},
run: {
working: "Trabalhando",
done: (time: string, tokens: string | null) =>
`Concluído em ${time}${tokens === null ? "" : ` · ${tokens} tokens`}`,
done: (time: string, tokens: string | null, reloaded: string | null) =>
`Concluído em ${time}${tokens === null ? "" : ` · ${tokens} tokens`}${
reloaded === null ? "" : ` · recarregou a conversa (${reloaded})`
}`,
took: (time: string, tokens: TurnTokens | null) =>
`Levou ${time}${
tokens === null
? ""
: `. Leu ${tokens.read} tokens novos e escreveu ${tokens.wrote}; releu ${tokens.reread} da conversa, que pesa bem menos.`
: `. Leu ${tokens.read} tokens novos e escreveu ${tokens.wrote}; releu ${tokens.reread} da conversa, que pesa bem menos.${
tokens.reloaded === null
? ""
: ` Também mandou de novo ${tokens.reloaded} da conversa, que o modelo já não tinha depois de uma pausa.`
}`
}`,
stopped: (reason: string) => `O bot parou de trabalhar nisto: ${reason}`,
},
Expand Down
14 changes: 12 additions & 2 deletions app/src/lib/fakeChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ import type {
ChatBody,
ChatItem,
RoutinesCreateParams,
TokenUsage,
ToolItem,
} from "./protocol.gen";

/** A turn's tokens unless a test says otherwise: a warm cache. */
const TURN_TOKENS: TokenUsage = {
input: 12,
cacheWrite: 1800,
reloaded: 0,
cacheRead: 24_000,
output: 640,
};

/** The chief's tool to suggest a bot (spec 10.2). */
export const SUGGEST_TOOL = "mcp__botloft__suggest_bot";
/** Any bot's tool to ask for a routine (spec 20.12). */
Expand Down Expand Up @@ -121,11 +131,11 @@ export class FakeChat {
return this.update(item.id, { ...item.body, status: failed ? "failed" : "done", output });
}

turn(botId: BotId, error: string | null = null): ChatItem {
turn(botId: BotId, error: string | null = null, tokens: TokenUsage = TURN_TOKENS): ChatItem {
return this.add(botId, {
kind: "turn",
durationMs: 4200,
tokens: { input: 12, cacheWrite: 1800, cacheRead: 24_000, output: 640 },
tokens,
error,
});
}
Expand Down
5 changes: 3 additions & 2 deletions app/src/lib/fakeUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ export function usageHandlers(fake: FakeBotloft): Pick<Handlers, "usage.tokens">
color: bot.color,
archived: bot.archivedAt !== null,
turns: 0,
tokens: { input: 0, cacheWrite: 0, cacheRead: 0, output: 0 },
tokens: { input: 0, cacheWrite: 0, reloaded: 0, cacheRead: 0, output: 0 },
};
byBot.set(bot.id, entry);
}
const { input, cacheWrite, cacheRead, output } = item.body.tokens;
const { input, cacheWrite, reloaded, cacheRead, output } = item.body.tokens;
entry.turns += 1;
entry.tokens = {
input: entry.tokens.input + input,
cacheWrite: entry.tokens.cacheWrite + cacheWrite,
reloaded: entry.tokens.reloaded + reloaded,
cacheRead: entry.tokens.cacheRead + cacheRead,
output: entry.tokens.output + output,
};
Expand Down
13 changes: 11 additions & 2 deletions app/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,19 @@ export function tokens(count: number): string {
/**
* The tokens a turn counts for (spec 8.7): new input read, with what went
* into the prompt cache, and what the model wrote. The conversation reread
* from the cache weighs much less and is shown apart.
* from the cache weighs much less and is shown apart, and so is the
* conversation written to the cache again after it expired.
*/
export function usedTokens(usage: TokenUsage): number {
return usage.input + usage.cacheWrite + usage.output;
return usage.input + usage.cacheWrite - usage.reloaded + usage.output;
}

/** Below this, the conversation sent again is not worth a mention. */
const RELOAD_SHOWN = 1000;

/** The conversation sent again, written as a number, or `null` if it is too little to tell. */
export function reloadedTokens(count: number): string | null {
return count >= RELOAD_SHOWN ? tokens(count) : null;
}

/** "0.8 s", "42 s", "3 min 5 s". */
Expand Down
5 changes: 5 additions & 0 deletions app/src/lib/protocol.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,11 @@ input: number,
* New input also written to the prompt cache (`cache_creation_input_tokens`).
*/
cacheWrite: number,
/**
* Of the cache write, the conversation from before written again because
* the cache had expired: the daemon's estimate, 0 in older turns.
*/
reloaded: number,
/**
* Input read back from the prompt cache (`cache_read_input_tokens`):
* mostly the conversation so far, and much cheaper than new input.
Expand Down
5 changes: 5 additions & 0 deletions crates/botloft-core/src/protocol/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ pub struct TokenUsage {
pub input: u64,
/// New input also written to the prompt cache (`cache_creation_input_tokens`).
pub cache_write: u64,
/// Of the cache write, the conversation from before written again because
/// the cache had expired: the daemon's estimate, 0 in older turns.
#[serde(default)]
pub reloaded: u64,
/// Input read back from the prompt cache (`cache_read_input_tokens`):
/// mostly the conversation so far, and much cheaper than new input.
pub cache_read: u64,
Expand All @@ -26,6 +30,7 @@ impl TokenUsage {
pub fn add(&mut self, other: &Self) {
self.input += other.input;
self.cache_write += other.cache_write;
self.reloaded += other.reloaded;
self.cache_read += other.cache_read;
self.output += other.output;
}
Expand Down
Loading
Loading