Rename turn transcript summaries

This commit is contained in:
murashit 2026-07-09 08:04:46 +09:00
parent 682a23d712
commit 2debad388b
24 changed files with 121 additions and 105 deletions

View file

@ -1,8 +1,8 @@
import {
conversationSummaryFromTranscriptEntries,
nonEmptyConversationSummaries,
type ThreadConversationSummary,
nonEmptyTurnTranscriptSummaries,
type ThreadTranscriptEntry,
type TurnTranscriptSummary,
turnTranscriptSummaryFromTranscriptEntries,
} from "../../domain/threads/transcript";
import type { ThreadItem as GeneratedThreadItem } from "../../generated/app-server/v2/ThreadItem";
import type { Turn as GeneratedTurn } from "../../generated/app-server/v2/Turn";
@ -21,30 +21,30 @@ export function transcriptEntriesFromTurnRecords(turns: readonly TurnRecord[]):
return turns.flatMap(transcriptEntriesFromTurnRecord);
}
function conversationSummaryFromTurnRecord(turn: TurnRecord): ThreadConversationSummary {
return conversationSummaryFromTranscriptEntries(transcriptEntriesFromTurnRecord(turn));
function turnTranscriptSummaryFromTurnRecord(turn: TurnRecord): TurnTranscriptSummary {
return turnTranscriptSummaryFromTranscriptEntries(transcriptEntriesFromTurnRecord(turn));
}
export function conversationAssistantTextFromTurnRecord(turn: TurnRecord): string | null {
return conversationSummaryFromTurnRecord(turn).assistantText;
export function turnTranscriptAssistantTextFromTurnRecord(turn: TurnRecord): string | null {
return turnTranscriptSummaryFromTurnRecord(turn).assistantText;
}
export function completedConversationSummaryFromTurnRecord(turn: TurnRecord): ThreadConversationSummary | null {
export function completedTurnTranscriptSummaryFromTurnRecord(turn: TurnRecord): TurnTranscriptSummary | null {
if (turn.status !== "completed") return null;
const summary = conversationSummaryFromTurnRecord(turn);
const summary = turnTranscriptSummaryFromTurnRecord(turn);
return summary.userText && summary.assistantText ? summary : null;
}
export function completedConversationSummariesFromTurnRecords(turns: readonly TurnRecord[]): ThreadConversationSummary[] {
export function completedTurnTranscriptSummariesFromTurnRecords(turns: readonly TurnRecord[]): TurnTranscriptSummary[] {
return turns.flatMap((turn) => {
const summary = completedConversationSummaryFromTurnRecord(turn);
const summary = completedTurnTranscriptSummaryFromTurnRecord(turn);
return summary ? [summary] : [];
});
}
export function chronologicalConversationSummariesFromTurnRecords(turns: readonly TurnRecord[]): ThreadConversationSummary[] {
return nonEmptyConversationSummaries(
[...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0)).map(conversationSummaryFromTurnRecord),
export function chronologicalTurnTranscriptSummariesFromTurnRecords(turns: readonly TurnRecord[]): TurnTranscriptSummary[] {
return nonEmptyTurnTranscriptSummaries(
[...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0)).map(turnTranscriptSummaryFromTurnRecord),
);
}

View file

@ -5,7 +5,7 @@ import {
threadTitleFromGeneratedText,
threadTitlePrompt,
} from "../../domain/threads/title-generation-model";
import { conversationAssistantTextFromTurnRecord } from "../protocol/turn";
import { turnTranscriptAssistantTextFromTurnRecord } from "../protocol/turn";
import {
type EphemeralStructuredTurnClientFactory,
runEphemeralStructuredTurn,
@ -63,6 +63,6 @@ export async function generateThreadTitleWithCodex(
resolvedRuntimeOverrideForClient(client, { model: runtimeSettings.threadNamingModel, effort: runtimeSettings.threadNamingEffort }),
clientFactory,
});
const response = conversationAssistantTextFromTurnRecord(turn);
const response = turnTranscriptAssistantTextFromTurnRecord(turn);
return response ? threadTitleFromGeneratedText(response) : null;
}

View file

@ -9,15 +9,15 @@ import type { ThreadGoal, ThreadGoalUpdate } from "../../domain/threads/goal";
import type { HistoricalTurn } from "../../domain/threads/history";
import type { Thread } from "../../domain/threads/model";
import { REFERENCED_THREAD_TURN_LIMIT } from "../../domain/threads/reference";
import type { ThreadConversationSummary } from "../../domain/threads/transcript";
import type { TurnTranscriptSummary } from "../../domain/threads/transcript";
import type { ClientResponseByMethod } from "../connection/client";
import type { ClientRequestParams } from "../connection/rpc-messages";
import { type ThreadRecord, threadFromThreadRecord, threadsFromThreadRecords } from "../protocol/thread";
import { appServerThreadGoalUpdate, appServerThreadGoalUserHistoryItem, threadGoalFromAppServerGoal } from "../protocol/thread-goal";
import { appServerRuntimeSettingsPatch } from "../protocol/thread-settings";
import {
chronologicalConversationSummariesFromTurnRecords,
completedConversationSummariesFromTurnRecords,
chronologicalTurnTranscriptSummariesFromTurnRecords,
completedTurnTranscriptSummariesFromTurnRecords,
transcriptEntriesFromTurnRecords,
} from "../protocol/turn";
import type { AppServerRequestClient } from "./request-client";
@ -25,13 +25,13 @@ import type { AppServerRequestClient } from "./request-client";
const THREAD_LIST_PAGE_LIMIT = 100;
export type ThreadTurnSortDirection = "asc" | "desc";
export type ThreadConversationSummaryClient = AppServerRequestClient;
export type TurnTranscriptSummaryClient = AppServerRequestClient;
export type ThreadForkClient = AppServerRequestClient;
export type ThreadRollbackClient = AppServerRequestClient;
export type ThreadCompactionClient = AppServerRequestClient;
interface ThreadConversationSummaryPage {
summaries: ThreadConversationSummary[];
interface TurnTranscriptSummaryPage {
summaries: TurnTranscriptSummary[];
nextCursor: string | null;
}
@ -142,27 +142,27 @@ export async function readThreadForArchiveExport(client: AppServerRequestClient,
};
}
export async function readCompletedConversationSummariesPage(
client: ThreadConversationSummaryClient,
export async function readCompletedTurnTranscriptSummariesPage(
client: TurnTranscriptSummaryClient,
threadId: string,
cursor: string | null,
limit: number,
sortDirection: ThreadTurnSortDirection = "asc",
): Promise<ThreadConversationSummaryPage> {
): Promise<TurnTranscriptSummaryPage> {
const response = await listThreadTurns(client, threadId, cursor, limit, sortDirection);
return {
summaries: completedConversationSummariesFromTurnRecords(response.data),
summaries: completedTurnTranscriptSummariesFromTurnRecords(response.data),
nextCursor: response.nextCursor,
};
}
export async function readReferencedThreadConversationSummaries(
client: ThreadConversationSummaryClient,
export async function readReferencedThreadTurnTranscriptSummaries(
client: TurnTranscriptSummaryClient,
threadId: string,
limit = REFERENCED_THREAD_TURN_LIMIT,
): Promise<ThreadConversationSummary[]> {
): Promise<TurnTranscriptSummary[]> {
const response = await listThreadTurns(client, threadId, null, limit);
return chronologicalConversationSummariesFromTurnRecords(response.data);
return chronologicalTurnTranscriptSummariesFromTurnRecords(response.data);
}
export interface ThreadRollbackSnapshot {

View file

@ -1,6 +1,6 @@
import type { Thread } from "./model";
import { threadDisplayTitle } from "./title";
import type { ThreadConversationSummary } from "./transcript";
import type { TurnTranscriptSummary } from "./transcript";
export const REFERENCED_THREAD_TURN_LIMIT = 20;
@ -22,7 +22,7 @@ export interface ReferencedThreadPromptBundle {
referencedThread: ReferencedThreadMetadata;
}
function referencedThreadPrompt(thread: Thread, turns: readonly ThreadConversationSummary[], userRequest: string): string {
function referencedThreadPrompt(thread: Thread, turns: readonly TurnTranscriptSummary[], userRequest: string): string {
const reference = referencedThreadMetadata(thread, turns.length);
const envelope = referencedThreadEnvelope(reference, userRequest);
@ -56,7 +56,7 @@ function referencedThreadMetadata(thread: Thread, count: number): ReferencedThre
export function referencedThreadPromptBundle(
thread: Thread,
turns: readonly ThreadConversationSummary[],
turns: readonly TurnTranscriptSummary[],
userRequest: string,
): ReferencedThreadPromptBundle {
const prompt = referencedThreadPrompt(thread, [...turns], userRequest);

View file

@ -1,5 +1,5 @@
import { truncate } from "../display/text-preview";
import type { ThreadConversationSummary } from "./transcript";
import type { TurnTranscriptSummary } from "./transcript";
const THREAD_TITLE_CONTEXT_MAX_CHARS = 4_000;
const DEFAULT_CONTEXT_PAGE_LIMIT = 20;
@ -15,7 +15,7 @@ export interface ThreadTitleContext {
}
interface ThreadTitleContextPage {
summaries: ThreadConversationSummary[];
summaries: TurnTranscriptSummary[];
nextCursor: string | null;
}
@ -26,7 +26,7 @@ export type ThreadTitleContextPageReader = (
sortDirection: "asc" | "desc",
) => Promise<ThreadTitleContextPage>;
export function threadTitleContextFromConversationSummary(summary: ThreadConversationSummary): ThreadTitleContext | null {
export function threadTitleContextFromTurnTranscriptSummary(summary: TurnTranscriptSummary): ThreadTitleContext | null {
if (!summary.userText || !summary.assistantText) return null;
return {
@ -48,7 +48,7 @@ export async function findThreadTitleContext(options: {
for (let page = 0; page < maxPages; page += 1) {
const response = await options.readTurns(options.threadId, cursor, pageLimit, "asc");
for (const summary of response.summaries) {
const context = threadTitleContextFromConversationSummary(summary);
const context = threadTitleContextFromTurnTranscriptSummary(summary);
if (context) return context;
}
if (!response.nextCursor) break;

View file

@ -6,19 +6,19 @@ export interface ThreadTranscriptEntry {
timestamp: number | null;
}
export interface ThreadConversationSummary {
export interface TurnTranscriptSummary {
userText: string | null;
assistantText: string | null;
}
export function conversationSummaryFromTranscriptEntries(entries: readonly ThreadTranscriptEntry[]): ThreadConversationSummary {
export function turnTranscriptSummaryFromTranscriptEntries(entries: readonly ThreadTranscriptEntry[]): TurnTranscriptSummary {
return {
userText: firstTranscriptText(entries, (entry) => entry.kind === "user"),
assistantText: lastTranscriptText(entries, (entry) => entry.kind === "assistant" || entry.kind === "plan"),
};
}
export function nonEmptyConversationSummaries(summaries: readonly ThreadConversationSummary[]): ThreadConversationSummary[] {
export function nonEmptyTurnTranscriptSummaries(summaries: readonly TurnTranscriptSummary[]): TurnTranscriptSummary[] {
return summaries.filter((summary) => summary.userText !== null || summary.assistantText !== null);
}

View file

@ -13,7 +13,7 @@ import {
type PendingRequestId,
type PendingUserInput,
} from "../../../../domain/pending-requests/model";
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
import type { TurnTranscriptSummary } from "../../../../domain/threads/transcript";
import type { ThreadCatalogEvent } from "../../../threads/catalog/thread-catalog";
import type { AppServerResourceEvent } from "../../application/connection/server-metadata-actions";
import type { LocalIdSource } from "../../application/local-id-source";
@ -34,7 +34,7 @@ export interface ChatInboundHandlerActions {
refreshActiveThreads: () => void;
refreshServerDiagnostics: (options?: { forceResourceProbes?: boolean }) => void;
applyAppServerResourceEvent: (event: AppServerResourceEvent) => void;
maybeNameThread: (threadId: string, turnId: string, completedSummary: ThreadConversationSummary | null) => void;
maybeNameThread: (threadId: string, turnId: string, completedTurnTranscriptSummary: TurnTranscriptSummary | null) => void;
applyThreadCatalogEvent: (event: ThreadCatalogEvent) => void;
respondToServerRequest: (requestId: RequestId, result: unknown) => boolean;
rejectServerRequest: (requestId: RequestId, code: number, message: string) => boolean;
@ -250,7 +250,7 @@ function runNotificationEffect(context: ChatInboundHandlerContext, effect: ChatN
context.actions.applyAppServerResourceEvent(effect.event);
return;
case "maybe-name-thread":
context.actions.maybeNameThread(effect.threadId, effect.turnId, effect.completedSummary);
context.actions.maybeNameThread(effect.threadId, effect.turnId, effect.completedTurnTranscriptSummary);
return;
case "apply-thread-catalog-event":
context.actions.applyThreadCatalogEvent(effect.event);

View file

@ -13,12 +13,17 @@ import { turnRuntimeEventsFromNotification } from "./runtime-events";
export type ChatNotificationEffect =
| { type: "refresh-threads" }
| { type: "maybe-name-thread"; threadId: string; turnId: string; completedSummary: TurnRuntimeCompletedSummary }
| {
type: "maybe-name-thread";
threadId: string;
turnId: string;
completedTurnTranscriptSummary: TurnRuntimeCompletedTurnTranscriptSummary;
}
| { type: "refresh-server-diagnostics"; forceResourceProbes?: boolean }
| { type: "apply-app-server-resource-event"; event: AppServerResourceEvent }
| { type: "apply-thread-catalog-event"; event: ThreadCatalogEvent };
type TurnRuntimeCompletedSummary = Extract<TurnRuntimeOutcome, { type: "turn-completed" }>["completedSummary"];
type TurnRuntimeCompletedTurnTranscriptSummary = Extract<TurnRuntimeOutcome, { type: "turn-completed" }>["completedTurnTranscriptSummary"];
export interface ChatNotificationPlan {
actions: readonly ChatAction[];
@ -79,7 +84,7 @@ function chatNotificationEffectsFromTurnRuntimeOutcome(outcome: TurnRuntimeOutco
type: "maybe-name-thread",
threadId: outcome.threadId,
turnId: outcome.turnId,
completedSummary: outcome.completedSummary,
completedTurnTranscriptSummary: outcome.completedTurnTranscriptSummary,
},
{ type: "refresh-threads" },
];

View file

@ -14,7 +14,7 @@ import { createAutoReviewResultItem, createReviewResultItem } from "../mappers/t
import { taskProgressThreadStreamItem } from "../mappers/thread-stream/task-progress";
import {
type AppServerTurnItem,
completedConversationSummaryFromAppServerTurn,
completedTurnTranscriptSummaryFromAppServerTurn,
shouldSuppressLifecycleItem,
threadStreamItemFromTurnItem,
threadStreamItemsFromTurns,
@ -154,7 +154,7 @@ export function turnRuntimeEventsFromNotification(
turnId: notification.params.turn.id,
status: notification.params.turn.status,
completedItems: threadStreamItemsFromTurns([notification.params.turn]),
completedSummary: completedConversationSummaryFromAppServerTurn(notification.params.turn),
completedTurnTranscriptSummary: completedTurnTranscriptSummaryFromAppServerTurn(notification.params.turn),
},
];
case "serverRequest/resolved":

View file

@ -1,5 +1,5 @@
import {
completedConversationSummaryFromTurnRecord,
completedTurnTranscriptSummaryFromTurnRecord,
type TurnItem,
type TurnRecord,
turnUserItemText,
@ -7,7 +7,7 @@ import {
import { jsonPreview } from "../../../../../domain/display/json-preview";
import type { HistoricalTurn } from "../../../../../domain/threads/history";
import { referencedThreadMetadataFromPrompt } from "../../../../../domain/threads/reference";
import type { ThreadConversationSummary } from "../../../../../domain/threads/transcript";
import type { TurnTranscriptSummary } from "../../../../../domain/threads/transcript";
import { fileMentionsFromInput } from "../../../domain/thread-stream/format/file-mentions";
import { normalizeProposedPlanMarkdown } from "../../../domain/thread-stream/format/proposed-plan";
import { userMessageDisplayText } from "../../../domain/thread-stream/format/user-message-text";
@ -48,8 +48,8 @@ interface TurnItemSourceFields {
export type AppServerTurnItem = TurnItem;
export function completedConversationSummaryFromAppServerTurn(turn: TurnRecord): ThreadConversationSummary | null {
return completedConversationSummaryFromTurnRecord(turn);
export function completedTurnTranscriptSummaryFromAppServerTurn(turn: TurnRecord): TurnTranscriptSummary | null {
return completedTurnTranscriptSummaryFromTurnRecord(turn);
}
export function threadStreamItemsFromTurns(turns: readonly HistoricalTurn[]): ThreadStreamItem[] {

View file

@ -1,4 +1,4 @@
import { readReferencedThreadConversationSummaries, type ThreadConversationSummaryClient } from "../../../app-server/services/threads";
import { readReferencedThreadTurnTranscriptSummaries, type TurnTranscriptSummaryClient } from "../../../app-server/services/threads";
import { type CodexInput, codexTextInputWithAttachments } from "../../../domain/chat/input";
import { shortThreadId } from "../../../domain/threads/id";
import type { Thread } from "../../../domain/threads/model";
@ -7,7 +7,7 @@ import type { ComposerInputSnapshot } from "../application/composer/input-snapsh
import type { ThreadReferenceInput } from "../application/turns/slash-command-execution";
interface ThreadReferenceResolverHost {
currentClient(): ThreadConversationSummaryClient | null;
currentClient(): TurnTranscriptSummaryClient | null;
prepareInput(text: string, snapshot: ComposerInputSnapshot): { text: string; input: CodexInput };
addSystemMessage(text: string): void;
setStatus(status: string): void;
@ -32,7 +32,7 @@ async function referencedThreadInput(
const client = host.currentClient();
if (!client) return null;
try {
const turns = await readReferencedThreadConversationSummaries(client, thread.id, REFERENCED_THREAD_TURN_LIMIT);
const turns = await readReferencedThreadTurnTranscriptSummaries(client, thread.id, REFERENCED_THREAD_TURN_LIMIT);
if (host.currentClient() !== client) return null;
if (turns.length === 0) {
host.addSystemMessage("Referenced thread has no readable turns.");

View file

@ -1,18 +1,18 @@
import type { Thread } from "../../../../domain/threads/model";
import type { ThreadTitleContext } from "../../../../domain/threads/title-generation-model";
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
import type { TurnTranscriptSummary } from "../../../../domain/threads/transcript";
import type { ChatStateStore } from "../state/store";
export interface AutoTitleCoordinatorHost {
stateStore: ChatStateStore;
completedTurnTitleContext(turnId: string, completedSummary: ThreadConversationSummary | null): ThreadTitleContext | null;
completedTurnTitleContext(turnId: string, completedTurnTranscriptSummary: TurnTranscriptSummary | null): ThreadTitleContext | null;
generateTitleFromContext(context: ThreadTitleContext): Promise<string | null>;
renameGeneratedTitle(threadId: string, title: string, options: { shouldPublish: () => boolean }): Promise<boolean>;
}
export interface AutoTitleCoordinator {
resetThreadTurnPresence(hadTurns: boolean): void;
maybeAutoTitleThread(threadId: string, turnId: string, completedSummary: ThreadConversationSummary | null): void;
maybeAutoTitleThread(threadId: string, turnId: string, completedTurnTranscriptSummary: TurnTranscriptSummary | null): void;
}
export function createAutoTitleCoordinator(host: AutoTitleCoordinatorHost): AutoTitleCoordinator {
@ -47,14 +47,14 @@ export function createAutoTitleCoordinator(host: AutoTitleCoordinatorHost): Auto
activeThreadHadTurns = hadTurns;
},
maybeAutoTitleThread(threadId, turnId, completedSummary) {
maybeAutoTitleThread(threadId, turnId, completedTurnTranscriptSummary) {
const hadTurnsBeforeThisCompletion = activeThreadHadTurns;
activeThreadHadTurns = true;
if (hadTurnsBeforeThisCompletion) return;
if (threadHasTitle(threadId)) return;
if (attemptedThreadIds.has(threadId) || inFlightThreadIds.has(threadId)) return;
const context = host.completedTurnTitleContext(turnId, completedSummary);
const context = host.completedTurnTitleContext(turnId, completedTurnTranscriptSummary);
if (!context) return;
attemptedThreadIds.add(threadId);

View file

@ -8,9 +8,17 @@ import { activeTurnId, pendingTurnStart as pendingTurnStartForState } from "./tu
export type TurnRuntimeOutcome =
| { type: "turn-started"; threadId: string; turnId: string; recencyAt: number | null }
| { type: "turn-completed"; threadId: string; turnId: string; completedSummary: TurnRuntimeEventCompletedSummary };
| {
type: "turn-completed";
threadId: string;
turnId: string;
completedTurnTranscriptSummary: TurnRuntimeEventCompletedTurnTranscriptSummary;
};
type TurnRuntimeEventCompletedSummary = Extract<TurnRuntimeEvent, { type: "turnCompleted" }>["completedSummary"];
type TurnRuntimeEventCompletedTurnTranscriptSummary = Extract<
TurnRuntimeEvent,
{ type: "turnCompleted" }
>["completedTurnTranscriptSummary"];
export interface TurnRuntimePlan {
actions: readonly ChatAction[];
@ -142,7 +150,7 @@ function turnCompletedPlan(state: ChatState, event: Extract<TurnRuntimeEvent, {
type: "turn-completed",
threadId: event.threadId,
turnId: event.turnId,
completedSummary: event.completedSummary,
completedTurnTranscriptSummary: event.completedTurnTranscriptSummary,
},
],
};

View file

@ -1,5 +1,5 @@
import type { PendingRequestId } from "../../../../domain/pending-requests/model";
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
import type { TurnTranscriptSummary } from "../../../../domain/threads/transcript";
import type { ThreadStreamItem } from "../../domain/thread-stream/items";
type TurnRuntimeTextItemKind = "tool" | "hook" | "reasoning";
@ -67,7 +67,7 @@ export type TurnRuntimeEvent =
turnId: string;
status: string;
completedItems: readonly ThreadStreamItem[];
completedSummary: ThreadConversationSummary | null;
completedTurnTranscriptSummary: TurnTranscriptSummary | null;
}
| {
type: "turnDiffUpdated";

View file

@ -150,8 +150,8 @@ export function createConnectionBundle(
status.addSystemMessage(error instanceof Error ? error.message : String(error));
});
},
maybeNameThread: (threadId, turnId, completedSummary) => {
autoTitleCoordinator.maybeAutoTitleThread(threadId, turnId, completedSummary);
maybeNameThread: (threadId, turnId, completedTurnTranscriptSummary) => {
autoTitleCoordinator.maybeAutoTitleThread(threadId, turnId, completedTurnTranscriptSummary);
},
applyThreadCatalogEvent: (event) => {
environment.plugin.threadCatalog.apply(event);

View file

@ -136,7 +136,8 @@ export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPan
});
const autoTitleCoordinator = createAutoTitleCoordinator({
stateStore,
completedTurnTitleContext: (turnId, completedSummary) => titleService.completedTurnContext(turnId, completedSummary),
completedTurnTitleContext: (turnId, completedTurnTranscriptSummary) =>
titleService.completedTurnContext(turnId, completedTurnTranscriptSummary),
generateTitleFromContext: (context) => titleService.generate(context),
renameGeneratedTitle: (threadId, title, options) =>
threadOperations.renameThread(threadId, title, { shouldPublish: options.shouldPublish }),

View file

@ -1,14 +1,14 @@
import type { AppServerClientAccess } from "../../../app-server/connection/client-access";
import { generateThreadTitleWithCodex } from "../../../app-server/services/thread-title-generation";
import { readCompletedConversationSummariesPage } from "../../../app-server/services/threads";
import { readCompletedTurnTranscriptSummariesPage } from "../../../app-server/services/threads";
import type { ReasoningEffort } from "../../../domain/catalog/metadata";
import {
findThreadTitleContext,
THREAD_TITLE_CONTEXT_UNAVAILABLE_MESSAGE,
type ThreadTitleContext,
threadTitleContextFromConversationSummary,
threadTitleContextFromTurnTranscriptSummary,
} from "../../../domain/threads/title-generation-model";
import type { ThreadConversationSummary } from "../../../domain/threads/transcript";
import type { TurnTranscriptSummary } from "../../../domain/threads/transcript";
export interface ThreadTitleServiceHost {
codexPath: () => string;
@ -24,7 +24,7 @@ export interface ThreadTitleServiceHost {
export interface ThreadTitleService {
generateTitle(threadId: string): Promise<string>;
resolveContext(threadId: string): Promise<ThreadTitleContext | null>;
completedTurnContext(turnId: string, completedSummary: ThreadConversationSummary | null): ThreadTitleContext | null;
completedTurnContext(turnId: string, completedTurnTranscriptSummary: TurnTranscriptSummary | null): ThreadTitleContext | null;
generate(context: ThreadTitleContext): Promise<string | null>;
}
@ -32,7 +32,7 @@ export function createThreadTitleService(host: ThreadTitleServiceHost): ThreadTi
return {
generateTitle: (threadId) => generateTitle(host, threadId),
resolveContext: (threadId) => resolveThreadTitleContext(host, threadId),
completedTurnContext: (turnId, completedSummary) => completedTurnContext(host, turnId, completedSummary),
completedTurnContext: (turnId, completedTurnTranscriptSummary) => completedTurnContext(host, turnId, completedTurnTranscriptSummary),
generate: (context) => generateTitleFromContext(host, context),
};
}
@ -56,7 +56,7 @@ async function persistedThreadTitleContext(host: ThreadTitleServiceHost, threadI
return host.clientAccess.withClient((client) =>
findThreadTitleContext({
threadId,
readTurns: (id, cursor, limit, sortDirection) => readCompletedConversationSummariesPage(client, id, cursor, limit, sortDirection),
readTurns: (id, cursor, limit, sortDirection) => readCompletedTurnTranscriptSummariesPage(client, id, cursor, limit, sortDirection),
}),
);
}
@ -64,10 +64,11 @@ async function persistedThreadTitleContext(host: ThreadTitleServiceHost, threadI
function completedTurnContext(
host: ThreadTitleServiceHost,
turnId: string,
completedSummary: ThreadConversationSummary | null,
completedTurnTranscriptSummary: TurnTranscriptSummary | null,
): ThreadTitleContext | null {
return (
host.visibleCompletedTurnContext?.(turnId) ?? (completedSummary ? threadTitleContextFromConversationSummary(completedSummary) : null)
host.visibleCompletedTurnContext?.(turnId) ??
(completedTurnTranscriptSummary ? threadTitleContextFromTurnTranscriptSummary(completedTurnTranscriptSummary) : null)
);
}

View file

@ -15,7 +15,7 @@ import type { ServerInitialization } from "../../src/domain/server/initializatio
import {
findThreadTitleContext,
THREAD_TITLE_MAX_CHARS,
threadTitleContextFromConversationSummary,
threadTitleContextFromTurnTranscriptSummary,
threadTitleFromGeneratedText,
threadTitlePrompt,
} from "../../src/domain/threads/title-generation-model";
@ -29,9 +29,9 @@ interface TurnStartResponse {
}
describe("thread title", () => {
it("builds title context from a conversation summary", () => {
it("builds title context from a turn transcript summary", () => {
expect(
threadTitleContextFromConversationSummary({
threadTitleContextFromTurnTranscriptSummary({
userText: "Codex Panelに自動命名を付けたい",
assistantText: "実装方針をまとめました。",
}),
@ -41,8 +41,8 @@ describe("thread title", () => {
});
});
it("does not build title context for incomplete summaries", () => {
expect(threadTitleContextFromConversationSummary({ userText: "hello", assistantText: null })).toBeNull();
it("does not build title context for incomplete turn transcript summaries", () => {
expect(threadTitleContextFromTurnTranscriptSummary({ userText: "hello", assistantText: null })).toBeNull();
});
it("scans older thread pages until it finds a usable title context", async () => {

View file

@ -1,14 +1,14 @@
import { describe, expect, it } from "vitest";
import {
chronologicalConversationSummariesFromTurnRecords,
completedConversationSummariesFromTurnRecords,
completedConversationSummaryFromTurnRecord,
conversationAssistantTextFromTurnRecord,
chronologicalTurnTranscriptSummariesFromTurnRecords,
completedTurnTranscriptSummariesFromTurnRecords,
completedTurnTranscriptSummaryFromTurnRecord,
lastAgentMessageTextFromTurnRecord,
type TurnItem,
type TurnRecord,
transcriptEntriesFromTurnRecords,
turnTranscriptAssistantTextFromTurnRecord,
} from "../../src/app-server/protocol/turn";
describe("app-server turn records", () => {
@ -27,9 +27,9 @@ describe("app-server turn records", () => {
]);
});
it("builds completed turn summaries from the first user message and last assistant-like item", () => {
it("builds completed turn transcript summaries from the first user message and last assistant-like item", () => {
expect(
completedConversationSummaryFromTurnRecord(
completedTurnTranscriptSummaryFromTurnRecord(
turn([
agentMessage("draft", "途中経過"),
userMessage("u1", "最初の依頼"),
@ -40,15 +40,15 @@ describe("app-server turn records", () => {
).toEqual({ userText: "最初の依頼", assistantText: "最終回答" });
});
it("does not build completed summaries for failed turns", () => {
it("does not build completed turn transcript summaries for failed turns", () => {
expect(
completedConversationSummaryFromTurnRecord(turn([userMessage("u1", "依頼"), agentMessage("a1", "回答")], { status: "failed" })),
completedTurnTranscriptSummaryFromTurnRecord(turn([userMessage("u1", "依頼"), agentMessage("a1", "回答")], { status: "failed" })),
).toBeNull();
});
it("projects completed summaries from turn lists without exposing filtering logic to callers", () => {
it("projects completed turn transcript summaries from turn lists without exposing filtering logic to callers", () => {
expect(
completedConversationSummariesFromTurnRecords([
completedTurnTranscriptSummariesFromTurnRecords([
turn([userMessage("u1", "依頼"), agentMessage("a1", "回答")], { id: "completed" }),
turn([userMessage("u2", "失敗した依頼"), agentMessage("a2", "失敗した回答")], { id: "failed", status: "failed" }),
turn([commandItem("cmd")], { id: "empty" }),
@ -56,9 +56,9 @@ describe("app-server turn records", () => {
).toEqual([{ userText: "依頼", assistantText: "回答" }]);
});
it("returns chronological summaries and drops turns without conversation text", () => {
it("returns chronological turn transcript summaries and drops turns without transcript text", () => {
expect(
chronologicalConversationSummariesFromTurnRecords([
chronologicalTurnTranscriptSummariesFromTurnRecords([
turn([userMessage("u2", "後の依頼"), agentMessage("a2", "後の回答")], { id: "turn-2", startedAt: 20 }),
turn([commandItem("cmd")], { id: "turn-empty", startedAt: 15 }),
turn([userMessage("u1", "先の依頼"), agentMessage("a1", "先の回答")], { id: "turn-1", startedAt: 10 }),
@ -124,8 +124,8 @@ describe("app-server turn records", () => {
]);
});
it("extracts assistant-like conversation text for generated turn consumers", () => {
expect(conversationAssistantTextFromTurnRecord(turn([userMessage("u1", "依頼"), planItem("p1", "計画")]))).toBe("計画");
it("extracts assistant-like transcript text for generated turn consumers", () => {
expect(turnTranscriptAssistantTextFromTurnRecord(turn([userMessage("u1", "依頼"), planItem("p1", "計画")]))).toBe("計画");
});
it("extracts the final non-empty agent message text from a turn", () => {

View file

@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import { conversationSummaryFromTranscriptEntries, nonEmptyConversationSummaries } from "../../../src/domain/threads/transcript";
import { nonEmptyTurnTranscriptSummaries, turnTranscriptSummaryFromTranscriptEntries } from "../../../src/domain/threads/transcript";
describe("thread transcript model", () => {
it("builds conversation summaries from the first user entry and last assistant-like entry", () => {
it("builds turn transcript summaries from the first user entry and last assistant-like entry", () => {
expect(
conversationSummaryFromTranscriptEntries([
turnTranscriptSummaryFromTranscriptEntries([
{ kind: "assistant", text: "途中経過", timestamp: 1 },
{ kind: "user", text: "最初の依頼", timestamp: 2 },
{ kind: "user", text: "補足", timestamp: 3 },
@ -14,9 +14,9 @@ describe("thread transcript model", () => {
).toEqual({ userText: "最初の依頼", assistantText: "最終計画" });
});
it("drops empty summaries", () => {
it("drops empty turn transcript summaries", () => {
expect(
nonEmptyConversationSummaries([
nonEmptyTurnTranscriptSummaries([
{ userText: null, assistantText: null },
{ userText: "依頼", assistantText: null },
]),

View file

@ -97,7 +97,7 @@ describe("chat inbound routing", () => {
type: "maybe-name-thread",
threadId: "thread-active",
turnId: "turn-active",
completedSummary: { userText: "hello", assistantText: "done" },
completedTurnTranscriptSummary: { userText: "hello", assistantText: "done" },
},
{ type: "refresh-threads" },
]);

View file

@ -43,7 +43,7 @@ describe("app-server turn runtime event mapping", () => {
threadId: "thread-active",
turnId: "turn-active",
status: "completed",
completedSummary: { userText: "hello", assistantText: "done" },
completedTurnTranscriptSummary: { userText: "hello", assistantText: "done" },
}),
]);
expect(events[0]).toMatchObject({

View file

@ -173,7 +173,8 @@ function coordinatorFixture(
});
const host = {
stateStore,
completedTurnTitleContext: (turnId: string, completedSummary) => titleService.completedTurnContext(turnId, completedSummary),
completedTurnTitleContext: (turnId: string, completedTurnTranscriptSummary) =>
titleService.completedTurnContext(turnId, completedTurnTranscriptSummary),
generateTitleFromContext: (context) => titleService.generate(context),
renameGeneratedTitle: (threadId: string, value: string, options: { shouldPublish: () => boolean }) =>
threadOperations.renameThread(threadId, value, options),

View file

@ -36,7 +36,7 @@ describe("TurnRuntimeEvent planner", () => {
threadId: "thread-active",
turnId: "turn-active",
status: "completed",
completedSummary: { userText: "hello", assistantText: "done" },
completedTurnTranscriptSummary: { userText: "hello", assistantText: "done" },
completedItems: [
{
id: "u1",
@ -71,7 +71,7 @@ describe("TurnRuntimeEvent planner", () => {
type: "turn-completed",
threadId: "thread-active",
turnId: "turn-active",
completedSummary: { userText: "hello", assistantText: "done" },
completedTurnTranscriptSummary: { userText: "hello", assistantText: "done" },
},
]);
});