godspeed-agent 0.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/LICENSE +21 -0
- package/README.md +99 -0
- package/bin/godspeed +2 -0
- package/package.json +32 -0
- package/src/cli.ts +25 -0
- package/src/commands/agent.ts +81 -0
- package/src/commands/chat.ts +65 -0
- package/src/commands/models.ts +49 -0
- package/src/commands/providers.ts +83 -0
- package/src/commands/tools.ts +40 -0
- package/src/commands/tui.tsx +24 -0
- package/src/lib/agent.ts +183 -0
- package/src/lib/agentMemory.ts +69 -0
- package/src/lib/config.ts +55 -0
- package/src/lib/history.ts +50 -0
- package/src/lib/llm.ts +86 -0
- package/src/lib/providers.ts +45 -0
- package/src/providers/gemini.ts +91 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/ollama.ts +84 -0
- package/src/providers/openrouter.ts +97 -0
- package/src/providers/types.ts +18 -0
- package/src/tools/labels.ts +38 -0
- package/src/tools/metadata.ts +102 -0
- package/src/tools/prompt.ts +37 -0
- package/src/tools/registry.ts +63 -0
- package/src/tools/tools.ts +179 -0
- package/src/tools/workspace.ts +22 -0
- package/src/tui/App.tsx +377 -0
package/src/tui/App.tsx
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
3
|
+
import { runAgent, type AgentStep } from "../lib/agent.js";
|
|
4
|
+
import { loadHistory, saveHistory } from "../lib/history.js";
|
|
5
|
+
import { getWorkspace } from "../tools/workspace.js";
|
|
6
|
+
import { getToolLabel } from "../tools/labels.js";
|
|
7
|
+
import {
|
|
8
|
+
loadAgentMemory,
|
|
9
|
+
saveAgentMemory,
|
|
10
|
+
updateAgentMemory,
|
|
11
|
+
} from "../lib/agentMemory.js";
|
|
12
|
+
import { streamLLMToCallback } from "../lib/llm.js";
|
|
13
|
+
|
|
14
|
+
type UiMessage = {
|
|
15
|
+
role: "user" | "ai";
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type AppProps = {
|
|
20
|
+
sessionName: string;
|
|
21
|
+
direct?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function App({ sessionName, direct = false }: AppProps) {
|
|
25
|
+
const { exit } = useApp();
|
|
26
|
+
|
|
27
|
+
const [input, setInput] = useState("");
|
|
28
|
+
const [agentMemory, setAgentMemory] = useState(loadAgentMemory(sessionName));
|
|
29
|
+
|
|
30
|
+
const [uiMessages, setUiMessages] = useState<UiMessage[]>(
|
|
31
|
+
loadHistory(sessionName).map((message) => ({
|
|
32
|
+
role: message.role === "user" ? "user" : "ai",
|
|
33
|
+
text: message.text,
|
|
34
|
+
})),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const [steps, setSteps] = useState<AgentStep[]>([]);
|
|
38
|
+
const [loading, setLoading] = useState(false);
|
|
39
|
+
|
|
40
|
+
const [pendingApproval, setPendingApproval] = useState<{
|
|
41
|
+
label: string;
|
|
42
|
+
resolve: (approved: boolean) => void;
|
|
43
|
+
} | null>(null);
|
|
44
|
+
|
|
45
|
+
const visibleSteps = steps.slice(-10);
|
|
46
|
+
const visibleMessages = uiMessages.slice(-8);
|
|
47
|
+
|
|
48
|
+
function persistHistory(messages: UiMessage[]) {
|
|
49
|
+
saveHistory(
|
|
50
|
+
sessionName,
|
|
51
|
+
messages.map((message) => ({
|
|
52
|
+
role: message.role === "user" ? "user" : "model",
|
|
53
|
+
text: message.text,
|
|
54
|
+
})),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function upsertStep(step: AgentStep) {
|
|
59
|
+
setSteps((prev) => {
|
|
60
|
+
const existingIndex = prev.findIndex(
|
|
61
|
+
(item) => item.step === step.step && item.action === step.action,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (existingIndex === -1) {
|
|
65
|
+
return [...prev, step];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const next = [...prev];
|
|
69
|
+
next[existingIndex] = step;
|
|
70
|
+
return next;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function streamAiMessage(text: string) {
|
|
75
|
+
let current = "";
|
|
76
|
+
|
|
77
|
+
setUiMessages((prev) => [
|
|
78
|
+
...prev,
|
|
79
|
+
{
|
|
80
|
+
role: "ai",
|
|
81
|
+
text: "",
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
for (const char of text) {
|
|
86
|
+
current += char;
|
|
87
|
+
|
|
88
|
+
setUiMessages((prev) => {
|
|
89
|
+
const next = [...prev];
|
|
90
|
+
|
|
91
|
+
if (next.length === 0) {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
role: "ai",
|
|
95
|
+
text: current,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
next[next.length - 1] = {
|
|
101
|
+
role: "ai",
|
|
102
|
+
text: current,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return next;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function sendMessage() {
|
|
113
|
+
const prompt = input.trim();
|
|
114
|
+
|
|
115
|
+
if (!prompt || loading) return;
|
|
116
|
+
|
|
117
|
+
setInput("");
|
|
118
|
+
setLoading(true);
|
|
119
|
+
setSteps([]);
|
|
120
|
+
|
|
121
|
+
const userMessage: UiMessage = {
|
|
122
|
+
role: "user",
|
|
123
|
+
text: prompt,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
setUiMessages((prev) => [...prev, userMessage]);
|
|
127
|
+
|
|
128
|
+
const isDirectChat = direct || prompt.startsWith("/chat ");
|
|
129
|
+
|
|
130
|
+
const directPrompt = prompt.startsWith("/chat ")
|
|
131
|
+
? prompt.replace(/^\/chat\s+/, "")
|
|
132
|
+
: prompt;
|
|
133
|
+
|
|
134
|
+
if (isDirectChat) {
|
|
135
|
+
let fullAnswer = "";
|
|
136
|
+
|
|
137
|
+
setUiMessages((prev) => [
|
|
138
|
+
...prev,
|
|
139
|
+
{
|
|
140
|
+
role: "ai",
|
|
141
|
+
text: "",
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await streamLLMToCallback(directPrompt, (token) => {
|
|
147
|
+
fullAnswer += token;
|
|
148
|
+
|
|
149
|
+
setUiMessages((prev) => {
|
|
150
|
+
const next = [...prev];
|
|
151
|
+
|
|
152
|
+
next[next.length - 1] = {
|
|
153
|
+
role: "ai",
|
|
154
|
+
text: fullAnswer,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return next;
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
setUiMessages((prev) => {
|
|
162
|
+
persistHistory(prev);
|
|
163
|
+
return prev;
|
|
164
|
+
});
|
|
165
|
+
} finally {
|
|
166
|
+
setLoading(false);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const answer = await runAgent(prompt, {
|
|
174
|
+
memory: agentMemory,
|
|
175
|
+
showSpinner: false,
|
|
176
|
+
|
|
177
|
+
async confirmTool(action) {
|
|
178
|
+
const needsApproval =
|
|
179
|
+
action.action === "WRITE_FILE" ||
|
|
180
|
+
action.action === "EDIT_FILE" ||
|
|
181
|
+
action.action === "DELETE_FILE" ||
|
|
182
|
+
action.action === "CREATE_DIRECTORY" ||
|
|
183
|
+
action.action === "MOVE_FILE" ||
|
|
184
|
+
action.action === "COPY_FILE" ||
|
|
185
|
+
action.action === "RUN_COMMAND";
|
|
186
|
+
|
|
187
|
+
if (!needsApproval) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const label = getToolLabel(action);
|
|
192
|
+
|
|
193
|
+
return new Promise<boolean>((resolve) => {
|
|
194
|
+
setPendingApproval({
|
|
195
|
+
label,
|
|
196
|
+
resolve,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
onStep(step) {
|
|
202
|
+
upsertStep(step);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const nextMemory = await updateAgentMemory({
|
|
207
|
+
previousMemory: agentMemory,
|
|
208
|
+
userPrompt: prompt,
|
|
209
|
+
finalAnswer: answer,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
setAgentMemory(nextMemory);
|
|
213
|
+
saveAgentMemory(sessionName, nextMemory);
|
|
214
|
+
|
|
215
|
+
await streamAiMessage(answer || "Done.");
|
|
216
|
+
|
|
217
|
+
setUiMessages((prev) => {
|
|
218
|
+
persistHistory(prev);
|
|
219
|
+
return prev;
|
|
220
|
+
});
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const errorMessage: UiMessage = {
|
|
223
|
+
role: "ai",
|
|
224
|
+
text: err instanceof Error ? err.message : String(err),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
setUiMessages((prev) => {
|
|
228
|
+
const next = [...prev, errorMessage];
|
|
229
|
+
persistHistory(next);
|
|
230
|
+
return next;
|
|
231
|
+
});
|
|
232
|
+
} finally {
|
|
233
|
+
setLoading(false);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
useInput((char, key) => {
|
|
238
|
+
if (pendingApproval) {
|
|
239
|
+
if (char?.toLowerCase() === "y") {
|
|
240
|
+
pendingApproval.resolve(true);
|
|
241
|
+
setPendingApproval(null);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (char?.toLowerCase() === "n") {
|
|
245
|
+
pendingApproval.resolve(false);
|
|
246
|
+
setPendingApproval(null);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (key.escape || (key.ctrl && char === "c")) {
|
|
253
|
+
exit();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (loading) return;
|
|
258
|
+
|
|
259
|
+
if (key.backspace || key.delete) {
|
|
260
|
+
setInput((prev) => prev.slice(0, -1));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (key.return) {
|
|
265
|
+
sendMessage();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (char) {
|
|
270
|
+
setInput((prev) => prev + char);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<Box flexDirection="column">
|
|
276
|
+
<Box
|
|
277
|
+
flexDirection="column"
|
|
278
|
+
borderStyle="round"
|
|
279
|
+
borderColor="green"
|
|
280
|
+
paddingX={1}
|
|
281
|
+
>
|
|
282
|
+
<Text color="green">TUI Starter</Text>
|
|
283
|
+
<Text color="gray">Session: {sessionName}</Text>
|
|
284
|
+
<Text color="gray">Workspace: {getWorkspace()}</Text>
|
|
285
|
+
<Text color="gray">Mode: {direct ? "Direct Chat" : "Agent"}</Text>
|
|
286
|
+
<Text color="gray">Press ESC or Ctrl+C to exit</Text>
|
|
287
|
+
</Box>
|
|
288
|
+
|
|
289
|
+
<Box marginTop={1} gap={2}>
|
|
290
|
+
<Box
|
|
291
|
+
width="40%"
|
|
292
|
+
flexDirection="column"
|
|
293
|
+
borderStyle="round"
|
|
294
|
+
borderColor="magenta"
|
|
295
|
+
paddingX={1}
|
|
296
|
+
>
|
|
297
|
+
<Text color="magenta">Steps</Text>
|
|
298
|
+
|
|
299
|
+
{visibleSteps.length === 0 && <Text color="gray">No steps yet</Text>}
|
|
300
|
+
|
|
301
|
+
{visibleSteps.map((step, index) => {
|
|
302
|
+
const icon =
|
|
303
|
+
step.status === "completed"
|
|
304
|
+
? "✅"
|
|
305
|
+
: step.status === "failed"
|
|
306
|
+
? "❌"
|
|
307
|
+
: "⏳";
|
|
308
|
+
|
|
309
|
+
const detail =
|
|
310
|
+
step.error ??
|
|
311
|
+
step.path ??
|
|
312
|
+
step.command ??
|
|
313
|
+
step.pattern ??
|
|
314
|
+
step.query ??
|
|
315
|
+
(step.from && step.to ? `${step.from} -> ${step.to}` : "");
|
|
316
|
+
|
|
317
|
+
const pointer = index === visibleSteps.length - 1 ? ">" : " ";
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<Text key={`${step.step}-${step.action}`}>
|
|
321
|
+
{pointer} {icon} {step.action} {detail}
|
|
322
|
+
</Text>
|
|
323
|
+
);
|
|
324
|
+
})}
|
|
325
|
+
</Box>
|
|
326
|
+
|
|
327
|
+
<Box
|
|
328
|
+
width="60%"
|
|
329
|
+
flexDirection="column"
|
|
330
|
+
borderStyle="round"
|
|
331
|
+
borderColor="cyan"
|
|
332
|
+
paddingX={1}
|
|
333
|
+
>
|
|
334
|
+
<Text color="cyan">Chat</Text>
|
|
335
|
+
|
|
336
|
+
{visibleMessages.length === 0 && (
|
|
337
|
+
<Text color="gray">No messages yet</Text>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
{visibleMessages.map((message, index) => (
|
|
341
|
+
<Box key={index} flexDirection="column" marginBottom={1}>
|
|
342
|
+
<Text color={message.role === "user" ? "cyan" : "yellow"}>
|
|
343
|
+
{message.role === "user" ? "You>" : "AI>"}
|
|
344
|
+
</Text>
|
|
345
|
+
|
|
346
|
+
<Text>{message.text}</Text>
|
|
347
|
+
</Box>
|
|
348
|
+
))}
|
|
349
|
+
</Box>
|
|
350
|
+
</Box>
|
|
351
|
+
|
|
352
|
+
{pendingApproval && (
|
|
353
|
+
<Box
|
|
354
|
+
flexDirection="column"
|
|
355
|
+
borderStyle="round"
|
|
356
|
+
borderColor="yellow"
|
|
357
|
+
marginTop={1}
|
|
358
|
+
paddingX={1}
|
|
359
|
+
>
|
|
360
|
+
<Text color="yellow">Tool Approval Required</Text>
|
|
361
|
+
<Text>{pendingApproval.label}</Text>
|
|
362
|
+
<Text color="gray">Press Y to approve, N to deny</Text>
|
|
363
|
+
</Box>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{loading && <Text color="gray">Thinking...</Text>}
|
|
367
|
+
|
|
368
|
+
<Box marginTop={1} borderStyle="round" borderColor="blue" paddingX={1}>
|
|
369
|
+
<Text color="cyan">You> </Text>
|
|
370
|
+
<Text>
|
|
371
|
+
{input}
|
|
372
|
+
<Text color="green">|</Text>
|
|
373
|
+
</Text>
|
|
374
|
+
</Box>
|
|
375
|
+
</Box>
|
|
376
|
+
);
|
|
377
|
+
}
|