feat(ai): add shared contentText utility (#6840)

Co-authored-by: Armin Ronacher <armin.ronacher@active-4.com>
This commit is contained in:
Alexey Zaytsev
2026-07-19 20:50:36 -03:00
committed by GitHub
parent 75cb0b873a
commit 94373d815d
12 changed files with 92 additions and 111 deletions
@@ -24,11 +24,11 @@ import type {
PrepareNextTurnContext,
ThinkingLevel,
} from "@earendil-works/pi-agent-core";
import { contentText } from "@earendil-works/pi-ai";
import type {
AssistantMessage,
AuthResult,
ImageContent,
Message,
Model,
ProviderHeaders,
TextContent,
@@ -576,7 +576,7 @@ export class AgentSession {
// This ensures the UI sees the updated queue state
if (event.type === "message_start" && event.message.role === "user") {
this._overflowRecoveryAttempted = false;
const messageText = this._getUserMessageText(event.message);
const messageText = contentText(event.message.content, "");
if (messageText) {
// Check steering queue first
const steeringIndex = this._steeringMessages.indexOf(messageText);
@@ -659,15 +659,6 @@ export class AgentSession {
return false;
}
/** Extract text content from a message */
private _getUserMessageText(message: Message): string {
if (message.role !== "user") return "";
const content = message.content;
if (typeof content === "string") return content;
const textBlocks = content.filter((c) => c.type === "text");
return textBlocks.map((c) => (c as TextContent).text).join("");
}
/** Find the last assistant message in agent state (including aborted ones) */
private _findLastAssistantMessage(): AssistantMessage | undefined {
const messages = this.agent.state.messages;
@@ -2954,17 +2945,11 @@ export class AgentSession {
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
// User message: leaf = parent (null if root), text goes to editor
newLeafId = targetEntry.parentId;
editorText = this._extractUserMessageText(targetEntry.message.content);
editorText = contentText(targetEntry.message.content, "");
} else if (targetEntry.type === "custom_message") {
// Custom message: leaf = parent (null if root), text goes to editor
newLeafId = targetEntry.parentId;
editorText =
typeof targetEntry.content === "string"
? targetEntry.content
: targetEntry.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
editorText = contentText(targetEntry.content, "");
} else {
// Non-user message: leaf = selected node
newLeafId = targetId;
@@ -3032,7 +3017,7 @@ export class AgentSession {
if (entry.type !== "message") continue;
if (entry.message.role !== "user") continue;
const text = this._extractUserMessageText(entry.message.content);
const text = contentText(entry.message.content, "");
if (text) {
result.push({ entryId: entry.id, text });
}
@@ -3041,17 +3026,6 @@ export class AgentSession {
return result;
}
private _extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
return content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
}
return "";
}
/**
* Get session statistics. Aggregates over ALL session entries (including
* history that was compacted away), so token/cost totals reflect what was
@@ -6,6 +6,7 @@
*/
import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core";
import { contentText } from "@earendil-works/pi-ai";
import type { Model, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
import { completeSimple } from "@earendil-works/pi-ai/compat";
import {
@@ -351,10 +352,7 @@ export async function generateBranchSummary(
return { error: response.errorMessage || "Summarization failed" };
}
let summary = response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n");
let summary = contentText(response.content);
// Prepend preamble to provide context about the branch summary
summary = BRANCH_SUMMARY_PREAMBLE + summary;
@@ -6,6 +6,7 @@
*/
import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
import { contentText } from "@earendil-works/pi-ai";
import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai/compat";
import { completeSimple } from "@earendil-works/pi-ai/compat";
import { convertToLlm } from "../messages.ts";
@@ -600,10 +601,7 @@ export async function generateSummary(
throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
}
const textContent = response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n");
const textContent = contentText(response.content);
return textContent;
}
@@ -865,8 +863,5 @@ async function generateTurnPrefixSummary(
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
}
return response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n");
return contentText(response.content);
}
@@ -3,7 +3,7 @@
*/
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import type { Message } from "@earendil-works/pi-ai";
import { contentText, type Message } from "@earendil-works/pi-ai";
// ============================================================================
// File Operation Tracking
@@ -111,23 +111,14 @@ export function serializeConversation(messages: Message[]): string {
for (const msg of messages) {
if (msg.role === "user") {
const content =
typeof msg.content === "string"
? msg.content
: msg.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
const content = contentText(msg.content, "");
if (content) parts.push(`[User]: ${content}`);
} else if (msg.role === "assistant") {
const textParts: string[] = [];
const thinkingParts: string[] = [];
const toolCalls: string[] = [];
for (const block of msg.content) {
if (block.type === "text") {
textParts.push(block.text);
} else if (block.type === "thinking") {
if (block.type === "thinking") {
thinkingParts.push(block.thinking);
} else if (block.type === "toolCall") {
const args = block.arguments as Record<string, unknown>;
@@ -141,17 +132,14 @@ export function serializeConversation(messages: Message[]): string {
if (thinkingParts.length > 0) {
parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
}
if (textParts.length > 0) {
parts.push(`[Assistant]: ${textParts.join("\n")}`);
if (msg.content.some((block) => block.type === "text")) {
parts.push(`[Assistant]: ${contentText(msg.content)}`);
}
if (toolCalls.length > 0) {
parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
}
} else if (msg.role === "toolResult") {
const content = msg.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
const content = contentText(msg.content, "");
if (content) {
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
}