@fastino-ai/pioneer-cli 0.2.1 → 0.2.3

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 (40) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/.cursor/rules/api-documentation.mdc +14 -0
  3. package/.cursor/rules/backend-location-rule.mdc +5 -0
  4. package/Medical_NER_Dataset_1.jsonl +50 -0
  5. package/README.md +4 -1
  6. package/bun.lock +52 -0
  7. package/package.json +5 -2
  8. package/src/api.ts +551 -22
  9. package/src/chat/ChatApp.tsx +548 -263
  10. package/src/client/ToolExecutor.ts +175 -0
  11. package/src/client/WebSocketClient.ts +333 -0
  12. package/src/client/index.ts +2 -0
  13. package/src/config.ts +49 -139
  14. package/src/index.tsx +815 -107
  15. package/src/telemetry.ts +173 -0
  16. package/src/tests/config.test.ts +19 -0
  17. package/src/tools/bash.ts +1 -1
  18. package/src/tools/filesystem.ts +1 -1
  19. package/src/tools/index.ts +2 -9
  20. package/src/tools/sandbox.ts +1 -1
  21. package/src/tools/types.ts +25 -0
  22. package/src/utils/index.ts +6 -0
  23. package/fastino-ai-pioneer-cli-0.2.0.tgz +0 -0
  24. package/ner_dataset.json +0 -111
  25. package/src/agent/Agent.ts +0 -342
  26. package/src/agent/BudgetManager.ts +0 -167
  27. package/src/agent/LLMClient.ts +0 -435
  28. package/src/agent/ToolRegistry.ts +0 -97
  29. package/src/agent/index.ts +0 -15
  30. package/src/agent/types.ts +0 -84
  31. package/src/evolution/EvalRunner.ts +0 -301
  32. package/src/evolution/EvolutionEngine.ts +0 -319
  33. package/src/evolution/FeedbackCollector.ts +0 -197
  34. package/src/evolution/ModelTrainer.ts +0 -371
  35. package/src/evolution/index.ts +0 -18
  36. package/src/evolution/types.ts +0 -110
  37. package/src/tools/modal.ts +0 -269
  38. package/src/tools/training.ts +0 -443
  39. package/src/tools/wandb.ts +0 -348
  40. /package/src/{agent → utils}/FileResolver.ts +0 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * ToolExecutor - Executes CLI tools locally
3
+ *
4
+ * When the MLE Agent calls a CLI tool (bash, read_file, etc.), this executor
5
+ * runs it on the user's machine and returns the result.
6
+ */
7
+
8
+ import { createBashTool } from "../tools/bash.js";
9
+ import {
10
+ createReadFileTool,
11
+ createWriteFileTool,
12
+ createEditFileTool,
13
+ createListDirTool,
14
+ createSearchFilesTool,
15
+ } from "../tools/filesystem.js";
16
+ import type { Tool, ToolResult } from "../tools/types.js";
17
+
18
+ export interface ToolExecutorOptions {
19
+ workingDirectory?: string;
20
+ allowedPaths?: string[];
21
+ maxFileSize?: number;
22
+ bashTimeout?: number;
23
+ }
24
+
25
+ export class ToolExecutor {
26
+ private tools: Map<string, Tool> = new Map();
27
+ private workingDirectory: string;
28
+
29
+ constructor(options: ToolExecutorOptions = {}) {
30
+ this.workingDirectory = options.workingDirectory || process.cwd();
31
+
32
+ const fsOptions = {
33
+ basePath: this.workingDirectory,
34
+ allowedPaths: options.allowedPaths,
35
+ maxFileSize: options.maxFileSize || 1024 * 1024, // 1MB default
36
+ };
37
+
38
+ const bashOptions = {
39
+ cwd: this.workingDirectory,
40
+ timeout: options.bashTimeout || 60000, // 60s default
41
+ };
42
+
43
+ // Register all CLI tools
44
+ this.registerTool(createBashTool(bashOptions));
45
+ this.registerTool(createReadFileTool(fsOptions));
46
+ this.registerTool(createWriteFileTool(fsOptions));
47
+ this.registerTool(createEditFileTool(fsOptions));
48
+ this.registerTool(createListDirTool(fsOptions));
49
+ this.registerTool(createSearchFilesTool(fsOptions));
50
+ }
51
+
52
+ private registerTool(tool: Tool): void {
53
+ this.tools.set(tool.name, tool);
54
+ }
55
+
56
+ /**
57
+ * Execute a tool by name with the given arguments.
58
+ * Returns the output string on success, throws on error.
59
+ */
60
+ async execute(toolName: string, args: Record<string, unknown>): Promise<string> {
61
+ const tool = this.tools.get(toolName);
62
+
63
+ if (!tool) {
64
+ throw new Error(`Unknown tool: ${toolName}. Available tools: ${this.getToolNames().join(", ")}`);
65
+ }
66
+
67
+ // Map argument names if needed (server uses different names)
68
+ const mappedArgs = this.mapArguments(toolName, args);
69
+
70
+ const result = await tool.execute(mappedArgs);
71
+
72
+ if (!result.success) {
73
+ throw new Error(result.error || `Tool ${toolName} failed`);
74
+ }
75
+
76
+ return result.output;
77
+ }
78
+
79
+ /**
80
+ * Map server argument names to local tool argument names.
81
+ * The server uses slightly different naming conventions.
82
+ */
83
+ private mapArguments(toolName: string, args: Record<string, unknown>): Record<string, unknown> {
84
+ const mapped = { ...args };
85
+
86
+ switch (toolName) {
87
+ case "read_file":
88
+ // Server: file_path, start_line, end_line
89
+ // Local: path, start_line, end_line
90
+ if ("file_path" in args) {
91
+ mapped.path = args.file_path;
92
+ delete mapped.file_path;
93
+ }
94
+ break;
95
+
96
+ case "write_file":
97
+ // Server: file_path, content
98
+ // Local: path, content
99
+ if ("file_path" in args) {
100
+ mapped.path = args.file_path;
101
+ delete mapped.file_path;
102
+ }
103
+ break;
104
+
105
+ case "edit_file":
106
+ // Server: file_path, old_text, new_text
107
+ // Local: path, old_text, new_text
108
+ if ("file_path" in args) {
109
+ mapped.path = args.file_path;
110
+ delete mapped.file_path;
111
+ }
112
+ break;
113
+
114
+ case "list_directory":
115
+ // Both use: path, recursive, max_depth
116
+ break;
117
+
118
+ case "search_files":
119
+ // Server: path, pattern, search_content, max_results
120
+ // Local: path, pattern, type
121
+ if (args.search_content) {
122
+ mapped.type = "content";
123
+ } else {
124
+ mapped.type = "filename";
125
+ }
126
+ delete mapped.search_content;
127
+ delete mapped.max_results;
128
+ break;
129
+
130
+ case "bash":
131
+ // Both use: command, workdir
132
+ break;
133
+ }
134
+
135
+ return mapped;
136
+ }
137
+
138
+ /**
139
+ * Get list of available tool names.
140
+ */
141
+ getToolNames(): string[] {
142
+ return Array.from(this.tools.keys());
143
+ }
144
+
145
+ /**
146
+ * Get a tool by name.
147
+ */
148
+ getTool(name: string): Tool | undefined {
149
+ return this.tools.get(name);
150
+ }
151
+
152
+ /**
153
+ * Check if a tool exists.
154
+ */
155
+ hasTool(name: string): boolean {
156
+ return this.tools.has(name);
157
+ }
158
+
159
+ /**
160
+ * Get working directory.
161
+ */
162
+ getWorkingDirectory(): string {
163
+ return this.workingDirectory;
164
+ }
165
+
166
+ /**
167
+ * Set working directory for bash commands.
168
+ */
169
+ setWorkingDirectory(dir: string): void {
170
+ this.workingDirectory = dir;
171
+
172
+ // Recreate bash tool with new working directory
173
+ this.registerTool(createBashTool({ cwd: dir }));
174
+ }
175
+ }
@@ -0,0 +1,333 @@
1
+ /**
2
+ * WebSocket client for Pioneer MLE Agent
3
+ *
4
+ * Connects to the /mle-agent/ws endpoint for bidirectional communication.
5
+ * Handles tool_call messages by executing tools locally and sending results back.
6
+ */
7
+
8
+ import WebSocket from "ws";
9
+ import { getApiKey, getBaseUrl } from "../config.js";
10
+
11
+ export interface WebSocketMessage {
12
+ type: string;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ export interface ToolCallRequest {
17
+ call_id: string;
18
+ tool: string;
19
+ args: Record<string, unknown>;
20
+ }
21
+
22
+ /**
23
+ * Message format for conversation history.
24
+ * Matches the format sent by the backend on done event.
25
+ */
26
+ export interface HistoryMessage {
27
+ role: "user" | "assistant" | "tool";
28
+ content: string;
29
+ tool_calls?: Array<{
30
+ id: string;
31
+ name: string;
32
+ args: Record<string, unknown>;
33
+ }>;
34
+ tool_call_id?: string;
35
+ }
36
+
37
+ export interface WebSocketClientCallbacks {
38
+ onStream?: (content: string) => void;
39
+ onToolStart?: (name: string, callId: string, args: Record<string, unknown>) => void;
40
+ onToolResult?: (name: string, callId: string, result: unknown) => void;
41
+ onToolError?: (name: string, callId: string, error: string) => void;
42
+ onToolCall?: (call: ToolCallRequest) => Promise<string>;
43
+ onAssistantMessage?: (content: string) => void;
44
+ onError?: (error: Error) => void;
45
+ onDone?: (messages?: HistoryMessage[]) => void;
46
+ }
47
+
48
+ export interface ChatOptions {
49
+ history?: HistoryMessage[];
50
+ fileReferences?: string[];
51
+ config?: { model?: string };
52
+ }
53
+
54
+ type ConnectionState = "disconnected" | "connecting" | "connected";
55
+
56
+ export class WebSocketClient {
57
+ private ws: WebSocket | null = null;
58
+ private baseUrl: string;
59
+ private apiKey: string | undefined;
60
+ private callbacks: WebSocketClientCallbacks = {};
61
+ private state: ConnectionState = "disconnected";
62
+ private messageResolvers: Map<string, (value: void) => void> = new Map();
63
+ private donePromise: Promise<void> | null = null;
64
+ private doneResolver: (() => void) | null = null;
65
+ private doneCalled: boolean = false; // Prevent duplicate onDone calls
66
+
67
+ constructor() {
68
+ // Convert HTTP URL to WebSocket URL
69
+ this.baseUrl = getBaseUrl()
70
+ .replace(/^https:/, "wss:")
71
+ .replace(/^http:/, "ws:")
72
+ .replace(/\/$/, "");
73
+ this.apiKey = getApiKey();
74
+ }
75
+
76
+ async connect(): Promise<void> {
77
+ if (this.state === "connected") return;
78
+ if (this.state === "connecting") {
79
+ // Wait for existing connection attempt
80
+ return new Promise((resolve) => {
81
+ const checkState = setInterval(() => {
82
+ if (this.state === "connected") {
83
+ clearInterval(checkState);
84
+ resolve();
85
+ }
86
+ }, 100);
87
+ });
88
+ }
89
+
90
+ this.state = "connecting";
91
+
92
+ // Build URL with optional auth token
93
+ let url = `${this.baseUrl}/mle-agent/ws`;
94
+ if (this.apiKey) {
95
+ url += `?token=${encodeURIComponent(this.apiKey)}`;
96
+ }
97
+
98
+ return new Promise((resolve, reject) => {
99
+ this.ws = new WebSocket(url);
100
+
101
+ this.ws.on("open", () => {
102
+ this.state = "connected";
103
+ resolve();
104
+ });
105
+
106
+ this.ws.on("error", (err: Error) => {
107
+ this.state = "disconnected";
108
+ reject(err);
109
+ });
110
+
111
+ this.ws.on("close", () => {
112
+ this.state = "disconnected";
113
+ // Only call onDone if not already called (prevents duplicate)
114
+ if (!this.doneCalled) {
115
+ this.doneCalled = true;
116
+ this.callbacks.onDone?.();
117
+ }
118
+ if (this.doneResolver) {
119
+ this.doneResolver();
120
+ this.doneResolver = null;
121
+ }
122
+ });
123
+
124
+ this.ws.on("message", async (data: WebSocket.RawData) => {
125
+ try {
126
+ const message = JSON.parse(data.toString()) as WebSocketMessage;
127
+ await this.handleMessage(message);
128
+ } catch (err: unknown) {
129
+ console.error("Failed to parse WebSocket message:", err);
130
+ }
131
+ });
132
+ });
133
+ }
134
+
135
+ private async handleMessage(message: WebSocketMessage): Promise<void> {
136
+ switch (message.type) {
137
+ case "stream":
138
+ this.callbacks.onStream?.(message.content as string);
139
+ break;
140
+
141
+ case "tool_start":
142
+ this.callbacks.onToolStart?.(
143
+ message.name as string,
144
+ message.call_id as string,
145
+ message.args as Record<string, unknown>
146
+ );
147
+ break;
148
+
149
+ case "tool_call":
150
+ // Server is asking client to execute a tool locally
151
+ if (this.callbacks.onToolCall) {
152
+ const call: ToolCallRequest = {
153
+ call_id: message.call_id as string,
154
+ tool: message.tool as string,
155
+ args: message.args as Record<string, unknown>,
156
+ };
157
+
158
+ try {
159
+ const result = await this.callbacks.onToolCall(call);
160
+ this.sendToolResult(call.call_id, result);
161
+ } catch (error) {
162
+ const errorMsg = error instanceof Error ? error.message : String(error);
163
+ this.sendToolResult(call.call_id, null, errorMsg);
164
+ }
165
+ } else {
166
+ // No tool executor configured, send error
167
+ this.sendToolResult(
168
+ message.call_id as string,
169
+ null,
170
+ "No tool executor configured on client"
171
+ );
172
+ }
173
+ break;
174
+
175
+ case "tool_result":
176
+ this.callbacks.onToolResult?.(
177
+ message.name as string,
178
+ message.call_id as string,
179
+ message.result
180
+ );
181
+ break;
182
+
183
+ case "tool_error":
184
+ this.callbacks.onToolError?.(
185
+ message.name as string,
186
+ message.call_id as string,
187
+ message.error as string
188
+ );
189
+ break;
190
+
191
+ case "assistant_message":
192
+ this.callbacks.onAssistantMessage?.(message.content as string);
193
+ break;
194
+
195
+ case "error":
196
+ this.callbacks.onError?.(new Error(message.message as string));
197
+ break;
198
+
199
+ case "done":
200
+ // Only call onDone once (prevents duplicate with ws.onclose)
201
+ if (!this.doneCalled) {
202
+ this.doneCalled = true;
203
+ // Pass the message history if provided by the backend
204
+ const messages = message.messages as HistoryMessage[] | undefined;
205
+ console.log("[DEBUG] Done event received, messages:", messages?.length, "roles:", messages?.map(m => m.role));
206
+ this.callbacks.onDone?.(messages);
207
+ }
208
+ if (this.doneResolver) {
209
+ this.doneResolver();
210
+ this.doneResolver = null;
211
+ }
212
+ break;
213
+
214
+ case "ping":
215
+ this.send({ type: "pong" });
216
+ break;
217
+
218
+ case "budget_warning":
219
+ // Could add a callback for this if needed
220
+ console.warn("Budget warning:", message.warnings);
221
+ break;
222
+ }
223
+ }
224
+
225
+ private send(message: WebSocketMessage): void {
226
+ if (this.ws?.readyState === WebSocket.OPEN) {
227
+ this.ws.send(JSON.stringify(message));
228
+ }
229
+ }
230
+
231
+ private sendToolResult(callId: string, result: string | null, error?: string): void {
232
+ this.send({
233
+ type: "tool_result",
234
+ call_id: callId,
235
+ result: result || "",
236
+ ...(error && { error }),
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Send a chat message and wait for completion.
242
+ * The onToolCall callback will be invoked for client-side tool execution.
243
+ */
244
+ async chat(
245
+ message: string,
246
+ callbacks: WebSocketClientCallbacks,
247
+ options: ChatOptions = {}
248
+ ): Promise<void> {
249
+ if (this.state !== "connected") {
250
+ await this.connect();
251
+ }
252
+
253
+ this.callbacks = callbacks;
254
+ this.doneCalled = false; // Reset for new chat
255
+
256
+ // Create promise that resolves when "done" is received
257
+ this.donePromise = new Promise((resolve) => {
258
+ this.doneResolver = resolve;
259
+ });
260
+
261
+ // Send chat message
262
+ this.send({
263
+ type: "chat",
264
+ message,
265
+ client_type: "cli",
266
+ history: options.history || [],
267
+ file_references: options.fileReferences || [],
268
+ config: options.config,
269
+ });
270
+
271
+ // Wait for completion
272
+ await this.donePromise;
273
+ }
274
+
275
+ /**
276
+ * Disconnect from the WebSocket server.
277
+ */
278
+ disconnect(): void {
279
+ if (this.ws) {
280
+ this.ws.close();
281
+ this.ws = null;
282
+ }
283
+ this.state = "disconnected";
284
+ }
285
+
286
+ /**
287
+ * Check if currently connected.
288
+ */
289
+ isConnected(): boolean {
290
+ return this.state === "connected";
291
+ }
292
+
293
+ /**
294
+ * Check server health via HTTP (WebSocket doesn't have health check).
295
+ */
296
+ async health(): Promise<boolean> {
297
+ try {
298
+ const httpUrl = this.baseUrl
299
+ .replace(/^wss:/, "https:")
300
+ .replace(/^ws:/, "http:");
301
+ const response = await fetch(`${httpUrl}/health`);
302
+ return response.ok;
303
+ } catch {
304
+ return false;
305
+ }
306
+ }
307
+
308
+ /**
309
+ * List available tools from the server.
310
+ */
311
+ async listTools(): Promise<Array<{ name: string; description: string }>> {
312
+ const httpUrl = this.baseUrl
313
+ .replace(/^wss:/, "https:")
314
+ .replace(/^ws:/, "http:");
315
+ const url = `${httpUrl}/mle-agent/tools?client_type=cli`;
316
+
317
+ const headers: Record<string, string> = {
318
+ "Content-Type": "application/json",
319
+ };
320
+
321
+ if (this.apiKey) {
322
+ headers["X-API-Key"] = this.apiKey;
323
+ }
324
+
325
+ const response = await fetch(url, { headers });
326
+ if (!response.ok) {
327
+ throw new Error(`Failed to list tools: ${response.status}`);
328
+ }
329
+
330
+ const data = await response.json();
331
+ return data.tools || [];
332
+ }
333
+ }
@@ -0,0 +1,2 @@
1
+ export { WebSocketClient } from "./WebSocketClient.js";
2
+ export { ToolExecutor } from "./ToolExecutor.js";