- CapabilityRegistry: descriptor(id/version/schema/scope/risk/idempotent/preview/replay), invoke = scope limit -> input schema -> server-side authorize -> idempotency key -> handler -> output schema -> audit; capability id <-> tool name mapping (download.submit <-> download__submit) - PiAgentRuntime (pi-agent-core AgentHarness 0.87.1): whitelist tools + before_tool re-check, redacted event stream, JSONL session at sessionDir/<runId>.jsonl, resume/abort/compact, tool-call/duration/token/cost budgets -> abort + run_failed(budget) - AgentSettings -> pi-ai createProvider (anthropic / openai-compatible), compaction mapping - FakeModelProvider + createFakeRuntime (scripted, transcript-derived, survives crash/resume) - buildSystemPrompt, redactForModel; 27 tests incl. SIGKILL + resume Co-Authored-By: Claude Fable 5.1 <[email protected]>
59 lines
2.6 KiB
TypeScript
59 lines
2.6 KiB
TypeScript
import { mkdir, readFile, rename, rmdir, stat } from "node:fs/promises";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { type Context, type JsonlSessionMetadata, JsonlSessionRepo, type Session } from "@earendil-works/pi-agent-core";
|
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/harness/env/nodejs";
|
|
|
|
export interface OpenedSession {
|
|
session: Session<JsonlSessionMetadata>;
|
|
repo: JsonlSessionRepo;
|
|
path: string;
|
|
created: boolean;
|
|
}
|
|
|
|
const SAFE_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
|
|
export function sessionPathFor(sessionDir: string, runId: string): string {
|
|
if (!SAFE_RUN_ID.test(runId) || runId.includes("..")) throw new Error(`runId 不能用作文件名: ${runId}`);
|
|
return join(resolve(sessionDir), `${runId}.jsonl`);
|
|
}
|
|
|
|
/**
|
|
* 打开/创建 sessionDir/<runId>.jsonl。
|
|
* JsonlSessionRepo 默认会把文件放到 `<root>/--cwd--/<时间戳>_<id>.jsonl`;这里创建后把空会话文件
|
|
* (只有 header)移到固定路径,再按元数据打开,之后所有写入都追加到固定路径。
|
|
* NodeExecutionEnv 只当作会话目录的 FileSystem 使用,它的 exec/shell 从不暴露给模型。
|
|
*/
|
|
export async function openRunSession(sessionDir: string, runId: string, ctx: Context): Promise<OpenedSession> {
|
|
const root = resolve(sessionDir);
|
|
await mkdir(root, { recursive: true });
|
|
const target = sessionPathFor(root, runId);
|
|
const fileSystem = new NodeExecutionEnv({ cwd: root });
|
|
const repo = new JsonlSessionRepo({ fileSystem, sessionsRoot: root });
|
|
|
|
const existing = await stat(target).catch(() => undefined);
|
|
if (existing) {
|
|
const firstLine = (await readFile(target, "utf8")).split("\n", 1)[0] ?? "";
|
|
const header = JSON.parse(firstLine) as { id: string; createdAt: number; storageVersion: number; cwd: string };
|
|
if (header.id !== runId) throw new Error(`会话文件 ${target} 属于 ${header.id},不是 ${runId}`);
|
|
const metadata: JsonlSessionMetadata = {
|
|
id: header.id,
|
|
createdAt: header.createdAt,
|
|
storageVersion: header.storageVersion,
|
|
cwd: header.cwd,
|
|
path: target,
|
|
modifiedAt: existing.mtimeMs,
|
|
};
|
|
const session = await repo.open(metadata, ctx);
|
|
return { session, repo, path: target, created: false };
|
|
}
|
|
|
|
const draft = await repo.create({ id: runId, cwd: root }, ctx);
|
|
const draftPath = draft.metadata.path;
|
|
await draft.close(ctx);
|
|
await rename(draftPath, target);
|
|
await rmdir(dirname(draftPath)).catch(() => undefined);
|
|
const info = await stat(target);
|
|
const session = await repo.open({ ...draft.metadata, path: target, modifiedAt: info.mtimeMs }, ctx);
|
|
return { session, repo, path: target, created: true };
|
|
}
|