openreport 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/openreport.ts +6 -0
- package/package.json +61 -0
- package/src/agents/api-documentation.ts +66 -0
- package/src/agents/architecture-analyst.ts +46 -0
- package/src/agents/code-quality-reviewer.ts +59 -0
- package/src/agents/dependency-analyzer.ts +51 -0
- package/src/agents/onboarding-guide.ts +59 -0
- package/src/agents/orchestrator.ts +41 -0
- package/src/agents/performance-analyzer.ts +57 -0
- package/src/agents/registry.ts +50 -0
- package/src/agents/security-auditor.ts +61 -0
- package/src/agents/test-coverage-analyst.ts +58 -0
- package/src/agents/todo-generator.ts +50 -0
- package/src/app/App.tsx +151 -0
- package/src/app/theme.ts +54 -0
- package/src/cli.ts +145 -0
- package/src/commands/init.ts +81 -0
- package/src/commands/interactive.tsx +29 -0
- package/src/commands/list.ts +53 -0
- package/src/commands/run.ts +168 -0
- package/src/commands/view.tsx +52 -0
- package/src/components/generation/AgentStatusItem.tsx +125 -0
- package/src/components/generation/AgentStatusList.tsx +70 -0
- package/src/components/generation/ProgressSummary.tsx +107 -0
- package/src/components/generation/StreamingOutput.tsx +154 -0
- package/src/components/layout/Container.tsx +24 -0
- package/src/components/layout/Footer.tsx +52 -0
- package/src/components/layout/Header.tsx +50 -0
- package/src/components/report/MarkdownRenderer.tsx +50 -0
- package/src/components/report/ReportCard.tsx +31 -0
- package/src/components/report/ScrollableView.tsx +164 -0
- package/src/config/cli-detection.ts +130 -0
- package/src/config/cli-model.ts +397 -0
- package/src/config/cli-prompt-formatter.ts +129 -0
- package/src/config/defaults.ts +79 -0
- package/src/config/loader.ts +168 -0
- package/src/config/ollama.ts +48 -0
- package/src/config/providers.ts +199 -0
- package/src/config/resolve-provider.ts +62 -0
- package/src/config/saver.ts +50 -0
- package/src/config/schema.ts +51 -0
- package/src/errors.ts +34 -0
- package/src/hooks/useReportGeneration.ts +199 -0
- package/src/hooks/useTerminalSize.ts +35 -0
- package/src/ingestion/context-selector.ts +247 -0
- package/src/ingestion/file-tree.ts +227 -0
- package/src/ingestion/token-budget.ts +52 -0
- package/src/pipeline/agent-runner.ts +360 -0
- package/src/pipeline/combiner.ts +199 -0
- package/src/pipeline/context.ts +108 -0
- package/src/pipeline/extraction.ts +153 -0
- package/src/pipeline/progress.ts +192 -0
- package/src/pipeline/runner.ts +526 -0
- package/src/report/html-renderer.ts +294 -0
- package/src/report/html-script.ts +123 -0
- package/src/report/html-styles.ts +1127 -0
- package/src/report/md-to-html.ts +153 -0
- package/src/report/open-browser.ts +22 -0
- package/src/schemas/findings.ts +48 -0
- package/src/schemas/report.ts +64 -0
- package/src/screens/ConfigScreen.tsx +271 -0
- package/src/screens/GenerationScreen.tsx +278 -0
- package/src/screens/HistoryScreen.tsx +108 -0
- package/src/screens/HomeScreen.tsx +143 -0
- package/src/screens/ViewerScreen.tsx +82 -0
- package/src/storage/metadata.ts +69 -0
- package/src/storage/report-store.ts +128 -0
- package/src/tools/get-file-tree.ts +157 -0
- package/src/tools/get-git-info.ts +123 -0
- package/src/tools/glob.ts +48 -0
- package/src/tools/grep.ts +149 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-directory.ts +57 -0
- package/src/tools/read-file.ts +52 -0
- package/src/tools/read-package-json.ts +48 -0
- package/src/tools/run-command.ts +154 -0
- package/src/tools/shared-ignore.ts +58 -0
- package/src/types/index.ts +127 -0
- package/src/types/marked-terminal.d.ts +17 -0
- package/src/utils/debug.ts +25 -0
- package/src/utils/file-utils.ts +77 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/grade-colors.ts +43 -0
- package/src/utils/project-detector.ts +296 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { safeReadFileAsync } from "../utils/file-utils.js";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import type {
|
|
4
|
+
FileTreeNode,
|
|
5
|
+
ProjectClassification,
|
|
6
|
+
} from "../types/index.js";
|
|
7
|
+
import type { BaseTools, ExtendedTools } from "../tools/index.js";
|
|
8
|
+
|
|
9
|
+
// ── Shared context for all agents ────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface SharedContext {
|
|
12
|
+
classification: ProjectClassification;
|
|
13
|
+
fileTreeString: string;
|
|
14
|
+
fileTree: FileTreeNode[];
|
|
15
|
+
fileContents: Map<string, string>; // relPath → content (pre-read)
|
|
16
|
+
contextInfo: string; // formatted project info
|
|
17
|
+
orchestratorAnalysis: string; // global project analysis from orchestrator agent
|
|
18
|
+
baseTools?: BaseTools; // pre-created base tools (shared across agents)
|
|
19
|
+
extendedTools?: ExtendedTools; // pre-created extended tools (shared across agents)
|
|
20
|
+
agentFilePaths?: Map<string, string[]>; // agentId → relevant file paths (cached)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Pre-read all files into shared context ──────────────────────────────
|
|
24
|
+
|
|
25
|
+
export async function preReadAllFiles(
|
|
26
|
+
projectRoot: string,
|
|
27
|
+
filePaths: string[],
|
|
28
|
+
maxFiles: number = 30,
|
|
29
|
+
maxCharsPerFile: number = 8000,
|
|
30
|
+
fileSizes?: Map<string, number>
|
|
31
|
+
): Promise<Map<string, string>> {
|
|
32
|
+
const contents = new Map<string, string>();
|
|
33
|
+
|
|
34
|
+
const candidates = filePaths.slice(0, maxFiles * 2);
|
|
35
|
+
|
|
36
|
+
const sorted = candidates
|
|
37
|
+
.map((p) => ({ path: p, size: fileSizes?.get(p) ?? 0 }))
|
|
38
|
+
.filter((r) => r.size > 0)
|
|
39
|
+
.sort((a, b) => a.size - b.size)
|
|
40
|
+
.slice(0, maxFiles);
|
|
41
|
+
|
|
42
|
+
// Read all selected files in parallel
|
|
43
|
+
const readResults = await Promise.all(
|
|
44
|
+
sorted.map(async ({ path: relPath }) => {
|
|
45
|
+
const content = await safeReadFileAsync(path.join(projectRoot, relPath));
|
|
46
|
+
return { path: relPath, content };
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
for (const { path: relPath, content } of readResults) {
|
|
50
|
+
if (content) {
|
|
51
|
+
if (content.length > maxCharsPerFile) {
|
|
52
|
+
const newlineIdx = content.lastIndexOf('\n', maxCharsPerFile);
|
|
53
|
+
const cutoff = newlineIdx > 0 ? newlineIdx : maxCharsPerFile;
|
|
54
|
+
contents.set(relPath, content.slice(0, cutoff) + "\n... (truncated)");
|
|
55
|
+
} else {
|
|
56
|
+
contents.set(relPath, content);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return contents;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Build formatted context info string ─────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export function buildContextInfo(
|
|
66
|
+
classification: ProjectClassification,
|
|
67
|
+
fileTreeString: string,
|
|
68
|
+
relevantFiles: string[]
|
|
69
|
+
): string {
|
|
70
|
+
return `
|
|
71
|
+
## Project Info
|
|
72
|
+
- Language: ${classification.language}
|
|
73
|
+
- Framework: ${classification.framework || "none"}
|
|
74
|
+
- Type: ${classification.projectType}
|
|
75
|
+
- Build Tool: ${classification.buildTool || "none"}
|
|
76
|
+
- Package Manager: ${classification.packageManager || "unknown"}
|
|
77
|
+
- Has Tests: ${classification.hasTests}
|
|
78
|
+
- Has CI: ${classification.hasCI}
|
|
79
|
+
|
|
80
|
+
## File Tree
|
|
81
|
+
${fileTreeString}
|
|
82
|
+
|
|
83
|
+
## Relevant Files for Analysis
|
|
84
|
+
${relevantFiles.slice(0, 50).join("\n")}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Build pre-loaded file content block for agent prompts ───────────────
|
|
89
|
+
|
|
90
|
+
export function buildFileContentBlock(
|
|
91
|
+
sharedContext: SharedContext,
|
|
92
|
+
agentRelevantPaths: string[]
|
|
93
|
+
): string {
|
|
94
|
+
const parts: string[] = [];
|
|
95
|
+
for (const relPath of agentRelevantPaths) {
|
|
96
|
+
const content = sharedContext.fileContents.get(relPath);
|
|
97
|
+
if (content) {
|
|
98
|
+
parts.push(`### ${relPath}\n\`\`\`\n${content}\n\`\`\``);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (parts.length === 0) return "";
|
|
102
|
+
|
|
103
|
+
return `## Pre-loaded Source Files (${parts.length} files)
|
|
104
|
+
> IMPORTANT: These files have already been read for you. Do NOT re-read them with tools.
|
|
105
|
+
> Only use readFile/glob/grep for files NOT listed below.
|
|
106
|
+
|
|
107
|
+
${parts.join("\n\n")}`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { SubReportSchema } from "../schemas/report.js";
|
|
2
|
+
import {
|
|
3
|
+
normalizeSeverity as normalizeSeverityStr,
|
|
4
|
+
normalizeEffort as normalizeEffortStr,
|
|
5
|
+
} from "../schemas/findings.js";
|
|
6
|
+
import { ExtractionError } from "../errors.js";
|
|
7
|
+
import { debugLog } from "../utils/debug.js";
|
|
8
|
+
import type { AgentId, SubReport } from "../types/index.js";
|
|
9
|
+
|
|
10
|
+
interface RawSection {
|
|
11
|
+
heading?: string;
|
|
12
|
+
content?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RawFinding {
|
|
17
|
+
id?: string;
|
|
18
|
+
severity?: string;
|
|
19
|
+
category?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
files?: string[];
|
|
23
|
+
recommendation?: string;
|
|
24
|
+
effort?: string;
|
|
25
|
+
codeSnippet?: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Value normalization (delegates to canonical functions in findings.ts) ──
|
|
30
|
+
|
|
31
|
+
export function normalizeSeverity(value: unknown): "critical" | "warning" | "info" | "suggestion" {
|
|
32
|
+
return normalizeSeverityStr(String(value || "info"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizeEffort(value: unknown): "low" | "medium" | "high" {
|
|
36
|
+
return normalizeEffortStr(String(value || "medium"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── JSON extraction from agent output ───────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export function extractJsonFromText(text: string): string | null {
|
|
42
|
+
// Strategy 1: Find the LAST <report>...</report> block (greedy from end)
|
|
43
|
+
const lastOpen = text.lastIndexOf("<report>");
|
|
44
|
+
const lastClose = text.lastIndexOf("</report>");
|
|
45
|
+
if (lastOpen >= 0 && lastClose > lastOpen) {
|
|
46
|
+
let json = text.substring(lastOpen + "<report>".length, lastClose).trim();
|
|
47
|
+
// Strip code block markers if agent wrapped JSON in ```json ... ```
|
|
48
|
+
json = json.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Strategy 2: Find a JSON object containing "sections" key
|
|
53
|
+
const jsonMatch = text.match(/\{[\s\S]*"sections"\s*:\s*\[[\s\S]*\][\s\S]*\}/);
|
|
54
|
+
if (jsonMatch) {
|
|
55
|
+
return jsonMatch[0];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Sub-report extraction with multi-fallback ───────────────────────────
|
|
62
|
+
|
|
63
|
+
export function extractSubReport(
|
|
64
|
+
agentId: AgentId,
|
|
65
|
+
agentName: string,
|
|
66
|
+
text: string,
|
|
67
|
+
filesAnalyzed: number,
|
|
68
|
+
usage?: { promptTokens: number; completionTokens: number }
|
|
69
|
+
): SubReport {
|
|
70
|
+
const makeMeta = () => ({
|
|
71
|
+
filesAnalyzed,
|
|
72
|
+
tokensUsed: {
|
|
73
|
+
input: usage?.promptTokens || 0,
|
|
74
|
+
output: usage?.completionTokens || 0,
|
|
75
|
+
},
|
|
76
|
+
duration: 0,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const jsonStr = extractJsonFromText(text);
|
|
80
|
+
|
|
81
|
+
if (jsonStr) {
|
|
82
|
+
// Try strict Zod parse first
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(jsonStr);
|
|
85
|
+
return SubReportSchema.parse({
|
|
86
|
+
...parsed,
|
|
87
|
+
agentId,
|
|
88
|
+
agentName,
|
|
89
|
+
metadata: makeMeta(),
|
|
90
|
+
});
|
|
91
|
+
} catch (e) {
|
|
92
|
+
debugLog("pipeline:strictParse", new ExtractionError(
|
|
93
|
+
`Strict parse failed for agent ${agentId}: ${e instanceof Error ? e.message : String(e)}`
|
|
94
|
+
));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try lenient manual parse
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(jsonStr);
|
|
100
|
+
const sections = Array.isArray(parsed.sections)
|
|
101
|
+
? parsed.sections
|
|
102
|
+
.filter((s: RawSection) => s && typeof s.heading === "string" && typeof s.content === "string")
|
|
103
|
+
.map((s: RawSection) => ({ heading: String(s.heading), content: String(s.content) }))
|
|
104
|
+
: [];
|
|
105
|
+
const findings = Array.isArray(parsed.findings)
|
|
106
|
+
? parsed.findings
|
|
107
|
+
.filter((f: RawFinding) => f && typeof f.title === "string")
|
|
108
|
+
.map((f: RawFinding) => ({
|
|
109
|
+
id: String(f.id || `${agentId}-${Math.random().toString(36).slice(2, 6)}`),
|
|
110
|
+
severity: normalizeSeverity(f.severity),
|
|
111
|
+
category: String(f.category || "general"),
|
|
112
|
+
title: String(f.title),
|
|
113
|
+
description: String(f.description || ""),
|
|
114
|
+
files: Array.isArray(f.files) ? f.files.map(String) : [],
|
|
115
|
+
recommendation: String(f.recommendation || ""),
|
|
116
|
+
effort: normalizeEffort(f.effort),
|
|
117
|
+
...(f.codeSnippet ? { codeSnippet: String(f.codeSnippet) } : {}),
|
|
118
|
+
}))
|
|
119
|
+
: [];
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
agentId,
|
|
123
|
+
agentName,
|
|
124
|
+
title: String(parsed.title || agentName),
|
|
125
|
+
summary: String(parsed.summary || "Analysis completed"),
|
|
126
|
+
sections: sections.length > 0 ? sections : [{ heading: "Analysis", content: String(parsed.summary || "Analysis completed") }],
|
|
127
|
+
findings,
|
|
128
|
+
metadata: makeMeta(),
|
|
129
|
+
};
|
|
130
|
+
} catch (e) {
|
|
131
|
+
debugLog("pipeline:lenientParse", new ExtractionError(
|
|
132
|
+
`Lenient parse failed for agent ${agentId}: ${e instanceof Error ? e.message : String(e)}`
|
|
133
|
+
));
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// No <report> block found in output, falling through to fallback
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fallback: strip the <report> JSON from content so it doesn't render as text
|
|
140
|
+
const cleanText = text
|
|
141
|
+
.replace(/<report>[\s\S]*?<\/report>/g, "")
|
|
142
|
+
.trim();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
agentId,
|
|
146
|
+
agentName,
|
|
147
|
+
title: agentName,
|
|
148
|
+
summary: "Analysis completed",
|
|
149
|
+
sections: [{ heading: "Analysis", content: cleanText || "No structured output was produced." }],
|
|
150
|
+
findings: [],
|
|
151
|
+
metadata: makeMeta(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import type {
|
|
3
|
+
AgentId,
|
|
4
|
+
AgentStatus,
|
|
5
|
+
AgentRunStatus,
|
|
6
|
+
PipelinePhase,
|
|
7
|
+
PipelineProgress,
|
|
8
|
+
} from "../types/index.js";
|
|
9
|
+
|
|
10
|
+
/** Maximum characters retained per agent stream to prevent unbounded memory growth. */
|
|
11
|
+
const MAX_STREAM_TEXT = 2000;
|
|
12
|
+
|
|
13
|
+
// ── Typed EventEmitter ─────────────────────────────────────────────────
|
|
14
|
+
// Maps each event name to its listener signature. This ensures that
|
|
15
|
+
// .on(), .emit(), and .off() calls are statically checked.
|
|
16
|
+
|
|
17
|
+
export type ProgressEventMap = {
|
|
18
|
+
phaseChange: [phase: PipelinePhase];
|
|
19
|
+
phaseDetailChange: [detail: string];
|
|
20
|
+
agentStatusChange: [status: AgentStatus];
|
|
21
|
+
streamText: [text: string, agentId: AgentId];
|
|
22
|
+
tokenUpdate: [tokens: { input: number; output: number }];
|
|
23
|
+
warning: [message: string];
|
|
24
|
+
complete: [progress: PipelineProgress];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A thin typed wrapper around Node's EventEmitter.
|
|
29
|
+
* Constrains `on`, `off`, `once`, and `emit` to the keys / signatures
|
|
30
|
+
* declared in the supplied event map `T`, where each value is a tuple
|
|
31
|
+
* of the event's argument types.
|
|
32
|
+
*/
|
|
33
|
+
interface ITypedEmitter<T extends Record<string, unknown[]>> {
|
|
34
|
+
on<K extends keyof T & string>(event: K, listener: (...args: T[K]) => void): this;
|
|
35
|
+
off<K extends keyof T & string>(event: K, listener: (...args: T[K]) => void): this;
|
|
36
|
+
once<K extends keyof T & string>(event: K, listener: (...args: T[K]) => void): this;
|
|
37
|
+
emit<K extends keyof T & string>(event: K, ...args: T[K]): boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Re-export EventEmitter cast through the typed interface so that
|
|
41
|
+
// ProgressTracker inherits the real implementation while exposing
|
|
42
|
+
// only the type-safe signatures to consumers.
|
|
43
|
+
const TypedEventEmitter = EventEmitter as {
|
|
44
|
+
new <T extends Record<string, unknown[]>>(): ITypedEmitter<T> & EventEmitter;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export class ProgressTracker extends TypedEventEmitter<ProgressEventMap> {
|
|
48
|
+
private agents: Map<AgentId, AgentStatus> = new Map();
|
|
49
|
+
private phase: PipelinePhase = "scanning";
|
|
50
|
+
private phaseDetail = "";
|
|
51
|
+
private totalTokens = { input: 0, output: 0 };
|
|
52
|
+
private previousTokens: Map<AgentId, { input: number; output: number }> = new Map();
|
|
53
|
+
private startedAt: Date = new Date();
|
|
54
|
+
private currentStreamText = "";
|
|
55
|
+
private agentStreamTexts: Map<AgentId, string> = new Map();
|
|
56
|
+
|
|
57
|
+
setPhase(phase: PipelinePhase): void {
|
|
58
|
+
this.phase = phase;
|
|
59
|
+
this.phaseDetail = "";
|
|
60
|
+
this.emit("phaseChange", phase);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setPhaseDetail(detail: string): void {
|
|
64
|
+
this.phaseDetail = detail;
|
|
65
|
+
this.emit("phaseDetailChange", detail);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registerAgent(agentId: AgentId, agentName: string): void {
|
|
69
|
+
const status: AgentStatus = {
|
|
70
|
+
agentId,
|
|
71
|
+
agentName,
|
|
72
|
+
status: "pending",
|
|
73
|
+
};
|
|
74
|
+
this.agents.set(agentId, status);
|
|
75
|
+
this.emit("agentStatusChange", status);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
updateAgentStatus(
|
|
79
|
+
agentId: AgentId,
|
|
80
|
+
status: AgentRunStatus,
|
|
81
|
+
message?: string
|
|
82
|
+
): void {
|
|
83
|
+
const agent = this.agents.get(agentId);
|
|
84
|
+
if (!agent) return;
|
|
85
|
+
|
|
86
|
+
agent.status = status;
|
|
87
|
+
if (message) agent.statusMessage = message;
|
|
88
|
+
|
|
89
|
+
if (status === "running" && !agent.startedAt) {
|
|
90
|
+
agent.startedAt = new Date();
|
|
91
|
+
}
|
|
92
|
+
if (status === "completed" || status === "failed") {
|
|
93
|
+
agent.completedAt = new Date();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.agents.set(agentId, agent);
|
|
97
|
+
this.emit("agentStatusChange", { ...agent });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setAgentError(agentId: AgentId, error: string): void {
|
|
101
|
+
const agent = this.agents.get(agentId);
|
|
102
|
+
if (!agent) return;
|
|
103
|
+
|
|
104
|
+
agent.status = "failed";
|
|
105
|
+
agent.error = error;
|
|
106
|
+
agent.completedAt = new Date();
|
|
107
|
+
this.agents.set(agentId, agent);
|
|
108
|
+
this.emit("agentStatusChange", { ...agent });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
updateAgentTokens(
|
|
112
|
+
agentId: AgentId,
|
|
113
|
+
tokens: { input: number; output: number }
|
|
114
|
+
): void {
|
|
115
|
+
const agent = this.agents.get(agentId);
|
|
116
|
+
if (!agent) return;
|
|
117
|
+
|
|
118
|
+
agent.tokensUsed = tokens;
|
|
119
|
+
this.agents.set(agentId, agent);
|
|
120
|
+
|
|
121
|
+
// O(1) delta update: subtract previous values, add new ones
|
|
122
|
+
const prev = this.previousTokens.get(agentId) || { input: 0, output: 0 };
|
|
123
|
+
this.totalTokens.input += tokens.input - prev.input;
|
|
124
|
+
this.totalTokens.output += tokens.output - prev.output;
|
|
125
|
+
this.previousTokens.set(agentId, { input: tokens.input, output: tokens.output });
|
|
126
|
+
|
|
127
|
+
this.emit("tokenUpdate", { ...this.totalTokens });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
addStreamText(delta: string, agentId: AgentId): void {
|
|
131
|
+
const existing = this.agentStreamTexts.get(agentId) || "";
|
|
132
|
+
let updated = existing + delta;
|
|
133
|
+
if (updated.length > MAX_STREAM_TEXT) {
|
|
134
|
+
updated = updated.slice(-MAX_STREAM_TEXT);
|
|
135
|
+
}
|
|
136
|
+
this.agentStreamTexts.set(agentId, updated);
|
|
137
|
+
this.currentStreamText = updated;
|
|
138
|
+
this.emit("streamText", delta, agentId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getProgress(): PipelineProgress {
|
|
142
|
+
const agents = Array.from(this.agents.values());
|
|
143
|
+
const completed = agents.filter(
|
|
144
|
+
(a) => a.status === "completed" || a.status === "failed"
|
|
145
|
+
).length;
|
|
146
|
+
const total = agents.length;
|
|
147
|
+
|
|
148
|
+
let estimatedTimeRemaining: number | undefined;
|
|
149
|
+
if (completed > 0 && completed < total) {
|
|
150
|
+
const elapsed = Date.now() - this.startedAt.getTime();
|
|
151
|
+
const avgTimePerAgent = elapsed / completed;
|
|
152
|
+
estimatedTimeRemaining = Math.round(
|
|
153
|
+
avgTimePerAgent * (total - completed) / 1000
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
phase: this.phase,
|
|
159
|
+
phaseDetail: this.phaseDetail || undefined,
|
|
160
|
+
agents,
|
|
161
|
+
totalTokens: { ...this.totalTokens },
|
|
162
|
+
startedAt: this.startedAt,
|
|
163
|
+
estimatedTimeRemaining,
|
|
164
|
+
currentStreamText: this.currentStreamText,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
getCompletionPercentage(): number {
|
|
169
|
+
const agents = Array.from(this.agents.values());
|
|
170
|
+
if (agents.length === 0) return 0;
|
|
171
|
+
|
|
172
|
+
const completed = agents.filter(
|
|
173
|
+
(a) =>
|
|
174
|
+
a.status === "completed" ||
|
|
175
|
+
a.status === "failed" ||
|
|
176
|
+
a.status === "skipped"
|
|
177
|
+
).length;
|
|
178
|
+
|
|
179
|
+
return Math.round((completed / agents.length) * 100);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
reset(): void {
|
|
183
|
+
this.agents.clear();
|
|
184
|
+
this.phase = "scanning";
|
|
185
|
+
this.phaseDetail = "";
|
|
186
|
+
this.totalTokens = { input: 0, output: 0 };
|
|
187
|
+
this.previousTokens.clear();
|
|
188
|
+
this.startedAt = new Date();
|
|
189
|
+
this.currentStreamText = "";
|
|
190
|
+
this.agentStreamTexts.clear();
|
|
191
|
+
}
|
|
192
|
+
}
|