v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes
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>
This commit is contained in:
120
apps/coder/src/services/acp-tool-snapshot.ts
Normal file
120
apps/coder/src/services/acp-tool-snapshot.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user