triagent 0.1.0-alpha9 → 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 -197
  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
package/src/tui/app.tsx CHANGED
@@ -1,19 +1,21 @@
1
1
  /* @jsxImportSource @opentui/solid */
2
2
  import { render } from "@opentui/solid";
3
- import { createSignal, For, Show, onMount } from "solid-js";
4
- import { createTextAttributes } from "@opentui/core";
5
- import "opentui-spinner/solid";
3
+ import { createSignal, onMount, type JSX } from "solid-js";
4
+ import { exec } from "child_process";
5
+ import { promisify } from "util";
6
6
  import { getDebuggerAgent, buildIncidentPrompt } from "../mastra/index.js";
7
7
  import type { IncidentInput } from "../mastra/agents/debugger.js";
8
-
9
- interface Message {
10
- id: string;
11
- role: "user" | "assistant" | "tool";
12
- content: string;
13
- timestamp: Date;
14
- toolName?: string;
15
- command?: string;
16
- }
8
+ import { approvalStore } from "../mastra/tools/approval-store.js";
9
+ import { ToastProvider, toastSuccess, toastError, toastWarning, toastInfo } from "./components/toast.js";
10
+ import { ApprovalDialogProvider, useApprovalDialog } from "./components/approval-dialog.js";
11
+ import { Header } from "./components/header.js";
12
+ import { MessagesPanel, type Message } from "./components/messages-panel.js";
13
+ import { Editor } from "./components/editor.js";
14
+ import { StatusBar } from "./components/status-bar.js";
15
+ import { type AppStatus, type RiskLevel } from "./theme/index.js";
16
+ import { loadConfig } from "../config.js";
17
+
18
+ const execAsync = promisify(exec);
17
19
 
18
20
  // Conversation history for multi-turn debugging
19
21
  interface ConversationMessage {
@@ -33,11 +35,6 @@ function formatHistoryAsPrompt(history: ConversationMessage[], newMessage: strin
33
35
  return `Previous conversation:\n${historyText}\n\nUser: ${newMessage}`;
34
36
  }
35
37
 
36
- type AppStatus = "idle" | "investigating" | "complete" | "error";
37
-
38
- const ATTR_DIM = createTextAttributes({ dim: true });
39
- const ATTR_BOLD = createTextAttributes({ bold: true });
40
-
41
38
  function buildDisplayCommand(toolName: string, args: unknown): string | undefined {
42
39
  if (!args || typeof args !== "object") return undefined;
43
40
 
@@ -45,11 +42,9 @@ function buildDisplayCommand(toolName: string, args: unknown): string | undefine
45
42
 
46
43
  switch (toolName) {
47
44
  case "cli":
48
- // CLI tool has direct command
49
45
  return "command" in a ? String(a.command) : undefined;
50
46
 
51
47
  case "git": {
52
- // Build git command: git <command> [args...] [path]
53
48
  if (!("command" in a)) return undefined;
54
49
  const parts = ["git", String(a.command)];
55
50
  if ("args" in a && Array.isArray(a.args)) {
@@ -62,7 +57,6 @@ function buildDisplayCommand(toolName: string, args: unknown): string | undefine
62
57
  }
63
58
 
64
59
  case "filesystem": {
65
- // Build filesystem display: <operation> <path> [pattern]
66
60
  if (!("operation" in a)) return undefined;
67
61
  const op = String(a.operation);
68
62
  const path = "path" in a ? String(a.path) : "";
@@ -79,18 +73,51 @@ function buildDisplayCommand(toolName: string, args: unknown): string | undefine
79
73
  }
80
74
 
81
75
  default:
82
- // Fallback: try to use command if it exists
83
76
  return "command" in a ? String(a.command) : undefined;
84
77
  }
85
78
  }
86
79
 
87
- function App() {
80
+ // Pending approval state for HITL
81
+ interface PendingApprovalState {
82
+ approvalId: string;
83
+ command: string;
84
+ riskLevel: RiskLevel;
85
+ selectedOption: number;
86
+ }
87
+
88
+ function AppContent() {
88
89
  const [messages, setMessages] = createSignal<Message[]>([]);
89
90
  const [conversationHistory, setConversationHistory] = createSignal<ConversationMessage[]>([]);
90
91
  const [status, setStatus] = createSignal<AppStatus>("idle");
91
92
  const [currentTool, setCurrentTool] = createSignal<string | null>(null);
92
93
  const [inputValue, setInputValue] = createSignal("");
93
- const [error, setError] = createSignal<string | null>(null);
94
+ const [kubeContext, setKubeContext] = createSignal<string>("loading...");
95
+ const [modelName, setModelName] = createSignal<string>("loading...");
96
+
97
+ // HITL approval state
98
+ const [pendingApproval, setPendingApproval] = createSignal<PendingApprovalState | null>(null);
99
+
100
+ // Approval dialog hook
101
+ const { showApproval } = useApprovalDialog();
102
+
103
+ // Fetch kubectl context and config on mount
104
+ onMount(async () => {
105
+ // Load kubectl context
106
+ try {
107
+ const { stdout } = await execAsync("kubectl config current-context");
108
+ setKubeContext(stdout.trim());
109
+ } catch {
110
+ setKubeContext("not connected");
111
+ }
112
+
113
+ // Load model name from config
114
+ try {
115
+ const config = await loadConfig();
116
+ setModelName(config.aiModel);
117
+ } catch {
118
+ setModelName("unknown");
119
+ }
120
+ });
94
121
 
95
122
  const addMessage = (msg: Omit<Message, "id" | "timestamp">) => {
96
123
  setMessages((prev) => [
@@ -103,27 +130,160 @@ function App() {
103
130
  ]);
104
131
  };
105
132
 
133
+ const handleApprovalDecision = async (approved: boolean) => {
134
+ const approval = pendingApproval();
135
+ if (!approval) return;
136
+
137
+ if (approved) {
138
+ const token = approvalStore.approve(approval.approvalId);
139
+ if (!token) {
140
+ addMessage({
141
+ role: "assistant",
142
+ content: "Approval expired. Please try the operation again.",
143
+ });
144
+ setPendingApproval(null);
145
+ setStatus("complete");
146
+ toastWarning("Approval expired", "Please retry the operation");
147
+ return;
148
+ }
149
+
150
+ addMessage({
151
+ role: "user",
152
+ content: `Approved: ${approval.command}`,
153
+ });
154
+ toastSuccess("Command approved");
155
+
156
+ setPendingApproval(null);
157
+ const approvalMessage = `User approved the command. The approval token is: ${token}. Please execute the command: ${approval.command} with approvalToken: "${token}"`;
158
+
159
+ setConversationHistory((prev) => [
160
+ ...prev,
161
+ { role: "user", content: approvalMessage },
162
+ ]);
163
+
164
+ setStatus("investigating");
165
+ continueWithApproval(approvalMessage);
166
+ } else {
167
+ approvalStore.reject(approval.approvalId);
168
+ addMessage({
169
+ role: "user",
170
+ content: `Rejected: ${approval.command}`,
171
+ });
172
+ addMessage({
173
+ role: "assistant",
174
+ content: "Command rejected by user. How would you like to proceed?",
175
+ });
176
+ setPendingApproval(null);
177
+ setStatus("complete");
178
+ toastWarning("Command rejected");
179
+ }
180
+ };
181
+
182
+ // Handle approval using dialog overlay
183
+ const handleApprovalRequest = async (approvalData: PendingApprovalState) => {
184
+ setPendingApproval(approvalData);
185
+ setStatus("awaiting_approval");
186
+
187
+ const approved = await showApproval({
188
+ command: approvalData.command,
189
+ riskLevel: approvalData.riskLevel,
190
+ description: `This ${approvalData.riskLevel}-risk operation requires your approval.`,
191
+ });
192
+
193
+ await handleApprovalDecision(approved);
194
+ };
195
+
196
+ const continueWithApproval = async (message: string) => {
197
+ try {
198
+ const agent = getDebuggerAgent();
199
+ let assistantContent = "";
200
+
201
+ const prompt = formatHistoryAsPrompt(conversationHistory(), message);
202
+
203
+ const stream = await agent.stream(prompt, {
204
+ maxSteps: 20,
205
+ onStepFinish: ({ toolCalls, toolResults }) => {
206
+ if (toolCalls && toolCalls.length > 0) {
207
+ const toolCallChunk = toolCalls[0] as { payload?: { toolName?: string; args?: unknown } };
208
+ const toolName = toolCallChunk?.payload?.toolName ?? "tool";
209
+ const args = toolCallChunk?.payload?.args ?? {};
210
+
211
+ const command = buildDisplayCommand(toolName, args);
212
+
213
+ addMessage({
214
+ role: "tool",
215
+ content: command ? `$ ${command}` : `Executing ${toolName}...`,
216
+ toolName,
217
+ command,
218
+ });
219
+ // Note: Tool is already complete when onStepFinish fires
220
+ }
221
+
222
+ if (toolResults && toolResults.length > 0) {
223
+ for (const toolResult of toolResults) {
224
+ const tr = toolResult as any;
225
+ const data = tr?.payload?.result ?? tr?.result ?? tr;
226
+
227
+ if (data?.requiresApproval && data?.approvalId) {
228
+ handleApprovalRequest({
229
+ approvalId: data.approvalId,
230
+ command: data.command || "unknown command",
231
+ riskLevel: (data.riskLevel as RiskLevel) || "medium",
232
+ selectedOption: 0,
233
+ });
234
+ }
235
+ }
236
+ }
237
+ },
238
+ });
239
+
240
+ for await (const chunk of stream.textStream) {
241
+ assistantContent += chunk;
242
+ }
243
+
244
+ if (status() !== "awaiting_approval") {
245
+ addMessage({
246
+ role: "assistant",
247
+ content: assistantContent,
248
+ });
249
+
250
+ setConversationHistory((prev) => [
251
+ ...prev,
252
+ { role: "assistant", content: assistantContent },
253
+ ]);
254
+
255
+ setStatus("complete");
256
+ toastSuccess("Command executed");
257
+ }
258
+ setCurrentTool(null);
259
+ } catch (err) {
260
+ const errorMsg = err instanceof Error ? err.message : String(err);
261
+ setStatus("error");
262
+ toastError("Execution failed", errorMsg);
263
+ addMessage({
264
+ role: "assistant",
265
+ content: `Error: ${errorMsg}`,
266
+ });
267
+ }
268
+ };
269
+
106
270
  const investigate = async (incident: IncidentInput) => {
107
271
  setStatus("investigating");
108
- setError(null);
109
272
  setCurrentTool(null);
273
+ toastInfo("Investigation started", incident.title);
110
274
 
111
- // Add user message to UI
112
275
  addMessage({
113
276
  role: "user",
114
277
  content: incident.description,
115
278
  });
116
279
 
117
- // Build prompt: use full incident prompt for first message, include history for follow-ups
118
280
  const isFirstMessage = conversationHistory().length === 0;
119
281
  const userContent = isFirstMessage
120
282
  ? buildIncidentPrompt(incident)
121
283
  : incident.description;
122
284
 
123
- // Format prompt with conversation history
124
285
  const prompt = formatHistoryAsPrompt(conversationHistory(), userContent);
125
286
 
126
- // Add user message to conversation history
127
287
  setConversationHistory((prev) => [
128
288
  ...prev,
129
289
  { role: "user", content: userContent },
@@ -131,28 +291,43 @@ function App() {
131
291
 
132
292
  try {
133
293
  const agent = getDebuggerAgent();
134
-
135
294
  let assistantContent = "";
136
295
 
137
- // Send the formatted prompt to the agent
138
296
  const stream = await agent.stream(prompt, {
139
297
  maxSteps: 20,
140
- onStepFinish: ({ toolCalls }) => {
298
+ onStepFinish: (stepResult) => {
299
+ const { toolCalls, toolResults } = stepResult;
300
+
141
301
  if (toolCalls && toolCalls.length > 0) {
142
- const toolCall = toolCalls[0] as { toolName?: string; args?: unknown };
143
- const toolName = toolCall.toolName ?? "tool";
144
- const args = toolCall.args ?? {};
302
+ const toolCallChunk = toolCalls[0] as { payload?: { toolName?: string; args?: unknown } };
303
+ const toolName = toolCallChunk?.payload?.toolName ?? "tool";
304
+ const args = toolCallChunk?.payload?.args ?? {};
145
305
 
146
- // Build display command based on tool type
147
306
  const command = buildDisplayCommand(toolName, args);
148
307
 
149
- setCurrentTool(toolName);
150
308
  addMessage({
151
309
  role: "tool",
152
310
  content: command ? `$ ${command}` : `Executing ${toolName}...`,
153
311
  toolName,
154
312
  command,
155
313
  });
314
+ // Note: Tool is already complete when onStepFinish fires
315
+ }
316
+
317
+ if (toolResults && toolResults.length > 0) {
318
+ for (const toolResult of toolResults) {
319
+ const tr = toolResult as any;
320
+ const data = tr?.payload?.result ?? tr?.result ?? tr;
321
+
322
+ if (data?.requiresApproval && data?.approvalId) {
323
+ handleApprovalRequest({
324
+ approvalId: data.approvalId,
325
+ command: data.command || "unknown command",
326
+ riskLevel: (data.riskLevel as RiskLevel) || "medium",
327
+ selectedOption: 0,
328
+ });
329
+ }
330
+ }
156
331
  }
157
332
  },
158
333
  });
@@ -161,24 +336,25 @@ function App() {
161
336
  assistantContent += chunk;
162
337
  }
163
338
 
164
- // Add assistant response to UI
165
- addMessage({
166
- role: "assistant",
167
- content: assistantContent,
168
- });
339
+ if (status() !== "awaiting_approval") {
340
+ addMessage({
341
+ role: "assistant",
342
+ content: assistantContent,
343
+ });
169
344
 
170
- // Add assistant response to conversation history
171
- setConversationHistory((prev) => [
172
- ...prev,
173
- { role: "assistant", content: assistantContent },
174
- ]);
345
+ setConversationHistory((prev) => [
346
+ ...prev,
347
+ { role: "assistant", content: assistantContent },
348
+ ]);
175
349
 
176
- setStatus("complete");
350
+ setStatus("complete");
351
+ toastSuccess("Investigation complete");
352
+ }
177
353
  setCurrentTool(null);
178
354
  } catch (err) {
179
355
  const errorMsg = err instanceof Error ? err.message : String(err);
180
- setError(errorMsg);
181
356
  setStatus("error");
357
+ toastError("Investigation failed", errorMsg);
182
358
  addMessage({
183
359
  role: "assistant",
184
360
  content: `Error: ${errorMsg}`,
@@ -202,165 +378,70 @@ function App() {
202
378
  setInputValue(value);
203
379
  };
204
380
 
205
- const getStatusColor = (): string => {
206
- switch (status()) {
207
- case "investigating":
208
- return "yellow";
209
- case "complete":
210
- return "green";
211
- case "error":
212
- return "red";
213
- default:
214
- return "gray";
381
+ const handleApprovalInput = (value: string) => {
382
+ if (value.toLowerCase() === "y") {
383
+ handleApprovalDecision(true);
384
+ } else if (value.toLowerCase() === "n") {
385
+ handleApprovalDecision(false);
215
386
  }
216
387
  };
217
388
 
218
- const getStatusText = (): string => {
219
- switch (status()) {
220
- case "investigating":
221
- return currentTool() ? `Running: ${currentTool()}` : "Investigating...";
222
- case "complete":
223
- return "Complete";
224
- case "error":
225
- return "Error";
226
- default:
227
- return "Ready";
389
+ const handleApprovalKeyDown = (key: { name: string }) => {
390
+ const approval = pendingApproval();
391
+ if (!approval) return;
392
+
393
+ if (key.name === "up" || key.name === "down") {
394
+ setPendingApproval({ ...approval, selectedOption: approval.selectedOption === 0 ? 1 : 0 });
395
+ } else if (key.name === "return") {
396
+ handleApprovalDecision(approval.selectedOption === 0);
228
397
  }
229
398
  };
230
399
 
231
400
  return (
232
401
  <box flexDirection="column" width="100%" height="100%">
233
402
  {/* Header */}
234
- <box
235
- borderStyle="single"
236
- borderColor="red"
237
- paddingLeft={2}
238
- paddingRight={2}
239
- flexDirection="row"
240
- justifyContent="space-between"
241
- >
242
- <text fg="red" attributes={ATTR_BOLD}>
243
- TRIAGENT
244
- </text>
245
- <text fg="gray">Kubernetes Debugging Agent</text>
246
- <text fg={getStatusColor()} attributes={ATTR_BOLD}>
247
- [{getStatusText()}]
248
- </text>
249
- </box>
250
-
251
- {/* Messages Area */}
252
- <scrollbox
253
- flexGrow={1}
254
- borderStyle="single"
255
- borderColor="gray"
256
- paddingLeft={1}
257
- paddingRight={1}
258
- stickyScroll
259
- stickyStart="bottom"
260
- >
261
- <box flexDirection="column" gap={1}>
262
- <Show
263
- when={messages().length > 0}
264
- fallback={
265
- <box paddingTop={2} paddingBottom={2}>
266
- <text fg="gray" attributes={ATTR_DIM}>
267
- Enter an incident description to start investigating...
268
- </text>
269
- </box>
270
- }
271
- >
272
- <For each={messages()}>
273
- {(msg) => (
274
- <box flexDirection="column" marginBottom={1}>
275
- <Show when={msg.role === "user"}>
276
- <box flexDirection="row" gap={1}>
277
- <text fg="cyan" attributes={ATTR_BOLD}>
278
- You:
279
- </text>
280
- <text fg="white">{msg.content}</text>
281
- </box>
282
- </Show>
283
- <Show when={msg.role === "tool"}>
284
- <box flexDirection="row" gap={1} alignItems="center">
285
- <Show
286
- when={status() === "investigating" && msg.id === messages().filter(m => m.role === "tool").at(-1)?.id}
287
- fallback={<text fg="green">✓</text>}
288
- >
289
- <spinner name="dots" color="blue" />
290
- </Show>
291
- <text fg="blue" attributes={ATTR_DIM}>
292
- [{msg.toolName}]
293
- </text>
294
- <text fg="gray" attributes={ATTR_DIM}>
295
- {msg.content}
296
- </text>
297
- </box>
298
- </Show>
299
- <Show when={msg.role === "assistant"}>
300
- <box flexDirection="column">
301
- <text fg="green" attributes={ATTR_BOLD}>
302
- Triagent:
303
- </text>
304
- <text fg="white" wrapMode="word">
305
- {msg.content}
306
- </text>
307
- </box>
308
- </Show>
309
- </box>
310
- )}
311
- </For>
312
- </Show>
313
- </box>
314
- </scrollbox>
315
-
316
- {/* Input Area */}
317
- <box
318
- borderStyle="single"
319
- borderColor={status() === "investigating" ? "yellow" : "cyan"}
320
- paddingLeft={1}
321
- paddingRight={1}
322
- flexDirection="row"
323
- gap={1}
324
- >
325
- <text fg="cyan" attributes={ATTR_BOLD}>
326
- {">"}
327
- </text>
328
- <input
329
- flexGrow={1}
330
- focused={true}
331
- value={inputValue()}
332
- onInput={handleInput}
333
- onSubmit={handleSubmit}
334
- placeholder={
335
- status() === "investigating"
336
- ? "Investigating..."
337
- : "Describe the incident..."
338
- }
339
- textColor="white"
340
- placeholderColor="gray"
341
- focusedTextColor="white"
342
- focusedBackgroundColor="#1a1a1a"
343
- />
344
- </box>
345
-
346
- {/* Footer */}
347
- <box
348
- paddingLeft={2}
349
- paddingRight={2}
350
- flexDirection="row"
351
- justifyContent="space-between"
352
- >
353
- <text fg="gray" attributes={ATTR_DIM}>
354
- Press Enter to submit | Ctrl+C to quit
355
- </text>
356
- <text fg="gray" attributes={ATTR_DIM}>
357
- {messages().length} messages
358
- </text>
359
- </box>
403
+ <Header
404
+ status={status()}
405
+ currentTool={currentTool()}
406
+ kubeContext={kubeContext()}
407
+ modelName={modelName()}
408
+ />
409
+
410
+ {/* Messages Panel */}
411
+ <MessagesPanel
412
+ messages={messages()}
413
+ status={status()}
414
+ />
415
+
416
+ {/* Editor */}
417
+ <Editor
418
+ status={status()}
419
+ value={inputValue()}
420
+ onInput={handleInput}
421
+ onSubmit={handleSubmit}
422
+ onApprovalInput={handleApprovalInput}
423
+ onApprovalKeyDown={handleApprovalKeyDown}
424
+ />
425
+
426
+ {/* Status Bar */}
427
+ <StatusBar
428
+ status={status()}
429
+ messageCount={messages().length}
430
+ />
360
431
  </box>
361
432
  );
362
433
  }
363
434
 
435
+ function App() {
436
+ return (
437
+ <ToastProvider>
438
+ <ApprovalDialogProvider>
439
+ <AppContent />
440
+ </ApprovalDialogProvider>
441
+ </ToastProvider>
442
+ );
443
+ }
444
+
364
445
  export interface TUIHandle {
365
446
  shutdown: () => void;
366
447
  handleWebhookIncident: (incident: IncidentInput) => Promise<string>;
@@ -371,13 +452,13 @@ export async function runTUI(): Promise<TUIHandle> {
371
452
 
372
453
  return {
373
454
  shutdown: () => {
374
- // The render function handles cleanup
375
455
  process.exit(0);
376
456
  },
377
457
  handleWebhookIncident: async (incident: IncidentInput) => {
378
- // For webhook mode, we'd need to integrate differently
379
- // This is a placeholder for now
380
458
  return "Webhook incident handling not yet implemented in TUI mode";
381
459
  },
382
460
  };
383
461
  }
462
+
463
+ // Export Message type for use in other components
464
+ export type { Message } from "./components/messages-panel.js";