import type { StateCreator } from 'zustand'
import type { Agent, Conversation, CreateAgentRequest } from '../types'
import { AdapterType, SandboxType, AgentStatus } from '../types'
import { agentService } from '@/services/agent-service'
import { sessionService } from '@/services/session-service'
import { generateUUID } from '../utils'
import { cacheDelete, cacheSetAll, cacheGetAll, CACHE_KEYS } from '../cache'
import { appConfig } from '@/app/config'
import { syncUrlParams, getUrlParam } from './utils'
import { isSessionPendingDelete, retryPendingSessionDeletes } from './pending-session-deletes'
import type { StoreState } from './index'
export interface AgentSlice {
agents: Agent[]
currentAgentId: string | null
agentStatus: Record<string, Agent['status']>
agentCreateFlag: number
isAgentsLoading: boolean
setCurrentAgent: (agentId: string | null) => void
triggerAgentCreate: () => void
addAgent: (agent: Agent) => void
updateAgent: (agent: Agent) => void
removeAgent: (agentId: string) => void
setAgents: (agents: Agent[]) => void
initializeAgent: (config: CreateAgentRequest) => Promise<Agent>
fetchAgentsWithConversations: () => Promise<void>
}
function patchAgentNames(
conversations: Conversation[],
sessionAgentNames: [string, string][]
): Conversation[] {
const nameMap = new Map(sessionAgentNames)
return conversations.map(c => {
if (!c.agentName && c.sessionId && nameMap.has(c.sessionId)) {
return { ...c, agentName: nameMap.get(c.sessionId)! }
}
return c
})
}
function deduplicateConversations<T extends { id: string; sessionId?: string }>(
existing: T[],
incoming: T[]
): T[] {
const existingIds = new Set(existing.map(c => c.id))
const existingSessionIds = new Set(existing.map(c => c.sessionId).filter(Boolean))
return incoming.filter(c => !existingIds.has(c.id) && !existingSessionIds.has(c.sessionId))
}
function mergeAndSort<T extends { updatedAt: Date | string }>(fresh: T[], patched: T[]): T[] {
return [...fresh, ...patched].sort(
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
)
}
export const createAgentSlice: StateCreator<StoreState, [], [], AgentSlice> = (set, get) => {
let pendingFetch: Promise<void> | null = null
return {
agents: [],
currentAgentId: null,
agentStatus: {},
agentCreateFlag: 0,
isAgentsLoading: false,
setCurrentAgent: agentId => {
const currentSessionId = getUrlParam('session')
set({ currentAgentId: agentId })
if (!get().currentConversationId) {
syncUrlParams(agentId || undefined, currentSessionId || undefined)
}
},
triggerAgentCreate: () =>
set(state => ({
agentCreateFlag: state.agentCreateFlag + 1,
})),
addAgent: agent => {
set(state => ({
agents: [...state.agents, agent],
}))
},
updateAgent: agent => {
set(state => ({
agents: state.agents.map(a => (a.id === agent.id ? agent : a)),
}))
},
removeAgent: agentId => {
cacheDelete(CACHE_KEYS.AGENTS)
cacheDelete(CACHE_KEYS.CONVERSATIONS_WITH_NAMES)
set(state => {
const agents = state.agents.filter(a => a.id !== agentId)
const agentStatus = { ...state.agentStatus }
delete agentStatus[agentId]
const conversations = state.conversations.filter(c => c.agentId !== agentId)
const currentConversationId =
state.currentConversationId &&
conversations.some(c => c.id === state.currentConversationId)
? state.currentConversationId
: null
const isCurrentAgentDeleted = state.currentAgentId === agentId
const nextAgentId = isCurrentAgentDeleted
? (agents.find(a => a.status !== AgentStatus.DELETED)?.id ?? null)
: state.currentAgentId
const conversationRemoved = !currentConversationId && state.currentConversationId
if (conversationRemoved || isCurrentAgentDeleted) {
syncUrlParams(nextAgentId || undefined)
}
return {
agents,
agentStatus,
conversations,
currentConversationId,
currentAgentId: nextAgentId,
}
})
},
setAgents: agents => {
set(() => ({ agents }))
},
initializeAgent: async (config: CreateAgentRequest) => {
let newAgent: Agent
if (appConfig.app.useMockData) {
const now = new Date().toISOString()
newAgent = {
id: generateUUID(),
name: config.name || 'New Agent',
description: config.description,
adapterType: config.adapterType || AdapterType.OPENCODE,
sandboxType: config.sandboxType || SandboxType.DOCKER,
status: AgentStatus.RUNNING,
sandboxId: undefined,
defaultSessionId: undefined,
idleTimeoutSeconds: config.idleTimeoutSeconds ?? 300,
createdAt: now,
updatedAt: now,
}
} else {
newAgent = await agentService.createAgent(config)
}
get().addAgent(newAgent)
get().setCurrentAgent(newAgent.id)
return newAgent
},
fetchAgentsWithConversations: async () => {
if (pendingFetch) {
return await pendingFetch
}
const cached = cacheGetAll()
if (cached) {
set(state => {
const existingIds = new Set(state.conversations.map(c => c.id))
const existingSessionIds = new Set(
state.conversations.map(c => c.sessionId).filter(Boolean)
)
const patched = patchAgentNames(state.conversations, cached.sessionAgentNames)
const freshConvs = cached.conversations.filter(
c =>
!existingIds.has(c.id) &&
!existingSessionIds.has(c.sessionId) &&
!isSessionPendingDelete(c.sessionId)
)
return {
agents: cached.agents,
conversations: mergeAndSort(freshConvs, patched),
}
})
}
const doNetworkRefresh = async () => {
try {
set({ isAgentsLoading: true })
const enriched = await agentService.getAgentsWithConversations()
const agents: Agent[] = enriched.map((item: any) => agentService.transformAgent(item))
const agentIds = new Set(agents.map(a => a.id))
const allConversations: Conversation[] = []
const sessionAgentNames: [string, string][] = []
const pendingRetry: Array<{ agentId: string; sessionId: string }> = []
for (const item of enriched) {
const agentName = item.name
for (const summary of item.conversations || []) {
const sessId: string = summary.id
if (isSessionPendingDelete(sessId)) {
pendingRetry.push({ agentId: item.id, sessionId: sessId })
continue
}
sessionAgentNames.push([sessId, agentName])
allConversations.push(sessionService.transformConversationSummary(summary, agentName))
}
}
cacheSetAll({ agents, conversations: allConversations, sessionAgentNames }, 2 * 60 * 1000)
retryPendingSessionDeletes(pendingRetry)
set(state => {
const newConversations = deduplicateConversations(state.conversations, allConversations)
const patched = patchAgentNames(state.conversations, sessionAgentNames).filter(
c => !c.agentId || agentIds.has(c.agentId)
)
const merged = mergeAndSort(newConversations, patched)
const convStillExists = merged.some(c => c.id === state.currentConversationId)
if (!convStillExists && state.currentConversationId) {
syncUrlParams(state.currentAgentId || undefined)
}
return {
agents,
conversations: merged,
isAgentsLoading: false,
...(convStillExists ? {} : { currentConversationId: null }),
}
})
} catch (error) {
console.error('Failed to fetch agents with conversations:', error)
set({ isAgentsLoading: false })
} finally {
pendingFetch = null
}
}
pendingFetch = doNetworkRefresh()
if (cached) {
return
}
return await pendingFetch
},
}
}