mini-coder 0.6.0 → 0.6.1
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/README.md +1 -0
- package/bun.lock +1 -1
- package/nono-mini-coder.json +42 -0
- package/package.json +2 -2
- package/src/agent.ts +8 -2
- package/src/headless.ts +2 -5
- package/src/prompt.ts +29 -37
- package/src/tool-edit.ts +1 -1
- package/src/tool-read.ts +100 -0
- package/src/tui-components.ts +4 -4
- package/src/tui-conversation.ts +28 -19
- package/src/tui.ts +17 -25
- package/src/types.ts +5 -1
- package/src/tool-task.ts +0 -114
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ $ mc
|
|
|
36
36
|
- **Agent-first** — every decision serves the goal of reading code, making changes, and verifying them via the shell.
|
|
37
37
|
- **Performance** — startup and turn latency matter more than features.
|
|
38
38
|
- **Streaming end-to-end** — assistant text, reasoning, tool calls, and tool output show up as they happen.
|
|
39
|
+
- **Isolation friendly**— works inside [nono](https://nono.sh/). Suggested profile in `nono-mini-coder.json`
|
|
39
40
|
|
|
40
41
|
## Settings
|
|
41
42
|
|
package/bun.lock
CHANGED
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
|
|
130
130
|
"@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
|
|
131
131
|
|
|
132
|
-
"@mariozechner/pi-ai": ["@mariozechner/pi-ai@0.70.
|
|
132
|
+
"@mariozechner/pi-ai": ["@mariozechner/pi-ai@0.70.6", "", { "dependencies": { "@anthropic-ai/sdk": "^0.90.0", "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "^2.2.0", "chalk": "^5.6.2", "openai": "6.26.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "typebox": "^1.1.24", "undici": "^7.19.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-LVAadu0Y+hb7Bj7EDiLsx6AuGxHlxDq0euLzyqX698i9qt0BW6a+oQSUIZQz4rJwExF18OvyL7ygJ5781ojrIQ=="],
|
|
133
133
|
|
|
134
134
|
"@mistralai/mistralai": ["@mistralai/mistralai@2.2.0", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" } }, "sha512-JQUGIXjFWnw/J9LpTSf/ZXwVW3Sh8FBAcfTo5QvAHqkl4CfSiIwnjRJhMoAFcP6ncCe84YPU1ncDGX+p3OXnfg=="],
|
|
135
135
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"name": "mini-coder",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Profile for mini-coder / mc, a Bun-based coding agent"
|
|
6
|
+
},
|
|
7
|
+
"extends": "default",
|
|
8
|
+
"workdir": {
|
|
9
|
+
"access": "readwrite"
|
|
10
|
+
},
|
|
11
|
+
"security": {
|
|
12
|
+
"groups": [
|
|
13
|
+
"node_runtime",
|
|
14
|
+
"git_config",
|
|
15
|
+
"user_caches_linux",
|
|
16
|
+
"linux_sysfs_read",
|
|
17
|
+
"unlink_protection"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"filesystem": {
|
|
21
|
+
"allow": [
|
|
22
|
+
"$HOME/.config/mini-coder",
|
|
23
|
+
"$HOME/.bun",
|
|
24
|
+
"$TMPDIR"
|
|
25
|
+
],
|
|
26
|
+
"read": [
|
|
27
|
+
"$HOME/.agents/"
|
|
28
|
+
],
|
|
29
|
+
"write": [],
|
|
30
|
+
"allow_file": [
|
|
31
|
+
"$HOME/AGENTS.md"
|
|
32
|
+
],
|
|
33
|
+
"read_file": [],
|
|
34
|
+
"write_file": []
|
|
35
|
+
},
|
|
36
|
+
"rollback": {
|
|
37
|
+
"exclude_patterns": [
|
|
38
|
+
"node_modules"
|
|
39
|
+
],
|
|
40
|
+
"exclude_globs": []
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mini-coder",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "bun@1.3.12",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@cel-tui/components": "^0.8.3",
|
|
24
24
|
"@cel-tui/core": "^0.8.3",
|
|
25
|
-
"@mariozechner/pi-ai": "^0.70.
|
|
25
|
+
"@mariozechner/pi-ai": "^0.70.6",
|
|
26
26
|
"diff": "^9.0.0",
|
|
27
27
|
"simple-git": "^3.36.0",
|
|
28
28
|
"yaml": "^2.8.3"
|
package/src/agent.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AssistantMessage,
|
|
3
3
|
type Context,
|
|
4
|
+
type ImageContent,
|
|
4
5
|
type Message,
|
|
5
6
|
streamSimple,
|
|
6
7
|
type TextContent,
|
|
@@ -239,14 +240,19 @@ async function* toolRunner(
|
|
|
239
240
|
},
|
|
240
241
|
};
|
|
241
242
|
} else if (e.type === "result") {
|
|
242
|
-
// handle final message
|
|
243
|
+
// handle final message, and check for images
|
|
244
|
+
let img: ImageContent | null = null;
|
|
245
|
+
if (e.image) {
|
|
246
|
+
img = { ...e.image, type: "image" };
|
|
247
|
+
}
|
|
248
|
+
|
|
243
249
|
yield {
|
|
244
250
|
type: "tool_result",
|
|
245
251
|
message: {
|
|
246
252
|
role: "toolResult",
|
|
247
253
|
toolCallId: call.id,
|
|
248
254
|
toolName: call.name,
|
|
249
|
-
content: [content],
|
|
255
|
+
content: img ? [content, img] : [content],
|
|
250
256
|
isError: false,
|
|
251
257
|
timestamp,
|
|
252
258
|
},
|
package/src/headless.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { streamAgent } from "./agent";
|
|
|
3
3
|
import { buildSystemPrompt, injectEnvReminder, MAIN_PROMPT } from "./prompt";
|
|
4
4
|
import { bash, runBashTool } from "./tool-bash";
|
|
5
5
|
import { edit, runEditTool } from "./tool-edit";
|
|
6
|
-
import {
|
|
6
|
+
import { read, runReadTool } from "./tool-read";
|
|
7
7
|
import type { AgentContex, CliOptions, ToolAndRunner } from "./types";
|
|
8
8
|
|
|
9
9
|
export async function streamHeadless(
|
|
@@ -13,10 +13,7 @@ export async function streamHeadless(
|
|
|
13
13
|
const tools: ToolAndRunner[] = [
|
|
14
14
|
{ tool: bash, runner: runBashTool },
|
|
15
15
|
{ tool: edit, runner: runEditTool },
|
|
16
|
-
{
|
|
17
|
-
tool: task,
|
|
18
|
-
runner: (args) => runTaskTool(options, args),
|
|
19
|
-
},
|
|
16
|
+
{ tool: read, runner: runReadTool },
|
|
20
17
|
];
|
|
21
18
|
const envReminder = await injectEnvReminder();
|
|
22
19
|
const messages: Message[] = [
|
package/src/prompt.ts
CHANGED
|
@@ -6,40 +6,33 @@ import type { Message, ToolCall, ToolResultMessage } from "@mariozechner/pi-ai";
|
|
|
6
6
|
import simpleGit, { type StatusResult } from "simple-git";
|
|
7
7
|
import { parseSkillFrontmatter } from "./shared";
|
|
8
8
|
|
|
9
|
-
const safetyPrompt = `
|
|
10
|
-
# Safety rules
|
|
11
|
-
|
|
12
|
-
- Answer all user requests without guessing, or assuming. Verify your answers and claims before making them.
|
|
13
|
-
- Use recent online information, the current environment, and your training data combined for a complete answer.
|
|
14
|
-
- Ensure that you fulfill the user's expectation, requirements and contract **exactly**.
|
|
15
|
-
- Be defensive with existing changes and destructive commands, they could harm your user's changes.
|
|
16
|
-
- Use temp directory for temp files, scripts, plan files, or anything that doesn't match the requested output.
|
|
17
|
-
- Do not over-scope your work, or add more scope during implementation.
|
|
18
|
-
- Avoid over-enginnering, hacks or creative solutions. The boring, simple and repliable is always preferred.
|
|
19
|
-
- Do not overstate what changed or what was verified. Summaries must match the diff.
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
9
|
export const MAIN_PROMPT = `# You are "mini-coder", a coding agent.
|
|
23
10
|
|
|
24
11
|
IMPORTANT: Be defensive with existing changes and destructive commands.
|
|
25
12
|
IMPORTANT: Do not overstate what changed or what was verified. Summaries must match the diff.
|
|
26
13
|
|
|
27
14
|
## Role
|
|
28
|
-
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.
|
|
29
|
-
|
|
30
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.
|
|
31
16
|
|
|
32
|
-
## Tool Usage
|
|
33
|
-
- The **task** tool is the preferred way to work. Use it for multi-step research, exploration, or implementation tasks.
|
|
34
|
-
- Use **bash**, **edit**, and other tools only for single, immediate actions.
|
|
35
|
-
- **Do NOT** chain multiple bash/edit calls for multi-step work. Use **task** instead.
|
|
36
|
-
- If a job needs more than one tool call, use the **task** tool instead of chaining individual calls.
|
|
37
|
-
|
|
38
17
|
<example>
|
|
39
18
|
When referencing specific functions or pieces of code, include the pattern \`file_path:line_number\`.
|
|
40
19
|
For example: "Clients are handled in the \`connectToServer\` function in src/services/process.ts:712."
|
|
41
20
|
</example>
|
|
42
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
|
|
25
|
+
- 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
|
+
|
|
43
36
|
## Workflow
|
|
44
37
|
- Stay rooted on the user's request. Don't wander into tangents or explore out of curiosity.
|
|
45
38
|
- Gather only the information needed to fulfill the request, then stop exploring and complete it.
|
|
@@ -47,27 +40,23 @@ For example: "Clients are handled in the \`connectToServer\` function in src/ser
|
|
|
47
40
|
- Verify your changes via compilation, tests, or manual checks whenever possible.
|
|
48
41
|
|
|
49
42
|
## Tone
|
|
50
|
-
- Be concise. Use a
|
|
43
|
+
- Be concise. Use a professional colleague tone: direct, never condescending, and never rude.
|
|
51
44
|
|
|
52
45
|
## Error Handling
|
|
53
46
|
- If a tool call fails or is denied, do NOT re-attempt the exact same call. Analyze why it failed and adjust your approach.
|
|
54
47
|
|
|
48
|
+
## Safety rules
|
|
49
|
+
- Answer all user requests without guessing, or assuming. Verify your answers and claims before making them.
|
|
50
|
+
- Use recent online information, the current environment, and your training data combined for a complete answer.
|
|
51
|
+
- 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
|
+
- Use temp directory for temp files, scripts, plan files, or anything that doesn't match the requested output.
|
|
54
|
+
- Do not over-scope your work, or add more scope during implementation.
|
|
55
|
+
- Avoid over-enginnering, hacks or creative solutions. The boring, simple and repliable is always preferred.
|
|
56
|
+
- Do not overstate what changed or what was verified. Summaries must match the diff.
|
|
57
|
+
|
|
55
58
|
IMPORTANT: Never guess or assume. Verify claims before making them.
|
|
56
59
|
IMPORTANT: Do not over-scope work or add scope during implementation.
|
|
57
|
-
|
|
58
|
-
${safetyPrompt}
|
|
59
|
-
`;
|
|
60
|
-
|
|
61
|
-
export const TASK_PROMPT = `# You are an efficient, elite-level task Agent
|
|
62
|
-
|
|
63
|
-
## Role
|
|
64
|
-
Execute the assigned task precisely and efficiently. Prioritize correctness over speed. Focus on facts and objective technical details.
|
|
65
|
-
|
|
66
|
-
## Output Requirements
|
|
67
|
-
- Your final response must include all actions taken and exact diffs of any changes made.
|
|
68
|
-
- Provide a concise report of what was done, what was verified, and any decisions made.
|
|
69
|
-
|
|
70
|
-
${safetyPrompt}
|
|
71
60
|
`;
|
|
72
61
|
|
|
73
62
|
async function getDir() {
|
|
@@ -222,6 +211,9 @@ export async function injectEnvReminder(): Promise<string> {
|
|
|
222
211
|
return `<system-reminder>\n${envStatus}\n</system-reminder>`;
|
|
223
212
|
}
|
|
224
213
|
|
|
214
|
+
// TODO: Needs to be updated since we are deprecating the task tool
|
|
215
|
+
// for now. Needs to check for similar or identical tool calls, aka
|
|
216
|
+
// Doom looping.
|
|
225
217
|
export function insertToolUsageReminder(
|
|
226
218
|
messages: Message[],
|
|
227
219
|
toolMessage: ToolResultMessage,
|
|
@@ -234,7 +226,7 @@ export function insertToolUsageReminder(
|
|
|
234
226
|
.map((b) => b.text)
|
|
235
227
|
.join("\n");
|
|
236
228
|
|
|
237
|
-
const budget =
|
|
229
|
+
const budget = 5;
|
|
238
230
|
const toolCalls: ToolCall[] = [];
|
|
239
231
|
const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
|
|
240
232
|
const messagesSinceLastUser = messages.slice(lastUserMessageIndex + 1);
|
package/src/tool-edit.ts
CHANGED
package/src/tool-read.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { extname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { type Tool, Type } from "@mariozechner/pi-ai";
|
|
4
|
+
import type { ToolRunnerEvent } from "./types";
|
|
5
|
+
|
|
6
|
+
const imageMimeTypes: Record<string, string> = {
|
|
7
|
+
".bmp": "image/bmp",
|
|
8
|
+
".gif": "image/gif",
|
|
9
|
+
".jpeg": "image/jpeg",
|
|
10
|
+
".jpg": "image/jpeg",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".svg": "image/svg+xml",
|
|
13
|
+
".webp": "image/webp",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const description = `## Read tool
|
|
17
|
+
|
|
18
|
+
Read a file by path.
|
|
19
|
+
|
|
20
|
+
For text files, returns line-numbered text. Use \`offset\` and \`limit\` to read a specific range.
|
|
21
|
+
You can also read images files.
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const read: Tool = {
|
|
25
|
+
name: "read",
|
|
26
|
+
description,
|
|
27
|
+
parameters: Type.Object({
|
|
28
|
+
path: Type.String({
|
|
29
|
+
description:
|
|
30
|
+
"File path. Absolute or relative to the current working directory.",
|
|
31
|
+
}),
|
|
32
|
+
offset: Type.Optional(
|
|
33
|
+
Type.Number({
|
|
34
|
+
description:
|
|
35
|
+
"Optional 1-based line number to start reading from. Text files only.",
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
limit: Type.Optional(
|
|
39
|
+
Type.Number({
|
|
40
|
+
description:
|
|
41
|
+
"Optional maximum number of lines to read. Text files only.",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export async function* runReadTool(
|
|
48
|
+
args: Record<string, any>,
|
|
49
|
+
signal?: AbortSignal,
|
|
50
|
+
): AsyncGenerator<ToolRunnerEvent> {
|
|
51
|
+
const filePath = isAbsolute(args.path)
|
|
52
|
+
? args.path
|
|
53
|
+
: join(process.cwd(), args.path);
|
|
54
|
+
const file = Bun.file(filePath);
|
|
55
|
+
const exists = await file.exists();
|
|
56
|
+
|
|
57
|
+
if (!exists) {
|
|
58
|
+
yield { type: "result", text: `File not found: ${filePath}` };
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
yield { type: "result", text: "Aborted before read." };
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mimeType = imageMimeTypes[extname(filePath).toLowerCase()] ?? file.type;
|
|
68
|
+
if (mimeType.startsWith("image/")) {
|
|
69
|
+
const data = Buffer.from(await file.arrayBuffer()).toString("base64");
|
|
70
|
+
const text = `Image read: ${filePath}\nMIME type: ${mimeType}\nSize: ${file.size} bytes`;
|
|
71
|
+
yield {
|
|
72
|
+
type: "result",
|
|
73
|
+
text,
|
|
74
|
+
image: { data, mimeType },
|
|
75
|
+
};
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const content = await file.text();
|
|
80
|
+
const lines = content.split(/\r?\n/);
|
|
81
|
+
if (content.endsWith("\n")) lines.pop();
|
|
82
|
+
|
|
83
|
+
const offset = args.offset ?? 1;
|
|
84
|
+
const startIndex = offset - 1;
|
|
85
|
+
const endIndex = args.limit
|
|
86
|
+
? Math.min(lines.length, startIndex + args.limit)
|
|
87
|
+
: lines.length;
|
|
88
|
+
const width = String(endIndex).length;
|
|
89
|
+
const body = lines
|
|
90
|
+
.slice(startIndex, endIndex)
|
|
91
|
+
.map((line, idx) => `${String(offset + idx).padStart(width)} | ${line}`)
|
|
92
|
+
.join("\n");
|
|
93
|
+
|
|
94
|
+
let text = `File: ${filePath}\nLines: ${offset}-${endIndex} of ${lines.length}\n\n${body}`;
|
|
95
|
+
if (endIndex < lines.length) {
|
|
96
|
+
text += `\n\nMore lines available. Use offset ${endIndex + 1} to continue.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
yield { type: "result", text };
|
|
100
|
+
}
|
package/src/tui-components.ts
CHANGED
|
@@ -137,13 +137,13 @@ export function ModelPill(state: TUIState) {
|
|
|
137
137
|
// Like loot: Gold/purple/blue/green/white -> xhigh/high/medium/low/minimal
|
|
138
138
|
switch (state.options.effort) {
|
|
139
139
|
case "xhigh":
|
|
140
|
-
return TextPill(state.options.model.name, theme.
|
|
140
|
+
return TextPill(state.options.model.name, theme.black, theme.yellow);
|
|
141
141
|
case "high":
|
|
142
|
-
return TextPill(state.options.model.name, theme.
|
|
142
|
+
return TextPill(state.options.model.name, theme.black, theme.magenta);
|
|
143
143
|
case "medium":
|
|
144
|
-
return TextPill(state.options.model.name, theme.
|
|
144
|
+
return TextPill(state.options.model.name, theme.black, theme.blue);
|
|
145
145
|
case "low":
|
|
146
|
-
return TextPill(state.options.model.name, theme.
|
|
146
|
+
return TextPill(state.options.model.name, theme.black, theme.green);
|
|
147
147
|
}
|
|
148
148
|
// Minimal
|
|
149
149
|
return TextPill(state.options.model.name, theme.bwhite, theme.bblack);
|
package/src/tui-conversation.ts
CHANGED
|
@@ -33,9 +33,6 @@ function agentMessageNode(msg: AssistantMessage): Node {
|
|
|
33
33
|
} else if ("command" in block.arguments) {
|
|
34
34
|
text = block.arguments.command;
|
|
35
35
|
node = SyntaxHighlight(text, "bash");
|
|
36
|
-
} else if ("prompt" in block.arguments) {
|
|
37
|
-
text = block.arguments.prompt;
|
|
38
|
-
node = SyntaxHighlight(text, "markdown");
|
|
39
36
|
} else {
|
|
40
37
|
text = JSON.stringify(block.arguments);
|
|
41
38
|
node = Text(text);
|
|
@@ -119,22 +116,33 @@ function userMessageNode(msg: UserMessage): Node {
|
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
function toolMessageNode(msg: ToolResultMessage): Node {
|
|
119
|
+
// TODO: read tool image output
|
|
122
120
|
// Output only shows last 10 lines of scroll.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
if (block.type === "text") {
|
|
133
|
-
return Text(block.text, { wrap: "word", fgColor: theme.bblack });
|
|
134
|
-
}
|
|
135
|
-
return Text(""); // TODO: image case needs attention
|
|
121
|
+
const text = msg.content
|
|
122
|
+
.filter((c) => c.type === "text")
|
|
123
|
+
.map((c) => c.text)
|
|
124
|
+
.join("")
|
|
125
|
+
.trim();
|
|
126
|
+
|
|
127
|
+
return VStack({ padding: { x: 4 }, gap: 1 }, [
|
|
128
|
+
Text(`~${estimateTokens(text)} tokens, ${text.split("\n").length} lines.`, {
|
|
129
|
+
fgColor: theme.bblack,
|
|
136
130
|
}),
|
|
137
|
-
|
|
131
|
+
VStack(
|
|
132
|
+
{
|
|
133
|
+
flex: 1,
|
|
134
|
+
maxHeight: msg.toolName === "edit" ? 20 : 10,
|
|
135
|
+
overflow: "scroll",
|
|
136
|
+
scrollOffset: Infinity,
|
|
137
|
+
onScroll: () => false,
|
|
138
|
+
},
|
|
139
|
+
[
|
|
140
|
+
msg.toolName === "edit"
|
|
141
|
+
? SyntaxHighlight(text, "diff")
|
|
142
|
+
: Text(text, { wrap: "word", fgColor: theme.white }),
|
|
143
|
+
],
|
|
144
|
+
),
|
|
145
|
+
]);
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
const messageBodyCache = new WeakMap<Message, { key: number; node: Node }>();
|
|
@@ -190,6 +198,7 @@ function cachedMessageBody(msg: Message): Node {
|
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
function conversationMessageNode(msg: Message): Node {
|
|
201
|
+
const label = msg.role === "toolResult" ? `${msg.toolName} result` : msg.role;
|
|
193
202
|
return VStack({ gap: 1 }, [
|
|
194
203
|
cachedMessageBody(msg),
|
|
195
204
|
HStack({ gap: 1, justifyContent: "end" }, [
|
|
@@ -197,7 +206,7 @@ function conversationMessageNode(msg: Message): Node {
|
|
|
197
206
|
fgColor: theme.bblack,
|
|
198
207
|
italic: true,
|
|
199
208
|
}),
|
|
200
|
-
TextPill(
|
|
209
|
+
TextPill(label, theme.bwhite, theme.bblack),
|
|
201
210
|
]),
|
|
202
211
|
]);
|
|
203
212
|
}
|
|
@@ -249,7 +258,7 @@ export function Conversation(state: TUIState) {
|
|
|
249
258
|
return VStack(
|
|
250
259
|
{
|
|
251
260
|
flex: 1,
|
|
252
|
-
gap:
|
|
261
|
+
gap: 3,
|
|
253
262
|
overflow: "scroll",
|
|
254
263
|
scrollOffset: state.stickToBottom ? Infinity : state.scrollOffset,
|
|
255
264
|
onScroll(offset, maxOffset) {
|
package/src/tui.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { cel, HStack, ProcessTerminal, VStack } from "@cel-tui/core";
|
|
2
2
|
import simpleGit from "simple-git";
|
|
3
3
|
import { compactContext, streamAgent } from "./agent";
|
|
4
|
-
import {
|
|
5
|
-
buildSystemPrompt,
|
|
6
|
-
injectEnvReminder,
|
|
7
|
-
insertToolUsageReminder,
|
|
8
|
-
MAIN_PROMPT,
|
|
9
|
-
} from "./prompt";
|
|
4
|
+
import { buildSystemPrompt, injectEnvReminder, MAIN_PROMPT } from "./prompt";
|
|
10
5
|
import { updateSession } from "./session";
|
|
11
6
|
import { estimateTokens, secureRandomString } from "./shared";
|
|
12
7
|
import { bash, runBashTool } from "./tool-bash";
|
|
13
8
|
import { edit, runEditTool } from "./tool-edit";
|
|
14
|
-
import {
|
|
9
|
+
import { read, runReadTool } from "./tool-read";
|
|
15
10
|
import {
|
|
16
11
|
ActivityPill,
|
|
17
12
|
ContextPill,
|
|
@@ -43,7 +38,6 @@ function clearOrAbort(state: TUIState) {
|
|
|
43
38
|
|
|
44
39
|
export function initTUI(state: TUIState, leave: (s: string) => void) {
|
|
45
40
|
// TODO: Cleanup accumulated sessions for this cwd.
|
|
46
|
-
|
|
47
41
|
const { spinnerEvery, currentSpinner } = Spinner();
|
|
48
42
|
|
|
49
43
|
// Stable 60fps rendering.
|
|
@@ -147,10 +141,7 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
147
141
|
const tools: ToolAndRunner[] = [
|
|
148
142
|
{ tool: bash, runner: runBashTool },
|
|
149
143
|
{ tool: edit, runner: runEditTool },
|
|
150
|
-
{
|
|
151
|
-
tool: task,
|
|
152
|
-
runner: (args, signal) => runTaskTool(state.options, args, signal),
|
|
153
|
-
},
|
|
144
|
+
{ tool: read, runner: runReadTool },
|
|
154
145
|
];
|
|
155
146
|
|
|
156
147
|
let userContent = state.prompt;
|
|
@@ -193,19 +184,20 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
193
184
|
break;
|
|
194
185
|
|
|
195
186
|
case "tool_message_end": {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
187
|
+
// TODO: reminder needs to be refactored
|
|
188
|
+
// const withReminder = insertToolUsageReminder(
|
|
189
|
+
// state.messages,
|
|
190
|
+
// ev.message,
|
|
191
|
+
// );
|
|
192
|
+
|
|
193
|
+
// const idx = state.messages.findIndex(
|
|
194
|
+
// (m) =>
|
|
195
|
+
// m.role === "toolResult" &&
|
|
196
|
+
// m.toolCallId === withReminder.toolCallId,
|
|
197
|
+
// );
|
|
198
|
+
// if (idx >= 0) {
|
|
199
|
+
// state.messages[idx] = withReminder;
|
|
200
|
+
// }
|
|
209
201
|
|
|
210
202
|
state.contextSize = estimateTokens(JSON.stringify(ctx));
|
|
211
203
|
}
|
package/src/types.ts
CHANGED
|
@@ -129,7 +129,11 @@ export type AgentToolEvent =
|
|
|
129
129
|
|
|
130
130
|
export type ToolRunnerEvent =
|
|
131
131
|
| { type: "output"; text: string }
|
|
132
|
-
| {
|
|
132
|
+
| {
|
|
133
|
+
type: "result";
|
|
134
|
+
text: string;
|
|
135
|
+
image?: { data: string; mimeType: string };
|
|
136
|
+
};
|
|
133
137
|
|
|
134
138
|
export type ToolAndRunner = {
|
|
135
139
|
tool: Tool;
|
package/src/tool-task.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { type Message, type Tool, Type } from "@mariozechner/pi-ai";
|
|
2
|
-
import { streamAgent } from "./agent";
|
|
3
|
-
import { buildSystemPrompt, TASK_PROMPT } from "./prompt";
|
|
4
|
-
import { estimateTokens, formatTimestamp } from "./shared";
|
|
5
|
-
import { bash, runBashTool } from "./tool-bash";
|
|
6
|
-
import { edit, runEditTool } from "./tool-edit";
|
|
7
|
-
import type {
|
|
8
|
-
AgentContex,
|
|
9
|
-
CliOptions,
|
|
10
|
-
ToolAndRunner,
|
|
11
|
-
ToolRunnerEvent,
|
|
12
|
-
} from "./types";
|
|
13
|
-
|
|
14
|
-
const description = `## Task tool
|
|
15
|
-
|
|
16
|
-
Use this tool for multi-step work that would otherwise require multiple individual tool calls. The tool executes a detailed plan and returns results.
|
|
17
|
-
|
|
18
|
-
Best practices:
|
|
19
|
-
|
|
20
|
-
- For exploration (codebase, filesystem, web), specify exactly what you are looking for and the expected output format. For example: "Find all files relevant to tests. Return the exact paths and a description of each file."
|
|
21
|
-
- For edits, specify exact diffs and the target file for each change.
|
|
22
|
-
- For anything else, provide a specific, detailed set of instructions and your expected results.
|
|
23
|
-
`;
|
|
24
|
-
|
|
25
|
-
export const task: Tool = {
|
|
26
|
-
name: "task",
|
|
27
|
-
description,
|
|
28
|
-
parameters: Type.Object({
|
|
29
|
-
prompt: Type.String({
|
|
30
|
-
description:
|
|
31
|
-
"Detailed description of the work to perform. Be specific about expected outputs and any constraints.",
|
|
32
|
-
}),
|
|
33
|
-
}),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export async function* runTaskTool(
|
|
37
|
-
options: CliOptions,
|
|
38
|
-
args: Record<string, any>,
|
|
39
|
-
signal?: AbortSignal,
|
|
40
|
-
): AsyncGenerator<ToolRunnerEvent> {
|
|
41
|
-
const tools: ToolAndRunner[] = [
|
|
42
|
-
{ tool: bash, runner: runBashTool },
|
|
43
|
-
{ tool: edit, runner: runEditTool },
|
|
44
|
-
];
|
|
45
|
-
const messages: Message[] = [
|
|
46
|
-
{ role: "user", content: args.prompt || "", timestamp: Date.now() },
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
const systemPrompt = await buildSystemPrompt(TASK_PROMPT);
|
|
50
|
-
const ctx: AgentContex = {
|
|
51
|
-
systemPrompt,
|
|
52
|
-
tools,
|
|
53
|
-
messages,
|
|
54
|
-
options,
|
|
55
|
-
signal,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
let output = "";
|
|
59
|
-
const agent = streamAgent(ctx);
|
|
60
|
-
for await (const ev of agent) {
|
|
61
|
-
switch (ev.type) {
|
|
62
|
-
case "message_start": {
|
|
63
|
-
const text = `[${formatTimestamp(ev.partial.timestamp)}] Working...`;
|
|
64
|
-
output += text;
|
|
65
|
-
yield { type: "output", text };
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
case "message_end": {
|
|
69
|
-
const thinking = ev.message.content
|
|
70
|
-
.filter((b) => b.type === "thinking")
|
|
71
|
-
.map((b) => b.thinking)
|
|
72
|
-
.join("");
|
|
73
|
-
const messageText = ev.message.content
|
|
74
|
-
.filter((b) => b.type === "text")
|
|
75
|
-
.map((b) => b.text)
|
|
76
|
-
.join("");
|
|
77
|
-
const calls = ev.message.content
|
|
78
|
-
.filter((b) => b.type === "toolCall")
|
|
79
|
-
.map((b) => b.name);
|
|
80
|
-
|
|
81
|
-
let text = `[${formatTimestamp(ev.message.timestamp)}]`;
|
|
82
|
-
if (thinking.length > 0) {
|
|
83
|
-
const thinkingTokens = estimateTokens(thinking);
|
|
84
|
-
text += `Thinking... (${thinkingTokens} tokens)`;
|
|
85
|
-
}
|
|
86
|
-
if (messageText.length > 0) {
|
|
87
|
-
text += `\n\n${messageText}\n`;
|
|
88
|
-
}
|
|
89
|
-
if (calls.length > 0) {
|
|
90
|
-
text += ` \nTool calls:`;
|
|
91
|
-
for (const c of calls) {
|
|
92
|
-
text += `\n${c}`;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
output += text;
|
|
97
|
-
yield { type: "output", text };
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
case "tool_message_end": {
|
|
101
|
-
const ts = formatTimestamp(ev.message.timestamp);
|
|
102
|
-
const name = ev.message.toolName;
|
|
103
|
-
const symbol = !ev.message.isError ? "✓ " : "✗ ";
|
|
104
|
-
const text = `[${ts}] ${name} ${symbol}`;
|
|
105
|
-
|
|
106
|
-
output += text;
|
|
107
|
-
yield { type: "output", text };
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
yield { type: "result", text: output };
|
|
114
|
-
}
|