757 lines
24 KiB
TypeScript
757 lines
24 KiB
TypeScript
import type {
|
|
ConversationContentBlock,
|
|
ConversationMessageNode,
|
|
ConversationPatch,
|
|
ConversationSnapshot,
|
|
ConversationToolNode,
|
|
PublicUsage,
|
|
} from '../contracts';
|
|
import { isAIGatewayUserContextMissing } from '../../../shared/ai-gateway-error-details';
|
|
import type { PiRpcEvent } from './rpc-client';
|
|
import { subagentDetailsOfResult } from '../subagent-protocol';
|
|
import {
|
|
isProductToolName,
|
|
productToolDetailsOfResult,
|
|
} from '../product-tool-protocol';
|
|
|
|
export interface PiEventProjectorOptions {
|
|
createId(): string;
|
|
now?: () => number;
|
|
projectImage?(image: PiImageProjectionInput): Promise<PiProjectedAttachment>;
|
|
}
|
|
|
|
export interface PiImageProjectionInput {
|
|
conversationId: string;
|
|
data: string;
|
|
mime: string;
|
|
source: 'live' | 'session';
|
|
}
|
|
|
|
export interface PiProjectedAttachment {
|
|
attachmentId: string;
|
|
mime: string;
|
|
}
|
|
|
|
export interface PiProjectionDiagnostic {
|
|
eventType: string;
|
|
reason: 'unsupported-event';
|
|
}
|
|
|
|
function retryFailure(message: string) {
|
|
return {
|
|
code: 'CODING_RUNTIME_START_FAILED' as const,
|
|
message,
|
|
recoverable: true,
|
|
};
|
|
}
|
|
|
|
function providerFailure(message: unknown) {
|
|
if (typeof message === 'string' && isAIGatewayUserContextMissing(message)) {
|
|
return {
|
|
code: 'CODING_PROVIDER_AUTH_REQUIRED' as const,
|
|
message: '模型服务身份上下文无效,请重试;若仍失败请重新登录。',
|
|
recoverable: true,
|
|
};
|
|
}
|
|
return retryFailure('模型服务请求失败,请稍后重试。');
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: null;
|
|
}
|
|
|
|
function usageOf(value: unknown): PublicUsage | undefined {
|
|
const usage = asRecord(value);
|
|
if (!usage
|
|
|| typeof usage.input !== 'number'
|
|
|| typeof usage.output !== 'number') return undefined;
|
|
return {
|
|
inputTokens: usage.input,
|
|
outputTokens: usage.output,
|
|
...(typeof usage.cacheRead === 'number' ? { cacheReadTokens: usage.cacheRead } : {}),
|
|
...(typeof usage.cacheWrite === 'number' ? { cacheWriteTokens: usage.cacheWrite } : {}),
|
|
};
|
|
}
|
|
|
|
function stopReasonOf(value: unknown): ConversationMessageNode['stopReason'] | undefined {
|
|
if (value === 'toolUse') return 'tool-use';
|
|
if (value === 'stop' || value === 'length' || value === 'error' || value === 'aborted') {
|
|
return value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function currentAssistant(snapshot: ConversationSnapshot): ConversationMessageNode | undefined {
|
|
return snapshot.nodes.findLast(
|
|
(node): node is ConversationMessageNode => node.kind === 'message'
|
|
&& node.role === 'assistant'
|
|
&& node.status === 'streaming',
|
|
);
|
|
}
|
|
|
|
function outputBlocks(toolId: string, value: unknown): ConversationContentBlock[] {
|
|
const result = asRecord(value);
|
|
if (!result || !Array.isArray(result.content)) return [];
|
|
return result.content.flatMap((item, contentIndex) => {
|
|
const block = asRecord(item);
|
|
if (!block || block.type !== 'text' || typeof block.text !== 'string') return [];
|
|
return [{
|
|
kind: 'text' as const,
|
|
id: `${toolId}:output:${contentIndex}`,
|
|
text: block.text,
|
|
status: 'complete' as const,
|
|
}];
|
|
});
|
|
}
|
|
|
|
function projectToolResult(
|
|
snapshot: ConversationSnapshot,
|
|
tool: ConversationToolNode,
|
|
status: ConversationToolNode['status'],
|
|
value: unknown,
|
|
): ConversationPatch[] {
|
|
const subagentDetails = subagentDetailsOfResult(value);
|
|
const details = subagentDetails ?? productToolDetailsOfResult(value);
|
|
const result = asRecord(value);
|
|
const unknownProductDetails = (tool.toolName === 'subagent' || isProductToolName(tool.toolName))
|
|
&& result?.details !== undefined
|
|
&& !details;
|
|
const output = unknownProductDetails
|
|
? [{
|
|
kind: 'text' as const,
|
|
id: `${tool.id}:output:unavailable`,
|
|
text: tool.toolName === 'subagent'
|
|
? 'Subagent details are unavailable for this version.'
|
|
: 'Tool details are unavailable for this version.',
|
|
status: 'complete' as const,
|
|
}]
|
|
: outputBlocks(tool.id, value);
|
|
const patches: ConversationPatch[] = [{
|
|
op: 'tool.upsert',
|
|
node: {
|
|
...tool,
|
|
status,
|
|
output,
|
|
...(details ? { details } : {}),
|
|
},
|
|
}];
|
|
const runId = snapshot.run.runId;
|
|
if (subagentDetails && runId) {
|
|
patches.push({
|
|
op: 'subagent.upsert',
|
|
node: {
|
|
kind: 'subagent',
|
|
id: `subagent:${subagentDetails.dispatchId}`,
|
|
runId,
|
|
details: subagentDetails,
|
|
},
|
|
});
|
|
}
|
|
return patches;
|
|
}
|
|
|
|
function currentTool(snapshot: ConversationSnapshot, toolCallId: string): ConversationToolNode | undefined {
|
|
return snapshot.nodes.find(
|
|
(node): node is ConversationToolNode => node.kind === 'tool'
|
|
&& node.toolCallId === toolCallId,
|
|
);
|
|
}
|
|
|
|
async function liveContentBlocks(
|
|
messageId: string,
|
|
content: unknown,
|
|
conversationId: string,
|
|
projectImage: PiEventProjectorOptions['projectImage'],
|
|
): Promise<ConversationContentBlock[]> {
|
|
const values = typeof content === 'string'
|
|
? [{ type: 'text', text: content }]
|
|
: Array.isArray(content) ? content : [];
|
|
const blocks: ConversationContentBlock[] = [];
|
|
for (let contentIndex = 0; contentIndex < values.length; contentIndex += 1) {
|
|
const block = asRecord(values[contentIndex]);
|
|
if (block?.type === 'text' && typeof block.text === 'string') {
|
|
blocks.push({
|
|
kind: 'text',
|
|
id: `${messageId}:content:${contentIndex}`,
|
|
text: block.text,
|
|
status: 'complete',
|
|
});
|
|
} else if (block?.type === 'thinking' && typeof block.thinking === 'string') {
|
|
blocks.push({
|
|
kind: 'thinking',
|
|
id: `${messageId}:content:${contentIndex}`,
|
|
text: block.thinking,
|
|
status: 'complete',
|
|
});
|
|
} else if (block?.type === 'image'
|
|
&& typeof block.data === 'string'
|
|
&& typeof block.mimeType === 'string'
|
|
&& projectImage) {
|
|
const attachment = await projectImage({
|
|
conversationId,
|
|
data: block.data,
|
|
mime: block.mimeType,
|
|
source: 'live',
|
|
});
|
|
blocks.push({
|
|
kind: 'image',
|
|
id: `${messageId}:content:${contentIndex}`,
|
|
attachmentId: attachment.attachmentId,
|
|
mime: attachment.mime,
|
|
});
|
|
}
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
export class PiEventProjector {
|
|
private readonly createId: () => string;
|
|
private readonly now: () => number;
|
|
private readonly projectImage: PiEventProjectorOptions['projectImage'];
|
|
private assistantMessageId: string | null = null;
|
|
private readonly toolDrafts = new Map<number, { id: string; argumentsText: string }>();
|
|
private compactionId: string | null = null;
|
|
private summarizationRetryBaseRun: ConversationSnapshot['run'] | null = null;
|
|
private readonly diagnostics: PiProjectionDiagnostic[] = [];
|
|
|
|
constructor(options: PiEventProjectorOptions) {
|
|
this.createId = options.createId;
|
|
this.now = options.now ?? Date.now;
|
|
this.projectImage = options.projectImage;
|
|
}
|
|
|
|
getDiagnostics(): PiProjectionDiagnostic[] {
|
|
return structuredClone(this.diagnostics);
|
|
}
|
|
|
|
async project(
|
|
snapshot: ConversationSnapshot,
|
|
event: PiRpcEvent,
|
|
): Promise<ConversationPatch[]> {
|
|
if (event.type === 'agent_start') {
|
|
return [{
|
|
op: 'run.state',
|
|
run: {
|
|
...snapshot.run,
|
|
status: 'running',
|
|
runId: snapshot.run.runId ?? this.createId(),
|
|
startedAt: snapshot.run.startedAt ?? this.now(),
|
|
},
|
|
}];
|
|
}
|
|
|
|
if (event.type === 'turn_start' || event.type === 'turn_end') {
|
|
const runId = snapshot.run.runId ?? this.createId();
|
|
return [{
|
|
op: 'boundary.upsert',
|
|
node: {
|
|
kind: 'boundary',
|
|
id: this.createId(),
|
|
runId,
|
|
boundary: event.type === 'turn_start' ? 'turn-start' : 'turn-end',
|
|
},
|
|
}];
|
|
}
|
|
|
|
if (event.type === 'auto_retry_start'
|
|
&& typeof event.attempt === 'number'
|
|
&& typeof event.delayMs === 'number') {
|
|
const runId = snapshot.run.runId ?? this.createId();
|
|
return [
|
|
{
|
|
op: 'boundary.upsert',
|
|
node: {
|
|
kind: 'boundary',
|
|
id: this.createId(),
|
|
runId,
|
|
boundary: 'retry',
|
|
attempt: event.attempt,
|
|
delayMs: event.delayMs,
|
|
},
|
|
},
|
|
{
|
|
op: 'run.state',
|
|
run: {
|
|
...snapshot.run,
|
|
status: 'retrying',
|
|
runId,
|
|
retry: { attempt: event.attempt, delayMs: event.delayMs },
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
if (event.type === 'auto_retry_end' && typeof event.success === 'boolean') {
|
|
const { retry: _retry, ...run } = snapshot.run;
|
|
if (event.success) {
|
|
return [{ op: 'run.state', run: { ...run, status: 'running' } }];
|
|
}
|
|
return [{
|
|
op: 'run.state',
|
|
run: {
|
|
...run,
|
|
status: 'error',
|
|
terminalReason: 'failed',
|
|
error: providerFailure(event.finalError),
|
|
},
|
|
}];
|
|
}
|
|
|
|
if (event.type === 'summarization_retry_scheduled'
|
|
&& typeof event.attempt === 'number'
|
|
&& typeof event.delayMs === 'number') {
|
|
const runId = snapshot.run.runId ?? this.createId();
|
|
this.summarizationRetryBaseRun ??= { ...snapshot.run, runId };
|
|
return [
|
|
{
|
|
op: 'boundary.upsert',
|
|
node: {
|
|
kind: 'boundary',
|
|
id: this.createId(),
|
|
runId,
|
|
boundary: 'retry',
|
|
attempt: event.attempt,
|
|
delayMs: event.delayMs,
|
|
},
|
|
},
|
|
{
|
|
op: 'run.state',
|
|
run: {
|
|
...snapshot.run,
|
|
status: 'retrying',
|
|
runId,
|
|
retry: { attempt: event.attempt, delayMs: event.delayMs },
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
if (event.type === 'summarization_retry_attempt_start'
|
|
&& (event.source === 'branchSummary' || event.source === 'compaction')) {
|
|
const base = this.summarizationRetryBaseRun ?? snapshot.run;
|
|
const { retry: _retry, ...run } = base;
|
|
return [
|
|
...(event.source === 'compaction'
|
|
? [{
|
|
op: 'context.replace' as const,
|
|
context: { ...snapshot.context, compaction: 'running' as const },
|
|
}]
|
|
: []),
|
|
{
|
|
op: 'run.state',
|
|
run: {
|
|
...run,
|
|
status: event.source === 'compaction' ? 'compacting' : run.status,
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
if (event.type === 'summarization_retry_finished') {
|
|
const base = this.summarizationRetryBaseRun ?? snapshot.run;
|
|
this.summarizationRetryBaseRun = null;
|
|
const { retry: _retry, ...run } = base;
|
|
return [{ op: 'run.state', run }];
|
|
}
|
|
|
|
if (event.type === 'compaction_start') {
|
|
const runId = snapshot.run.runId ?? this.createId();
|
|
const id = this.createId();
|
|
this.compactionId = id;
|
|
return [
|
|
{
|
|
op: 'compaction.upsert',
|
|
node: {
|
|
kind: 'compaction',
|
|
id,
|
|
runId,
|
|
source: event.reason === 'manual' ? 'manual' : 'automatic',
|
|
status: 'running',
|
|
willRetry: false,
|
|
},
|
|
},
|
|
{
|
|
op: 'context.replace',
|
|
context: { ...snapshot.context, compaction: 'running', lastCompactionId: id },
|
|
},
|
|
{ op: 'run.state', run: { ...snapshot.run, status: 'compacting', runId } },
|
|
];
|
|
}
|
|
|
|
if (event.type === 'compaction_end') {
|
|
const id = this.compactionId ?? this.createId();
|
|
const runId = snapshot.run.runId ?? this.createId();
|
|
this.compactionId = null;
|
|
const compactionFailed = typeof event.errorMessage === 'string';
|
|
const { retry: _retry, ...run } = snapshot.run;
|
|
return [
|
|
{
|
|
op: 'compaction.upsert',
|
|
node: {
|
|
kind: 'compaction',
|
|
id,
|
|
runId,
|
|
source: event.reason === 'manual' ? 'manual' : 'automatic',
|
|
status: event.aborted === true || typeof event.errorMessage === 'string'
|
|
? 'error'
|
|
: 'complete',
|
|
willRetry: event.willRetry === true,
|
|
},
|
|
},
|
|
{
|
|
op: 'context.replace',
|
|
context: { ...snapshot.context, compaction: 'idle', lastCompactionId: id },
|
|
},
|
|
{
|
|
op: 'run.state',
|
|
run: {
|
|
...run,
|
|
status: event.willRetry === true
|
|
? 'running'
|
|
: compactionFailed ? 'error' : snapshot.run.status,
|
|
runId,
|
|
...(compactionFailed
|
|
? {
|
|
terminalReason: 'failed' as const,
|
|
error: retryFailure('The local Agent summarization failed'),
|
|
}
|
|
: {}),
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
if (event.type === 'queue_update'
|
|
&& Array.isArray(event.steering)
|
|
&& Array.isArray(event.followUp)) {
|
|
const existing = [...snapshot.queue.items];
|
|
const item = (mode: 'steer' | 'follow-up', text: string) => {
|
|
const matchIndex = existing.findIndex((candidate) => candidate.mode === mode
|
|
&& candidate.text === text);
|
|
if (matchIndex >= 0) return existing.splice(matchIndex, 1)[0]!;
|
|
const id = this.createId();
|
|
return { id, clientRequestId: id, mode, text, attachmentIds: [] };
|
|
};
|
|
return [{
|
|
op: 'queue.replace',
|
|
queue: {
|
|
items: [
|
|
...event.steering.filter((value): value is string => typeof value === 'string')
|
|
.map((text) => item('steer', text)),
|
|
...event.followUp.filter((value): value is string => typeof value === 'string')
|
|
.map((text) => item('follow-up', text)),
|
|
],
|
|
},
|
|
}];
|
|
}
|
|
|
|
if (event.type === 'agent_settled') {
|
|
const terminalReason = snapshot.run.status === 'aborting'
|
|
? 'aborted' as const
|
|
: snapshot.run.status === 'error' || snapshot.run.terminalReason === 'failed'
|
|
? 'failed' as const
|
|
: 'completed' as const;
|
|
this.summarizationRetryBaseRun = null;
|
|
return [
|
|
{
|
|
op: 'run.state',
|
|
run: {
|
|
status: 'idle',
|
|
...(snapshot.run.runId ? { runId: snapshot.run.runId } : {}),
|
|
settledAt: this.now(),
|
|
terminalReason,
|
|
...(terminalReason === 'failed' && snapshot.run.error
|
|
? { error: snapshot.run.error }
|
|
: {}),
|
|
},
|
|
},
|
|
{ op: 'queue.replace', queue: { items: [] } },
|
|
];
|
|
}
|
|
|
|
if (event.type === 'agent_end' && event.willRetry === false && Array.isArray(event.messages)) {
|
|
const assistant = [...event.messages]
|
|
.reverse()
|
|
.map(asRecord)
|
|
.find((message) => message?.role === 'assistant');
|
|
if (assistant?.stopReason === 'error') {
|
|
return [{
|
|
op: 'run.state',
|
|
run: {
|
|
...snapshot.run,
|
|
status: 'error',
|
|
terminalReason: 'failed',
|
|
error: providerFailure(assistant.errorMessage),
|
|
},
|
|
}];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
if (event.type === 'extension_ui_request'
|
|
&& typeof event.id === 'string'
|
|
&& typeof event.method === 'string'
|
|
&& typeof event.title === 'string'
|
|
&& ['select', 'confirm', 'input', 'editor'].includes(event.method)) {
|
|
const kind = event.method as 'select' | 'confirm' | 'input' | 'editor';
|
|
return [{
|
|
op: 'interaction.upsert',
|
|
interaction: {
|
|
id: event.id,
|
|
conversationId: snapshot.conversation.id,
|
|
runId: snapshot.run.runId ?? `interaction:${event.id}`,
|
|
kind,
|
|
title: event.title,
|
|
...(typeof event.message === 'string' ? { message: event.message } : {}),
|
|
...(kind === 'select' && Array.isArray(event.options)
|
|
? {
|
|
options: event.options
|
|
.filter((option): option is string => typeof option === 'string')
|
|
.map((label, index) => ({
|
|
id: `${event.id}:option:${index}`,
|
|
label,
|
|
})),
|
|
}
|
|
: {}),
|
|
status: 'pending',
|
|
},
|
|
}];
|
|
}
|
|
|
|
if (event.type === 'message_start') {
|
|
const message = asRecord(event.message);
|
|
if (message?.role === 'user') {
|
|
const optimistic = snapshot.nodes.findLast(
|
|
(node): node is ConversationMessageNode => node.kind === 'message'
|
|
&& node.role === 'user'
|
|
&& node.status === 'optimistic',
|
|
);
|
|
const id = optimistic?.id ?? this.createId();
|
|
return [{
|
|
op: 'message.upsert',
|
|
node: {
|
|
kind: 'message',
|
|
id,
|
|
...(optimistic?.clientRequestId
|
|
? { clientRequestId: optimistic.clientRequestId }
|
|
: {}),
|
|
role: 'user',
|
|
status: 'complete',
|
|
blocks: await liveContentBlocks(
|
|
id,
|
|
message.content,
|
|
snapshot.conversation.id,
|
|
this.projectImage,
|
|
),
|
|
},
|
|
}];
|
|
}
|
|
if (message?.role !== 'assistant') return [];
|
|
const id = this.createId();
|
|
this.assistantMessageId = id;
|
|
return [{
|
|
op: 'message.upsert',
|
|
node: {
|
|
kind: 'message',
|
|
id,
|
|
role: 'assistant',
|
|
status: 'streaming',
|
|
blocks: [],
|
|
...(usageOf(message.usage) ? { usage: usageOf(message.usage) } : {}),
|
|
},
|
|
}];
|
|
}
|
|
|
|
if (event.type === 'message_update') {
|
|
const update = asRecord(event.assistantMessageEvent);
|
|
const message = currentAssistant(snapshot);
|
|
if (!update || !message || typeof update.contentIndex !== 'number') return [];
|
|
const blockId = `${message.id}:content:${update.contentIndex}`;
|
|
if (update.type === 'text_start') {
|
|
return [{
|
|
op: 'message.upsert',
|
|
node: {
|
|
...message,
|
|
blocks: [
|
|
...message.blocks.filter((block) => block.id !== blockId),
|
|
{ kind: 'text', id: blockId, text: '', status: 'streaming' },
|
|
],
|
|
...(usageOf(event.usage) ? { usage: usageOf(event.usage) } : {}),
|
|
},
|
|
}];
|
|
}
|
|
if (update.type === 'text_delta' && typeof update.delta === 'string') {
|
|
return [{
|
|
op: 'message.block-delta',
|
|
messageId: message.id,
|
|
blockId,
|
|
delta: update.delta,
|
|
}];
|
|
}
|
|
if (update.type === 'text_end' && typeof update.content === 'string') {
|
|
return [{
|
|
op: 'message.upsert',
|
|
node: {
|
|
...message,
|
|
blocks: message.blocks.map((block) => block.id === blockId && block.kind === 'text'
|
|
? { ...block, text: update.content as string, status: 'complete' }
|
|
: block),
|
|
...(usageOf(event.usage) ? { usage: usageOf(event.usage) } : {}),
|
|
},
|
|
}];
|
|
}
|
|
if (update.type === 'thinking_start') {
|
|
return [{
|
|
op: 'message.upsert',
|
|
node: {
|
|
...message,
|
|
blocks: [
|
|
...message.blocks.filter((block) => block.id !== blockId),
|
|
{ kind: 'thinking', id: blockId, text: '', status: 'streaming' },
|
|
],
|
|
...(usageOf(event.usage) ? { usage: usageOf(event.usage) } : {}),
|
|
},
|
|
}];
|
|
}
|
|
if (update.type === 'thinking_delta' && typeof update.delta === 'string') {
|
|
return [{
|
|
op: 'message.block-delta',
|
|
messageId: message.id,
|
|
blockId,
|
|
delta: update.delta,
|
|
}];
|
|
}
|
|
if (update.type === 'thinking_end' && typeof update.content === 'string') {
|
|
return [{
|
|
op: 'message.upsert',
|
|
node: {
|
|
...message,
|
|
blocks: message.blocks.map((block) => block.id === blockId && block.kind === 'thinking'
|
|
? { ...block, text: update.content as string, status: 'complete' }
|
|
: block),
|
|
...(usageOf(event.usage) ? { usage: usageOf(event.usage) } : {}),
|
|
},
|
|
}];
|
|
}
|
|
if (update.type === 'toolcall_start') {
|
|
const id = this.createId();
|
|
this.toolDrafts.set(update.contentIndex, { id, argumentsText: '' });
|
|
return [{
|
|
op: 'tool.upsert',
|
|
node: {
|
|
kind: 'tool',
|
|
id,
|
|
toolCallId: `pending:${id}`,
|
|
toolName: 'tool',
|
|
title: 'tool',
|
|
inputText: '',
|
|
status: 'declared',
|
|
output: [],
|
|
},
|
|
}];
|
|
}
|
|
if (update.type === 'toolcall_delta' && typeof update.delta === 'string') {
|
|
const draft = this.toolDrafts.get(update.contentIndex);
|
|
if (draft) draft.argumentsText += update.delta;
|
|
return [];
|
|
}
|
|
if (update.type === 'toolcall_end') {
|
|
const toolCall = asRecord(update.toolCall);
|
|
if (!toolCall || typeof toolCall.id !== 'string' || typeof toolCall.name !== 'string') return [];
|
|
const draft = this.toolDrafts.get(update.contentIndex) ?? {
|
|
id: this.createId(),
|
|
argumentsText: '',
|
|
};
|
|
this.toolDrafts.delete(update.contentIndex);
|
|
return [{
|
|
op: 'tool.upsert',
|
|
node: {
|
|
kind: 'tool',
|
|
id: draft.id,
|
|
toolCallId: toolCall.id,
|
|
toolName: toolCall.name,
|
|
title: toolCall.name,
|
|
inputText: asRecord(toolCall.arguments)
|
|
? JSON.stringify(toolCall.arguments)
|
|
: draft.argumentsText,
|
|
status: 'declared',
|
|
output: [],
|
|
},
|
|
}];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
if (event.type === 'tool_execution_start') {
|
|
if (typeof event.toolCallId !== 'string') return [];
|
|
const tool = currentTool(snapshot, event.toolCallId);
|
|
if (!tool) return [];
|
|
return [{ op: 'tool.upsert', node: { ...tool, status: 'running' } }];
|
|
}
|
|
|
|
if (event.type === 'tool_execution_update') {
|
|
if (typeof event.toolCallId !== 'string') return [];
|
|
const tool = currentTool(snapshot, event.toolCallId);
|
|
if (!tool) return [];
|
|
return projectToolResult(snapshot, tool, 'running', event.partialResult);
|
|
}
|
|
|
|
if (event.type === 'tool_execution_end') {
|
|
if (typeof event.toolCallId !== 'string') return [];
|
|
const tool = currentTool(snapshot, event.toolCallId);
|
|
if (!tool) return [];
|
|
return projectToolResult(
|
|
snapshot,
|
|
tool,
|
|
event.isError === true ? 'error' : 'complete',
|
|
event.result,
|
|
);
|
|
}
|
|
|
|
if (event.type === 'message_end') {
|
|
const message = asRecord(event.message);
|
|
if (message?.role === 'toolResult' && typeof message.toolCallId === 'string') {
|
|
const tool = currentTool(snapshot, message.toolCallId);
|
|
if (!tool) return [];
|
|
return projectToolResult(
|
|
snapshot,
|
|
tool,
|
|
message.isError === true ? 'error' : 'complete',
|
|
message,
|
|
);
|
|
}
|
|
if (message?.role !== 'assistant') return [];
|
|
const current = currentAssistant(snapshot);
|
|
const id = current?.id ?? this.assistantMessageId ?? this.createId();
|
|
this.assistantMessageId = null;
|
|
return [{
|
|
op: 'message.upsert',
|
|
node: {
|
|
kind: 'message',
|
|
id,
|
|
role: 'assistant',
|
|
status: message.stopReason === 'aborted'
|
|
? 'aborted'
|
|
: message.stopReason === 'error' ? 'error' : 'complete',
|
|
blocks: await liveContentBlocks(
|
|
id,
|
|
message.content,
|
|
snapshot.conversation.id,
|
|
this.projectImage,
|
|
),
|
|
...(usageOf(message.usage) ? { usage: usageOf(message.usage) } : {}),
|
|
...(stopReasonOf(message.stopReason) ? { stopReason: stopReasonOf(message.stopReason) } : {}),
|
|
},
|
|
}];
|
|
}
|
|
|
|
if (event.type !== 'agent_end') {
|
|
this.diagnostics.push({ eventType: event.type, reason: 'unsupported-event' });
|
|
if (this.diagnostics.length > 32) this.diagnostics.shift();
|
|
}
|
|
return [];
|
|
}
|
|
}
|