Dynavera/site/src/stores/agentStore.ts

183 lines
5.8 KiB
TypeScript
Raw Normal View History

import { defineStore } from 'pinia'
import { ref } from 'vue'
import type {
AgentEvent,
AgentExecutionStatus,
AgentSocketEventPayload,
AgentStartPayload,
} from '../types/agent'
2026-03-18 10:20:57 +00:00
import { BACKOFF_BASE_MS, BACKOFF_MAX_MS, BACKOFF_MAX_ATTEMPTS } from './agentBackoff'
export const useAgentStore = defineStore('agent', () => {
const isConnected = ref(false)
const executionStatus = ref<AgentExecutionStatus>('idle')
const eventLog = ref<AgentEvent[]>([])
const lastExecutionId = ref<string | null>(null)
const socket = ref<WebSocket | null>(null)
2026-03-18 10:20:57 +00:00
let currentUrl = ''
let reconnectAttempts = 0
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let intentionalClose = false
const pushEvent = (evt: AgentSocketEventPayload) => {
eventLog.value.unshift({
type: evt.type,
message: evt.message,
content: evt.content,
timestamp: evt.timestamp ? new Date(evt.timestamp) : new Date(),
})
}
2026-03-18 10:20:57 +00:00
const clearReconnectTimer = () => {
if (reconnectTimer !== null) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
}
const scheduleReconnect = () => {
if (reconnectAttempts >= BACKOFF_MAX_ATTEMPTS) {
pushEvent({ type: 'error', message: 'Connection lost. Please refresh the page.' })
executionStatus.value = 'idle'
return
}
2026-03-18 10:20:57 +00:00
const delay = Math.min(BACKOFF_BASE_MS * 2 ** reconnectAttempts, BACKOFF_MAX_MS)
reconnectAttempts++
pushEvent({ type: 'status', message: `Reconnecting in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempts}/${BACKOFF_MAX_ATTEMPTS})...` })
reconnectTimer = setTimeout(() => {
if (!intentionalClose) openSocket(currentUrl)
}, delay)
}
2026-03-18 10:20:57 +00:00
const openSocket = (url: string) => {
socket.value = new WebSocket(url)
socket.value.onopen = () => {
2026-03-18 10:20:57 +00:00
reconnectAttempts = 0
isConnected.value = true
pushEvent({ type: 'status', message: 'Connected to Orchestrator' })
}
socket.value.onmessage = (event) => {
try {
const payload = JSON.parse(event.data) as AgentSocketEventPayload
const type = payload.type
if (payload.execution_id) {
lastExecutionId.value = String(payload.execution_id)
}
if (type === 'status' || type === 'thought' || type === 'tool_start') {
executionStatus.value = 'running'
pushEvent({
type,
message: payload.message || payload.thought,
content: payload.content,
})
} else if (
type === 'tool_call' ||
type === 'tool_result' ||
type === 'tool_complete'
) {
pushEvent({
type,
message: payload.message,
content: payload.content || payload,
})
} else if (type === 'completed') {
executionStatus.value = 'completed'
pushEvent({
type: 'completed',
message: 'Generation loop finished successfully',
content: payload.content,
timestamp: payload.timestamp,
})
} else if (type === 'error') {
executionStatus.value = 'failed'
pushEvent({ type: 'error', message: payload.message })
}
} catch (e) {
console.error('Store message error', e)
}
}
2026-03-18 10:20:57 +00:00
socket.value.onclose = (event) => {
isConnected.value = false
2026-03-18 10:20:57 +00:00
// code 1000 = clean close (server finished normally); don't reconnect
if (!intentionalClose && event.code !== 1000) {
scheduleReconnect()
} else {
executionStatus.value = 'idle'
}
}
}
2026-03-18 10:20:57 +00:00
const connect = (id: string) => {
intentionalClose = false
clearReconnectTimer()
reconnectAttempts = 0
if (socket.value) {
socket.value.close()
socket.value = null
}
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
currentUrl = `${wsProtocol}://${window.location.host}/ws/onboarding/chat/${id}/`
openSocket(currentUrl)
}
const disconnect = () => {
2026-03-18 10:20:57 +00:00
intentionalClose = true
clearReconnectTimer()
if (socket.value) {
socket.value.close()
socket.value = null
}
isConnected.value = false
executionStatus.value = 'idle'
}
const startAgent = (data: AgentStartPayload) => {
if (!socket.value || socket.value.readyState !== WebSocket.OPEN) return
executionStatus.value = 'running'
socket.value.send(
JSON.stringify({
action: 'message',
query: data.query,
max_tokens: data.max_tokens,
}),
)
}
const stopAgent = (executionId?: string) => {
if (!socket.value || socket.value.readyState !== WebSocket.OPEN) return
socket.value.send(
JSON.stringify({
action: 'stop_agent',
execution_id: executionId ?? lastExecutionId.value,
}),
)
}
const clearLog = () => {
eventLog.value = []
}
return {
isConnected,
executionStatus,
eventLog,
socket,
connect,
disconnect,
startAgent,
stopAgent,
clearLog,
lastExecutionId,
}
})