Files
UHD_Media_Tool/packages/agent/src/pi/session.ts
T
rootandClaude Fable 5.1 9c881e737f feat(agent): capability registry, Pi AgentRuntimePort, fake model provider and redaction guard (W1-5)
- 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]>
2026-09-27 17:55:28 +08:00

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 };
}