Spaces:
Runtime error
Runtime error
| /** | |
| * A2A SSE Streaming Support | |
| * | |
| * Provides SSE event formatting for A2A `message/stream` responses. | |
| * Features: heartbeat (15s), chunk emission, metadata final event, cancellation. | |
| */ | |
| import type { A2ATask } from "./taskManager"; | |
| export interface SSEChunkEvent { | |
| jsonrpc: "2.0"; | |
| method: "message/stream"; | |
| params: { | |
| task: { id: string; state: string }; | |
| chunk?: { type: string; content: string }; | |
| metadata?: Record<string, unknown>; | |
| }; | |
| } | |
| /** | |
| * Format an SSE event line. | |
| */ | |
| export function formatSSE(event: SSEChunkEvent): string { | |
| return `data: ${JSON.stringify(event)}\n\n`; | |
| } | |
| /** | |
| * Create a chunk event for streaming text content. | |
| */ | |
| export function createChunkEvent(taskId: string, content: string): string { | |
| return formatSSE({ | |
| jsonrpc: "2.0", | |
| method: "message/stream", | |
| params: { | |
| task: { id: taskId, state: "working" }, | |
| chunk: { type: "text", content }, | |
| }, | |
| }); | |
| } | |
| /** | |
| * Create the final completion event with metadata. | |
| */ | |
| export function createCompletionEvent(taskId: string, metadata: Record<string, unknown>): string { | |
| return formatSSE({ | |
| jsonrpc: "2.0", | |
| method: "message/stream", | |
| params: { | |
| task: { id: taskId, state: "completed" }, | |
| metadata, | |
| }, | |
| }); | |
| } | |
| /** | |
| * Create a heartbeat event to keep the connection alive. | |
| */ | |
| export function createHeartbeat(taskId: string): string { | |
| return `: heartbeat ${new Date().toISOString()}\n\n`; | |
| } | |
| /** | |
| * Create a failure event. | |
| */ | |
| export function createFailureEvent(taskId: string, error: string): string { | |
| return formatSSE({ | |
| jsonrpc: "2.0", | |
| method: "message/stream", | |
| params: { | |
| task: { id: taskId, state: "failed" }, | |
| metadata: { error }, | |
| }, | |
| }); | |
| } | |
| /** | |
| * SSE response headers for A2A streaming. | |
| */ | |
| export const SSE_HEADERS = { | |
| "Content-Type": "text/event-stream", | |
| "Cache-Control": "no-cache, no-transform", | |
| Connection: "keep-alive", | |
| "X-Accel-Buffering": "no", | |
| } as const; | |
| /** | |
| * Create a streaming SSE handler that wraps a fetch-based LLM call. | |
| * Returns a ReadableStream suitable for a Response object. | |
| */ | |
| export function createA2AStream( | |
| task: A2ATask, | |
| executeSkill: ( | |
| task: A2ATask | |
| ) => Promise<{ artifacts: Array<{ content: string }>; metadata: Record<string, unknown> }>, | |
| abortSignal?: AbortSignal, | |
| lifecycle?: { | |
| onStart?: () => void; | |
| onEnd?: () => void; | |
| } | |
| ): ReadableStream<Uint8Array> { | |
| const encoder = new TextEncoder(); | |
| return new ReadableStream({ | |
| async start(controller) { | |
| lifecycle?.onStart?.(); | |
| // Heartbeat interval | |
| const heartbeatInterval = setInterval(() => { | |
| try { | |
| controller.enqueue(encoder.encode(createHeartbeat(task.id))); | |
| } catch { | |
| /* stream closed */ | |
| } | |
| }, 15_000); | |
| try { | |
| // Check for cancellation | |
| if (abortSignal?.aborted) { | |
| controller.enqueue(encoder.encode(createFailureEvent(task.id, "Cancelled"))); | |
| controller.close(); | |
| return; | |
| } | |
| // Execute the skill | |
| const result = await executeSkill(task); | |
| // Emit content as chunks (simulated streaming for non-streaming skills) | |
| for (const artifact of result.artifacts) { | |
| if (abortSignal?.aborted) break; | |
| controller.enqueue(encoder.encode(createChunkEvent(task.id, artifact.content))); | |
| } | |
| if (abortSignal?.aborted) { | |
| controller.enqueue(encoder.encode(createFailureEvent(task.id, "Cancelled"))); | |
| return; | |
| } | |
| // Emit completion with metadata | |
| controller.enqueue(encoder.encode(createCompletionEvent(task.id, result.metadata))); | |
| } catch (err) { | |
| const msg = err instanceof Error ? err.message : String(err); | |
| controller.enqueue(encoder.encode(createFailureEvent(task.id, msg))); | |
| } finally { | |
| clearInterval(heartbeatInterval); | |
| lifecycle?.onEnd?.(); | |
| controller.close(); | |
| } | |
| }, | |
| }); | |
| } | |