diff --git a/docs/cli.md b/docs/cli.md index 38a6522..7d40915 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -29,6 +29,7 @@ Set the usual provider API keys in the environment (see [README](../README.md#qu ### `oma run` Runs **`OpenMultiAgent.runTeam(team, goal)`**: coordinator decomposition, task queue, optional synthesis. +Each `runTeam` call also writes a static post-execution DAG dashboard HTML to `oma-dashboards/runTeam-.html` in the current working directory. | Argument | Required | Description | |----------|----------|-------------| diff --git a/src/orchestrator/orchestrator.ts b/src/orchestrator/orchestrator.ts index 12bcf2e..6e1054f 100644 --- a/src/orchestrator/orchestrator.ts +++ b/src/orchestrator/orchestrator.ts @@ -54,6 +54,8 @@ import type { TokenUsage, } from '../types.js' import type { RunOptions } from '../agent/runner.js' +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' import { Agent } from '../agent/agent.js' import { AgentPool } from '../agent/pool.js' import { emitTrace, generateRunId } from '../utils/trace.js' @@ -205,6 +207,519 @@ function resolveTokenBudget(primary?: number, fallback?: number): number | undef return Math.min(primary, fallback) } +interface DashboardTaskMetrics { + readonly startMs: number + readonly endMs: number + readonly durationMs: number + readonly tokenUsage: TokenUsage + readonly toolCalls: AgentRunResult['toolCalls'] +} + +interface DashboardTaskNode { + readonly id: string + readonly title: string + readonly assignee?: string + readonly status: TaskStatus + readonly dependsOn: readonly string[] + readonly metrics?: DashboardTaskMetrics +} + +function buildRunTeamDashboardHtml(_goal: string, _tasks: DashboardTaskNode[]): string { + const dataJson = JSON.stringify({ + generatedAt: new Date().toISOString(), + goal: _goal, + tasks: _tasks, + }) + + return ` + + + + + Open Multi Agent + + + + + + + + +
+
+
+ +
+
+
+ +
+
+ + + +` +} + +async function writeRunTeamDashboard(goal: string, tasks: DashboardTaskNode[]): Promise { + const directory = join(process.cwd(), 'oma-dashboards') + await mkdir(directory, { recursive: true }) + const stamp = new Date().toISOString().replaceAll(':', '-') + const path = join(directory, `runTeam-${stamp}.html`) + await writeFile(path, buildRunTeamDashboardHtml(goal, tasks), 'utf8') + return path +} + /** * Build a minimal {@link Agent} with its own fresh registry/executor. * Registers all built-in tools so coordinator/worker agents can use them. @@ -409,6 +924,7 @@ interface RunContext { readonly maxTokenBudget?: number budgetExceededTriggered: boolean budgetExceededReason?: string + readonly taskMetrics: Map } /** @@ -514,7 +1030,7 @@ async function executeQueue( ? { onTrace: config.onTrace, runId: ctx.runId ?? '', taskId: task.id, traceAgent: assignee, abortSignal: ctx.abortSignal } : ctx.abortSignal ? { abortSignal: ctx.abortSignal } : undefined - const taskStartMs = config.onTrace ? Date.now() : 0 + const taskStartMs = Date.now() let retryCount = 0 const result = await executeWithRetry( @@ -531,9 +1047,10 @@ async function executeQueue( }, ) + const taskEndMs = Date.now() + // Emit task trace if (config.onTrace) { - const taskEndMs = Date.now() emitTrace(config.onTrace, { type: 'task', runId: ctx.runId ?? '', @@ -549,6 +1066,14 @@ async function executeQueue( } ctx.agentResults.set(`${assignee}:${task.id}`, result) + + ctx.taskMetrics.set(task.id, { + startMs: taskStartMs, + endMs: taskEndMs, + durationMs: Math.max(0, taskEndMs - taskStartMs), + tokenUsage: result.tokenUsage, + toolCalls: result.toolCalls, + }) ctx.cumulativeUsage = addUsage(ctx.cumulativeUsage, result.tokenUsage) const totalTokens = ctx.cumulativeUsage.input_tokens + ctx.cumulativeUsage.output_tokens if ( @@ -706,6 +1231,26 @@ export class OpenMultiAgent { private readonly teams: Map = new Map() private completedTaskCount = 0 + private async emitRunTeamDashboard( + goal: string, + tasks: Task[], + taskMetrics: Map, + ): Promise { + const dashboardTasks: DashboardTaskNode[] = tasks.map((task) => ({ + id: task.id, + title: task.title, + assignee: task.assignee, + status: task.status, + dependsOn: task.dependsOn ?? [], + metrics: taskMetrics.get(task.id), + })) + const htmlPath = await writeRunTeamDashboard(goal, dashboardTasks) + this.config.onProgress?.({ + type: 'message', + data: { kind: 'runTeam_dashboard', path: htmlPath }, + } satisfies OrchestratorEvent) + } + /** * @param config - Optional top-level configuration. * @@ -904,7 +1449,11 @@ export class OpenMultiAgent { ? { ...(traceFields ?? {}), ...(abortFields ?? {}) } : undefined + const scStartMs = Date.now() + const result = await agent.run(goal, runOptions) + + const scEndMs = Date.now() if (result.budgetExceeded) { this.config.onProgress?.({ @@ -926,6 +1475,31 @@ export class OpenMultiAgent { const agentResults = new Map() agentResults.set(bestAgent.name, result) + + + await this.emitRunTeamDashboard( + goal, + [{ + id: 'short-circuit', + title: `Short-circuit: ${bestAgent.name}`, + description: goal, + status: result.success ? 'completed' : 'failed', + assignee: bestAgent.name, + dependsOn: [], + result: result.output, + createdAt: new Date(), + updatedAt: new Date(), + }], + new Map([ + ['short-circuit', { + startMs: scStartMs, + endMs: scEndMs, + durationMs: scEndMs - scStartMs, + tokenUsage: result.tokenUsage, + toolCalls: result.toolCalls, + }], + ]), + ) return this.buildTeamRunResult(agentResults) } @@ -981,6 +1555,7 @@ export class OpenMultiAgent { maxTokenBudget, ), }) + await this.emitRunTeamDashboard(goal, [], new Map()) return this.buildTeamRunResult(agentResults) } @@ -991,6 +1566,7 @@ export class OpenMultiAgent { const queue = new TaskQueue() const scheduler = new Scheduler('dependency-first') + const taskMetrics = new Map() if (taskSpecs && taskSpecs.length > 0) { // Map title-based dependsOn references to real task IDs so we can @@ -1030,10 +1606,12 @@ export class OpenMultiAgent { maxTokenBudget, budgetExceededTriggered: false, budgetExceededReason: undefined, + taskMetrics, } await executeQueue(queue, ctx) cumulativeUsage = ctx.cumulativeUsage + await this.emitRunTeamDashboard(goal, queue.list(), taskMetrics) // ------------------------------------------------------------------ // Step 5: Coordinator synthesises final result @@ -1142,6 +1720,7 @@ export class OpenMultiAgent { maxTokenBudget: this.config.maxTokenBudget, budgetExceededTriggered: false, budgetExceededReason: undefined, + taskMetrics: new Map(), } await executeQueue(queue, ctx)