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.
- package/README.md +51 -0
- package/bunfig.toml +1 -0
- package/package.json +57 -0
- package/src/cli/config.ts +60 -0
- package/src/config.ts +97 -0
- package/src/index.ts +310 -0
- package/src/mastra/agents/debugger.ts +176 -0
- package/src/mastra/index.ts +36 -0
- package/src/mastra/tools/filesystem.ts +129 -0
- package/src/mastra/tools/git.ts +83 -0
- package/src/mastra/tools/kubectl.ts +107 -0
- package/src/sandbox/bashlet.ts +151 -0
- package/src/server/webhook.ts +186 -0
- package/src/tui/app.tsx +281 -0
|
@@ -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
|
+
}
|
package/src/tui/app.tsx
ADDED
|
@@ -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
|
+
}
|