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<string, unknown> { 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<AcpSessionMode>;} 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<string, unknown>;} 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: "AvailableCommandsUpdated"; readonly availableCommands: ReadonlyArray<EffectAcpSchema.AvailableCommand>; readonly rawPayload: unknown; } | { readonly _tag: "ConfigOptionsUpdated"; readonly configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>; readonly rawPayload: unknown; } | { 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; } | { readonly _tag: "ThoughtDelta"; 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<EffectAcpSchema.SessionConfigOption> | 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<string> { 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<AcpSessionMode> = []; 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<string> = []; 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);} // Some ACP agents (observed with Grok's CLI) resend the ENTIRE accumulated tool-call// output on every `tool_call_update` notification instead of a delta, so a redrawing// terminal progress bar can balloon a single tool call to hundreds of KB per update at// several updates per second. Cap what we retain/emit to a bounded tail so one busy tool// call cannot flood runtime event ingestion. We always keep the tail: `tool_call_update`// deltas routinely omit `kind`, so there is no reliable way to tell a redrawing terminal// from another tool here, and the end is the useful part of any live-growing output.const TOOL_CALL_CONTENT_MAX_CHARS = 8_000;const TOOL_CALL_CONTENT_TRUNCATION_MARKER = "[Earlier output truncated]\n\n"; function boundToolCallOutputText(text: string): string { if (text.length <= TOOL_CALL_CONTENT_MAX_CHARS) { return text; } const tail = text.slice(text.length - TOOL_CALL_CONTENT_MAX_CHARS); return `${TOOL_CALL_CONTENT_TRUNCATION_MARKER}${tail}`;} const RAW_OUTPUT_TEXT_FIELDS = ["content", "stdout", "stderr", "output"] as const; // `rawOutput` is provider-defined and, for terminal-shaped tools, mirrors the same// cumulative text-growth problem as `content` (see the comment above). Bound its known// text-bearing fields the same way so a chatty provider cannot smuggle unbounded output// through this field instead.function boundToolCallRawOutput(rawOutput: unknown): unknown { if (!isRecord(rawOutput)) { return rawOutput; } let changed = false; const bounded: Record<string, unknown> = { ...rawOutput }; for (const field of RAW_OUTPUT_TEXT_FIELDS) { const value = rawOutput[field]; if (typeof value === "string" && value.length > TOOL_CALL_CONTENT_MAX_CHARS) { bounded[field] = boundToolCallOutputText(value); changed = true; } } return changed ? bounded : rawOutput;} interface ExtractedToolCallContent { readonly text: string | undefined; readonly content: ReadonlyArray<EffectAcpSchema.ToolCallContent> | undefined;} function toolCallContentText(entry: EffectAcpSchema.ToolCallContent): string | undefined { if (entry.type !== "content" || entry.content.type !== "text") { return undefined; } return entry.content.text;} // Trim is used for display `text`, so whitespace-only (or whitespace-padded) entries never// contribute to `chunks` and used to take the early returns with the original array. Bound// each text entry independently so those paths cannot persist an unbounded terminal buffer// on `toolCall.data.content` / `rawPayload`.function boundToolCallContentEntries( content: ReadonlyArray<EffectAcpSchema.ToolCallContent>,): ReadonlyArray<EffectAcpSchema.ToolCallContent> { let changed = false; const bounded = content.map((entry) => { const text = toolCallContentText(entry); if (text === undefined || text.length <= TOOL_CALL_CONTENT_MAX_CHARS) { return entry; } changed = true; const trimmed = text.trim(); return { type: "content", content: { type: "text", text: boundToolCallOutputText(trimmed.length > 0 ? trimmed : text), }, } as const; }); return changed ? bounded : content;} function extractTextContentFromToolCallContent( content: ReadonlyArray<EffectAcpSchema.ToolCallContent> | null | undefined,): ExtractedToolCallContent { if (!content) { return { text: undefined, content: undefined }; } const chunks: Array<string> = []; for (const entry of content) { const text = toolCallContentText(entry)?.trim(); if (text) { chunks.push(text); } } if (chunks.length === 0) { return { text: undefined, content: boundToolCallContentEntries(content) }; } const joined = chunks.join("\n"); if (joined.length <= TOOL_CALL_CONTENT_MAX_CHARS) { return { text: joined, content: boundToolCallContentEntries(content) }; } const bounded = boundToolCallOutputText(joined); const tail = joined.slice(joined.length - TOOL_CALL_CONTENT_MAX_CHARS); return { text: bounded, content: distributeRetainedTailAcrossContent(content, tail), };} // Walk the original text entries from the joined tail window so a retained slice that// spans entries around an image/diff stays on those entries. Non-text kinds keep their// relative order; blank text entries are dropped; the truncation marker is prepended to// the first remaining text entry.function distributeRetainedTailAcrossContent( content: ReadonlyArray<EffectAcpSchema.ToolCallContent>, tail: string,): ReadonlyArray<EffectAcpSchema.ToolCallContent> { const textRanges: Array< | { readonly start: number; readonly end: number; readonly text: string; } | undefined > = Array.from({ length: content.length }); let offset = 0; let seenText = false; for (const [index, entry] of content.entries()) { const text = toolCallContentText(entry)?.trim(); if (!text) { continue; } if (seenText) { offset += 1; } seenText = true; const start = offset; const end = offset + text.length; textRanges[index] = { start, end, text }; offset = end; } const tailStart = Math.max(0, offset - tail.length); let markerPending = true; return content.flatMap((entry, index) => { if (toolCallContentText(entry) === undefined) { return [entry]; } const range = textRanges[index]; if (range === undefined) { return []; } const overlapStart = Math.max(range.start, tailStart); const overlapEnd = Math.min(range.end, offset); if (overlapEnd <= overlapStart) { return []; } let piece = range.text.slice(overlapStart - range.start, overlapEnd - range.start); if (markerPending) { piece = `${TOOL_CALL_CONTENT_TRUNCATION_MARKER}${piece}`; markerPending = false; } return [{ type: "content", content: { type: "text", text: piece } } as const]; });} function normalizeToolKind(kind: unknown): string | undefined { return typeof kind === "string" && kind.trim().length > 0 ? kind.trim() : undefined;} /** * Map an ACP tool kind onto the canonical runtime item type used by the * thread activity model. Unknown kinds fall back to a generic tool call. */export 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<EffectAcpSchema.ToolCallContent> | null | undefined; readonly locations?: ReadonlyArray<EffectAcpSchema.ToolCallLocation> | 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 extractedContent = extractTextContentFromToolCallContent(input.content); const textContent = extractedContent.text; const normalizedTitle = title && title.toLowerCase() !== "terminal" && title.toLowerCase() !== "tool call" ? title : undefined; const data: Record<string, unknown> = { toolCallId }; const kind = normalizeToolKind(input.kind); if (kind) { data.kind = kind; } if (command) {Lines 1–500 of 887. Load more
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<string, unknown> { 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<AcpSessionMode>; } 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<string, unknown>; } 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: "AvailableCommandsUpdated"; readonly availableCommands: ReadonlyArray<EffectAcpSchema.AvailableCommand>; readonly rawPayload: unknown; } | { readonly _tag: "ConfigOptionsUpdated"; readonly configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>; readonly rawPayload: unknown; } | { 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; } | { readonly _tag: "ThoughtDelta"; 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<EffectAcpSchema.SessionConfigOption> | 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<string> { 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<AcpSessionMode> = []; 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<string> = []; 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); } // Some ACP agents (observed with Grok's CLI) resend the ENTIRE accumulated tool-call // output on every `tool_call_update` notification instead of a delta, so a redrawing // terminal progress bar can balloon a single tool call to hundreds of KB per update at // several updates per second. Cap what we retain/emit to a bounded tail so one busy tool // call cannot flood runtime event ingestion. We always keep the tail: `tool_call_update` // deltas routinely omit `kind`, so there is no reliable way to tell a redrawing terminal // from another tool here, and the end is the useful part of any live-growing output. const TOOL_CALL_CONTENT_MAX_CHARS = 8_000; const TOOL_CALL_CONTENT_TRUNCATION_MARKER = "[Earlier output truncated]\n\n"; function boundToolCallOutputText(text: string): string { if (text.length <= TOOL_CALL_CONTENT_MAX_CHARS) { return text; } const tail = text.slice(text.length - TOOL_CALL_CONTENT_MAX_CHARS); return `${TOOL_CALL_CONTENT_TRUNCATION_MARKER}${tail}`; } const RAW_OUTPUT_TEXT_FIELDS = ["content", "stdout", "stderr", "output"] as const; // `rawOutput` is provider-defined and, for terminal-shaped tools, mirrors the same // cumulative text-growth problem as `content` (see the comment above). Bound its known // text-bearing fields the same way so a chatty provider cannot smuggle unbounded output // through this field instead. function boundToolCallRawOutput(rawOutput: unknown): unknown { if (!isRecord(rawOutput)) { return rawOutput; } let changed = false; const bounded: Record<string, unknown> = { ...rawOutput }; for (const field of RAW_OUTPUT_TEXT_FIELDS) { const value = rawOutput[field]; if (typeof value === "string" && value.length > TOOL_CALL_CONTENT_MAX_CHARS) { bounded[field] = boundToolCallOutputText(value); changed = true; } } return changed ? bounded : rawOutput; } interface ExtractedToolCallContent { readonly text: string | undefined; readonly content: ReadonlyArray<EffectAcpSchema.ToolCallContent> | undefined; } function toolCallContentText(entry: EffectAcpSchema.ToolCallContent): string | undefined { if (entry.type !== "content" || entry.content.type !== "text") { return undefined; } return entry.content.text; } // Trim is used for display `text`, so whitespace-only (or whitespace-padded) entries never // contribute to `chunks` and used to take the early returns with the original array. Bound // each text entry independently so those paths cannot persist an unbounded terminal buffer // on `toolCall.data.content` / `rawPayload`. function boundToolCallContentEntries( content: ReadonlyArray<EffectAcpSchema.ToolCallContent>, ): ReadonlyArray<EffectAcpSchema.ToolCallContent> { let changed = false; const bounded = content.map((entry) => { const text = toolCallContentText(entry); if (text === undefined || text.length <= TOOL_CALL_CONTENT_MAX_CHARS) { return entry; } changed = true; const trimmed = text.trim(); return { type: "content", content: { type: "text", text: boundToolCallOutputText(trimmed.length > 0 ? trimmed : text), }, } as const; }); return changed ? bounded : content; } function extractTextContentFromToolCallContent( content: ReadonlyArray<EffectAcpSchema.ToolCallContent> | null | undefined, ): ExtractedToolCallContent { if (!content) { return { text: undefined, content: undefined }; } const chunks: Array<string> = []; for (const entry of content) { const text = toolCallContentText(entry)?.trim(); if (text) { chunks.push(text); } } if (chunks.length === 0) { return { text: undefined, content: boundToolCallContentEntries(content) }; } const joined = chunks.join("\n"); if (joined.length <= TOOL_CALL_CONTENT_MAX_CHARS) { return { text: joined, content: boundToolCallContentEntries(content) }; } const bounded = boundToolCallOutputText(joined); const tail = joined.slice(joined.length - TOOL_CALL_CONTENT_MAX_CHARS); return { text: bounded, content: distributeRetainedTailAcrossContent(content, tail), }; } // Walk the original text entries from the joined tail window so a retained slice that // spans entries around an image/diff stays on those entries. Non-text kinds keep their // relative order; blank text entries are dropped; the truncation marker is prepended to // the first remaining text entry. function distributeRetainedTailAcrossContent( content: ReadonlyArray<EffectAcpSchema.ToolCallContent>, tail: string, ): ReadonlyArray<EffectAcpSchema.ToolCallContent> { const textRanges: Array< | { readonly start: number; readonly end: number; readonly text: string; } | undefined > = Array.from({ length: content.length }); let offset = 0; let seenText = false; for (const [index, entry] of content.entries()) { const text = toolCallContentText(entry)?.trim(); if (!text) { continue; } if (seenText) { offset += 1; } seenText = true; const start = offset; const end = offset + text.length; textRanges[index] = { start, end, text }; offset = end; } const tailStart = Math.max(0, offset - tail.length); let markerPending = true; return content.flatMap((entry, index) => { if (toolCallContentText(entry) === undefined) { return [entry]; } const range = textRanges[index]; if (range === undefined) { return []; } const overlapStart = Math.max(range.start, tailStart); const overlapEnd = Math.min(range.end, offset); if (overlapEnd <= overlapStart) { return []; } let piece = range.text.slice(overlapStart - range.start, overlapEnd - range.start); if (markerPending) { piece = `${TOOL_CALL_CONTENT_TRUNCATION_MARKER}${piece}`; markerPending = false; } return [{ type: "content", content: { type: "text", text: piece } } as const]; }); } function normalizeToolKind(kind: unknown): string | undefined { return typeof kind === "string" && kind.trim().length > 0 ? kind.trim() : undefined; } /** * Map an ACP tool kind onto the canonical runtime item type used by the * thread activity model. Unknown kinds fall back to a generic tool call. */ export 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<EffectAcpSchema.ToolCallContent> | null | undefined; readonly locations?: ReadonlyArray<EffectAcpSchema.ToolCallLocation> | 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 extractedContent = extractTextContentFromToolCallContent(input.content); const textContent = extractedContent.text; const normalizedTitle = title && title.toLowerCase() !== "terminal" && title.toLowerCase() !== "tool call" ? title : undefined; const data: Record<string, unknown> = { toolCallId }; const kind = normalizeToolKind(input.kind); if (kind) { data.kind = kind; } if (command) {