openaxies 1.0.4 → 1.1.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/App.js +91 -19
- package/src/agent/prompt.js +46 -0
- package/src/agent/tools.js +129 -0
- package/src/providers/index.js +97 -62
- package/src/providers/websearch.js +1 -1
package/package.json
CHANGED
package/src/App.js
CHANGED
|
@@ -44,10 +44,16 @@ export default function AppRoot() {
|
|
|
44
44
|
const [ovIdx, setOvIdx] = React.useState(0);
|
|
45
45
|
const [showT, setShowT] = React.useState(true);
|
|
46
46
|
const [tool, setTool] = React.useState(null);
|
|
47
|
+
const [pendingQ, setPendingQ] = React.useState(false);
|
|
48
|
+
const [qText, setQText] = React.useState('');
|
|
49
|
+
const [todos, setTodos] = React.useState([]);
|
|
50
|
+
const [toolCalls, setToolCalls] = React.useState([]);
|
|
47
51
|
|
|
48
52
|
const acRef = React.useRef(null);
|
|
49
53
|
const accRef = React.useRef('');
|
|
50
54
|
const lastRef = React.useRef('');
|
|
55
|
+
const qResolveRef = React.useRef(null);
|
|
56
|
+
const genRef = React.useRef(null);
|
|
51
57
|
|
|
52
58
|
React.useEffect(() => {
|
|
53
59
|
if (!think) return;
|
|
@@ -67,7 +73,7 @@ export default function AppRoot() {
|
|
|
67
73
|
const hd = function (s) { return R + ' ' + s + ' '.repeat(Math.max(0, W - s.length)) + ' ' + R; };
|
|
68
74
|
return h(Box, { flexShrink: 0, flexDirection: 'column', paddingLeft: 1, paddingRight: 1 },
|
|
69
75
|
h(Text, { color: '#555577' }, '\u250C' + D.repeat(W + 2) + '\u2510'),
|
|
70
|
-
h(Text, { color: '#555577' }, hd('\u203A_ OpenAxies (v1.0
|
|
76
|
+
h(Text, { color: '#555577' }, hd('\u203A_ OpenAxies (v1.1.0)')),
|
|
71
77
|
h(Text, { color: '#555577' }, hd('')),
|
|
72
78
|
h(Text, { color: '#555577' }, hd('model: ' + label + ' /model to change')),
|
|
73
79
|
h(Text, { color: '#555577' }, hd('directory: ~')),
|
|
@@ -80,6 +86,7 @@ export default function AppRoot() {
|
|
|
80
86
|
if (!t || busy) return;
|
|
81
87
|
setReady(true); lastRef.current = t; accRef.current = '';
|
|
82
88
|
setBusy(true); setStream(''); setThinkTxt(''); setThinkSec(0); setTool(null);
|
|
89
|
+
setToolCalls([]);
|
|
83
90
|
setMsgs(p => [...p, { role: 'user', c: t }]);
|
|
84
91
|
setInp('');
|
|
85
92
|
|
|
@@ -93,12 +100,43 @@ export default function AppRoot() {
|
|
|
93
100
|
const hist = msgs.map(x => ({ role: x.role === 'user' ? 'user' : 'assistant', content: x.c || '' }));
|
|
94
101
|
hist.push({ role: 'user', content: t });
|
|
95
102
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
const gen = callModel({ id: model }, hist, ac.signal);
|
|
104
|
+
genRef.current = gen;
|
|
105
|
+
let result = await gen.next();
|
|
106
|
+
|
|
107
|
+
while (!result.done && !ac.signal.aborted) {
|
|
108
|
+
const ev = result.value;
|
|
109
|
+
if (ev.type === 'question') {
|
|
110
|
+
setPendingQ(true); setQText(ev.text);
|
|
111
|
+
const answer = await new Promise(resolve => { qResolveRef.current = resolve; });
|
|
112
|
+
setPendingQ(false); setQText('');
|
|
113
|
+
result = await gen.next(answer);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ev.type === 'tool_call') {
|
|
117
|
+
setToolCalls(p => [...p, { tool: ev.tool, args: ev.args }]);
|
|
118
|
+
setTool({ tool: ev.tool, query: ev.args?.query || '', sites: 0 });
|
|
119
|
+
result = await gen.next();
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (ev.type === 'tool_result') {
|
|
123
|
+
setToolCalls(p => [...p, { tool: ev.tool, result: ev.result, done: true }]);
|
|
124
|
+
result = await gen.next();
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (ev.type === 'todo_add') {
|
|
128
|
+
setTodos(p => [...p, { text: ev.text, status: 'pending' }]);
|
|
129
|
+
result = await gen.next();
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ev.type === 'todo_done') {
|
|
133
|
+
setTodos(p => p.map((td, i) => i === ev.id ? { ...td, status: 'done' } : td));
|
|
134
|
+
result = await gen.next();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ev.type === 'token') {
|
|
100
138
|
const c = typeof ev.content === 'string' ? ev.content : '';
|
|
101
|
-
if (!c) continue;
|
|
139
|
+
if (!c) { result = await gen.next(); continue; }
|
|
102
140
|
if (c.includes('<think>')) op = true;
|
|
103
141
|
accRef.current += c;
|
|
104
142
|
const cl = strip(accRef.current);
|
|
@@ -107,16 +145,20 @@ export default function AppRoot() {
|
|
|
107
145
|
if (st) { setThink(true); setThinkTxt(cl); setStream(''); }
|
|
108
146
|
else { op = false; setThink(false); setStream(cl); setThinkTxt(''); }
|
|
109
147
|
} else setStream(cl);
|
|
148
|
+
result = await gen.next();
|
|
149
|
+
continue;
|
|
110
150
|
}
|
|
111
151
|
if (ev.type === 'done' || ev.type === 'timeout') {
|
|
112
152
|
if (ev.type === 'timeout' && accRef.current) accRef.current += '\n\u2014timed out\u2014';
|
|
113
153
|
break;
|
|
114
154
|
}
|
|
155
|
+
result = await gen.next();
|
|
115
156
|
}
|
|
116
157
|
} catch (e) {
|
|
117
158
|
if (!ac.signal.aborted) setMsgs(p => [...p, { role: 'asst', c: 'Error: ' + (e.message || 'connection') }]);
|
|
118
159
|
} finally {
|
|
119
|
-
setBusy(false); setThink(false);
|
|
160
|
+
setBusy(false); setThink(false); setPendingQ(false);
|
|
161
|
+
acRef.current = null; genRef.current = null;
|
|
120
162
|
const f = accRef.current;
|
|
121
163
|
if (f) setMsgs(p => [...p, { role: 'asst', c: strip(f) }]);
|
|
122
164
|
setStream(''); setThinkTxt('');
|
|
@@ -131,7 +173,7 @@ export default function AppRoot() {
|
|
|
131
173
|
function cmd(item) {
|
|
132
174
|
if (!item) return;
|
|
133
175
|
if (item.t === '/exit') { exit(); return; }
|
|
134
|
-
if (item.t === '/clear') setMsgs([]);
|
|
176
|
+
if (item.t === '/clear') { setMsgs([]); setTodos([]); }
|
|
135
177
|
if (item.t === '/model') cycle();
|
|
136
178
|
if (item.t === '/thoughts') setShowT(p => !p);
|
|
137
179
|
if (item.t === '/resume') { const q = lastRef.current; if (q && !busy) submit(q); return; }
|
|
@@ -140,6 +182,13 @@ export default function AppRoot() {
|
|
|
140
182
|
}
|
|
141
183
|
|
|
142
184
|
useInput((i, k) => {
|
|
185
|
+
if (k.ctrl && (i === 'c' || i === 'C')) { exit(); return; }
|
|
186
|
+
if (k.escape && pendingQ) {
|
|
187
|
+
if (acRef.current) acRef.current.abort();
|
|
188
|
+
setPendingQ(false); setQText(''); setBusy(false); setStream(''); setThink(false);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (pendingQ) return;
|
|
143
192
|
if (overlay) {
|
|
144
193
|
if (k.escape) { setOverlay(false); setInp(''); setOvIdx(0); return; }
|
|
145
194
|
if (k.upArrow) setOvIdx(p => p > 0 ? p - 1 : CMDS.length - 1);
|
|
@@ -148,13 +197,13 @@ export default function AppRoot() {
|
|
|
148
197
|
return;
|
|
149
198
|
}
|
|
150
199
|
if (!ready && !busy) { if (k.return) { setReady(true); setInp(''); } return; }
|
|
151
|
-
if (k.escape && busy) { if (acRef.current) acRef.current.abort(); setBusy(false); setStream(''); setThink(false); return; }
|
|
200
|
+
if (k.escape && busy) { if (acRef.current) acRef.current.abort(); setBusy(false); setStream(''); setThink(false); setPendingQ(false); return; }
|
|
152
201
|
if (k.ctrl) {
|
|
153
202
|
if (i === 'c' || i === 'C') { exit(); return; }
|
|
154
203
|
if (i === 't' || i === 'T') { setShowT(p => !p); return; }
|
|
155
204
|
if (i === 'p' || i === 'P') { cycle(); return; }
|
|
156
205
|
if (i === 'r' || i === 'R') { const q = lastRef.current; if (q && !busy) submit(q); return; }
|
|
157
|
-
if (i === 'l' || i === 'L') { setMsgs([]); return; }
|
|
206
|
+
if (i === 'l' || i === 'L') { setMsgs([]); setTodos([]); return; }
|
|
158
207
|
if (i === 'k' || i === 'K') { setInp('/'); setOverlay(true); setOvIdx(0); return; }
|
|
159
208
|
}
|
|
160
209
|
});
|
|
@@ -166,7 +215,7 @@ export default function AppRoot() {
|
|
|
166
215
|
...STARTUP.map((l, i) => h(Box, { key: 'l' + i, height: 1, paddingLeft: 2 }, h(Text, { color: '#00BBFF', bold: true }, l))),
|
|
167
216
|
h(Box, { height: 1 }),
|
|
168
217
|
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#FFFFFF', bold: true }, 'OpenAxies')),
|
|
169
|
-
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#777788' }, '
|
|
218
|
+
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#777788' }, 'autonomous agent')),
|
|
170
219
|
h(Box, { height: 2 }),
|
|
171
220
|
h(Box, { height: 1, paddingLeft: 2 }, h(Text, { color: '#555577' }, 'Press Enter to begin')),
|
|
172
221
|
h(Box, { flexGrow: 1 }),
|
|
@@ -180,10 +229,16 @@ export default function AppRoot() {
|
|
|
180
229
|
if (m.role === 'user') entries.push({ t: 'user', c: m.c });
|
|
181
230
|
else if (m.role === 'asst') entries.push({ t: 'asst', c: m.c });
|
|
182
231
|
}
|
|
183
|
-
|
|
232
|
+
for (const tc of toolCalls) {
|
|
233
|
+
if (tc.done) entries.push({ t: 'tool', tool: tc.tool, result: tc.result });
|
|
234
|
+
}
|
|
235
|
+
if (tool && busy) entries.push({ t: 'tool', tool: tool.tool, q: tool.query, sites: tool.sites });
|
|
236
|
+
for (const td of todos) {
|
|
237
|
+
entries.push({ t: 'todo', text: td.text, status: td.status });
|
|
238
|
+
}
|
|
184
239
|
if (think && thinkTxt) entries.push({ t: 'thought', c: thinkTxt, sec: thinkSec.toFixed(1) + 's' });
|
|
185
240
|
if (busy && stream && !think) entries.push({ t: 'asst', c: stream });
|
|
186
|
-
|
|
241
|
+
if (pendingQ) entries.push({ t: 'question', c: qText });
|
|
187
242
|
if (overlay) entries.push({ t: '_ov', idx: ovIdx });
|
|
188
243
|
|
|
189
244
|
const maxChat = Math.max(3, rows - 7);
|
|
@@ -213,6 +268,12 @@ export default function AppRoot() {
|
|
|
213
268
|
h(Text, { color: '#88FF88' }, '\u2022 '),
|
|
214
269
|
h(Text, { color: '#FFFFFF', wrap: 'wrap' }, e.c),
|
|
215
270
|
));
|
|
271
|
+
} else if (e.t === 'todo') {
|
|
272
|
+
const mark = e.status === 'done' ? '\u2713' : '\u25A1';
|
|
273
|
+
const color = e.status === 'done' ? '#44AA44' : '#888899';
|
|
274
|
+
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
275
|
+
h(Text, { color: color }, '\u2022 ' + mark + ' ' + e.text),
|
|
276
|
+
));
|
|
216
277
|
} else if (e.t === 'thought') {
|
|
217
278
|
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
218
279
|
h(Text, { color: '#FF9F43', bold: true }, '\u2022 Thinking \u2022 ' + e.sec),
|
|
@@ -220,7 +281,12 @@ export default function AppRoot() {
|
|
|
220
281
|
if (e.c) chEls.push(h(Box, { key: k + 'c', height: 1, paddingLeft: 4 }, h(Text, { color: '#FF9F43', wrap: 'wrap' }, e.c)));
|
|
221
282
|
} else if (e.t === 'tool') {
|
|
222
283
|
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
223
|
-
h(Text, { color: '#5B5B8A' }, '\u2022 ' + (e.tool || 'tool') + (e.q ? '
|
|
284
|
+
h(Text, { color: '#5B5B8A' }, '\u2022 ' + (e.tool || 'tool') + (e.q ? ' ' + e.q : '') + (e.sites ? ' (' + e.sites + ' sites)' : '') + (e.result ? ' \u2713' : ' \u2026')),
|
|
285
|
+
));
|
|
286
|
+
} else if (e.t === 'question') {
|
|
287
|
+
chEls.push(h(Box, { key: k, height: 1, paddingLeft: 2 },
|
|
288
|
+
h(Text, { color: '#FFD700' }, '\u2753 '),
|
|
289
|
+
h(Text, { color: '#FFD700', wrap: 'wrap' }, e.c),
|
|
224
290
|
));
|
|
225
291
|
}
|
|
226
292
|
}
|
|
@@ -233,16 +299,22 @@ export default function AppRoot() {
|
|
|
233
299
|
|
|
234
300
|
// Input line
|
|
235
301
|
const inputBar = h(Box, { height: 1, flexShrink: 0, paddingLeft: 2, paddingRight: 2 },
|
|
236
|
-
h(Text, { color: '#888899' }, '\u203A '),
|
|
237
|
-
h(Text, { color: hex.greenOnline }, '\u258C'),
|
|
302
|
+
h(Text, { color: pendingQ ? '#FFD700' : '#888899' }, pendingQ ? '\u2753 ' : '\u203A '),
|
|
303
|
+
pendingQ ? h(Text, { color: '#FFFFFF' }, '\u258C') : h(Text, { color: hex.greenOnline }, '\u258C'),
|
|
238
304
|
h(Box, { width: 1 }),
|
|
239
305
|
h(TextInput, {
|
|
240
306
|
value: inp,
|
|
241
307
|
onChange: setInp,
|
|
242
|
-
onSubmit: v => {
|
|
243
|
-
|
|
308
|
+
onSubmit: v => {
|
|
309
|
+
if (pendingQ && qResolveRef.current) {
|
|
310
|
+
qResolveRef.current(v);
|
|
311
|
+
setInp('');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (typeof v === 'string' && v.trim() && !busy) submit(v);
|
|
315
|
+
},
|
|
316
|
+
placeholder: pendingQ ? '' : '',
|
|
244
317
|
focus: !overlay,
|
|
245
|
-
showCursor: true,
|
|
246
318
|
}),
|
|
247
319
|
);
|
|
248
320
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TOOL_DEFS } from './tools.js';
|
|
2
|
+
|
|
3
|
+
export function buildSystemPrompt(cwd) {
|
|
4
|
+
const toolLines = TOOL_DEFS.map(t => {
|
|
5
|
+
const args = Object.entries(t.args).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
6
|
+
return `- ${t.name}(${args}): ${t.desc}`;
|
|
7
|
+
}).join('\n');
|
|
8
|
+
|
|
9
|
+
return `You are OpenAxies, an autonomous AI coding agent running in the terminal.
|
|
10
|
+
|
|
11
|
+
You have access to tools. When you need to gather information, modify files, or interact with the system, call a tool.
|
|
12
|
+
|
|
13
|
+
TOOLS:
|
|
14
|
+
${toolLines}
|
|
15
|
+
|
|
16
|
+
IMPORTANT RULES:
|
|
17
|
+
1. When you need to use a tool, respond with EXACTLY this format (no extra text before/after):
|
|
18
|
+
Action: tool_name
|
|
19
|
+
Action Input: {"arg1": "value1", "arg2": "value2"}
|
|
20
|
+
|
|
21
|
+
2. After each tool result (Observation), decide what to do next.
|
|
22
|
+
|
|
23
|
+
3. When you have enough information to respond to the user, use:
|
|
24
|
+
Final Answer: your complete response here
|
|
25
|
+
|
|
26
|
+
4. You can call multiple tools sequentially. Each tool call is followed by an Observation.
|
|
27
|
+
|
|
28
|
+
5. Current working directory: ${cwd}
|
|
29
|
+
|
|
30
|
+
6. For coding tasks:
|
|
31
|
+
- Read files before editing them
|
|
32
|
+
- Use glob/grep to find relevant files
|
|
33
|
+
- Write clear, correct code
|
|
34
|
+
- Run bash commands to verify your work (tests, lint, etc.)
|
|
35
|
+
- Edit files with edit_file (search/replace) when possible
|
|
36
|
+
|
|
37
|
+
7. For web searches, use web_search to get current information.
|
|
38
|
+
|
|
39
|
+
8. If you need user input, use ask_question.
|
|
40
|
+
|
|
41
|
+
9. Track your progress with todo_add and todo_done.
|
|
42
|
+
|
|
43
|
+
10. Keep your Final Answer concise and helpful. Show the user what you did.
|
|
44
|
+
|
|
45
|
+
Remember: only call ONE tool at a time. Wait for the Observation before deciding next step.`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { searchWeb } from '../providers/websearch.js';
|
|
5
|
+
|
|
6
|
+
function r(s) { return typeof s === 'string' ? s : ''; }
|
|
7
|
+
|
|
8
|
+
function simpleGlob(pattern) {
|
|
9
|
+
const parts = pattern.split('/');
|
|
10
|
+
const hasStar = parts.some(p => p.includes('*'));
|
|
11
|
+
if (!hasStar) return fs.existsSync(pattern) ? [pattern] : [];
|
|
12
|
+
const root = pattern.startsWith('/') ? '/' : '.';
|
|
13
|
+
const results = [];
|
|
14
|
+
function walk(dir, idx) {
|
|
15
|
+
if (idx >= parts.length) { results.push(dir); return; }
|
|
16
|
+
const part = parts[idx];
|
|
17
|
+
if (part.includes('**')) {
|
|
18
|
+
walk(dir, idx + 1);
|
|
19
|
+
try { for (const e of fs.readdirSync(dir)) { const p = dir + '/' + e; if (fs.statSync(p).isDirectory()) walk(p, idx); } } catch {}
|
|
20
|
+
} else if (part.includes('*')) {
|
|
21
|
+
const re = new RegExp('^' + part.replace(/\*/g, '.*') + '$');
|
|
22
|
+
try { for (const e of fs.readdirSync(dir)) { if (re.test(e)) walk(dir + '/' + e, idx + 1); } } catch {}
|
|
23
|
+
} else {
|
|
24
|
+
const p = dir + '/' + part;
|
|
25
|
+
if (fs.existsSync(p)) walk(p, idx + 1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
walk(root, 0);
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const TOOL_DEFS = [
|
|
33
|
+
{ name: 'read_file', args: { path: 'string' }, desc: 'Read a file from the filesystem' },
|
|
34
|
+
{ name: 'write_file', args: { path: 'string', content: 'string' }, desc: 'Write content to a file (overwrites)' },
|
|
35
|
+
{ name: 'edit_file', args: { path: 'string', old_string: 'string', new_string: 'string' }, desc: 'Edit a file by replacing old_string with new_string' },
|
|
36
|
+
{ name: 'glob', args: { pattern: 'string' }, desc: 'Find files matching a glob pattern (e.g. "src/**/*.js")' },
|
|
37
|
+
{ name: 'grep', args: { pattern: 'string', path: 'string' }, desc: 'Search file contents with a regex pattern' },
|
|
38
|
+
{ name: 'bash', args: { command: 'string' }, desc: 'Execute a shell command and get output' },
|
|
39
|
+
{ name: 'web_search', args: { query: 'string' }, desc: 'Search the web for current information' },
|
|
40
|
+
{ name: 'ask_question', args: { question: 'string' }, desc: 'Ask the user a question when you need clarification' },
|
|
41
|
+
{ name: 'todo_add', args: { text: 'string' }, desc: 'Add a task to the todo list' },
|
|
42
|
+
{ name: 'todo_done', args: { id: 'number' }, desc: 'Mark a todo item as complete by its index (0-based)' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function parseToolCall(text) {
|
|
46
|
+
const s = r(text);
|
|
47
|
+
const m = s.match(/Action:\s*(\w+)\s*\nAction Input:\s*(\{[\s\S]*?\}|"[^"]*"|`[^`]*`|\S+)/);
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
const name = m[1];
|
|
50
|
+
let raw = m[2].trim();
|
|
51
|
+
let args = {};
|
|
52
|
+
try {
|
|
53
|
+
if (raw.startsWith('{')) args = JSON.parse(raw);
|
|
54
|
+
else if (raw.startsWith('"')) args = { query: JSON.parse(raw) };
|
|
55
|
+
else if (raw.startsWith('`')) args = { command: raw.slice(1, -1) };
|
|
56
|
+
else args = { path: raw };
|
|
57
|
+
} catch { return null; }
|
|
58
|
+
return { name, args };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function execTool(toolCall, signal, questionCb) {
|
|
62
|
+
const { name, args } = toolCall;
|
|
63
|
+
try {
|
|
64
|
+
switch (name) {
|
|
65
|
+
case 'read_file': {
|
|
66
|
+
const p = r(args.path);
|
|
67
|
+
if (!fs.existsSync(p)) return 'Error: file not found: ' + p;
|
|
68
|
+
const c = fs.readFileSync(p, 'utf8');
|
|
69
|
+
return 'File ' + p + ':\n' + c.slice(0, 8000) + (c.length > 8000 ? '\n... [truncated]' : '');
|
|
70
|
+
}
|
|
71
|
+
case 'write_file': {
|
|
72
|
+
const p = r(args.path);
|
|
73
|
+
const d = path.dirname(p);
|
|
74
|
+
if (d && !fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
75
|
+
fs.writeFileSync(p, r(args.content));
|
|
76
|
+
const lines = r(args.content).split('\n').length;
|
|
77
|
+
return 'Wrote ' + p + ' (' + lines + ' lines)';
|
|
78
|
+
}
|
|
79
|
+
case 'edit_file': {
|
|
80
|
+
const p = r(args.path);
|
|
81
|
+
if (!fs.existsSync(p)) return 'Error: file not found: ' + p;
|
|
82
|
+
let c = fs.readFileSync(p, 'utf8');
|
|
83
|
+
const old = r(args.old_string);
|
|
84
|
+
const nw = r(args.new_string);
|
|
85
|
+
if (!c.includes(old)) return 'Error: old_string not found in ' + p;
|
|
86
|
+
c = c.replace(old, nw);
|
|
87
|
+
fs.writeFileSync(p, c);
|
|
88
|
+
return 'Edited ' + p;
|
|
89
|
+
}
|
|
90
|
+
case 'glob': {
|
|
91
|
+
const results = simpleGlob(r(args.pattern));
|
|
92
|
+
return results.length > 0 ? results.join('\n') : 'No files matched: ' + r(args.pattern);
|
|
93
|
+
}
|
|
94
|
+
case 'grep': {
|
|
95
|
+
const pat = r(args.pattern).replace(/"/g, '\\"');
|
|
96
|
+
const pth = r(args.path) || '.';
|
|
97
|
+
try {
|
|
98
|
+
const out = execSync('rg -l "' + pat + '" ' + pth + ' 2>/dev/null || true', { encoding: 'utf8', maxBuffer: 1024 * 1024 });
|
|
99
|
+
return out.trim() || 'No matches for: ' + r(args.pattern);
|
|
100
|
+
} catch { return 'No matches for: ' + r(args.pattern); }
|
|
101
|
+
}
|
|
102
|
+
case 'bash': {
|
|
103
|
+
const out = execSync(r(args.command), { encoding: 'utf8', maxBuffer: 1024 * 1024, timeout: 30000 });
|
|
104
|
+
return out.slice(0, 4000) || '(no output)';
|
|
105
|
+
}
|
|
106
|
+
case 'web_search': {
|
|
107
|
+
const res = await searchWeb(r(args.query), signal);
|
|
108
|
+
return res.sites > 0 ? 'Search results for "' + r(args.query) + '":\n' + res.summary : 'No search results';
|
|
109
|
+
}
|
|
110
|
+
case 'ask_question': {
|
|
111
|
+
if (typeof questionCb === 'function') {
|
|
112
|
+
const answer = await questionCb(r(args.question));
|
|
113
|
+
return 'User answered: ' + answer;
|
|
114
|
+
}
|
|
115
|
+
return 'Error: no question handler';
|
|
116
|
+
}
|
|
117
|
+
case 'todo_add': {
|
|
118
|
+
return 'Todo added: ' + r(args.text);
|
|
119
|
+
}
|
|
120
|
+
case 'todo_done': {
|
|
121
|
+
return 'Todo ' + args.id + ' marked done';
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
return 'Error: unknown tool "' + name + '"';
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return 'Error: ' + (e.message || String(e));
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { StateGraph, START, END } from '@langchain/langgraph';
|
|
1
2
|
import { streamResponse } from './streaming.js';
|
|
2
|
-
import {
|
|
3
|
+
import { buildSystemPrompt } from '../agent/prompt.js';
|
|
4
|
+
import { parseToolCall, execTool } from '../agent/tools.js';
|
|
3
5
|
|
|
4
6
|
const MODEL_ROUTES = Object.freeze({
|
|
5
7
|
'openaxis/openaxis-llama': Object.freeze([
|
|
@@ -17,91 +19,124 @@ const MODEL_ROUTES = Object.freeze({
|
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
function checkMessages(messages) {
|
|
20
|
-
if (Array.isArray(messages)
|
|
21
|
-
throw new Error('messages must be an array');
|
|
22
|
-
}
|
|
22
|
+
if (!Array.isArray(messages)) throw new Error('messages must be an array');
|
|
23
23
|
for (let i = 0; i < messages.length; i++) {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (typeof msg.role !== 'string' || msg.role.length === 0) {
|
|
29
|
-
throw new Error('message at index ' + i + ' must have a non-empty role');
|
|
30
|
-
}
|
|
31
|
-
if (typeof msg.content !== 'string') {
|
|
32
|
-
throw new Error('message at index ' + i + ' content must be a string');
|
|
33
|
-
}
|
|
24
|
+
const m = messages[i];
|
|
25
|
+
if (!m || typeof m !== 'object') throw new Error('message at ' + i + ' must be an object');
|
|
26
|
+
if (typeof m.role !== 'string' || !m.role) throw new Error('message at ' + i + ' must have non-empty role');
|
|
27
|
+
if (typeof m.content !== 'string') throw new Error('message at ' + i + ' content must be string');
|
|
34
28
|
}
|
|
35
29
|
return messages;
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const endpoints = MODEL_ROUTES[modelConfig.id];
|
|
49
|
-
if (endpoints === null || endpoints === undefined) {
|
|
50
|
-
throw new Error('No endpoints configured for model: ' + modelConfig.id);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const modelShort = modelConfig.id.replace(/^[^/]+\//, '');
|
|
54
|
-
let modelMessages = messages;
|
|
55
|
-
let webUsed = false;
|
|
56
|
-
let webSites = 0;
|
|
57
|
-
let webQuery = '';
|
|
32
|
+
function buildRouterGraph() {
|
|
33
|
+
const graph = new StateGraph({
|
|
34
|
+
channels: {
|
|
35
|
+
lastResponse: { reducer: (a, b) => b ?? a, default: () => '' },
|
|
36
|
+
iteration: { reducer: (a, b) => b ?? a, default: () => 0 },
|
|
37
|
+
decision: { reducer: (a, b) => b ?? a, default: () => 'final' },
|
|
38
|
+
toolName: { reducer: (a, b) => b ?? a, default: () => null },
|
|
39
|
+
toolArgs: { reducer: (a, b) => b ?? a, default: () => null },
|
|
40
|
+
}
|
|
41
|
+
});
|
|
58
42
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
webUsed = true;
|
|
64
|
-
webSites = webResult.sites;
|
|
65
|
-
webQuery = webResult.query || '';
|
|
66
|
-
modelMessages = [{
|
|
67
|
-
role: 'system',
|
|
68
|
-
content:
|
|
69
|
-
'Current web search results (use when relevant, cite URLs inline):\n\n' +
|
|
70
|
-
webResult.summary +
|
|
71
|
-
'\n\nNow respond to the user using these results as needed.',
|
|
72
|
-
}].concat(messages);
|
|
73
|
-
}
|
|
74
|
-
} catch (_) {
|
|
43
|
+
graph.addNode('router', async (state) => {
|
|
44
|
+
const tc = parseToolCall(state.lastResponse);
|
|
45
|
+
if (tc) {
|
|
46
|
+
return { decision: 'tool', toolName: tc.name, toolArgs: tc.args };
|
|
75
47
|
}
|
|
76
|
-
|
|
48
|
+
return { decision: 'final' };
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
graph.addEdge(START, 'router');
|
|
52
|
+
graph.addEdge('router', END);
|
|
77
53
|
|
|
78
|
-
|
|
54
|
+
return graph.compile();
|
|
55
|
+
}
|
|
79
56
|
|
|
57
|
+
async function* callLLMStream(endpoints, modelShort, messages, signal) {
|
|
80
58
|
const body = {
|
|
81
59
|
model: modelShort,
|
|
82
|
-
messages
|
|
60
|
+
messages,
|
|
83
61
|
max_tokens: 4096,
|
|
84
62
|
temperature: 0.7,
|
|
85
63
|
stream: true,
|
|
86
64
|
};
|
|
87
65
|
|
|
88
66
|
let lastError = null;
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < endpoints.length; i++) {
|
|
91
|
-
const endpoint = endpoints[i];
|
|
67
|
+
for (const ep of endpoints) {
|
|
92
68
|
try {
|
|
93
|
-
const stream = streamResponse(
|
|
94
|
-
for await (const
|
|
95
|
-
yield
|
|
69
|
+
const stream = streamResponse(ep, body, signal);
|
|
70
|
+
for await (const ev of stream) {
|
|
71
|
+
yield ev;
|
|
96
72
|
}
|
|
97
73
|
return;
|
|
98
74
|
} catch (err) {
|
|
99
75
|
lastError = err;
|
|
100
|
-
if (signal
|
|
76
|
+
if (signal?.aborted) return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw lastError || new Error('All endpoints failed');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const routerGraph = buildRouterGraph();
|
|
83
|
+
|
|
84
|
+
export async function* callModel(modelConfig, messages, signal) {
|
|
85
|
+
if (!modelConfig || typeof modelConfig !== 'object') throw new Error('modelConfig must be object');
|
|
86
|
+
if (typeof modelConfig.id !== 'string') throw new Error('modelConfig.id must be string');
|
|
87
|
+
checkMessages(messages);
|
|
88
|
+
|
|
89
|
+
const endpoints = MODEL_ROUTES[modelConfig.id];
|
|
90
|
+
if (!endpoints) throw new Error('No endpoints for ' + modelConfig.id);
|
|
91
|
+
const modelShort = modelConfig.id.replace(/^[^/]+\//, '');
|
|
92
|
+
const systemPrompt = buildSystemPrompt(process.cwd());
|
|
93
|
+
|
|
94
|
+
let msgs = [{ role: 'system', content: systemPrompt }].concat(messages);
|
|
95
|
+
let fullResponse = '';
|
|
96
|
+
|
|
97
|
+
for (let iter = 0; iter < 15 && !signal?.aborted; iter++) {
|
|
98
|
+
fullResponse = '';
|
|
99
|
+
|
|
100
|
+
const llmStream = callLLMStream(endpoints, modelShort, msgs, signal);
|
|
101
|
+
for await (const ev of llmStream) {
|
|
102
|
+
if (ev.type === 'token') {
|
|
103
|
+
fullResponse += ev.content || '';
|
|
104
|
+
yield ev;
|
|
105
|
+
} else if (ev.type === 'done') {
|
|
106
|
+
break;
|
|
107
|
+
} else if (ev.type === 'timeout') {
|
|
108
|
+
yield ev;
|
|
101
109
|
return;
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
|
-
}
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
if (signal?.aborted) return;
|
|
114
|
+
|
|
115
|
+
const state = await routerGraph.invoke({ lastResponse: fullResponse, iteration: iter });
|
|
116
|
+
|
|
117
|
+
if (state.decision === 'final') {
|
|
118
|
+
yield { type: 'done' };
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
yield { type: 'tool_call', tool: state.toolName, args: state.toolArgs };
|
|
123
|
+
|
|
124
|
+
let result;
|
|
125
|
+
if (state.toolName === 'ask_question') {
|
|
126
|
+
const answer = yield { type: 'question', text: state.toolArgs.question };
|
|
127
|
+
result = 'User answered: ' + (answer || '');
|
|
128
|
+
} else if (state.toolName === 'todo_add') {
|
|
129
|
+
result = await execTool({ name: state.toolName, args: state.toolArgs }, signal);
|
|
130
|
+
yield { type: 'todo_add', text: state.toolArgs.text, result };
|
|
131
|
+
} else if (state.toolName === 'todo_done') {
|
|
132
|
+
result = await execTool({ name: state.toolName, args: state.toolArgs }, signal);
|
|
133
|
+
yield { type: 'todo_done', id: state.toolArgs.id, result };
|
|
134
|
+
} else {
|
|
135
|
+
result = await execTool({ name: state.toolName, args: state.toolArgs }, signal);
|
|
136
|
+
yield { type: 'tool_result', tool: state.toolName, result: result.substring(0, 300) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
msgs.push({ role: 'assistant', content: fullResponse });
|
|
140
|
+
msgs.push({ role: 'user', content: 'Observation: ' + result });
|
|
141
|
+
}
|
|
107
142
|
}
|
|
@@ -46,7 +46,7 @@ export function shouldUseWebSearch(messages) {
|
|
|
46
46
|
return asksExternalFact === true && hasNamedThing === true;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async function searchWeb(query, signal) {
|
|
49
|
+
export async function searchWeb(query, signal) {
|
|
50
50
|
const key = getWebSearchKey();
|
|
51
51
|
if (key.length === 0) {
|
|
52
52
|
return {
|