one-code-extension 0.1.0
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/LICENSE +32 -0
- package/README.md +202 -0
- package/agents/explore.md +24 -0
- package/agents/general-purpose.md +21 -0
- package/agents/plan.md +22 -0
- package/docs/decisions/auto-mode.md +526 -0
- package/docs/decisions/branding.md +127 -0
- package/docs/decisions/code-review-remediation.md +106 -0
- package/docs/decisions/compaction.md +140 -0
- package/docs/decisions/distribution.md +133 -0
- package/docs/decisions/hooks.md +64 -0
- package/docs/decisions/lsp.md +58 -0
- package/docs/decisions/mcp.md +104 -0
- package/docs/decisions/memory-state.md +170 -0
- package/docs/decisions/model-policy.md +126 -0
- package/docs/decisions/modes.md +178 -0
- package/docs/decisions/skills-plugins.md +56 -0
- package/docs/decisions/subagents-workflows.md +225 -0
- package/docs/decisions/system-prompt.md +98 -0
- package/docs/decisions/tools.md +407 -0
- package/docs/decisions/tui.md +202 -0
- package/docs/decisions.md +148 -0
- package/extensions/ask-user/index.ts +124 -0
- package/extensions/ask-user/questions.ts +60 -0
- package/extensions/auto-mode/actions.ts +78 -0
- package/extensions/auto-mode/classifier.ts +388 -0
- package/extensions/auto-mode/config.ts +237 -0
- package/extensions/auto-mode/decision-log.ts +60 -0
- package/extensions/auto-mode/defaults.ts +65 -0
- package/extensions/auto-mode/instructions.ts +74 -0
- package/extensions/auto-mode/model-picker.ts +225 -0
- package/extensions/auto-mode/model-select.ts +178 -0
- package/extensions/auto-mode/paths.ts +109 -0
- package/extensions/auto-mode/pause.ts +96 -0
- package/extensions/auto-mode/prompt.ts +307 -0
- package/extensions/auto-mode/rules.ts +96 -0
- package/extensions/auto-mode/safety-floor.ts +100 -0
- package/extensions/auto-mode/sensitive.ts +137 -0
- package/extensions/auto-mode/shell-analysis.ts +702 -0
- package/extensions/background/index.ts +405 -0
- package/extensions/background/registry.ts +81 -0
- package/extensions/background/wakeup.ts +41 -0
- package/extensions/bash/background.ts +136 -0
- package/extensions/bash/index.ts +149 -0
- package/extensions/branding/assistant-marker.ts +53 -0
- package/extensions/branding/index.ts +366 -0
- package/extensions/branding/prompt-editor.ts +42 -0
- package/extensions/branding/prompt-marker.ts +29 -0
- package/extensions/branding/startup.ts +152 -0
- package/extensions/claude-compat/index.ts +50 -0
- package/extensions/claude-context/index.ts +79 -0
- package/extensions/compaction/index.ts +231 -0
- package/extensions/compaction/prompt.ts +191 -0
- package/extensions/context-management/index.ts +147 -0
- package/extensions/effort/index.ts +216 -0
- package/extensions/effort/slider.ts +253 -0
- package/extensions/exit/index.ts +28 -0
- package/extensions/file-tracker/index.ts +103 -0
- package/extensions/file-tracker/tracker.ts +145 -0
- package/extensions/hooks/debug.ts +53 -0
- package/extensions/hooks/executor.ts +132 -0
- package/extensions/hooks/index.ts +310 -0
- package/extensions/hooks/matcher.ts +72 -0
- package/extensions/hooks/plugin-hooks.ts +79 -0
- package/extensions/hooks/protocol.ts +151 -0
- package/extensions/hooks/settings.ts +163 -0
- package/extensions/hooks/trust.ts +151 -0
- package/extensions/lib/branch-restore.ts +35 -0
- package/extensions/lib/claude-context.ts +148 -0
- package/extensions/lib/deferred.ts +119 -0
- package/extensions/lib/git.ts +14 -0
- package/extensions/lib/memory.ts +219 -0
- package/extensions/lib/model-policy.ts +436 -0
- package/extensions/lib/model-tier.ts +99 -0
- package/extensions/lib/notifications.ts +18 -0
- package/extensions/lib/paths.ts +26 -0
- package/extensions/lib/per-cwd.ts +21 -0
- package/extensions/lib/persisted-output.ts +70 -0
- package/extensions/lib/pi-version.ts +54 -0
- package/extensions/lib/plugins.ts +227 -0
- package/extensions/lib/reminders.ts +212 -0
- package/extensions/lib/scratchpad.ts +68 -0
- package/extensions/lib/tui-render.ts +522 -0
- package/extensions/lsp/client.ts +278 -0
- package/extensions/lsp/format.ts +46 -0
- package/extensions/lsp/index.ts +193 -0
- package/extensions/lsp/protocol.ts +64 -0
- package/extensions/lsp/servers.ts +79 -0
- package/extensions/mcp/client.ts +205 -0
- package/extensions/mcp/config.ts +199 -0
- package/extensions/mcp/index.ts +373 -0
- package/extensions/mcp/schema.ts +161 -0
- package/extensions/memory/index.ts +90 -0
- package/extensions/notebook/index.ts +77 -0
- package/extensions/notebook/notebook.ts +108 -0
- package/extensions/permissions/index.ts +876 -0
- package/extensions/permissions/matcher.ts +455 -0
- package/extensions/permissions/modes.ts +107 -0
- package/extensions/permissions/protected-paths.ts +125 -0
- package/extensions/permissions/settings.ts +98 -0
- package/extensions/plan-mode/index.ts +209 -0
- package/extensions/plan-mode/reminder.ts +33 -0
- package/extensions/plan-mode/slug.ts +28 -0
- package/extensions/plan-mode/viewer.ts +124 -0
- package/extensions/plugins/index.ts +115 -0
- package/extensions/plugins/template.ts +42 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +24 -0
- package/extensions/skill/index.ts +170 -0
- package/extensions/subagents/agents.ts +126 -0
- package/extensions/subagents/child.ts +414 -0
- package/extensions/subagents/default-model.ts +122 -0
- package/extensions/subagents/index.ts +1119 -0
- package/extensions/subagents/model-select.ts +346 -0
- package/extensions/subagents/rpc-turns.ts +123 -0
- package/extensions/subagents/runs.ts +93 -0
- package/extensions/subagents/usage.ts +53 -0
- package/extensions/subagents/worktree.ts +69 -0
- package/extensions/system-prompt/environment.ts +35 -0
- package/extensions/system-prompt/index.ts +54 -0
- package/extensions/system-prompt/template.ts +103 -0
- package/extensions/system-prompt/tiers/common.ts +63 -0
- package/extensions/system-prompt/tiers/frontier.ts +24 -0
- package/extensions/system-prompt/tiers/low.ts +71 -0
- package/extensions/system-prompt/tiers/mid.ts +56 -0
- package/extensions/system-reminder/index.ts +37 -0
- package/extensions/tasks/index.ts +140 -0
- package/extensions/tasks/store.ts +174 -0
- package/extensions/todo/index.ts +133 -0
- package/extensions/tool-search/index.ts +168 -0
- package/extensions/tool-style/index.ts +84 -0
- package/extensions/web/index.ts +43 -0
- package/extensions/web-fetch/extract.ts +139 -0
- package/extensions/web-fetch/index.ts +291 -0
- package/extensions/web-fetch/summarize.ts +78 -0
- package/extensions/workflow/agent-session.ts +368 -0
- package/extensions/workflow/globals.ts +251 -0
- package/extensions/workflow/index.ts +364 -0
- package/extensions/workflow/journal.ts +75 -0
- package/extensions/workflow/permission-gate.ts +71 -0
- package/extensions/workflow/run-manager.ts +306 -0
- package/extensions/workflow/saved-workflows.ts +45 -0
- package/extensions/workflow/script-source.ts +201 -0
- package/extensions/workflow/types.ts +99 -0
- package/extensions/workflow/vm-runtime.ts +31 -0
- package/extensions/workflow/widget.ts +56 -0
- package/extensions/worktree/index.ts +266 -0
- package/extensions/worktree/rewrite.ts +59 -0
- package/package.json +101 -0
- package/themes/one-code-light.json +76 -0
- package/themes/one-code.json +76 -0
- package/types/app.d.ts +5 -0
- package/types/community-packages.d.ts +12 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process subagent runner: spawns `pi --mode json -p`, parses its JSONL
|
|
3
|
+
* event stream, and exposes a handle usable both foreground (await result)
|
|
4
|
+
* and background (kill/snapshot, registered as a background task).
|
|
5
|
+
*
|
|
6
|
+
* Children persist their sessions (`--session-dir` per run) so a finished
|
|
7
|
+
* agent can be resumed later via `--session <file>` — that is what
|
|
8
|
+
* send_message builds on. `--no-session` is only the fallback when the parent
|
|
9
|
+
* itself has no session directory.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import { basename, join } from "node:path";
|
|
16
|
+
import { type ChildAction, recordAction } from "../auto-mode/actions.ts";
|
|
17
|
+
import type { AgentDefinition } from "./agents.ts";
|
|
18
|
+
import { RpcTurnTracker, toMainMessage } from "./rpc-turns.ts";
|
|
19
|
+
import { addUsage, emptyUsage, type UsageTotals } from "./usage.ts";
|
|
20
|
+
|
|
21
|
+
export const OUTPUT_CAP = 50_000;
|
|
22
|
+
|
|
23
|
+
export function piInvocation(args: string[]): { command: string; args: string[] } {
|
|
24
|
+
const currentScript = process.argv[1];
|
|
25
|
+
if (currentScript && !currentScript.startsWith("/$bunfs/root/") && existsSync(currentScript)) {
|
|
26
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
27
|
+
}
|
|
28
|
+
const execName = basename(process.execPath).toLowerCase();
|
|
29
|
+
if (!/^(node|bun)(\.exe)?$/.test(execName)) {
|
|
30
|
+
return { command: process.execPath, args };
|
|
31
|
+
}
|
|
32
|
+
return { command: "pi", args };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Framing wrapped around a fork child's task. A fork inherits the parent's
|
|
37
|
+
* entire transcript; without this, a weaker model tends to abandon its
|
|
38
|
+
* assigned task and continue (or confabulate about) the inherited topic —
|
|
39
|
+
* then its output returns to the parent looking like independent
|
|
40
|
+
* confirmation. See docs/handoff-tool-ambiguity.md (fork confabulation).
|
|
41
|
+
*/
|
|
42
|
+
export function forkTaskMessage(task: string): string {
|
|
43
|
+
return [
|
|
44
|
+
"You are a forked subagent. The conversation above is inherited context, for reference only — do NOT continue its open threads, verify its claims, or act on its plans. Do ONLY the task below; your final message is returned to the parent conversation verbatim, as data. You cannot see the parent's background tasks: its task ids are not addressable from here.",
|
|
45
|
+
"",
|
|
46
|
+
"Task:",
|
|
47
|
+
task,
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ChildOptions {
|
|
52
|
+
agent?: AgentDefinition;
|
|
53
|
+
task: string;
|
|
54
|
+
cwd: string;
|
|
55
|
+
/** Parent session file, for fork runs. */
|
|
56
|
+
forkFrom?: string;
|
|
57
|
+
/** Existing child session to continue (send_message). */
|
|
58
|
+
sessionFile?: string;
|
|
59
|
+
/** Where a new run's session file lands; falls back to --no-session. */
|
|
60
|
+
sessionDir?: string;
|
|
61
|
+
model?: string;
|
|
62
|
+
thinking?: string;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
onProgress: (toolCalls: number, lastText: string, usage: UsageTotals) => void;
|
|
65
|
+
/** The child called send_message {to: "main"} — relay it to the main conversation. */
|
|
66
|
+
onMessageToMain?: (message: string, summary?: string) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Session/agent/model flags shared by print and rpc children. */
|
|
70
|
+
function buildChildFlags(options: Omit<ChildOptions, "task" | "signal" | "onProgress">): string[] {
|
|
71
|
+
const { agent, forkFrom, sessionFile, sessionDir } = options;
|
|
72
|
+
const args: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (sessionFile) {
|
|
75
|
+
args.push("--session", sessionFile);
|
|
76
|
+
} else if (forkFrom) {
|
|
77
|
+
// pi forks the session file into a fresh session, so the child starts with
|
|
78
|
+
// the parent's transcript and our own system prompt still applies.
|
|
79
|
+
args.push("--fork", forkFrom);
|
|
80
|
+
if (sessionDir) args.push("--session-dir", sessionDir);
|
|
81
|
+
} else {
|
|
82
|
+
if (sessionDir) args.push("--session-dir", sessionDir);
|
|
83
|
+
else args.push("--no-session");
|
|
84
|
+
if (agent) {
|
|
85
|
+
const tmpDir = mkdtempSync(join(os.tmpdir(), "cc-subagent-"));
|
|
86
|
+
const promptPath = join(tmpDir, "system.md");
|
|
87
|
+
writeFileSync(promptPath, agent.systemPrompt);
|
|
88
|
+
args.push("--system-prompt", promptPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const model = options.model ?? agent?.model;
|
|
93
|
+
if (model) args.push("--model", model);
|
|
94
|
+
if (options.thinking) args.push("--thinking", options.thinking);
|
|
95
|
+
if (agent?.tools && !forkFrom && !sessionFile) args.push("--tools", agent.tools.join(","));
|
|
96
|
+
return args;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ChildOutcome {
|
|
100
|
+
output: string;
|
|
101
|
+
toolCalls: number;
|
|
102
|
+
usage: UsageTotals;
|
|
103
|
+
failed?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* What the child actually did, names and short subjects only. Auto mode
|
|
106
|
+
* reviews this when the child returns, to catch a sequence whose individual
|
|
107
|
+
* steps each passed. Never carries tool output — see auto-mode/actions.ts.
|
|
108
|
+
*/
|
|
109
|
+
actions: ChildAction[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A provider failure in a child (auth, billing, rate limit) surfaces ONLY as
|
|
114
|
+
* an assistant message with stopReason "error" in the event stream — stderr
|
|
115
|
+
* stays empty and the process exits normally. Without capturing it, the
|
|
116
|
+
* parent gets "Subagent produced no output (exit code 0)", the exact
|
|
117
|
+
* ambiguous result a weak model confabulates a cause for. The error is
|
|
118
|
+
* cleared by any later successful assistant message, so pi's internal
|
|
119
|
+
* retries don't leave a stale failure.
|
|
120
|
+
*/
|
|
121
|
+
export function providerErrorFrom(message: { stopReason?: string; errorMessage?: string } | undefined): string | undefined {
|
|
122
|
+
if (!message) return undefined;
|
|
123
|
+
if (message.stopReason !== "error") return undefined;
|
|
124
|
+
return message.errorMessage || "unknown provider error";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The outcome a finished child resolves to, given everything its stream said. */
|
|
128
|
+
export function closeOutcome(args: {
|
|
129
|
+
finalText: string;
|
|
130
|
+
providerError?: string;
|
|
131
|
+
code: number | null;
|
|
132
|
+
closeSignal: NodeJS.Signals | null;
|
|
133
|
+
stderr: string;
|
|
134
|
+
toolCalls: number;
|
|
135
|
+
usage: UsageTotals;
|
|
136
|
+
actions: ChildAction[];
|
|
137
|
+
}): ChildOutcome {
|
|
138
|
+
const { finalText, providerError, code, closeSignal, stderr, toolCalls, usage, actions } = args;
|
|
139
|
+
const output = finalText.slice(0, OUTPUT_CAP);
|
|
140
|
+
if (providerError) {
|
|
141
|
+
return {
|
|
142
|
+
output: output.trim()
|
|
143
|
+
? `${output}\n\n[The subagent's last request ended with a provider error: ${providerError}]`
|
|
144
|
+
: `Subagent failed with a provider error (its model could not be called — an auth/billing/rate-limit problem, not a task failure): ${providerError}`,
|
|
145
|
+
toolCalls,
|
|
146
|
+
usage,
|
|
147
|
+
actions,
|
|
148
|
+
failed: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (output.trim()) {
|
|
152
|
+
return { output, toolCalls, usage, actions };
|
|
153
|
+
}
|
|
154
|
+
const why = closeSignal ? `terminated by ${closeSignal}` : `exit code ${code}`;
|
|
155
|
+
const detail = stderr.trim().split("\n").slice(-5).join("\n");
|
|
156
|
+
return {
|
|
157
|
+
output: `Subagent produced no output (${why}).${detail ? `\n${detail}` : ""}`,
|
|
158
|
+
toolCalls,
|
|
159
|
+
usage,
|
|
160
|
+
actions,
|
|
161
|
+
failed: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ChildHandle {
|
|
166
|
+
result: Promise<ChildOutcome>;
|
|
167
|
+
kill(): void;
|
|
168
|
+
snapshot(): { toolCalls: number; text: string; usage: UsageTotals };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function startChild(options: ChildOptions): ChildHandle {
|
|
172
|
+
const { cwd, signal, onProgress } = options;
|
|
173
|
+
const task = options.forkFrom ? forkTaskMessage(options.task) : options.task;
|
|
174
|
+
const args = ["--mode", "json", "-p", ...buildChildFlags(options), task];
|
|
175
|
+
|
|
176
|
+
const invocation = piInvocation(args);
|
|
177
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
178
|
+
cwd,
|
|
179
|
+
shell: false,
|
|
180
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
181
|
+
env: { ...process.env, PI_SUBAGENT_CHILD: "1" },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
let buffer = "";
|
|
185
|
+
let finalText = "";
|
|
186
|
+
let toolCalls = 0;
|
|
187
|
+
const actions: ChildAction[] = [];
|
|
188
|
+
let stderr = "";
|
|
189
|
+
let providerError: string | undefined;
|
|
190
|
+
const usage = emptyUsage();
|
|
191
|
+
|
|
192
|
+
const onAbort = () => child.kill("SIGTERM");
|
|
193
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
194
|
+
|
|
195
|
+
const processLine = (line: string) => {
|
|
196
|
+
if (!line.trim()) return;
|
|
197
|
+
let event: {
|
|
198
|
+
type?: string;
|
|
199
|
+
message?: { role?: string; content?: unknown; usage?: unknown; stopReason?: string; errorMessage?: string };
|
|
200
|
+
};
|
|
201
|
+
try {
|
|
202
|
+
event = JSON.parse(line);
|
|
203
|
+
} catch {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (event.type === "tool_execution_end") {
|
|
207
|
+
const toMain = toMainMessage(event);
|
|
208
|
+
if (toMain) options.onMessageToMain?.(toMain.message, toMain.summary);
|
|
209
|
+
} else if (event.type === "tool_execution_start") {
|
|
210
|
+
toolCalls++;
|
|
211
|
+
const start = event as { toolName?: string; args?: unknown };
|
|
212
|
+
if (start.toolName) recordAction(actions, start.toolName, start.args);
|
|
213
|
+
onProgress(toolCalls, finalText, usage);
|
|
214
|
+
} else if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
215
|
+
addUsage(usage, event.message.usage);
|
|
216
|
+
providerError = providerErrorFrom(event.message);
|
|
217
|
+
const blocks = Array.isArray(event.message.content) ? event.message.content : [];
|
|
218
|
+
const text = blocks
|
|
219
|
+
.filter((b): b is { type: string; text: string } => (b as { type?: string }).type === "text")
|
|
220
|
+
.map((b) => b.text)
|
|
221
|
+
.join("");
|
|
222
|
+
if (text.trim()) finalText = text;
|
|
223
|
+
onProgress(toolCalls, finalText, usage);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const result = new Promise<ChildOutcome>((resolve) => {
|
|
228
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
229
|
+
buffer += chunk.toString();
|
|
230
|
+
let idx: number;
|
|
231
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
232
|
+
processLine(buffer.slice(0, idx));
|
|
233
|
+
buffer = buffer.slice(idx + 1);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
237
|
+
stderr += chunk.toString();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
child.on("error", (error) => {
|
|
241
|
+
signal?.removeEventListener("abort", onAbort);
|
|
242
|
+
resolve({ output: `Failed to start subagent: ${error.message}`, toolCalls, usage, actions, failed: true });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
child.on("close", (code, closeSignal) => {
|
|
246
|
+
signal?.removeEventListener("abort", onAbort);
|
|
247
|
+
if (buffer.trim()) processLine(buffer);
|
|
248
|
+
resolve(closeOutcome({ finalText, providerError, code, closeSignal, stderr, toolCalls, usage, actions }));
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
result,
|
|
254
|
+
kill: () => child.kill("SIGTERM"),
|
|
255
|
+
snapshot: () => ({ toolCalls, text: finalText, usage }),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface RpcChildOptions extends Omit<ChildOptions, "task" | "signal" | "onProgress"> {
|
|
260
|
+
onProgress: (toolCalls: number, lastText: string, usage: UsageTotals) => void;
|
|
261
|
+
/** Fires at the end of EVERY turn (initial task and later messages alike). */
|
|
262
|
+
onTurnEnd: (outcome: ChildOutcome) => void;
|
|
263
|
+
onExit?: () => void;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface RpcChildHandle {
|
|
267
|
+
/**
|
|
268
|
+
* Deliver a message. "started" = the child was idle and this began a new
|
|
269
|
+
* turn (onTurnEnd will fire for it); "steered" = the child was mid-turn and
|
|
270
|
+
* the message joined it (covered by that turn's onTurnEnd).
|
|
271
|
+
*/
|
|
272
|
+
send(message: string): "started" | "steered";
|
|
273
|
+
busy(): boolean;
|
|
274
|
+
exited(): boolean;
|
|
275
|
+
kill(): void;
|
|
276
|
+
snapshot(): { toolCalls: number; text: string; usage: UsageTotals };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Resident child over `pi --mode rpc`: same event stream as json mode, plus a
|
|
281
|
+
* stdin command channel — which is what makes messaging a *running* agent
|
|
282
|
+
* possible (`streamingBehavior: "steer"` queues into the current turn; when
|
|
283
|
+
* idle the same command starts a new turn, so there is no idle/busy race).
|
|
284
|
+
* RPC children have hasUI=true, so extension_ui_request dialogs must be
|
|
285
|
+
* answered: every one is cancelled, which reproduces print-mode's
|
|
286
|
+
* non-interactive fallback (gated tools deny instead of deadlocking).
|
|
287
|
+
*/
|
|
288
|
+
export function startRpcChild(options: RpcChildOptions): RpcChildHandle {
|
|
289
|
+
const args = ["--mode", "rpc", ...buildChildFlags(options)];
|
|
290
|
+
const invocation = piInvocation(args);
|
|
291
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
292
|
+
cwd: options.cwd,
|
|
293
|
+
shell: false,
|
|
294
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
295
|
+
env: { ...process.env, PI_SUBAGENT_CHILD: "1" },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const tracker = new RpcTurnTracker();
|
|
299
|
+
let buffer = "";
|
|
300
|
+
let stderr = "";
|
|
301
|
+
let exited = false;
|
|
302
|
+
/**
|
|
303
|
+
* Messages accepted while a prompt was sent but the child's agent loop had
|
|
304
|
+
* not started yet (it boots for a few seconds) — a steer written in that
|
|
305
|
+
* window would run as its own turn instead of joining the pending one.
|
|
306
|
+
* Flushed as steers the moment the child reports agent_start.
|
|
307
|
+
*/
|
|
308
|
+
let earlySteers: string[] = [];
|
|
309
|
+
|
|
310
|
+
const send = (command: Record<string, unknown>) => {
|
|
311
|
+
try {
|
|
312
|
+
child.stdin.write(`${JSON.stringify(command)}\n`);
|
|
313
|
+
} catch {
|
|
314
|
+
// A dead pipe surfaces via the close handler.
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
319
|
+
buffer += chunk.toString();
|
|
320
|
+
let idx: number;
|
|
321
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
322
|
+
const line = buffer.slice(0, idx);
|
|
323
|
+
buffer = buffer.slice(idx + 1);
|
|
324
|
+
const action = tracker.process(line);
|
|
325
|
+
if (!action) continue;
|
|
326
|
+
if (action.kind === "ui_request") {
|
|
327
|
+
send({ type: "extension_ui_response", id: action.id, cancelled: true });
|
|
328
|
+
} else if (action.kind === "stream_start") {
|
|
329
|
+
for (const message of earlySteers) {
|
|
330
|
+
send({ type: "prompt", message, streamingBehavior: "steer" });
|
|
331
|
+
}
|
|
332
|
+
earlySteers = [];
|
|
333
|
+
} else if (action.kind === "progress") {
|
|
334
|
+
options.onProgress(tracker.toolCalls, tracker.turnText, tracker.usage);
|
|
335
|
+
} else if (action.kind === "message_to_main") {
|
|
336
|
+
options.onMessageToMain?.(action.message, action.summary);
|
|
337
|
+
} else if (action.kind === "turn_end") {
|
|
338
|
+
options.onTurnEnd(turnOutcome());
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
343
|
+
stderr += chunk.toString();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const turnOutcome = (): ChildOutcome => {
|
|
347
|
+
const output = tracker.turnText.slice(0, OUTPUT_CAP);
|
|
348
|
+
if (tracker.providerError) {
|
|
349
|
+
return {
|
|
350
|
+
output: output.trim()
|
|
351
|
+
? `${output}\n\n[The subagent's last request ended with a provider error: ${tracker.providerError}]`
|
|
352
|
+
: `Subagent failed with a provider error (its model could not be called — an auth/billing/rate-limit problem, not a task failure): ${tracker.providerError}`,
|
|
353
|
+
toolCalls: tracker.toolCalls,
|
|
354
|
+
usage: tracker.usage,
|
|
355
|
+
actions: tracker.actions,
|
|
356
|
+
failed: true,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (output.trim()) return { output, toolCalls: tracker.toolCalls, usage: tracker.usage, actions: tracker.actions };
|
|
360
|
+
const detail = stderr.trim().split("\n").slice(-5).join("\n");
|
|
361
|
+
return {
|
|
362
|
+
output: `Subagent produced no output.${detail ? `\n${detail}` : ""}`,
|
|
363
|
+
toolCalls: tracker.toolCalls,
|
|
364
|
+
usage: tracker.usage,
|
|
365
|
+
actions: tracker.actions,
|
|
366
|
+
failed: true,
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const onClose = () => {
|
|
371
|
+
if (exited) return;
|
|
372
|
+
exited = true;
|
|
373
|
+
if (tracker.busy) {
|
|
374
|
+
tracker.busy = false;
|
|
375
|
+
const detail = stderr.trim().split("\n").slice(-5).join("\n");
|
|
376
|
+
const provider = tracker.providerError ? `\nLast provider error: ${tracker.providerError}` : "";
|
|
377
|
+
options.onTurnEnd({
|
|
378
|
+
output: `Subagent exited mid-turn.${provider}${detail ? `\n${detail}` : ""}`,
|
|
379
|
+
toolCalls: tracker.toolCalls,
|
|
380
|
+
usage: tracker.usage,
|
|
381
|
+
actions: tracker.actions,
|
|
382
|
+
failed: true,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
options.onExit?.();
|
|
386
|
+
};
|
|
387
|
+
child.on("close", onClose);
|
|
388
|
+
child.on("error", onClose);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
send(message: string): "started" | "steered" {
|
|
392
|
+
if (exited) throw new Error("subagent process has exited");
|
|
393
|
+
if (!tracker.busy) {
|
|
394
|
+
tracker.beginTurn();
|
|
395
|
+
send({ type: "prompt", message, streamingBehavior: "steer" });
|
|
396
|
+
return "started";
|
|
397
|
+
}
|
|
398
|
+
if (tracker.streaming) {
|
|
399
|
+
send({ type: "prompt", message, streamingBehavior: "steer" });
|
|
400
|
+
} else {
|
|
401
|
+
earlySteers.push(message);
|
|
402
|
+
}
|
|
403
|
+
return "steered";
|
|
404
|
+
},
|
|
405
|
+
busy: () => tracker.busy,
|
|
406
|
+
exited: () => exited,
|
|
407
|
+
kill: () => child.kill("SIGTERM"),
|
|
408
|
+
snapshot: () => ({
|
|
409
|
+
toolCalls: tracker.toolCalls,
|
|
410
|
+
text: tracker.busy && tracker.turnText ? `${tracker.transcript ? `${tracker.transcript}\n\n---\n\n` : ""}${tracker.turnText}` : tracker.transcript,
|
|
411
|
+
usage: tracker.usage,
|
|
412
|
+
}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The configured default subagent model (pure fs).
|
|
3
|
+
*
|
|
4
|
+
* Two knobs, Claude Code's first:
|
|
5
|
+
*
|
|
6
|
+
* - `CLAUDE_CODE_SUBAGENT_MODEL` — Claude Code's env var. Honored from the real
|
|
7
|
+
* environment, and from the `env` block of *user and managed* settings files
|
|
8
|
+
* (Claude Code applies settings `env` to the process; One Code does not, so this
|
|
9
|
+
* reads the one variable it needs). Project-scope settings are deliberately
|
|
10
|
+
* not read: both project files live in the repository, and a checked-in `env`
|
|
11
|
+
* block must not get to pick which provider receives subagent tasks — the
|
|
12
|
+
* same containment rule as `autoMode` config.
|
|
13
|
+
* - `subagentModel` — One Code's own top-level setting, same shape as
|
|
14
|
+
* `autoMode.classifierModel`. Wins over the env var when both are set,
|
|
15
|
+
* because it is the more specific statement of intent.
|
|
16
|
+
*
|
|
17
|
+
* The env var additionally applies only to Claude-family sessions (see
|
|
18
|
+
* `applicableSubagentDefault`): it is Claude Code's knob, and its typical
|
|
19
|
+
* values ("sonnet") were written for Anthropic models. On any other provider
|
|
20
|
+
* it is ignored; One Code's automatic same-provider role profile then chooses the
|
|
21
|
+
* default unless the user overrides it with `subagentModel`.
|
|
22
|
+
*
|
|
23
|
+
* The value is a spec, not a model: "sonnet", "inherit", or "provider/id" all
|
|
24
|
+
* pass through to resolveSubagentModel, which owns the semantics.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { autoModeSettingsPaths } from "../auto-mode/config.ts";
|
|
30
|
+
|
|
31
|
+
export interface SubagentDefault {
|
|
32
|
+
spec: string;
|
|
33
|
+
/** Which knob set it, for notices. */
|
|
34
|
+
source: "subagentModel setting" | "CLAUDE_CODE_SUBAGENT_MODEL";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SettingsFile {
|
|
38
|
+
subagentModel?: unknown;
|
|
39
|
+
env?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readSettings(path: string): SettingsFile | undefined {
|
|
43
|
+
if (!existsSync(path)) return undefined;
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(path, "utf-8")) as SettingsFile;
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function loadSubagentDefault(home: string, env: NodeJS.ProcessEnv = process.env): SubagentDefault | undefined {
|
|
52
|
+
let setting: string | undefined;
|
|
53
|
+
let settingsEnv: string | undefined;
|
|
54
|
+
|
|
55
|
+
// Lowest precedence first, so managed settings override user settings.
|
|
56
|
+
for (const path of autoModeSettingsPaths(home)) {
|
|
57
|
+
const file = readSettings(path);
|
|
58
|
+
if (!file) continue;
|
|
59
|
+
if (typeof file.subagentModel === "string" && file.subagentModel.trim()) {
|
|
60
|
+
setting = file.subagentModel.trim();
|
|
61
|
+
}
|
|
62
|
+
const fromEnvBlock = file.env?.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
63
|
+
if (typeof fromEnvBlock === "string" && fromEnvBlock.trim()) {
|
|
64
|
+
settingsEnv = fromEnvBlock.trim();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (setting) return { spec: setting, source: "subagentModel setting" };
|
|
69
|
+
|
|
70
|
+
const fromEnv = env.CLAUDE_CODE_SUBAGENT_MODEL?.trim();
|
|
71
|
+
if (fromEnv) return { spec: fromEnv, source: "CLAUDE_CODE_SUBAGENT_MODEL" };
|
|
72
|
+
if (settingsEnv) return { spec: settingsEnv, source: "CLAUDE_CODE_SUBAGENT_MODEL" };
|
|
73
|
+
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The configured default that actually applies to this session.
|
|
79
|
+
*
|
|
80
|
+
* `subagentModel` is One Code's own setting — the user told *this* harness what
|
|
81
|
+
* subagents run on, so it applies on any provider. `CLAUDE_CODE_SUBAGENT_MODEL`
|
|
82
|
+
* was written for Claude Code, almost always as an Anthropic alias ("sonnet"),
|
|
83
|
+
* so it applies only when the session is already on a Claude model — borrowing
|
|
84
|
+
* it elsewhere silently moved subagent work off the provider the user chose
|
|
85
|
+
* for this session (observed live: an opencode deepseek session's subagents
|
|
86
|
+
* landing on claude-sonnet-5 because a settings `env` block said "sonnet").
|
|
87
|
+
*/
|
|
88
|
+
export function applicableSubagentDefault(
|
|
89
|
+
configured: SubagentDefault | undefined,
|
|
90
|
+
sessionModel: { provider: string; id: string } | undefined,
|
|
91
|
+
): SubagentDefault | undefined {
|
|
92
|
+
if (!configured || configured.source === "subagentModel setting") return configured;
|
|
93
|
+
if (!sessionModel) return configured;
|
|
94
|
+
const claudeFamily = sessionModel.provider === "anthropic" || /claude/i.test(sessionModel.id);
|
|
95
|
+
return claudeFamily ? configured : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Persist `subagentModel` in the *user* settings file, preserving every other
|
|
100
|
+
* key. `undefined` removes the setting. Always user scope, like the loader:
|
|
101
|
+
* the default may move subagent work to another provider, so it lives where
|
|
102
|
+
* only the user writes — never in a project file (see the module comment).
|
|
103
|
+
*
|
|
104
|
+
* Unlike loading, this throws on a malformed file: a lenient read merely
|
|
105
|
+
* skips a setting, but a lenient write would replace the user's whole
|
|
106
|
+
* settings file with only ours.
|
|
107
|
+
*/
|
|
108
|
+
export function persistSubagentModel(spec: string | undefined, home: string): void {
|
|
109
|
+
const path = join(home, ".claude", "settings.json");
|
|
110
|
+
let file: Record<string, unknown> = {};
|
|
111
|
+
if (existsSync(path)) {
|
|
112
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
113
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
114
|
+
throw new Error(`${path}: root must be a JSON object`);
|
|
115
|
+
}
|
|
116
|
+
file = parsed as Record<string, unknown>;
|
|
117
|
+
}
|
|
118
|
+
if (spec === undefined) delete file.subagentModel;
|
|
119
|
+
else file.subagentModel = spec;
|
|
120
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
121
|
+
writeFileSync(path, `${JSON.stringify(file, null, 2)}\n`);
|
|
122
|
+
}
|