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]>
This commit is contained in:
1 parent
1262340856
commit
9c881e737f
24 files changed
+2419
-8
No files matched your search
@@ -0,0 +1,58 @@
|
||||
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 };
|
||||
}
|
||||
Reference in new issue
Block a user