gx402 1.4.0 → 1.5.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.
- package/package.json +1 -1
- package/src/index.ts +3 -2
- package/src/loop.ts +342 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -31,5 +31,6 @@ export { generateRequestId } from './utils';
|
|
|
31
31
|
// Gemini multimodal
|
|
32
32
|
export { gemini, generateImage, generateVideo, generateMusic, deepResearch } from './gemini/multimodal';
|
|
33
33
|
|
|
34
|
-
// Loop Agent
|
|
35
|
-
|
|
34
|
+
// Loop Agent
|
|
35
|
+
export { LoopAgent } from './loop';
|
|
36
|
+
export type { LoopConfig, LoopOutcome, LoopState, LoopEvent, LoopEventCallback, LoopResult, ToolCall, ToolResult, OutcomeResult } from './loop';
|
package/src/loop.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoopAgent — Self-healing agentic loop
|
|
3
|
+
*
|
|
4
|
+
* Iteratively calls an LLM with tool access (write_file, exec, read_file)
|
|
5
|
+
* and checks user-defined outcome predicates after each iteration.
|
|
6
|
+
* Stops when all outcomes are met or maxIterations is reached.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const agent = new LoopAgent({ llm, outcomes: [...], maxIterations: 10 })
|
|
10
|
+
* const result = await agent.execute("Build a script that...", onEvent)
|
|
11
|
+
*/
|
|
12
|
+
import { callLLM } from './inference';
|
|
13
|
+
import type { LLMType } from './types';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
|
|
17
|
+
// ─── Types ──────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface LoopOutcome {
|
|
20
|
+
description: string;
|
|
21
|
+
validate: (state: LoopState) => Promise<{ met: boolean; reason: string }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LoopConfig {
|
|
25
|
+
llm: LLMType | string;
|
|
26
|
+
systemPrompt?: string;
|
|
27
|
+
cwd?: string;
|
|
28
|
+
maxIterations?: number;
|
|
29
|
+
confidenceThreshold?: number;
|
|
30
|
+
temperature?: number;
|
|
31
|
+
maxTokens?: number;
|
|
32
|
+
outcomes: LoopOutcome[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ToolCall {
|
|
36
|
+
tool: string;
|
|
37
|
+
params: Record<string, any>;
|
|
38
|
+
result?: ToolResult;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ToolResult {
|
|
42
|
+
success: boolean;
|
|
43
|
+
output?: string;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LoopState {
|
|
48
|
+
iteration: number;
|
|
49
|
+
toolHistory: ToolCall[];
|
|
50
|
+
outcomeResults: OutcomeResult[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface OutcomeResult {
|
|
54
|
+
outcome: string;
|
|
55
|
+
met: boolean;
|
|
56
|
+
confidence: number;
|
|
57
|
+
reason: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type LoopEvent =
|
|
61
|
+
| { type: 'iteration_start'; iteration: number }
|
|
62
|
+
| { type: 'tool_start'; tool: string; params: Record<string, any> }
|
|
63
|
+
| { type: 'tool_result'; tool: string; result: ToolResult }
|
|
64
|
+
| { type: 'outcome_check'; outcomes: OutcomeResult[] }
|
|
65
|
+
| { type: 'complete'; iteration: number; totalElapsedMs: number }
|
|
66
|
+
| { type: 'max_iterations_reached'; iteration: number }
|
|
67
|
+
| { type: 'error'; error: string };
|
|
68
|
+
|
|
69
|
+
export type LoopEventCallback = (event: LoopEvent) => void;
|
|
70
|
+
|
|
71
|
+
export interface LoopResult {
|
|
72
|
+
success: boolean;
|
|
73
|
+
iterations: number;
|
|
74
|
+
elapsedMs: number;
|
|
75
|
+
state: LoopState;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Built-in Tools ─────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function executeWriteFile(params: { path: string; content: string }, cwd: string): ToolResult {
|
|
81
|
+
try {
|
|
82
|
+
const filePath = path.isAbsolute(params.path)
|
|
83
|
+
? params.path
|
|
84
|
+
: path.resolve(cwd, params.path);
|
|
85
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
86
|
+
fs.writeFileSync(filePath, params.content, 'utf-8');
|
|
87
|
+
return { success: true, output: `Wrote ${params.content.length} bytes to ${filePath}` };
|
|
88
|
+
} catch (e: any) {
|
|
89
|
+
return { success: false, error: e.message };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function executeReadFile(params: { path: string }, cwd: string): ToolResult {
|
|
94
|
+
try {
|
|
95
|
+
const filePath = path.isAbsolute(params.path)
|
|
96
|
+
? params.path
|
|
97
|
+
: path.resolve(cwd, params.path);
|
|
98
|
+
if (!fs.existsSync(filePath)) {
|
|
99
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
100
|
+
}
|
|
101
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
102
|
+
return { success: true, output: content.substring(0, 10000) };
|
|
103
|
+
} catch (e: any) {
|
|
104
|
+
return { success: false, error: e.message };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function executeCommand(params: { command: string }, cwd: string): Promise<ToolResult> {
|
|
109
|
+
try {
|
|
110
|
+
const proc = Bun.spawn(['sh', '-c', params.command], {
|
|
111
|
+
cwd,
|
|
112
|
+
stdout: 'pipe',
|
|
113
|
+
stderr: 'pipe',
|
|
114
|
+
});
|
|
115
|
+
const stdout = await new Response(proc.stdout).text();
|
|
116
|
+
const stderr = await new Response(proc.stderr).text();
|
|
117
|
+
const exitCode = await proc.exited;
|
|
118
|
+
const output = (stdout + (stderr ? `\n[stderr] ${stderr}` : '')).substring(0, 10000);
|
|
119
|
+
return {
|
|
120
|
+
success: exitCode === 0,
|
|
121
|
+
output,
|
|
122
|
+
error: exitCode !== 0 ? `Exit code: ${exitCode}` : undefined,
|
|
123
|
+
};
|
|
124
|
+
} catch (e: any) {
|
|
125
|
+
// Fallback for Windows (no sh)
|
|
126
|
+
try {
|
|
127
|
+
const proc = Bun.spawn(['cmd', '/c', params.command], {
|
|
128
|
+
cwd,
|
|
129
|
+
stdout: 'pipe',
|
|
130
|
+
stderr: 'pipe',
|
|
131
|
+
});
|
|
132
|
+
const stdout = await new Response(proc.stdout).text();
|
|
133
|
+
const stderr = await new Response(proc.stderr).text();
|
|
134
|
+
const exitCode = await proc.exited;
|
|
135
|
+
const output = (stdout + (stderr ? `\n[stderr] ${stderr}` : '')).substring(0, 10000);
|
|
136
|
+
return {
|
|
137
|
+
success: exitCode === 0,
|
|
138
|
+
output,
|
|
139
|
+
error: exitCode !== 0 ? `Exit code: ${exitCode}` : undefined,
|
|
140
|
+
};
|
|
141
|
+
} catch (e2: any) {
|
|
142
|
+
return { success: false, error: e2.message };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function executeTool(name: string, params: Record<string, any>, cwd: string): Promise<ToolResult> {
|
|
148
|
+
switch (name) {
|
|
149
|
+
case 'write_file': return executeWriteFile(params as any, cwd);
|
|
150
|
+
case 'read_file': return executeReadFile(params as any, cwd);
|
|
151
|
+
case 'exec': return executeCommand(params as any, cwd);
|
|
152
|
+
default: return { success: false, error: `Unknown tool: ${name}` };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── LLM Prompt ─────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
const TOOL_DOCS = `You have the following tools:
|
|
159
|
+
|
|
160
|
+
1. write_file(path, content) — Write content to a file. Creates directories automatically.
|
|
161
|
+
2. read_file(path) — Read a file's content.
|
|
162
|
+
3. exec(command) — Run a shell command and get stdout/stderr.
|
|
163
|
+
|
|
164
|
+
To use a tool, respond with a JSON block inside <tool_call> tags:
|
|
165
|
+
<tool_call>
|
|
166
|
+
{"tool": "write_file", "params": {"path": "output/hello.ts", "content": "console.log('hello')"}}
|
|
167
|
+
</tool_call>
|
|
168
|
+
|
|
169
|
+
You can make multiple tool calls in one response. Each must be in its own <tool_call> block.
|
|
170
|
+
After tool results are returned, analyze them and decide if you need more actions.`;
|
|
171
|
+
|
|
172
|
+
function buildSystemPrompt(config: LoopConfig): string {
|
|
173
|
+
const parts = [
|
|
174
|
+
config.systemPrompt || 'You are a skilled developer. Complete the given task step by step.',
|
|
175
|
+
'',
|
|
176
|
+
TOOL_DOCS,
|
|
177
|
+
'',
|
|
178
|
+
'## Outcomes to achieve:',
|
|
179
|
+
...config.outcomes.map((o, i) => `${i + 1}. ${o.description}`),
|
|
180
|
+
'',
|
|
181
|
+
'Work iteratively. Use tools to make progress, then check results. Fix any errors you encounter.',
|
|
182
|
+
];
|
|
183
|
+
return parts.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildIterationPrompt(state: LoopState, task: string): string {
|
|
187
|
+
const parts = [task];
|
|
188
|
+
|
|
189
|
+
if (state.toolHistory.length > 0) {
|
|
190
|
+
parts.push('\n\n## Previous tool results:');
|
|
191
|
+
for (const call of state.toolHistory.slice(-10)) { // Last 10 calls
|
|
192
|
+
parts.push(`\n[${call.tool}] ${JSON.stringify(call.params)}`);
|
|
193
|
+
if (call.result) {
|
|
194
|
+
parts.push(call.result.success
|
|
195
|
+
? `✅ ${call.result.output?.substring(0, 500) || 'OK'}`
|
|
196
|
+
: `❌ ${call.result.error || 'Failed'}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (state.outcomeResults.length > 0) {
|
|
202
|
+
parts.push('\n\n## Outcome status:');
|
|
203
|
+
for (const o of state.outcomeResults) {
|
|
204
|
+
parts.push(`${o.met ? '✅' : '❌'} ${o.outcome}: ${o.reason}`);
|
|
205
|
+
}
|
|
206
|
+
parts.push('\nFix any unmet outcomes.');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return parts.join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Tool Call Parser ───────────────────────────────────
|
|
213
|
+
|
|
214
|
+
function parseToolCalls(response: string): ToolCall[] {
|
|
215
|
+
const calls: ToolCall[] = [];
|
|
216
|
+
const regex = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
|
|
217
|
+
let match;
|
|
218
|
+
while ((match = regex.exec(response)) !== null) {
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(match[1]!);
|
|
221
|
+
if (parsed.tool && parsed.params) {
|
|
222
|
+
calls.push({ tool: parsed.tool, params: parsed.params });
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Skip malformed tool calls
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return calls;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── LoopAgent ──────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
export class LoopAgent {
|
|
234
|
+
private config: LoopConfig;
|
|
235
|
+
|
|
236
|
+
constructor(config: LoopConfig) {
|
|
237
|
+
if (!config.outcomes?.length) {
|
|
238
|
+
throw new Error('LoopAgent requires at least one outcome');
|
|
239
|
+
}
|
|
240
|
+
this.config = {
|
|
241
|
+
maxIterations: 10,
|
|
242
|
+
confidenceThreshold: 0.9,
|
|
243
|
+
temperature: 0.3,
|
|
244
|
+
maxTokens: 8000,
|
|
245
|
+
...config,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async execute(task: string, onEvent?: LoopEventCallback): Promise<LoopResult> {
|
|
250
|
+
const startTime = Date.now();
|
|
251
|
+
const cwd = this.config.cwd || process.cwd();
|
|
252
|
+
const state: LoopState = {
|
|
253
|
+
iteration: 0,
|
|
254
|
+
toolHistory: [],
|
|
255
|
+
outcomeResults: [],
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const systemPrompt = buildSystemPrompt(this.config);
|
|
259
|
+
const maxIter = this.config.maxIterations!;
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < maxIter; i++) {
|
|
262
|
+
state.iteration = i;
|
|
263
|
+
onEvent?.({ type: 'iteration_start', iteration: i });
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// 1. Call LLM
|
|
267
|
+
const userPrompt = buildIterationPrompt(state, task);
|
|
268
|
+
const response = await callLLM(
|
|
269
|
+
this.config.llm as string,
|
|
270
|
+
[
|
|
271
|
+
{ role: 'system', content: systemPrompt },
|
|
272
|
+
{ role: 'user', content: userPrompt },
|
|
273
|
+
],
|
|
274
|
+
{
|
|
275
|
+
temperature: this.config.temperature,
|
|
276
|
+
maxTokens: this.config.maxTokens,
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// 2. Parse tool calls from response
|
|
281
|
+
const toolCalls = parseToolCalls(response);
|
|
282
|
+
|
|
283
|
+
// 3. Execute each tool call
|
|
284
|
+
for (const call of toolCalls) {
|
|
285
|
+
onEvent?.({ type: 'tool_start', tool: call.tool, params: call.params });
|
|
286
|
+
call.result = await executeTool(call.tool, call.params, cwd);
|
|
287
|
+
state.toolHistory.push(call);
|
|
288
|
+
onEvent?.({ type: 'tool_result', tool: call.tool, result: call.result });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 4. Check outcomes
|
|
292
|
+
state.outcomeResults = await this.checkOutcomes(state);
|
|
293
|
+
onEvent?.({ type: 'outcome_check', outcomes: state.outcomeResults });
|
|
294
|
+
|
|
295
|
+
// 5. All met?
|
|
296
|
+
const allMet = state.outcomeResults.every(o => o.met);
|
|
297
|
+
if (allMet) {
|
|
298
|
+
const elapsed = Date.now() - startTime;
|
|
299
|
+
onEvent?.({ type: 'complete', iteration: i, totalElapsedMs: elapsed });
|
|
300
|
+
return { success: true, iterations: i + 1, elapsedMs: elapsed, state };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 6. No tool calls and no progress? Bail to avoid infinite no-ops
|
|
304
|
+
if (toolCalls.length === 0 && i > 0) {
|
|
305
|
+
// LLM didn't produce any tool calls — might be stuck
|
|
306
|
+
// Give it one more chance with a nudge
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
} catch (err: any) {
|
|
310
|
+
onEvent?.({ type: 'error', error: err.message });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Max iterations reached
|
|
315
|
+
const elapsed = Date.now() - startTime;
|
|
316
|
+
onEvent?.({ type: 'max_iterations_reached', iteration: maxIter });
|
|
317
|
+
return { success: false, iterations: maxIter, elapsedMs: elapsed, state };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async checkOutcomes(state: LoopState): Promise<OutcomeResult[]> {
|
|
321
|
+
const results: OutcomeResult[] = [];
|
|
322
|
+
for (const outcome of this.config.outcomes) {
|
|
323
|
+
try {
|
|
324
|
+
const { met, reason } = await outcome.validate(state);
|
|
325
|
+
results.push({
|
|
326
|
+
outcome: outcome.description,
|
|
327
|
+
met,
|
|
328
|
+
confidence: met ? 1.0 : 0.0,
|
|
329
|
+
reason,
|
|
330
|
+
});
|
|
331
|
+
} catch (err: any) {
|
|
332
|
+
results.push({
|
|
333
|
+
outcome: outcome.description,
|
|
334
|
+
met: false,
|
|
335
|
+
confidence: 0,
|
|
336
|
+
reason: `Validation error: ${err.message}`,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return results;
|
|
341
|
+
}
|
|
342
|
+
}
|