mirror of
https://github.com/earendil-works/pi.git
synced 2026-10-20 05:59:17 +08:00
Codemode sums the usage of models.classify() calls into its tool result and shows each call's cost. Usage of tools called through ctx.executeTool() is added to the calling tool's result usage instead of being dropped.
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
import { stripVTControlCharacters } from "node:util";
|
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
import type { Text } from "@earendil-works/pi-tui";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
import type { ToolRenderContext } from "../src/core/extensions/types.ts";
|
|
import { codemodeRenderers } from "../src/extensions/codemode/renderer.ts";
|
|
import type { CodemodeToolDetails } from "../src/extensions/codemode/tool.ts";
|
|
import { initTheme, theme } from "../src/modes/interactive/theme/theme.ts";
|
|
|
|
function render(result: AgentToolResult<CodemodeToolDetails | undefined>, isError = false): string {
|
|
const context = {
|
|
args: { code: "" },
|
|
toolCallId: "call",
|
|
invalidate: () => {},
|
|
lastComponent: undefined,
|
|
state: {},
|
|
cwd: "/",
|
|
executionStarted: true,
|
|
argsComplete: true,
|
|
isPartial: false,
|
|
expanded: true,
|
|
showImages: false,
|
|
isError,
|
|
} satisfies ToolRenderContext;
|
|
const component = codemodeRenderers.renderResult?.(
|
|
result,
|
|
{ expanded: true, isPartial: false },
|
|
theme,
|
|
context,
|
|
) as Text;
|
|
return stripVTControlCharacters(component.render(200).join("\n"))
|
|
.split("\n")
|
|
.map((line) => line.trimEnd())
|
|
.join("\n")
|
|
.trim();
|
|
}
|
|
|
|
describe("codemode renderer", () => {
|
|
beforeAll(() => initTheme("dark"));
|
|
|
|
it("hides the script header and shows the output", () => {
|
|
const text = render({
|
|
content: [
|
|
{ type: "text", text: "Script completed\nWall time 0.1 seconds\nOutput:\n" },
|
|
{ type: "text", text: "hello" },
|
|
],
|
|
details: { calls: [{ id: "call/1", name: "read", args: '{"path":"a"}', status: "ok", durationMs: 5 }] },
|
|
});
|
|
expect(text).toBe('✓ read {"path":"a"} 5ms\n\nhello');
|
|
});
|
|
|
|
it("shows the cost of model calls and their total", () => {
|
|
const call = { name: "models.classify", args: "scorer/judge", status: "ok" as const, durationMs: 5 };
|
|
const text = render({
|
|
content: [{ type: "text", text: "Script completed\nWall time 0.1 seconds\nOutput:\n" }],
|
|
details: {
|
|
calls: [
|
|
{ ...call, id: "call/models.classify/1", cost: 0.000012936 },
|
|
{ ...call, id: "call/models.classify/2", cost: 0.02 },
|
|
{ ...call, id: "call/models.classify/3" },
|
|
],
|
|
},
|
|
});
|
|
expect(text).toBe(
|
|
[
|
|
"✓ models.classify scorer/judge 5ms $0.000013",
|
|
"✓ models.classify scorer/judge 5ms $0.02",
|
|
"✓ models.classify scorer/judge 5ms",
|
|
"Model calls: $0.02",
|
|
].join("\n"),
|
|
);
|
|
});
|
|
|
|
it("shows results without a header, such as rejected options", () => {
|
|
const text = render(
|
|
{
|
|
content: [{ type: "text", text: "The @options line must be followed by JavaScript source" }],
|
|
details: undefined,
|
|
},
|
|
true,
|
|
);
|
|
expect(text).toBe("The @options line must be followed by JavaScript source");
|
|
});
|
|
});
|