mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
150 lines
3.9 KiB
TypeScript
150 lines
3.9 KiB
TypeScript
/**
|
|
* 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();
|
|
}
|
|
},
|
|
});
|
|
}
|