Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
/**
|
|
* ACP tool snapshot merge + wire mapping — lifted from Paseo acp-agent.ts patterns.
|
|
* Stable toolCallId, merge on tool_call_update, status lifecycle for UI + DB.
|
|
*/
|
|
import type { ToolCall, ToolCallUpdate, ToolCallStatus, ToolKind } from '@agentclientprotocol/sdk';
|
|
|
|
export type AcpToolLifecycleStatus = 'running' | 'completed' | 'failed' | 'canceled';
|
|
|
|
export interface AcpToolSnapshot {
|
|
toolCallId: string;
|
|
title: string;
|
|
kind?: ToolKind | null;
|
|
status?: ToolCallStatus | null;
|
|
rawInput?: unknown;
|
|
rawOutput?: unknown;
|
|
}
|
|
|
|
export interface AcpWireMeta {
|
|
status: AcpToolLifecycleStatus;
|
|
kind?: string | null;
|
|
title?: string;
|
|
output?: unknown;
|
|
error?: string;
|
|
}
|
|
|
|
function coalesceDefined<T>(next: T | null | undefined, previous: T | null | undefined, fallback: T | null): T | null {
|
|
if (next !== undefined && next !== null) return next;
|
|
if (previous !== undefined && previous !== null) return previous;
|
|
return fallback;
|
|
}
|
|
|
|
export function mergeToolSnapshot(
|
|
toolCallId: string,
|
|
update: ToolCall | ToolCallUpdate,
|
|
previous?: AcpToolSnapshot,
|
|
): AcpToolSnapshot {
|
|
return {
|
|
toolCallId,
|
|
title: update.title ?? previous?.title ?? toolCallId,
|
|
kind: update.kind ?? previous?.kind ?? null,
|
|
status: update.status ?? previous?.status ?? null,
|
|
rawInput: update.rawInput !== undefined ? update.rawInput : previous?.rawInput,
|
|
rawOutput: update.rawOutput !== undefined ? update.rawOutput : previous?.rawOutput,
|
|
};
|
|
}
|
|
|
|
export function mapToolLifecycleStatus(
|
|
status: ToolCallStatus | null | undefined,
|
|
rawOutput?: unknown,
|
|
): AcpToolLifecycleStatus {
|
|
if (rawOutput === 'canceled') return 'canceled';
|
|
switch (status) {
|
|
case 'completed':
|
|
return 'completed';
|
|
case 'failed':
|
|
return 'failed';
|
|
case 'pending':
|
|
case 'in_progress':
|
|
default:
|
|
return 'running';
|
|
}
|
|
}
|
|
|
|
function readErrorMessage(rawOutput: unknown): string | undefined {
|
|
if (typeof rawOutput === 'string' && rawOutput.trim()) return rawOutput;
|
|
if (rawOutput && typeof rawOutput === 'object' && !Array.isArray(rawOutput)) {
|
|
const rec = rawOutput as Record<string, unknown>;
|
|
const msg = rec.message ?? rec.error ?? rec.reason;
|
|
if (typeof msg === 'string' && msg.trim()) return msg;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
return value as Record<string, unknown>;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
export function snapshotToWireToolCall(snapshot: AcpToolSnapshot): {
|
|
id: string;
|
|
name: string;
|
|
args: Record<string, unknown>;
|
|
} {
|
|
const lifecycle = mapToolLifecycleStatus(snapshot.status, snapshot.rawOutput);
|
|
const input = asRecord(snapshot.rawInput);
|
|
const error = lifecycle === 'failed' ? readErrorMessage(snapshot.rawOutput) : undefined;
|
|
const meta: AcpWireMeta = {
|
|
status: lifecycle,
|
|
kind: snapshot.kind ?? null,
|
|
title: snapshot.title,
|
|
...(snapshot.rawOutput !== undefined ? { output: snapshot.rawOutput } : {}),
|
|
...(error ? { error } : {}),
|
|
};
|
|
return {
|
|
id: snapshot.toolCallId,
|
|
name: String(snapshot.kind ?? snapshot.title),
|
|
args: { ...input, _acp: meta },
|
|
};
|
|
}
|
|
|
|
export function snapshotToPartPayload(snapshot: AcpToolSnapshot): {
|
|
id: string;
|
|
name: string;
|
|
args: Record<string, unknown>;
|
|
} {
|
|
const wire = snapshotToWireToolCall(snapshot);
|
|
return { id: wire.id, name: wire.name, args: wire.args };
|
|
}
|
|
|
|
export function synthesizeCanceledSnapshots(snapshots: Iterable<AcpToolSnapshot>): AcpToolSnapshot[] {
|
|
const out: AcpToolSnapshot[] = [];
|
|
for (const snapshot of snapshots) {
|
|
if (mapToolLifecycleStatus(snapshot.status) === 'running') {
|
|
out.push({ ...snapshot, status: 'failed', rawOutput: snapshot.rawOutput ?? 'canceled' });
|
|
}
|
|
}
|
|
return out;
|
|
}
|