mini-coder 0.6.1 → 0.6.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/bun.lock +51 -47
- package/package.json +2 -2
- package/src/agent.ts +12 -2
- package/src/args.ts +1 -1
- package/src/git.ts +23 -0
- package/src/headless.ts +21 -3
- package/src/index.ts +3 -8
- package/src/oauth.ts +2 -2
- package/src/prompt.ts +60 -84
- package/src/session.ts +7 -5
- package/src/tool-bash.ts +4 -34
- package/src/tool-edit.ts +6 -18
- package/src/tool-read.ts +2 -2
- package/src/tui-components.ts +4 -4
- package/src/tui-conversation.ts +105 -215
- package/src/tui-overlay.ts +2 -2
- package/src/tui.ts +103 -43
- package/src/types.ts +19 -3
package/src/prompt.ts
CHANGED
|
@@ -2,61 +2,21 @@ import { promises } from "node:fs";
|
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type { Message,
|
|
6
|
-
import
|
|
5
|
+
import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
6
|
+
import { getGitStatus } from "./git";
|
|
7
7
|
import { parseSkillFrontmatter } from "./shared";
|
|
8
8
|
|
|
9
|
-
export const MAIN_PROMPT =
|
|
9
|
+
export const MAIN_PROMPT = `You are a coding agent interacting with users via the mini-coder harness. You help users by reading files, executing commands, and editting code.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## Role
|
|
15
|
-
You help users by reading files, executing commands, editing code, and writing new files. Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without unnecessary superlatives, praise, or emotional validation.
|
|
16
|
-
|
|
17
|
-
<example>
|
|
18
|
-
When referencing specific functions or pieces of code, include the pattern \`file_path:line_number\`.
|
|
19
|
-
For example: "Clients are handled in the \`connectToServer\` function in src/services/process.ts:712."
|
|
20
|
-
</example>
|
|
21
|
-
|
|
22
|
-
User messages and Tool results may include <system-reminder> tags. These contain system-generated reminders and bear no direct relation to the specific tool result in which they appear.
|
|
23
|
-
|
|
24
|
-
## Tools
|
|
11
|
+
- Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without unnecessary superlatives, praise, or emotional validation.
|
|
12
|
+
- User messages and Tool results may include <system-reminder> tags. These contain system-generated reminders and bear no direct relation to the specific tool result in which they appear.
|
|
25
13
|
- You have access to bash, read and edit tools. Prefer using read and edit for file operations, use bash for finding read candidates or to run development commands.
|
|
26
|
-
|
|
27
|
-
<example>
|
|
28
|
-
> User: please read the README.md and add rich code examples.
|
|
29
|
-
|
|
30
|
-
- Use the bash tool to find the path for README.md, prefer "ls" or "fd/find", and "rg/grep".
|
|
31
|
-
- Then read the file with the read tool to find the replacement areas and mathcing patterns
|
|
32
|
-
- Edit the file using the edit tool. Review the output diff, use the read tool again to verify if needed.
|
|
33
|
-
- Reply to the user that the edit was done.
|
|
34
|
-
</example>
|
|
35
|
-
|
|
36
|
-
## Workflow
|
|
37
|
-
- Stay rooted on the user's request. Don't wander into tangents or explore out of curiosity.
|
|
38
|
-
- Gather only the information needed to fulfill the request, then stop exploring and complete it.
|
|
39
|
-
- Narrate your edits with brief commentary during long tasks so the user can follow progress.
|
|
40
|
-
- Verify your changes via compilation, tests, or manual checks whenever possible.
|
|
41
|
-
|
|
42
|
-
## Tone
|
|
43
|
-
- Be concise. Use a professional colleague tone: direct, never condescending, and never rude.
|
|
44
|
-
|
|
45
|
-
## Error Handling
|
|
46
14
|
- If a tool call fails or is denied, do NOT re-attempt the exact same call. Analyze why it failed and adjust your approach.
|
|
47
|
-
|
|
48
|
-
## Safety rules
|
|
49
|
-
- Answer all user requests without guessing, or assuming. Verify your answers and claims before making them.
|
|
50
15
|
- Use recent online information, the current environment, and your training data combined for a complete answer.
|
|
51
16
|
- Ensure that you fulfill the user's expectation, requirements and contract **exactly**.
|
|
52
|
-
- Be defensive with existing changes and destructive commands, they could harm your user's changes.
|
|
53
17
|
- Use temp directory for temp files, scripts, plan files, or anything that doesn't match the requested output.
|
|
54
|
-
-
|
|
55
|
-
- Avoid over-enginnering, hacks or creative solutions. The boring, simple and repliable is always preferred.
|
|
18
|
+
- Be concise. Use a professional colleague tone: direct, never condescending, and never rude.
|
|
56
19
|
- Do not overstate what changed or what was verified. Summaries must match the diff.
|
|
57
|
-
|
|
58
|
-
IMPORTANT: Never guess or assume. Verify claims before making them.
|
|
59
|
-
IMPORTANT: Do not over-scope work or add scope during implementation.
|
|
60
20
|
`;
|
|
61
21
|
|
|
62
22
|
async function getDir() {
|
|
@@ -76,12 +36,7 @@ async function getDir() {
|
|
|
76
36
|
|
|
77
37
|
async function getEnvPrompt() {
|
|
78
38
|
// TODO: What else do the agents always check before answering every time?
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
gitStatus = await simpleGit().status();
|
|
82
|
-
} catch (_) {
|
|
83
|
-
gitStatus = { nogit: "No git repo in this folder." };
|
|
84
|
-
}
|
|
39
|
+
const gitStatus = await getGitStatus();
|
|
85
40
|
const envKeys = ["PATH", "USER", "LANG", "HOME", "SHELL", "BUN_INSTALL"];
|
|
86
41
|
const env: Record<string, string> = {};
|
|
87
42
|
for (const key of envKeys) {
|
|
@@ -211,47 +166,68 @@ export async function injectEnvReminder(): Promise<string> {
|
|
|
211
166
|
return `<system-reminder>\n${envStatus}\n</system-reminder>`;
|
|
212
167
|
}
|
|
213
168
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
169
|
+
const doomLoopReminder = `<system-reminder>
|
|
170
|
+
You may be entering a tool-call doom loop: recent tool usage is repetitive or not clearly progressing.
|
|
171
|
+
|
|
172
|
+
- Stop repeating the same or similar tool call unless new evidence requires it.
|
|
173
|
+
- Re-read the user's request and summarize what is known, what failed, and what is still needed.
|
|
174
|
+
- Change strategy before calling more tools: narrow the next check, use a different source of evidence, or ask the user if blocked.
|
|
175
|
+
- If the request is complete, stop calling tools and provide the final answer.
|
|
176
|
+
</system-reminder>`;
|
|
177
|
+
|
|
178
|
+
// Checks recent tool usage for simple repeated-call patterns and inserts an
|
|
179
|
+
// anti-doom-loop reminder when the agent appears stuck.
|
|
217
180
|
export function insertToolUsageReminder(
|
|
218
181
|
messages: Message[],
|
|
219
182
|
toolMessage: ToolResultMessage,
|
|
220
|
-
) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
|
|
232
|
-
const messagesSinceLastUser = messages.slice(lastUserMessageIndex + 1);
|
|
233
|
-
|
|
234
|
-
messagesSinceLastUser.forEach((m) => {
|
|
235
|
-
if (m.role === "assistant") {
|
|
236
|
-
const toolCallsBlocks = m.content.filter((b) => b.type === "toolCall");
|
|
237
|
-
toolCalls.push(...toolCallsBlocks);
|
|
238
|
-
}
|
|
183
|
+
): ToolResultMessage {
|
|
184
|
+
const lastReminderIdx = messages.findLastIndex((m) => {
|
|
185
|
+
return (
|
|
186
|
+
m.role === "toolResult" &&
|
|
187
|
+
m.content.find(
|
|
188
|
+
(b) => b.type === "text" && b.text.includes(doomLoopReminder),
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
const lastUserMessageIdx = messages.findLastIndex((m) => {
|
|
193
|
+
return m.role === "user";
|
|
239
194
|
});
|
|
240
|
-
const
|
|
241
|
-
|
|
195
|
+
const messagesSinceLast = messages.slice(
|
|
196
|
+
Math.max(lastReminderIdx, lastUserMessageIdx) + 1,
|
|
197
|
+
);
|
|
198
|
+
const minMessagesForReminder = 4;
|
|
199
|
+
|
|
200
|
+
if (messagesSinceLast.length < minMessagesForReminder) return toolMessage;
|
|
242
201
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
202
|
+
let errorCount = 0;
|
|
203
|
+
let sameToolCount = 0;
|
|
204
|
+
const seenArgs = new Set<string>();
|
|
246
205
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
</system-reminder>
|
|
206
|
+
for (const msg of messagesSinceLast) {
|
|
207
|
+
if (msg.role === "toolResult" && msg.isError) errorCount++;
|
|
250
208
|
|
|
251
|
-
|
|
209
|
+
if (msg.role === "assistant") {
|
|
210
|
+
const calls = msg.content.filter((b) => b.type === "toolCall");
|
|
211
|
+
for (const c of calls) {
|
|
212
|
+
const args = JSON.stringify(c.arguments);
|
|
213
|
+
if (seenArgs.has(args)) sameToolCount++;
|
|
214
|
+
seenArgs.add(args);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
252
217
|
}
|
|
253
218
|
|
|
254
|
-
|
|
219
|
+
if (
|
|
220
|
+
errorCount >= minMessagesForReminder ||
|
|
221
|
+
sameToolCount >= minMessagesForReminder
|
|
222
|
+
) {
|
|
223
|
+
return {
|
|
224
|
+
...toolMessage,
|
|
225
|
+
content: [
|
|
226
|
+
...toolMessage.content,
|
|
227
|
+
{ type: "text", text: doomLoopReminder },
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
255
231
|
|
|
256
232
|
return toolMessage;
|
|
257
233
|
}
|
package/src/session.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type { Message } from "@
|
|
3
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
4
4
|
import { Value } from "typebox/value";
|
|
5
5
|
import { SESSIONS_DIR } from "./shared";
|
|
6
6
|
import { type Session, SessionSchema } from "./types";
|
|
7
7
|
|
|
8
|
-
// TODO: sessions are json files in SESSIONS_DIR inside of DATA_DIR, use a 10 length `secureRandomString()` for the ids.
|
|
9
|
-
|
|
10
8
|
export async function ensureSessionsDir(): Promise<void> {
|
|
11
9
|
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
12
10
|
}
|
|
@@ -71,8 +69,12 @@ export async function saveSession(s: Session) {
|
|
|
71
69
|
export async function updateSession(id: string, messages: Message[]) {
|
|
72
70
|
const existing = await getSession(id);
|
|
73
71
|
if (existing) {
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// Only append new messages, so we don't save compacted messages.
|
|
73
|
+
if (existing.messages.length < messages.length) {
|
|
74
|
+
const newMessages = messages.slice(existing.messages.length);
|
|
75
|
+
existing.messages = [...existing.messages, ...newMessages];
|
|
76
|
+
await saveSession(existing);
|
|
77
|
+
}
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
|
package/src/tool-bash.ts
CHANGED
|
@@ -1,24 +1,10 @@
|
|
|
1
|
-
import { type Tool, Type } from "@
|
|
2
|
-
import { secureRandomString } from "./shared";
|
|
1
|
+
import { type Tool, Type } from "@earendil-works/pi-ai";
|
|
3
2
|
import type { ToolRunnerEvent } from "./types";
|
|
4
3
|
|
|
5
|
-
const
|
|
6
|
-
const description = `## Bash CLI tool
|
|
4
|
+
const description = `Bash CLI tool
|
|
7
5
|
|
|
8
6
|
Execute shell commands on the user's environment.
|
|
9
7
|
|
|
10
|
-
Best practices:
|
|
11
|
-
|
|
12
|
-
- Use \`ls\` to list files.
|
|
13
|
-
- Use \`fd\` or \`find\` to locate files/directories by name, type, size, time, permissions, etc.
|
|
14
|
-
- Use \`rg\` or \`grep\` to search inside files for matching patterns.
|
|
15
|
-
- Use \`cat -n\` or \`nl\` to read small files, or when you need the whole file.
|
|
16
|
-
- Use \`sed -n\` with ranges to read sections of files. Prefer targeted reads. Avoid dumping very large (more than ~200 lines) files all at once.
|
|
17
|
-
- Use \`cp\`, \`mv\`, and \`mkdir\` for file and directory operations, and \`rm\` to remove files and directories.
|
|
18
|
-
- Use \`curl\` for web access. Use redirection to temp files for targeted reads.
|
|
19
|
-
- Use development tools like \`git\`, \`gh\`, \`jq\`, etc, when appropriate.
|
|
20
|
-
- Prefer \`cp -i\`, \`mv -i\`, \`rm -i\` when learning.
|
|
21
|
-
- NEVER run destructive commands (\`rm -rf\`, \`git reset --hard\`, overwriting files) without confirming the target first.
|
|
22
8
|
- Chain commands **only** when failure should stop the flow. Avoid long chains, **2 to 3 maximum**.
|
|
23
9
|
- Avoid overly complex one-liners; readability matters.
|
|
24
10
|
- Quote filenames: use \`"$file"\` not \`$file\`.
|
|
@@ -80,25 +66,9 @@ export async function* runBashTool(
|
|
|
80
66
|
|
|
81
67
|
const exitCode = await proc.exited;
|
|
82
68
|
|
|
83
|
-
let result =
|
|
69
|
+
let result = `${output.length ? output : "(no ouput)"}\n\nExit code: ${exitCode}`;
|
|
84
70
|
if (output.length) {
|
|
85
|
-
result +=
|
|
86
|
-
# OUTPUT:
|
|
87
|
-
|
|
88
|
-
${output}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// If `out` is too big, more than ~XXKB, write it to a temp file
|
|
92
|
-
// And add that to the truncation label for the agent to be able
|
|
93
|
-
// to continue the read with scans. This is to protect context,
|
|
94
|
-
// not a general read guard. The hint is for the agent, not the TUI
|
|
95
|
-
if (result.length > OUTPUT_THRESHOLD) {
|
|
96
|
-
const key = `${Date.now()}-${secureRandomString(4)}`;
|
|
97
|
-
const pathname = `/tmp/bash_result_${key}.txt`;
|
|
98
|
-
await Bun.write(pathname, result);
|
|
99
|
-
result = `${result.substring(0, OUTPUT_THRESHOLD)}
|
|
100
|
-
|
|
101
|
-
Truncated at ~${OUTPUT_THRESHOLD / 1000}KB. Full output at ${pathname}`;
|
|
71
|
+
result += `${output}`;
|
|
102
72
|
}
|
|
103
73
|
|
|
104
74
|
yield {
|
package/src/tool-edit.ts
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path";
|
|
2
|
-
import { type Tool, Type } from "@
|
|
2
|
+
import { type Tool, Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import { createPatch } from "diff";
|
|
4
4
|
import type { ToolRunnerEvent } from "./types";
|
|
5
5
|
|
|
6
|
-
const description =
|
|
6
|
+
const description = `Edit tool
|
|
7
7
|
|
|
8
8
|
A find-and-replace file editor. Use it to create new files or modify existing ones safely. Always prefer this tool over bash editing methods (sed, awk, etc).
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Rules
|
|
11
|
+
|
|
11
12
|
- The tool refuses to edit on multiple matches of \`oldText\`. Be specific with your matching text.
|
|
12
13
|
- Prefer patch-based edits (small targeted replacements) for multi-line or semantic changes.
|
|
13
14
|
- Do NOT reproduce entire files. Use shell file operations (\`cp\`, \`mv\`, etc) for wholesale file replacement instead.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
Failure modes
|
|
17
|
+
|
|
16
18
|
- If \`oldText\` is not found, the edit fails. Verify the exact text first.
|
|
17
19
|
- If \`oldText\` matches multiple locations, the edit fails. Narrow your match and retry.
|
|
18
20
|
- If the file does not exist and \`oldText\` is non-empty, the edit fails.
|
|
19
|
-
|
|
20
|
-
<example>
|
|
21
|
-
Edit a single line:
|
|
22
|
-
path: src/utils.ts
|
|
23
|
-
oldText: const MAX_RETRIES = 3;
|
|
24
|
-
newText: const MAX_RETRIES = 5;
|
|
25
|
-
</example>
|
|
26
|
-
|
|
27
|
-
<example>
|
|
28
|
-
Patch-based edit (preferred for multi-line changes):
|
|
29
|
-
path: src/utils.ts
|
|
30
|
-
oldText: function oldHelper() {\n return 1;\n}
|
|
31
|
-
newText: function newHelper() {\n return 2;\n}\n\nfunction oldHelper() {\n return 1;\n}
|
|
32
|
-
</example>
|
|
33
21
|
`;
|
|
34
22
|
|
|
35
23
|
export const edit: Tool = {
|
package/src/tool-read.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { extname, isAbsolute, join } from "node:path";
|
|
3
|
-
import { type Tool, Type } from "@
|
|
3
|
+
import { type Tool, Type } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ToolRunnerEvent } from "./types";
|
|
5
5
|
|
|
6
6
|
const imageMimeTypes: Record<string, string> = {
|
|
@@ -13,7 +13,7 @@ const imageMimeTypes: Record<string, string> = {
|
|
|
13
13
|
".webp": "image/webp",
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const description =
|
|
16
|
+
const description = `Read tool
|
|
17
17
|
|
|
18
18
|
Read a file by path.
|
|
19
19
|
|
package/src/tui-components.ts
CHANGED
|
@@ -114,11 +114,11 @@ export function ContextPill(state: TUIState) {
|
|
|
114
114
|
const smartPercent = Math.floor((state.contextSize / smartMax) * 100);
|
|
115
115
|
if (smartPercent > 90) {
|
|
116
116
|
bg = theme.bred;
|
|
117
|
-
} else if (smartPercent >
|
|
117
|
+
} else if (smartPercent > 85) {
|
|
118
118
|
bg = theme.red;
|
|
119
|
-
} else if (smartPercent >
|
|
119
|
+
} else if (smartPercent > 80) {
|
|
120
120
|
bg = theme.yellow;
|
|
121
|
-
} else if (smartPercent >
|
|
121
|
+
} else if (smartPercent > 60) {
|
|
122
122
|
bg = theme.byellow;
|
|
123
123
|
}
|
|
124
124
|
text = `~${percent}%`;
|
|
@@ -126,7 +126,7 @@ export function ContextPill(state: TUIState) {
|
|
|
126
126
|
|
|
127
127
|
text += ` (${state.options.model.contextWindow / 1000}k)`;
|
|
128
128
|
|
|
129
|
-
return TextPill(text, theme.
|
|
129
|
+
return TextPill(text, theme.black, bg);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
export function GitPill(state: TUIState) {
|