triagent 0.1.0-alpha8 → 0.1.0-beta2

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.
Files changed (47) hide show
  1. package/README.md +101 -1
  2. package/package.json +9 -3
  3. package/src/cli/config.ts +118 -2
  4. package/src/config.ts +23 -3
  5. package/src/index.ts +262 -6
  6. package/src/integrations/elasticsearch/client.ts +210 -0
  7. package/src/integrations/grafana/client.ts +186 -0
  8. package/src/integrations/kubernetes/multi-cluster.ts +199 -0
  9. package/src/integrations/kubernetes/types.ts +24 -0
  10. package/src/integrations/loki/client.ts +219 -0
  11. package/src/integrations/prometheus/client.ts +163 -0
  12. package/src/integrations/slack/client.ts +265 -0
  13. package/src/integrations/teams/client.ts +199 -0
  14. package/src/mastra/agents/debugger.ts +164 -109
  15. package/src/mastra/index.ts +2 -2
  16. package/src/mastra/tools/approval-store.ts +180 -0
  17. package/src/mastra/tools/cli.ts +94 -2
  18. package/src/mastra/tools/cost.ts +389 -0
  19. package/src/mastra/tools/logs.ts +210 -0
  20. package/src/mastra/tools/network.ts +253 -0
  21. package/src/mastra/tools/prometheus.ts +221 -0
  22. package/src/mastra/tools/remediation.ts +365 -0
  23. package/src/mastra/tools/runbook.ts +186 -0
  24. package/src/sandbox/bashlet.ts +76 -10
  25. package/src/server/routes/history.ts +207 -0
  26. package/src/server/routes/notifications.ts +236 -0
  27. package/src/server/webhook.ts +36 -2
  28. package/src/storage/index.ts +3 -0
  29. package/src/storage/investigation-history.ts +277 -0
  30. package/src/storage/runbook-index.ts +330 -0
  31. package/src/storage/types.ts +72 -0
  32. package/src/tui/app.tsx +278 -198
  33. package/src/tui/components/approval-dialog.tsx +147 -0
  34. package/src/tui/components/approval-modal.tsx +278 -0
  35. package/src/tui/components/centered-layout.tsx +33 -0
  36. package/src/tui/components/editor.tsx +87 -0
  37. package/src/tui/components/header.tsx +53 -0
  38. package/src/tui/components/index.ts +55 -0
  39. package/src/tui/components/message-item.tsx +131 -0
  40. package/src/tui/components/messages-panel.tsx +71 -0
  41. package/src/tui/components/status-badge.tsx +20 -0
  42. package/src/tui/components/status-bar.tsx +39 -0
  43. package/src/tui/components/styled-span.tsx +24 -0
  44. package/src/tui/components/timeline.tsx +223 -0
  45. package/src/tui/components/toast.tsx +104 -0
  46. package/src/tui/theme/index.ts +21 -0
  47. package/src/tui/theme/tokens.ts +180 -0
@@ -0,0 +1,277 @@
1
+ import { mkdir, readFile, writeFile, readdir, unlink, stat } from "fs/promises";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import type {
5
+ InvestigationHistory,
6
+ HistoryQueryOptions,
7
+ HistoryStats,
8
+ InvestigationEvent,
9
+ ToolCallRecord,
10
+ } from "./types.js";
11
+
12
+ const HISTORY_DIR = join(homedir(), ".config", "triagent", "history");
13
+
14
+ export class InvestigationHistoryStore {
15
+ private historyDir: string;
16
+ private retentionDays: number;
17
+
18
+ constructor(retentionDays: number = 30) {
19
+ this.historyDir = HISTORY_DIR;
20
+ this.retentionDays = retentionDays;
21
+ }
22
+
23
+ async init(): Promise<void> {
24
+ await mkdir(this.historyDir, { recursive: true });
25
+ await this.cleanupOldRecords();
26
+ }
27
+
28
+ private getFilePath(id: string): string {
29
+ return join(this.historyDir, `${id}.json`);
30
+ }
31
+
32
+ async save(investigation: InvestigationHistory): Promise<void> {
33
+ await mkdir(this.historyDir, { recursive: true });
34
+ const filePath = this.getFilePath(investigation.id);
35
+ const serialized = JSON.stringify(investigation, (key, value) => {
36
+ if (value instanceof Date) {
37
+ return { __type: "Date", value: value.toISOString() };
38
+ }
39
+ return value;
40
+ }, 2);
41
+ await writeFile(filePath, serialized, "utf-8");
42
+ }
43
+
44
+ async get(id: string): Promise<InvestigationHistory | null> {
45
+ try {
46
+ const filePath = this.getFilePath(id);
47
+ const content = await readFile(filePath, "utf-8");
48
+ return this.deserialize(content);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ async delete(id: string): Promise<boolean> {
55
+ try {
56
+ const filePath = this.getFilePath(id);
57
+ await unlink(filePath);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ async list(options: HistoryQueryOptions = {}): Promise<InvestigationHistory[]> {
65
+ const {
66
+ limit = 50,
67
+ offset = 0,
68
+ status,
69
+ cluster,
70
+ tags,
71
+ startDate,
72
+ endDate,
73
+ searchQuery,
74
+ } = options;
75
+
76
+ try {
77
+ const files = await readdir(this.historyDir);
78
+ const investigations: InvestigationHistory[] = [];
79
+
80
+ for (const file of files) {
81
+ if (!file.endsWith(".json")) continue;
82
+
83
+ const content = await readFile(join(this.historyDir, file), "utf-8");
84
+ const investigation = this.deserialize(content);
85
+
86
+ // Apply filters
87
+ if (status && investigation.status !== status) continue;
88
+ if (cluster && investigation.cluster !== cluster) continue;
89
+ if (tags && tags.length > 0) {
90
+ const hasAllTags = tags.every((tag) =>
91
+ investigation.tags?.includes(tag)
92
+ );
93
+ if (!hasAllTags) continue;
94
+ }
95
+ if (startDate && investigation.startedAt < startDate) continue;
96
+ if (endDate && investigation.startedAt > endDate) continue;
97
+ if (searchQuery) {
98
+ const searchLower = searchQuery.toLowerCase();
99
+ const matchesTitle = investigation.incident.title
100
+ .toLowerCase()
101
+ .includes(searchLower);
102
+ const matchesDescription = investigation.incident.description
103
+ .toLowerCase()
104
+ .includes(searchLower);
105
+ if (!matchesTitle && !matchesDescription) continue;
106
+ }
107
+
108
+ investigations.push(investigation);
109
+ }
110
+
111
+ // Sort by startedAt descending
112
+ investigations.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
113
+
114
+ // Apply pagination
115
+ return investigations.slice(offset, offset + limit);
116
+ } catch {
117
+ return [];
118
+ }
119
+ }
120
+
121
+ async getStats(): Promise<HistoryStats> {
122
+ const investigations = await this.list({ limit: 10000 });
123
+ const now = new Date();
124
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
125
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
126
+
127
+ const stats: HistoryStats = {
128
+ total: investigations.length,
129
+ byStatus: {
130
+ pending: 0,
131
+ running: 0,
132
+ completed: 0,
133
+ failed: 0,
134
+ },
135
+ byCluster: {},
136
+ averageDuration: 0,
137
+ last24Hours: 0,
138
+ last7Days: 0,
139
+ };
140
+
141
+ let totalDuration = 0;
142
+ let completedCount = 0;
143
+
144
+ for (const inv of investigations) {
145
+ stats.byStatus[inv.status]++;
146
+
147
+ if (inv.cluster) {
148
+ stats.byCluster[inv.cluster] = (stats.byCluster[inv.cluster] || 0) + 1;
149
+ }
150
+
151
+ if (inv.startedAt >= oneDayAgo) {
152
+ stats.last24Hours++;
153
+ }
154
+ if (inv.startedAt >= sevenDaysAgo) {
155
+ stats.last7Days++;
156
+ }
157
+
158
+ if (inv.completedAt && inv.startedAt) {
159
+ totalDuration += inv.completedAt.getTime() - inv.startedAt.getTime();
160
+ completedCount++;
161
+ }
162
+ }
163
+
164
+ if (completedCount > 0) {
165
+ stats.averageDuration = totalDuration / completedCount;
166
+ }
167
+
168
+ return stats;
169
+ }
170
+
171
+ async addEvent(
172
+ investigationId: string,
173
+ event: Omit<InvestigationEvent, "id" | "timestamp">
174
+ ): Promise<void> {
175
+ const investigation = await this.get(investigationId);
176
+ if (!investigation) return;
177
+
178
+ const newEvent: InvestigationEvent = {
179
+ ...event,
180
+ id: crypto.randomUUID(),
181
+ timestamp: new Date(),
182
+ };
183
+
184
+ investigation.events.push(newEvent);
185
+ await this.save(investigation);
186
+ }
187
+
188
+ async addToolCall(
189
+ investigationId: string,
190
+ toolCall: Omit<ToolCallRecord, "id" | "timestamp">
191
+ ): Promise<void> {
192
+ const investigation = await this.get(investigationId);
193
+ if (!investigation) return;
194
+
195
+ const newToolCall: ToolCallRecord = {
196
+ ...toolCall,
197
+ id: crypto.randomUUID(),
198
+ timestamp: new Date(),
199
+ };
200
+
201
+ investigation.toolCalls.push(newToolCall);
202
+ await this.save(investigation);
203
+ }
204
+
205
+ async updateStatus(
206
+ investigationId: string,
207
+ status: InvestigationHistory["status"],
208
+ result?: InvestigationHistory["result"],
209
+ rawResult?: string,
210
+ error?: string
211
+ ): Promise<void> {
212
+ const investigation = await this.get(investigationId);
213
+ if (!investigation) return;
214
+
215
+ investigation.status = status;
216
+ if (status === "completed" || status === "failed") {
217
+ investigation.completedAt = new Date();
218
+ }
219
+ if (result) {
220
+ investigation.result = result;
221
+ }
222
+ if (rawResult) {
223
+ investigation.rawResult = rawResult;
224
+ }
225
+ if (error) {
226
+ investigation.error = error;
227
+ }
228
+
229
+ await this.save(investigation);
230
+ }
231
+
232
+ private async cleanupOldRecords(): Promise<void> {
233
+ try {
234
+ const files = await readdir(this.historyDir);
235
+ const cutoffDate = new Date();
236
+ cutoffDate.setDate(cutoffDate.getDate() - this.retentionDays);
237
+
238
+ for (const file of files) {
239
+ if (!file.endsWith(".json")) continue;
240
+
241
+ const filePath = join(this.historyDir, file);
242
+ const fileStat = await stat(filePath);
243
+
244
+ if (fileStat.mtime < cutoffDate) {
245
+ await unlink(filePath);
246
+ }
247
+ }
248
+ } catch {
249
+ // Ignore cleanup errors
250
+ }
251
+ }
252
+
253
+ private deserialize(content: string): InvestigationHistory {
254
+ return JSON.parse(content, (key, value) => {
255
+ if (value && typeof value === "object" && value.__type === "Date") {
256
+ return new Date(value.value);
257
+ }
258
+ return value;
259
+ });
260
+ }
261
+ }
262
+
263
+ // Singleton instance
264
+ let historyStore: InvestigationHistoryStore | null = null;
265
+
266
+ export function getHistoryStore(retentionDays?: number): InvestigationHistoryStore {
267
+ if (!historyStore) {
268
+ historyStore = new InvestigationHistoryStore(retentionDays);
269
+ }
270
+ return historyStore;
271
+ }
272
+
273
+ export async function initHistoryStore(retentionDays?: number): Promise<InvestigationHistoryStore> {
274
+ const store = getHistoryStore(retentionDays);
275
+ await store.init();
276
+ return store;
277
+ }
@@ -0,0 +1,330 @@
1
+ import { readFile, writeFile, readdir, stat } from "fs/promises";
2
+ import { homedir } from "os";
3
+ import { join, extname, basename } from "path";
4
+ import type { RunbookEntry, RunbookIndex } from "./types.js";
5
+
6
+ const INDEX_FILE = join(homedir(), ".config", "triagent", "runbook-index.json");
7
+
8
+ export class RunbookIndexer {
9
+ private index: RunbookIndex;
10
+ private paths: string[];
11
+
12
+ constructor(paths: string[] = []) {
13
+ this.paths = paths;
14
+ this.index = {
15
+ entries: [],
16
+ vocabulary: [],
17
+ idfValues: {},
18
+ lastIndexed: new Date(),
19
+ };
20
+ }
21
+
22
+ async load(): Promise<void> {
23
+ try {
24
+ const content = await readFile(INDEX_FILE, "utf-8");
25
+ this.index = JSON.parse(content, (key, value) => {
26
+ if (key === "lastIndexed" || key === "lastModified") {
27
+ return new Date(value);
28
+ }
29
+ return value;
30
+ });
31
+ } catch {
32
+ // Index doesn't exist yet
33
+ }
34
+ }
35
+
36
+ async save(): Promise<void> {
37
+ await writeFile(INDEX_FILE, JSON.stringify(this.index, null, 2), "utf-8");
38
+ }
39
+
40
+ async indexPaths(paths: string[]): Promise<number> {
41
+ this.paths = paths;
42
+ const entries: RunbookEntry[] = [];
43
+
44
+ for (const path of paths) {
45
+ const files = await this.findMarkdownFiles(path);
46
+ for (const file of files) {
47
+ const entry = await this.parseRunbook(file);
48
+ if (entry) {
49
+ entries.push(entry);
50
+ }
51
+ }
52
+ }
53
+
54
+ // Build vocabulary and IDF values
55
+ this.buildVocabulary(entries);
56
+
57
+ // Compute TF-IDF vectors for each entry
58
+ for (const entry of entries) {
59
+ entry.tfidfVector = this.computeTfIdf(entry.content);
60
+ }
61
+
62
+ this.index = {
63
+ entries,
64
+ vocabulary: this.index.vocabulary,
65
+ idfValues: this.index.idfValues,
66
+ lastIndexed: new Date(),
67
+ };
68
+
69
+ await this.save();
70
+ return entries.length;
71
+ }
72
+
73
+ private async findMarkdownFiles(dirPath: string): Promise<string[]> {
74
+ const files: string[] = [];
75
+
76
+ try {
77
+ const entries = await readdir(dirPath, { withFileTypes: true });
78
+
79
+ for (const entry of entries) {
80
+ const fullPath = join(dirPath, entry.name);
81
+
82
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
83
+ const subFiles = await this.findMarkdownFiles(fullPath);
84
+ files.push(...subFiles);
85
+ } else if (entry.isFile() && [".md", ".markdown"].includes(extname(entry.name).toLowerCase())) {
86
+ files.push(fullPath);
87
+ }
88
+ }
89
+ } catch {
90
+ // Directory not accessible
91
+ }
92
+
93
+ return files;
94
+ }
95
+
96
+ private async parseRunbook(filePath: string): Promise<RunbookEntry | null> {
97
+ try {
98
+ const content = await readFile(filePath, "utf-8");
99
+ const fileStat = await stat(filePath);
100
+
101
+ // Extract title from first heading or filename
102
+ const titleMatch = content.match(/^#\s+(.+)$/m);
103
+ const title = titleMatch ? titleMatch[1] : basename(filePath, extname(filePath));
104
+
105
+ // Extract tags from frontmatter or content
106
+ const tags = this.extractTags(content);
107
+
108
+ // Extract symptoms (lines that might describe problems)
109
+ const symptoms = this.extractSymptoms(content);
110
+
111
+ return {
112
+ id: Buffer.from(filePath).toString("base64"),
113
+ path: filePath,
114
+ title,
115
+ content,
116
+ tags,
117
+ symptoms,
118
+ lastModified: fileStat.mtime,
119
+ };
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ private extractTags(content: string): string[] {
126
+ const tags: string[] = [];
127
+
128
+ // Check for YAML frontmatter tags
129
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
130
+ if (frontmatterMatch) {
131
+ const tagsMatch = frontmatterMatch[1].match(/tags:\s*\[([^\]]+)\]/);
132
+ if (tagsMatch) {
133
+ tags.push(...tagsMatch[1].split(",").map((t) => t.trim().replace(/['"]/g, "")));
134
+ }
135
+ }
136
+
137
+ // Extract keywords from headings
138
+ const headings = content.match(/^#+\s+(.+)$/gm) || [];
139
+ for (const heading of headings) {
140
+ const text = heading.replace(/^#+\s+/, "").toLowerCase();
141
+ if (text.includes("error") || text.includes("issue") || text.includes("problem")) {
142
+ tags.push("troubleshooting");
143
+ }
144
+ if (text.includes("deploy") || text.includes("rollback")) {
145
+ tags.push("deployment");
146
+ }
147
+ if (text.includes("scale") || text.includes("performance")) {
148
+ tags.push("scaling");
149
+ }
150
+ }
151
+
152
+ return [...new Set(tags)];
153
+ }
154
+
155
+ private extractSymptoms(content: string): string[] {
156
+ const symptoms: string[] = [];
157
+
158
+ // Look for sections that describe symptoms
159
+ const lines = content.split("\n");
160
+ let inSymptomsSection = false;
161
+
162
+ for (const line of lines) {
163
+ const lowerLine = line.toLowerCase();
164
+
165
+ // Check if entering a symptoms section
166
+ if (lowerLine.includes("symptom") || lowerLine.includes("sign") || lowerLine.includes("indicator")) {
167
+ inSymptomsSection = true;
168
+ continue;
169
+ }
170
+
171
+ // Check if leaving symptoms section
172
+ if (inSymptomsSection && line.match(/^#+\s/)) {
173
+ inSymptomsSection = false;
174
+ }
175
+
176
+ // Collect bullet points in symptoms section
177
+ if (inSymptomsSection && line.match(/^[-*]\s/)) {
178
+ symptoms.push(line.replace(/^[-*]\s+/, "").trim());
179
+ }
180
+
181
+ // Also look for error patterns
182
+ if (lowerLine.includes("error:") || lowerLine.includes("exception:") || lowerLine.includes("failed:")) {
183
+ symptoms.push(line.trim());
184
+ }
185
+ }
186
+
187
+ return symptoms.slice(0, 10); // Limit to 10 symptoms
188
+ }
189
+
190
+ private buildVocabulary(entries: RunbookEntry[]): void {
191
+ const docFrequency = new Map<string, number>();
192
+ const vocabulary = new Set<string>();
193
+
194
+ for (const entry of entries) {
195
+ const words = this.tokenize(entry.content);
196
+ const uniqueWords = new Set(words);
197
+
198
+ for (const word of uniqueWords) {
199
+ vocabulary.add(word);
200
+ docFrequency.set(word, (docFrequency.get(word) || 0) + 1);
201
+ }
202
+ }
203
+
204
+ this.index.vocabulary = Array.from(vocabulary);
205
+
206
+ // Compute IDF values
207
+ const numDocs = entries.length;
208
+ for (const [word, df] of docFrequency) {
209
+ this.index.idfValues[word] = Math.log((numDocs + 1) / (df + 1)) + 1;
210
+ }
211
+ }
212
+
213
+ private computeTfIdf(content: string): Record<string, number> {
214
+ const words = this.tokenize(content);
215
+ const tf = new Map<string, number>();
216
+
217
+ for (const word of words) {
218
+ tf.set(word, (tf.get(word) || 0) + 1);
219
+ }
220
+
221
+ const vector: Record<string, number> = {};
222
+ const maxTf = Math.max(...tf.values());
223
+
224
+ for (const [word, count] of tf) {
225
+ const normalizedTf = count / maxTf;
226
+ const idf = this.index.idfValues[word] || 1;
227
+ vector[word] = normalizedTf * idf;
228
+ }
229
+
230
+ return vector;
231
+ }
232
+
233
+ private tokenize(text: string): string[] {
234
+ return text
235
+ .toLowerCase()
236
+ .replace(/[^a-z0-9\s-]/g, " ")
237
+ .split(/\s+/)
238
+ .filter((word) => word.length > 2 && !this.isStopWord(word));
239
+ }
240
+
241
+ private isStopWord(word: string): boolean {
242
+ const stopWords = new Set([
243
+ "the", "is", "at", "which", "on", "a", "an", "and", "or", "but",
244
+ "in", "with", "to", "for", "of", "as", "by", "that", "this",
245
+ "it", "from", "be", "are", "was", "were", "been", "being",
246
+ "have", "has", "had", "do", "does", "did", "will", "would",
247
+ "could", "should", "may", "might", "must", "can", "not", "no",
248
+ "all", "any", "both", "each", "few", "more", "most", "other",
249
+ "some", "such", "only", "own", "same", "than", "too", "very",
250
+ ]);
251
+ return stopWords.has(word);
252
+ }
253
+
254
+ search(query: string, limit: number = 5): RunbookEntry[] {
255
+ const queryVector = this.computeTfIdf(query);
256
+ const scores: Array<{ entry: RunbookEntry; score: number }> = [];
257
+
258
+ for (const entry of this.index.entries) {
259
+ if (!entry.tfidfVector) continue;
260
+
261
+ const score = this.cosineSimilarity(queryVector, entry.tfidfVector);
262
+ if (score > 0) {
263
+ scores.push({ entry, score });
264
+ }
265
+ }
266
+
267
+ // Sort by score descending
268
+ scores.sort((a, b) => b.score - a.score);
269
+
270
+ return scores.slice(0, limit).map((s) => s.entry);
271
+ }
272
+
273
+ private cosineSimilarity(a: Record<string, number>, b: Record<string, number>): number {
274
+ let dotProduct = 0;
275
+ let normA = 0;
276
+ let normB = 0;
277
+
278
+ for (const key of Object.keys(a)) {
279
+ const aVal = a[key] || 0;
280
+ const bVal = b[key] || 0;
281
+ dotProduct += aVal * bVal;
282
+ normA += aVal * aVal;
283
+ }
284
+
285
+ for (const key of Object.keys(b)) {
286
+ const bVal = b[key] || 0;
287
+ normB += bVal * bVal;
288
+ }
289
+
290
+ if (normA === 0 || normB === 0) return 0;
291
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
292
+ }
293
+
294
+ searchBySymptoms(symptoms: string[], limit: number = 5): RunbookEntry[] {
295
+ const query = symptoms.join(" ");
296
+ return this.search(query, limit);
297
+ }
298
+
299
+ getByTags(tags: string[]): RunbookEntry[] {
300
+ return this.index.entries.filter((entry) =>
301
+ tags.some((tag) => entry.tags.includes(tag.toLowerCase()))
302
+ );
303
+ }
304
+
305
+ getStats(): { totalRunbooks: number; lastIndexed: Date } {
306
+ return {
307
+ totalRunbooks: this.index.entries.length,
308
+ lastIndexed: this.index.lastIndexed,
309
+ };
310
+ }
311
+ }
312
+
313
+ // Singleton instance
314
+ let runbookIndexer: RunbookIndexer | null = null;
315
+
316
+ export function getRunbookIndexer(): RunbookIndexer {
317
+ if (!runbookIndexer) {
318
+ runbookIndexer = new RunbookIndexer();
319
+ }
320
+ return runbookIndexer;
321
+ }
322
+
323
+ export async function initRunbookIndexer(paths?: string[]): Promise<RunbookIndexer> {
324
+ const indexer = getRunbookIndexer();
325
+ await indexer.load();
326
+ if (paths && paths.length > 0) {
327
+ await indexer.indexPaths(paths);
328
+ }
329
+ return indexer;
330
+ }
@@ -0,0 +1,72 @@
1
+ import type { IncidentInput, InvestigationResult } from "../mastra/agents/debugger.js";
2
+
3
+ export interface InvestigationEvent {
4
+ id: string;
5
+ timestamp: Date;
6
+ type: "tool_call" | "alert" | "k8s_event" | "log_entry" | "user_action";
7
+ source: string;
8
+ data: Record<string, unknown>;
9
+ }
10
+
11
+ export interface InvestigationHistory {
12
+ id: string;
13
+ incident: IncidentInput;
14
+ status: "pending" | "running" | "completed" | "failed";
15
+ startedAt: Date;
16
+ completedAt?: Date;
17
+ result?: InvestigationResult;
18
+ rawResult?: string;
19
+ error?: string;
20
+ cluster?: string;
21
+ events: InvestigationEvent[];
22
+ toolCalls: ToolCallRecord[];
23
+ tags?: string[];
24
+ }
25
+
26
+ export interface ToolCallRecord {
27
+ id: string;
28
+ timestamp: Date;
29
+ toolName: string;
30
+ args: Record<string, unknown>;
31
+ result?: string;
32
+ duration?: number;
33
+ error?: string;
34
+ }
35
+
36
+ export interface HistoryQueryOptions {
37
+ limit?: number;
38
+ offset?: number;
39
+ status?: InvestigationHistory["status"];
40
+ cluster?: string;
41
+ tags?: string[];
42
+ startDate?: Date;
43
+ endDate?: Date;
44
+ searchQuery?: string;
45
+ }
46
+
47
+ export interface HistoryStats {
48
+ total: number;
49
+ byStatus: Record<InvestigationHistory["status"], number>;
50
+ byCluster: Record<string, number>;
51
+ averageDuration: number;
52
+ last24Hours: number;
53
+ last7Days: number;
54
+ }
55
+
56
+ export interface RunbookEntry {
57
+ id: string;
58
+ path: string;
59
+ title: string;
60
+ content: string;
61
+ tags: string[];
62
+ symptoms: string[];
63
+ lastModified: Date;
64
+ tfidfVector?: Record<string, number>;
65
+ }
66
+
67
+ export interface RunbookIndex {
68
+ entries: RunbookEntry[];
69
+ vocabulary: string[];
70
+ idfValues: Record<string, number>;
71
+ lastIndexed: Date;
72
+ }