import { join } from "node:path";
import { PilotDeckToolRuntimeError } from "../../tool/protocol/errors.js";
import type { PilotDeckToolDefinition } from "../../tool/protocol/types.js";
import { getPilotProjectChatDir } from "../../pilot/paths.js";
import { readTranscript } from "../../session/transcript/TranscriptReader.js";
import { replayTranscriptEntries } from "../../session/transcript/TranscriptReplay.js";
import { sanitizeSessionIdForPath } from "../../session/storage/ProjectSessionStorage.js";
import { listProjectSessions } from "../../session/storage/SessionList.js";
import type { AlwaysOnRunContextRegistry } from "../runtime/AlwaysOnRunContextRegistry.js";
export type AlwaysOnChatHistoryInput = {
sessionId: string;
};
export type AlwaysOnChatHistoryConversationEntry = {
role: "user" | "assistant";
text: string;
createdAt: string;
};
export type AlwaysOnChatHistoryOutput = {
sessionId: string;
title: string;
messageCount: number;
conversation: AlwaysOnChatHistoryConversationEntry[];
};
export type CreateAlwaysOnChatHistoryToolOptions = {
runContexts: AlwaysOnRunContextRegistry;
};
export const ALWAYS_ON_CHAT_HISTORY_TOOL_NAME = "always_on_read_chat_history";
const ASSISTANT_TEXT_LIMIT = 300;
export function createAlwaysOnChatHistoryTool(
options: CreateAlwaysOnChatHistoryToolOptions,
): PilotDeckToolDefinition<AlwaysOnChatHistoryInput, AlwaysOnChatHistoryOutput> {
return {
name: ALWAYS_ON_CHAT_HISTORY_TOOL_NAME,
aliases: ["AlwaysOnReadChatHistory"],
description:
"Read the full conversation from a user chat session. " +
"Use the sessionId from the chat digest in the discovery prompt to expand a session of interest. " +
"Only available during Always-On discovery (Phase 1).",
kind: "session",
inputSchema: {
type: "object",
required: ["sessionId"],
additionalProperties: false,
properties: {
sessionId: {
type: "string",
description: "The sessionId of the chat session to read (from the chat digest).",
},
},
},
isReadOnly: () => true,
isConcurrencySafe: () => true,
execute: async (input, context) => {
const ctx = options.runContexts.getDiscovery(context.sessionId);
if (!ctx) {
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
`${ALWAYS_ON_CHAT_HISTORY_TOOL_NAME} is only available during Always-On discovery (Phase 1).`,
);
}
const realSessionId = ctx.chatSessionAliases?.get(input.sessionId) ?? input.sessionId;
const chatDir = getPilotProjectChatDir(ctx.projectKey, ctx.paths.pilotHome);
const transcriptPath = join(chatDir, `${sanitizeSessionIdForPath(realSessionId)}.jsonl`);
const { entries, diagnostics } = await readTranscript(transcriptPath);
if (entries.length === 0) {
const reason = diagnostics.length > 0
? diagnostics[0].message
: "No transcript entries found.";
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
`Could not read session ${input.sessionId}: ${reason}`,
);
}
const { messages, metadata } = replayTranscriptEntries(entries);
const sessions = await listProjectSessions({
projectRoot: ctx.projectKey,
pilotHome: ctx.paths.pilotHome,
includeInternal: false,
});
const sessionInfo = sessions.find((s) => s.sessionId === realSessionId);
const title = metadata.title ?? metadata.aiTitle ?? sessionInfo?.summary ?? realSessionId;
const conversation: AlwaysOnChatHistoryConversationEntry[] = [];
for (const msg of messages) {
if (msg.role !== "user" && msg.role !== "assistant") continue;
const textParts = msg.content
.filter((b): b is { type: "text"; text: string } => b.type === "text" && typeof (b as { text?: unknown }).text === "string")
.map((b) => b.text.trim())
.filter((t) => t.length > 0);
if (textParts.length === 0) continue;
const fullText = textParts.join("\n\n");
const role = msg.role as "user" | "assistant";
const text = role === "assistant" && fullText.length > ASSISTANT_TEXT_LIMIT
? `${fullText.slice(0, ASSISTANT_TEXT_LIMIT)}...`
: fullText;
conversation.push({ role, text, createdAt: "" });
}
const data: AlwaysOnChatHistoryOutput = {
sessionId: realSessionId,
title,
messageCount: conversation.length,
conversation,
};
return {
content: [
{ type: "text", text: `Session "${title}" — ${conversation.length} messages.` },
{ type: "json", value: data },
],
data,
metadata: { runId: ctx.runId },
};
},
};
}