feat(agent): add profile system and improve tools
- Add Agent Profile module for managing agent identity, soul, tools, memory, and bootstrap configuration - Add profile CLI (pnpm agent:profile) for creating/listing/showing profiles - Default sessionId to UUIDv7 instead of "default" - Expose Agent.sessionId as public readonly property - Improve exec/process tools error handling (no more crashes on spawn errors) - Add 'output' action to process tool for reading stdout/stderr - Better tool descriptions to guide agent in choosing exec vs process Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
46a6cb3061
commit
200b2cefda
15 changed files with 740 additions and 58 deletions
|
|
@ -28,7 +28,7 @@ export function createExecTool(defaultCwd?: string): AgentTool<typeof ExecSchema
|
|||
return {
|
||||
name: "exec",
|
||||
label: "Exec",
|
||||
description: "Execute a shell command and return its output.",
|
||||
description: "Execute a shell command and wait for it to complete. Returns stdout/stderr output. Use this for short-lived commands only (e.g., ls, cat, pip install). Do NOT use for long-running processes like servers - use the 'process' tool instead.",
|
||||
parameters: ExecSchema,
|
||||
execute: async (_toolCallId, args, signal) => {
|
||||
const { command, cwd, timeoutMs } = args as ExecArgs;
|
||||
|
|
@ -66,12 +66,29 @@ export function createExecTool(defaultCwd?: string): AgentTool<typeof ExecSchema
|
|||
|
||||
child.stdout?.on("data", handleData);
|
||||
child.stderr?.on("data", handleData);
|
||||
|
||||
let spawnError: Error | null = null;
|
||||
child.on("error", (err) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
reject(err);
|
||||
spawnError = err;
|
||||
// 不 reject,让 close 事件处理
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
|
||||
// 如果有 spawn 错误,返回错误信息
|
||||
if (spawnError) {
|
||||
resolve({
|
||||
content: [{ type: "text", text: `Error: ${spawnError.message}` }],
|
||||
details: {
|
||||
output: `Error: ${spawnError.message}`,
|
||||
exitCode: code ?? 1,
|
||||
truncated: false,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const output = Buffer.concat(chunks).toString("utf8");
|
||||
resolve({
|
||||
content: [{ type: "text", text: output || (timedOut ? "Process timed out." : "") }],
|
||||
|
|
|
|||
|
|
@ -4,35 +4,40 @@ import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|||
import { v7 as uuidv7 } from "uuid";
|
||||
|
||||
const ProcessSchema = Type.Object({
|
||||
action: Type.String({ description: "Action: start | status | stop." }),
|
||||
id: Type.Optional(Type.String({ description: "Process id for status/stop." })),
|
||||
action: Type.String({ description: "Action: start | status | stop | output." }),
|
||||
id: Type.Optional(Type.String({ description: "Process id for status/stop/output." })),
|
||||
command: Type.Optional(Type.String({ description: "Command to run for start." })),
|
||||
cwd: Type.Optional(Type.String({ description: "Working directory." })),
|
||||
});
|
||||
|
||||
const MAX_OUTPUT_BUFFER = 64 * 1024; // 64KB per process
|
||||
|
||||
type ProcessEntry = {
|
||||
id: string;
|
||||
command: string;
|
||||
cwd?: string;
|
||||
cwd?: string | undefined;
|
||||
child: ChildProcess;
|
||||
exitCode: number | null;
|
||||
startedAt: number;
|
||||
outputBuffer: string[];
|
||||
outputSize: number;
|
||||
};
|
||||
|
||||
const PROCESS_REGISTRY = new Map<string, ProcessEntry>();
|
||||
|
||||
export type ProcessResult = {
|
||||
id?: string;
|
||||
running?: boolean;
|
||||
exitCode?: number | null;
|
||||
message?: string;
|
||||
id?: string | undefined;
|
||||
running?: boolean | undefined;
|
||||
exitCode?: number | null | undefined;
|
||||
message?: string | undefined;
|
||||
output?: string | undefined;
|
||||
};
|
||||
|
||||
export function createProcessTool(defaultCwd?: string): AgentTool<typeof ProcessSchema, ProcessResult> {
|
||||
return {
|
||||
name: "process",
|
||||
label: "Process",
|
||||
description: "Manage background processes (start, status, stop).",
|
||||
description: "Manage long-running background processes like servers, watchers, or daemons. Actions: 'start' to launch (returns immediately with process id), 'status' to check if running, 'output' to read stdout/stderr, 'stop' to terminate. Use this for servers (e.g., python server.py, npm run dev) instead of 'exec'.",
|
||||
parameters: ProcessSchema,
|
||||
execute: async (_toolCallId, params, signal) => {
|
||||
const action = String(params.action ?? "").toLowerCase();
|
||||
|
|
@ -47,29 +52,81 @@ export function createProcessTool(defaultCwd?: string): AgentTool<typeof Process
|
|||
if (PROCESS_REGISTRY.has(id)) {
|
||||
throw new Error(`Process already exists: ${id}`);
|
||||
}
|
||||
const child = spawn(command, {
|
||||
shell: true,
|
||||
cwd: params.cwd || defaultCwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
});
|
||||
const entry: ProcessEntry = {
|
||||
id,
|
||||
command,
|
||||
cwd: params.cwd || defaultCwd,
|
||||
child,
|
||||
exitCode: null,
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
PROCESS_REGISTRY.set(id, entry);
|
||||
child.on("close", (code) => {
|
||||
entry.exitCode = code;
|
||||
});
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => {
|
||||
child.kill("SIGTERM");
|
||||
|
||||
// 使用 Promise 等待进程启动或失败
|
||||
const result = await new Promise<{ success: boolean; error?: string }>((resolve) => {
|
||||
const child = spawn(command, {
|
||||
shell: true,
|
||||
cwd: params.cwd || defaultCwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
});
|
||||
|
||||
let resolved = false;
|
||||
|
||||
// 处理 spawn 错误(如 shell 不存在)
|
||||
child.on("error", (err) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
resolve({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 进程启动成功后注册到 registry
|
||||
child.on("spawn", () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const entry: ProcessEntry = {
|
||||
id,
|
||||
command,
|
||||
cwd: params.cwd || defaultCwd,
|
||||
child,
|
||||
exitCode: null,
|
||||
startedAt: Date.now(),
|
||||
outputBuffer: [],
|
||||
outputSize: 0,
|
||||
};
|
||||
PROCESS_REGISTRY.set(id, entry);
|
||||
|
||||
// 收集输出到缓冲区
|
||||
const collectOutput = (data: Buffer) => {
|
||||
const text = data.toString("utf8");
|
||||
if (entry.outputSize + text.length > MAX_OUTPUT_BUFFER) {
|
||||
// 超出限制时,移除旧的输出
|
||||
while (entry.outputBuffer.length > 0 && entry.outputSize + text.length > MAX_OUTPUT_BUFFER) {
|
||||
const removed = entry.outputBuffer.shift();
|
||||
if (removed) entry.outputSize -= removed.length;
|
||||
}
|
||||
}
|
||||
entry.outputBuffer.push(text);
|
||||
entry.outputSize += text.length;
|
||||
};
|
||||
|
||||
child.stdout?.on("data", collectOutput);
|
||||
child.stderr?.on("data", collectOutput);
|
||||
|
||||
child.on("close", (code) => {
|
||||
entry.exitCode = code;
|
||||
});
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => {
|
||||
child.kill("SIGTERM");
|
||||
});
|
||||
}
|
||||
|
||||
resolve({ success: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed to start process: ${result.error}` }],
|
||||
details: { id, running: false, message: result.error },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Started process ${id}` }],
|
||||
details: { id, running: true },
|
||||
|
|
@ -108,6 +165,23 @@ export function createProcessTool(defaultCwd?: string): AgentTool<typeof Process
|
|||
};
|
||||
}
|
||||
|
||||
if (action === "output") {
|
||||
const id = String(params.id ?? "");
|
||||
const entry = PROCESS_REGISTRY.get(id);
|
||||
if (!entry) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Process not found: ${id}` }],
|
||||
details: { id, running: false },
|
||||
};
|
||||
}
|
||||
const output = entry.outputBuffer.join("");
|
||||
const running = entry.exitCode === null;
|
||||
return {
|
||||
content: [{ type: "text", text: output || "(no output)" }],
|
||||
details: { id, running, exitCode: entry.exitCode, output },
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue