threewzrd 1.0.7 → 1.0.8
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/dist/core/AgentEngine.js +19 -3
- package/dist/tools/ToolExecutor.js +10 -5
- package/dist/ui/TerminalUI.d.ts +4 -0
- package/dist/ui/TerminalUI.js +21 -0
- package/package.json +1 -1
package/dist/core/AgentEngine.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ToolExecutor } from '../tools/ToolExecutor.js';
|
|
|
5
5
|
import { THREEJS_SYSTEM_PROMPT } from '../prompts/system.js';
|
|
6
6
|
// Limits to prevent hitting rate limits
|
|
7
7
|
const MAX_HISTORY_MESSAGES = 20; // Keep last N messages
|
|
8
|
-
const MAX_TOKENS =
|
|
8
|
+
const MAX_TOKENS = 16384; // Needs to be large enough for file contents in tool calls
|
|
9
9
|
const MAX_RETRIES = 3;
|
|
10
10
|
const RETRY_DELAY_MS = 5000; // 5 seconds base delay
|
|
11
11
|
export class AgentEngine {
|
|
@@ -93,6 +93,10 @@ export class AgentEngine {
|
|
|
93
93
|
if (!isFirstText) {
|
|
94
94
|
this.ui.endStreaming();
|
|
95
95
|
}
|
|
96
|
+
// Check for truncated response
|
|
97
|
+
if (response.stop_reason === 'max_tokens') {
|
|
98
|
+
this.ui.printWarning('Response was truncated due to length limit. Some tool calls may be incomplete.');
|
|
99
|
+
}
|
|
96
100
|
// Process all content blocks
|
|
97
101
|
for (const block of response.content) {
|
|
98
102
|
contentBlocks.push(block);
|
|
@@ -108,9 +112,15 @@ export class AgentEngine {
|
|
|
108
112
|
// No tool calls, we're done
|
|
109
113
|
return false;
|
|
110
114
|
}
|
|
115
|
+
// Show tool processing spinner
|
|
116
|
+
this.ui.startToolProcessing(toolUseBlocks.length);
|
|
111
117
|
// Execute all tool calls
|
|
112
118
|
const toolResults = [];
|
|
113
119
|
for (const toolUse of toolUseBlocks) {
|
|
120
|
+
// Update spinner with current tool name
|
|
121
|
+
this.ui.updateToolProcessing(toolUse.name);
|
|
122
|
+
// Stop spinner before tool execution (which prints its own output)
|
|
123
|
+
this.ui.stopToolProcessing();
|
|
114
124
|
const result = await this.toolExecutor.execute(toolUse.name, toolUse.input);
|
|
115
125
|
// Truncate large outputs to save tokens
|
|
116
126
|
let output = result.success ? result.output : `Error: ${result.error}`;
|
|
@@ -123,17 +133,23 @@ export class AgentEngine {
|
|
|
123
133
|
content: output,
|
|
124
134
|
is_error: !result.success,
|
|
125
135
|
});
|
|
136
|
+
// Restart spinner if more tools remain
|
|
137
|
+
const remainingTools = toolUseBlocks.length - toolResults.length;
|
|
138
|
+
if (remainingTools > 0) {
|
|
139
|
+
this.ui.startToolProcessing(remainingTools);
|
|
140
|
+
}
|
|
126
141
|
}
|
|
127
142
|
// Add tool results to history
|
|
128
143
|
this.conversationHistory.push({
|
|
129
144
|
role: 'user',
|
|
130
145
|
content: toolResults,
|
|
131
146
|
});
|
|
132
|
-
//
|
|
133
|
-
return
|
|
147
|
+
// Always continue after tool execution - model must see results
|
|
148
|
+
return true;
|
|
134
149
|
}
|
|
135
150
|
catch (error) {
|
|
136
151
|
this.ui.stopThinking();
|
|
152
|
+
this.ui.stopToolProcessing();
|
|
137
153
|
// Handle rate limit errors with retry
|
|
138
154
|
if (error instanceof Anthropic.RateLimitError) {
|
|
139
155
|
if (retryCount < MAX_RETRIES) {
|
|
@@ -58,20 +58,21 @@ export class ToolExecutor {
|
|
|
58
58
|
*/
|
|
59
59
|
validateWriteFileInput(input) {
|
|
60
60
|
if (!input || typeof input !== 'object') {
|
|
61
|
-
throw new Error(
|
|
61
|
+
throw new Error(`Invalid input: expected object, got ${typeof input}: ${JSON.stringify(input)}`);
|
|
62
62
|
}
|
|
63
63
|
const obj = input;
|
|
64
64
|
if (typeof obj.path !== 'string' || !obj.path.trim()) {
|
|
65
|
-
throw new Error(
|
|
65
|
+
throw new Error(`Invalid input: path must be a non-empty string. Received keys: ${Object.keys(obj).join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
// Content is required - reject null/undefined
|
|
68
|
+
if (obj.content === null || obj.content === undefined) {
|
|
69
|
+
throw new Error(`Invalid input: content is required. Received keys: [${Object.keys(obj).join(', ')}], content type: ${typeof obj.content}`);
|
|
66
70
|
}
|
|
67
71
|
// Coerce content to string - handle various types the model might return
|
|
68
72
|
let content;
|
|
69
73
|
if (typeof obj.content === 'string') {
|
|
70
74
|
content = obj.content;
|
|
71
75
|
}
|
|
72
|
-
else if (obj.content === null || obj.content === undefined) {
|
|
73
|
-
content = '';
|
|
74
|
-
}
|
|
75
76
|
else if (Array.isArray(obj.content)) {
|
|
76
77
|
// Model sometimes returns content as an array of strings
|
|
77
78
|
content = obj.content.map(item => String(item)).join('\n');
|
|
@@ -84,6 +85,10 @@ export class ToolExecutor {
|
|
|
84
85
|
// Fallback for numbers, booleans, etc.
|
|
85
86
|
content = String(obj.content);
|
|
86
87
|
}
|
|
88
|
+
// Reject empty content - likely a model error
|
|
89
|
+
if (!content.trim()) {
|
|
90
|
+
throw new Error('Invalid input: content cannot be empty');
|
|
91
|
+
}
|
|
87
92
|
return {
|
|
88
93
|
path: obj.path.trim(),
|
|
89
94
|
content,
|
package/dist/ui/TerminalUI.d.ts
CHANGED
|
@@ -7,9 +7,13 @@ export declare class TerminalUI {
|
|
|
7
7
|
private rl;
|
|
8
8
|
private isStreaming;
|
|
9
9
|
private thinkingSpinner;
|
|
10
|
+
private toolSpinner;
|
|
10
11
|
constructor();
|
|
11
12
|
startThinking(message?: string): void;
|
|
12
13
|
stopThinking(): void;
|
|
14
|
+
startToolProcessing(toolCount: number): void;
|
|
15
|
+
updateToolProcessing(toolName: string): void;
|
|
16
|
+
stopToolProcessing(): void;
|
|
13
17
|
confirm(message: string): Promise<boolean>;
|
|
14
18
|
select(question: string, options: SelectOption[]): Promise<string>;
|
|
15
19
|
printBanner(): void;
|
package/dist/ui/TerminalUI.js
CHANGED
|
@@ -5,6 +5,7 @@ export class TerminalUI {
|
|
|
5
5
|
rl;
|
|
6
6
|
isStreaming = false;
|
|
7
7
|
thinkingSpinner = null;
|
|
8
|
+
toolSpinner = null;
|
|
8
9
|
constructor() {
|
|
9
10
|
this.rl = readline.createInterface({
|
|
10
11
|
input: process.stdin,
|
|
@@ -29,6 +30,26 @@ export class TerminalUI {
|
|
|
29
30
|
process.stdin.resume();
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
// Tool processing spinner
|
|
34
|
+
startToolProcessing(toolCount) {
|
|
35
|
+
const plural = toolCount > 1 ? 's' : '';
|
|
36
|
+
this.toolSpinner = ora({
|
|
37
|
+
text: chalk.yellow(`Executing ${toolCount} tool${plural}...`),
|
|
38
|
+
spinner: 'dots',
|
|
39
|
+
discardStdin: false,
|
|
40
|
+
}).start();
|
|
41
|
+
}
|
|
42
|
+
updateToolProcessing(toolName) {
|
|
43
|
+
if (this.toolSpinner) {
|
|
44
|
+
this.toolSpinner.text = chalk.yellow(`Executing: ${toolName}...`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
stopToolProcessing() {
|
|
48
|
+
if (this.toolSpinner) {
|
|
49
|
+
this.toolSpinner.stop();
|
|
50
|
+
this.toolSpinner = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
32
53
|
// Confirmation prompt for dangerous actions
|
|
33
54
|
async confirm(message) {
|
|
34
55
|
// Ensure any spinner is stopped
|