Skip to main content
AI operations take time. Streaming shows real-time progress as the agent works.

Event Types

type SSEEvent<T> = ProgressEvent | CompleteEvent<T> | ErrorEvent;

ProgressEvent

interface ProgressEvent {
  type: 'progress';
  percentage: number;
  message?: string;
  metadata?: Record<string, unknown>;
}

CompleteEvent

interface CompleteEvent<T> {
  type: 'complete';
  data: T;
}
For Chart Maker streams, data is a GenerateGraphResponse: it includes config and response. Narrative (title, subtitle, caption) is embedded in config.content as TipTap JSON documents; which fields are populated depends on storytellingEffort. See Storytelling and narrative for details.

ErrorEvent

interface ErrorEvent {
  type: 'error';
  error: string;
  code?: string;
  retryable?: boolean;
}

Async Iterator Pattern

generateGraphStream() returns an async iterator:
import { isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk';

const stream = await ai.generateGraphStream({
  config,
  userPrompt: 'Add a trend line',
});

for await (const event of stream) {
  if (isProgressEvent(event)) {
    console.log(`${event.percentage}% - ${event.message}`);
  }

  if (isCompleteEvent(event)) {
    return event.data.config;
  }

  if (isErrorEvent(event)) {
    throw new Error(event.error);
  }
}

Cancellation

Pass an AbortSignal to cancel mid-operation:
import { isCompleteEvent } from '@graphysdk/agents-sdk';

const controller = new AbortController();

const stream = await ai.generateGraphStream({ config, userPrompt: 'Create a complex heatmap' }, controller.signal);

// Cancel from a button click
cancelButton.onclick = () => controller.abort();

try {
  for await (const event of stream) {
    if (isCompleteEvent(event)) {
      return event.data;
    }
  }
} catch (error) {
  if (error instanceof Error && error.name === 'AbortError') {
    console.log('Cancelled by user');
    return;
  }
  throw error;
}

Progress Callback Alternative

If you want progress updates without the streaming API, use the onProgress callback with generateGraph():
const result = await ai.generateGraph({ config, userPrompt: 'Add a trend line' }, (progress) => {
  setProgress(progress.percentage);
  setStatus(progress.message ?? '');
});

// result contains the final GenerateGraphResponse
This collects the stream internally and returns the final result.

React Pattern

Store the abort controller in a ref for proper cleanup:
import { useState, useRef, useEffect } from 'react';
import {
  GraphyAiSdk,
  isProgressEvent,
  isCompleteEvent,
  isErrorEvent,
} from '@graphysdk/agents-sdk';
import type { GraphConfig } from '@graphysdk/core';

interface Props {
  initialConfig: GraphConfig;
  onUpdate: (config: GraphConfig) => void;
}

function ChartEditor({ initialConfig, onUpdate }: Props) {
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const abortRef = useRef<AbortController | null>(null);

  const ai = useRef(
    new GraphyAiSdk({
      apiKey: process.env.NEXT_PUBLIC_GRAPHY_API_KEY,
      baseUrl: 'https://agents.graphy.dev',
    })
  ).current;

  const handleGenerate = async (prompt: string) => {
    // Cancel any in-flight request
    abortRef.current?.abort();
    abortRef.current = new AbortController();

    setIsLoading(true);
    setProgress(0);
    setError(null);

    try {
      const stream = await ai.generateGraphStream(
        { config: initialConfig, userPrompt: prompt },
        abortRef.current.signal
      );

      for await (const event of stream) {
        if (isProgressEvent(event)) {
          setProgress(event.percentage);
          setStatus(event.message ?? '');
        }

        if (isCompleteEvent(event)) {
          onUpdate(event.data.config);
        }

        if (isErrorEvent(event)) {
          setError(event.error);
        }
      }
    } catch (err) {
      if (err instanceof Error && err.name !== 'AbortError') {
        setError(err.message);
      }
    } finally {
      setIsLoading(false);
    }
  };

  // Cleanup on unmount
  useEffect(() => {
    return () => abortRef.current?.abort();
  }, []);

  return (
    <div>
      {isLoading && (
        <div>
          <progress value={progress} max={100} />
          <span>{status}</span>
          <button onClick={() => abortRef.current?.abort()}>Cancel</button>
        </div>
      )}
      {error && <div className="error">{error}</div>}
      <PromptInput onSubmit={handleGenerate} disabled={isLoading} />
    </div>
  );
}
Key patterns:
  1. Cancel previous requests — Abort any in-flight request before starting a new one
  2. Store in ref — The abort controller persists across renders
  3. Cleanup on unmount — Cancel pending requests when the component unmounts
  4. Handle AbortError — Don’t treat user cancellation as an error

Type Guards

The SDK exports isProgressEvent(), isCompleteEvent(), and isErrorEvent() for narrowing SSE events, and isGraphyApiError() for narrowing caught errors. All examples on this page use these type guards. See the Type Reference for full signatures.

Error Handling in Streams

Errors can come from two sources:
  1. Error events — The API returns an error during processing
  2. Exceptions — Network failure, timeout, or abort
import { isErrorEvent, isCompleteEvent, isGraphyApiError } from '@graphysdk/agents-sdk';

try {
  const stream = await ai.generateGraphStream({ config, userPrompt });

  for await (const event of stream) {
    if (isErrorEvent(event)) {
      // API-level error
      if (event.retryable) {
        // Offer retry option
      }
      throw new Error(event.error);
    }

    if (isCompleteEvent(event)) {
      return event.data;
    }
  }
} catch (error) {
  if (error instanceof Error && error.name === 'AbortError') {
    // User cancelled
    return;
  }
  // Network or other error
  console.error('Stream failed:', error);
}
See Error Handling for details on error codes and retry behavior.