diff --git a/src/needleFabric.integration.test.ts b/src/needleFabric.integration.test.ts new file mode 100644 index 0000000..c41a976 --- /dev/null +++ b/src/needleFabric.integration.test.ts @@ -0,0 +1,600 @@ +/** + * NEEDLE-FABRIC Integration Test + * + * Verifies FABRIC can correctly parse NEEDLE logs in production format. + * Uses real log samples from ~/.needle/logs/ to ensure compatibility. + */ + +import { describe, it, expect } from 'vitest'; +import { parseLogLine, parseLogLines } from './parser.js'; +import { LogEvent } from './types.js'; + +describe('NEEDLE-FABRIC Integration', () => { + describe('worker.started events', () => { + it('should parse worker.started with object worker format', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:34.008Z', + event: 'worker.started', + session: 'needle-claude-anthropic-sonnet-test12', + worker: { + runner: 'claude', + provider: 'anthropic', + model: 'sonnet', + identifier: 'test12', + }, + data: { + pid: 1929276, + workspace: '/home/coder/NEEDLE', + agent: 'claude-anthropic-sonnet', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.ts).toBe(new Date('2026-03-04T16:17:34.008Z').getTime()); + expect(result?.worker).toBe('claude-test12'); + expect(result?.level).toBe('info'); + expect(result?.msg).toBe('worker.started'); + expect(result?.session).toBe('needle-claude-anthropic-sonnet-test12'); + expect(result?.provider).toBe('anthropic'); + expect(result?.model).toBe('sonnet'); + expect(result?.pid).toBe(1929276); + expect(result?.workspace).toBe('/home/coder/NEEDLE'); + expect(result?.agent).toBe('claude-anthropic-sonnet'); + }); + + it('should parse worker.started with string worker format', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:34.051Z', + event: 'worker.started', + session: 'test-session', + worker: 'claude-code-sonnet-worker1', + data: { + workspace: '/home/coder/NEEDLE', + agent: 'claude-anthropic-sonnet', + timestamp: '2026-03-04T16:17:34Z', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.worker).toBe('claude-code-sonnet-worker1'); + expect(result?.msg).toBe('worker.started'); + expect(result?.level).toBe('info'); + expect(result?.workspace).toBe('/home/coder/NEEDLE'); + }); + }); + + describe('bead lifecycle events', () => { + it('should parse bead.claimed event with bead_id extraction', () => { + const log = JSON.stringify({ + ts: '2026-03-04T19:31:34.851Z', + event: 'bead.claimed', + session: 'forge-glm-test', + worker: { + runner: 'claude', + provider: 'code', + model: 'glm-4.7', + identifier: 'test', + }, + data: { + bead_id: 'bd-2ok0', + actor: 'forge-glm-test', + attempt: 1, + workspace: '/home/coder/forge', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.bead).toBe('bd-2ok0'); + expect(result?.msg).toBe('bead.claimed'); + expect(result?.level).toBe('info'); + expect(result?.actor).toBe('forge-glm-test'); + expect(result?.attempt).toBe(1); + }); + + it('should parse bead.completed event with duration', () => { + const log = JSON.stringify({ + ts: '2026-03-04T19:37:19.590Z', + event: 'bead.completed', + session: 'forge-glm-test', + worker: { + runner: 'claude', + provider: 'code', + model: 'glm-4.7', + identifier: 'test', + }, + data: { + bead_id: 'bd-2ok0', + duration_ms: 28854, + output_file: '/tmp/needle-dispatch-bd-2ok0-FHwgcG7A.log', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.bead).toBe('bd-2ok0'); + expect(result?.msg).toBe('bead.completed'); + expect(result?.level).toBe('info'); + expect(result?.duration_ms).toBe(28854); + expect(result?.output_file).toBe('/tmp/needle-dispatch-bd-2ok0-FHwgcG7A.log'); + }); + + it('should parse bead.agent_started event', () => { + const log = JSON.stringify({ + ts: '2026-03-04T19:31:35.516Z', + event: 'bead.agent_started', + session: 'forge-glm-test', + worker: { + runner: 'claude', + provider: 'code', + model: 'glm-4.7', + identifier: 'test', + }, + data: { + bead_id: 'bd-2ok0', + agent: 'claude-code-glm-4.7', + workspace: '/home/coder/forge', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.bead).toBe('bd-2ok0'); + expect(result?.msg).toBe('bead.agent_started'); + expect(result?.level).toBe('info'); + expect(result?.agent).toBe('claude-code-glm-4.7'); + }); + + it('should parse bead.claim_retry event with warn level', () => { + const log = JSON.stringify({ + ts: '2026-03-04T19:37:22.192Z', + event: 'bead.claim_retry', + session: 'forge-glm-test', + worker: { + runner: 'claude', + provider: 'code', + model: 'glm-4.7', + identifier: 'test', + }, + data: { + bead_id: 'bd-e6jq', + attempt: 1, + max_retries: 5, + actor: 'forge-glm-test', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.bead).toBe('bd-e6jq'); + expect(result?.msg).toBe('bead.claim_retry'); + expect(result?.level).toBe('warn'); + expect(result?.attempt).toBe(1); + expect(result?.max_retries).toBe(5); + }); + + it('should parse bead.prompt_built event', () => { + const log = JSON.stringify({ + ts: '2026-03-04T19:31:35.501Z', + event: 'bead.prompt_built', + session: 'forge-glm-test', + worker: { + runner: 'claude', + provider: 'code', + model: 'glm-4.7', + identifier: 'test', + }, + data: { + bead_id: 'bd-2ok0', + workspace: '/home/coder/forge', + prompt_length: 1541, + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.bead).toBe('bd-2ok0'); + expect(result?.msg).toBe('bead.prompt_built'); + expect(result?.prompt_length).toBe(1541); + }); + }); + + describe('worker state events', () => { + it('should parse worker.idle event with consecutive counts', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:36.243Z', + event: 'worker.idle', + session: 'needle-claude-anthropic-sonnet-test12', + worker: { + runner: 'claude', + provider: 'anthropic', + model: 'sonnet', + identifier: 'test12', + }, + data: { + consecutive_empty: 1, + idle_seconds: 0, + workspace: '/home/coder/NEEDLE', + agent: 'claude-anthropic-sonnet', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.msg).toBe('worker.idle'); + expect(result?.level).toBe('info'); + expect(result?.consecutive_empty).toBe(1); + expect(result?.idle_seconds).toBe(0); + }); + + it('should parse worker.draining event', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:43.261Z', + event: 'worker.draining', + session: 'needle-claude-anthropic-sonnet-test12', + worker: { + runner: 'claude', + provider: 'anthropic', + model: 'sonnet', + identifier: 'test12', + }, + data: {}, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.msg).toBe('worker.draining'); + expect(result?.level).toBe('info'); + }); + + it('should parse worker.shutdown_initiated event', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:43.269Z', + event: 'worker.shutdown_initiated', + session: 'needle-claude-anthropic-sonnet-test12', + worker: { + runner: 'claude', + provider: 'anthropic', + model: 'sonnet', + identifier: 'test12', + }, + data: { + signal: 'TERM', + session: 'needle-claude-anthropic-sonnet-test12', + timestamp: '2026-03-04T16:17:43Z', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.msg).toBe('worker.shutdown_initiated'); + expect(result?.level).toBe('info'); + expect(result?.signal).toBe('TERM'); + }); + }); + + describe('effort tracking events', () => { + it('should parse effort.recorded event', () => { + const log = JSON.stringify({ + ts: '2026-03-04T19:37:19.616Z', + event: 'effort.recorded', + session: 'forge-glm-test', + worker: { + runner: 'claude', + provider: 'code', + model: 'glm-4.7', + identifier: 'test', + }, + data: { + bead_id: 'bd-2ok0', + duration_ms: 28854, + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.bead).toBe('bd-2ok0'); + expect(result?.msg).toBe('effort.recorded'); + expect(result?.duration_ms).toBe(28854); + }); + }); + + describe('error events', () => { + it('should infer error level for events with "error" in name', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:34.008Z', + event: 'bead.error', + session: 'test-session', + worker: { + runner: 'claude', + provider: 'code', + model: 'sonnet', + identifier: 'test', + }, + data: { + bead_id: 'bd-abc', + error: 'Failed to process bead', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.level).toBe('error'); + expect(result?.msg).toBe('bead.error'); + expect(result?.error).toBe('Failed to process bead'); + }); + + it('should infer error level for events with "fail" in name', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:34.008Z', + event: 'bead.failed', + session: 'test-session', + worker: 'claude-test', + data: { + bead_id: 'bd-xyz', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.level).toBe('error'); + expect(result?.msg).toBe('bead.failed'); + }); + + it('should infer error level for queue.exhausted events', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:34.008Z', + event: 'queue.exhausted', + session: 'test-session', + worker: 'claude-test', + data: {}, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.level).toBe('error'); + expect(result?.msg).toBe('queue.exhausted'); + }); + }); + + describe('multi-line log parsing', () => { + it('should parse complete worker session from multiple log lines', () => { + const logs = [ + '{"ts":"2026-03-04T16:17:34.008Z","event":"worker.started","session":"test","worker":{"runner":"claude","provider":"code","model":"sonnet","identifier":"w1"},"data":{"pid":123}}', + '{"ts":"2026-03-04T16:17:35.000Z","event":"bead.claimed","session":"test","worker":{"runner":"claude","provider":"code","model":"sonnet","identifier":"w1"},"data":{"bead_id":"bd-abc"}}', + '{"ts":"2026-03-04T16:17:40.000Z","event":"bead.completed","session":"test","worker":{"runner":"claude","provider":"code","model":"sonnet","identifier":"w1"},"data":{"bead_id":"bd-abc","duration_ms":5000}}', + '{"ts":"2026-03-04T16:17:41.000Z","event":"worker.idle","session":"test","worker":{"runner":"claude","provider":"code","model":"sonnet","identifier":"w1"},"data":{"consecutive_empty":1}}', + ].join('\n'); + + const results = parseLogLines(logs); + + expect(results).toHaveLength(4); + + // Verify first event + expect(results[0].msg).toBe('worker.started'); + expect(results[0].worker).toBe('claude-w1'); + expect(results[0].pid).toBe(123); + + // Verify second event + expect(results[1].msg).toBe('bead.claimed'); + expect(results[1].bead).toBe('bd-abc'); + + // Verify third event + expect(results[2].msg).toBe('bead.completed'); + expect(results[2].bead).toBe('bd-abc'); + expect(results[2].duration_ms).toBe(5000); + + // Verify fourth event + expect(results[3].msg).toBe('worker.idle'); + expect(results[3].consecutive_empty).toBe(1); + + // All events should have same worker + expect(results.every((r) => r.worker === 'claude-w1')).toBe(true); + + // All events should have same session + expect(results.every((r) => r.session === 'test')).toBe(true); + }); + + it('should handle mixed NEEDLE and legacy format logs', () => { + const logs = [ + '{"ts":"2026-03-04T16:17:34.008Z","event":"worker.started","session":"test","worker":"claude-w1","data":{}}', + '{"ts":1709569054008,"worker":"legacy-w2","level":"info","msg":"Legacy message"}', + '{"ts":"2026-03-04T16:17:36.000Z","event":"bead.claimed","session":"test","worker":"claude-w1","data":{"bead_id":"bd-xyz"}}', + ].join('\n'); + + const results = parseLogLines(logs); + + expect(results).toHaveLength(3); + + // First NEEDLE event + expect(results[0].worker).toBe('claude-w1'); + expect(results[0].msg).toBe('worker.started'); + expect(results[0].level).toBe('info'); + + // Legacy event + expect(results[1].worker).toBe('legacy-w2'); + expect(results[1].msg).toBe('Legacy message'); + expect(results[1].level).toBe('info'); + + // Second NEEDLE event + expect(results[2].worker).toBe('claude-w1'); + expect(results[2].msg).toBe('bead.claimed'); + expect(results[2].bead).toBe('bd-xyz'); + }); + + it('should skip invalid lines and continue parsing', () => { + const logs = [ + '{"ts":"2026-03-04T16:17:34.008Z","event":"worker.started","session":"test","worker":"claude-w1","data":{}}', + 'not valid json', + '', + '{"ts":"2026-03-04T16:17:35.000Z","event":"bead.claimed","session":"test","worker":"claude-w1","data":{"bead_id":"bd-abc"}}', + '{"invalid":"structure"}', + '{"ts":"2026-03-04T16:17:36.000Z","event":"bead.completed","session":"test","worker":"claude-w1","data":{"bead_id":"bd-abc"}}', + ].join('\n'); + + const results = parseLogLines(logs); + + expect(results).toHaveLength(3); + expect(results[0].msg).toBe('worker.started'); + expect(results[1].msg).toBe('bead.claimed'); + expect(results[2].msg).toBe('bead.completed'); + }); + }); + + describe('timestamp conversion', () => { + it('should correctly convert ISO timestamps to Unix milliseconds', () => { + const isoTimestamp = '2026-03-04T19:31:34.851Z'; + const expectedUnixMs = new Date(isoTimestamp).getTime(); + + const log = JSON.stringify({ + ts: isoTimestamp, + event: 'test.event', + session: 'test', + worker: 'test-worker', + data: {}, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.ts).toBe(expectedUnixMs); + // Verify it's a valid timestamp in the future (2026) + expect(result?.ts).toBeGreaterThan(1700000000000); // > Nov 2023 + }); + + it('should handle different ISO timestamp formats', () => { + const timestamps = [ + '2026-03-04T16:17:34.008Z', + '2026-03-04T16:17:34Z', + '2026-03-04T16:17:34.123456Z', + ]; + + timestamps.forEach((ts) => { + const log = JSON.stringify({ + ts, + event: 'test.event', + session: 'test', + worker: 'test-worker', + data: {}, + }); + + const result = parseLogLine(log); + expect(result).not.toBeNull(); + expect(result?.ts).toBe(new Date(ts).getTime()); + }); + }); + }); + + describe('field extraction from data payload', () => { + it('should extract all common fields from data', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:34.008Z', + event: 'test.event', + session: 'test-session', + worker: 'test-worker', + data: { + bead_id: 'bd-123', + duration_ms: 1000, + error: 'test error', + tool: 'Read', + path: '/test/path.ts', + custom_field: 'custom_value', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.bead).toBe('bd-123'); + expect(result?.duration_ms).toBe(1000); + expect(result?.error).toBe('test error'); + expect(result?.tool).toBe('Read'); + expect(result?.path).toBe('/test/path.ts'); + expect(result?.custom_field).toBe('custom_value'); + }); + + it('should preserve NEEDLE-specific fields', () => { + const log = JSON.stringify({ + ts: '2026-03-04T16:17:34.008Z', + event: 'worker.started', + session: 'test-session', + worker: { + runner: 'claude', + provider: 'anthropic', + model: 'sonnet', + identifier: 'test', + }, + data: { + workspace: '/home/coder/NEEDLE', + agent: 'claude-anthropic-sonnet', + }, + }); + + const result = parseLogLine(log); + + expect(result).not.toBeNull(); + expect(result?.session).toBe('test-session'); + expect(result?.provider).toBe('anthropic'); + expect(result?.model).toBe('sonnet'); + expect(result?.workspace).toBe('/home/coder/NEEDLE'); + expect(result?.agent).toBe('claude-anthropic-sonnet'); + }); + }); + + describe('complete real-world log sequence', () => { + it('should parse complete bead lifecycle from real NEEDLE logs', () => { + // This simulates a real bead execution from start to finish + const realLogs = `{"ts":"2026-03-04T19:31:34.851Z","event":"bead.claimed","session":"forge-glm-test","worker":{"runner":"claude","provider":"code","model":"glm-4.7","identifier":"test"},"data":{"bead_id":"bd-2ok0","actor":"forge-glm-test","attempt":1,"workspace":"/home/coder/forge"}} +{"ts":"2026-03-04T19:31:34.978Z","event":"bead.claimed","session":"forge-glm-test","worker":{"runner":"claude","provider":"code","model":"glm-4.7","identifier":"test"},"data":{"bead_id":"bd-2ok0","workspace":"/home/coder/forge","agent":"claude-code-glm-4.7","title":"Add model alias mapping for opencode (sonnet -> provider/model)"}} +{"ts":"2026-03-04T19:31:34.997Z","event":"bead.mitosis.check","session":"forge-glm-test","worker":{"runner":"claude","provider":"code","model":"glm-4.7","identifier":"test"},"data":{"bead_id":"bd-2ok0"}} +{"ts":"2026-03-04T19:31:35.501Z","event":"bead.prompt_built","session":"forge-glm-test","worker":{"runner":"claude","provider":"code","model":"glm-4.7","identifier":"test"},"data":{"bead_id":"bd-2ok0","workspace":"/home/coder/forge","prompt_length":1541}} +{"ts":"2026-03-04T19:31:35.516Z","event":"bead.agent_started","session":"forge-glm-test","worker":{"runner":"claude","provider":"code","model":"glm-4.7","identifier":"test"},"data":{"bead_id":"bd-2ok0","agent":"claude-code-glm-4.7","workspace":"/home/coder/forge"}} +{"ts":"2026-03-04T19:37:19.590Z","event":"bead.completed","session":"forge-glm-test","worker":{"runner":"claude","provider":"code","model":"glm-4.7","identifier":"test"},"data":{"bead_id":"bd-2ok0","duration_ms":28854,"output_file":"/tmp/needle-dispatch-bd-2ok0-FHwgcG7A.log"}} +{"ts":"2026-03-04T19:37:19.616Z","event":"effort.recorded","session":"forge-glm-test","worker":{"runner":"claude","provider":"code","model":"glm-4.7","identifier":"test"},"data":{"bead_id":"bd-2ok0","duration_ms":28854}}`; + + const events = parseLogLines(realLogs); + + expect(events).toHaveLength(7); + + // Verify all events have correct worker + expect(events.every((e) => e.worker === 'claude-test')).toBe(true); + + // Verify all events have correct session + expect(events.every((e) => e.session === 'forge-glm-test')).toBe(true); + + // Verify all events have correct bead_id (except first claim) + expect(events.slice(0).every((e) => e.bead === 'bd-2ok0')).toBe(true); + + // Verify event sequence + expect(events[0].msg).toBe('bead.claimed'); + expect(events[1].msg).toBe('bead.claimed'); + expect(events[2].msg).toBe('bead.mitosis.check'); + expect(events[3].msg).toBe('bead.prompt_built'); + expect(events[4].msg).toBe('bead.agent_started'); + expect(events[5].msg).toBe('bead.completed'); + expect(events[6].msg).toBe('effort.recorded'); + + // Verify specific fields + expect(events[3].prompt_length).toBe(1541); + expect(events[5].duration_ms).toBe(28854); + expect(events[5].output_file).toBe('/tmp/needle-dispatch-bd-2ok0-FHwgcG7A.log'); + expect(events[6].duration_ms).toBe(28854); + }); + }); +}); diff --git a/src/semanticNarrative.test.ts b/src/semanticNarrative.test.ts index 0885f39..dacb84b 100644 --- a/src/semanticNarrative.test.ts +++ b/src/semanticNarrative.test.ts @@ -659,18 +659,11 @@ describe('SemanticNarrativeGenerator', () => { }); describe('narrative updates', () => { - it('should emit updates when events are processed', (done) => { + it('should emit updates when events are processed', async () => { const updates: NarrativeUpdate[] = []; const unsubscribe = generator.onUpdate((update) => { updates.push(update); - - if (updates.length === 2) { - expect(updates[0].type).toBe('segment_updated'); - expect(updates[1].type).toBe('segment_updated'); - unsubscribe(); - done(); - } }); const baseTime = Date.now(); @@ -691,6 +684,21 @@ describe('SemanticNarrativeGenerator', () => { tool: 'Edit', path: '/src/file.ts', }); + + // Wait for updates to be emitted + await new Promise((resolve) => { + const checkUpdates = () => { + if (updates.length >= 2) { + expect(updates[0].type).toBe('segment_updated'); + expect(updates[1].type).toBe('segment_updated'); + unsubscribe(); + resolve(); + } else { + setTimeout(checkUpdates, 10); + } + }; + checkUpdates(); + }); }); it('should allow unsubscribing from updates', () => { diff --git a/src/semanticNarrative.ts b/src/semanticNarrative.ts index 3127c45..40c3e90 100644 --- a/src/semanticNarrative.ts +++ b/src/semanticNarrative.ts @@ -814,6 +814,12 @@ export class SemanticNarrativeGenerator implements SemanticNarrativeManager { investigation: 'investigating', collision_detected: 'resolving conflicts', error_recovery: 'recovering from errors', + tool_usage: 'using tools', + error_handling: 'handling errors', + task_completion: 'completing tasks', + exploration: 'exploring', + planning: 'planning', + research: 'researching', }; return verbs[pattern] || 'working'; diff --git a/src/store.ts b/src/store.ts index bd5de31..97157d6 100644 --- a/src/store.ts +++ b/src/store.ts @@ -286,8 +286,12 @@ export class InMemoryEventStore implements EventStore { activeBead: event.bead, activeDirectories: [], collisionTypes: [], + eventCount: 1, }; this.workers.set(event.worker, worker); + } else { + // Increment event count + worker.eventCount++; } // Update last activity diff --git a/src/types.ts b/src/types.ts index 33e8bff..730941b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -295,6 +295,9 @@ export interface WorkerInfo { /** All collision types this worker is involved in */ collisionTypes: ('file' | 'bead' | 'task')[]; + + /** Total number of events received for this worker */ + eventCount: number; } export interface EventFilter { @@ -1607,6 +1610,16 @@ export interface WorkerMetrics { /** Tokens per bead */ tokensPerBead: number; + + /** Efficiency score (0-1, calculated from active time vs idle time) */ + efficiencyScore: number; + + /** Performance trend data */ + trend?: { + direction: 'improving' | 'declining' | 'stable'; + confidence: number; + factors: string[]; + }; } /** @@ -1692,6 +1705,18 @@ export interface AggregatedAnalytics { /** Most cost-efficient workers (lowest cost per bead) */ costEfficientWorkers: WorkerMetrics[]; + + /** Number of currently active workers */ + activeWorkerCount: number; + + /** Total tokens used across all workers */ + totalTokens: number; + + /** Average efficiency across all workers */ + avgEfficiency: number; + + /** Underperforming workers (high error rate or low efficiency) */ + underperformers: WorkerMetrics[]; } /** @@ -1776,7 +1801,13 @@ export type EventPattern = | 'collision_detected' // Workers colliding | 'error_recovery' // Recovering from errors | 'iteration' // Iterative refinement - | 'investigation'; // Investigating/researching + | 'investigation' // Investigating/researching + | 'tool_usage' // Tool usage patterns + | 'error_handling' // Error handling patterns + | 'task_completion' // Task completion patterns + | 'exploration' // Exploration patterns + | 'planning' // Planning patterns + | 'research'; // Research patterns /** * A single narrative segment describing a sequence of events diff --git a/src/workerAnalytics.ts b/src/workerAnalytics.ts index cf865a0..7905184 100644 --- a/src/workerAnalytics.ts +++ b/src/workerAnalytics.ts @@ -200,6 +200,16 @@ export class WorkerAnalytics implements WorkerAnalyticsStore { .sort((a, b) => a.costPerBead - b.costPerBead) .slice(0, opts.maxWorkers); + // Calculate aggregated additional metrics + const activeWorkerCount = allMetrics.filter(m => m.beadsCompleted > 0).length; + const totalTokens = allMetrics.reduce((sum, m) => sum + m.totalTokens, 0); + const avgEfficiency = allMetrics.length > 0 + ? allMetrics.reduce((sum, m) => sum + m.efficiencyScore, 0) / allMetrics.length + : 0; + const underperformers = allMetrics + .filter(m => m.errorRate > 0.2 || m.efficiencyScore < 0.5) + .slice(0, opts.maxWorkers); + return { periodStart: startTime, periodEnd: endTime, @@ -214,6 +224,10 @@ export class WorkerAnalytics implements WorkerAnalyticsStore { topPerformers, highErrorRateWorkers, costEfficientWorkers, + activeWorkerCount, + totalTokens, + avgEfficiency, + underperformers, }; } @@ -473,6 +487,10 @@ export class WorkerAnalytics implements WorkerAnalyticsStore { const totalTokens = worker.totalTokens; const tokensPerBead = beadsCompleted > 0 ? totalTokens / beadsCompleted : 0; + // Efficiency score: ratio of active time to total time (0-1) + const totalTimeMs = activeTimeMs + idleTimeMs; + const efficiencyScore = totalTimeMs > 0 ? activeTimeMs / totalTimeMs : 0; + return { workerId: worker.workerId, periodStart: startTime, @@ -490,6 +508,7 @@ export class WorkerAnalytics implements WorkerAnalyticsStore { totalEvents: eventsInRange.length, totalTokens, tokensPerBead, + efficiencyScore, }; } @@ -602,6 +621,10 @@ export class WorkerAnalytics implements WorkerAnalyticsStore { topPerformers: [], highErrorRateWorkers: [], costEfficientWorkers: [], + activeWorkerCount: 0, + totalTokens: 0, + avgEfficiency: 0, + underperformers: [], }; } }