murashit_codex-panel/src/domain/threads/reference.ts
2026-05-29 07:14:04 +09:00

148 lines
5 KiB
TypeScript

import type { Thread } from "../../generated/app-server/v2/Thread";
import type { ThreadItem } from "../../generated/app-server/v2/ThreadItem";
import type { Turn } from "../../generated/app-server/v2/Turn";
import type { UserInput } from "../../generated/app-server/v2/UserInput";
import { inputToText, shortThreadId } from "../../utils";
import { getThreadTitle } from "./model";
export const REFERENCED_THREAD_TURN_LIMIT = 20;
export interface ReferencedThreadDisplay {
threadId: string;
title: string;
includedTurns: number;
turnLimit: number;
}
export interface ReferencedThreadTurn {
userText: string | null;
assistantText: string | null;
}
export interface ReferencedThreadInput {
input: UserInput[];
referencedThread: ReferencedThreadDisplay;
status: string;
}
export function referencedThreadTurns(turns: Turn[]): ReferencedThreadTurn[] {
return [...turns]
.sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0))
.map((turn) => ({
userText: firstUserMessage(turn.items),
assistantText: lastAssistantMessage(turn.items),
}))
.filter((turn) => turn.userText !== null || turn.assistantText !== null);
}
export function referencedThreadPrompt(thread: Thread, turns: ReferencedThreadTurn[], userRequest: string): string {
const title = getThreadTitle(thread);
const heading = [
"[Codex Panel referenced thread]",
`Title: ${title}`,
`Thread ID: ${thread.id}`,
`Included turns: ${String(turns.length)}/${String(REFERENCED_THREAD_TURN_LIMIT)}`,
"Included history: user input and final Codex responses only.",
];
return [
...heading,
"",
"Reference thread history:",
...turns.flatMap((turn, index) => {
const lines = [`Turn ${String(index + 1)}:`];
if (turn.userText) lines.push(`User:\n${turn.userText}`);
if (turn.assistantText) lines.push(`Codex:\n${turn.assistantText}`);
return ["", ...lines];
}),
"",
"[/Codex Panel referenced thread]",
"",
"Current user request:",
userRequest,
].join("\n");
}
export function referencedThreadStatus(thread: Thread, count: number): string {
return `Referencing ${shortThreadId(thread.id)} (${String(count)}/${String(REFERENCED_THREAD_TURN_LIMIT)} turns).`;
}
export function referencedThreadDisplay(thread: Thread, count: number): ReferencedThreadDisplay {
return {
threadId: thread.id,
title: getThreadTitle(thread),
includedTurns: count,
turnLimit: REFERENCED_THREAD_TURN_LIMIT,
};
}
export function referencedThreadInput(
thread: Thread,
turns: readonly ReferencedThreadTurn[],
userRequest: string,
messageInput: UserInput[],
): ReferencedThreadInput {
const prompt = referencedThreadPrompt(thread, [...turns], userRequest);
return {
input: [{ type: "text", text: prompt, text_elements: [] }, ...messageInput.filter((item) => item.type !== "text")],
referencedThread: referencedThreadDisplay(thread, turns.length),
status: referencedThreadStatus(thread, turns.length),
};
}
export function referencedThreadDisplayFromPrompt(text: string): { text: string; reference: ReferencedThreadDisplay } | null {
const headerStart = text.indexOf("[Codex Panel referenced thread]");
const headerEnd = text.indexOf("[/Codex Panel referenced thread]");
const requestMarker = "\nCurrent user request:\n";
const requestStart = text.indexOf(requestMarker);
if (headerStart !== 0 || headerEnd === -1 || requestStart === -1 || requestStart < headerEnd) return null;
const header = text.slice(headerStart, headerEnd);
const title = lineValue(header, "Title") ?? "Referenced thread";
const threadId = lineValue(header, "Thread ID") ?? "";
const included = lineValue(header, "Included turns") ?? "";
const turnsMatch = /^(\d+)\/(\d+)$/.exec(included);
const includedTurns = turnsMatch?.[1] ? Number.parseInt(turnsMatch[1], 10) : REFERENCED_THREAD_TURN_LIMIT;
const turnLimit = turnsMatch?.[2] ? Number.parseInt(turnsMatch[2], 10) : REFERENCED_THREAD_TURN_LIMIT;
const visibleText = text.slice(requestStart + requestMarker.length).trim();
if (!visibleText || !threadId) return null;
return {
text: visibleText,
reference: {
threadId,
title,
includedTurns,
turnLimit,
},
};
}
function lineValue(text: string, key: string): string | null {
const prefix = `${key}:`;
const line = text
.split(/\r?\n/)
.find((candidate) => candidate.startsWith(prefix))
?.slice(prefix.length)
.trim();
return line && line.length > 0 ? line : null;
}
function firstUserMessage(items: ThreadItem[]): string | null {
for (const item of items) {
if (item.type !== "userMessage") continue;
const text = inputToText(item.content).trim();
if (text) return text;
}
return null;
}
function lastAssistantMessage(items: ThreadItem[]): string | null {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (item === undefined) continue;
if (item.type !== "agentMessage" && item.type !== "plan") continue;
const text = item.text.trim();
if (text) return text;
}
return null;
}