feat: sync upstream pi-mono (386 commits)

从上游 badlogic/pi-mono 同步 386 个提交,手动解决所有冲突:

保留 SproutClaw 独有功能:
- RPC: reload 命令、bash 流式输出、get_extensions 命令
- settings-manager: showChangelogOnStartup 设置
- agent-session: turnIndex getter
- 移除 pi 版本检查通知(interactive-mode)
- skills 系统、启动脚本、自定义工具脚本

直接采用上游版本:
- packages/ai 模型列表及测试文件
- packages/coding-agent 文档
- 其余未冲突的全部上游代码

升级 @mistralai/mistralai 至 2.2.6(修复 promptCacheKey 类型错误)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 20:18:02 +08:00
409 changed files with 27402 additions and 6514 deletions

View File

@@ -59,6 +59,7 @@ export class RpcClient {
new Map();
private requestId = 0;
private stderr = "";
private exitError: Error | null = null;
private options: RpcClientOptions;
constructor(options: RpcClientOptions = {}) {
@@ -73,6 +74,8 @@ export class RpcClient {
throw new Error("Client already started");
}
this.exitError = null;
const cliPath = this.options.cliPath ?? "dist/cli.js";
const args = ["--mode", "rpc"];
@@ -86,20 +89,41 @@ export class RpcClient {
args.push(...this.options.args);
}
this.process = spawn("node", [cliPath, ...args], {
const childProcess = spawn("node", [cliPath, ...args], {
cwd: this.options.cwd,
env: { ...process.env, ...this.options.env },
stdio: ["pipe", "pipe", "pipe"],
});
this.process = childProcess;
// Collect stderr for debugging
this.process.stderr?.on("data", (data) => {
childProcess.stderr?.on("data", (data) => {
this.stderr += data.toString();
process.stderr.write(data);
});
childProcess.once("exit", (code, signal) => {
if (this.process !== childProcess) return;
const error = this.createProcessExitError(code, signal);
this.exitError = error;
this.rejectPendingRequests(error);
});
childProcess.once("error", (error) => {
if (this.process !== childProcess) return;
const processError = new Error(`Agent process error: ${error.message}. Stderr: ${this.stderr}`);
this.exitError = processError;
this.rejectPendingRequests(processError);
});
childProcess.stdin?.on("error", (error) => {
if (this.process !== childProcess) return;
const stdinError =
this.exitError ?? new Error(`Agent process stdin error: ${error.message}. Stderr: ${this.stderr}`);
this.exitError = stdinError;
this.rejectPendingRequests(stdinError);
});
// Set up strict JSONL reader for stdout.
this.stopReadingStdout = attachJsonlLineReader(this.process.stdout!, (line) => {
this.stopReadingStdout = attachJsonlLineReader(childProcess.stdout!, (line) => {
this.handleLine(line);
});
@@ -107,7 +131,9 @@ export class RpcClient {
await new Promise((resolve) => setTimeout(resolve, 100));
if (this.process.exitCode !== null) {
throw new Error(`Agent process exited immediately with code ${this.process.exitCode}. Stderr: ${this.stderr}`);
const error = this.exitError ?? this.createProcessExitError(this.process.exitCode, this.process.signalCode);
this.exitError = error;
throw error;
}
}
@@ -474,17 +500,41 @@ export class RpcClient {
}
}
private createProcessExitError(code: number | null, signal: NodeJS.Signals | null): Error {
return new Error(`Agent process exited (code=${code} signal=${signal}). Stderr: ${this.stderr}`);
}
private rejectPendingRequests(error: Error): void {
for (const pending of this.pendingRequests.values()) {
pending.reject(error);
}
this.pendingRequests.clear();
}
private async send(command: RpcCommandBody): Promise<RpcResponse> {
if (!this.process?.stdin) {
const childProcess = this.process;
const stdin = childProcess?.stdin;
if (!childProcess || !stdin) {
throw new Error("Client not started");
}
if (this.exitError) {
throw this.exitError;
}
if (childProcess.exitCode !== null) {
const error = this.createProcessExitError(childProcess.exitCode, childProcess.signalCode);
this.exitError = error;
throw error;
}
if (stdin.destroyed || !stdin.writable) {
const error = new Error(`Agent process stdin is not writable. Stderr: ${this.stderr}`);
this.exitError = error;
throw error;
}
const id = `req_${++this.requestId}`;
const fullCommand = { ...command, id } as RpcCommand;
return new Promise((resolve, reject) => {
this.pendingRequests.set(id, { resolve, reject });
const timeout = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`));
@@ -501,7 +551,14 @@ export class RpcClient {
},
});
this.process!.stdin!.write(serializeJsonLine(fullCommand));
try {
stdin.write(serializeJsonLine(fullCommand));
} catch (error: unknown) {
const writeError = error instanceof Error ? error : new Error(String(error));
const pending = this.pendingRequests.get(id);
this.pendingRequests.delete(id);
pending?.reject(writeError);
}
});
}

View File

@@ -19,7 +19,12 @@ import type {
ExtensionWidgetOptions,
WorkingIndicatorOptions,
} from "../../core/extensions/index.ts";
import { takeOverStdout, writeRawStdout } from "../../core/output-guard.ts";
import {
flushRawStdout,
takeOverStdout,
waitForRawStdoutBackpressure,
writeRawStdout,
} from "../../core/output-guard.ts";
import { killTrackedDetachedChildren } from "../../utils/shell.ts";
import { type Theme, theme } from "../interactive/theme/theme.ts";
import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.ts";
@@ -50,6 +55,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
takeOverStdout();
let session = runtimeHost.session;
let unsubscribe: (() => void) | undefined;
let unsubscribeBackpressure: (() => void) | undefined;
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
writeRawStdout(serializeJsonLine(obj));
@@ -312,6 +318,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
session = runtimeHost.session;
await session.bindExtensions({
uiContext: createExtensionUIContext(),
mode: "rpc",
commandContextActions: {
waitForIdle: () => session.agent.waitForIdle(),
newSession: async (options) => runtimeHost.newSession(options),
@@ -344,9 +351,13 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
});
unsubscribe?.();
unsubscribeBackpressure?.();
unsubscribe = session.subscribe((event) => {
output(event);
});
unsubscribeBackpressure = session.agent.subscribe(async () => {
await waitForRawStdoutBackpressure();
});
};
const registerSignalHandlers = (): void => {
@@ -358,7 +369,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
for (const signal of signals) {
const handler = () => {
killTrackedDetachedChildren();
void shutdown(signal === "SIGHUP" ? 129 : 143);
void shutdown(signal === "SIGHUP" ? 129 : 143, signal);
};
process.on(signal, handler);
signalCleanupHandlers.push(() => process.off(signal, handler));
@@ -691,7 +702,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
default: {
const unknownCommand = command as { type: string };
return error(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
return error(id, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
}
}
};
@@ -702,7 +713,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
*/
let detachInput = () => {};
async function shutdown(exitCode = 0): Promise<never> {
async function shutdown(exitCode = 0, signal?: NodeJS.Signals): Promise<never> {
if (shuttingDown) {
process.exit(exitCode);
}
@@ -711,9 +722,13 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
cleanup();
}
unsubscribe?.();
unsubscribeBackpressure?.();
await runtimeHost.dispose();
detachInput();
process.stdin.pause();
if (signal !== "SIGTERM") {
await flushRawStdout();
}
process.exit(exitCode);
}
@@ -734,6 +749,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
`Failed to parse command: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
),
);
await waitForRawStdoutBackpressure();
return;
}
@@ -758,6 +774,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
const response = await handleCommand(command);
if (response) {
output(response);
await waitForRawStdoutBackpressure();
}
await checkShutdownRequested();
} catch (commandError: unknown) {
@@ -768,6 +785,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
commandError instanceof Error ? commandError.message : String(commandError),
),
);
await waitForRawStdoutBackpressure();
}
};