mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Rename turn transcript summaries
This commit is contained in:
parent
682a23d712
commit
2debad388b
24 changed files with 121 additions and 105 deletions
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue