refactor(agent): Pi 会话运行时 - 接入原生 Agent 与 Skills 工具链

This commit is contained in:
ddcat666 committed 2026-09-29 08:21:11 +08:00
1 parent f9b8c5a43e
commit 387d3562ad
69 files changed
+11460 -207

No files matched your search

+424
View File
@@ -0,0 +1,424 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
import { Type } from "typebox";
const providerID = "infinite-canvas";
const api = "openai-completions";
async function readRequest() {
let raw = "";
for await (const chunk of process.stdin) raw += chunk;
if (!raw.trim()) throw new Error("Agent runtime input is empty");
return JSON.parse(raw);
}
function emit(event, payload = {}) {
process.stdout.write(`${JSON.stringify({ event, ...payload })}\n`);
}
function createMessage(model, result) {
const content = [];
if (result.text) content.push({ type: "text", text: result.text });
for (const call of result.toolCalls ?? []) {
content.push({
type: "toolCall",
id: call.id,
name: call.name,
arguments: call.arguments ?? {},
});
}
return {
role: "assistant",
content,
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: Number(result.usage?.input ?? 0),
output: Number(result.usage?.output ?? 0),
cacheRead: Number(result.usage?.cacheRead ?? 0),
cacheWrite: Number(result.usage?.cacheWrite ?? 0),
totalTokens: Number(result.usage?.totalTokens ?? 0),
cost: result.usage?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: content.some((item) => item.type === "toolCall") ? "toolUse" : "stop",
timestamp: Date.now(),
};
}
async function bridge(request, path, body, signal) {
const response = await fetch(new URL(path, request.bridgeURL), {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${request.bridgeToken}`,
},
body: JSON.stringify(body),
signal,
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `Agent bridge returned HTTP ${response.status}`);
return data;
}
async function run() {
const request = await readRequest();
if (!request.bridgeURL || !request.bridgeToken || !request.model?.id) {
throw new Error("Agent runtime is missing its bridge, session, or model configuration");
}
const modelRuntime = await ModelRuntime.create({ agentDir: request.agentDir });
modelRuntime.registerProvider(providerID, {
name: "影策模型任务",
baseUrl: "http://agent-runtime.invalid/v1",
apiKey: "managed-by-go-bridge",
api,
authHeader: false,
models: [{
id: request.model.id,
name: request.model.name || request.model.id,
api,
reasoning: Boolean(request.model.reasoning),
input: request.model.input?.length ? request.model.input : ["text", "image"],
cost: request.model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: Math.max(1, Number(request.model.contextWindow || 128000)),
maxTokens: Math.max(1, Number(request.model.maxTokens || 8192)),
}],
streamSimple(model, context, options) {
const stream = createAssistantMessageEventStream();
const partial = {
role: "assistant",
content: [],
api: model.api,
provider: model.provider,
model: model.id,
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
stopReason: "pending",
timestamp: Date.now(),
};
void (async () => {
stream.push({ type: "start", partial });
try {
const result = await bridge(request, "/model", {
modelId: model.id,
messages: context.messages,
tools: (context.tools ?? []).map((tool) => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
})),
thinkingLevel: options?.reasoning,
}, options?.signal);
for (const text of result.steeringMessages ?? []) {
await session.steer(text);
}
const message = createMessage(model, result);
if (result.text) {
partial.content.push({ type: "text", text: result.text });
stream.push({ type: "text_start", contentIndex: 0, partial });
stream.push({ type: "text_delta", contentIndex: 0, delta: result.text, partial });
stream.push({ type: "text_end", contentIndex: 0, content: result.text, partial });
}
let contentIndex = result.text ? 1 : 0;
for (const call of result.toolCalls ?? []) {
const toolCall = message.content.find((item) => item.type === "toolCall" && item.id === call.id);
partial.content.push(toolCall);
stream.push({ type: "toolcall_start", contentIndex, partial });
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial });
contentIndex++;
}
stream.push({ type: "done", reason: message.stopReason, message });
stream.end(message);
} catch (error) {
stream.push({ type: "error", reason: "error", error: {
...partial,
stopReason: "error",
errorMessage: error instanceof Error ? error.message : String(error),
} });
stream.end();
}
})();
return stream;
},
});
const model = modelRuntime.getModel(providerID, request.model.id);
if (!model) throw new Error("Agent model registration failed");
let session;
// 工具进入审批时主动中止本轮;这是暂停,不是失败。
let pausedForApproval = false;
const tools = (request.tools ?? []).map((tool) => ({
name: tool.name,
label: tool.label || tool.name,
description: tool.description || tool.name,
parameters: Type.Unsafe(tool.parameters ?? { type: "object", properties: {} }),
executionMode: tool.executionMode || "sequential",
async execute(toolCallId, params, signal) {
const result = await bridge(request, "/tool", {
callId: toolCallId,
name: tool.name,
arguments: params,
}, signal);
if (result.pause) {
pausedForApproval = true;
emit("approval_wait", { approvalId: result.approvalId, toolName: tool.name, callId: toolCallId });
session.abort();
return {
content: [{ type: "text", text: result.content || "操作正在等待用户审批。" }],
details: { approvalId: result.approvalId, paused: true },
isError: true,
};
}
// 工具结果必须是非空文本:交回 "null" 会让模型返回空回复。
let text = typeof result.content === "string" ? result.content : JSON.stringify(result.content ?? result ?? {});
if (!text || text === "null") text = JSON.stringify({ error: "工具没有返回结果" });
return {
content: [{ type: "text", text }],
details: result.details,
isError: Boolean(result.isError),
};
},
}));
let sessionManager;
if (request.sessionJSONL) {
if (!request.sessionFile) throw new Error("Agent session restore requires a session file path");
await mkdir(dirname(request.sessionFile), { recursive: true, mode: 0o700 });
await writeFile(request.sessionFile, request.sessionJSONL, { mode: 0o600, flag: "w" });
sessionManager = SessionManager.open(request.sessionFile, undefined, request.cwd);
} else {
sessionManager = SessionManager.create(request.cwd, request.sessionDir);
}
const resourceLoader = new DefaultResourceLoader({
cwd: request.cwd,
agentDir: request.agentDir,
noExtensions: true,
noPromptTemplates: true,
noThemes: true,
noContextFiles: true,
additionalSkillPaths: request.skillPaths ?? [],
systemPrompt: request.systemPrompt,
});
await resourceLoader.reload();
const contextWindow = Math.max(1, Number(request.model?.contextWindow || 128000));
const sessionConfig = {
cwd: request.cwd,
agentDir: request.agentDir,
modelRuntime,
model,
resourceLoader,
sessionManager,
customTools: tools,
};
if (request.profile) {
sessionConfig.profile = {
revision: request.profile.revision,
hash: request.profile.hash,
layers: request.profile.layers || [],
};
}
if (request.memory?.enabled) {
sessionConfig.memory = {
enabled: true,
storePath: request.memory.storePath,
userId: request.userId,
canvasId: request.canvasId,
maxEntries: request.memory.maxEntries || 1000,
};
}
if (request.canvas) {
sessionConfig.canvas = request.canvas;
}
if (request.features) {
sessionConfig.features = {
planning: request.features.planningEnabled ?? true,
forms: request.features.formsEnabled ?? true,
approval: request.features.approvalRequired ?? [],
};
}
if (request.permissions) {
sessionConfig.permissions = request.permissions;
}
({ session } = await createAgentSession(sessionConfig));
const compactionConfig = request.compaction || {
enabled: true,
strategy: "balanced",
reserveTokens: Math.max(1024, Math.ceil(contextWindow * 0.2)),
keepRecentTokens: Math.min(20000, Math.max(1024, Math.floor(contextWindow * 0.2))),
};
session.settingsManager.applyOverrides({
compaction: compactionConfig,
// 重试只由 Go 模型桥决定(按上游真实 HTTP 状态区分临时故障与参数错误);
// 这里再重试一层会让 400 也被重发、让 500 被重试 3×3 次。
retry: { enabled: false },
});
const eventChain = { current: Promise.resolve() };
let runtimeError = null;
const enqueueEvent = (payload) => {
eventChain.current = eventChain.current.then(() => bridge(request, "/event", payload));
};
session.subscribe((event) => {
// 转发完整的 Pi 事件流到 Go 后端
if (event.type === "message_start") {
const message = event.message;
if (message?.role === "assistant") {
enqueueEvent({
type: "message_start",
role: "assistant",
messageId: event.messageId,
});
}
} else if (event.type === "message_delta") {
enqueueEvent({
type: "message_delta",
delta: event.delta,
messageId: event.messageId,
});
} else if (event.type === "message_end") {
const message = event.message;
if (message?.role !== "assistant") return;
if (pausedForApproval || message.stopReason === "aborted") return;
if (message.stopReason === "error" || message.errorMessage) {
runtimeError = new Error(message.errorMessage || "Agent assistant message failed");
return;
}
enqueueEvent({
type: "message_end",
message,
contextUsage: session.getContextUsage(),
});
} else if (event.type === "thinking_start") {
enqueueEvent({
type: "thinking_start",
thinkingId: event.thinkingId,
});
} else if (event.type === "thinking_delta") {
enqueueEvent({
type: "thinking_delta",
delta: event.delta,
thinkingId: event.thinkingId,
});
} else if (event.type === "thinking_end") {
enqueueEvent({
type: "thinking_end",
content: event.content,
thinkingId: event.thinkingId,
});
} else if (event.type === "planning_start") {
enqueueEvent({
type: "planning_start",
planId: event.planId,
});
} else if (event.type === "planning_update") {
enqueueEvent({
type: "planning_update",
plan: event.plan,
planId: event.planId,
});
} else if (event.type === "planning_end") {
enqueueEvent({
type: "planning_end",
plan: event.plan,
planId: event.planId,
});
} else if (event.type === "form_start") {
enqueueEvent({
type: "form_start",
form: event.form,
formId: event.formId,
});
} else if (event.type === "form_update") {
enqueueEvent({
type: "form_update",
form: event.form,
formId: event.formId,
});
} else if (event.type === "form_submit") {
enqueueEvent({
type: "form_submit",
answers: event.answers,
formId: event.formId,
});
} else if (event.type === "tool_call_start") {
enqueueEvent({
type: "tool_call_start",
toolCall: event.toolCall,
callId: event.callId,
});
} else if (event.type === "tool_call_end") {
enqueueEvent({
type: "tool_call_end",
result: event.result,
callId: event.callId,
});
} else if (event.type === "approval_requested") {
enqueueEvent({
type: "approval_requested",
approval: event.approval,
approvalId: event.approvalId,
});
} else if (event.type === "entry_appended") {
eventChain.current = eventChain.current.then(async () => {
const sessionFile = sessionManager.getSessionFile();
if (!sessionFile) return;
let sessionJSONL;
try {
sessionJSONL = await readFile(sessionFile, "utf8");
} catch (error) {
if (error?.code === "ENOENT") return;
throw error;
}
return bridge(request, "/event", {
type: "session_snapshot",
sessionJSONL,
contextUsage: session.getContextUsage(),
});
});
} else if (event.type === "compaction_start" || event.type === "compaction_end") {
enqueueEvent({ type: event.type, reason: event.reason, contextUsage: session.getContextUsage() });
}
});
try {
try {
await session.prompt(request.prompt);
} catch (error) {
if (!pausedForApproval) throw error;
}
await eventChain.current;
if (runtimeError && !pausedForApproval) throw runtimeError;
const sessionFile = sessionManager.getSessionFile();
const sessionJSONL = await readFile(sessionFile, "utf8");
await bridge(request, "/event", {
type: "session_snapshot",
sessionJSONL,
contextUsage: session.getContextUsage(),
});
emit("settled", {
contextUsage: session.getContextUsage(),
sessionFile,
entries: sessionManager.getEntries().length,
});
} finally {
session.dispose();
}
}
run().catch((error) => {
emit("runtime_error", { message: error instanceof Error ? error.message : String(error) });
process.exitCode = 1;
});