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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for extracting user-visible assistant output.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
8
|
+
import {
|
|
9
|
+
collapseWhitespaceToNull,
|
|
10
|
+
joinTextBlocks,
|
|
11
|
+
truncateText,
|
|
12
|
+
} from "./text.ts";
|
|
13
|
+
|
|
14
|
+
const ASSISTANT_ACTIVITY_MAX_CHARS = 160;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract concatenated text blocks from an assistant message.
|
|
18
|
+
*
|
|
19
|
+
* @param message - Assistant message to inspect.
|
|
20
|
+
* @returns The combined text content, or an empty string when none exists.
|
|
21
|
+
*/
|
|
22
|
+
export function extractAssistantText(message: AssistantMessage | null): string {
|
|
23
|
+
if (!message) {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return message.content
|
|
28
|
+
.filter(
|
|
29
|
+
(
|
|
30
|
+
block,
|
|
31
|
+
): block is Extract<
|
|
32
|
+
AssistantMessage["content"][number],
|
|
33
|
+
{ type: "text" }
|
|
34
|
+
> => {
|
|
35
|
+
return block.type === "text";
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
.map((block) => block.text)
|
|
39
|
+
.join("");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract a short assistant activity snippet from a tool-using message.
|
|
44
|
+
*
|
|
45
|
+
* @param message - Assistant message to inspect.
|
|
46
|
+
* @returns A collapsed activity snippet, or `null` when no snippet applies.
|
|
47
|
+
*/
|
|
48
|
+
export function extractAssistantActivitySnippet(
|
|
49
|
+
message: AssistantMessage,
|
|
50
|
+
): string | null {
|
|
51
|
+
if (!message.content.some((block) => block.type === "toolCall")) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const text = collapseWhitespaceToNull(joinTextBlocks(message.content));
|
|
56
|
+
return text ? truncateText(text, ASSISTANT_ACTIVITY_MAX_CHARS) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract terminal assistant error text from a failed assistant message.
|
|
61
|
+
*
|
|
62
|
+
* @param message - Assistant message to inspect.
|
|
63
|
+
* @returns Collapsed error text, or `null` when the message is not terminally errored.
|
|
64
|
+
*/
|
|
65
|
+
export function extractAssistantErrorText(
|
|
66
|
+
message: AssistantMessage | null,
|
|
67
|
+
): string | null {
|
|
68
|
+
if (!message || message.stopReason !== "error" || !message.errorMessage) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return collapseWhitespaceToNull(message.errorMessage) ?? null;
|
|
73
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -32,7 +32,8 @@ export interface TtyState {
|
|
|
32
32
|
* Parse supported CLI arguments.
|
|
33
33
|
*
|
|
34
34
|
* Supports `-p, --prompt <text>` for headless one-shot mode and `--json`
|
|
35
|
-
* to stream NDJSON events instead of the default final-text
|
|
35
|
+
* to stream NDJSON events instead of the default final-text mode
|
|
36
|
+
* (stdout final answer plus stderr activity snippets).
|
|
36
37
|
* Unknown flags and positional arguments fail eagerly.
|
|
37
38
|
*
|
|
38
39
|
* @param argv - Process arguments excluding the Bun executable and script path.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent delegation safeguards.
|
|
3
|
+
*
|
|
4
|
+
* Tracks a shallow delegation depth plus a small per-run delegation budget so
|
|
5
|
+
* first-class `delegate` tool runs and shell-authored `mc -p` child processes
|
|
6
|
+
* cannot recurse forever.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SHELL_DELEGATION_COMMAND =
|
|
12
|
+
/(?:^|[;&|()\s])(?:[^\s;&|()]+\/)?mc\s+(?:-p\b|--prompt(?:\b|=))/g;
|
|
13
|
+
|
|
14
|
+
/** Environment variable carrying the current subagent-delegation depth. */
|
|
15
|
+
export const SHELL_DELEGATION_DEPTH_ENV = "MC_SUBAGENT_DEPTH";
|
|
16
|
+
|
|
17
|
+
/** Environment variable carrying the remaining subagent-delegation budget. */
|
|
18
|
+
export const SHELL_DELEGATION_BUDGET_ENV = "MC_SUBAGENT_BUDGET";
|
|
19
|
+
|
|
20
|
+
/** Maximum allowed subagent delegation depth. */
|
|
21
|
+
export const MAX_SHELL_DELEGATION_DEPTH = 1;
|
|
22
|
+
|
|
23
|
+
/** Default number of delegated subagent launches allowed per agent run. */
|
|
24
|
+
export const DEFAULT_SHELL_DELEGATION_BUDGET = 4;
|
|
25
|
+
|
|
26
|
+
/** Current subagent-delegation context for one app run. */
|
|
27
|
+
export interface ShellDelegationContext {
|
|
28
|
+
/** Current subagent-delegation depth for this app process. */
|
|
29
|
+
depth: number;
|
|
30
|
+
/** Remaining delegated-subagent launches available in the active run. */
|
|
31
|
+
remainingBudget: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Result of reserving subagent-delegation budget for one launch. */
|
|
35
|
+
export interface ShellDelegationReservation {
|
|
36
|
+
/** Number of delegated-subagent launches reserved by the request. */
|
|
37
|
+
launchCount: number;
|
|
38
|
+
/** Updated parent-run context after reserving any delegated launches. */
|
|
39
|
+
updatedContext: ShellDelegationContext;
|
|
40
|
+
/** Context that delegated child runs should inherit for this request. */
|
|
41
|
+
childContext: ShellDelegationContext;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Successful or blocked delegation-reservation outcome. */
|
|
45
|
+
export type ReserveShellDelegationResult =
|
|
46
|
+
| {
|
|
47
|
+
/** Whether the delegation request may proceed. */
|
|
48
|
+
ok: true;
|
|
49
|
+
/** Reserved delegation details for the request. */
|
|
50
|
+
reservation: ShellDelegationReservation;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
/** Whether the delegation request may proceed. */
|
|
54
|
+
ok: false;
|
|
55
|
+
/** Human-readable tool error for the blocked delegation attempt. */
|
|
56
|
+
error: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type DelegationReservationError = "nested" | "exhausted" | "over_budget";
|
|
60
|
+
|
|
61
|
+
function readNonNegativeInteger(
|
|
62
|
+
value: string | undefined,
|
|
63
|
+
fallback: number,
|
|
64
|
+
): number {
|
|
65
|
+
if (value == null || value === "") {
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parsed = Number.parseInt(value, 10);
|
|
70
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function reserveDelegationBudget(
|
|
74
|
+
context: ShellDelegationContext,
|
|
75
|
+
launchCount: number,
|
|
76
|
+
):
|
|
77
|
+
| {
|
|
78
|
+
ok: true;
|
|
79
|
+
reservation: ShellDelegationReservation;
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
ok: false;
|
|
83
|
+
reason: DelegationReservationError;
|
|
84
|
+
} {
|
|
85
|
+
if (launchCount === 0) {
|
|
86
|
+
const unchanged = { ...context };
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
reservation: {
|
|
90
|
+
launchCount,
|
|
91
|
+
updatedContext: unchanged,
|
|
92
|
+
childContext: unchanged,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (context.depth >= MAX_SHELL_DELEGATION_DEPTH) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
reason: "nested",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (context.remainingBudget === 0) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: "exhausted",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (launchCount > context.remainingBudget) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason: "over_budget",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const remainingBudget = context.remainingBudget - launchCount;
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
reservation: {
|
|
122
|
+
launchCount,
|
|
123
|
+
updatedContext: {
|
|
124
|
+
depth: context.depth,
|
|
125
|
+
remainingBudget,
|
|
126
|
+
},
|
|
127
|
+
childContext: {
|
|
128
|
+
depth: context.depth + 1,
|
|
129
|
+
remainingBudget,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read the current subagent-delegation context from environment variables.
|
|
137
|
+
*
|
|
138
|
+
* Missing or invalid values fall back to the safe root-run defaults.
|
|
139
|
+
*
|
|
140
|
+
* @param env - Environment variables to inspect.
|
|
141
|
+
* @returns The parsed subagent-delegation context.
|
|
142
|
+
*/
|
|
143
|
+
export function readShellDelegationContext(
|
|
144
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
145
|
+
): ShellDelegationContext {
|
|
146
|
+
return {
|
|
147
|
+
depth: readNonNegativeInteger(env[SHELL_DELEGATION_DEPTH_ENV], 0),
|
|
148
|
+
remainingBudget: readNonNegativeInteger(
|
|
149
|
+
env[SHELL_DELEGATION_BUDGET_ENV],
|
|
150
|
+
DEFAULT_SHELL_DELEGATION_BUDGET,
|
|
151
|
+
),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build the environment-variable overrides for a subagent-delegation context.
|
|
157
|
+
*
|
|
158
|
+
* @param context - Subagent-delegation context to serialize.
|
|
159
|
+
* @returns Environment overrides for child shell processes.
|
|
160
|
+
*/
|
|
161
|
+
export function buildShellDelegationEnv(
|
|
162
|
+
context: ShellDelegationContext,
|
|
163
|
+
): Record<string, string> {
|
|
164
|
+
return {
|
|
165
|
+
[SHELL_DELEGATION_DEPTH_ENV]: String(context.depth),
|
|
166
|
+
[SHELL_DELEGATION_BUDGET_ENV]: String(context.remainingBudget),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Count likely shell-authored `mc -p` / `mc --prompt` launches in one command.
|
|
172
|
+
*
|
|
173
|
+
* The detector is intentionally narrow and optimized for the prompt-guided
|
|
174
|
+
* `mc -p "subtask"` pattern mini-coder still supports for CLI-level testing.
|
|
175
|
+
*
|
|
176
|
+
* @param command - Raw shell command.
|
|
177
|
+
* @returns Number of likely `mc -p` launches in the command.
|
|
178
|
+
*/
|
|
179
|
+
export function countShellDelegationLaunches(command: string): number {
|
|
180
|
+
return command.match(SHELL_DELEGATION_COMMAND)?.length ?? 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Reserve first-class `delegate` tool budget for one delegated subagent run.
|
|
185
|
+
*
|
|
186
|
+
* @param context - Current subagent-delegation context for the active run.
|
|
187
|
+
* @returns Reservation details, or a blocking error when this delegation would
|
|
188
|
+
* exceed the allowed subagent-delegation policy.
|
|
189
|
+
*/
|
|
190
|
+
export function reserveToolDelegation(
|
|
191
|
+
context: ShellDelegationContext,
|
|
192
|
+
): ReserveShellDelegationResult {
|
|
193
|
+
const reservation = reserveDelegationBudget(context, 1);
|
|
194
|
+
if (!reservation.ok) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
error:
|
|
198
|
+
reservation.reason === "nested"
|
|
199
|
+
? "Tool delegation blocked: delegated `delegate` tool runs may not delegate again."
|
|
200
|
+
: "Tool delegation blocked: this run has no remaining `delegate` delegation budget.",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return reservation;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Reserve shell-level delegation budget for a command before execution.
|
|
209
|
+
*
|
|
210
|
+
* Non-delegating commands pass through unchanged. Commands that would exceed
|
|
211
|
+
* the per-run delegation budget or the maximum allowed depth are rejected with
|
|
212
|
+
* a user-visible tool error.
|
|
213
|
+
*
|
|
214
|
+
* @param command - Raw shell command to inspect.
|
|
215
|
+
* @param context - Current subagent-delegation context for the active run.
|
|
216
|
+
* @returns Reservation details, or a blocking error when the command would
|
|
217
|
+
* exceed the allowed shell-level delegation policy.
|
|
218
|
+
*/
|
|
219
|
+
export function reserveShellDelegation(
|
|
220
|
+
command: string,
|
|
221
|
+
context: ShellDelegationContext,
|
|
222
|
+
): ReserveShellDelegationResult {
|
|
223
|
+
const launchCount = countShellDelegationLaunches(command);
|
|
224
|
+
const reservation = reserveDelegationBudget(context, launchCount);
|
|
225
|
+
if (!reservation.ok) {
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
error:
|
|
229
|
+
reservation.reason === "nested"
|
|
230
|
+
? "Shell delegation blocked: delegated `mc -p` runs may not launch more `mc -p` subagents."
|
|
231
|
+
: reservation.reason === "exhausted"
|
|
232
|
+
? "Shell delegation blocked: this run has no remaining `mc -p` delegation budget."
|
|
233
|
+
: "Shell delegation blocked: this command appears to launch more `mc -p` subagents than the remaining delegation budget allows.",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return reservation;
|
|
238
|
+
}
|
package/src/headless.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import type { AssistantMessage, UserMessage } from "@mariozechner/pi-ai";
|
|
8
8
|
import type { AgentEvent } from "./agent.ts";
|
|
9
|
+
import {
|
|
10
|
+
extractAssistantActivitySnippet,
|
|
11
|
+
extractAssistantErrorText,
|
|
12
|
+
extractAssistantText,
|
|
13
|
+
} from "./assistant-output.ts";
|
|
9
14
|
import type { AppState } from "./index.ts";
|
|
10
15
|
import {
|
|
11
16
|
resolveRawInput,
|
|
@@ -27,31 +32,45 @@ export interface HeadlessRunOptions {
|
|
|
27
32
|
|
|
28
33
|
/** Options for a headless final-text run. */
|
|
29
34
|
export interface HeadlessTextRunOptions {
|
|
35
|
+
/** Optional writer for lightweight assistant-activity snippets. */
|
|
36
|
+
writeActivity?: (text: string) => void | Promise<void>;
|
|
30
37
|
/** Optional writer for the final assistant text output. */
|
|
31
38
|
writeText?: (text: string) => void | Promise<void>;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
interface HeadlessOutputController {
|
|
35
|
-
/** Queue text for
|
|
42
|
+
/** Queue text for one output stream with broken-pipe handling. */
|
|
36
43
|
write(text: string): void;
|
|
37
|
-
/** Attach
|
|
44
|
+
/** Attach error handlers for the active run. */
|
|
38
45
|
attach(): void;
|
|
39
|
-
/** Remove
|
|
46
|
+
/** Remove error handlers after the run. */
|
|
40
47
|
detach(): void;
|
|
41
48
|
/** Wait for queued writes and resolve the final stop reason. */
|
|
42
49
|
finalize(stopReason: HeadlessStopReason): Promise<HeadlessStopReason>;
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
interface HeadlessProcessStream {
|
|
53
|
+
/** Register an output-stream error handler. */
|
|
54
|
+
on(event: "error", listener: (error: unknown) => void): void;
|
|
55
|
+
/** Remove an output-stream error handler. */
|
|
56
|
+
off(event: "error", listener: (error: unknown) => void): void;
|
|
57
|
+
/** Write a text chunk to the stream. */
|
|
58
|
+
write(text: string, callback?: () => void): boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
// ---------------------------------------------------------------------------
|
|
46
62
|
// Helpers
|
|
47
63
|
// ---------------------------------------------------------------------------
|
|
48
64
|
|
|
49
|
-
function defaultWrite(
|
|
65
|
+
function defaultWrite(
|
|
66
|
+
stream: HeadlessProcessStream,
|
|
67
|
+
text: string,
|
|
68
|
+
): Promise<void> {
|
|
50
69
|
return new Promise((resolve, reject) => {
|
|
51
70
|
let settled = false;
|
|
52
71
|
|
|
53
72
|
const cleanup = (): void => {
|
|
54
|
-
|
|
73
|
+
stream.off("error", handleError);
|
|
55
74
|
};
|
|
56
75
|
|
|
57
76
|
const settle = (callback: () => void): void => {
|
|
@@ -69,9 +88,9 @@ function defaultWrite(text: string): Promise<void> {
|
|
|
69
88
|
});
|
|
70
89
|
};
|
|
71
90
|
|
|
72
|
-
|
|
91
|
+
stream.on("error", handleError);
|
|
73
92
|
try {
|
|
74
|
-
|
|
93
|
+
stream.write(text, () => {
|
|
75
94
|
settle(resolve);
|
|
76
95
|
});
|
|
77
96
|
} catch (error) {
|
|
@@ -122,26 +141,6 @@ function resolveHeadlessContent(
|
|
|
122
141
|
}
|
|
123
142
|
}
|
|
124
143
|
|
|
125
|
-
function extractAssistantText(message: AssistantMessage | null): string {
|
|
126
|
-
if (!message) {
|
|
127
|
-
return "";
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return message.content
|
|
131
|
-
.filter(
|
|
132
|
-
(
|
|
133
|
-
block,
|
|
134
|
-
): block is Extract<
|
|
135
|
-
AssistantMessage["content"][number],
|
|
136
|
-
{ type: "text" }
|
|
137
|
-
> => {
|
|
138
|
-
return block.type === "text";
|
|
139
|
-
},
|
|
140
|
-
)
|
|
141
|
-
.map((block) => block.text)
|
|
142
|
-
.join("");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
144
|
function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
|
|
146
145
|
switch (event.type) {
|
|
147
146
|
case "user_message":
|
|
@@ -158,12 +157,17 @@ function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
|
|
|
158
157
|
|
|
159
158
|
function createHeadlessOutputController(
|
|
160
159
|
state: AppState,
|
|
160
|
+
stream: HeadlessProcessStream,
|
|
161
161
|
writeImpl: (text: string) => void | Promise<void>,
|
|
162
|
+
options?: {
|
|
163
|
+
attachSigint?: boolean;
|
|
164
|
+
},
|
|
162
165
|
): HeadlessOutputController {
|
|
163
166
|
let brokenPipe = false;
|
|
164
167
|
let outputError: unknown = null;
|
|
165
168
|
let pendingWrite = Promise.resolve();
|
|
166
169
|
const sigintHandler = createSigintHandler(state);
|
|
170
|
+
const attachSigint = options?.attachSigint ?? true;
|
|
167
171
|
|
|
168
172
|
const stopForBrokenPipe = (): void => {
|
|
169
173
|
if (brokenPipe) {
|
|
@@ -183,7 +187,7 @@ function createHeadlessOutputController(
|
|
|
183
187
|
state.abortController?.abort();
|
|
184
188
|
};
|
|
185
189
|
|
|
186
|
-
const
|
|
190
|
+
const streamErrorHandler = (error: unknown): void => {
|
|
187
191
|
failOutput(error);
|
|
188
192
|
};
|
|
189
193
|
|
|
@@ -206,12 +210,16 @@ function createHeadlessOutputController(
|
|
|
206
210
|
});
|
|
207
211
|
},
|
|
208
212
|
attach() {
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
stream.on("error", streamErrorHandler);
|
|
214
|
+
if (attachSigint) {
|
|
215
|
+
process.on("SIGINT", sigintHandler);
|
|
216
|
+
}
|
|
211
217
|
},
|
|
212
218
|
detach() {
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
stream.off("error", streamErrorHandler);
|
|
220
|
+
if (attachSigint) {
|
|
221
|
+
process.off("SIGINT", sigintHandler);
|
|
222
|
+
}
|
|
215
223
|
},
|
|
216
224
|
async finalize(stopReason) {
|
|
217
225
|
await pendingWrite;
|
|
@@ -244,7 +252,8 @@ export async function runHeadlessPrompt(
|
|
|
244
252
|
const content = resolveHeadlessContent(state, rawInput);
|
|
245
253
|
const output = createHeadlessOutputController(
|
|
246
254
|
state,
|
|
247
|
-
|
|
255
|
+
process.stdout,
|
|
256
|
+
options?.writeLine ?? ((line) => defaultWrite(process.stdout, `${line}\n`)),
|
|
248
257
|
);
|
|
249
258
|
const hooks: SubmitTurnHooks = {
|
|
250
259
|
onEvent: (event) => {
|
|
@@ -270,11 +279,12 @@ export async function runHeadlessPrompt(
|
|
|
270
279
|
}
|
|
271
280
|
|
|
272
281
|
/**
|
|
273
|
-
* Run a single headless prompt to completion and write
|
|
282
|
+
* Run a single headless prompt to completion and write the final assistant text.
|
|
274
283
|
*
|
|
275
284
|
* The raw input is parsed with the same rules as interactive input. Slash
|
|
276
|
-
* commands are rejected in headless mode.
|
|
277
|
-
*
|
|
285
|
+
* commands are rejected in headless mode. The final assistant text is written
|
|
286
|
+
* to stdout, while lightweight assistant commentary snippets from tool-use
|
|
287
|
+
* turns and terminal assistant error messages are written to stderr.
|
|
278
288
|
*
|
|
279
289
|
* @param state - Mutable application state for the run.
|
|
280
290
|
* @param rawInput - Exact raw prompt text supplied by the user.
|
|
@@ -287,20 +297,43 @@ export async function runHeadlessPromptText(
|
|
|
287
297
|
options?: HeadlessTextRunOptions,
|
|
288
298
|
): Promise<HeadlessStopReason> {
|
|
289
299
|
const content = resolveHeadlessContent(state, rawInput);
|
|
290
|
-
const
|
|
300
|
+
const finalOutput = createHeadlessOutputController(
|
|
301
|
+
state,
|
|
302
|
+
process.stdout,
|
|
303
|
+
options?.writeText ?? ((text) => defaultWrite(process.stdout, text)),
|
|
304
|
+
);
|
|
305
|
+
const activityOutput = createHeadlessOutputController(
|
|
291
306
|
state,
|
|
292
|
-
|
|
307
|
+
process.stderr,
|
|
308
|
+
options?.writeActivity ?? ((text) => defaultWrite(process.stderr, text)),
|
|
309
|
+
{ attachSigint: false },
|
|
293
310
|
);
|
|
294
311
|
let finalAssistantMessage: AssistantMessage | null = null;
|
|
295
312
|
const hooks: SubmitTurnHooks = {
|
|
296
313
|
onEvent: (event) => {
|
|
297
|
-
|
|
298
|
-
|
|
314
|
+
switch (event.type) {
|
|
315
|
+
case "assistant_message": {
|
|
316
|
+
const activitySnippet = extractAssistantActivitySnippet(
|
|
317
|
+
event.message,
|
|
318
|
+
);
|
|
319
|
+
if (activitySnippet) {
|
|
320
|
+
activityOutput.write(`${activitySnippet}\n`);
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
case "done":
|
|
325
|
+
case "error":
|
|
326
|
+
case "aborted":
|
|
327
|
+
finalAssistantMessage = event.message;
|
|
328
|
+
return;
|
|
329
|
+
default:
|
|
330
|
+
return;
|
|
299
331
|
}
|
|
300
332
|
},
|
|
301
333
|
};
|
|
302
334
|
|
|
303
|
-
|
|
335
|
+
finalOutput.attach();
|
|
336
|
+
activityOutput.attach();
|
|
304
337
|
try {
|
|
305
338
|
const stopReason = await submitResolvedInput(
|
|
306
339
|
rawInput,
|
|
@@ -310,10 +343,23 @@ export async function runHeadlessPromptText(
|
|
|
310
343
|
);
|
|
311
344
|
const finalText = extractAssistantText(finalAssistantMessage);
|
|
312
345
|
if (finalText.length > 0) {
|
|
313
|
-
|
|
346
|
+
finalOutput.write(finalText);
|
|
314
347
|
}
|
|
315
|
-
|
|
348
|
+
|
|
349
|
+
const terminalErrorText = extractAssistantErrorText(finalAssistantMessage);
|
|
350
|
+
if (terminalErrorText) {
|
|
351
|
+
activityOutput.write(`${terminalErrorText}\n`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const [finalStopReason, activityStopReason] = await Promise.all([
|
|
355
|
+
finalOutput.finalize(stopReason),
|
|
356
|
+
activityOutput.finalize(stopReason),
|
|
357
|
+
]);
|
|
358
|
+
return finalStopReason === "stop" || activityStopReason === "stop"
|
|
359
|
+
? "stop"
|
|
360
|
+
: stopReason;
|
|
316
361
|
} finally {
|
|
317
|
-
|
|
362
|
+
activityOutput.detach();
|
|
363
|
+
finalOutput.detach();
|
|
318
364
|
}
|
|
319
365
|
}
|