Pi sometimes sends tool results as a direct array [{"type":"text","text":"..."}]
rather than wrapped in {"content": [...]}. Now handles both formats to
properly extract text content instead of showing raw JSON.
180 lines
5.2 KiB
TypeScript
180 lines
5.2 KiB
TypeScript
import type { TranscriptEntry } from "@paperclipai/adapter-utils";
|
|
|
|
function safeJsonParse(text: string): unknown {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function asString(value: unknown, fallback = ""): string {
|
|
return typeof value === "string" ? value : fallback;
|
|
}
|
|
|
|
function extractTextContent(content: string | Array<{ type: string; text?: string }>): string {
|
|
if (typeof content === "string") return content;
|
|
if (!Array.isArray(content)) return "";
|
|
return content
|
|
.filter((c) => c.type === "text" && c.text)
|
|
.map((c) => c.text!)
|
|
.join("");
|
|
}
|
|
|
|
export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] {
|
|
const parsed = asRecord(safeJsonParse(line));
|
|
if (!parsed) {
|
|
return [{ kind: "stdout", ts, text: line }];
|
|
}
|
|
|
|
const type = asString(parsed.type);
|
|
|
|
// RPC protocol messages - filter these out (internal implementation detail)
|
|
if (type === "response" || type === "extension_ui_request" || type === "extension_ui_response" || type === "extension_error") {
|
|
return [];
|
|
}
|
|
|
|
// Agent lifecycle
|
|
if (type === "agent_start") {
|
|
return [{ kind: "system", ts, text: "Pi agent started" }];
|
|
}
|
|
|
|
if (type === "agent_end") {
|
|
return [{ kind: "system", ts, text: "Pi agent finished" }];
|
|
}
|
|
|
|
// Turn lifecycle
|
|
if (type === "turn_start") {
|
|
return [{ kind: "system", ts, text: "Turn started" }];
|
|
}
|
|
|
|
if (type === "turn_end") {
|
|
const message = asRecord(parsed.message);
|
|
const toolResults = parsed.toolResults as Array<Record<string, unknown>> | undefined;
|
|
|
|
const entries: TranscriptEntry[] = [];
|
|
|
|
if (message) {
|
|
const content = message.content as string | Array<{ type: string; text?: string }>;
|
|
const text = extractTextContent(content);
|
|
if (text) {
|
|
entries.push({ kind: "assistant", ts, text });
|
|
}
|
|
}
|
|
|
|
// Process tool results
|
|
if (toolResults) {
|
|
for (const tr of toolResults) {
|
|
const content = tr.content;
|
|
const isError = tr.isError === true;
|
|
|
|
// Extract text from Pi's content array format
|
|
let contentStr: string;
|
|
if (typeof content === "string") {
|
|
contentStr = content;
|
|
} else if (Array.isArray(content)) {
|
|
contentStr = extractTextContent(content as Array<{ type: string; text?: string }>);
|
|
} else {
|
|
contentStr = JSON.stringify(content);
|
|
}
|
|
|
|
entries.push({
|
|
kind: "tool_result",
|
|
ts,
|
|
toolUseId: asString(tr.toolCallId, "unknown"),
|
|
toolName: asString(tr.toolName),
|
|
content: contentStr,
|
|
isError,
|
|
});
|
|
}
|
|
}
|
|
|
|
return entries.length > 0 ? entries : [{ kind: "system", ts, text: "Turn ended" }];
|
|
}
|
|
|
|
// Message streaming
|
|
if (type === "message_start") {
|
|
return [];
|
|
}
|
|
|
|
if (type === "message_update") {
|
|
const assistantEvent = asRecord(parsed.assistantMessageEvent);
|
|
if (assistantEvent) {
|
|
const msgType = asString(assistantEvent.type);
|
|
if (msgType === "text_delta") {
|
|
const delta = asString(assistantEvent.delta);
|
|
if (delta) {
|
|
return [{ kind: "assistant", ts, text: delta, delta: true }];
|
|
}
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
if (type === "message_end") {
|
|
return [];
|
|
}
|
|
|
|
// Tool execution
|
|
if (type === "tool_execution_start") {
|
|
const toolName = asString(parsed.toolName);
|
|
const args = parsed.args;
|
|
if (toolName) {
|
|
return [{
|
|
kind: "tool_call",
|
|
ts,
|
|
name: toolName,
|
|
input: args,
|
|
}];
|
|
}
|
|
return [{ kind: "system", ts, text: `Tool started` }];
|
|
}
|
|
|
|
if (type === "tool_execution_update") {
|
|
return [];
|
|
}
|
|
|
|
if (type === "tool_execution_end") {
|
|
const toolCallId = asString(parsed.toolCallId);
|
|
const toolName = asString(parsed.toolName);
|
|
const result = parsed.result;
|
|
const isError = parsed.isError === true;
|
|
|
|
// Extract text from Pi's content array format
|
|
// Can be: {"content": [{"type": "text", "text": "..."}]} or [{"type": "text", "text": "..."}]
|
|
let contentStr: string;
|
|
if (typeof result === "string") {
|
|
contentStr = result;
|
|
} else if (Array.isArray(result)) {
|
|
// Direct array format: result is [{"type": "text", "text": "..."}]
|
|
contentStr = extractTextContent(result as Array<{ type: string; text?: string }>);
|
|
} else if (result && typeof result === "object") {
|
|
const resultObj = result as Record<string, unknown>;
|
|
if (Array.isArray(resultObj.content)) {
|
|
// Wrapped format: result is {"content": [{"type": "text", "text": "..."}]}
|
|
contentStr = extractTextContent(resultObj.content as Array<{ type: string; text?: string }>);
|
|
} else {
|
|
contentStr = JSON.stringify(result);
|
|
}
|
|
} else {
|
|
contentStr = JSON.stringify(result);
|
|
}
|
|
|
|
return [{
|
|
kind: "tool_result",
|
|
ts,
|
|
toolUseId: toolCallId || "unknown",
|
|
toolName,
|
|
content: contentStr,
|
|
isError,
|
|
}];
|
|
}
|
|
|
|
return [{ kind: "stdout", ts, text: line }];
|
|
}
|