| | import { create } from 'zustand';
|
| | import { persist } from 'zustand/middleware';
|
| | import type { Message, User, TraceLog } from '@/types/agent';
|
| |
|
| | export interface PlanItem {
|
| | id: string;
|
| | content: string;
|
| | status: 'pending' | 'in_progress' | 'completed';
|
| | }
|
| |
|
| | interface PanelTab {
|
| | id: string;
|
| | title: string;
|
| | content: string;
|
| | language?: string;
|
| | parameters?: Record<string, unknown>;
|
| | }
|
| |
|
| | export interface LLMHealthError {
|
| | error: string;
|
| | errorType: 'auth' | 'credits' | 'rate_limit' | 'network' | 'unknown';
|
| | model: string;
|
| | }
|
| |
|
| | interface AgentStore {
|
| |
|
| | messagesBySession: Record<string, Message[]>;
|
| | isProcessing: boolean;
|
| | isConnected: boolean;
|
| | user: User | null;
|
| | error: string | null;
|
| | llmHealthError: LLMHealthError | null;
|
| | traceLogs: TraceLog[];
|
| | panelContent: { title: string; content: string; language?: string; parameters?: Record<string, unknown> } | null;
|
| | panelTabs: PanelTab[];
|
| | activePanelTab: string | null;
|
| | plan: PlanItem[];
|
| | currentTurnMessageId: string | null;
|
| | editedScripts: Record<string, string>;
|
| |
|
| |
|
| | addMessage: (sessionId: string, message: Message) => void;
|
| | updateMessage: (sessionId: string, messageId: string, updates: Partial<Message>) => void;
|
| | clearMessages: (sessionId: string) => void;
|
| | setProcessing: (isProcessing: boolean) => void;
|
| | setConnected: (isConnected: boolean) => void;
|
| | setUser: (user: User | null) => void;
|
| | setError: (error: string | null) => void;
|
| | getMessages: (sessionId: string) => Message[];
|
| | addTraceLog: (log: TraceLog) => void;
|
| | updateTraceLog: (toolCallId: string, toolName: string, updates: Partial<TraceLog>) => void;
|
| | clearTraceLogs: () => void;
|
| | setPanelContent: (content: { title: string; content: string; language?: string; parameters?: Record<string, unknown> } | null) => void;
|
| | setPanelTab: (tab: PanelTab) => void;
|
| | updatePanelTabContent: (tabId: string, content: string) => void;
|
| | setActivePanelTab: (tabId: string) => void;
|
| | clearPanelTabs: () => void;
|
| | removePanelTab: (tabId: string) => void;
|
| | setPlan: (plan: PlanItem[]) => void;
|
| | setCurrentTurnMessageId: (id: string | null) => void;
|
| | updateCurrentTurnTrace: (sessionId: string) => void;
|
| | showToolOutput: (log: TraceLog) => void;
|
| | setEditedScript: (toolCallId: string, content: string) => void;
|
| | getEditedScript: (toolCallId: string) => string | undefined;
|
| | clearEditedScripts: () => void;
|
| |
|
| | appendToMessage: (sessionId: string, messageId: string, delta: string) => void;
|
| |
|
| | deleteSessionMessages: (sessionId: string) => void;
|
| |
|
| | removeLastTurn: (sessionId: string) => void;
|
| | setLlmHealthError: (error: LLMHealthError | null) => void;
|
| | }
|
| |
|
| | export const useAgentStore = create<AgentStore>()(
|
| | persist(
|
| | (set, get) => ({
|
| | messagesBySession: {},
|
| | isProcessing: false,
|
| | isConnected: false,
|
| | user: null,
|
| | error: null,
|
| | llmHealthError: null,
|
| | traceLogs: [],
|
| | panelContent: null,
|
| | panelTabs: [],
|
| | activePanelTab: null,
|
| | plan: [],
|
| | currentTurnMessageId: null,
|
| | editedScripts: {},
|
| |
|
| | addMessage: (sessionId: string, message: Message) => {
|
| | set((state) => {
|
| | const currentMessages = state.messagesBySession[sessionId] || [];
|
| | return {
|
| | messagesBySession: {
|
| | ...state.messagesBySession,
|
| | [sessionId]: [...currentMessages, message],
|
| | },
|
| | };
|
| | });
|
| | },
|
| |
|
| | updateMessage: (sessionId: string, messageId: string, updates: Partial<Message>) => {
|
| | set((state) => {
|
| | const currentMessages = state.messagesBySession[sessionId] || [];
|
| | const updatedMessages = currentMessages.map((msg) =>
|
| | msg.id === messageId ? { ...msg, ...updates } : msg
|
| | );
|
| | return {
|
| | messagesBySession: {
|
| | ...state.messagesBySession,
|
| | [sessionId]: updatedMessages,
|
| | },
|
| | };
|
| | });
|
| | },
|
| |
|
| | clearMessages: (sessionId: string) => {
|
| | set((state) => ({
|
| | messagesBySession: {
|
| | ...state.messagesBySession,
|
| | [sessionId]: [],
|
| | },
|
| | }));
|
| | },
|
| |
|
| | setProcessing: (isProcessing: boolean) => {
|
| | set({ isProcessing });
|
| | },
|
| |
|
| | setConnected: (isConnected: boolean) => {
|
| | set({ isConnected });
|
| | },
|
| |
|
| | setUser: (user: User | null) => {
|
| | set({ user });
|
| | },
|
| |
|
| | setError: (error: string | null) => {
|
| | set({ error });
|
| | },
|
| |
|
| | getMessages: (sessionId: string) => {
|
| | return get().messagesBySession[sessionId] || [];
|
| | },
|
| |
|
| | addTraceLog: (log: TraceLog) => {
|
| | set((state) => ({
|
| | traceLogs: [...state.traceLogs, log],
|
| | }));
|
| | },
|
| |
|
| | updateTraceLog: (toolCallId: string, toolName: string, updates: Partial<TraceLog>) => {
|
| | set((state) => {
|
| | const traceLogs = [...state.traceLogs];
|
| |
|
| | let matched = false;
|
| | if (toolCallId) {
|
| | for (let i = traceLogs.length - 1; i >= 0; i--) {
|
| | if (traceLogs[i].toolCallId === toolCallId) {
|
| | traceLogs[i] = { ...traceLogs[i], ...updates };
|
| | matched = true;
|
| | break;
|
| | }
|
| | }
|
| | }
|
| | if (!matched) {
|
| |
|
| | for (let i = traceLogs.length - 1; i >= 0; i--) {
|
| | if (traceLogs[i].tool === toolName && traceLogs[i].type === 'call' && !traceLogs[i].completed) {
|
| | traceLogs[i] = { ...traceLogs[i], ...updates };
|
| | break;
|
| | }
|
| | }
|
| | }
|
| | return { traceLogs };
|
| | });
|
| | },
|
| |
|
| | clearTraceLogs: () => {
|
| | set({ traceLogs: [] });
|
| | },
|
| |
|
| | setPanelContent: (content) => {
|
| | set({ panelContent: content });
|
| | },
|
| |
|
| | setPanelTab: (tab: PanelTab) => {
|
| | set((state) => {
|
| | const existingIndex = state.panelTabs.findIndex(t => t.id === tab.id);
|
| | let newTabs: PanelTab[];
|
| | if (existingIndex >= 0) {
|
| |
|
| | newTabs = [...state.panelTabs];
|
| | newTabs[existingIndex] = tab;
|
| | } else {
|
| |
|
| | newTabs = [...state.panelTabs, tab];
|
| | }
|
| | return {
|
| | panelTabs: newTabs,
|
| | activePanelTab: state.activePanelTab || tab.id,
|
| | };
|
| | });
|
| | },
|
| |
|
| | updatePanelTabContent: (tabId: string, content: string) => {
|
| | set((state) => {
|
| | const newTabs = state.panelTabs.map(tab =>
|
| | tab.id === tabId ? { ...tab, content } : tab
|
| | );
|
| | return { panelTabs: newTabs };
|
| | });
|
| | },
|
| |
|
| | setActivePanelTab: (tabId: string) => {
|
| | set({ activePanelTab: tabId });
|
| | },
|
| |
|
| | clearPanelTabs: () => {
|
| | set({ panelTabs: [], activePanelTab: null });
|
| | },
|
| |
|
| | removePanelTab: (tabId: string) => {
|
| | set((state) => {
|
| | const newTabs = state.panelTabs.filter(t => t.id !== tabId);
|
| |
|
| | let newActiveTab = state.activePanelTab;
|
| | if (state.activePanelTab === tabId) {
|
| | newActiveTab = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
|
| | }
|
| | return {
|
| | panelTabs: newTabs,
|
| | activePanelTab: newActiveTab,
|
| | };
|
| | });
|
| | },
|
| |
|
| | setPlan: (plan: PlanItem[]) => {
|
| | set({ plan });
|
| | },
|
| |
|
| | setCurrentTurnMessageId: (id: string | null) => {
|
| | set({ currentTurnMessageId: id });
|
| | },
|
| |
|
| | updateCurrentTurnTrace: (sessionId: string) => {
|
| | const state = get();
|
| | if (!state.currentTurnMessageId) return;
|
| |
|
| | const currentMessages = state.messagesBySession[sessionId] || [];
|
| | const latestTools = state.traceLogs.length > 0 ? [...state.traceLogs] : undefined;
|
| | if (!latestTools) return;
|
| |
|
| |
|
| | const toolById = new Map(latestTools.map(t => [t.id, t]));
|
| |
|
| | const updatedMessages = currentMessages.map((msg) => {
|
| | if (msg.id !== state.currentTurnMessageId) return msg;
|
| |
|
| | const segments = msg.segments ? [...msg.segments] : [];
|
| |
|
| |
|
| | const placedToolIds = new Set<string>();
|
| | for (let i = 0; i < segments.length; i++) {
|
| | if (segments[i].type === 'tools' && segments[i].tools) {
|
| | segments[i] = {
|
| | ...segments[i],
|
| | tools: segments[i].tools!.map(t => {
|
| | placedToolIds.add(t.id);
|
| | return toolById.get(t.id) || t;
|
| | }),
|
| | };
|
| | }
|
| | }
|
| |
|
| |
|
| | const newTools = latestTools.filter(t => !placedToolIds.has(t.id));
|
| |
|
| | if (newTools.length > 0) {
|
| | const lastToolsIdx = segments.map((s) => s.type).lastIndexOf('tools');
|
| |
|
| | if (lastToolsIdx >= 0 && lastToolsIdx === segments.length - 1) {
|
| |
|
| | segments[lastToolsIdx] = {
|
| | ...segments[lastToolsIdx],
|
| | tools: [...(segments[lastToolsIdx].tools || []), ...newTools],
|
| | };
|
| | } else {
|
| |
|
| | segments.push({ type: 'tools', tools: newTools });
|
| | }
|
| | }
|
| |
|
| | return { ...msg, segments };
|
| | });
|
| |
|
| | set({
|
| | messagesBySession: {
|
| | ...state.messagesBySession,
|
| | [sessionId]: updatedMessages,
|
| | },
|
| | });
|
| | },
|
| |
|
| | showToolOutput: (log: TraceLog) => {
|
| |
|
| | const state = get();
|
| |
|
| |
|
| | let language = 'text';
|
| | const content = log.output || '';
|
| |
|
| |
|
| | if (content.trim().startsWith('{') || content.trim().startsWith('[') || content.includes('```json')) {
|
| | language = 'json';
|
| | }
|
| |
|
| | else if (content.includes('|') && content.includes('---') || content.includes('```')) {
|
| | language = 'markdown';
|
| | }
|
| |
|
| |
|
| | const otherTabs = state.panelTabs.filter(t => t.id !== 'tool_output');
|
| |
|
| |
|
| | const newTab = {
|
| | id: 'tool_output',
|
| | title: log.tool,
|
| | content: content || 'No output available',
|
| | language,
|
| | };
|
| |
|
| | set({
|
| | panelTabs: [...otherTabs, newTab],
|
| | activePanelTab: 'tool_output',
|
| | });
|
| | },
|
| |
|
| | setEditedScript: (toolCallId: string, content: string) => {
|
| | set((state) => ({
|
| | editedScripts: { ...state.editedScripts, [toolCallId]: content },
|
| | }));
|
| | },
|
| |
|
| | getEditedScript: (toolCallId: string) => {
|
| | return get().editedScripts[toolCallId];
|
| | },
|
| |
|
| | clearEditedScripts: () => {
|
| | set({ editedScripts: {} });
|
| | },
|
| |
|
| | appendToMessage: (sessionId: string, messageId: string, delta: string) => {
|
| | set((state) => {
|
| | const messages = state.messagesBySession[sessionId] || [];
|
| | return {
|
| | messagesBySession: {
|
| | ...state.messagesBySession,
|
| | [sessionId]: messages.map((msg) => {
|
| | if (msg.id !== messageId) return msg;
|
| | const newContent = msg.content + delta;
|
| | const segments = msg.segments ? [...msg.segments] : [];
|
| | const lastSeg = segments[segments.length - 1];
|
| |
|
| | if (lastSeg && lastSeg.type === 'text') {
|
| |
|
| | segments[segments.length - 1] = {
|
| | ...lastSeg,
|
| | content: (lastSeg.content || '') + delta,
|
| | };
|
| | } else {
|
| |
|
| |
|
| | segments.push({ type: 'text', content: delta });
|
| | }
|
| |
|
| | return { ...msg, content: newContent, segments };
|
| | }),
|
| | },
|
| | };
|
| | });
|
| | },
|
| |
|
| | deleteSessionMessages: (sessionId: string) => {
|
| | set((state) => {
|
| | const { [sessionId]: _, ...rest } = state.messagesBySession;
|
| | return { messagesBySession: rest };
|
| | });
|
| | },
|
| |
|
| | removeLastTurn: (sessionId: string) => {
|
| | set((state) => {
|
| | const msgs = state.messagesBySession[sessionId];
|
| | if (!msgs || msgs.length === 0) return state;
|
| |
|
| |
|
| | let lastUserIdx = -1;
|
| | for (let i = msgs.length - 1; i >= 0; i--) {
|
| | if (msgs[i].role === 'user') {
|
| | lastUserIdx = i;
|
| | break;
|
| | }
|
| | }
|
| | if (lastUserIdx === -1) return state;
|
| |
|
| |
|
| | return {
|
| | messagesBySession: {
|
| | ...state.messagesBySession,
|
| | [sessionId]: msgs.slice(0, lastUserIdx),
|
| | },
|
| | };
|
| | });
|
| | },
|
| |
|
| | setLlmHealthError: (error: LLMHealthError | null) => {
|
| | set({ llmHealthError: error });
|
| | },
|
| | }),
|
| | {
|
| | name: 'hf-agent-messages',
|
| |
|
| | partialize: (state) => ({
|
| | messagesBySession: state.messagesBySession,
|
| | }),
|
| | }
|
| | )
|
| | );
|
| |
|