mini-coder 0.5.13 → 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/PROGRESS.md +3 -2
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/agent.ts +506 -13
- package/src/assistant-output.ts +73 -0
- package/src/delegation.ts +238 -0
- package/src/headless.ts +12 -39
- package/src/index.ts +191 -11
- package/src/prompt.ts +9 -1
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/submit.ts +7 -2
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +52 -2
- package/src/tools.ts +331 -6
- package/src/ui/agent.ts +3 -0
- package/src/ui/commands.test.ts +50 -6
- package/src/ui/commands.ts +14 -0
|
@@ -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
|
+
}
|
|
@@ -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,17 +6,17 @@
|
|
|
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,
|
|
12
17
|
type SubmitTurnHooks,
|
|
13
18
|
submitResolvedInput,
|
|
14
19
|
} from "./submit.ts";
|
|
15
|
-
import {
|
|
16
|
-
collapseWhitespaceToNull,
|
|
17
|
-
joinTextBlocks,
|
|
18
|
-
truncateText,
|
|
19
|
-
} from "./text.ts";
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Types
|
|
@@ -58,8 +58,6 @@ interface HeadlessProcessStream {
|
|
|
58
58
|
write(text: string, callback?: () => void): boolean;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const HEADLESS_ACTIVITY_MAX_CHARS = 160;
|
|
62
|
-
|
|
63
61
|
// ---------------------------------------------------------------------------
|
|
64
62
|
// Helpers
|
|
65
63
|
// ---------------------------------------------------------------------------
|
|
@@ -143,37 +141,6 @@ function resolveHeadlessContent(
|
|
|
143
141
|
}
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
function extractAssistantText(message: AssistantMessage | null): string {
|
|
147
|
-
if (!message) {
|
|
148
|
-
return "";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return message.content
|
|
152
|
-
.filter(
|
|
153
|
-
(
|
|
154
|
-
block,
|
|
155
|
-
): block is Extract<
|
|
156
|
-
AssistantMessage["content"][number],
|
|
157
|
-
{ type: "text" }
|
|
158
|
-
> => {
|
|
159
|
-
return block.type === "text";
|
|
160
|
-
},
|
|
161
|
-
)
|
|
162
|
-
.map((block) => block.text)
|
|
163
|
-
.join("");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function extractAssistantActivitySnippet(
|
|
167
|
-
message: AssistantMessage,
|
|
168
|
-
): string | null {
|
|
169
|
-
if (!message.content.some((block) => block.type === "toolCall")) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const text = collapseWhitespaceToNull(joinTextBlocks(message.content));
|
|
174
|
-
return text ? truncateText(text, HEADLESS_ACTIVITY_MAX_CHARS) : null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
144
|
function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
|
|
178
145
|
switch (event.type) {
|
|
179
146
|
case "user_message":
|
|
@@ -317,7 +284,7 @@ export async function runHeadlessPrompt(
|
|
|
317
284
|
* The raw input is parsed with the same rules as interactive input. Slash
|
|
318
285
|
* commands are rejected in headless mode. The final assistant text is written
|
|
319
286
|
* to stdout, while lightweight assistant commentary snippets from tool-use
|
|
320
|
-
* turns are written to stderr.
|
|
287
|
+
* turns and terminal assistant error messages are written to stderr.
|
|
321
288
|
*
|
|
322
289
|
* @param state - Mutable application state for the run.
|
|
323
290
|
* @param rawInput - Exact raw prompt text supplied by the user.
|
|
@@ -378,6 +345,12 @@ export async function runHeadlessPromptText(
|
|
|
378
345
|
if (finalText.length > 0) {
|
|
379
346
|
finalOutput.write(finalText);
|
|
380
347
|
}
|
|
348
|
+
|
|
349
|
+
const terminalErrorText = extractAssistantErrorText(finalAssistantMessage);
|
|
350
|
+
if (terminalErrorText) {
|
|
351
|
+
activityOutput.write(`${terminalErrorText}\n`);
|
|
352
|
+
}
|
|
353
|
+
|
|
381
354
|
const [finalStopReason, activityStopReason] = await Promise.all([
|
|
382
355
|
finalOutput.finalize(stopReason),
|
|
383
356
|
activityOutput.finalize(stopReason),
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { homedir } from "node:os";
|
|
|
13
13
|
import { basename, dirname, join } from "node:path";
|
|
14
14
|
import { isDeepStrictEqual } from "node:util";
|
|
15
15
|
import type {
|
|
16
|
+
AssistantMessage,
|
|
16
17
|
KnownProvider,
|
|
17
18
|
Model,
|
|
18
19
|
OAuthCredentials,
|
|
@@ -22,7 +23,16 @@ import type {
|
|
|
22
23
|
} from "@mariozechner/pi-ai";
|
|
23
24
|
import { getEnvApiKey, getModels, getProviders } from "@mariozechner/pi-ai";
|
|
24
25
|
import { getOAuthApiKey, getOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
|
25
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
runAgentLoop,
|
|
28
|
+
type ToolHandler,
|
|
29
|
+
type ToolUpdateCallback,
|
|
30
|
+
} from "./agent.ts";
|
|
31
|
+
import {
|
|
32
|
+
extractAssistantActivitySnippet,
|
|
33
|
+
extractAssistantErrorText,
|
|
34
|
+
extractAssistantText,
|
|
35
|
+
} from "./assistant-output.ts";
|
|
26
36
|
import {
|
|
27
37
|
type CliOptions,
|
|
28
38
|
parseCliArgs,
|
|
@@ -30,6 +40,10 @@ import {
|
|
|
30
40
|
shouldUseHeadlessMode,
|
|
31
41
|
type TtyState,
|
|
32
42
|
} from "./cli.ts";
|
|
43
|
+
import {
|
|
44
|
+
readShellDelegationContext,
|
|
45
|
+
type ShellDelegationContext,
|
|
46
|
+
} from "./delegation.ts";
|
|
33
47
|
import { getErrorMessage } from "./errors.ts";
|
|
34
48
|
import { type GitState, getGitState } from "./git.ts";
|
|
35
49
|
import { discoverMcpServers, type McpServerState } from "./mcp.ts";
|
|
@@ -44,6 +58,8 @@ import {
|
|
|
44
58
|
appendMessage,
|
|
45
59
|
createConversationSnapshot,
|
|
46
60
|
createSession,
|
|
61
|
+
deleteSession,
|
|
62
|
+
loadCompactedModelMessages,
|
|
47
63
|
type loadMessages,
|
|
48
64
|
openDatabase,
|
|
49
65
|
type Session,
|
|
@@ -60,8 +76,12 @@ import {
|
|
|
60
76
|
import { discoverSkills, type Skill } from "./skills.ts";
|
|
61
77
|
import { DEFAULT_THEME, type Theme } from "./theme.ts";
|
|
62
78
|
import {
|
|
79
|
+
createDelegateToolHandler,
|
|
80
|
+
createDelegationAwareShellToolHandler,
|
|
63
81
|
createTodoReadToolHandler,
|
|
64
82
|
createTodoWriteToolHandler,
|
|
83
|
+
type DelegateRunResult,
|
|
84
|
+
delegateTool,
|
|
65
85
|
editTool,
|
|
66
86
|
editToolHandler,
|
|
67
87
|
grepTool,
|
|
@@ -71,7 +91,6 @@ import {
|
|
|
71
91
|
readTool,
|
|
72
92
|
readToolHandler,
|
|
73
93
|
shellTool,
|
|
74
|
-
shellToolHandler,
|
|
75
94
|
todoReadTool,
|
|
76
95
|
todoWriteTool,
|
|
77
96
|
} from "./tools.ts";
|
|
@@ -351,19 +370,128 @@ function selectModel(
|
|
|
351
370
|
// Tool wiring
|
|
352
371
|
// ---------------------------------------------------------------------------
|
|
353
372
|
|
|
373
|
+
type ToolRuntimeState = Pick<
|
|
374
|
+
AppState,
|
|
375
|
+
| "agentsMd"
|
|
376
|
+
| "cwd"
|
|
377
|
+
| "db"
|
|
378
|
+
| "delegationDepth"
|
|
379
|
+
| "delegationBudgetRemaining"
|
|
380
|
+
| "effort"
|
|
381
|
+
| "git"
|
|
382
|
+
| "mcpServers"
|
|
383
|
+
| "messages"
|
|
384
|
+
| "providers"
|
|
385
|
+
| "skills"
|
|
386
|
+
> & {
|
|
387
|
+
model: Model<string>;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Run one isolated delegated subtask with the current model and toolset.
|
|
392
|
+
*
|
|
393
|
+
* @param task - Raw delegated subtask prompt.
|
|
394
|
+
* @param state - Runtime state to inherit into the delegated child run.
|
|
395
|
+
* @param context - Delegation context reserved for the child run.
|
|
396
|
+
* @param signal - Optional abort signal from the parent run.
|
|
397
|
+
* @param onUpdate - Optional progressive tool-update callback.
|
|
398
|
+
* @returns The delegated subagent result summary.
|
|
399
|
+
*/
|
|
400
|
+
async function runDelegatedTask(
|
|
401
|
+
task: string,
|
|
402
|
+
state: ToolRuntimeState,
|
|
403
|
+
context: ShellDelegationContext,
|
|
404
|
+
signal?: AbortSignal,
|
|
405
|
+
onUpdate?: ToolUpdateCallback,
|
|
406
|
+
): Promise<DelegateRunResult> {
|
|
407
|
+
const session = createSession(state.db, {
|
|
408
|
+
cwd: state.cwd,
|
|
409
|
+
model: `${state.model.provider}/${state.model.id}`,
|
|
410
|
+
effort: state.effort,
|
|
411
|
+
});
|
|
412
|
+
let finalAssistantMessage: AssistantMessage | null = null;
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const userMessage = {
|
|
416
|
+
role: "user",
|
|
417
|
+
content: task,
|
|
418
|
+
timestamp: Date.now(),
|
|
419
|
+
} satisfies UserMessage;
|
|
420
|
+
const turn = appendMessage(state.db, session.id, userMessage);
|
|
421
|
+
const messages = loadCompactedModelMessages(state.db, session.id);
|
|
422
|
+
const childState: ToolRuntimeState = {
|
|
423
|
+
...state,
|
|
424
|
+
delegationDepth: context.depth,
|
|
425
|
+
delegationBudgetRemaining: context.remainingBudget,
|
|
426
|
+
messages,
|
|
427
|
+
};
|
|
428
|
+
const { tools, toolHandlers } = buildTools(childState);
|
|
429
|
+
|
|
430
|
+
const result = await runAgentLoop({
|
|
431
|
+
db: state.db,
|
|
432
|
+
sessionId: session.id,
|
|
433
|
+
turn,
|
|
434
|
+
model: state.model,
|
|
435
|
+
systemPrompt: buildPrompt(childState),
|
|
436
|
+
tools,
|
|
437
|
+
toolHandlers,
|
|
438
|
+
messages,
|
|
439
|
+
cwd: state.cwd,
|
|
440
|
+
apiKey: state.providers.get(state.model.provider),
|
|
441
|
+
effort: state.effort,
|
|
442
|
+
signal,
|
|
443
|
+
onEvent: (event) => {
|
|
444
|
+
switch (event.type) {
|
|
445
|
+
case "assistant_message": {
|
|
446
|
+
const snippet = extractAssistantActivitySnippet(event.message);
|
|
447
|
+
if (snippet) {
|
|
448
|
+
onUpdate?.({
|
|
449
|
+
content: [
|
|
450
|
+
{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: `Subagent: ${snippet}`,
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
isError: false,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "done":
|
|
461
|
+
case "error":
|
|
462
|
+
case "aborted":
|
|
463
|
+
finalAssistantMessage = event.message;
|
|
464
|
+
break;
|
|
465
|
+
default:
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
stopReason: result.stopReason,
|
|
473
|
+
finalText: extractAssistantText(finalAssistantMessage),
|
|
474
|
+
errorText: extractAssistantErrorText(finalAssistantMessage),
|
|
475
|
+
};
|
|
476
|
+
} finally {
|
|
477
|
+
deleteSession(state.db, session.id);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
354
481
|
/**
|
|
355
482
|
* Build tool definitions and handler map for the current model.
|
|
356
483
|
*
|
|
357
484
|
* Returns the `Tool[]` to send to the model and the handler map
|
|
358
485
|
* for the agent loop to dispatch tool calls.
|
|
359
486
|
*/
|
|
360
|
-
function buildTools(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
487
|
+
function buildTools(state: ToolRuntimeState): {
|
|
488
|
+
tools: Tool[];
|
|
489
|
+
toolHandlers: Map<string, ToolHandler>;
|
|
490
|
+
} {
|
|
491
|
+
const { mcpServers, messages, model } = state;
|
|
365
492
|
const tools: Tool[] = [
|
|
366
493
|
shellTool,
|
|
494
|
+
delegateTool,
|
|
367
495
|
readTool,
|
|
368
496
|
grepTool,
|
|
369
497
|
editTool,
|
|
@@ -371,7 +499,33 @@ function buildTools(
|
|
|
371
499
|
todoReadTool,
|
|
372
500
|
];
|
|
373
501
|
const toolHandlers = new Map<string, ToolHandler>([
|
|
374
|
-
[
|
|
502
|
+
[
|
|
503
|
+
shellTool.name,
|
|
504
|
+
createDelegationAwareShellToolHandler({
|
|
505
|
+
getDelegationContext: () => ({
|
|
506
|
+
depth: state.delegationDepth,
|
|
507
|
+
remainingBudget: state.delegationBudgetRemaining,
|
|
508
|
+
}),
|
|
509
|
+
setDelegationContext: (context) => {
|
|
510
|
+
state.delegationBudgetRemaining = context.remainingBudget;
|
|
511
|
+
},
|
|
512
|
+
}),
|
|
513
|
+
],
|
|
514
|
+
[
|
|
515
|
+
delegateTool.name,
|
|
516
|
+
createDelegateToolHandler({
|
|
517
|
+
getDelegationContext: () => ({
|
|
518
|
+
depth: state.delegationDepth,
|
|
519
|
+
remainingBudget: state.delegationBudgetRemaining,
|
|
520
|
+
}),
|
|
521
|
+
setDelegationContext: (context) => {
|
|
522
|
+
state.delegationBudgetRemaining = context.remainingBudget;
|
|
523
|
+
},
|
|
524
|
+
runSubagent: (task, context, signal, onUpdate) => {
|
|
525
|
+
return runDelegatedTask(task, state, context, signal, onUpdate);
|
|
526
|
+
},
|
|
527
|
+
}),
|
|
528
|
+
],
|
|
375
529
|
[readTool.name, readToolHandler],
|
|
376
530
|
[grepTool.name, grepToolHandler],
|
|
377
531
|
[editTool.name, editToolHandler],
|
|
@@ -527,6 +681,12 @@ export interface AppState {
|
|
|
527
681
|
versionLabel: string;
|
|
528
682
|
/** Current git state (null if not in a repo). */
|
|
529
683
|
git: GitState | null;
|
|
684
|
+
/** Delegated-subagent depth inherited by this app process. */
|
|
685
|
+
delegationDepth: number;
|
|
686
|
+
/** Delegated-subagent budget reset at the start of each top-level agent run. */
|
|
687
|
+
delegationBudgetLimit: number;
|
|
688
|
+
/** Remaining delegated-subagent launches in the active run. */
|
|
689
|
+
delegationBudgetRemaining: number;
|
|
530
690
|
/** Available provider credentials (provider → API key). */
|
|
531
691
|
providers: Map<string, string>;
|
|
532
692
|
/** OAuth credentials on disk. */
|
|
@@ -595,6 +755,7 @@ export async function init(): Promise<AppState> {
|
|
|
595
755
|
}
|
|
596
756
|
|
|
597
757
|
const mcpResult = await discoverMcpServers(effectiveSettings.mcp);
|
|
758
|
+
const delegation = readShellDelegationContext(process.env);
|
|
598
759
|
|
|
599
760
|
const builtInModels = listAvailableModels(providers);
|
|
600
761
|
const availableModels = [...builtInModels, ...customResult.models];
|
|
@@ -622,6 +783,9 @@ export async function init(): Promise<AppState> {
|
|
|
622
783
|
theme: promptContext.theme,
|
|
623
784
|
versionLabel: resolveAppVersionLabel(),
|
|
624
785
|
git: promptContext.git,
|
|
786
|
+
delegationDepth: delegation.depth,
|
|
787
|
+
delegationBudgetLimit: delegation.remainingBudget,
|
|
788
|
+
delegationBudgetRemaining: delegation.remainingBudget,
|
|
625
789
|
providers,
|
|
626
790
|
oauthCredentials,
|
|
627
791
|
settings,
|
|
@@ -663,7 +827,9 @@ function resolvePromptOs(): "linux" | "mac" | "docker" {
|
|
|
663
827
|
* Separated from `init` because turns still rebuild the assembled prompt
|
|
664
828
|
* from the session-stable prompt context plus the current runtime state.
|
|
665
829
|
*/
|
|
666
|
-
export function buildPrompt(
|
|
830
|
+
export function buildPrompt(
|
|
831
|
+
state: Pick<AppState, "cwd" | "model" | "git" | "agentsMd" | "skills">,
|
|
832
|
+
): string {
|
|
667
833
|
return buildSystemPrompt({
|
|
668
834
|
cwd: state.cwd,
|
|
669
835
|
modelLabel: state.model
|
|
@@ -683,8 +849,22 @@ export function buildToolList(state: AppState): {
|
|
|
683
849
|
tools: Tool[];
|
|
684
850
|
toolHandlers: Map<string, ToolHandler>;
|
|
685
851
|
} {
|
|
686
|
-
|
|
687
|
-
|
|
852
|
+
const { model } = state;
|
|
853
|
+
if (!model) return { tools: [], toolHandlers: new Map() };
|
|
854
|
+
return buildTools({
|
|
855
|
+
agentsMd: state.agentsMd,
|
|
856
|
+
cwd: state.cwd,
|
|
857
|
+
db: state.db,
|
|
858
|
+
delegationDepth: state.delegationDepth,
|
|
859
|
+
delegationBudgetRemaining: state.delegationBudgetRemaining,
|
|
860
|
+
effort: state.effort,
|
|
861
|
+
git: state.git,
|
|
862
|
+
mcpServers: state.mcpServers,
|
|
863
|
+
messages: state.messages,
|
|
864
|
+
model,
|
|
865
|
+
providers: state.providers,
|
|
866
|
+
skills: state.skills,
|
|
867
|
+
});
|
|
688
868
|
}
|
|
689
869
|
|
|
690
870
|
/**
|