@devang0907/agent-dev 0.1.2 → 0.1.3
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/agent/loop.js +66 -5
- package/dist/agent/tools/read.js +4 -0
- package/dist/providers/groq.js +7 -81
- package/dist/providers/openai-compat.d.ts +8 -0
- package/dist/providers/openai-compat.js +117 -0
- package/dist/providers/openrouter-free.js +7 -81
- package/dist/ui/ApiKeyPrompt.js +2 -3
- package/dist/ui/App.js +56 -22
- package/dist/ui/Editor.js +1 -1
- package/dist/ui/ModelSelector.js +1 -1
- package/dist/ui/SettingsView.d.ts +3 -1
- package/dist/ui/SettingsView.js +15 -9
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
|
+
import { normalizeToolCalls } from "../providers/openai-compat.js";
|
|
1
2
|
import { streamChat } from "../providers/registry.js";
|
|
2
3
|
import { getToolDefinitions, executeTool } from "./tools/index.js";
|
|
4
|
+
const MAX_TOOL_ROUNDS = 6;
|
|
5
|
+
const MAX_SAME_TOOL_CALLS = 2;
|
|
3
6
|
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant with access to tools: read, write, edit, and bash.
|
|
4
|
-
|
|
7
|
+
When the user asks you to create or modify files, call write or edit once with the full file content, then reply briefly to confirm.
|
|
8
|
+
Do NOT call the same tool repeatedly with the same arguments. One successful write is enough.
|
|
5
9
|
Working directory: ${process.cwd()}`;
|
|
10
|
+
function isToolUseFailedError(message) {
|
|
11
|
+
return /Failed to call a function|tool_use_failed|failed_generation/i.test(message);
|
|
12
|
+
}
|
|
13
|
+
function hadSuccessfulToolResults(context) {
|
|
14
|
+
return context.some((m) => m.role === "tool" && m.content.length > 0 && !m.content.startsWith("Error:"));
|
|
15
|
+
}
|
|
16
|
+
function toolSignature(name, args) {
|
|
17
|
+
return `${name}:${JSON.stringify(args)}`;
|
|
18
|
+
}
|
|
19
|
+
function dedupeToolCalls(toolCalls) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
return toolCalls.filter((tc) => {
|
|
22
|
+
const key = `${tc.name}:${tc.arguments}`;
|
|
23
|
+
if (seen.has(key))
|
|
24
|
+
return false;
|
|
25
|
+
seen.add(key);
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
6
29
|
async function collectStream(model, messages, settings, systemPrompt, signal, onEvent) {
|
|
7
30
|
const tools = getToolDefinitions();
|
|
8
31
|
let content = "";
|
|
@@ -35,32 +58,53 @@ async function collectStream(model, messages, settings, systemPrompt, signal, on
|
|
|
35
58
|
return { content, toolCalls: [], error: event.message };
|
|
36
59
|
}
|
|
37
60
|
}
|
|
38
|
-
const toolCalls = Array.from(toolCallMap.values()).filter((tc) => tc.name);
|
|
61
|
+
const toolCalls = normalizeToolCalls(Array.from(toolCallMap.values()).filter((tc) => tc.name));
|
|
39
62
|
return { content, toolCalls };
|
|
40
63
|
}
|
|
64
|
+
function finishGracefully(context, content, onEvent) {
|
|
65
|
+
const msg = content.trim() || "Done — changes saved successfully.";
|
|
66
|
+
if (!content.trim()) {
|
|
67
|
+
onEvent({ type: "text_delta", delta: msg });
|
|
68
|
+
}
|
|
69
|
+
context.push({ role: "assistant", content: msg });
|
|
70
|
+
onEvent({ type: "turn_end" });
|
|
71
|
+
}
|
|
41
72
|
export async function runAgentLoop(options) {
|
|
42
73
|
const { model, messages, settings, workdir, systemPrompt = DEFAULT_SYSTEM_PROMPT, signal, onEvent, } = options;
|
|
43
74
|
const context = [...messages];
|
|
75
|
+
const callCounts = new Map();
|
|
76
|
+
let toolRound = 0;
|
|
44
77
|
while (true) {
|
|
45
78
|
if (signal?.aborted)
|
|
46
79
|
break;
|
|
80
|
+
toolRound++;
|
|
81
|
+
if (toolRound > MAX_TOOL_ROUNDS) {
|
|
82
|
+
finishGracefully(context, "Done — stopped after too many tool calls.", onEvent);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
47
85
|
onEvent({ type: "message_start", role: "assistant" });
|
|
48
86
|
const { content, toolCalls, error } = await collectStream(model, context, settings, systemPrompt, signal, onEvent);
|
|
49
87
|
if (error) {
|
|
88
|
+
if (isToolUseFailedError(error) && hadSuccessfulToolResults(context)) {
|
|
89
|
+
finishGracefully(context, content, onEvent);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
50
92
|
onEvent({ type: "error", message: error });
|
|
51
93
|
break;
|
|
52
94
|
}
|
|
95
|
+
const uniqueCalls = dedupeToolCalls(toolCalls);
|
|
53
96
|
const assistantMsg = {
|
|
54
97
|
role: "assistant",
|
|
55
98
|
content,
|
|
56
|
-
toolCalls:
|
|
99
|
+
toolCalls: uniqueCalls.length > 0 ? uniqueCalls : undefined,
|
|
57
100
|
};
|
|
58
101
|
context.push(assistantMsg);
|
|
59
|
-
if (
|
|
102
|
+
if (uniqueCalls.length === 0) {
|
|
60
103
|
onEvent({ type: "turn_end" });
|
|
61
104
|
break;
|
|
62
105
|
}
|
|
63
|
-
|
|
106
|
+
let stopAfterBatch = false;
|
|
107
|
+
for (const tc of uniqueCalls) {
|
|
64
108
|
onEvent({ type: "tool_call", toolCall: tc });
|
|
65
109
|
let args = {};
|
|
66
110
|
try {
|
|
@@ -69,6 +113,16 @@ export async function runAgentLoop(options) {
|
|
|
69
113
|
catch {
|
|
70
114
|
args = {};
|
|
71
115
|
}
|
|
116
|
+
const sig = toolSignature(tc.name, args);
|
|
117
|
+
const prev = callCounts.get(sig) ?? 0;
|
|
118
|
+
callCounts.set(sig, prev + 1);
|
|
119
|
+
if (prev >= MAX_SAME_TOOL_CALLS) {
|
|
120
|
+
const skip = "Skipped — already executed this action.";
|
|
121
|
+
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result: skip });
|
|
122
|
+
context.push({ role: "tool", content: skip, toolCallId: tc.id, name: tc.name });
|
|
123
|
+
stopAfterBatch = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
72
126
|
const result = await executeTool(tc.name, args, workdir);
|
|
73
127
|
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result });
|
|
74
128
|
context.push({
|
|
@@ -77,6 +131,13 @@ export async function runAgentLoop(options) {
|
|
|
77
131
|
toolCallId: tc.id,
|
|
78
132
|
name: tc.name,
|
|
79
133
|
});
|
|
134
|
+
if (!result.startsWith("Error:") && (tc.name === "write" || tc.name === "edit")) {
|
|
135
|
+
stopAfterBatch = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (stopAfterBatch) {
|
|
139
|
+
finishGracefully(context, content, onEvent);
|
|
140
|
+
break;
|
|
80
141
|
}
|
|
81
142
|
}
|
|
82
143
|
return context.slice(messages.length);
|
package/dist/agent/tools/read.js
CHANGED
|
@@ -20,6 +20,7 @@ export const readTool = {
|
|
|
20
20
|
path: { type: "string", description: "File path relative to project root" },
|
|
21
21
|
},
|
|
22
22
|
required: ["path"],
|
|
23
|
+
additionalProperties: false,
|
|
23
24
|
},
|
|
24
25
|
};
|
|
25
26
|
export async function executeRead(args, workdir = DEFAULT_WORKDIR) {
|
|
@@ -41,6 +42,7 @@ export const writeTool = {
|
|
|
41
42
|
content: { type: "string", description: "Content to write" },
|
|
42
43
|
},
|
|
43
44
|
required: ["path", "content"],
|
|
45
|
+
additionalProperties: false,
|
|
44
46
|
},
|
|
45
47
|
};
|
|
46
48
|
export async function executeWrite(args, workdir = DEFAULT_WORKDIR) {
|
|
@@ -60,6 +62,7 @@ export const editTool = {
|
|
|
60
62
|
new_string: { type: "string", description: "Replacement string" },
|
|
61
63
|
},
|
|
62
64
|
required: ["path", "old_string", "new_string"],
|
|
65
|
+
additionalProperties: false,
|
|
63
66
|
},
|
|
64
67
|
};
|
|
65
68
|
export async function executeEdit(args, workdir = DEFAULT_WORKDIR) {
|
|
@@ -85,6 +88,7 @@ export const bashTool = {
|
|
|
85
88
|
command: { type: "string", description: "Shell command to execute" },
|
|
86
89
|
},
|
|
87
90
|
required: ["command"],
|
|
91
|
+
additionalProperties: false,
|
|
88
92
|
},
|
|
89
93
|
};
|
|
90
94
|
export async function executeBash(args, workdir = DEFAULT_WORKDIR) {
|
package/dist/providers/groq.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import { toOpenAIMessages, toOpenAITools, processOpenAIStream, formatApiError, } from "./openai-compat.js";
|
|
2
3
|
export const PROVIDER_ID = "groq";
|
|
3
4
|
export const DEFAULT_MODEL = "llama-3.3-70b-versatile";
|
|
4
5
|
export const API_KEY_ENV = "GROQ_API_KEY";
|
|
@@ -13,51 +14,6 @@ export function getApiKey(settings) {
|
|
|
13
14
|
export function hasAuth(settings) {
|
|
14
15
|
return !!getApiKey(settings);
|
|
15
16
|
}
|
|
16
|
-
function toOpenAIMessages(ctx) {
|
|
17
|
-
const msgs = [];
|
|
18
|
-
if (ctx.systemPrompt) {
|
|
19
|
-
msgs.push({ role: "system", content: ctx.systemPrompt });
|
|
20
|
-
}
|
|
21
|
-
for (const m of ctx.messages) {
|
|
22
|
-
if (m.role === "user") {
|
|
23
|
-
msgs.push({ role: "user", content: m.content });
|
|
24
|
-
}
|
|
25
|
-
else if (m.role === "assistant") {
|
|
26
|
-
if (m.toolCalls?.length) {
|
|
27
|
-
msgs.push({
|
|
28
|
-
role: "assistant",
|
|
29
|
-
content: m.content || null,
|
|
30
|
-
tool_calls: m.toolCalls.map((tc) => ({
|
|
31
|
-
id: tc.id,
|
|
32
|
-
type: "function",
|
|
33
|
-
function: { name: tc.name, arguments: tc.arguments },
|
|
34
|
-
})),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
msgs.push({ role: "assistant", content: m.content });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else if (m.role === "tool") {
|
|
42
|
-
msgs.push({
|
|
43
|
-
role: "tool",
|
|
44
|
-
tool_call_id: m.toolCallId,
|
|
45
|
-
content: m.content,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return msgs;
|
|
50
|
-
}
|
|
51
|
-
function toOpenAITools(tools) {
|
|
52
|
-
return tools.map((t) => ({
|
|
53
|
-
type: "function",
|
|
54
|
-
function: {
|
|
55
|
-
name: t.name,
|
|
56
|
-
description: t.description,
|
|
57
|
-
parameters: t.parameters,
|
|
58
|
-
},
|
|
59
|
-
}));
|
|
60
|
-
}
|
|
61
17
|
export async function* streamChat(model, ctx, settings) {
|
|
62
18
|
const apiKey = getApiKey(settings);
|
|
63
19
|
if (!apiKey) {
|
|
@@ -65,49 +21,19 @@ export async function* streamChat(model, ctx, settings) {
|
|
|
65
21
|
return;
|
|
66
22
|
}
|
|
67
23
|
const client = new OpenAI({ apiKey, baseURL: BASE_URL });
|
|
24
|
+
const hasTools = ctx.tools.length > 0;
|
|
68
25
|
try {
|
|
69
26
|
const stream = await client.chat.completions.create({
|
|
70
27
|
model: model.id,
|
|
71
28
|
messages: toOpenAIMessages(ctx),
|
|
72
|
-
tools:
|
|
29
|
+
tools: hasTools ? toOpenAITools(ctx.tools) : undefined,
|
|
30
|
+
tool_choice: hasTools ? "auto" : undefined,
|
|
31
|
+
parallel_tool_calls: false,
|
|
73
32
|
stream: true,
|
|
74
33
|
}, { signal: ctx.signal });
|
|
75
|
-
|
|
76
|
-
for await (const chunk of stream) {
|
|
77
|
-
const delta = chunk.choices[0]?.delta;
|
|
78
|
-
if (!delta)
|
|
79
|
-
continue;
|
|
80
|
-
if (delta.content) {
|
|
81
|
-
yield { type: "text_delta", delta: delta.content };
|
|
82
|
-
}
|
|
83
|
-
if (delta.tool_calls) {
|
|
84
|
-
for (const tc of delta.tool_calls) {
|
|
85
|
-
const idx = tc.index;
|
|
86
|
-
if (!toolCalls.has(idx)) {
|
|
87
|
-
toolCalls.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", arguments: "" });
|
|
88
|
-
}
|
|
89
|
-
const existing = toolCalls.get(idx);
|
|
90
|
-
if (tc.id)
|
|
91
|
-
existing.id = tc.id;
|
|
92
|
-
if (tc.function?.name)
|
|
93
|
-
existing.name = tc.function.name;
|
|
94
|
-
if (tc.function?.arguments) {
|
|
95
|
-
existing.arguments += tc.function.arguments;
|
|
96
|
-
yield {
|
|
97
|
-
type: "tool_call_delta",
|
|
98
|
-
index: idx,
|
|
99
|
-
id: existing.id,
|
|
100
|
-
name: existing.name,
|
|
101
|
-
argumentsDelta: tc.function.arguments,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
yield { type: "done" };
|
|
34
|
+
yield* processOpenAIStream(stream);
|
|
108
35
|
}
|
|
109
36
|
catch (err) {
|
|
110
|
-
|
|
111
|
-
yield { type: "error", message };
|
|
37
|
+
yield { type: "error", message: formatApiError(err) };
|
|
112
38
|
}
|
|
113
39
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
import type { ChatContext, StreamEvent, ToolCall } from "./types.js";
|
|
3
|
+
export declare function sanitizeToolParameters(params: Record<string, unknown>): Record<string, unknown>;
|
|
4
|
+
export declare function toOpenAITools(tools: ChatContext["tools"]): OpenAI.Chat.ChatCompletionTool[];
|
|
5
|
+
export declare function normalizeToolCalls(toolCalls: ToolCall[]): ToolCall[];
|
|
6
|
+
export declare function toOpenAIMessages(ctx: ChatContext): OpenAI.Chat.ChatCompletionMessageParam[];
|
|
7
|
+
export declare function processOpenAIStream(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): AsyncGenerator<StreamEvent>;
|
|
8
|
+
export declare function formatApiError(err: unknown): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export function sanitizeToolParameters(params) {
|
|
2
|
+
return {
|
|
3
|
+
...params,
|
|
4
|
+
type: "object",
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function toOpenAITools(tools) {
|
|
9
|
+
return tools.map((t) => ({
|
|
10
|
+
type: "function",
|
|
11
|
+
function: {
|
|
12
|
+
name: t.name,
|
|
13
|
+
description: t.description,
|
|
14
|
+
parameters: sanitizeToolParameters(t.parameters),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
export function normalizeToolCalls(toolCalls) {
|
|
19
|
+
return toolCalls.map((tc, i) => ({
|
|
20
|
+
id: tc.id?.trim() || `call_${Date.now()}_${i}`,
|
|
21
|
+
name: tc.name,
|
|
22
|
+
arguments: tc.arguments?.trim() || "{}",
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
export function toOpenAIMessages(ctx) {
|
|
26
|
+
const msgs = [];
|
|
27
|
+
if (ctx.systemPrompt) {
|
|
28
|
+
msgs.push({ role: "system", content: ctx.systemPrompt });
|
|
29
|
+
}
|
|
30
|
+
for (const m of ctx.messages) {
|
|
31
|
+
if (m.role === "user") {
|
|
32
|
+
msgs.push({ role: "user", content: m.content });
|
|
33
|
+
}
|
|
34
|
+
else if (m.role === "assistant") {
|
|
35
|
+
if (m.toolCalls?.length) {
|
|
36
|
+
const toolCalls = normalizeToolCalls(m.toolCalls);
|
|
37
|
+
msgs.push({
|
|
38
|
+
role: "assistant",
|
|
39
|
+
content: m.content || null,
|
|
40
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
41
|
+
id: tc.id,
|
|
42
|
+
type: "function",
|
|
43
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
msgs.push({ role: "assistant", content: m.content });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (m.role === "tool") {
|
|
52
|
+
msgs.push({
|
|
53
|
+
role: "tool",
|
|
54
|
+
tool_call_id: m.toolCallId,
|
|
55
|
+
content: m.content,
|
|
56
|
+
...(m.name ? { name: m.name } : {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return msgs;
|
|
61
|
+
}
|
|
62
|
+
export async function* processOpenAIStream(stream) {
|
|
63
|
+
const toolCalls = new Map();
|
|
64
|
+
for await (const chunk of stream) {
|
|
65
|
+
const choice = chunk.choices[0];
|
|
66
|
+
if (choice?.finish_reason === "tool_calls" || choice?.delta?.tool_calls) {
|
|
67
|
+
// normal path
|
|
68
|
+
}
|
|
69
|
+
const delta = choice?.delta;
|
|
70
|
+
if (!delta)
|
|
71
|
+
continue;
|
|
72
|
+
if (delta.content) {
|
|
73
|
+
yield { type: "text_delta", delta: delta.content };
|
|
74
|
+
}
|
|
75
|
+
if (delta.tool_calls) {
|
|
76
|
+
for (const tc of delta.tool_calls) {
|
|
77
|
+
const idx = tc.index ?? 0;
|
|
78
|
+
if (!toolCalls.has(idx)) {
|
|
79
|
+
toolCalls.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", arguments: "" });
|
|
80
|
+
}
|
|
81
|
+
const existing = toolCalls.get(idx);
|
|
82
|
+
if (tc.id)
|
|
83
|
+
existing.id = tc.id;
|
|
84
|
+
if (tc.function?.name)
|
|
85
|
+
existing.name = tc.function.name;
|
|
86
|
+
if (tc.function?.arguments) {
|
|
87
|
+
existing.arguments += tc.function.arguments;
|
|
88
|
+
yield {
|
|
89
|
+
type: "tool_call_delta",
|
|
90
|
+
index: idx,
|
|
91
|
+
id: existing.id,
|
|
92
|
+
name: existing.name,
|
|
93
|
+
argumentsDelta: tc.function.arguments,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (chunk.usage) {
|
|
99
|
+
yield {
|
|
100
|
+
type: "done",
|
|
101
|
+
usage: {
|
|
102
|
+
inputTokens: chunk.usage.prompt_tokens,
|
|
103
|
+
outputTokens: chunk.usage.completion_tokens,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
yield { type: "done" };
|
|
109
|
+
}
|
|
110
|
+
export function formatApiError(err) {
|
|
111
|
+
const apiErr = err;
|
|
112
|
+
const failed = apiErr.error?.failed_generation;
|
|
113
|
+
const msg = apiErr.error?.message ?? apiErr.message ?? "API error";
|
|
114
|
+
if (failed)
|
|
115
|
+
return `${msg}\nModel output: ${failed.slice(0, 300)}`;
|
|
116
|
+
return msg;
|
|
117
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import { toOpenAIMessages, toOpenAITools, processOpenAIStream, formatApiError, } from "./openai-compat.js";
|
|
2
3
|
export const PROVIDER_ID = "free";
|
|
3
4
|
export const DEFAULT_MODEL = "meta-llama/llama-3.3-70b-instruct:free";
|
|
4
5
|
export const API_KEY_ENV = "OPENROUTER_API_KEY";
|
|
@@ -26,51 +27,6 @@ export function getApiKey(settings) {
|
|
|
26
27
|
export function hasAuth(settings) {
|
|
27
28
|
return !!getApiKey(settings);
|
|
28
29
|
}
|
|
29
|
-
function toOpenAIMessages(ctx) {
|
|
30
|
-
const msgs = [];
|
|
31
|
-
if (ctx.systemPrompt) {
|
|
32
|
-
msgs.push({ role: "system", content: ctx.systemPrompt });
|
|
33
|
-
}
|
|
34
|
-
for (const m of ctx.messages) {
|
|
35
|
-
if (m.role === "user") {
|
|
36
|
-
msgs.push({ role: "user", content: m.content });
|
|
37
|
-
}
|
|
38
|
-
else if (m.role === "assistant") {
|
|
39
|
-
if (m.toolCalls?.length) {
|
|
40
|
-
msgs.push({
|
|
41
|
-
role: "assistant",
|
|
42
|
-
content: m.content || null,
|
|
43
|
-
tool_calls: m.toolCalls.map((tc) => ({
|
|
44
|
-
id: tc.id,
|
|
45
|
-
type: "function",
|
|
46
|
-
function: { name: tc.name, arguments: tc.arguments },
|
|
47
|
-
})),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
msgs.push({ role: "assistant", content: m.content });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else if (m.role === "tool") {
|
|
55
|
-
msgs.push({
|
|
56
|
-
role: "tool",
|
|
57
|
-
tool_call_id: m.toolCallId,
|
|
58
|
-
content: m.content,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return msgs;
|
|
63
|
-
}
|
|
64
|
-
function toOpenAITools(tools) {
|
|
65
|
-
return tools.map((t) => ({
|
|
66
|
-
type: "function",
|
|
67
|
-
function: {
|
|
68
|
-
name: t.name,
|
|
69
|
-
description: t.description,
|
|
70
|
-
parameters: t.parameters,
|
|
71
|
-
},
|
|
72
|
-
}));
|
|
73
|
-
}
|
|
74
30
|
export async function* streamChat(model, ctx, settings) {
|
|
75
31
|
const apiKey = getApiKey(settings);
|
|
76
32
|
if (!apiKey) {
|
|
@@ -85,49 +41,19 @@ export async function* streamChat(model, ctx, settings) {
|
|
|
85
41
|
"X-Title": "agent-dev",
|
|
86
42
|
},
|
|
87
43
|
});
|
|
44
|
+
const hasTools = ctx.tools.length > 0;
|
|
88
45
|
try {
|
|
89
46
|
const stream = await client.chat.completions.create({
|
|
90
47
|
model: model.id,
|
|
91
48
|
messages: toOpenAIMessages(ctx),
|
|
92
|
-
tools:
|
|
49
|
+
tools: hasTools ? toOpenAITools(ctx.tools) : undefined,
|
|
50
|
+
tool_choice: hasTools ? "auto" : undefined,
|
|
51
|
+
parallel_tool_calls: false,
|
|
93
52
|
stream: true,
|
|
94
53
|
}, { signal: ctx.signal });
|
|
95
|
-
|
|
96
|
-
for await (const chunk of stream) {
|
|
97
|
-
const delta = chunk.choices[0]?.delta;
|
|
98
|
-
if (!delta)
|
|
99
|
-
continue;
|
|
100
|
-
if (delta.content) {
|
|
101
|
-
yield { type: "text_delta", delta: delta.content };
|
|
102
|
-
}
|
|
103
|
-
if (delta.tool_calls) {
|
|
104
|
-
for (const tc of delta.tool_calls) {
|
|
105
|
-
const idx = tc.index;
|
|
106
|
-
if (!toolCalls.has(idx)) {
|
|
107
|
-
toolCalls.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", arguments: "" });
|
|
108
|
-
}
|
|
109
|
-
const existing = toolCalls.get(idx);
|
|
110
|
-
if (tc.id)
|
|
111
|
-
existing.id = tc.id;
|
|
112
|
-
if (tc.function?.name)
|
|
113
|
-
existing.name = tc.function.name;
|
|
114
|
-
if (tc.function?.arguments) {
|
|
115
|
-
existing.arguments += tc.function.arguments;
|
|
116
|
-
yield {
|
|
117
|
-
type: "tool_call_delta",
|
|
118
|
-
index: idx,
|
|
119
|
-
id: existing.id,
|
|
120
|
-
name: existing.name,
|
|
121
|
-
argumentsDelta: tc.function.arguments,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
yield { type: "done" };
|
|
54
|
+
yield* processOpenAIStream(stream);
|
|
128
55
|
}
|
|
129
56
|
catch (err) {
|
|
130
|
-
|
|
131
|
-
yield { type: "error", message };
|
|
57
|
+
yield { type: "error", message: formatApiError(err) };
|
|
132
58
|
}
|
|
133
59
|
}
|
package/dist/ui/ApiKeyPrompt.js
CHANGED
|
@@ -4,7 +4,6 @@ import { Box, Text, useInput } from "ink";
|
|
|
4
4
|
import { PROVIDER_LABELS } from "../config/models.js";
|
|
5
5
|
import { PROVIDER_ENV_VARS } from "../providers/registry.js";
|
|
6
6
|
import { LeftBorder } from "./LeftBorder.js";
|
|
7
|
-
import { Panel } from "./Panel.js";
|
|
8
7
|
function BlinkingCursor({ theme, visible }) {
|
|
9
8
|
if (!visible)
|
|
10
9
|
return null;
|
|
@@ -35,8 +34,8 @@ export function ApiKeyPrompt({ theme, provider, model, onSubmit, onCancel }) {
|
|
|
35
34
|
if (input && !key.ctrl && !key.meta) {
|
|
36
35
|
setValue((v) => v + input);
|
|
37
36
|
}
|
|
38
|
-
});
|
|
37
|
+
}, { isActive: true });
|
|
39
38
|
const envVars = PROVIDER_ENV_VARS[provider];
|
|
40
39
|
const masked = "•".repeat(value.length);
|
|
41
|
-
return (_jsx(Box, {
|
|
40
|
+
return (_jsx(Box, { flexDirection: "column", marginX: 2, marginTop: 1, marginBottom: 1, children: _jsxs(LeftBorder, { theme: theme, borderColor: theme.primary, children: [_jsx(Text, { color: theme.text, bold: true, children: "API key required" }), _jsx(Text, { color: theme.textMuted, children: " Enter save \u00B7 Esc cancel" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.textMuted, children: ["Provider: ", PROVIDER_LABELS[provider]] }), _jsxs(Text, { color: theme.textMuted, children: ["Model: ", model.name] })] }), _jsx(Box, { flexDirection: "row", marginTop: 1, borderStyle: "round", borderColor: theme.primary, paddingX: 1, paddingY: 0, children: value.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text, children: masked }), _jsx(BlinkingCursor, { theme: theme, visible: cursorOn })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textMuted, children: "Paste API key here\u2026" }), _jsx(BlinkingCursor, { theme: theme, visible: cursorOn })] })) }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.textMuted, children: ["Or set env: ", envVars.join(" · ")] }), _jsx(Text, { color: theme.textMuted, children: "Saved to ~/.agent-dev/settings.json" })] })] }) }));
|
|
42
41
|
}
|
package/dist/ui/App.js
CHANGED
|
@@ -7,10 +7,16 @@ import { Footer } from "./Footer.js";
|
|
|
7
7
|
import { ModelSelector } from "./ModelSelector.js";
|
|
8
8
|
import { ApiKeyPrompt } from "./ApiKeyPrompt.js";
|
|
9
9
|
import { SettingsView } from "./SettingsView.js";
|
|
10
|
-
import { hasProviderAuth } from "../providers/registry.js";
|
|
10
|
+
import { hasProviderAuth, getDefaultModelForProvider } from "../providers/registry.js";
|
|
11
|
+
import { findModel } from "../config/models.js";
|
|
11
12
|
import { StartupBanner } from "./StartupBanner.js";
|
|
12
13
|
import { getTheme } from "./theme.js";
|
|
13
|
-
|
|
14
|
+
function modelForProvider(provider, settings) {
|
|
15
|
+
const current = findModel(settings.defaultProvider, settings.defaultModel);
|
|
16
|
+
if (current?.provider === provider)
|
|
17
|
+
return current;
|
|
18
|
+
return getDefaultModelForProvider(provider);
|
|
19
|
+
}
|
|
14
20
|
export function App({ session, workdir, onQuit }) {
|
|
15
21
|
const { exit } = useApp();
|
|
16
22
|
const [displayMessages, setDisplayMessages] = useState(() => session.getMessages().map((m) => ({
|
|
@@ -22,11 +28,43 @@ export function App({ session, workdir, onQuit }) {
|
|
|
22
28
|
const [overlay, setOverlay] = useState("none");
|
|
23
29
|
const [modelFilter, setModelFilter] = useState();
|
|
24
30
|
const [pendingModel, setPendingModel] = useState(null);
|
|
31
|
+
const [apiKeyReturnOverlay, setApiKeyReturnOverlay] = useState("none");
|
|
25
32
|
const [settings, setSettings] = useState(session.getSettings());
|
|
26
33
|
const [model, setModel] = useState(session.getModel());
|
|
27
34
|
const [running, setRunning] = useState(false);
|
|
28
35
|
const streamingRef = useRef("");
|
|
36
|
+
const startupChecked = useRef(false);
|
|
29
37
|
const theme = getTheme();
|
|
38
|
+
const openApiKeyPrompt = useCallback((target, returnTo = "none") => {
|
|
39
|
+
setPendingModel(target);
|
|
40
|
+
setApiKeyReturnOverlay(returnTo);
|
|
41
|
+
setOverlay("apiKey");
|
|
42
|
+
}, []);
|
|
43
|
+
const saveApiKey = useCallback((apiKey) => {
|
|
44
|
+
if (!pendingModel)
|
|
45
|
+
return;
|
|
46
|
+
const updated = {
|
|
47
|
+
...settings,
|
|
48
|
+
apiKeys: { ...settings.apiKeys, [pendingModel.provider]: apiKey },
|
|
49
|
+
};
|
|
50
|
+
session.updateSettings(updated);
|
|
51
|
+
setSettings(updated);
|
|
52
|
+
session.setModel(pendingModel);
|
|
53
|
+
setModel(pendingModel);
|
|
54
|
+
setPendingModel(null);
|
|
55
|
+
setOverlay(apiKeyReturnOverlay);
|
|
56
|
+
setApiKeyReturnOverlay("none");
|
|
57
|
+
setModelFilter(undefined);
|
|
58
|
+
}, [pendingModel, settings, session, apiKeyReturnOverlay]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (startupChecked.current)
|
|
61
|
+
return;
|
|
62
|
+
startupChecked.current = true;
|
|
63
|
+
const current = session.getModel();
|
|
64
|
+
if (!hasProviderAuth(current.provider, settings)) {
|
|
65
|
+
openApiKeyPrompt(current, "none");
|
|
66
|
+
}
|
|
67
|
+
}, [session, settings, openApiKeyPrompt]);
|
|
30
68
|
useEffect(() => {
|
|
31
69
|
const handler = (event) => {
|
|
32
70
|
switch (event.type) {
|
|
@@ -75,6 +113,9 @@ export function App({ session, workdir, onQuit }) {
|
|
|
75
113
|
streamingRef.current = "";
|
|
76
114
|
setStreamingText("");
|
|
77
115
|
setRunning(false);
|
|
116
|
+
if (/Missing .*API_KEY/i.test(event.message)) {
|
|
117
|
+
openApiKeyPrompt(model, "none");
|
|
118
|
+
}
|
|
78
119
|
break;
|
|
79
120
|
case "model_changed":
|
|
80
121
|
setModel(event.model);
|
|
@@ -85,14 +126,14 @@ export function App({ session, workdir, onQuit }) {
|
|
|
85
126
|
return () => {
|
|
86
127
|
session.off("event", handler);
|
|
87
128
|
};
|
|
88
|
-
}, [session]);
|
|
129
|
+
}, [session, model, openApiKeyPrompt]);
|
|
89
130
|
useInput((_, key) => {
|
|
90
131
|
if (overlay !== "none")
|
|
91
132
|
return;
|
|
92
133
|
if (key.escape && running) {
|
|
93
134
|
session.abort();
|
|
94
135
|
}
|
|
95
|
-
});
|
|
136
|
+
}, { isActive: overlay === "none" });
|
|
96
137
|
const handleSubmit = useCallback(async (value) => {
|
|
97
138
|
if (value === "/quit") {
|
|
98
139
|
onQuit();
|
|
@@ -117,13 +158,16 @@ export function App({ session, workdir, onQuit }) {
|
|
|
117
158
|
}
|
|
118
159
|
if (running)
|
|
119
160
|
return;
|
|
161
|
+
if (!hasProviderAuth(model.provider, settings)) {
|
|
162
|
+
openApiKeyPrompt(model, "none");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
120
165
|
await session.prompt(value);
|
|
121
|
-
}, [session, running, onQuit, exit]);
|
|
166
|
+
}, [session, running, onQuit, exit, model, settings, openApiKeyPrompt]);
|
|
122
167
|
const hasChat = displayMessages.length > 0 || streamingText.length > 0;
|
|
123
168
|
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Box, { paddingX: 2, marginBottom: 1, children: _jsx(StartupBanner, { theme: theme, compact: hasChat }) }), _jsx(ChatView, { messages: displayMessages, theme: theme, model: model, streamingText: streamingText, running: running }), _jsx(Footer, { workdir: workdir, model: model, theme: theme }), overlay === "none" && (_jsx(Editor, { theme: theme, model: model, disabled: running, running: running, onSubmit: handleSubmit })), overlay === "model" && (_jsx(ModelSelector, { theme: theme, settings: settings, filter: modelFilter, onSelect: (m) => {
|
|
124
169
|
if (!hasProviderAuth(m.provider, settings)) {
|
|
125
|
-
|
|
126
|
-
setOverlay("apiKey");
|
|
170
|
+
openApiKeyPrompt(m, "model");
|
|
127
171
|
return;
|
|
128
172
|
}
|
|
129
173
|
session.setModel(m);
|
|
@@ -133,24 +177,14 @@ export function App({ session, workdir, onQuit }) {
|
|
|
133
177
|
}, onClose: () => {
|
|
134
178
|
setOverlay("none");
|
|
135
179
|
setModelFilter(undefined);
|
|
136
|
-
} })), overlay === "apiKey" && pendingModel && (_jsx(ApiKeyPrompt, { theme: theme, provider: pendingModel.provider, model: pendingModel, onSubmit: (
|
|
137
|
-
const updated = {
|
|
138
|
-
...settings,
|
|
139
|
-
apiKeys: { ...settings.apiKeys, [pendingModel.provider]: apiKey },
|
|
140
|
-
};
|
|
141
|
-
session.updateSettings(updated);
|
|
142
|
-
setSettings(updated);
|
|
143
|
-
session.setModel(pendingModel);
|
|
144
|
-
setModel(pendingModel);
|
|
145
|
-
setPendingModel(null);
|
|
146
|
-
setOverlay("none");
|
|
147
|
-
setModelFilter(undefined);
|
|
148
|
-
}, onCancel: () => {
|
|
180
|
+
} })), overlay === "apiKey" && pendingModel && (_jsx(ApiKeyPrompt, { theme: theme, provider: pendingModel.provider, model: pendingModel, onSubmit: saveApiKey, onCancel: () => {
|
|
149
181
|
setPendingModel(null);
|
|
150
|
-
setOverlay(
|
|
182
|
+
setOverlay(apiKeyReturnOverlay);
|
|
183
|
+
setApiKeyReturnOverlay("none");
|
|
151
184
|
} })), overlay === "settings" && (_jsx(SettingsView, { theme: theme, settings: settings, onUpdate: (s) => {
|
|
152
185
|
session.updateSettings(s);
|
|
153
186
|
setSettings(s);
|
|
154
|
-
|
|
187
|
+
}, onSetApiKey: (provider) => {
|
|
188
|
+
openApiKeyPrompt(modelForProvider(provider, settings), "settings");
|
|
155
189
|
}, onClose: () => setOverlay("none") }))] }));
|
|
156
190
|
}
|
package/dist/ui/Editor.js
CHANGED
|
@@ -71,7 +71,7 @@ export function Editor({ theme, model, disabled, running, onSubmit }) {
|
|
|
71
71
|
setValue(newVal);
|
|
72
72
|
updateSuggestions(newVal);
|
|
73
73
|
}
|
|
74
|
-
});
|
|
74
|
+
}, { isActive: !disabled });
|
|
75
75
|
const placeholder = "Ask anything…";
|
|
76
76
|
const showCursor = !disabled && cursorOn;
|
|
77
77
|
return (_jsxs(Box, { flexDirection: "column", marginX: 2, children: [suggestions.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginBottom: 1, children: suggestions.map((s) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.primary, children: s.cmd }), _jsxs(Text, { color: theme.textMuted, children: [" \u2014 ", s.desc] })] }, s.cmd))) })), _jsxs(Panel, { theme: theme, borderColor: disabled ? theme.border : theme.primary, marginBottom: 0, children: [_jsx(Box, { flexDirection: "row", children: value.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text, children: value }), _jsx(BlinkingCursor, { theme: theme, visible: showCursor })] })) : showCursor ? (_jsx(BlinkingCursor, { theme: theme, visible: true })) : (_jsx(Text, { color: theme.textMuted, children: placeholder })) }), _jsxs(Text, { color: theme.textMuted, children: ["agent-dev \u00B7 ", _jsx(Text, { color: theme.text, children: modelRef(model) })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: running ? (_jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[spinIdx] }), " ", "esc interrupt"] })) : (_jsx(Text, { color: theme.textMuted, children: "Tab completes /commands" })) })] }));
|
package/dist/ui/ModelSelector.js
CHANGED
|
@@ -30,7 +30,7 @@ export function ModelSelector({ theme, settings, filter, onSelect, onClose }) {
|
|
|
30
30
|
if (key.return && filtered[safeIndex]) {
|
|
31
31
|
onSelect(filtered[safeIndex]);
|
|
32
32
|
}
|
|
33
|
-
});
|
|
33
|
+
}, { isActive: true });
|
|
34
34
|
const providers = ["openai", "groq", "gemini", "free"];
|
|
35
35
|
let lastProvider;
|
|
36
36
|
return (_jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(LeftBorder, { theme: theme, borderColor: theme.borderActive, children: [_jsx(Text, { color: theme.text, bold: true, children: "/model" }), _jsx(Text, { color: theme.textMuted, children: " \u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc close" }), _jsx(Text, { color: theme.textMuted, children: " Models without a key will prompt for an API key" }), filter && _jsxs(Text, { color: theme.textMuted, children: [" filter: ", filter] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [filtered.length === 0 && _jsx(Text, { color: theme.textMuted, children: "No models match" }), filtered.map((m, i) => {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ThemeColors } from "./theme.js";
|
|
3
3
|
import type { Settings } from "../config/settings.js";
|
|
4
|
+
import type { ProviderId } from "../providers/types.js";
|
|
4
5
|
interface SettingsViewProps {
|
|
5
6
|
theme: ThemeColors;
|
|
6
7
|
settings: Settings;
|
|
7
8
|
onUpdate: (settings: Settings) => void;
|
|
9
|
+
onSetApiKey: (provider: ProviderId) => void;
|
|
8
10
|
onClose: () => void;
|
|
9
11
|
}
|
|
10
|
-
export declare function SettingsView({ theme, settings, onUpdate, onClose }: SettingsViewProps): React.JSX.Element;
|
|
12
|
+
export declare function SettingsView({ theme, settings, onUpdate, onSetApiKey, onClose }: SettingsViewProps): React.JSX.Element;
|
|
11
13
|
export {};
|
package/dist/ui/SettingsView.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import { PROVIDER_ENV_VARS } from "../providers/registry.js";
|
|
4
|
+
import { PROVIDER_ENV_VARS, hasProviderAuth } from "../providers/registry.js";
|
|
5
5
|
import { LeftBorder } from "./LeftBorder.js";
|
|
6
6
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
7
|
-
|
|
7
|
+
const PROVIDERS = ["openai", "groq", "gemini", "free"];
|
|
8
|
+
export function SettingsView({ theme, settings, onUpdate, onSetApiKey, onClose }) {
|
|
9
|
+
const items = ["thinkingLevel", ...PROVIDERS];
|
|
8
10
|
const [index, setIndex] = useState(0);
|
|
9
|
-
const items = ["thinkingLevel", "envKeys"];
|
|
10
11
|
useInput((_, key) => {
|
|
11
12
|
if (key.escape)
|
|
12
13
|
onClose();
|
|
@@ -21,12 +22,17 @@ export function SettingsView({ theme, settings, onUpdate, onClose }) {
|
|
|
21
22
|
const next = THINKING_LEVELS[(cur + 1) % THINKING_LEVELS.length];
|
|
22
23
|
onUpdate({ ...settings, thinkingLevel: next });
|
|
23
24
|
}
|
|
25
|
+
else {
|
|
26
|
+
onSetApiKey(item);
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
}, { isActive: true });
|
|
30
|
+
return (_jsx(Box, { paddingX: 2, marginTop: 1, marginBottom: 1, children: _jsxs(LeftBorder, { theme: theme, borderColor: theme.borderActive, children: [_jsx(Text, { color: theme.text, bold: true, children: "/settings" }), _jsx(Text, { color: theme.textMuted, children: " \u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc close" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: index === 0 ? theme.primary : theme.text, children: [index === 0 ? "› " : " ", "Thinking:", " ", _jsx(Text, { bold: true, children: settings.thinkingLevel }), index === 0 && _jsx(Text, { color: theme.textMuted, children: " (Enter to cycle)" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.textMuted, children: "API keys \u2014 Enter to set / update" }) }), PROVIDERS.map((p, i) => {
|
|
31
|
+
const idx = i + 1;
|
|
28
32
|
const vars = PROVIDER_ENV_VARS[p];
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
const fromEnv = vars.some((v) => !!process.env[v]);
|
|
34
|
+
const fromSettings = !!settings.apiKeys?.[p];
|
|
35
|
+
const ok = hasProviderAuth(p, settings);
|
|
36
|
+
return (_jsxs(Text, { color: index === idx ? theme.primary : theme.text, children: [index === idx ? "› " : " ", ok ? "✓" : "○", " ", p, fromEnv && _jsx(Text, { color: theme.textMuted, children: " env" }), fromSettings && _jsx(Text, { color: theme.textMuted, children: " saved" })] }, p));
|
|
37
|
+
})] })] }) }));
|
|
32
38
|
}
|