Files
openmaic/OpenMAIC/lib/agent/client/merge-assistant-parts.ts
2026-08-16 14:58:47 +08:00

67 lines
2.4 KiB
TypeScript

/**
* Chronological assembly of the assistant message from pi agent turns.
*
* A run produces multiple assistant turns (the tool-call turn, then a wrap-up
* turn). Each turn's content array is already ordered (text / toolCall parts
* interleaved as generated), and the runtime replaces a turn's array wholesale
* on every stream update. Flattening the turns in arrival order makes the
* rendered sequence match what actually happened: turn-1 text → tool call →
* wrap-up text BELOW the tool card (never above it). Tool results attach by
* toolCallId. Pure and side-effect-free so it can be unit-tested without React.
*/
export type PiPart =
| { type: 'text'; text: string }
| { type: 'reasoning'; text: string }
| { type: 'toolCall'; id: string; name: string; arguments: Record<string, unknown> };
export type AssistantPart =
| { type: 'text'; text: string }
| { type: 'reasoning'; text: string }
| {
type: 'tool-call';
toolCallId: string;
toolName: string;
args: Record<string, unknown>;
result?: unknown;
isError?: boolean;
};
export interface MergeInput {
/** Chronological assistant turns; each is that turn's ordered content. */
turns: PiPart[][];
/** Executed tool results keyed by toolCallId. */
toolResults: Map<string, { result: unknown; isError: boolean }>;
/** Run-level error (stream failure / turn errorMessage); appended as text. */
error: string;
}
export function mergeAssistantParts({ turns, toolResults, error }: MergeInput): AssistantPart[] {
const parts: AssistantPart[] = [];
for (const turn of turns) {
for (const p of turn) {
if (p.type === 'text') {
if (p.text) parts.push({ type: 'text', text: p.text });
} else if (p.type === 'reasoning') {
// Reasoning/thinking renders as assistant-ui's `reasoning` part (a
// collapsible panel), kept separate from the answer text. Empty drops.
if (p.text) parts.push({ type: 'reasoning', text: p.text });
} else {
const r = toolResults.get(p.id);
parts.push({
type: 'tool-call',
toolCallId: p.id,
toolName: p.name,
args: p.arguments,
...(r ? { result: r.result, isError: r.isError } : {}),
});
}
}
}
if (error) parts.push({ type: 'text', text: error });
if (parts.length === 0) parts.push({ type: 'text', text: '' });
return parts;
}