adk-ui 2.1.0

Dynamic UI generation for ADK-Rust agents - render forms, cards, tables, charts and more
Documentation
import React, { useMemo, useState } from 'react';
import clsx from 'clsx';

export type ReasoningMessage = {
    messageId: string;
    text: string;
    status: 'streaming' | 'complete';
};

export type ReasoningPanelProps = {
    messages: ReasoningMessage[];
    title?: string;
    className?: string;
    defaultOpen?: boolean;
    emptyLabel?: string;
};

/**
 * Collapsible panel for AG-UI REASONING_* event streams.
 */
export const ReasoningPanel: React.FC<ReasoningPanelProps> = ({
    messages,
    title = 'Agent reasoning',
    className,
    defaultOpen = true,
    emptyLabel = 'No reasoning captured for this run yet.',
}) => {
    const [open, setOpen] = useState(defaultOpen);
    const streaming = useMemo(
        () => messages.some((message) => message.status === 'streaming'),
        [messages],
    );

    return (
        <section
            className={clsx('adk-reasoning-panel', className)}
            aria-label={title}
            data-streaming={streaming ? 'true' : 'false'}
        >
            <button
                type="button"
                className="adk-reasoning-toggle"
                aria-expanded={open}
                onClick={() => setOpen((value) => !value)}
            >
                <span className="adk-reasoning-title">{title}</span>
                <span className="adk-reasoning-meta">
                    {streaming ? 'thinking…' : `${messages.length} step${messages.length === 1 ? '' : 's'}`}
                </span>
                <span className="adk-reasoning-chevron" aria-hidden="true">
                    {open ? '▾' : '▸'}
                </span>
            </button>
            {open ? (
                <div className="adk-reasoning-body">
                    {messages.length === 0 ? (
                        <p className="adk-reasoning-empty">{emptyLabel}</p>
                    ) : (
                        messages.map((message) => (
                            <article
                                key={message.messageId}
                                className={clsx(
                                    'adk-reasoning-item',
                                    message.status === 'streaming' && 'is-streaming',
                                )}
                            >
                                <header className="adk-reasoning-item-header">
                                    <span className="adk-reasoning-id">{message.messageId}</span>
                                    <span className="adk-reasoning-status">{message.status}</span>
                                </header>
                                <pre className="adk-reasoning-text">{message.text || '…'}</pre>
                            </article>
                        ))
                    )}
                </div>
            ) : null}
        </section>
    );
};

export type ReasoningRuntime = {
    messages: Map<string, ReasoningMessage>;
};

export function createReasoningRuntime(): ReasoningRuntime {
    return { messages: new Map() };
}

export function applyReasoningEvent(
    runtime: ReasoningRuntime,
    event: Record<string, unknown>,
): ReasoningMessage[] {
    const type = typeof event.type === 'string' ? event.type.trim().toUpperCase() : '';
    const messageIdRaw = event.messageId ?? event.message_id;
    const messageId = typeof messageIdRaw === 'string' ? messageIdRaw : undefined;
    const deltaRaw = event.delta ?? event.content;
    const delta = typeof deltaRaw === 'string' ? deltaRaw : undefined;
    const isReasoningEvent = type.startsWith('REASONING_') || type.startsWith('THINKING_');

    // AG-UI implicitly closes active reasoning when another event family begins.
    if (type && !isReasoningEvent) {
        completeStreamingMessages(runtime);
        return Array.from(runtime.messages.values());
    }

    if (type === 'REASONING_START' || type === 'REASONING_MESSAGE_START') {
        if (messageId) {
            const existing = runtime.messages.get(messageId);
            runtime.messages.set(messageId, {
                messageId,
                text: existing?.text ?? '',
                status: 'streaming',
            });
        }
    } else if (
        type === 'REASONING_MESSAGE_CONTENT'
        || type === 'REASONING_MESSAGE_CHUNK'
        || type === 'THINKING_TEXT_MESSAGE_CONTENT'
    ) {
        const keys = Array.from(runtime.messages.keys());
        const id = messageId ?? keys[keys.length - 1];
        if (id) {
            const existing = runtime.messages.get(id) ?? {
                messageId: id,
                text: '',
                status: 'streaming' as const,
            };
            const closesChunk = type === 'REASONING_MESSAGE_CHUNK' && delta === '';
            runtime.messages.set(id, closesChunk
                ? { ...existing, status: 'complete' }
                : {
                    ...existing,
                    text: `${existing.text}${delta ?? ''}`,
                    status: 'streaming',
                });
        }
    } else if (
        type === 'REASONING_MESSAGE_END'
        || type === 'REASONING_END'
        || type === 'THINKING_TEXT_MESSAGE_END'
        || type === 'THINKING_END'
    ) {
        if (messageId && runtime.messages.has(messageId)) {
            const existing = runtime.messages.get(messageId)!;
            runtime.messages.set(messageId, { ...existing, status: 'complete' });
        } else {
            completeStreamingMessages(runtime);
        }
    }

    return Array.from(runtime.messages.values());
}

function completeStreamingMessages(runtime: ReasoningRuntime) {
    for (const [id, existing] of runtime.messages) {
        if (existing.status === 'streaming') {
            runtime.messages.set(id, { ...existing, status: 'complete' });
        }
    }
}