import * as Clock from "effect/Clock"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import type * as EffectAcpSchema from "effect-acp/schema"; import { deriveToolActivityPresentation } from "@t3tools/shared/toolActivity"; import type { ToolLifecycleItemType } from "@t3tools/contracts"; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isSessionModelState(value: unknown): value is EffectAcpSchema.SessionModelState { if (!isRecord(value) || typeof value.currentModelId !== "string") { return false; } if (!Array.isArray(value.availableModels)) { return false; } return value.availableModels.every( (model) => isRecord(model) && typeof model.modelId === "string" && typeof model.name === "string" && (model.description === undefined || model.description === null || typeof model.description === "string"), ); } function isSessionModeState(value: unknown): value is EffectAcpSchema.SessionModeState { if (!isRecord(value) || typeof value.currentModeId !== "string") { return false; } if (!Array.isArray(value.availableModes)) { return false; } return value.availableModes.every( (mode) => isRecord(mode) && typeof mode.id === "string" && typeof mode.name === "string" && (mode.description === undefined || typeof mode.description === "string"), ); } export interface AcpSessionMode { readonly id: string; readonly name: string; readonly description?: string; } export interface AcpSessionModeState { readonly currentModeId: string; readonly availableModes: ReadonlyArray; } export interface AcpToolCallState { readonly toolCallId: string; readonly kind?: string; readonly title?: string; readonly status?: "pending" | "inProgress" | "completed" | "failed"; readonly command?: string; readonly detail?: string; readonly data: Record; } export interface AcpPlanUpdate { readonly explanation?: string | null; readonly plan: ReadonlyArray<{ readonly step: string; readonly status: "pending" | "inProgress" | "completed"; }>; } export interface AcpPermissionRequest { readonly kind: string | "unknown"; readonly detail?: string; readonly toolCall?: AcpToolCallState; } export type AcpParsedSessionEvent = | { readonly _tag: "ModeChanged"; readonly modeId: string; } | { readonly _tag: "AssistantItemStarted"; readonly itemId: string; } | { readonly _tag: "AssistantItemCompleted"; readonly itemId: string; } | { readonly _tag: "PlanUpdated"; readonly payload: AcpPlanUpdate; readonly rawPayload: unknown; } | { readonly _tag: "ToolCallUpdated"; readonly toolCall: AcpToolCallState; readonly rawPayload: unknown; } | { readonly _tag: "ContentDelta"; readonly itemId?: string; readonly text: string; readonly rawPayload: unknown; }; type AcpSessionSetupResponse = | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse; type AcpToolCallUpdate = Extract< EffectAcpSchema.SessionNotification["update"], { readonly sessionUpdate: "tool_call" | "tool_call_update" } >; export function extractModelConfigId(sessionResponse: AcpSessionSetupResponse): string | undefined { const configOptions = sessionResponse.configOptions; if (!configOptions) return undefined; for (const opt of configOptions) { if (opt.category === "model" && opt.id.trim().length > 0) { return opt.id.trim(); } } return undefined; } export function findSessionConfigOption( configOptions: ReadonlyArray | null | undefined, configId: string, ): EffectAcpSchema.SessionConfigOption | undefined { if (!configOptions) { return undefined; } const normalizedConfigId = configId.trim(); if (!normalizedConfigId) { return undefined; } return configOptions.find((option) => option.id.trim() === normalizedConfigId); } export function collectSessionConfigOptionValues( configOption: EffectAcpSchema.SessionConfigOption, ): ReadonlyArray { if (configOption.type !== "select") { return []; } return configOption.options.flatMap((entry) => "value" in entry ? [entry.value] : entry.options.map((option) => option.value), ); } export function parseSessionModeState( sessionResponse: AcpSessionSetupResponse, ): AcpSessionModeState | undefined { const modes = sessionResponse.modes; if (!modes) return undefined; const currentModeId = modes.currentModeId.trim(); if (!currentModeId) { return undefined; } const availableModes: Array = []; for (const mode of modes.availableModes) { const id = mode.id.trim(); const name = mode.name.trim(); if (!id || !name) { continue; } const description = mode.description?.trim() || undefined; availableModes.push( description !== undefined ? ({ id, name, description } satisfies AcpSessionMode) : ({ id, name } satisfies AcpSessionMode), ); } if (availableModes.length === 0) { return undefined; } return { currentModeId, availableModes, }; } function normalizePlanStepStatus(raw: unknown): "pending" | "inProgress" | "completed" { switch (raw) { case "completed": return "completed"; case "in_progress": case "inProgress": return "inProgress"; default: return "pending"; } } function normalizeToolCallStatus( raw: unknown, fallback?: "pending" | "inProgress" | "completed" | "failed", ): "pending" | "inProgress" | "completed" | "failed" | undefined { switch (raw) { case "pending": return "pending"; case "in_progress": case "inProgress": return "inProgress"; case "completed": return "completed"; case "failed": return "failed"; default: return fallback; } } function normalizeCommandValue(value: unknown): string | undefined { if (typeof value === "string" && value.trim().length > 0) { return value.trim(); } if (!Array.isArray(value)) { return undefined; } const parts: Array = []; for (const entry of value) { if (typeof entry === "string") { const part = entry.trim(); if (part.length > 0) { parts.push(part); } } } return parts.length > 0 ? parts.join(" ") : undefined; } function extractCommandFromTitle(title: string | undefined): string | undefined { if (!title) { return undefined; } const match = /`([^`]+)`/.exec(title); return match?.[1]?.trim() || undefined; } function extractToolCallCommand(rawInput: unknown, title: string | undefined): string | undefined { if (isRecord(rawInput)) { const directCommand = normalizeCommandValue(rawInput.command); if (directCommand) { return directCommand; } const executable = typeof rawInput.executable === "string" ? rawInput.executable.trim() : ""; const args = normalizeCommandValue(rawInput.args); if (executable && args) { return `${executable} ${args}`; } if (executable) { return executable; } } return extractCommandFromTitle(title); } function extractTextContentFromToolCallContent( content: ReadonlyArray | null | undefined, ): string | undefined { if (!content) return undefined; const chunks: Array = []; for (const entry of content) { if (entry.type !== "content") { continue; } const nestedContent = entry.content; if (nestedContent.type !== "text") { continue; } const text = nestedContent.text.trim(); if (text.length > 0) { chunks.push(text); } } return chunks.length > 0 ? chunks.join("\n") : undefined; } function normalizeToolKind(kind: unknown): string | undefined { return typeof kind === "string" && kind.trim().length > 0 ? kind.trim() : undefined; } function canonicalItemTypeFromAcpToolKind(kind: string | undefined): ToolLifecycleItemType { switch (kind) { case "execute": return "command_execution"; case "edit": case "delete": case "move": return "file_change"; case "search": case "fetch": return "web_search"; default: return "dynamic_tool_call"; } } function makeToolCallState( input: { readonly toolCallId: string; readonly title?: string | null | undefined; readonly kind?: EffectAcpSchema.ToolKind | null | undefined; readonly status?: EffectAcpSchema.ToolCallStatus | null | undefined; readonly rawInput?: unknown; readonly rawOutput?: unknown; readonly content?: ReadonlyArray | null | undefined; readonly locations?: ReadonlyArray | null | undefined; }, options?: { readonly fallbackStatus?: "pending" | "inProgress" | "completed" | "failed"; }, ): AcpToolCallState | undefined { const toolCallId = input.toolCallId.trim(); if (!toolCallId) { return undefined; } const title = input.title?.trim() || undefined; const command = extractToolCallCommand(input.rawInput, title); const textContent = extractTextContentFromToolCallContent(input.content); const normalizedTitle = title && title.toLowerCase() !== "terminal" && title.toLowerCase() !== "tool call" ? title : undefined; const data: Record = { toolCallId }; const kind = normalizeToolKind(input.kind); if (kind) { data.kind = kind; } if (command) { data.command = command; } if (input.rawInput !== undefined) { data.rawInput = input.rawInput; } if (input.rawOutput !== undefined) { data.rawOutput = input.rawOutput; } if (input.content !== undefined) { data.content = input.content; } if (input.locations !== undefined) { data.locations = input.locations; } const fallbackDetail = command ?? normalizedTitle ?? textContent; const hasPresentationSeed = title !== undefined || kind !== undefined || command !== undefined || normalizedTitle !== undefined || textContent !== undefined; const presentation = hasPresentationSeed ? deriveToolActivityPresentation({ itemType: canonicalItemTypeFromAcpToolKind(kind), title, detail: fallbackDetail, data, fallbackSummary: title ?? "Tool", }) : undefined; const status = normalizeToolCallStatus(input.status, options?.fallbackStatus); return { toolCallId, ...(kind ? { kind } : {}), ...(presentation?.summary ? { title: presentation.summary } : {}), ...(status ? { status } : {}), ...(command ? { command } : {}), ...(presentation?.detail ? { detail: presentation.detail } : {}), data, }; } function parseTypedToolCallState( event: AcpToolCallUpdate, options?: { readonly fallbackStatus?: "pending" | "inProgress" | "completed" | "failed"; }, ): AcpToolCallState | undefined { return makeToolCallState( { toolCallId: event.toolCallId, title: event.title, kind: event.kind, status: event.status, rawInput: event.rawInput, rawOutput: event.rawOutput, content: event.content, locations: event.locations, }, options, ); } export function mergeToolCallState( previous: AcpToolCallState | undefined, next: AcpToolCallState, ): AcpToolCallState { const nextKind = typeof next.data.kind === "string" ? next.data.kind : undefined; const kind = nextKind ?? previous?.kind; const title = next.title ?? previous?.title; const status = next.status ?? previous?.status; const command = next.command ?? previous?.command; const detail = next.detail ?? previous?.detail; return { toolCallId: next.toolCallId, ...(kind ? { kind } : {}), ...(title ? { title } : {}), ...(status ? { status } : {}), ...(command ? { command } : {}), ...(detail ? { detail } : {}), data: { ...previous?.data, ...next.data, }, }; } export function parsePermissionRequest( params: EffectAcpSchema.RequestPermissionRequest, ): AcpPermissionRequest { const toolCall = makeToolCallState( { toolCallId: params.toolCall.toolCallId, title: params.toolCall.title, kind: params.toolCall.kind, status: params.toolCall.status, rawInput: params.toolCall.rawInput, rawOutput: params.toolCall.rawOutput, content: params.toolCall.content, locations: params.toolCall.locations, }, { fallbackStatus: "pending" }, ); const kind = normalizeToolKind(params.toolCall.kind) ?? "unknown"; const detail = toolCall?.command ?? toolCall?.title ?? toolCall?.detail ?? (typeof params.sessionId === "string" ? `Session ${params.sessionId}` : undefined); return { kind, ...(detail ? { detail } : {}), ...(toolCall ? { toolCall } : {}), }; } export function sessionUpdateIsReplay(params: EffectAcpSchema.SessionNotification): boolean { const meta = params._meta; return isRecord(meta) && meta.isReplay === true; } export interface SessionLoadGate { readonly active: boolean; readonly lastActivityAtMillis: number | undefined; readonly idleGap: Duration.Duration; readonly initializeResult: EffectAcpSchema.InitializeResponse; } export const waitForSessionLoadReplayIdle = (input: { readonly gateRef: Ref.Ref>; }): Effect.Effect => Effect.gen(function* () { const pollInterval = Duration.millis(25); while (true) { const gate = yield* Ref.get(input.gateRef); if ( Option.isSome(gate) && gate.value.active && gate.value.lastActivityAtMillis !== undefined ) { const idleGapMillis = Duration.toMillis(gate.value.idleGap); const nowMillis = yield* Clock.currentTimeMillis; if (nowMillis - gate.value.lastActivityAtMillis >= idleGapMillis) { return syntheticLoadSessionResponseFromInitialize(gate.value.initializeResult); } } yield* Effect.sleep(pollInterval); } }); export function syntheticLoadSessionResponseFromInitialize( initializeResult: EffectAcpSchema.InitializeResponse, ): EffectAcpSchema.LoadSessionResponse { const meta = initializeResult._meta; const modelState = isRecord(meta) ? meta.modelState : undefined; const modeState = isRecord(meta) ? meta.modeState : undefined; const models = isSessionModelState(modelState) ? modelState : undefined; const modes = isSessionModeState(modeState) ? modeState : undefined; return { ...(models ? { models } : {}), ...(modes ? { modes } : {}), _meta: { t3SessionLoadReady: "replay_idle", }, }; } export function parseSessionUpdateEvent(params: EffectAcpSchema.SessionNotification): { readonly modeId?: string; readonly events: ReadonlyArray; } { const upd = params.update; const events: Array = []; let modeId: string | undefined; switch (upd.sessionUpdate) { case "current_mode_update": { modeId = upd.currentModeId.trim(); if (modeId) { events.push({ _tag: "ModeChanged", modeId, }); } break; } case "plan": { const plan = upd.entries.map((entry, index) => ({ step: entry.content.trim().length > 0 ? entry.content.trim() : `Step ${index + 1}`, status: normalizePlanStepStatus(entry.status), })); if (plan.length > 0) { events.push({ _tag: "PlanUpdated", payload: { plan, }, rawPayload: params, }); } break; } case "tool_call": { const toolCall = parseTypedToolCallState(upd, { fallbackStatus: "pending", }); if (toolCall) { events.push({ _tag: "ToolCallUpdated", toolCall, rawPayload: params, }); } break; } case "tool_call_update": { const toolCall = parseTypedToolCallState(upd); if (toolCall) { events.push({ _tag: "ToolCallUpdated", toolCall, rawPayload: params, }); } break; } case "agent_message_chunk": { if (upd.content.type === "text" && upd.content.text.length > 0) { events.push({ _tag: "ContentDelta", text: upd.content.text, rawPayload: params, }); } break; } default: break; } return { ...(modeId !== undefined ? { modeId } : {}), events }; }