triagent 0.1.0-alpha1 → 0.1.0-alpha2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triagent",
3
- "version": "0.1.0-alpha1",
3
+ "version": "0.1.0-alpha2",
4
4
  "description": "AI-powered Kubernetes debugging agent with terminal UI",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -46,7 +46,9 @@
46
46
  "@opentui/core": "^0.1.72",
47
47
  "@opentui/solid": "^0.1.72",
48
48
  "hono": "^4.6.0",
49
+ "opentui-spinner": "^0.0.6",
49
50
  "solid-js": "^1.9.10",
51
+ "triagent": "^0.1.0-alpha1",
50
52
  "zod": "^3.24.0"
51
53
  },
52
54
  "devDependencies": {
package/src/index.ts CHANGED
@@ -152,7 +152,12 @@ async function runDirectIncident(description: string): Promise<void> {
152
152
  if (toolCalls && toolCalls.length > 0) {
153
153
  const toolCall = toolCalls[0];
154
154
  const toolName = "toolName" in toolCall ? toolCall.toolName : "tool";
155
- console.log(`\n[Tool: ${toolName}]\n`);
155
+ const args = "args" in toolCall ? toolCall.args : {};
156
+ console.log(`\n[Tool: ${toolName}]`);
157
+ if (args && typeof args === "object" && "command" in args) {
158
+ console.log(`$ ${args.command}`);
159
+ }
160
+ console.log();
156
161
  }
157
162
  },
158
163
  });
@@ -1,6 +1,6 @@
1
1
  import { Agent } from "@mastra/core/agent";
2
2
  import { z } from "zod";
3
- import { kubectlTool } from "../tools/kubectl.js";
3
+ import { cliTool } from "../tools/cli.js";
4
4
  import { gitTool } from "../tools/git.js";
5
5
  import { filesystemTool } from "../tools/filesystem.js";
6
6
  import type { Config } from "../../config.js";
@@ -9,12 +9,14 @@ const DEBUGGER_INSTRUCTIONS = `You are an expert Kubernetes debugging agent name
9
9
 
10
10
  ## Your Capabilities
11
11
 
12
- 1. **Kubernetes Inspection** (kubectl tool):
13
- - Get resource status (pods, deployments, services, configmaps)
14
- - Describe resources for detailed information
15
- - Fetch container logs
16
- - Check resource usage (top)
17
- - Review cluster events
12
+ 1. **CLI Access** (cli tool):
13
+ - Run any shell command including kubectl, grep, awk, jq, curl, etc.
14
+ - Pipe commands together for powerful filtering and processing
15
+ - Examples:
16
+ - \`kubectl get pods -A | grep inventory\`
17
+ - \`kubectl logs deploy/myapp --tail 100 | grep -i error\`
18
+ - \`kubectl get pods -o json | jq '.items[].metadata.name'\`
19
+ - \`kubectl describe pod mypod | grep -A10 Events\`
18
20
 
19
21
  2. **Code Analysis** (filesystem tool):
20
22
  - Read source code files
@@ -27,6 +29,39 @@ const DEBUGGER_INSTRUCTIONS = `You are an expert Kubernetes debugging agent name
27
29
  - Show specific commit details
28
30
  - Blame files to find who changed what
29
31
 
32
+ ## Resource Discovery Strategy
33
+
34
+ When asked to find resources for a service (e.g., "inventory service"), DO NOT simply try one label like \`app=inventory\` and give up if not found. Instead, use a systematic discovery approach:
35
+
36
+ 1. **Search by partial name match using grep**:
37
+ - \`kubectl get pods -A | grep -i inventory\`
38
+ - \`kubectl get deploy,svc -A | grep -i inventory\`
39
+ - This finds resources with "inventory" anywhere in the name (e.g., \`inventory-api\`, \`svc-inventory\`)
40
+
41
+ 2. **If grep returns no results, list all resources to browse**:
42
+ - \`kubectl get pods,deploy,svc -A\` to see everything
43
+ - \`kubectl get pods -n <namespace>\` if namespace is known
44
+
45
+ 3. **Try common label patterns**:
46
+ - \`kubectl get pods -A -l app=inventory\`
47
+ - \`kubectl get pods -A -l app.kubernetes.io/name=inventory\`
48
+ - \`kubectl get pods -A -l component=inventory\`
49
+
50
+ 4. **Follow the resource chain**:
51
+ - Found a Service? \`kubectl describe svc <name> | grep Selector\` then find pods with that selector
52
+ - Found a Deployment? \`kubectl get pods -l app=<deployment-name>\`
53
+ - Use \`kubectl get endpoints <svc-name>\` to see which pods back a service
54
+
55
+ 5. **Check events for context**:
56
+ - \`kubectl get events -A --sort-by='.lastTimestamp' | grep -i inventory\`
57
+ - \`kubectl get events -A --sort-by='.lastTimestamp' | head -20\` for recent cluster activity
58
+
59
+ 6. **When you find a potential match**:
60
+ - \`kubectl describe <resource> <name>\` to confirm it's the right one
61
+ - Check related resources (pods for a deployment, endpoints for a service)
62
+
63
+ Always report what you searched for and what you found, even if it's not an exact match. The user can confirm if you found the right resource.
64
+
30
65
  ## Investigation Process
31
66
 
32
67
  When given an incident, follow this systematic approach:
@@ -36,26 +71,32 @@ When given an incident, follow this systematic approach:
36
71
  - What symptoms are being observed
37
72
  - When the issue started (if known)
38
73
 
39
- 2. **Check Cluster State**:
40
- - Get pod status for affected services
41
- - Check for recent events
74
+ 2. **Discover Relevant Resources**:
75
+ - Use the Resource Discovery Strategy above to find the affected resources
76
+ - Don't assume exact names or labels - search broadly first
77
+ - Follow the resource chain (Service → Deployment → Pods → Containers)
78
+
79
+ 3. **Check Cluster State**:
80
+ - Get pod status for discovered resources
81
+ - Check for recent events related to those resources
42
82
  - Look at resource usage
43
83
 
44
- 3. **Analyze Logs**:
45
- - Fetch logs from affected pods
84
+ 4. **Analyze Logs**:
85
+ - Fetch logs from affected pods (use \`--tail 100\` to get recent logs)
46
86
  - Look for errors, exceptions, or unusual patterns
87
+ - If multiple containers, check each one
47
88
 
48
- 4. **Investigate Recent Changes**:
89
+ 5. **Investigate Recent Changes**:
49
90
  - Check git log for recent commits
50
91
  - Review diffs of suspicious changes
51
92
  - Correlate timing with when issues started
52
93
 
53
- 5. **Examine Code**:
94
+ 6. **Examine Code**:
54
95
  - Read relevant configuration files
55
96
  - Check application code if needed
56
97
  - Look for misconfigurations
57
98
 
58
- 6. **Synthesize Findings**:
99
+ 7. **Synthesize Findings**:
59
100
  - Identify the root cause
60
101
  - List affected resources
61
102
  - Provide actionable recommendations
@@ -131,7 +172,7 @@ export function createDebuggerAgent(config: Config) {
131
172
  instructions: DEBUGGER_INSTRUCTIONS,
132
173
  model: modelString as any, // Mastra handles model routing
133
174
  tools: {
134
- kubectl: kubectlTool,
175
+ cli: cliTool,
135
176
  git: gitTool,
136
177
  filesystem: filesystemTool,
137
178
  },
@@ -0,0 +1,65 @@
1
+ import { createTool } from "@mastra/core/tools";
2
+ import { z } from "zod";
3
+ import { execCommand } from "../../sandbox/bashlet.js";
4
+
5
+ interface CliOutput {
6
+ success: boolean;
7
+ output: string;
8
+ error?: string;
9
+ }
10
+
11
+ function filterSensitiveData(output: string): string {
12
+ // Redact potential secrets, tokens, and passwords
13
+ return output
14
+ .replace(
15
+ /(password|secret|token|key|credential)[\s:=]+["']?[^\s"'\n]+["']?/gi,
16
+ "$1: [REDACTED]"
17
+ )
18
+ .replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]")
19
+ .replace(/-----BEGIN[^-]+-----[\s\S]*?-----END[^-]+-----/g, "[REDACTED CERTIFICATE/KEY]");
20
+ }
21
+
22
+ export const cliTool = createTool({
23
+ id: "cli",
24
+ description: `Execute shell commands in the sandbox environment.
25
+ Use this to run any CLI commands including kubectl, grep, awk, jq, curl, etc.
26
+ Supports pipes and command chaining.
27
+
28
+ Examples:
29
+ - List all pods: kubectl get pods -A
30
+ - Find pods by name: kubectl get pods -A | grep inventory
31
+ - Get logs with filtering: kubectl logs deployment/myapp -n prod --tail 100 | grep -i error
32
+ - Check resource usage: kubectl top pods -n default
33
+ - Describe and search: kubectl describe pod mypod | grep -A5 "Events"
34
+ - JSON processing: kubectl get pods -o json | jq '.items[].metadata.name'`,
35
+
36
+ inputSchema: z.object({
37
+ command: z.string().describe("The shell command to execute"),
38
+ }),
39
+
40
+ execute: async ({ command }): Promise<CliOutput> => {
41
+ try {
42
+ const result = await execCommand(command);
43
+
44
+ if (result.exitCode !== 0) {
45
+ return {
46
+ success: false,
47
+ output: result.stdout ? filterSensitiveData(result.stdout) : "",
48
+ error: result.stderr || `Command failed with exit code ${result.exitCode}`,
49
+ };
50
+ }
51
+
52
+ return {
53
+ success: true,
54
+ output: filterSensitiveData(result.stdout),
55
+ error: result.stderr ? filterSensitiveData(result.stderr) : undefined,
56
+ };
57
+ } catch (error) {
58
+ return {
59
+ success: false,
60
+ output: "",
61
+ error: error instanceof Error ? error.message : String(error),
62
+ };
63
+ }
64
+ },
65
+ });
@@ -149,7 +149,11 @@ async function runInvestigation(id: string): Promise<void> {
149
149
  if (toolCalls && toolCalls.length > 0) {
150
150
  const toolCall = toolCalls[0];
151
151
  const toolName = "toolName" in toolCall ? toolCall.toolName : "tool";
152
+ const args = "args" in toolCall ? toolCall.args : {};
152
153
  console.log(`[Investigation ${id}] Tool: ${toolName}`);
154
+ if (args && typeof args === "object" && "command" in args) {
155
+ console.log(`[Investigation ${id}] $ ${args.command}`);
156
+ }
153
157
  }
154
158
  },
155
159
  });
package/src/tui/app.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { render } from "@opentui/solid";
2
2
  import { createSignal, For, Show, onMount } from "solid-js";
3
3
  import { createTextAttributes } from "@opentui/core";
4
+ import "opentui-spinner/solid";
4
5
  import { getDebuggerAgent, buildIncidentPrompt } from "../mastra/index.js";
5
6
  import type { IncidentInput } from "../mastra/agents/debugger.js";
6
7
 
@@ -10,6 +11,25 @@ interface Message {
10
11
  content: string;
11
12
  timestamp: Date;
12
13
  toolName?: string;
14
+ command?: string;
15
+ }
16
+
17
+ // Conversation history for multi-turn debugging
18
+ interface ConversationMessage {
19
+ role: "user" | "assistant";
20
+ content: string;
21
+ }
22
+
23
+ function formatHistoryAsPrompt(history: ConversationMessage[], newMessage: string): string {
24
+ if (history.length === 0) {
25
+ return newMessage;
26
+ }
27
+
28
+ const historyText = history
29
+ .map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
30
+ .join("\n\n");
31
+
32
+ return `Previous conversation:\n${historyText}\n\nUser: ${newMessage}`;
13
33
  }
14
34
 
15
35
  type AppStatus = "idle" | "investigating" | "complete" | "error";
@@ -17,8 +37,55 @@ type AppStatus = "idle" | "investigating" | "complete" | "error";
17
37
  const ATTR_DIM = createTextAttributes({ dim: true });
18
38
  const ATTR_BOLD = createTextAttributes({ bold: true });
19
39
 
40
+ function buildDisplayCommand(toolName: string, args: unknown): string | undefined {
41
+ if (!args || typeof args !== "object") return undefined;
42
+
43
+ const a = args as Record<string, unknown>;
44
+
45
+ switch (toolName) {
46
+ case "cli":
47
+ // CLI tool has direct command
48
+ return "command" in a ? String(a.command) : undefined;
49
+
50
+ case "git": {
51
+ // Build git command: git <command> [args...] [path]
52
+ if (!("command" in a)) return undefined;
53
+ const parts = ["git", String(a.command)];
54
+ if ("args" in a && Array.isArray(a.args)) {
55
+ parts.push(...a.args.map(String));
56
+ }
57
+ if ("path" in a && a.path) {
58
+ parts.push(String(a.path));
59
+ }
60
+ return parts.join(" ");
61
+ }
62
+
63
+ case "filesystem": {
64
+ // Build filesystem display: <operation> <path> [pattern]
65
+ if (!("operation" in a)) return undefined;
66
+ const op = String(a.operation);
67
+ const path = "path" in a ? String(a.path) : "";
68
+ if (op === "search" && "pattern" in a) {
69
+ return `grep "${a.pattern}" ${path}`;
70
+ }
71
+ if (op === "read") {
72
+ return `cat ${path}`;
73
+ }
74
+ if (op === "list") {
75
+ return `ls ${path}`;
76
+ }
77
+ return `${op} ${path}`;
78
+ }
79
+
80
+ default:
81
+ // Fallback: try to use command if it exists
82
+ return "command" in a ? String(a.command) : undefined;
83
+ }
84
+ }
85
+
20
86
  function App() {
21
87
  const [messages, setMessages] = createSignal<Message[]>([]);
88
+ const [conversationHistory, setConversationHistory] = createSignal<ConversationMessage[]>([]);
22
89
  const [status, setStatus] = createSignal<AppStatus>("idle");
23
90
  const [currentTool, setCurrentTool] = createSignal<string | null>(null);
24
91
  const [inputValue, setInputValue] = createSignal("");
@@ -40,29 +107,51 @@ function App() {
40
107
  setError(null);
41
108
  setCurrentTool(null);
42
109
 
110
+ // Add user message to UI
43
111
  addMessage({
44
112
  role: "user",
45
113
  content: incident.description,
46
114
  });
47
115
 
116
+ // Build prompt: use full incident prompt for first message, include history for follow-ups
117
+ const isFirstMessage = conversationHistory().length === 0;
118
+ const userContent = isFirstMessage
119
+ ? buildIncidentPrompt(incident)
120
+ : incident.description;
121
+
122
+ // Format prompt with conversation history
123
+ const prompt = formatHistoryAsPrompt(conversationHistory(), userContent);
124
+
125
+ // Add user message to conversation history
126
+ setConversationHistory((prev) => [
127
+ ...prev,
128
+ { role: "user", content: userContent },
129
+ ]);
130
+
48
131
  try {
49
132
  const agent = getDebuggerAgent();
50
- const prompt = buildIncidentPrompt(incident);
51
133
 
52
134
  let assistantContent = "";
53
135
 
136
+ // Send the formatted prompt to the agent
54
137
  const stream = await agent.stream(prompt, {
55
138
  maxSteps: 20,
56
139
  onStepFinish: ({ toolCalls }) => {
57
140
  if (toolCalls && toolCalls.length > 0) {
58
- const toolCall = toolCalls[0];
59
- const toolName =
60
- "toolName" in toolCall ? String(toolCall.toolName) : "tool";
141
+ const toolCall = toolCalls[0] as { payload?: { toolName?: string; args?: unknown } };
142
+ const payload = toolCall.payload;
143
+ const toolName = payload?.toolName ?? "tool";
144
+ const args = payload?.args ?? {};
145
+
146
+ // Build display command based on tool type
147
+ const command = buildDisplayCommand(toolName, args);
148
+
61
149
  setCurrentTool(toolName);
62
150
  addMessage({
63
151
  role: "tool",
64
- content: `Executing ${toolName}...`,
152
+ content: command ? `$ ${command}` : `Executing ${toolName}...`,
65
153
  toolName,
154
+ command,
66
155
  });
67
156
  }
68
157
  },
@@ -72,11 +161,18 @@ function App() {
72
161
  assistantContent += chunk;
73
162
  }
74
163
 
164
+ // Add assistant response to UI
75
165
  addMessage({
76
166
  role: "assistant",
77
167
  content: assistantContent,
78
168
  });
79
169
 
170
+ // Add assistant response to conversation history
171
+ setConversationHistory((prev) => [
172
+ ...prev,
173
+ { role: "assistant", content: assistantContent },
174
+ ]);
175
+
80
176
  setStatus("complete");
81
177
  setCurrentTool(null);
82
178
  } catch (err) {
@@ -185,7 +281,13 @@ function App() {
185
281
  </box>
186
282
  </Show>
187
283
  <Show when={msg.role === "tool"}>
188
- <box flexDirection="row" gap={1}>
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>
189
291
  <text fg="blue" attributes={ATTR_DIM}>
190
292
  [{msg.toolName}]
191
293
  </text>
@@ -1,107 +0,0 @@
1
- import { createTool } from "@mastra/core/tools";
2
- import { z } from "zod";
3
- import { execCommand } from "../../sandbox/bashlet.js";
4
-
5
- const ALLOWED_COMMANDS = ["get", "describe", "logs", "top", "events"] as const;
6
-
7
- const KubectlInputSchema = z.object({
8
- command: z.enum(ALLOWED_COMMANDS).describe("The kubectl command to run"),
9
- resource: z
10
- .string()
11
- .optional()
12
- .describe(
13
- "Resource type (e.g., pods, deployments, services, configmaps, secrets)"
14
- ),
15
- name: z.string().optional().describe("Specific resource name"),
16
- namespace: z
17
- .string()
18
- .optional()
19
- .describe("Kubernetes namespace (defaults to current context namespace)"),
20
- flags: z
21
- .array(z.string())
22
- .optional()
23
- .describe(
24
- "Additional flags (e.g., ['-o', 'yaml'], ['--tail', '100'], ['-l', 'app=myapp'])"
25
- ),
26
- });
27
-
28
- // Output type (no schema validation to allow error returns)
29
- interface KubectlOutput {
30
- success: boolean;
31
- output: string;
32
- error?: string;
33
- }
34
-
35
- function filterSensitiveData(output: string): string {
36
- // Redact potential secrets, tokens, and passwords
37
- return output
38
- .replace(
39
- /(password|secret|token|key|credential)[\s:=]+["']?[^\s"'\n]+["']?/gi,
40
- "$1: [REDACTED]"
41
- )
42
- .replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]")
43
- .replace(/-----BEGIN[^-]+-----[\s\S]*?-----END[^-]+-----/g, "[REDACTED CERTIFICATE/KEY]");
44
- }
45
-
46
- export const kubectlTool = createTool({
47
- id: "kubectl",
48
- description: `Execute kubectl commands to inspect Kubernetes resources.
49
- Available commands: ${ALLOWED_COMMANDS.join(", ")}.
50
- Use this to get information about pods, deployments, services, logs, and cluster events.
51
- Examples:
52
- - Get all pods: { command: "get", resource: "pods", flags: ["-A"] }
53
- - Get pod logs: { command: "logs", name: "my-pod", namespace: "default", flags: ["--tail", "100"] }
54
- - Describe deployment: { command: "describe", resource: "deployment", name: "my-app" }
55
- - Get events: { command: "events", namespace: "production", flags: ["--sort-by", ".lastTimestamp"] }`,
56
-
57
- inputSchema: KubectlInputSchema,
58
-
59
- execute: async (inputData): Promise<KubectlOutput> => {
60
- const { command, resource, name, namespace, flags } = inputData;
61
-
62
- // Build kubectl command
63
- const parts = ["kubectl", command];
64
-
65
- if (resource) {
66
- parts.push(resource);
67
- }
68
-
69
- if (name) {
70
- parts.push(name);
71
- }
72
-
73
- if (namespace) {
74
- parts.push("-n", namespace);
75
- }
76
-
77
- if (flags && flags.length > 0) {
78
- parts.push(...flags);
79
- }
80
-
81
- const fullCommand = parts.join(" ");
82
-
83
- try {
84
- const result = await execCommand(fullCommand);
85
-
86
- if (result.exitCode !== 0) {
87
- return {
88
- success: false,
89
- output: "",
90
- error: result.stderr || `Command failed with exit code ${result.exitCode}`,
91
- };
92
- }
93
-
94
- return {
95
- success: true,
96
- output: filterSensitiveData(result.stdout),
97
- error: result.stderr ? filterSensitiveData(result.stderr) : undefined,
98
- };
99
- } catch (error) {
100
- return {
101
- success: false,
102
- output: "",
103
- error: error instanceof Error ? error.message : String(error),
104
- };
105
- }
106
- },
107
- });