mini-coder 0.7.3 → 0.8.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/AGENTS.md +114 -0
- package/README.md +53 -66
- package/bin/mini-coder.ts +2 -0
- package/demo.gif +0 -0
- package/package.json +17 -20
- package/src/agent.ts +177 -255
- package/src/cli.ts +101 -0
- package/src/config.ts +150 -0
- package/src/prompt.ts +54 -207
- package/src/session.ts +124 -69
- package/src/tools/bash.ts +89 -0
- package/src/tools/common.ts +32 -0
- package/src/tools/edit.ts +41 -0
- package/src/tools/index.ts +47 -0
- package/src/tools/read.ts +64 -0
- package/src/tui/commands.ts +63 -0
- package/src/tui/editor.ts +291 -0
- package/src/tui/highlight.ts +189 -0
- package/src/tui/stream.ts +142 -0
- package/src/tui/styles.ts +42 -0
- package/src/tui/term.ts +436 -0
- package/src/tui/theme.ts +121 -0
- package/src/tui/tui.ts +595 -0
- package/src/tui/usage.ts +67 -0
- package/tsconfig.json +8 -8
- package/bin/mc.ts +0 -11
- package/bun.lock +0 -346
- package/nono-mini-coder.json +0 -42
- package/src/args.ts +0 -300
- package/src/error-handling.test.ts +0 -163
- package/src/git.ts +0 -23
- package/src/headless.ts +0 -66
- package/src/index.ts +0 -43
- package/src/oauth.ts +0 -157
- package/src/shared.ts +0 -119
- package/src/themes.ts +0 -234
- package/src/tool-bash.ts +0 -77
- package/src/tool-edit.ts +0 -121
- package/src/tool-read.ts +0 -100
- package/src/tui-components.ts +0 -127
- package/src/tui-conversation.ts +0 -218
- package/src/tui-editor.ts +0 -29
- package/src/tui-overlay.ts +0 -618
- package/src/tui.ts +0 -314
- package/src/types.ts +0 -194
- package/src/update.ts +0 -171
package/src/agent.ts
CHANGED
|
@@ -1,287 +1,209 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type Api,
|
|
2
3
|
type AssistantMessage,
|
|
3
|
-
type
|
|
4
|
-
type ImageContent,
|
|
4
|
+
type JsonObject,
|
|
5
5
|
type Message,
|
|
6
|
-
|
|
7
|
-
type
|
|
6
|
+
type Model,
|
|
7
|
+
type ModelThinkingLevel,
|
|
8
|
+
type Models,
|
|
9
|
+
type Tool,
|
|
8
10
|
type ToolCall,
|
|
9
11
|
type ToolResultMessage,
|
|
12
|
+
type UserMessage,
|
|
10
13
|
} from "@earendil-works/pi-ai";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (messages[i].role !== "toolResult") continue;
|
|
34
|
-
const content = (messages[i] as ToolResultMessage).content;
|
|
35
|
-
if (
|
|
36
|
-
content.length === 1 &&
|
|
37
|
-
content[0].type === "text" &&
|
|
38
|
-
content[0].text.startsWith("Old environment output:")
|
|
39
|
-
) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
toolResultIndices.push(i);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (toolResultIndices.length <= KEEP_OBSERVATIONS) return;
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < toolResultIndices.length - KEEP_OBSERVATIONS; i++) {
|
|
48
|
-
const idx = toolResultIndices[i];
|
|
49
|
-
const msg = messages[idx] as ToolResultMessage;
|
|
50
|
-
let lines = 0;
|
|
51
|
-
let images = 0;
|
|
52
|
-
for (const c of msg.content) {
|
|
53
|
-
if (c.type === "text") {
|
|
54
|
-
lines += c.text.split(/\r?\n/).length;
|
|
55
|
-
} else if (c.type === "image") {
|
|
56
|
-
images++;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
let text = `Old environment output: (${lines} lines omitted)`;
|
|
60
|
-
if (images > 0) {
|
|
61
|
-
text += ` (${images} image${images > 1 ? "s" : ""} omitted)`;
|
|
62
|
-
}
|
|
63
|
-
msg.content = [{ type: "text", text }];
|
|
64
|
-
}
|
|
14
|
+
import type { Session } from "./session.ts";
|
|
15
|
+
import { acceptsImages, executeTool, type ToolDetails, type ToolResult } from "./tools/index.ts";
|
|
16
|
+
|
|
17
|
+
export type Phase = "preparing" | "waitingModel" | "streaming" | "runningTool" | "pausing" | "idle";
|
|
18
|
+
|
|
19
|
+
export type AgentEvent =
|
|
20
|
+
| { type: "phase"; phase: Phase; detail?: string }
|
|
21
|
+
| { type: "text"; delta: string }
|
|
22
|
+
| { type: "reasoning"; delta: string }
|
|
23
|
+
| { type: "toolCallStart"; name: string }
|
|
24
|
+
| { type: "toolCall"; name: string; arguments: JsonObject }
|
|
25
|
+
| { type: "toolOutput"; chunk: string }
|
|
26
|
+
| { type: "toolResult"; name: string; text: string; isError: boolean }
|
|
27
|
+
| { type: "message"; message: AssistantMessage }
|
|
28
|
+
| { type: "error"; message: string }
|
|
29
|
+
| { type: "cancelled" }
|
|
30
|
+
| { type: "complete" };
|
|
31
|
+
|
|
32
|
+
interface Interaction {
|
|
33
|
+
isPauseRequested(): boolean;
|
|
34
|
+
clearPause(): void;
|
|
35
|
+
requestSteering(): Promise<string>;
|
|
65
36
|
}
|
|
66
37
|
|
|
67
|
-
export
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
reasoning: agentCtx.options.effort,
|
|
83
|
-
signal: agentCtx.signal,
|
|
84
|
-
apiKey,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
let partial: AssistantMessage | null = null;
|
|
88
|
-
let added = false;
|
|
89
|
-
|
|
90
|
-
for await (const e of s) {
|
|
91
|
-
switch (e.type) {
|
|
92
|
-
case "start":
|
|
93
|
-
partial = e.partial;
|
|
94
|
-
llmCtx.messages.push(e.partial);
|
|
95
|
-
added = true;
|
|
96
|
-
yield { type: "message_start", partial };
|
|
97
|
-
break;
|
|
98
|
-
|
|
99
|
-
case "text_start":
|
|
100
|
-
case "text_delta":
|
|
101
|
-
case "text_end":
|
|
102
|
-
case "thinking_start":
|
|
103
|
-
case "thinking_delta":
|
|
104
|
-
case "thinking_end":
|
|
105
|
-
case "toolcall_start":
|
|
106
|
-
case "toolcall_delta":
|
|
107
|
-
case "toolcall_end": {
|
|
108
|
-
if (partial) {
|
|
109
|
-
partial = e.partial;
|
|
110
|
-
llmCtx.messages[llmCtx.messages.length - 1] = partial;
|
|
111
|
-
yield { type: "message_update", partial };
|
|
112
|
-
}
|
|
38
|
+
export const NO_INTERACTION: Interaction = {
|
|
39
|
+
isPauseRequested: () => false,
|
|
40
|
+
clearPause: () => {},
|
|
41
|
+
requestSteering: async () => "",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** What one turn needs, shared by both projections: the TUI and `--print`. */
|
|
45
|
+
export interface AgentOptions {
|
|
46
|
+
models: Models;
|
|
47
|
+
model: Model<Api>;
|
|
48
|
+
systemPrompt: string;
|
|
49
|
+
tools: Tool[];
|
|
50
|
+
thinkingEffort: ModelThinkingLevel;
|
|
51
|
+
session: Session;
|
|
52
|
+
}
|
|
113
53
|
|
|
114
|
-
|
|
115
|
-
|
|
54
|
+
interface AgentRun extends AgentOptions {
|
|
55
|
+
messages: Message[];
|
|
56
|
+
signal: AbortSignal;
|
|
57
|
+
interaction: Interaction;
|
|
58
|
+
onEvent: (event: AgentEvent) => void;
|
|
59
|
+
}
|
|
116
60
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (added) {
|
|
120
|
-
llmCtx.messages[llmCtx.messages.length - 1] = finalMessage;
|
|
121
|
-
} else {
|
|
122
|
-
llmCtx.messages.push(finalMessage);
|
|
123
|
-
yield { type: "message_start", partial: { ...finalMessage } };
|
|
124
|
-
}
|
|
61
|
+
export async function runAgentTurn(run: AgentRun): Promise<void> {
|
|
62
|
+
const { messages, session, signal, interaction, onEvent } = run;
|
|
125
63
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
64
|
+
/** Appends a user message the model reads at the next step boundary. */
|
|
65
|
+
const steer = (content: string): void => {
|
|
66
|
+
if (content === "") return;
|
|
67
|
+
const message: UserMessage = { role: "user", content, timestamp: Date.now() };
|
|
68
|
+
messages.push(message);
|
|
69
|
+
session.appendMessage(message);
|
|
70
|
+
};
|
|
130
71
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Returns the steering typed while paused, or ""; the caller decides where
|
|
74
|
+
* it lands, because a user message between an assistant turn and its tool
|
|
75
|
+
* results is rejected by the provider. Null once the turn has been aborted;
|
|
76
|
+
* every step boundary is a cancellation point.
|
|
77
|
+
*/
|
|
78
|
+
const pauseStep = async (): Promise<string | null> => {
|
|
79
|
+
if (signal.aborted) return null;
|
|
80
|
+
if (interaction.isPauseRequested()) {
|
|
81
|
+
interaction.clearPause();
|
|
82
|
+
onEvent({ type: "phase", phase: "pausing" });
|
|
83
|
+
const steering = await interaction.requestSteering();
|
|
84
|
+
onEvent({ type: "phase", phase: "idle" });
|
|
85
|
+
if (signal.aborted) return null;
|
|
86
|
+
return steering;
|
|
136
87
|
}
|
|
88
|
+
return "";
|
|
89
|
+
};
|
|
137
90
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
yield { type: "message_start", partial: { ...message } };
|
|
91
|
+
for (;;) {
|
|
92
|
+
const steering = await pauseStep();
|
|
93
|
+
if (steering === null) {
|
|
94
|
+
onEvent({ type: "cancelled" });
|
|
95
|
+
return;
|
|
144
96
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
97
|
+
steer(steering);
|
|
98
|
+
|
|
99
|
+
onEvent({ type: "phase", phase: "preparing" });
|
|
100
|
+
session.appendRequest({
|
|
101
|
+
provider: run.model.provider,
|
|
102
|
+
model: run.model.id,
|
|
103
|
+
api: run.model.api,
|
|
104
|
+
thinkingEffort: run.thinkingEffort,
|
|
105
|
+
systemPrompt: run.systemPrompt,
|
|
106
|
+
tools: run.tools,
|
|
107
|
+
});
|
|
148
108
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
109
|
+
onEvent({ type: "phase", phase: "waitingModel" });
|
|
110
|
+
const context = { systemPrompt: run.systemPrompt, tools: run.tools, messages };
|
|
111
|
+
let stream;
|
|
112
|
+
try {
|
|
113
|
+
stream = run.models.streamSimple(run.model, context, {
|
|
114
|
+
reasoning: run.thinkingEffort === "off" ? undefined : run.thinkingEffort,
|
|
115
|
+
signal,
|
|
116
|
+
sessionId: session.id ?? undefined,
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
onEvent({ type: "error", message: (error as Error).message });
|
|
120
|
+
return;
|
|
152
121
|
}
|
|
153
122
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
yield {
|
|
173
|
-
type: "tool_message_update",
|
|
174
|
-
partial: e.partial,
|
|
175
|
-
};
|
|
176
|
-
} else {
|
|
177
|
-
llmCtx.messages.push(e.partial);
|
|
178
|
-
seenToolIds.set(e.partial.toolCallId, llmCtx.messages.length - 1);
|
|
179
|
-
|
|
180
|
-
yield {
|
|
181
|
-
type: "tool_message_start",
|
|
182
|
-
partial: e.partial,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
} else if (e.type === "tool_result") {
|
|
186
|
-
// Update context with full message and yield
|
|
187
|
-
const idx = llmCtx.messages.findIndex(
|
|
188
|
-
(m) =>
|
|
189
|
-
m.role === "toolResult" && m.toolCallId === e.message.toolCallId,
|
|
190
|
-
);
|
|
191
|
-
if (idx >= 0) {
|
|
192
|
-
llmCtx.messages[idx] = e.message;
|
|
193
|
-
} else {
|
|
194
|
-
llmCtx.messages.push(e.message);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
yield {
|
|
198
|
-
type: "tool_message_end",
|
|
199
|
-
message: e.message,
|
|
200
|
-
};
|
|
123
|
+
onEvent({ type: "phase", phase: "streaming" });
|
|
124
|
+
try {
|
|
125
|
+
for await (const event of stream) {
|
|
126
|
+
if (event.type === "text_delta") onEvent({ type: "text", delta: event.delta });
|
|
127
|
+
else if (event.type === "thinking_delta") onEvent({ type: "reasoning", delta: event.delta });
|
|
128
|
+
// The tool's name is known as soon as the call starts being written.
|
|
129
|
+
else if (event.type === "toolcall_start") {
|
|
130
|
+
const block = event.partial.content[event.contentIndex];
|
|
131
|
+
if (block.type === "toolCall") onEvent({ type: "toolCallStart", name: block.name });
|
|
132
|
+
} else if (event.type === "toolcall_end") {
|
|
133
|
+
onEvent({
|
|
134
|
+
type: "toolCall",
|
|
135
|
+
name: event.toolCall.name,
|
|
136
|
+
arguments: event.toolCall.arguments,
|
|
137
|
+
});
|
|
201
138
|
}
|
|
202
139
|
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
onEvent({ type: "error", message: (error as Error).message });
|
|
142
|
+
return;
|
|
203
143
|
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
144
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
tools: ToolAndRunner[],
|
|
212
|
-
signal?: AbortSignal,
|
|
213
|
-
): AsyncGenerator<AgentToolEvent> {
|
|
214
|
-
for (const call of toolCalls) {
|
|
215
|
-
const timestamp = Date.now();
|
|
216
|
-
const tool = tools.find((t) => t.tool.name === call.name);
|
|
145
|
+
const assistant = await stream.result();
|
|
146
|
+
messages.push(assistant);
|
|
147
|
+
session.appendMessage(assistant);
|
|
148
|
+
onEvent({ type: "message", message: assistant });
|
|
217
149
|
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
content: [{ type: "text", text: "Error: Tool not found" }],
|
|
226
|
-
isError: true,
|
|
227
|
-
timestamp,
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
continue;
|
|
150
|
+
if (assistant.stopReason === "aborted") {
|
|
151
|
+
onEvent({ type: "cancelled" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (assistant.stopReason === "error") {
|
|
155
|
+
onEvent({ type: "error", message: assistant.errorMessage ?? "provider error" });
|
|
156
|
+
return;
|
|
231
157
|
}
|
|
232
158
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (e.type === "output") {
|
|
239
|
-
// handle deltas
|
|
240
|
-
yield {
|
|
241
|
-
type: "tool_update",
|
|
242
|
-
partial: {
|
|
243
|
-
role: "toolResult",
|
|
244
|
-
toolCallId: call.id,
|
|
245
|
-
toolName: call.name,
|
|
246
|
-
content: [content],
|
|
247
|
-
isError: false,
|
|
248
|
-
timestamp,
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
} else if (e.type === "result") {
|
|
252
|
-
// handle final message, and check for images
|
|
253
|
-
let img: ImageContent | null = null;
|
|
254
|
-
if (e.image) {
|
|
255
|
-
img = { ...e.image, type: "image" };
|
|
256
|
-
}
|
|
159
|
+
const toolCalls = assistant.content.filter((block): block is ToolCall => block.type === "toolCall");
|
|
160
|
+
if (toolCalls.length === 0) {
|
|
161
|
+
onEvent({ type: "complete" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
257
164
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
165
|
+
// Steering is held until every result of this assistant turn has landed:
|
|
166
|
+
// the provider rejects a user message between an assistant turn and its
|
|
167
|
+
// tool results.
|
|
168
|
+
const held: string[] = [];
|
|
169
|
+
for (const call of toolCalls) {
|
|
170
|
+
const steering = await pauseStep();
|
|
171
|
+
if (steering === null) {
|
|
172
|
+
onEvent({ type: "cancelled" });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (steering !== "") held.push(steering);
|
|
176
|
+
onEvent({ type: "phase", phase: "runningTool", detail: call.name });
|
|
177
|
+
|
|
178
|
+
const tool = run.tools.find((candidate) => candidate.name === call.name);
|
|
179
|
+
let result: ToolResult;
|
|
180
|
+
if (tool === undefined) {
|
|
181
|
+
result = { text: `unknown tool: ${call.name}`, isError: true };
|
|
182
|
+
} else {
|
|
183
|
+
try {
|
|
184
|
+
result = await executeTool(call, {
|
|
185
|
+
signal,
|
|
186
|
+
supportsImages: acceptsImages(run.model),
|
|
187
|
+
onOutput: (chunk) => onEvent({ type: "toolOutput", chunk }),
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
result = { text: (error as Error).message, isError: true };
|
|
269
191
|
}
|
|
270
192
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
content: [{ type: "text", text: `Error: ${error}` }],
|
|
281
|
-
isError: true,
|
|
282
|
-
timestamp,
|
|
283
|
-
},
|
|
193
|
+
|
|
194
|
+
const toolMessage: ToolResultMessage<ToolDetails> = {
|
|
195
|
+
role: "toolResult",
|
|
196
|
+
toolCallId: call.id,
|
|
197
|
+
toolName: call.name,
|
|
198
|
+
content: [{ type: "text", text: result.text }, ...(result.images ?? [])],
|
|
199
|
+
details: result.details,
|
|
200
|
+
isError: result.isError,
|
|
201
|
+
timestamp: Date.now(),
|
|
284
202
|
};
|
|
203
|
+
messages.push(toolMessage);
|
|
204
|
+
session.appendMessage(toolMessage);
|
|
205
|
+
onEvent({ type: "toolResult", name: call.name, text: result.text, isError: result.isError });
|
|
285
206
|
}
|
|
207
|
+
steer(held.join("\n"));
|
|
286
208
|
}
|
|
287
209
|
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { clampThinkingLevel, type AssistantMessage, type Message, type UserMessage } from "@earendil-works/pi-ai";
|
|
3
|
+
import { loadConfig, resolveModel } from "./config.ts";
|
|
4
|
+
import { buildSystemPrompt } from "./prompt.ts";
|
|
5
|
+
import { acceptsImages, toolSchemas } from "./tools/index.ts";
|
|
6
|
+
import { Session } from "./session.ts";
|
|
7
|
+
import { NO_INTERACTION, runAgentTurn, type AgentOptions } from "./agent.ts";
|
|
8
|
+
import { runTui } from "./tui/tui.ts";
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv: string[]): { print: string | null } {
|
|
11
|
+
let print: string | null = null;
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const match = /^(?:-p|--print)(?:=(.*))?$/.exec(argv[i]);
|
|
14
|
+
if (match === null) throw new Error(`unknown argument: ${argv[i]}`);
|
|
15
|
+
const value = match[1] ?? argv[++i];
|
|
16
|
+
if (value === undefined) throw new Error(`${argv[i - 1]} requires a prompt`);
|
|
17
|
+
print = value;
|
|
18
|
+
}
|
|
19
|
+
return { print };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runPrint(prompt: string, ctx: AgentOptions): Promise<number> {
|
|
23
|
+
const messages: Message[] = [];
|
|
24
|
+
const user: UserMessage = { role: "user", content: prompt, timestamp: Date.now() };
|
|
25
|
+
messages.push(user);
|
|
26
|
+
ctx.session.appendMessage(user);
|
|
27
|
+
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const onSignal = () => controller.abort();
|
|
30
|
+
process.on("SIGINT", onSignal);
|
|
31
|
+
process.on("SIGTERM", onSignal);
|
|
32
|
+
|
|
33
|
+
let failed = false;
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
try {
|
|
36
|
+
await runAgentTurn({
|
|
37
|
+
...ctx,
|
|
38
|
+
messages,
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
interaction: NO_INTERACTION,
|
|
41
|
+
onEvent: (event) => {
|
|
42
|
+
if (event.type === "toolCall") process.stderr.write(`[tool] ${event.name}\n`);
|
|
43
|
+
else if (event.type === "toolOutput") process.stderr.write(event.chunk);
|
|
44
|
+
else if (event.type === "error") {
|
|
45
|
+
failed = true;
|
|
46
|
+
process.stderr.write(`[error] ${event.message}\n`);
|
|
47
|
+
} else if (event.type === "cancelled") {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
process.stderr.write("[cancelled]\n");
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
failed = true;
|
|
55
|
+
process.stderr.write(`[error] ${(error as Error).message}\n`);
|
|
56
|
+
} finally {
|
|
57
|
+
process.off("SIGINT", onSignal);
|
|
58
|
+
process.off("SIGTERM", onSignal);
|
|
59
|
+
ctx.session.close();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const last = messages.filter((message): message is AssistantMessage => message.role === "assistant").at(-1);
|
|
63
|
+
if (!failed && !cancelled && last !== undefined) {
|
|
64
|
+
const text = last.content
|
|
65
|
+
.filter((block) => block.type === "text")
|
|
66
|
+
.map((block) => block.text)
|
|
67
|
+
.join("");
|
|
68
|
+
if (text !== "") process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
69
|
+
}
|
|
70
|
+
return failed || cancelled ? 1 : 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main(): Promise<void> {
|
|
74
|
+
const args = parseArgs(process.argv.slice(2));
|
|
75
|
+
const config = loadConfig();
|
|
76
|
+
const { models, model } = resolveModel(config);
|
|
77
|
+
const session = new Session(config.sessionsDir, process.cwd());
|
|
78
|
+
const ctx: AgentOptions = {
|
|
79
|
+
models,
|
|
80
|
+
model,
|
|
81
|
+
systemPrompt: buildSystemPrompt(config),
|
|
82
|
+
tools: toolSchemas(config.tools, acceptsImages(model)),
|
|
83
|
+
thinkingEffort: clampThinkingLevel(model, config.thinkingEffort),
|
|
84
|
+
session,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (args.print !== null) {
|
|
88
|
+
process.exitCode = await runPrint(args.print, ctx);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
93
|
+
throw new Error("interactive mode requires a TTY; use -p for non-interactive mode");
|
|
94
|
+
}
|
|
95
|
+
await runTui(ctx);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch((error) => {
|
|
99
|
+
process.stderr.write(`${(error as Error).message}\n`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|