triagent 0.1.0-alpha1

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.
@@ -0,0 +1,186 @@
1
+ import { Hono } from "hono";
2
+ import { serve } from "bun";
3
+ import { z } from "zod";
4
+ import { randomUUID } from "crypto";
5
+ import {
6
+ getDebuggerAgent,
7
+ buildIncidentPrompt,
8
+ type IncidentInput,
9
+ } from "../mastra/index.js";
10
+
11
+ const IncidentRequestSchema = z.object({
12
+ title: z.string().min(1),
13
+ description: z.string().min(1),
14
+ severity: z.enum(["critical", "warning", "info"]).optional(),
15
+ labels: z.record(z.string()).optional(),
16
+ });
17
+
18
+ interface Investigation {
19
+ id: string;
20
+ incident: IncidentInput;
21
+ status: "pending" | "running" | "completed" | "failed";
22
+ startedAt: Date;
23
+ completedAt?: Date;
24
+ result?: string;
25
+ error?: string;
26
+ }
27
+
28
+ const investigations = new Map<string, Investigation>();
29
+
30
+ export function createWebhookServer() {
31
+ const app = new Hono();
32
+
33
+ // Health check
34
+ app.get("/health", (c) => {
35
+ return c.json({ status: "ok", timestamp: new Date().toISOString() });
36
+ });
37
+
38
+ // Submit incident for investigation
39
+ app.post("/webhook/incident", async (c) => {
40
+ try {
41
+ const body = await c.req.json();
42
+ const parsed = IncidentRequestSchema.safeParse(body);
43
+
44
+ if (!parsed.success) {
45
+ return c.json(
46
+ {
47
+ error: "Invalid request",
48
+ details: parsed.error.errors,
49
+ },
50
+ 400
51
+ );
52
+ }
53
+
54
+ const incident = parsed.data;
55
+ const investigationId = randomUUID();
56
+
57
+ const investigation: Investigation = {
58
+ id: investigationId,
59
+ incident,
60
+ status: "pending",
61
+ startedAt: new Date(),
62
+ };
63
+
64
+ investigations.set(investigationId, investigation);
65
+
66
+ // Start investigation in background
67
+ runInvestigation(investigationId).catch((error) => {
68
+ console.error(`Investigation ${investigationId} failed:`, error);
69
+ });
70
+
71
+ return c.json(
72
+ {
73
+ investigationId,
74
+ status: "pending",
75
+ message: "Investigation started",
76
+ },
77
+ 202
78
+ );
79
+ } catch (error) {
80
+ return c.json(
81
+ {
82
+ error: "Internal server error",
83
+ message: error instanceof Error ? error.message : String(error),
84
+ },
85
+ 500
86
+ );
87
+ }
88
+ });
89
+
90
+ // Get investigation status/result
91
+ app.get("/investigations/:id", (c) => {
92
+ const id = c.req.param("id");
93
+ const investigation = investigations.get(id);
94
+
95
+ if (!investigation) {
96
+ return c.json({ error: "Investigation not found" }, 404);
97
+ }
98
+
99
+ return c.json({
100
+ id: investigation.id,
101
+ status: investigation.status,
102
+ incident: investigation.incident,
103
+ startedAt: investigation.startedAt.toISOString(),
104
+ completedAt: investigation.completedAt?.toISOString(),
105
+ result: investigation.result,
106
+ error: investigation.error,
107
+ });
108
+ });
109
+
110
+ // List recent investigations
111
+ app.get("/investigations", (c) => {
112
+ const limit = parseInt(c.req.query("limit") || "10", 10);
113
+ const recent = Array.from(investigations.values())
114
+ .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())
115
+ .slice(0, limit)
116
+ .map((inv) => ({
117
+ id: inv.id,
118
+ status: inv.status,
119
+ title: inv.incident.title,
120
+ severity: inv.incident.severity,
121
+ startedAt: inv.startedAt.toISOString(),
122
+ completedAt: inv.completedAt?.toISOString(),
123
+ }));
124
+
125
+ return c.json({ investigations: recent });
126
+ });
127
+
128
+ return app;
129
+ }
130
+
131
+ async function runInvestigation(id: string): Promise<void> {
132
+ const investigation = investigations.get(id);
133
+ if (!investigation) return;
134
+
135
+ investigation.status = "running";
136
+
137
+ try {
138
+ const agent = getDebuggerAgent();
139
+ const prompt = buildIncidentPrompt(investigation.incident);
140
+
141
+ console.log(`[Investigation ${id}] Starting...`);
142
+ console.log(`[Investigation ${id}] Incident: ${investigation.incident.title}`);
143
+
144
+ const response = await agent.generate([
145
+ { role: "user", content: prompt },
146
+ ], {
147
+ maxSteps: 20,
148
+ onStepFinish: ({ toolCalls }) => {
149
+ if (toolCalls && toolCalls.length > 0) {
150
+ const toolCall = toolCalls[0];
151
+ const toolName = "toolName" in toolCall ? toolCall.toolName : "tool";
152
+ console.log(`[Investigation ${id}] Tool: ${toolName}`);
153
+ }
154
+ },
155
+ });
156
+
157
+ investigation.status = "completed";
158
+ investigation.completedAt = new Date();
159
+ investigation.result = response.text;
160
+
161
+ console.log(`[Investigation ${id}] Completed`);
162
+ } catch (error) {
163
+ investigation.status = "failed";
164
+ investigation.completedAt = new Date();
165
+ investigation.error = error instanceof Error ? error.message : String(error);
166
+
167
+ console.error(`[Investigation ${id}] Failed:`, investigation.error);
168
+ }
169
+ }
170
+
171
+ export async function startWebhookServer(port: number): Promise<void> {
172
+ const app = createWebhookServer();
173
+
174
+ console.log(`🚨 Triagent webhook server starting on port ${port}...`);
175
+
176
+ serve({
177
+ fetch: app.fetch,
178
+ port,
179
+ });
180
+
181
+ console.log(`✅ Webhook server running at http://localhost:${port}`);
182
+ console.log(` POST /webhook/incident - Submit an incident`);
183
+ console.log(` GET /investigations/:id - Get investigation status`);
184
+ console.log(` GET /investigations - List recent investigations`);
185
+ console.log(` GET /health - Health check`);
186
+ }
@@ -0,0 +1,281 @@
1
+ import { render } from "@opentui/solid";
2
+ import { createSignal, For, Show, onMount } from "solid-js";
3
+ import { createTextAttributes } from "@opentui/core";
4
+ import { getDebuggerAgent, buildIncidentPrompt } from "../mastra/index.js";
5
+ import type { IncidentInput } from "../mastra/agents/debugger.js";
6
+
7
+ interface Message {
8
+ id: string;
9
+ role: "user" | "assistant" | "tool";
10
+ content: string;
11
+ timestamp: Date;
12
+ toolName?: string;
13
+ }
14
+
15
+ type AppStatus = "idle" | "investigating" | "complete" | "error";
16
+
17
+ const ATTR_DIM = createTextAttributes({ dim: true });
18
+ const ATTR_BOLD = createTextAttributes({ bold: true });
19
+
20
+ function App() {
21
+ const [messages, setMessages] = createSignal<Message[]>([]);
22
+ const [status, setStatus] = createSignal<AppStatus>("idle");
23
+ const [currentTool, setCurrentTool] = createSignal<string | null>(null);
24
+ const [inputValue, setInputValue] = createSignal("");
25
+ const [error, setError] = createSignal<string | null>(null);
26
+
27
+ const addMessage = (msg: Omit<Message, "id" | "timestamp">) => {
28
+ setMessages((prev) => [
29
+ ...prev,
30
+ {
31
+ ...msg,
32
+ id: crypto.randomUUID(),
33
+ timestamp: new Date(),
34
+ },
35
+ ]);
36
+ };
37
+
38
+ const investigate = async (incident: IncidentInput) => {
39
+ setStatus("investigating");
40
+ setError(null);
41
+ setCurrentTool(null);
42
+
43
+ addMessage({
44
+ role: "user",
45
+ content: incident.description,
46
+ });
47
+
48
+ try {
49
+ const agent = getDebuggerAgent();
50
+ const prompt = buildIncidentPrompt(incident);
51
+
52
+ let assistantContent = "";
53
+
54
+ const stream = await agent.stream(prompt, {
55
+ maxSteps: 20,
56
+ onStepFinish: ({ toolCalls }) => {
57
+ if (toolCalls && toolCalls.length > 0) {
58
+ const toolCall = toolCalls[0];
59
+ const toolName =
60
+ "toolName" in toolCall ? String(toolCall.toolName) : "tool";
61
+ setCurrentTool(toolName);
62
+ addMessage({
63
+ role: "tool",
64
+ content: `Executing ${toolName}...`,
65
+ toolName,
66
+ });
67
+ }
68
+ },
69
+ });
70
+
71
+ for await (const chunk of stream.textStream) {
72
+ assistantContent += chunk;
73
+ }
74
+
75
+ addMessage({
76
+ role: "assistant",
77
+ content: assistantContent,
78
+ });
79
+
80
+ setStatus("complete");
81
+ setCurrentTool(null);
82
+ } catch (err) {
83
+ const errorMsg = err instanceof Error ? err.message : String(err);
84
+ setError(errorMsg);
85
+ setStatus("error");
86
+ addMessage({
87
+ role: "assistant",
88
+ content: `Error: ${errorMsg}`,
89
+ });
90
+ }
91
+ };
92
+
93
+ const handleSubmit = (value: string) => {
94
+ const trimmed = value.trim();
95
+ if (!trimmed) return;
96
+ if (status() === "investigating") return;
97
+
98
+ setInputValue("");
99
+ investigate({
100
+ title: "Manual Investigation",
101
+ description: trimmed,
102
+ });
103
+ };
104
+
105
+ const handleInput = (value: string) => {
106
+ setInputValue(value);
107
+ };
108
+
109
+ const getStatusColor = (): string => {
110
+ switch (status()) {
111
+ case "investigating":
112
+ return "yellow";
113
+ case "complete":
114
+ return "green";
115
+ case "error":
116
+ return "red";
117
+ default:
118
+ return "gray";
119
+ }
120
+ };
121
+
122
+ const getStatusText = (): string => {
123
+ switch (status()) {
124
+ case "investigating":
125
+ return currentTool() ? `Running: ${currentTool()}` : "Investigating...";
126
+ case "complete":
127
+ return "Complete";
128
+ case "error":
129
+ return "Error";
130
+ default:
131
+ return "Ready";
132
+ }
133
+ };
134
+
135
+ return (
136
+ <box flexDirection="column" width="100%" height="100%">
137
+ {/* Header */}
138
+ <box
139
+ borderStyle="single"
140
+ borderColor="red"
141
+ paddingLeft={2}
142
+ paddingRight={2}
143
+ flexDirection="row"
144
+ justifyContent="space-between"
145
+ >
146
+ <text fg="red" attributes={ATTR_BOLD}>
147
+ TRIAGENT
148
+ </text>
149
+ <text fg="gray">Kubernetes Debugging Agent</text>
150
+ <text fg={getStatusColor()} attributes={ATTR_BOLD}>
151
+ [{getStatusText()}]
152
+ </text>
153
+ </box>
154
+
155
+ {/* Messages Area */}
156
+ <scrollbox
157
+ flexGrow={1}
158
+ borderStyle="single"
159
+ borderColor="gray"
160
+ paddingLeft={1}
161
+ paddingRight={1}
162
+ stickyScroll
163
+ stickyStart="bottom"
164
+ >
165
+ <box flexDirection="column" gap={1}>
166
+ <Show
167
+ when={messages().length > 0}
168
+ fallback={
169
+ <box paddingTop={2} paddingBottom={2}>
170
+ <text fg="gray" attributes={ATTR_DIM}>
171
+ Enter an incident description to start investigating...
172
+ </text>
173
+ </box>
174
+ }
175
+ >
176
+ <For each={messages()}>
177
+ {(msg) => (
178
+ <box flexDirection="column" marginBottom={1}>
179
+ <Show when={msg.role === "user"}>
180
+ <box flexDirection="row" gap={1}>
181
+ <text fg="cyan" attributes={ATTR_BOLD}>
182
+ You:
183
+ </text>
184
+ <text fg="white">{msg.content}</text>
185
+ </box>
186
+ </Show>
187
+ <Show when={msg.role === "tool"}>
188
+ <box flexDirection="row" gap={1}>
189
+ <text fg="blue" attributes={ATTR_DIM}>
190
+ [{msg.toolName}]
191
+ </text>
192
+ <text fg="gray" attributes={ATTR_DIM}>
193
+ {msg.content}
194
+ </text>
195
+ </box>
196
+ </Show>
197
+ <Show when={msg.role === "assistant"}>
198
+ <box flexDirection="column">
199
+ <text fg="green" attributes={ATTR_BOLD}>
200
+ Triagent:
201
+ </text>
202
+ <text fg="white" wrapMode="word">
203
+ {msg.content}
204
+ </text>
205
+ </box>
206
+ </Show>
207
+ </box>
208
+ )}
209
+ </For>
210
+ </Show>
211
+ </box>
212
+ </scrollbox>
213
+
214
+ {/* Input Area */}
215
+ <box
216
+ borderStyle="single"
217
+ borderColor={status() === "investigating" ? "yellow" : "cyan"}
218
+ paddingLeft={1}
219
+ paddingRight={1}
220
+ flexDirection="row"
221
+ gap={1}
222
+ >
223
+ <text fg="cyan" attributes={ATTR_BOLD}>
224
+ {">"}
225
+ </text>
226
+ <input
227
+ flexGrow={1}
228
+ focused={true}
229
+ value={inputValue()}
230
+ onInput={handleInput}
231
+ onSubmit={handleSubmit}
232
+ placeholder={
233
+ status() === "investigating"
234
+ ? "Investigating..."
235
+ : "Describe the incident..."
236
+ }
237
+ textColor="white"
238
+ placeholderColor="gray"
239
+ focusedTextColor="white"
240
+ focusedBackgroundColor="#1a1a1a"
241
+ />
242
+ </box>
243
+
244
+ {/* Footer */}
245
+ <box
246
+ paddingLeft={2}
247
+ paddingRight={2}
248
+ flexDirection="row"
249
+ justifyContent="space-between"
250
+ >
251
+ <text fg="gray" attributes={ATTR_DIM}>
252
+ Press Enter to submit | Ctrl+C to quit
253
+ </text>
254
+ <text fg="gray" attributes={ATTR_DIM}>
255
+ {messages().length} messages
256
+ </text>
257
+ </box>
258
+ </box>
259
+ );
260
+ }
261
+
262
+ export interface TUIHandle {
263
+ shutdown: () => void;
264
+ handleWebhookIncident: (incident: IncidentInput) => Promise<string>;
265
+ }
266
+
267
+ export async function runTUI(): Promise<TUIHandle> {
268
+ await render(() => <App />);
269
+
270
+ return {
271
+ shutdown: () => {
272
+ // The render function handles cleanup
273
+ process.exit(0);
274
+ },
275
+ handleWebhookIncident: async (incident: IncidentInput) => {
276
+ // For webhook mode, we'd need to integrate differently
277
+ // This is a placeholder for now
278
+ return "Webhook incident handling not yet implemented in TUI mode";
279
+ },
280
+ };
281
+ }