import type { ChatDocumentAttachment, ChatImageAttachment, ChatTimelineItem as ChatTimelineItemType, ChatTimelineMessageItem, } from '@easyflow/common-ui'; import type { AguiEvent } from '../../shared/agent-agui/client'; import type { AgentInfo, AgentKnowledgeBinding, AgentSkillBinding, AgentToolBinding, } from '../types'; import { ref } from 'vue'; import { ChatTimelineBuilder } from '@easyflow/common-ui'; import { EventType } from '@ag-ui/client'; import { createAguiRunId, EasyFlowAguiClient, } from '../../shared/agent-agui/client'; import { isRetryableAguiTransportError, resumeAguiRunUntilTerminal, } from '../../shared/agent-agui/reconnect'; import { clearAgentDraftSession } from '../api'; import { useAgentTryoutRawRounds } from './useAgentTryoutRawRounds'; function resolveDraftSessionId(agent: AgentInfo) { return `agent-draft-${agent.id || agent.localId || 'unsaved'}`; } function asText(value: unknown) { return value === null || value === undefined ? '' : String(value); } function asRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {}; } function draftAgentTransport(agent: AgentInfo) { return { avatar: agent.avatar, categoryId: agent.categoryId, description: agent.description, executionConfigJson: agent.executionConfigJson, generationConfigJson: agent.generationConfigJson, id: agent.id, memoryConfigJson: agent.memoryConfigJson, modelConfigJson: agent.modelConfigJson, modelId: agent.modelId, name: agent.name, promptConfigJson: agent.promptConfigJson, publishStatus: agent.publishStatus, status: agent.status, visibilityScope: agent.visibilityScope, }; } function draftToolBindingTransport(binding: AgentToolBinding) { return { enabled: binding.enabled, hitlConfigJson: binding.hitlConfigJson, hitlEnabled: binding.hitlEnabled, id: binding.id, optionsJson: binding.optionsJson, sortNo: binding.sortNo, targetId: binding.targetId, toolName: binding.toolName, toolType: binding.toolType, }; } function draftKnowledgeBindingTransport(binding: AgentKnowledgeBinding) { return { enabled: binding.enabled, id: binding.id, knowledgeId: binding.knowledgeId, optionsJson: binding.optionsJson, retrievalMode: binding.retrievalMode, sortNo: binding.sortNo, }; } function draftSkillBindingTransport(binding: AgentSkillBinding) { return { skillId: binding.skillId, sortNo: binding.sortNo, }; } interface DraftRuntimeContext { agent: AgentInfo; knowledgeBindings: AgentKnowledgeBinding[]; skillBindings: AgentSkillBinding[]; toolBindings: AgentToolBinding[]; } interface ActiveDraftRun { cancelling: boolean; inputAcceptedNotified: boolean; lastCursor: number; roundId: string; runId: string; sessionId: string; startedAt: number; stopped: boolean; } export function useAgentTryoutStream() { const timelineItems = ref([]); const loading = ref(false); let rawRounds: ReturnType | undefined; let activeRoundId = ''; let activeSessionId = ''; let activeRun: ActiveDraftRun | undefined; let resumeOnAccepted: (() => Promise | void) | undefined; const aguiClient = new EasyFlowAguiClient(); function errorMessageOf(error: unknown) { if (error instanceof Error) { return `${error.name} ${error.message}`.trim(); } if (typeof error === 'string') { return error; } const value = asRecord(error); const nested = asRecord(value.cause); return [value.name, value.message, value.error, nested.name, nested.message] .map((item) => asText(item).trim()) .filter(Boolean) .join(' '); } function finishStoppedRun(roundId: string) { if (roundId) { rawRounds?.failRound(roundId); rebuildTimeline(); } loading.value = false; } function rebuildTimeline() { timelineItems.value = rawRounds?.buildTimelineItems() || []; } function syncDraftContext( payload: DraftRuntimeContext, restore = false, requestedSessionId?: string, onAccepted?: () => Promise | void, ) { resumeOnAccepted = onAccepted; const sessionId = requestedSessionId || resolveDraftSessionId(payload.agent); const sessionChanged = activeSessionId !== sessionId; activeSessionId = sessionId; if (!rawRounds || sessionChanged) { rawRounds?.flush(); rawRounds = useAgentTryoutRawRounds({ mode: 'draft', sessionId, }); activeRoundId = ''; } if (restore && sessionChanged && !loading.value) { rebuildTimeline(); void resumeDraftRun(); } } function notifyInputAccepted(run: ActiveDraftRun) { if (run.inputAcceptedNotified || !resumeOnAccepted) { return; } run.inputAcceptedNotified = true; void resumeOnAccepted(); } function projectRunEvent( run: ActiveDraftRun, rounds: NonNullable, event: AguiEvent, ) { if (activeRun !== run) return; const runtimeEvent = rounds.recordEvent(run.roundId, event); if (!runtimeEvent) return; rounds.projectEvent(timelineItems.value, run.roundId, runtimeEvent, () => notifyInputAccepted(run), ); } function resumeActiveRun( run: ActiveDraftRun, rounds: NonNullable, ) { return resumeAguiRunUntilTerminal({ client: aguiClient, initialCursor: run.lastCursor, onCursor: (cursor) => { run.lastCursor = Math.max(run.lastCursor, cursor); rounds.updateCursor(run.roundId, run.lastCursor); }, onEvent: (event) => projectRunEvent(run, rounds, event), runId: run.runId, shouldContinue: () => activeRun === run && !run.stopped, startedAt: run.startedAt, }); } async function resumeDraftRun() { const rounds = rawRounds; const recovery = rounds?.recoverableRun(); if (!rounds || !recovery || activeRun) { return; } const run: ActiveDraftRun = { cancelling: false, inputAcceptedNotified: false, lastCursor: recovery.lastCursor, roundId: recovery.roundId, runId: recovery.runId, sessionId: activeSessionId, startedAt: recovery.startedAt, stopped: false, }; activeRun = run; activeRoundId = run.roundId; if (run.lastCursor === 0) { rounds.resetRuntimeEvents(run.roundId); } rebuildTimeline(); loading.value = true; try { if (recovery.inputAccepted) { notifyInputAccepted(run); } const terminalReceived = await resumeActiveRun(run, rounds); if (terminalReceived && activeRun === run && !run.stopped) { finishAssistant(); markRoundCompleted(run.roundId); } } catch (error) { if (activeRun === run && !run.stopped) { const runError = { message: errorMessageOf(error) || '试运行恢复失败,请稍后再试', runId: run.runId, threadId: run.sessionId, type: EventType.RUN_ERROR, } as AguiEvent; rounds.recordEvent(run.roundId, runError); rounds.projectEvent(timelineItems.value, run.roundId, runError); finishAssistant(); rounds.flush(); } } finally { if (activeRun === run && !run.cancelling) { activeRun = undefined; loading.value = false; } } } function markRoundCompleted(roundId: string) { if (!roundId) { return; } rawRounds?.completeRound(roundId); rebuildTimeline(); } function finishAssistant() { ChatTimelineBuilder.finalize(timelineItems.value); } function markToolApproving(payload: { approvalId?: string; toolCallId?: string; }) { ChatTimelineBuilder.markToolApproving(timelineItems.value, payload); } function markToolRejected(payload: { approvalId?: string; reason?: string; toolCallId?: string; }) { ChatTimelineBuilder.markToolRejected(timelineItems.value, payload); } async function runDraft(payload: { agent: AgentInfo; documents?: ChatDocumentAttachment[]; documentUploadIds?: string[]; images?: ChatImageAttachment[]; imageUploadIds?: string[]; knowledgeBindings: AgentKnowledgeBinding[]; onAccepted?: () => Promise | void; prompt: string; sessionId?: string; skillBindings: AgentSkillBinding[]; toolBindings: AgentToolBinding[]; }) { syncDraftContext(payload, false, payload.sessionId); if (!rawRounds) { return; } const rounds = rawRounds; activeRoundId = rounds.createRound( payload.prompt, payload.images, payload.documents, ); const runId = createAguiRunId(); rounds.bindRun(activeRoundId, runId); const startedAt = Date.now(); const run: ActiveDraftRun = { cancelling: false, inputAcceptedNotified: false, lastCursor: 0, roundId: activeRoundId, runId, sessionId: activeSessionId, startedAt, stopped: false, }; resumeOnAccepted = payload.onAccepted; activeRun = run; rebuildTimeline(); loading.value = true; try { let terminalReceived = true; try { await aguiClient.run({ forwardedProps: { easyflow: { draft: { agent: draftAgentTransport(payload.agent), knowledgeBindings: payload.knowledgeBindings.map((binding) => draftKnowledgeBindingTransport(binding), ), skillBindings: payload.skillBindings.map((binding) => draftSkillBindingTransport(binding), ), toolBindings: payload.toolBindings.map((binding) => draftToolBindingTransport(binding), ), }, input: { documentUploadIds: payload.documentUploadIds, imageUploadIds: payload.imageUploadIds, }, }, }, onCursor(cursor) { run.lastCursor = Math.max(run.lastCursor, cursor); rounds.updateCursor(run.roundId, run.lastCursor); }, onEvent: (event) => projectRunEvent(run, rounds, event), runId, threadId: run.sessionId, url: '/api/v1/agent/agui/run/draft', userMessage: { content: payload.prompt, id: `user-${run.roundId}`, role: 'user', }, }); } catch (error) { if (!isRetryableAguiTransportError(error, run.startedAt)) { throw error; } terminalReceived = await resumeActiveRun(run, rounds); } if (terminalReceived && activeRun === run && !run.stopped) { finishAssistant(); markRoundCompleted(run.roundId); } } catch (error) { if (activeRun === run && !run.stopped) { const runError = { message: errorMessageOf(error) || '试运行失败,请稍后再试', runId: run.runId, threadId: run.sessionId, type: EventType.RUN_ERROR, } as AguiEvent; rounds.recordEvent(run.roundId, runError); rounds.projectEvent(timelineItems.value, run.roundId, runError); finishAssistant(); rounds.flush(); } } finally { if (activeRun === run && !run.cancelling) { activeRun = undefined; loading.value = false; } } } async function sendDraft(payload: { agent: AgentInfo; documents?: ChatDocumentAttachment[]; documentUploadIds?: string[]; images?: ChatImageAttachment[]; imageUploadIds?: string[]; knowledgeBindings: AgentKnowledgeBinding[]; onAccepted?: () => Promise | void; prompt: string; sessionId?: string; skillBindings: AgentSkillBinding[]; toolBindings: AgentToolBinding[]; }) { await runDraft(payload); } async function regenerateDraft(item: ChatTimelineMessageItem) { // 有状态 runtime 的历史重生成需要后端 session fork/rollback,第一版先禁用入口。 void item; } function selectVariant( item: ChatTimelineMessageItem, direction: 'next' | 'previous', ) { if (!item.roundId || !rawRounds?.canSwitch(item, direction)) { return; } rawRounds.selectVariant(item.roundId, direction); rebuildTimeline(); } function copyMessageText(item: ChatTimelineMessageItem) { return rawRounds?.copyText(item) || ''; } async function clearDraftSession() { if (loading.value) { throw new Error('试运行进行中,暂时无法清理会话'); } const sessionId = activeSessionId; rawRounds?.clear(); timelineItems.value = []; activeRoundId = ''; if (sessionId) { await clearAgentDraftSession(sessionId); } } function recoverAfterCancelFailure(run: ActiveDraftRun) { run.cancelling = false; run.stopped = false; aguiClient.detach(); if (activeRun === run) { activeRun = undefined; } loading.value = false; rebuildTimeline(); void resumeDraftRun(); } async function stop() { if (!loading.value) { return; } const stoppedRoundId = activeRun?.roundId || activeRoundId; const run = activeRun; if (run) { if (run.cancelling) { return; } run.cancelling = true; run.stopped = true; try { await aguiClient.cancel(run.runId); } catch (error) { recoverAfterCancelFailure(run); throw error; } run.cancelling = false; } activeRun = undefined; if (run && rawRounds?.currentVariant(run.roundId)?.status !== 'running') { loading.value = false; rebuildTimeline(); return; } if (run) { // 取消接口成功后重新订阅服务端日志,以 RUN_FINISHED/RUN_ERROR 决定最终展示状态。 loading.value = false; rebuildTimeline(); await resumeDraftRun(); return; } finishStoppedRun(stoppedRoundId); } function dispose() { if (loading.value) { activeRun = undefined; aguiClient.detach(); rawRounds?.flush(); return; } rawRounds?.flush(); } return { loading, clearDraftSession, markToolApproving, markToolRejected, copyMessageText, dispose, regenerateDraft, selectVariant, syncDraftContext, timelineItems, sendDraft, stop, }; }