/* oxlint-disable typescript-eslint/no-unsafe-declaration-merging, eslint-plugin-import/namespace -- Event2 class+payload-interface declaration merging is the sanctioned event-declaration idiom. */ import { z } from 'zod'; import type { PromptOrigin } from '#/agent/contextMemory/types'; import { parseDaemonFileUrl } from '#/agent/media/mediaRef'; import { AgentEvent2, registerEvent2Class } from '#/app/event/event2'; import type { FinishReason } from '#human/llm/finish-reason'; import type { ContentPart, TextPart } from '#human/llm/message'; import type { TokenUsage } from '#human/llm/usage'; export type TurnEndReason = 'completed' | 'cancelled' | 'failed' | 'blocked'; export type TurnInterruptReason = | 'user_cancelled' | 'aborted' | 'max_steps' | 'error' | 'filtered' | 'blocked'; export interface TurnPromptAttachmentFile { readonly kind: 'file'; readonly name: string; readonly mediaType: string; readonly size: number; readonly path: string; } export type TurnPromptAttachment = | { readonly kind: 'image' | 'video' | 'audio'; readonly fileId: string; readonly name?: string } | TurnPromptAttachmentFile; export interface TurnStartedPayload { readonly agentId: string; readonly turnId: number; readonly promptId?: string; readonly origin: PromptOrigin; readonly prompt?: string; readonly promptAttachments?: readonly TurnPromptAttachment[]; } export class TurnStarted extends AgentEvent2 { static override readonly type = 'turn.started'; static override readonly observable = true; } export interface TurnStarted extends TurnStartedPayload {} export function turnPromptText( input: readonly ContentPart[], origin?: PromptOrigin, ): string | undefined { const bundledBlocks = origin?.kind === 'user' ? (origin.skillActivations?.length ?? 0) : 0; const text = input .filter((part): part is TextPart => part.type === 'text') .slice(bundledBlocks) .map((part) => part.text) .join(''); return text.length > 0 ? text : undefined; } export function turnPromptAttachments( input: readonly ContentPart[], origin?: PromptOrigin, ): TurnStartedPayload['promptAttachments'] { const attachments: TurnPromptAttachment[] = []; const promptMediaFileId = (url: string, id: string | undefined): string | undefined => { const fileId = parseDaemonFileUrl(url)?.fileId; if (id === undefined) return fileId; return fileId === id ? id : undefined; }; for (const part of input) { if (part.type === 'image_url') { const fileId = promptMediaFileId(part.imageUrl.url, part.imageUrl.id); if (fileId !== undefined) attachments.push({ kind: 'image', fileId, name: part.imageUrl.name }); } else if (part.type === 'video_url') { const fileId = promptMediaFileId(part.videoUrl.url, part.videoUrl.id); if (fileId !== undefined) attachments.push({ kind: 'video', fileId, name: part.videoUrl.name }); } else if (part.type === 'audio_url') { const fileId = promptMediaFileId(part.audioUrl.url, part.audioUrl.id); if (fileId !== undefined) attachments.push({ kind: 'audio', fileId }); } } if (origin?.kind === 'user' || origin?.kind === 'skill_activation') { for (const attachment of origin.attachments ?? []) { attachments.push({ kind: 'file', ...attachment }); } } return attachments.length > 0 ? attachments : undefined; } export function isDisplayablePromptOrigin(origin: PromptOrigin): boolean { if (origin.kind === 'user') return true; if (origin.kind === 'system_trigger' && origin.name === 'subagent') return true; return ( (origin.kind === 'skill_activation' || origin.kind === 'plugin_command') && origin.trigger === 'user-slash' ); } export interface TurnStepStartedPayload { readonly agentId: string; readonly turnId: number; readonly step: number; readonly stepId?: string; } export class TurnStepStarted extends AgentEvent2 { static override readonly type = 'turn.step.started'; static override readonly observable = true; } export interface TurnStepStarted extends TurnStepStartedPayload {} export interface TurnStepCompletedPayload { readonly agentId: string; readonly turnId: number; readonly step: number; readonly stepId?: string; readonly usage?: TokenUsage; readonly finishReason?: string; readonly llmFirstTokenLatencyMs?: number; readonly llmStreamDurationMs?: number; readonly llmRequestBuildMs?: number; readonly llmServerFirstTokenMs?: number; readonly llmServerDecodeMs?: number; readonly llmClientConsumeMs?: number; readonly llmClientBlockedMs?: number; readonly providerFinishReason?: FinishReason; readonly rawFinishReason?: string; } export class TurnStepCompleted extends AgentEvent2 { static override readonly type = 'turn.step.completed'; static override readonly observable = true; } export interface TurnStepCompleted extends TurnStepCompletedPayload {} export interface TurnStepInterruptedPayload { readonly agentId: string; readonly turnId: number; readonly step: number; readonly stepId?: string; readonly reason: string; readonly message?: string; } const turnStepInterruptedSchema = z.object({ agentId: z.string(), turnId: z.number(), step: z.number(), stepId: z.string().optional(), reason: z.string(), message: z.string().optional(), }); export class TurnStepInterrupted extends AgentEvent2 { static override readonly type = 'turn.step.interrupted'; static override readonly durable = true; static override readonly observable = true; static override readonly schema = turnStepInterruptedSchema; } export interface TurnStepInterrupted extends TurnStepInterruptedPayload {} export interface TurnStepRetryingPayload { readonly agentId: string; readonly turnId: number; readonly step: number; readonly stepId?: string; readonly failedAttempt: number; readonly nextAttempt: number; readonly maxAttempts: number; readonly delayMs: number; readonly errorName: string; readonly errorMessage: string; readonly statusCode?: number; } const turnStepRetryingSchema = z.object({ agentId: z.string(), turnId: z.number(), step: z.number(), stepId: z.string().optional(), failedAttempt: z.number(), nextAttempt: z.number(), maxAttempts: z.number(), delayMs: z.number(), errorName: z.string(), errorMessage: z.string(), statusCode: z.number().optional(), }); export class TurnStepRetrying extends AgentEvent2 { static override readonly type = 'turn.step.retrying'; static override readonly durable = true; static override readonly observable = true; static override readonly schema = turnStepRetryingSchema; } export interface TurnStepRetrying extends TurnStepRetryingPayload {} export interface AssistantDeltaPayload { readonly agentId: string; readonly turnId: number; readonly delta: string; } export class AssistantDelta extends AgentEvent2 { static override readonly type = 'assistant.delta'; static override readonly observable = true; } export interface AssistantDelta extends AssistantDeltaPayload {} export interface ThinkingDeltaPayload { readonly agentId: string; readonly turnId: number; readonly delta: string; } export class ThinkingDelta extends AgentEvent2 { static override readonly type = 'thinking.delta'; static override readonly observable = true; } export interface ThinkingDelta extends ThinkingDeltaPayload {} export interface ToolCallDeltaPayload { readonly agentId: string; readonly turnId: number; readonly toolCallId: string; readonly name?: string; readonly argumentsPart?: string; } export class ToolCallDelta extends AgentEvent2 { static override readonly type = 'tool.call.delta'; static override readonly observable = true; } export interface ToolCallDelta extends ToolCallDeltaPayload {} registerEvent2Class(TurnStepInterrupted); registerEvent2Class(TurnStepRetrying); export interface TurnStartedEvent extends Omit { readonly type: 'turn.started'; } export interface TurnStepStartedEvent extends Omit { readonly type: 'turn.step.started'; } export interface TurnStepCompletedEvent extends Omit { readonly type: 'turn.step.completed'; } export interface TurnStepRetryingEvent extends Omit { readonly type: 'turn.step.retrying'; } export interface TurnStepInterruptedEvent extends Omit { readonly type: 'turn.step.interrupted'; } export interface AssistantDeltaEvent extends Omit { readonly type: 'assistant.delta'; } export interface ThinkingDeltaEvent extends Omit { readonly type: 'thinking.delta'; }