Clarify Claude transcript event categories
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
c610192c53
commit
a3e125f796
2 changed files with 198 additions and 1 deletions
|
|
@ -17,6 +17,27 @@ function asErrorText(value: unknown): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printToolResult(block: Record<string, unknown>): void {
|
||||||
|
const isError = block.is_error === true;
|
||||||
|
let text = "";
|
||||||
|
if (typeof block.content === "string") {
|
||||||
|
text = block.content;
|
||||||
|
} else if (Array.isArray(block.content)) {
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const part of block.content) {
|
||||||
|
if (typeof part !== "object" || part === null || Array.isArray(part)) continue;
|
||||||
|
const record = part as Record<string, unknown>;
|
||||||
|
if (typeof record.text === "string") parts.push(record.text);
|
||||||
|
}
|
||||||
|
text = parts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log((isError ? pc.red : pc.cyan)(`tool_result${isError ? " (error)" : ""}`));
|
||||||
|
if (text) {
|
||||||
|
console.log((isError ? pc.red : pc.gray)(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function printClaudeStreamEvent(raw: string, debug: boolean): void {
|
export function printClaudeStreamEvent(raw: string, debug: boolean): void {
|
||||||
const line = raw.trim();
|
const line = raw.trim();
|
||||||
if (!line) return;
|
if (!line) return;
|
||||||
|
|
@ -51,6 +72,9 @@ export function printClaudeStreamEvent(raw: string, debug: boolean): void {
|
||||||
if (blockType === "text") {
|
if (blockType === "text") {
|
||||||
const text = typeof block.text === "string" ? block.text : "";
|
const text = typeof block.text === "string" ? block.text : "";
|
||||||
if (text) console.log(pc.green(`assistant: ${text}`));
|
if (text) console.log(pc.green(`assistant: ${text}`));
|
||||||
|
} else if (blockType === "thinking") {
|
||||||
|
const text = typeof block.thinking === "string" ? block.thinking : "";
|
||||||
|
if (text) console.log(pc.gray(`thinking: ${text}`));
|
||||||
} else if (blockType === "tool_use") {
|
} else if (blockType === "tool_use") {
|
||||||
const name = typeof block.name === "string" ? block.name : "unknown";
|
const name = typeof block.name === "string" ? block.name : "unknown";
|
||||||
console.log(pc.yellow(`tool_call: ${name}`));
|
console.log(pc.yellow(`tool_call: ${name}`));
|
||||||
|
|
@ -62,6 +86,22 @@ export function printClaudeStreamEvent(raw: string, debug: boolean): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "user") {
|
||||||
|
const message =
|
||||||
|
typeof parsed.message === "object" && parsed.message !== null && !Array.isArray(parsed.message)
|
||||||
|
? (parsed.message as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
const content = Array.isArray(message.content) ? message.content : [];
|
||||||
|
for (const blockRaw of content) {
|
||||||
|
if (typeof blockRaw !== "object" || blockRaw === null || Array.isArray(blockRaw)) continue;
|
||||||
|
const block = blockRaw as Record<string, unknown>;
|
||||||
|
if (typeof block.type === "string" && block.type === "tool_result") {
|
||||||
|
printToolResult(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "result") {
|
if (type === "result") {
|
||||||
const usage =
|
const usage =
|
||||||
typeof parsed.usage === "object" && parsed.usage !== null && !Array.isArray(parsed.usage)
|
typeof parsed.usage === "object" && parsed.usage !== null && !Array.isArray(parsed.usage)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { isClaudeMaxTurnsResult } from "@paperclipai/adapter-claude-local/server";
|
import { isClaudeMaxTurnsResult } from "@paperclipai/adapter-claude-local/server";
|
||||||
|
import { parseClaudeStdoutLine } from "@paperclipai/adapter-claude-local/ui";
|
||||||
|
import { printClaudeStreamEvent } from "@paperclipai/adapter-claude-local/cli";
|
||||||
|
|
||||||
describe("claude_local max-turn detection", () => {
|
describe("claude_local max-turn detection", () => {
|
||||||
it("detects max-turn exhaustion by subtype", () => {
|
it("detects max-turn exhaustion by subtype", () => {
|
||||||
|
|
@ -28,3 +30,158 @@ describe("claude_local max-turn detection", () => {
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("claude_local ui stdout parser", () => {
|
||||||
|
it("maps assistant text, thinking, tool calls, and tool results into transcript entries", () => {
|
||||||
|
const ts = "2026-03-29T00:00:00.000Z";
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseClaudeStdoutLine(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "system",
|
||||||
|
subtype: "init",
|
||||||
|
model: "claude-sonnet-4-6",
|
||||||
|
session_id: "claude-session-1",
|
||||||
|
}),
|
||||||
|
ts,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "init",
|
||||||
|
ts,
|
||||||
|
model: "claude-sonnet-4-6",
|
||||||
|
sessionId: "claude-session-1",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseClaudeStdoutLine(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "assistant",
|
||||||
|
session_id: "claude-session-1",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "I will inspect the repo." },
|
||||||
|
{ type: "thinking", thinking: "Checking the adapter wiring" },
|
||||||
|
{ type: "tool_use", id: "tool_1", name: "bash", input: { command: "ls -1" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ts,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{ kind: "assistant", ts, text: "I will inspect the repo." },
|
||||||
|
{ kind: "thinking", ts, text: "Checking the adapter wiring" },
|
||||||
|
{ kind: "tool_call", ts, name: "bash", toolUseId: "tool_1", input: { command: "ls -1" } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseClaudeStdoutLine(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_result",
|
||||||
|
tool_use_id: "tool_1",
|
||||||
|
content: [{ type: "text", text: "AGENTS.md\nREADME.md" }],
|
||||||
|
is_error: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ts,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "tool_result",
|
||||||
|
ts,
|
||||||
|
toolUseId: "tool_1",
|
||||||
|
content: "AGENTS.md\nREADME.md",
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function stripAnsi(value: string) {
|
||||||
|
return value.replace(/\x1b\[[0-9;]*m/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("claude_local cli formatter", () => {
|
||||||
|
it("prints the user-visible and background transcript events from stream-json output", () => {
|
||||||
|
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
printClaudeStreamEvent(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "system",
|
||||||
|
subtype: "init",
|
||||||
|
model: "claude-sonnet-4-6",
|
||||||
|
session_id: "claude-session-1",
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
printClaudeStreamEvent(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "I will inspect the repo." },
|
||||||
|
{ type: "thinking", thinking: "Checking the adapter wiring" },
|
||||||
|
{ type: "tool_use", id: "tool_1", name: "bash", input: { command: "ls -1" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
printClaudeStreamEvent(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_result",
|
||||||
|
tool_use_id: "tool_1",
|
||||||
|
content: [{ type: "text", text: "AGENTS.md\nREADME.md" }],
|
||||||
|
is_error: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
printClaudeStreamEvent(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "result",
|
||||||
|
subtype: "success",
|
||||||
|
result: "Done",
|
||||||
|
usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 2 },
|
||||||
|
total_cost_usd: 0.00042,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = spy.mock.calls
|
||||||
|
.map((call) => call.map((value) => String(value)).join(" "))
|
||||||
|
.map(stripAnsi);
|
||||||
|
|
||||||
|
expect(lines).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"Claude initialized (model: claude-sonnet-4-6, session: claude-session-1)",
|
||||||
|
"assistant: I will inspect the repo.",
|
||||||
|
"thinking: Checking the adapter wiring",
|
||||||
|
"tool_call: bash",
|
||||||
|
'{\n "command": "ls -1"\n}',
|
||||||
|
"tool_result",
|
||||||
|
"AGENTS.md\nREADME.md",
|
||||||
|
"result:",
|
||||||
|
"Done",
|
||||||
|
"tokens: in=10 out=5 cached=2 cost=$0.000420",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
spy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue