@fastino-ai/pioneer-cli 0.1.0 → 0.2.0

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,342 @@
1
+ /**
2
+ * Agent - Main orchestrator that coordinates tools and LLM calls
3
+ */
4
+
5
+ import type {
6
+ Message,
7
+ Tool,
8
+ ToolCall,
9
+ ToolResult,
10
+ AgentConfig,
11
+ ConversationContext,
12
+ LLMResponse,
13
+ Budget,
14
+ } from "./types.js";
15
+ import { LLMClient } from "./LLMClient.js";
16
+ import { ToolRegistry } from "./ToolRegistry.js";
17
+ import { BudgetManager } from "./BudgetManager.js";
18
+ import { FileResolver, type FileReference } from "./FileResolver.js";
19
+
20
+ const DEFAULT_SYSTEM_PROMPT = `You are an intelligent AI assistant with access to powerful tools for coding, file manipulation, and system operations.
21
+
22
+ Available capabilities:
23
+ - Execute bash commands to interact with the system
24
+ - Read, write, and edit files
25
+ - List and search directories
26
+ - Execute code in isolated sandboxes (Python, JavaScript, TypeScript, Bash, Ruby, Go)
27
+
28
+ Guidelines:
29
+ 1. Be concise but thorough in your responses
30
+ 2. When given a task, break it down into steps and execute them
31
+ 3. Use tools to verify your work when appropriate
32
+ 4. If something fails, analyze the error and try alternative approaches
33
+ 5. Ask clarifying questions only when truly necessary
34
+ 6. Report your progress and results clearly
35
+
36
+ You are operating in the user's local environment. Be careful with destructive operations.`;
37
+
38
+ export interface AgentEvents {
39
+ onMessage?: (message: Message) => void;
40
+ onToolCall?: (toolCall: ToolCall) => void;
41
+ onToolResult?: (result: ToolResult) => void;
42
+ onStream?: (chunk: string) => void;
43
+ onBudgetWarning?: (warnings: string[]) => void;
44
+ onFileReference?: (refs: FileReference[]) => void;
45
+ onError?: (error: Error) => void;
46
+ }
47
+
48
+ export class Agent {
49
+ private llm: LLMClient;
50
+ private tools: ToolRegistry;
51
+ private budget: BudgetManager;
52
+ private config: AgentConfig;
53
+ private context: ConversationContext;
54
+ private events: AgentEvents;
55
+ private fileResolver: FileResolver;
56
+ private abortController: AbortController | null = null;
57
+
58
+ constructor(config: AgentConfig, events: AgentEvents = {}) {
59
+ this.config = config;
60
+ this.events = events;
61
+
62
+ this.llm = new LLMClient({
63
+ provider: config.provider,
64
+ model: config.model,
65
+ apiKey: config.apiKey,
66
+ baseUrl: config.baseUrl,
67
+ });
68
+
69
+ this.tools = new ToolRegistry();
70
+ this.budget = new BudgetManager(
71
+ config.budget,
72
+ this.llm.getCostPerMillion()
73
+ );
74
+ this.fileResolver = new FileResolver(process.cwd());
75
+
76
+ this.context = {
77
+ messages: [],
78
+ budgetUsage: this.budget.getUsage(),
79
+ startTime: new Date(),
80
+ workingDirectory: process.cwd(),
81
+ };
82
+ }
83
+
84
+ registerTool(tool: Tool): void {
85
+ this.tools.register(tool);
86
+ }
87
+
88
+ registerTools(tools: Tool[]): void {
89
+ for (const tool of tools) {
90
+ this.tools.register(tool);
91
+ }
92
+ }
93
+
94
+ getTools(): Tool[] {
95
+ return this.tools.getAll();
96
+ }
97
+
98
+ async chat(userMessage: string, stream = true): Promise<string> {
99
+ // Create new abort controller for this chat session
100
+ this.abortController = new AbortController();
101
+
102
+ // Check budget before starting
103
+ const budgetCheck = this.budget.checkBudget();
104
+ if (!budgetCheck.withinBudget) {
105
+ const error = `Budget exhausted: ${budgetCheck.warnings.join(", ")}`;
106
+ this.events.onError?.(new Error(error));
107
+ return error;
108
+ }
109
+
110
+ if (budgetCheck.warnings.length > 0) {
111
+ this.events.onBudgetWarning?.(budgetCheck.warnings);
112
+ }
113
+
114
+ // Resolve @file references in the message
115
+ const resolved = this.fileResolver.resolve(userMessage);
116
+
117
+ // Notify about file references
118
+ if (resolved.references.length > 0) {
119
+ this.events.onFileReference?.(resolved.references);
120
+ }
121
+
122
+ // Build the actual message content with file context
123
+ let messageContent = userMessage;
124
+ if (resolved.contextBlock) {
125
+ messageContent = resolved.contextBlock + "\n" + userMessage;
126
+ }
127
+
128
+ // Add user message to context
129
+ const userMsg: Message = {
130
+ role: "user",
131
+ content: messageContent,
132
+ timestamp: new Date(),
133
+ };
134
+ this.context.messages.push(userMsg);
135
+ this.events.onMessage?.(userMsg);
136
+
137
+ // Run the agent loop
138
+ try {
139
+ return await this.runAgentLoop(stream);
140
+ } finally {
141
+ this.abortController = null;
142
+ }
143
+ }
144
+
145
+ // Stop the current generation/tool execution
146
+ stop(): void {
147
+ if (this.abortController) {
148
+ this.abortController.abort();
149
+ }
150
+ }
151
+
152
+ // Check if agent is currently processing
153
+ isProcessing(): boolean {
154
+ return this.abortController !== null;
155
+ }
156
+
157
+ private async runAgentLoop(stream: boolean): Promise<string> {
158
+ const maxToolCalls = this.config.maxToolCalls || 25;
159
+ let toolCallCount = 0;
160
+ let finalResponse = "";
161
+
162
+ while (toolCallCount < maxToolCalls) {
163
+ // Check if aborted
164
+ if (this.abortController?.signal.aborted) {
165
+ finalResponse += "\n\n[Stopped by user]";
166
+ break;
167
+ }
168
+
169
+ // Check budget
170
+ const budgetCheck = this.budget.checkBudget();
171
+ if (!budgetCheck.withinBudget) {
172
+ finalResponse += `\n\n[Budget exhausted: ${budgetCheck.warnings.join(", ")}]`;
173
+ break;
174
+ }
175
+
176
+ if (budgetCheck.warnings.length > 0) {
177
+ this.events.onBudgetWarning?.(budgetCheck.warnings);
178
+ }
179
+
180
+ // Get LLM response
181
+ const systemPrompt = this.config.systemPrompt || DEFAULT_SYSTEM_PROMPT;
182
+
183
+ let response: LLMResponse;
184
+ try {
185
+ response = await this.llm.chat(
186
+ this.context.messages,
187
+ this.tools.getAll(),
188
+ systemPrompt,
189
+ stream ? this.events.onStream : undefined,
190
+ this.abortController?.signal
191
+ );
192
+ } catch (error) {
193
+ const err = error instanceof Error ? error : new Error(String(error));
194
+ // Don't report abort as an error
195
+ if (err.message === "Aborted") {
196
+ return finalResponse + "\n\n[Stopped by user]";
197
+ }
198
+ this.events.onError?.(err);
199
+ return `Error communicating with LLM: ${err.message}`;
200
+ }
201
+
202
+ // Record token usage
203
+ this.budget.recordUsage(response.usage);
204
+ this.budget.recordIteration();
205
+
206
+ // Add assistant message to context
207
+ const assistantMsg: Message = {
208
+ role: "assistant",
209
+ content: response.content,
210
+ toolCalls: response.toolCalls,
211
+ timestamp: new Date(),
212
+ };
213
+ this.context.messages.push(assistantMsg);
214
+ this.events.onMessage?.(assistantMsg);
215
+
216
+ // If no tool calls, we're done
217
+ if (!response.toolCalls || response.toolCalls.length === 0) {
218
+ finalResponse = response.content;
219
+ break;
220
+ }
221
+
222
+ // Execute tool calls
223
+ for (const toolCall of response.toolCalls) {
224
+ // Check if aborted before each tool call
225
+ if (this.abortController?.signal.aborted) {
226
+ return finalResponse + "\n\n[Stopped by user]";
227
+ }
228
+
229
+ this.events.onToolCall?.(toolCall);
230
+ toolCallCount++;
231
+
232
+ const result = await this.executeToolCall(toolCall);
233
+ this.events.onToolResult?.(result);
234
+
235
+ // Add tool result to context
236
+ const toolMsg: Message = {
237
+ role: "tool",
238
+ content: result.output || result.error || "(no output)",
239
+ toolCallId: toolCall.id,
240
+ timestamp: new Date(),
241
+ };
242
+ this.context.messages.push(toolMsg);
243
+ }
244
+
245
+ // If we hit max tool calls, break
246
+ if (toolCallCount >= maxToolCalls) {
247
+ finalResponse += `\n\n[Reached maximum tool call limit: ${maxToolCalls}]`;
248
+ break;
249
+ }
250
+ }
251
+
252
+ return finalResponse;
253
+ }
254
+
255
+ private async executeToolCall(toolCall: ToolCall): Promise<ToolResult> {
256
+ const result = await this.tools.execute(toolCall.name, toolCall.arguments);
257
+ result.toolCallId = toolCall.id;
258
+ return result;
259
+ }
260
+
261
+ // Get current conversation
262
+ getMessages(): Message[] {
263
+ return [...this.context.messages];
264
+ }
265
+
266
+ // Clear conversation history
267
+ clearHistory(): void {
268
+ this.context.messages = [];
269
+ this.budget.reset();
270
+ this.context.startTime = new Date();
271
+ }
272
+
273
+ // Get budget status
274
+ getBudgetStatus(): {
275
+ usage: ReturnType<BudgetManager["getUsage"]>;
276
+ check: ReturnType<BudgetManager["checkBudget"]>;
277
+ summary: string;
278
+ } {
279
+ return {
280
+ usage: this.budget.getUsage(),
281
+ check: this.budget.checkBudget(),
282
+ summary: this.budget.formatUsageSummary(),
283
+ };
284
+ }
285
+
286
+ // Update budget
287
+ updateBudget(budget: Partial<Budget>): void {
288
+ this.budget.updateBudget(budget);
289
+ }
290
+
291
+ // Update max tool calls
292
+ setMaxToolCalls(limit: number): void {
293
+ this.config.maxToolCalls = limit;
294
+ }
295
+
296
+ // Get max tool calls
297
+ getMaxToolCalls(): number {
298
+ return this.config.maxToolCalls || 50;
299
+ }
300
+
301
+ // Get current model info
302
+ getModelInfo(): { provider: string; model: string } {
303
+ return {
304
+ provider: this.config.provider,
305
+ model: this.llm.getModel(),
306
+ };
307
+ }
308
+
309
+ // Set model
310
+ setModel(model: string): void {
311
+ this.config.model = model;
312
+ this.llm.setModel(model);
313
+ // Update cost estimation for new model
314
+ this.budget.setCostPerMillion(this.llm.getCostPerMillion());
315
+ }
316
+
317
+ // Set working directory
318
+ setWorkingDirectory(dir: string): void {
319
+ this.context.workingDirectory = dir;
320
+ this.fileResolver.setBasePath(dir);
321
+ }
322
+
323
+ // Get working directory
324
+ getWorkingDirectory(): string {
325
+ return this.context.workingDirectory;
326
+ }
327
+
328
+ // Add a system message
329
+ addSystemMessage(content: string): void {
330
+ this.context.messages.push({
331
+ role: "system",
332
+ content,
333
+ timestamp: new Date(),
334
+ });
335
+ }
336
+
337
+ // Get file suggestions for autocomplete
338
+ getFileSuggestions(partial: string): string[] {
339
+ return this.fileResolver.getSuggestions(partial);
340
+ }
341
+ }
342
+
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Budget Manager - Track and enforce token/time/cost budgets
3
+ */
4
+
5
+ import type { Budget, BudgetUsage, TokenUsage } from "./types.js";
6
+
7
+ export interface BudgetCheckResult {
8
+ withinBudget: boolean;
9
+ warnings: string[];
10
+ tokensRemaining?: number;
11
+ costRemaining?: number;
12
+ timeRemaining?: number;
13
+ iterationsRemaining?: number;
14
+ }
15
+
16
+ export class BudgetManager {
17
+ private budget: Budget;
18
+ private usage: BudgetUsage;
19
+ private startTime: number;
20
+ private costPerMillion: { input: number; output: number };
21
+
22
+ constructor(budget: Budget = {}, costPerMillion = { input: 3, output: 15 }) {
23
+ this.budget = budget;
24
+ this.costPerMillion = costPerMillion;
25
+ this.usage = {
26
+ tokensUsed: 0,
27
+ costUsed: 0,
28
+ timeUsed: 0,
29
+ iterationsUsed: 0,
30
+ };
31
+ this.startTime = Date.now();
32
+ }
33
+
34
+ recordUsage(tokenUsage: TokenUsage): void {
35
+ this.usage.tokensUsed += tokenUsage.totalTokens;
36
+ this.usage.costUsed +=
37
+ (tokenUsage.inputTokens * this.costPerMillion.input +
38
+ tokenUsage.outputTokens * this.costPerMillion.output) /
39
+ 1_000_000;
40
+ this.usage.timeUsed = (Date.now() - this.startTime) / 1000;
41
+ }
42
+
43
+ recordIteration(): void {
44
+ this.usage.iterationsUsed += 1;
45
+ }
46
+
47
+ checkBudget(): BudgetCheckResult {
48
+ const warnings: string[] = [];
49
+ let withinBudget = true;
50
+
51
+ // Update time
52
+ this.usage.timeUsed = (Date.now() - this.startTime) / 1000;
53
+
54
+ // Check tokens
55
+ if (this.budget.maxTokens !== undefined) {
56
+ const tokensRemaining = this.budget.maxTokens - this.usage.tokensUsed;
57
+ if (tokensRemaining <= 0) {
58
+ withinBudget = false;
59
+ warnings.push(`Token budget exhausted (${this.usage.tokensUsed}/${this.budget.maxTokens})`);
60
+ } else if (tokensRemaining < this.budget.maxTokens * 0.1) {
61
+ warnings.push(`Low token budget: ${tokensRemaining} remaining`);
62
+ }
63
+ }
64
+
65
+ // Check cost
66
+ if (this.budget.maxCost !== undefined) {
67
+ const costRemaining = this.budget.maxCost - this.usage.costUsed;
68
+ if (costRemaining <= 0) {
69
+ withinBudget = false;
70
+ warnings.push(`Cost budget exhausted ($${this.usage.costUsed.toFixed(4)}/$${this.budget.maxCost})`);
71
+ } else if (costRemaining < this.budget.maxCost * 0.1) {
72
+ warnings.push(`Low cost budget: $${costRemaining.toFixed(4)} remaining`);
73
+ }
74
+ }
75
+
76
+ // Check time
77
+ if (this.budget.maxTime !== undefined) {
78
+ const timeRemaining = this.budget.maxTime - this.usage.timeUsed;
79
+ if (timeRemaining <= 0) {
80
+ withinBudget = false;
81
+ warnings.push(`Time budget exhausted (${this.usage.timeUsed.toFixed(0)}s/${this.budget.maxTime}s)`);
82
+ } else if (timeRemaining < this.budget.maxTime * 0.1) {
83
+ warnings.push(`Low time budget: ${timeRemaining.toFixed(0)}s remaining`);
84
+ }
85
+ }
86
+
87
+ // Check iterations
88
+ if (this.budget.maxIterations !== undefined) {
89
+ const iterationsRemaining = this.budget.maxIterations - this.usage.iterationsUsed;
90
+ if (iterationsRemaining <= 0) {
91
+ withinBudget = false;
92
+ warnings.push(`Iteration limit reached (${this.usage.iterationsUsed}/${this.budget.maxIterations})`);
93
+ }
94
+ }
95
+
96
+ return {
97
+ withinBudget,
98
+ warnings,
99
+ tokensRemaining: this.budget.maxTokens
100
+ ? Math.max(0, this.budget.maxTokens - this.usage.tokensUsed)
101
+ : undefined,
102
+ costRemaining: this.budget.maxCost
103
+ ? Math.max(0, this.budget.maxCost - this.usage.costUsed)
104
+ : undefined,
105
+ timeRemaining: this.budget.maxTime
106
+ ? Math.max(0, this.budget.maxTime - this.usage.timeUsed)
107
+ : undefined,
108
+ iterationsRemaining: this.budget.maxIterations
109
+ ? Math.max(0, this.budget.maxIterations - this.usage.iterationsUsed)
110
+ : undefined,
111
+ };
112
+ }
113
+
114
+ getUsage(): BudgetUsage {
115
+ this.usage.timeUsed = (Date.now() - this.startTime) / 1000;
116
+ return { ...this.usage };
117
+ }
118
+
119
+ getBudget(): Budget {
120
+ return { ...this.budget };
121
+ }
122
+
123
+ formatUsageSummary(): string {
124
+ const usage = this.getUsage();
125
+ const parts: string[] = [];
126
+
127
+ parts.push(`Tokens: ${usage.tokensUsed.toLocaleString()}`);
128
+ if (this.budget.maxTokens) {
129
+ parts[parts.length - 1] += `/${this.budget.maxTokens.toLocaleString()}`;
130
+ }
131
+
132
+ parts.push(`Cost: $${usage.costUsed.toFixed(4)}`);
133
+ if (this.budget.maxCost) {
134
+ parts[parts.length - 1] += `/$${this.budget.maxCost.toFixed(2)}`;
135
+ }
136
+
137
+ parts.push(`Time: ${usage.timeUsed.toFixed(1)}s`);
138
+ if (this.budget.maxTime) {
139
+ parts[parts.length - 1] += `/${this.budget.maxTime}s`;
140
+ }
141
+
142
+ if (this.budget.maxIterations) {
143
+ parts.push(`Iterations: ${usage.iterationsUsed}/${this.budget.maxIterations}`);
144
+ }
145
+
146
+ return parts.join(" | ");
147
+ }
148
+
149
+ reset(): void {
150
+ this.usage = {
151
+ tokensUsed: 0,
152
+ costUsed: 0,
153
+ timeUsed: 0,
154
+ iterationsUsed: 0,
155
+ };
156
+ this.startTime = Date.now();
157
+ }
158
+
159
+ updateBudget(budget: Partial<Budget>): void {
160
+ this.budget = { ...this.budget, ...budget };
161
+ }
162
+
163
+ setCostPerMillion(costs: { input: number; output: number }): void {
164
+ this.costPerMillion = costs;
165
+ }
166
+ }
167
+