feat(minecraft): better debug server, event queue for the brain
This commit is contained in:
@@ -0,0 +1 @@
|
||||
logs/
|
||||
@@ -4,11 +4,12 @@ import type { Neuri } from 'neuri'
|
||||
import type { TaskExecutor } from '../action/task-executor'
|
||||
import type { ActionInstruction } from '../action/types'
|
||||
import type { EventManager } from '../perception/event-manager'
|
||||
import type { MineflayerWithAgents, StimulusPayload } from '../types'
|
||||
import type { BotEvent, MineflayerWithAgents, StimulusPayload } from '../types'
|
||||
|
||||
import { system, user } from 'neuri/openai'
|
||||
|
||||
import { config } from '../../composables/config'
|
||||
import { DebugService } from '../../debug-server'
|
||||
import { Blackboard } from './blackboard'
|
||||
import { generateBrainSystemPrompt } from './prompts/brain-prompt'
|
||||
|
||||
@@ -29,31 +30,43 @@ interface BrainResponse {
|
||||
actions: ActionInstruction[]
|
||||
}
|
||||
|
||||
interface QueuedEvent {
|
||||
event: BotEvent
|
||||
resolve: () => void
|
||||
reject: (err: Error) => void
|
||||
}
|
||||
|
||||
export class Brain {
|
||||
private blackboard: Blackboard
|
||||
private debugService: DebugService
|
||||
|
||||
// Event Queue
|
||||
private queue: QueuedEvent[] = []
|
||||
private isProcessing = false
|
||||
|
||||
constructor(private readonly deps: BrainDeps) {
|
||||
this.blackboard = new Blackboard()
|
||||
this.debugService = DebugService.getInstance()
|
||||
}
|
||||
|
||||
public init(bot: MineflayerWithAgents): void {
|
||||
this.deps.logger.log('Brain: Initializing...')
|
||||
this.log('INFO', 'Brain: Initializing...')
|
||||
|
||||
// Listen to Stimuli (Chat/Voice)
|
||||
// We treat these as "Sensory Inputs" that trigger the Cognitive Cycle
|
||||
this.deps.eventManager.on<StimulusPayload>('stimulus', async (event) => {
|
||||
if (event.handled) {
|
||||
this.deps.logger.log(`Brain: Stimulus from ${event.source.id} already handled by reflex, ignoring.`)
|
||||
this.log('INFO', `Brain: Stimulus from ${event.source.id} already handled by reflex, ignoring.`)
|
||||
return
|
||||
}
|
||||
this.deps.logger.log(`Brain: Received stimulus from ${event.source.id}: ${event.payload.content}`)
|
||||
await this.processEvent(bot, event)
|
||||
this.log('INFO', `Brain: Received stimulus from ${event.source.id}: ${event.payload.content}`)
|
||||
await this.enqueueEvent(bot, event)
|
||||
})
|
||||
|
||||
// Listen to Task Execution Events (Action Feedback)
|
||||
this.deps.taskExecutor.on('action:completed', async ({ action, result }) => {
|
||||
this.deps.logger.log(`Brain: Action completed: ${action.type}`)
|
||||
await this.processEvent(bot, {
|
||||
this.log('INFO', `Brain: Action completed: ${action.type}`)
|
||||
await this.enqueueEvent(bot, {
|
||||
type: 'feedback',
|
||||
payload: {
|
||||
status: 'success',
|
||||
@@ -66,8 +79,8 @@ export class Brain {
|
||||
})
|
||||
|
||||
this.deps.taskExecutor.on('action:failed', async ({ action, error }) => {
|
||||
this.deps.logger.withError(error).warn(`Brain: Action failed: ${action.type}`)
|
||||
await this.processEvent(bot, {
|
||||
this.log('WARN', `Brain: Action failed: ${action.type}`, { error })
|
||||
await this.enqueueEvent(bot, {
|
||||
type: 'feedback',
|
||||
payload: {
|
||||
status: 'failure',
|
||||
@@ -79,10 +92,51 @@ export class Brain {
|
||||
})
|
||||
})
|
||||
|
||||
this.deps.logger.log('Brain: Online.')
|
||||
this.log('INFO', 'Brain: Online.')
|
||||
this.updateDebugState()
|
||||
}
|
||||
|
||||
private async processEvent(bot: MineflayerWithAgents, event: any): Promise<void> {
|
||||
// --- Event Queue Logic ---
|
||||
|
||||
private async enqueueEvent(bot: MineflayerWithAgents, event: BotEvent): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({ event, resolve, reject })
|
||||
this.updateDebugState()
|
||||
this.processQueue(bot)
|
||||
})
|
||||
}
|
||||
|
||||
private async processQueue(bot: MineflayerWithAgents): Promise<void> {
|
||||
if (this.isProcessing)
|
||||
return
|
||||
if (this.queue.length === 0)
|
||||
return
|
||||
|
||||
this.isProcessing = true
|
||||
const item = this.queue.shift()!
|
||||
this.updateDebugState(item.event)
|
||||
|
||||
try {
|
||||
await this.processEvent(bot, item.event)
|
||||
item.resolve()
|
||||
}
|
||||
catch (err) {
|
||||
this.log('ERROR', 'Brain: Error processing event', { error: err })
|
||||
item.reject(err as Error)
|
||||
}
|
||||
finally {
|
||||
this.isProcessing = false
|
||||
this.updateDebugState()
|
||||
// Context switch: Check queue again
|
||||
if (this.queue.length > 0) {
|
||||
setImmediate(() => this.processQueue(bot))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Cognitive Cycle ---
|
||||
|
||||
private async processEvent(bot: MineflayerWithAgents, event: BotEvent): Promise<void> {
|
||||
// OODA Loop: Observe -> Orient -> Decide -> Act
|
||||
|
||||
// 1. Observe (Update Blackboard with Environment Sense)
|
||||
@@ -104,12 +158,12 @@ export class Brain {
|
||||
const decision = await this.decide(systemPrompt, contextMsg)
|
||||
|
||||
if (!decision) {
|
||||
this.deps.logger.warn('Brain: No decision made.')
|
||||
this.log('WARN', 'Brain: No decision made.')
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Act (Execute Decision)
|
||||
this.deps.logger.log(`Brain: Thought: ${decision.thought}`)
|
||||
this.log('INFO', `Brain: Thought: ${decision.thought}`)
|
||||
|
||||
// Update Blackboard
|
||||
this.blackboard.update({
|
||||
@@ -118,6 +172,9 @@ export class Brain {
|
||||
executionStrategy: decision.blackboard.executionStrategy || this.blackboard.strategy,
|
||||
})
|
||||
|
||||
// Sync Blackboard to Debug
|
||||
this.debugService.updateBlackboard(this.blackboard)
|
||||
|
||||
// Issue Actions
|
||||
if (decision.actions && decision.actions.length > 0) {
|
||||
this.deps.taskExecutor.executeActions(decision.actions)
|
||||
@@ -137,6 +194,9 @@ export class Brain {
|
||||
weather: bot.bot.isRaining ? 'rain' : 'clear',
|
||||
nearbyAgents: Object.keys(bot.bot.players).filter(p => p !== bot.bot.username),
|
||||
})
|
||||
|
||||
// Sync Blackboard to Debug
|
||||
this.debugService.updateBlackboard(this.blackboard)
|
||||
}
|
||||
|
||||
private async decide(sysPrompt: string, userMsg: string): Promise<BrainResponse | null> {
|
||||
@@ -153,6 +213,15 @@ export class Brain {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any) as any
|
||||
|
||||
// Trace LLM
|
||||
this.debugService.traceLLM({
|
||||
route: 'action',
|
||||
messages: ctx.messages,
|
||||
content: completion?.choices?.[0]?.message?.content,
|
||||
usage: completion?.usage,
|
||||
model: config.openai.model,
|
||||
})
|
||||
|
||||
if (!completion || !completion.choices?.[0]?.message?.content) {
|
||||
throw new Error('LLM failed to return content')
|
||||
}
|
||||
@@ -167,7 +236,7 @@ export class Brain {
|
||||
return parsed
|
||||
}
|
||||
catch (err) {
|
||||
this.deps.logger.withError(err).error('Brain: Decision failed')
|
||||
this.log('ERROR', 'Brain: Decision failed', { error: err })
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -176,4 +245,24 @@ export class Brain {
|
||||
const actions = this.deps.taskExecutor.getAvailableActions()
|
||||
return generateBrainSystemPrompt(blackboard, actions)
|
||||
}
|
||||
|
||||
// --- Debug Helpers ---
|
||||
|
||||
private log(level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG', message: string, fields?: any) {
|
||||
// Dual logging: Console/File via Logger AND DebugServer
|
||||
if (level === 'ERROR')
|
||||
this.deps.logger.withError(fields?.error).error(message)
|
||||
else if (level === 'WARN')
|
||||
this.deps.logger.warn(message, fields)
|
||||
else this.deps.logger.log(message, fields)
|
||||
|
||||
this.debugService.log(level, message, fields)
|
||||
}
|
||||
|
||||
private updateDebugState(processingEvent?: BotEvent) {
|
||||
this.debugService.updateQueue(
|
||||
this.queue.map(q => q.event),
|
||||
processingEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { MineflayerWithAgents } from '../types'
|
||||
import { assistant } from 'neuri/openai'
|
||||
|
||||
import { config } from '../../composables/config'
|
||||
import { DebugServer } from '../../debug-server'
|
||||
import { DebugService } from '../../debug-server'
|
||||
|
||||
export async function handleLLMCompletion(context: NeuriContext, bot: MineflayerWithAgents, logger: Logger): Promise<string> {
|
||||
logger.log('rerouting...')
|
||||
@@ -26,12 +26,12 @@ export async function handleLLMCompletion(context: NeuriContext, bot: Mineflayer
|
||||
logger.withFields({ usage: completion.usage, content }).log('output')
|
||||
|
||||
// Broadcast LLM trace
|
||||
DebugServer.getInstance().broadcast('llm', {
|
||||
DebugService.getInstance().traceLLM({
|
||||
route: 'action',
|
||||
messages: context.messages,
|
||||
content,
|
||||
usage: completion.usage,
|
||||
timestamp: Date.now(),
|
||||
model: config.openai.model,
|
||||
})
|
||||
|
||||
bot.memory.chatHistory.push(assistant(content))
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { LLMConfig, LLMResponse } from '../types'
|
||||
import { withRetry } from '@moeru/std'
|
||||
|
||||
import { config } from '../../composables/config'
|
||||
import { DebugServer } from '../../debug-server'
|
||||
import { DebugService } from '../../debug-server'
|
||||
import { useLogger } from '../../utils/logger'
|
||||
|
||||
export abstract class BaseLLMHandler {
|
||||
@@ -35,12 +35,12 @@ export abstract class BaseLLMHandler {
|
||||
this.logger.withFields({ usage: completion.usage, content }).log('Generated content')
|
||||
|
||||
// Broadcast LLM trace
|
||||
DebugServer.getInstance().broadcast('llm', {
|
||||
DebugService.getInstance().traceLLM({
|
||||
route,
|
||||
messages,
|
||||
content,
|
||||
usage: completion.usage,
|
||||
timestamp: Date.now(),
|
||||
model: this.config.model ?? config.openai.model,
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
@@ -11,18 +12,42 @@ import { useLogger } from './utils/logger'
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
export class DebugServer {
|
||||
private static instance: DebugServer
|
||||
interface DebugEvent {
|
||||
type: string
|
||||
payload: any
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export class DebugService {
|
||||
private static instance: DebugService
|
||||
private clients: Set<ServerResponse> = new Set()
|
||||
private server: http.Server | null = null
|
||||
|
||||
private constructor() {}
|
||||
// History buffer (Ring buffer)
|
||||
private history: DebugEvent[] = []
|
||||
private readonly MAX_HISTORY = 1000
|
||||
|
||||
public static getInstance(): DebugServer {
|
||||
if (!DebugServer.instance) {
|
||||
DebugServer.instance = new DebugServer()
|
||||
// File Logging
|
||||
private logStream: fs.WriteStream | null = null
|
||||
|
||||
private constructor() {
|
||||
this.initLogFile()
|
||||
}
|
||||
|
||||
public static getInstance(): DebugService {
|
||||
if (!DebugService.instance) {
|
||||
DebugService.instance = new DebugService()
|
||||
}
|
||||
return DebugServer.instance
|
||||
return DebugService.instance
|
||||
}
|
||||
|
||||
private initLogFile() {
|
||||
const logsDir = path.join(process.cwd(), 'logs')
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true })
|
||||
}
|
||||
const filename = `session-${new Date().toISOString().replace(/:/g, '-')}.jsonl`
|
||||
this.logStream = fs.createWriteStream(path.join(logsDir, filename), { flags: 'a' })
|
||||
}
|
||||
|
||||
public start(port = 3000): void {
|
||||
@@ -34,12 +59,24 @@ export class DebugServer {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
if (req.url === '/') {
|
||||
// Serve dashboard
|
||||
const htmlPath = path.join(__dirname, 'web', 'dashboard.html')
|
||||
const html = fs.readFileSync(htmlPath, 'utf-8')
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(html)
|
||||
try {
|
||||
const html = fs.readFileSync(htmlPath, 'utf-8')
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(html)
|
||||
}
|
||||
catch {
|
||||
res.writeHead(500)
|
||||
res.end('Dashboard not found')
|
||||
}
|
||||
}
|
||||
else if (req.url === '/events') {
|
||||
// Serve SSE stream
|
||||
@@ -56,9 +93,73 @@ export class DebugServer {
|
||||
})
|
||||
}
|
||||
|
||||
public broadcast(type: string, payload: any): void {
|
||||
const data = JSON.stringify(payload)
|
||||
const message = `event: ${type}\ndata: ${data}\n\n`
|
||||
// --- Public API for Brain/Agents ---
|
||||
|
||||
public log(level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG', message: string, fields?: any) {
|
||||
this.emit('log', { level, message, fields, timestamp: Date.now() })
|
||||
}
|
||||
|
||||
public traceLLM(trace: any) {
|
||||
this.emit('llm', { ...trace, timestamp: Date.now() })
|
||||
}
|
||||
|
||||
public updateBlackboard(state: any) {
|
||||
this.emit('blackboard', { state, timestamp: Date.now() })
|
||||
}
|
||||
|
||||
public updateQueue(queue: any[], processing?: any) {
|
||||
this.emit('queue', { queue, processing, timestamp: Date.now() })
|
||||
}
|
||||
|
||||
// Generic emit
|
||||
public emit(type: string, payload: any): void {
|
||||
const event: DebugEvent = {
|
||||
type,
|
||||
payload,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
// 1. Add to Histroy
|
||||
this.addToHistory(event)
|
||||
|
||||
// 2. Persist to Disk
|
||||
this.persistEvent(event)
|
||||
|
||||
// 3. Broadcast to Clients
|
||||
this.broadcast(event)
|
||||
}
|
||||
|
||||
// --- Internal Logic ---
|
||||
|
||||
private addToHistory(event: DebugEvent) {
|
||||
this.history.push(event)
|
||||
if (this.history.length > this.MAX_HISTORY) {
|
||||
this.history.shift()
|
||||
}
|
||||
}
|
||||
|
||||
private persistEvent(event: DebugEvent) {
|
||||
if (this.logStream) {
|
||||
try {
|
||||
this.logStream.write(`${JSON.stringify(event)}\n`)
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to write to log file', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private broadcast(event: DebugEvent): void {
|
||||
// Safe stringify to handle circular refs if any (basic protection)
|
||||
let data = ''
|
||||
try {
|
||||
data = JSON.stringify(event.payload)
|
||||
}
|
||||
catch {
|
||||
data = JSON.stringify({ error: 'Circular Reference or Serialization Error' })
|
||||
}
|
||||
|
||||
const message = `event: ${event.type}\ndata: ${data}\n\n`
|
||||
|
||||
for (const client of this.clients) {
|
||||
client.write(message)
|
||||
@@ -72,6 +173,18 @@ export class DebugServer {
|
||||
'Connection': 'keep-alive',
|
||||
})
|
||||
|
||||
// Send history immediately on connection
|
||||
for (const event of this.history) {
|
||||
let data = ''
|
||||
try {
|
||||
data = JSON.stringify(event.payload)
|
||||
}
|
||||
catch {
|
||||
continue
|
||||
}
|
||||
res.write(`event: ${event.type}\ndata: ${data}\n\n`)
|
||||
}
|
||||
|
||||
this.clients.add(res)
|
||||
|
||||
const keepAlive = setInterval(() => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { CognitiveEngine } from './cognitive'
|
||||
import { initBot } from './composables/bot'
|
||||
import { config, initEnv } from './composables/config'
|
||||
import { createNeuriAgent } from './composables/neuri'
|
||||
import { DebugServer } from './debug-server'
|
||||
import { DebugService } from './debug-server'
|
||||
import { wrapPlugin } from './libs/mineflayer'
|
||||
import { initLogger, useLogger } from './utils/logger'
|
||||
|
||||
@@ -24,7 +24,7 @@ async function main() {
|
||||
initEnv()
|
||||
|
||||
// Start debug server
|
||||
DebugServer.getInstance().start()
|
||||
DebugService.getInstance().start()
|
||||
|
||||
const { bot } = await initBot({
|
||||
botConfig: config.bot,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg'
|
||||
|
||||
import { DebugServer } from '../debug-server'
|
||||
import { DebugService } from '../debug-server'
|
||||
|
||||
export type Logger = ReturnType<typeof useLogg>
|
||||
|
||||
@@ -32,28 +32,15 @@ export function useLogger() {
|
||||
return {
|
||||
log: (message: string, ...args: any[]) => {
|
||||
logger.log(message, ...args)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'INFO',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
timestamp: Date.now(),
|
||||
fields: (logger as any).fields, // Access fields if stored on instance, currently Logg API might differ so we simplify
|
||||
})
|
||||
DebugService.getInstance().log('INFO', `[${dirName}/${fileName}] ${message}`, { args })
|
||||
},
|
||||
error: (message: string, ...args: any[]) => {
|
||||
logger.error(message, ...args)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'ERROR',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
DebugService.getInstance().log('ERROR', `[${dirName}/${fileName}] ${message}`, { args })
|
||||
},
|
||||
warn: (message: string, ...args: any[]) => {
|
||||
logger.warn(message, ...args)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'WARN',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
DebugService.getInstance().log('WARN', `[${dirName}/${fileName}] ${message}`, { args })
|
||||
},
|
||||
withFields: (fields: Record<string, any>) => {
|
||||
const subLogger = logger.withFields(fields)
|
||||
@@ -61,39 +48,19 @@ export function useLogger() {
|
||||
return {
|
||||
log: (message: string) => {
|
||||
subLogger.log(message)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'INFO',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
DebugService.getInstance().log('INFO', `[${dirName}/${fileName}] ${message}`, fields)
|
||||
},
|
||||
error: (message: string) => {
|
||||
subLogger.error(message)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'ERROR',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
DebugService.getInstance().log('ERROR', `[${dirName}/${fileName}] ${message}`, fields)
|
||||
},
|
||||
warn: (message: string) => {
|
||||
subLogger.warn(message)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'WARN',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
DebugService.getInstance().log('WARN', `[${dirName}/${fileName}] ${message}`, fields)
|
||||
},
|
||||
errorWithError: (message: string, error: unknown) => {
|
||||
subLogger.errorWithError(message, error)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'ERROR',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields: { ...fields, error },
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
DebugService.getInstance().log('ERROR', `[${dirName}/${fileName}] ${message}`, { ...fields, error })
|
||||
},
|
||||
withFields: (newFields: Record<string, any>) => useLogger().withFields({ ...fields, ...newFields }), // Recursion hack for simplicity, ideally properly implement interface
|
||||
withError: (err: unknown) => useLogger().withFields({ ...fields, error: err }), // Recursion hack
|
||||
@@ -104,12 +71,7 @@ export function useLogger() {
|
||||
},
|
||||
errorWithError: (message: string, error: unknown) => {
|
||||
logger.errorWithError(message, error)
|
||||
DebugServer.getInstance().broadcast('log', {
|
||||
level: 'ERROR',
|
||||
message: `[${dirName}/${fileName}] ${message}`,
|
||||
fields: { error },
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
DebugService.getInstance().log('ERROR', `[${dirName}/${fileName}] ${message}`, { error })
|
||||
},
|
||||
} as unknown as ReturnType<typeof useLogg> // Force type for now as complete Logg interface is complex to mock fully perfectly without more boilerplate
|
||||
}
|
||||
|
||||
@@ -235,35 +235,45 @@
|
||||
</header>
|
||||
<main>
|
||||
<nav>
|
||||
<div class="nav-item active" onclick="switchTab('task-status')">Task Status</div>
|
||||
<div class="nav-item active" onclick="switchTab('overview')">Overview</div>
|
||||
<div class="nav-item" onclick="switchTab('logs')">Logs</div>
|
||||
<div class="nav-item" onclick="switchTab('llm')">LLM Traces</div>
|
||||
<div class="nav-item" onclick="switchTab('blackboard')">Blackboard</div>
|
||||
</nav>
|
||||
|
||||
<div id="task-status-panel" class="content-panel active">
|
||||
<div id="overview-panel" class="content-panel active">
|
||||
<div class="toolbar">
|
||||
<h3 style="margin: 0;">Current Task State</h3>
|
||||
<h3 style="margin: 0;">System Overview</h3>
|
||||
</div>
|
||||
<div class="log-container" style="padding: 2rem;">
|
||||
<div id="current-task" style="margin-bottom: 2rem;">
|
||||
<div class="section-title">Active Task</div>
|
||||
<div id="task-info" class="llm-card" style="background-color: #1e1e1e;">
|
||||
<div style="padding: 1rem; color: #666;">No active task</div>
|
||||
<!-- Event Queue -->
|
||||
<div id="queue-section" style="margin-bottom: 2rem;">
|
||||
<div class="section-title">Event Queue (<span id="queue-size">0</span>)</div>
|
||||
<div id="queue-list" style="display: flex; flex-direction: column; gap: 0.5rem; min-height: 50px; background: #111; padding: 1rem; border-radius: 4px;">
|
||||
<div style="color: #666;">Queue is empty</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="task-queue" style="margin-bottom: 2rem;">
|
||||
<div class="section-title">Event Queue (<span id="queue-count">0</span>)</div>
|
||||
<div id="queue-list" style="display: flex; flex-direction: column; gap: 0.5rem;"></div>
|
||||
</div>
|
||||
|
||||
<div id="task-history">
|
||||
<div class="section-title">Recent Tasks (Last 5)</div>
|
||||
<div id="history-list" style="display: flex; flex-direction: column; gap: 0.5rem;"></div>
|
||||
<!-- Current Processing -->
|
||||
<div id="processing-section">
|
||||
<div class="section-title">Currently Processing</div>
|
||||
<div id="current-event" class="llm-card" style="background-color: #1e1e1e;">
|
||||
<div style="padding: 1rem; color: #666;">Idle</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logs-panel" class="content-panel active">
|
||||
|
||||
<div id="blackboard-panel" class="content-panel">
|
||||
<div class="toolbar">
|
||||
<h3 style="margin: 0;">Blackboard State</h3>
|
||||
</div>
|
||||
<div class="log-container">
|
||||
<pre id="blackboard-content" class="json-block" style="height: 100%; border: none;">{}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="logs-panel" class="content-panel">
|
||||
<div class="toolbar">
|
||||
<button onclick="clearLogs()">Clear</button>
|
||||
<label><input type="checkbox" id="auto-scroll" checked> Auto-scroll</label>
|
||||
@@ -288,6 +298,10 @@
|
||||
autoScroll = e.target.checked;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function switchTab(tabId) {
|
||||
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
||||
document.querySelectorAll('.content-panel').forEach(el => el.classList.remove('active'));
|
||||
@@ -309,7 +323,7 @@
|
||||
const div = document.createElement('div');
|
||||
div.className = 'log-entry level-' + (entry.level || 'info').toLowerCase();
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
const time = new Date(entry.timestamp || Date.now()).toLocaleTimeString();
|
||||
const fieldsJson = entry.fields ? JSON.stringify(entry.fields) : '';
|
||||
|
||||
div.innerHTML = `<span class="log-time">[${time}]</span> <span class="log-level">[${entry.level}]</span> ${entry.message} ${fieldsJson ? `<br><span style="color:#666;font-size:0.9em">${fieldsJson}</span>` : ''}`;
|
||||
@@ -322,10 +336,7 @@
|
||||
}
|
||||
|
||||
function addLLMTrace(trace) {
|
||||
if (!trace) {
|
||||
console.error('Received undefined trace');
|
||||
return;
|
||||
}
|
||||
if (!trace) return;
|
||||
|
||||
const container = document.getElementById('llm-list');
|
||||
const div = document.createElement('div');
|
||||
@@ -386,7 +397,63 @@
|
||||
container.insertBefore(div, container.firstChild);
|
||||
}
|
||||
|
||||
let eventSource = null;
|
||||
function updateQueue(data) {
|
||||
const queueList = document.getElementById('queue-list');
|
||||
const queueSize = document.getElementById('queue-size');
|
||||
const currentEventEl = document.getElementById('current-event');
|
||||
|
||||
if (!data) return;
|
||||
|
||||
// Update Header
|
||||
queueSize.textContent = data.queue ? data.queue.length : 0;
|
||||
|
||||
// Render Queue
|
||||
if (data.queue && data.queue.length > 0) {
|
||||
queueList.innerHTML = data.queue.map((item, idx) => {
|
||||
return `
|
||||
<div class="llm-card" style="margin-bottom: 0.5rem; background-color: #1e1e1e;">
|
||||
<div style="padding: 0.5rem; display: flex; justify-content: space-between;">
|
||||
<span><span style="color: #666; margin-right: 0.5rem;">#${idx+1}</span> ${item.type}</span>
|
||||
<span class="badge">${item.source?.id || 'unknown'}</span>
|
||||
</div>
|
||||
<div style="padding: 0 0.5rem 0.5rem 0.5rem; color: #aaa; font-size: 0.9em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
${item.payload?.content || JSON.stringify(item.payload)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}).join('');
|
||||
} else {
|
||||
queueList.innerHTML = '<div style="color: #666;">Queue is empty</div>';
|
||||
}
|
||||
|
||||
// Render Current Processing
|
||||
if (data.processing) {
|
||||
const item = data.processing;
|
||||
currentEventEl.innerHTML = `
|
||||
<div style="padding: 1rem;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span style="color: var(--accent-color); font-weight: bold;">${item.type}</span>
|
||||
<span class="badge" style="background-color: var(--accent-color);">PROCESSING</span>
|
||||
</div>
|
||||
<div style="background-color: #111; padding: 0.5rem; border-radius: 4px; margin-top: 0.5rem;">
|
||||
<pre style="margin: 0; white-space: pre-wrap;">${JSON.stringify(item.payload, null, 2)}</pre>
|
||||
</div>
|
||||
<div style="margin-top: 0.5rem; font-size: 0.9em; color: #888;">
|
||||
Source: ${item.source?.type} / ${item.source?.id}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
currentEventEl.innerHTML = '<div style="padding: 1rem; color: #666;">Idle</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateBlackboard(data) {
|
||||
const el = document.getElementById('blackboard-content');
|
||||
if (data && data.state) {
|
||||
el.textContent = JSON.stringify(data.state, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
function reconnect() {
|
||||
if (eventSource) {
|
||||
@@ -419,133 +486,27 @@
|
||||
|
||||
eventSource.addEventListener('log', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
addLog(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse log', e);
|
||||
}
|
||||
addLog(JSON.parse(e.data));
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
eventSource.addEventListener('llm', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
addLLMTrace(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse llm trace', e);
|
||||
}
|
||||
addLLMTrace(JSON.parse(e.data));
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
eventSource.addEventListener('task-status', (e) => {
|
||||
eventSource.addEventListener('queue', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
updateTaskStatus(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse task status', e);
|
||||
}
|
||||
updateQueue(JSON.parse(e.data));
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
}
|
||||
|
||||
function updateTaskStatus(status) {
|
||||
// Update active tasks
|
||||
const taskInfo = document.getElementById('task-info');
|
||||
|
||||
// Handle new multi-task structure or fallback to single task
|
||||
let tasks = [];
|
||||
if (status.activeTasks && Array.isArray(status.activeTasks)) {
|
||||
tasks = status.activeTasks;
|
||||
} else if (status.currentTask) {
|
||||
tasks = [status.currentTask];
|
||||
}
|
||||
|
||||
if (tasks.length > 0) {
|
||||
taskInfo.innerHTML = tasks.map(task => {
|
||||
// Determine if primary based on data or assumption
|
||||
// Note: TaskManager now logs 'type' but task object structure might not have it explicitly unless we added it to TaskContext
|
||||
// But we can infer order or just display all.
|
||||
// Let's assume the first one might be primary if we used getAllActiveTasks() order (primary first)
|
||||
|
||||
const elapsed = Math.floor((Date.now() - task.startTime) / 1000);
|
||||
const statusColor = {
|
||||
'idle': '#888',
|
||||
'planning': '#ffb74d',
|
||||
'executing': '#4a9eff',
|
||||
'responding': '#9c27b0',
|
||||
'cancelling': '#ef5350'
|
||||
}[task.status] || '#888';
|
||||
|
||||
// Visual distinction for secondary vs primary could be nice, but simple list is fine for now
|
||||
return `
|
||||
<div style="padding: 1rem; border-bottom: 1px solid #333; last-child: border-bottom: none;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span style="font-weight: bold; font-size: 1.1em;">${task.goal}</span>
|
||||
<span class="badge" style="background-color: ${statusColor}">${task.status.toUpperCase()}</span>
|
||||
</div>
|
||||
<div style="color: #888; font-size: 0.9em;">
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<span>ID: <span title="${task.id}">${task.id.substr(0,12)}...</span></span>
|
||||
<span>Time: ${elapsed}s</span>
|
||||
</div>
|
||||
${task.currentStep ? `<div style="margin-top:0.25rem;">Step: ${task.currentStep}</div>` : ''}
|
||||
${task.plan ? `<div style="margin-top:0.25rem;">Plan: ${task.plan.steps.length} steps (${task.plan.status})</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
taskInfo.innerHTML = '<div style="padding: 1rem; color: #666;">No active tasks</div>';
|
||||
}
|
||||
|
||||
// Update queue
|
||||
// ... (rest same, removing queue updates here as they are separate chunks?)
|
||||
// Actually I need to include the rest of the function or it will be cut off.
|
||||
// Let's just replace the task rendering part if I target specific lines.
|
||||
|
||||
const queueCount = document.getElementById('queue-count');
|
||||
const queueList = document.getElementById('queue-list');
|
||||
queueCount.textContent = status.queueSize || 0;
|
||||
|
||||
if (status.queue && status.queue.length > 0) {
|
||||
queueList.innerHTML = status.queue.map((event, idx) => `
|
||||
<div class="message" style="background-color: #1e1e1e; border-left-color: #ffb74d;">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>#${idx + 1}: ${event.payload?.content || 'Unknown event'}</span>
|
||||
<span class="badge">Priority: ${event.priority || 5}</span>
|
||||
</div>
|
||||
<div style="color: #666; font-size: 0.85em; margin-top: 0.5rem;">
|
||||
From: ${event.source?.id || 'unknown'}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
queueList.innerHTML = '<div style="color: #666; padding: 1rem;">Queue is empty</div>';
|
||||
}
|
||||
|
||||
// Update history
|
||||
const historyList = document.getElementById('history-list');
|
||||
if (status.history && status.history.length > 0) {
|
||||
historyList.innerHTML = status.history.slice(0, 5).map(task => {
|
||||
const elapsed = Math.floor((Date.now() - task.startTime) / 1000);
|
||||
const statusColor = {
|
||||
'completed': '#4caf50',
|
||||
'failed': '#ef5350',
|
||||
'cancelled': '#ff9800'
|
||||
}[task.plan?.status] || '#888';
|
||||
|
||||
return `
|
||||
<div class="message" style="background-color: #1e1e1e; border-left-color: ${statusColor};">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>${task.goal}</span>
|
||||
<span class="badge" style="background-color: ${statusColor}">${task.plan?.status || task.status}</span>
|
||||
</div>
|
||||
<div style="color: #666; font-size: 0.85em; margin-top: 0.5rem;">
|
||||
Duration: ${elapsed}s
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
historyList.innerHTML = '<div style="color: #666; padding: 1rem;">No task history</div>';
|
||||
}
|
||||
eventSource.addEventListener('blackboard', (e) => {
|
||||
try {
|
||||
updateBlackboard(JSON.parse(e.data));
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
}
|
||||
|
||||
connectSSE();
|
||||
|
||||
Reference in New Issue
Block a user