my harness for niri
1import OpenAI from "openai"
2import type { LoopState } from "./types.js"
3import type { FunctionToolCall } from "./loop-shared.js"
4
5/**
6 * Normalizes assistant/tool message content into plain text.
7 *
8 * @param content - Raw OpenAI-compatible message content.
9 * @returns Trimmed text representation.
10 */
11export function assistantContentText(content: unknown): string {
12 if (typeof content === "string") return content.trim()
13 if (!Array.isArray(content)) return ""
14
15 let combined = ""
16 for (const part of content) {
17 if (!part || typeof part !== "object") continue
18 const record = part as Record<string, unknown>
19
20 if (typeof record.text === "string") {
21 combined += record.text
22 continue
23 }
24 if (
25 record.text &&
26 typeof record.text === "object" &&
27 "value" in record.text &&
28 typeof (record.text as { value?: unknown }).value === "string"
29 ) {
30 combined += (record.text as { value: string }).value
31 }
32 }
33
34 return combined.trim()
35}
36
37/**
38 * Returns the most recent assistant message text from loop state.
39 *
40 * @param state - Mutable loop state.
41 * @returns Latest assistant text, or empty string when none exists.
42 */
43export function latestAssistantContent(state: LoopState): string {
44 for (let i = state.conversation.length - 1; i >= 0; i--) {
45 const message = state.conversation[i]
46 if (!message || message.role !== "assistant") continue
47 return assistantContentText(message.content)
48 }
49 return ""
50}
51
52/**
53 * Type guard that narrows a tool call to function-call shape.
54 *
55 * @param call - Raw tool call from the assistant response.
56 * @returns `true` when the call is a function tool call.
57 */
58export function isFunctionToolCall(call: OpenAI.Chat.ChatCompletionMessageToolCall): call is FunctionToolCall {
59 return call.type === "function"
60}