mini-coder 0.5.12 → 0.5.14
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/BENCHMARK.md +15 -316
- package/PROGRESS.md +3 -3
- package/README.md +54 -21
- package/benchmark-baseline.sh +15 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +526 -13
- package/src/assistant-output.ts +73 -0
- package/src/cli.ts +2 -1
- package/src/delegation.ts +238 -0
- package/src/headless.ts +90 -44
- package/src/index.ts +267 -102
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +11 -13
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +24 -2
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +190 -8
- package/src/tools.ts +335 -6
- package/src/ui/agent.ts +10 -0
- package/src/ui/commands.test.ts +525 -10
- package/src/ui/commands.ts +224 -8
- package/src/ui/conversation.test.ts +252 -27
- package/src/ui/conversation.ts +468 -390
- package/src/ui/help.ts +27 -11
- package/src/ui.ts +230 -75
- package/src/plugins.ts +0 -183
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class delegated-subagent tool.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Static, Tool } from "@mariozechner/pi-ai";
|
|
8
|
+
import { Type } from "@mariozechner/pi-ai";
|
|
9
|
+
import type { ToolHandler, ToolUpdateCallback } from "./agent.ts";
|
|
10
|
+
import {
|
|
11
|
+
reserveToolDelegation,
|
|
12
|
+
type ShellDelegationContext,
|
|
13
|
+
} from "./delegation.ts";
|
|
14
|
+
import { textResult, validateBuiltinToolArgs } from "./tool-common.ts";
|
|
15
|
+
|
|
16
|
+
const delegateToolParameters = Type.Object({
|
|
17
|
+
task: Type.String({
|
|
18
|
+
description:
|
|
19
|
+
"Bounded subtask prompt for the delegated subagent. Include the specific goal, constraints, and expected deliverable.",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Arguments for the `delegate` tool. */
|
|
24
|
+
export type DelegateArgs = Static<typeof delegateToolParameters>;
|
|
25
|
+
|
|
26
|
+
/** Structured details preserved on a persisted `delegate` tool result. */
|
|
27
|
+
export interface DelegateResultDetails {
|
|
28
|
+
/** How the delegated subagent run ended. */
|
|
29
|
+
stopReason: "stop" | "length" | "error" | "aborted";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Summarized output returned from one delegated subagent run. */
|
|
33
|
+
export interface DelegateRunResult {
|
|
34
|
+
/** How the delegated subagent run ended. */
|
|
35
|
+
stopReason: DelegateResultDetails["stopReason"];
|
|
36
|
+
/** Final assistant text emitted by the subagent. */
|
|
37
|
+
finalText: string;
|
|
38
|
+
/** Terminal assistant error text when the subagent failed. */
|
|
39
|
+
errorText: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** pi-ai tool definition for `delegate`. */
|
|
43
|
+
export const delegateTool: Tool<typeof delegateToolParameters> = {
|
|
44
|
+
name: "delegate",
|
|
45
|
+
description:
|
|
46
|
+
"Run a bounded subtask in an isolated subagent session using the current model, prompt context, and tools. " +
|
|
47
|
+
"Use this when a focused second agent pass will help, and prefer it over shelling out to `mc -p` unless you are explicitly testing the CLI itself.",
|
|
48
|
+
parameters: delegateToolParameters,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Options for a `delegate` handler that enforces delegation safeguards. */
|
|
52
|
+
export interface CreateDelegateToolHandlerOpts {
|
|
53
|
+
/** Return the current subagent-delegation context for the active run. */
|
|
54
|
+
getDelegationContext: () => ShellDelegationContext;
|
|
55
|
+
/** Persist the updated delegation context after a launch reservation. */
|
|
56
|
+
setDelegationContext: (context: ShellDelegationContext) => void;
|
|
57
|
+
/** Execute one delegated subagent run. */
|
|
58
|
+
runSubagent: (
|
|
59
|
+
task: string,
|
|
60
|
+
context: ShellDelegationContext,
|
|
61
|
+
signal?: AbortSignal,
|
|
62
|
+
onUpdate?: ToolUpdateCallback,
|
|
63
|
+
) => Promise<DelegateRunResult>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format the model-visible text payload returned from a delegated subagent run.
|
|
68
|
+
*
|
|
69
|
+
* @param result - Delegated subagent result.
|
|
70
|
+
* @returns Text content suitable for the parent tool result.
|
|
71
|
+
*/
|
|
72
|
+
export function formatDelegateResultText(result: DelegateRunResult): string {
|
|
73
|
+
const lines = [`Subagent stop reason: ${result.stopReason}`];
|
|
74
|
+
|
|
75
|
+
if (result.finalText.length > 0) {
|
|
76
|
+
lines.push("", "Final answer:", result.finalText);
|
|
77
|
+
} else {
|
|
78
|
+
lines.push("", "Final answer:", "(no final text)");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (result.errorText) {
|
|
82
|
+
lines.push("", "Terminal error:", result.errorText);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a `delegate` tool handler that enforces delegation limits.
|
|
90
|
+
*
|
|
91
|
+
* @param opts - Delegation-context accessors plus the delegated-run executor.
|
|
92
|
+
* @returns A tool handler for first-class delegated subagent runs.
|
|
93
|
+
*/
|
|
94
|
+
export function createDelegateToolHandler(
|
|
95
|
+
opts: CreateDelegateToolHandlerOpts,
|
|
96
|
+
): ToolHandler {
|
|
97
|
+
return async (args, _cwd, signal, onUpdate) => {
|
|
98
|
+
const validatedArgs = validateBuiltinToolArgs(delegateTool, args);
|
|
99
|
+
const reservation = reserveToolDelegation(opts.getDelegationContext());
|
|
100
|
+
if (!reservation.ok) {
|
|
101
|
+
return textResult(reservation.error, true);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
opts.setDelegationContext(reservation.reservation.updatedContext);
|
|
105
|
+
const result = await opts.runSubagent(
|
|
106
|
+
validatedArgs.task,
|
|
107
|
+
reservation.reservation.childContext,
|
|
108
|
+
signal,
|
|
109
|
+
onUpdate,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: formatDelegateResultText(result),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
details: {
|
|
120
|
+
stopReason: result.stopReason,
|
|
121
|
+
} satisfies DelegateResultDetails,
|
|
122
|
+
isError: result.stopReason === "error" || result.stopReason === "aborted",
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/tool-shell.ts
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
import type { Static, Tool } from "@mariozechner/pi-ai";
|
|
8
8
|
import { Type } from "@mariozechner/pi-ai";
|
|
9
9
|
import type { ToolHandler, ToolUpdateCallback } from "./agent.ts";
|
|
10
|
+
import {
|
|
11
|
+
buildShellDelegationEnv,
|
|
12
|
+
reserveShellDelegation,
|
|
13
|
+
type ShellDelegationContext,
|
|
14
|
+
} from "./delegation.ts";
|
|
15
|
+
import { readFiniteNumber, readString, toRecord } from "./shared.ts";
|
|
10
16
|
import {
|
|
11
17
|
detectLineEnding,
|
|
12
18
|
normalizeLineEndings,
|
|
@@ -32,6 +38,18 @@ export interface ShellOpts {
|
|
|
32
38
|
signal?: AbortSignal;
|
|
33
39
|
/** Callback for progressive output updates while the command is running. */
|
|
34
40
|
onUpdate?: ToolUpdateCallback;
|
|
41
|
+
/** Optional environment overrides for the spawned shell process. */
|
|
42
|
+
env?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Structured shell result preserved on tool-result messages and events. */
|
|
46
|
+
export interface ShellResultDetails {
|
|
47
|
+
/** Captured stdout text after shell-tool truncation. */
|
|
48
|
+
stdout: string;
|
|
49
|
+
/** Captured stderr text after shell-tool truncation. */
|
|
50
|
+
stderr: string;
|
|
51
|
+
/** Process exit code. */
|
|
52
|
+
exitCode: number;
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
/** pi-ai tool definition for `shell`. */
|
|
@@ -59,27 +77,178 @@ export const shellToolHandler: ToolHandler = (args, cwd, signal, onUpdate) =>
|
|
|
59
77
|
...(onUpdate ? { onUpdate } : {}),
|
|
60
78
|
});
|
|
61
79
|
|
|
80
|
+
/** Options for a shell handler that enforces shell-level delegation safeguards. */
|
|
81
|
+
export interface DelegationAwareShellToolHandlerOpts {
|
|
82
|
+
/** Return the current shell-level delegation context for the active run. */
|
|
83
|
+
getDelegationContext: () => ShellDelegationContext;
|
|
84
|
+
/** Persist the updated delegation context after a launch reservation. */
|
|
85
|
+
setDelegationContext: (context: ShellDelegationContext) => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a shell handler that propagates and enforces `mc -p` delegation limits.
|
|
90
|
+
*
|
|
91
|
+
* @param opts - Delegation-context accessors for the active run.
|
|
92
|
+
* @returns A shell tool handler that blocks recursive or over-budget delegation.
|
|
93
|
+
*/
|
|
94
|
+
export function createDelegationAwareShellToolHandler(
|
|
95
|
+
opts: DelegationAwareShellToolHandlerOpts,
|
|
96
|
+
): ToolHandler {
|
|
97
|
+
return (args, cwd, signal, onUpdate) => {
|
|
98
|
+
const validatedArgs = validateBuiltinToolArgs(shellTool, args);
|
|
99
|
+
const reservation = reserveShellDelegation(
|
|
100
|
+
validatedArgs.command,
|
|
101
|
+
opts.getDelegationContext(),
|
|
102
|
+
);
|
|
103
|
+
if (!reservation.ok) {
|
|
104
|
+
return textResult(reservation.error, true);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
opts.setDelegationContext(reservation.reservation.updatedContext);
|
|
108
|
+
return executeShell(validatedArgs, cwd, {
|
|
109
|
+
...(signal ? { signal } : {}),
|
|
110
|
+
...(onUpdate ? { onUpdate } : {}),
|
|
111
|
+
env: buildShellDelegationEnv(reservation.reservation.childContext),
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
62
116
|
type ShellProcess = ReturnType<typeof Bun.spawn>;
|
|
63
117
|
|
|
64
118
|
const DEFAULT_MAX_LINES = 1000;
|
|
65
119
|
const DEFAULT_MAX_BYTES = 50_000;
|
|
66
120
|
const SHELL_UPDATE_INTERVAL_MS = 75;
|
|
67
121
|
const SHELL_STREAM_DRAIN_TIMEOUT_MS = 25;
|
|
122
|
+
const LEGACY_SHELL_STDERR_PREFIX = "[stderr]\n";
|
|
123
|
+
const LEGACY_SHELL_STDERR_SEPARATOR = `\n\n${LEGACY_SHELL_STDERR_PREFIX}`;
|
|
68
124
|
|
|
69
|
-
/** Format combined stdout/stderr for
|
|
125
|
+
/** Format combined stdout/stderr for the legacy text payload preserved for model context. */
|
|
70
126
|
function formatShellOutput(stdout: string, stderr: string): string {
|
|
71
127
|
if (stdout && stderr) {
|
|
72
|
-
return `${stdout}
|
|
128
|
+
return `${stdout}${LEGACY_SHELL_STDERR_SEPARATOR}${stderr}`;
|
|
73
129
|
}
|
|
74
130
|
if (stdout) {
|
|
75
131
|
return stdout;
|
|
76
132
|
}
|
|
77
133
|
if (stderr) {
|
|
78
|
-
return
|
|
134
|
+
return `${LEGACY_SHELL_STDERR_PREFIX}${stderr}`;
|
|
79
135
|
}
|
|
80
136
|
return "";
|
|
81
137
|
}
|
|
82
138
|
|
|
139
|
+
/** Format the shell result as the legacy text payload stored in tool content. */
|
|
140
|
+
export function formatShellResultText(result: ShellResultDetails): string {
|
|
141
|
+
const body = formatShellOutput(result.stdout, result.stderr) || "(no output)";
|
|
142
|
+
return `Exit code: ${result.exitCode}\n${body}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Parse structured shell-result details from a persisted tool-result message. */
|
|
146
|
+
export function parseShellResultDetails(
|
|
147
|
+
details: unknown,
|
|
148
|
+
): ShellResultDetails | null {
|
|
149
|
+
const record = toRecord(details);
|
|
150
|
+
if (!record) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stdout = readString(record, "stdout");
|
|
155
|
+
const stderr = readString(record, "stderr");
|
|
156
|
+
const exitCode = readFiniteNumber(record, "exitCode");
|
|
157
|
+
if (stdout === null || stderr === null || exitCode === null) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { stdout, stderr, exitCode };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Parse the legacy flattened shell-result text stored by older builds. */
|
|
165
|
+
export function parseLegacyShellResult(
|
|
166
|
+
text: string,
|
|
167
|
+
): ShellResultDetails | null {
|
|
168
|
+
const match = /^Exit code: (\d+)(?:\n([\s\S]*))?$/.exec(
|
|
169
|
+
normalizeLineEndings(text, "\n"),
|
|
170
|
+
);
|
|
171
|
+
if (!match) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const exitCodeText = match[1];
|
|
176
|
+
if (!exitCodeText) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const exitCode = Number.parseInt(exitCodeText, 10);
|
|
181
|
+
const body = match[2] ?? "";
|
|
182
|
+
if (body === "" || body === "(no output)") {
|
|
183
|
+
return { stdout: "", stderr: "", exitCode };
|
|
184
|
+
}
|
|
185
|
+
if (body.startsWith(LEGACY_SHELL_STDERR_PREFIX)) {
|
|
186
|
+
return {
|
|
187
|
+
stdout: "",
|
|
188
|
+
stderr: body.slice(LEGACY_SHELL_STDERR_PREFIX.length),
|
|
189
|
+
exitCode,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const separatorIndex = body.indexOf(LEGACY_SHELL_STDERR_SEPARATOR);
|
|
194
|
+
if (separatorIndex === -1) {
|
|
195
|
+
return { stdout: body, stderr: "", exitCode };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
stdout: body.slice(0, separatorIndex),
|
|
200
|
+
stderr: body.slice(separatorIndex + LEGACY_SHELL_STDERR_SEPARATOR.length),
|
|
201
|
+
exitCode,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function truncateShellResult(
|
|
206
|
+
result: ShellResultDetails,
|
|
207
|
+
maxLines: number,
|
|
208
|
+
maxBytes: number,
|
|
209
|
+
): ShellResultDetails {
|
|
210
|
+
if (result.stdout === "" || result.stderr === "") {
|
|
211
|
+
return {
|
|
212
|
+
stdout:
|
|
213
|
+
result.stdout === ""
|
|
214
|
+
? ""
|
|
215
|
+
: truncateOutput(result.stdout, maxLines, maxBytes),
|
|
216
|
+
stderr:
|
|
217
|
+
result.stderr === ""
|
|
218
|
+
? ""
|
|
219
|
+
: truncateOutput(result.stderr, maxLines, maxBytes),
|
|
220
|
+
exitCode: result.exitCode,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
stdout: truncateOutput(
|
|
226
|
+
result.stdout,
|
|
227
|
+
Math.max(1, Math.ceil(maxLines / 2)),
|
|
228
|
+
Math.max(1, Math.ceil(maxBytes / 2)),
|
|
229
|
+
),
|
|
230
|
+
stderr: truncateOutput(
|
|
231
|
+
result.stderr,
|
|
232
|
+
Math.max(1, Math.floor(maxLines / 2)),
|
|
233
|
+
Math.max(1, Math.floor(maxBytes / 2)),
|
|
234
|
+
),
|
|
235
|
+
exitCode: result.exitCode,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildShellToolResult(
|
|
240
|
+
result: ShellResultDetails,
|
|
241
|
+
maxLines: number,
|
|
242
|
+
maxBytes: number,
|
|
243
|
+
): ToolExecResult {
|
|
244
|
+
const truncated = truncateShellResult(result, maxLines, maxBytes);
|
|
245
|
+
return {
|
|
246
|
+
content: [{ type: "text", text: formatShellResultText(truncated) }],
|
|
247
|
+
details: truncated,
|
|
248
|
+
isError: truncated.exitCode !== 0,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
83
252
|
interface ShellCommandLines {
|
|
84
253
|
lines: string[];
|
|
85
254
|
lineEnding: "\n" | "\r\n";
|
|
@@ -488,9 +657,13 @@ async function finalizeShellStreamCaptures(
|
|
|
488
657
|
await Promise.all(captures.map((capture) => capture.close()));
|
|
489
658
|
}
|
|
490
659
|
|
|
491
|
-
function buildShellSpawnOptions(
|
|
660
|
+
function buildShellSpawnOptions(
|
|
661
|
+
cwd: string,
|
|
662
|
+
env?: Record<string, string>,
|
|
663
|
+
): Parameters<typeof Bun.spawn>[1] {
|
|
492
664
|
return {
|
|
493
665
|
cwd,
|
|
666
|
+
...(env ? { env: { ...process.env, ...env } } : {}),
|
|
494
667
|
stdout: "pipe",
|
|
495
668
|
stderr: "pipe",
|
|
496
669
|
...(process.platform === "win32" ? {} : { detached: true }),
|
|
@@ -621,7 +794,10 @@ export async function executeShell(
|
|
|
621
794
|
};
|
|
622
795
|
|
|
623
796
|
const command = normalizeShellCommand(args.command);
|
|
624
|
-
const proc = Bun.spawn(
|
|
797
|
+
const proc = Bun.spawn(
|
|
798
|
+
[shell, "-c", command],
|
|
799
|
+
buildShellSpawnOptions(cwd, opts?.env),
|
|
800
|
+
);
|
|
625
801
|
cleanupAbort = registerShellAbort(opts?.signal, proc);
|
|
626
802
|
stdoutCapture = startShellStreamCapture(
|
|
627
803
|
proc.stdout as ReadableStream<Uint8Array>,
|
|
@@ -648,9 +824,15 @@ export async function executeShell(
|
|
|
648
824
|
opts.onUpdate(textResult(output, false));
|
|
649
825
|
}
|
|
650
826
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
827
|
+
return buildShellToolResult(
|
|
828
|
+
{
|
|
829
|
+
stdout: stdoutCapture?.getOutput().trimEnd() ?? "",
|
|
830
|
+
stderr: stderrCapture?.getOutput().trimEnd() ?? "",
|
|
831
|
+
exitCode,
|
|
832
|
+
},
|
|
833
|
+
maxLines,
|
|
834
|
+
maxBytes,
|
|
835
|
+
);
|
|
654
836
|
} catch (err) {
|
|
655
837
|
cleanupAbort?.();
|
|
656
838
|
cleanupAbort = null;
|