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:
rootandClaude Fable 5.1 committed 2026-09-27 17:55:28 +08:00
1 parent 1262340856
commit 9c881e737f
24 files changed
+2419 -8

No files matched your search

@@ -0,0 +1,40 @@
/** 追加式事件通道:全部事件保存在内存,支持多个消费者按 seq 回放 + 实时跟随 */
export class EventChannel<T> {
private readonly items: T[] = [];
private waiters: Array<() => void> = [];
private closed = false;
push(item: T): void {
if (this.closed) return;
this.items.push(item);
this.wake();
}
close(): void {
this.closed = true;
this.wake();
}
get size(): number {
return this.items.length;
}
snapshot(): T[] {
return [...this.items];
}
async *iterate(fromIndex = 0): AsyncGenerator<T> {
let i = Math.max(0, fromIndex);
while (true) {
while (i < this.items.length) yield this.items[i++] as T;
if (this.closed) return;
await new Promise<void>((resolve) => this.waiters.push(resolve));
}
}
private wake(): void {
const waiters = this.waiters;
this.waiters = [];
for (const w of waiters) w();
}
}
+108
View File
@@ -0,0 +1,108 @@
import type { CapabilityScope } from "../capability/types";
import type { AgentSettings } from "../settings";
/** 本包对外的用量统计(与 SDK 类型解耦) */
export interface AgentUsage {
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheWriteTokens: number;
totalTokens: number;
costUsd: number;
}
export type AgentFailureReason = "budget" | "aborted" | "error";
export type AgentEventBody =
| { type: "run_started"; goal: string; resumed: boolean }
| { type: "text_delta"; delta: string }
| { type: "thinking_delta"; delta: string }
| {
type: "tool_call_start";
toolCallId: string;
capabilityId: string;
/** 已脱敏、截断的输入摘要 */
inputSummary: string;
recovery: boolean;
}
| {
type: "tool_call_end";
toolCallId: string;
capabilityId: string;
isError: boolean;
/** 已脱敏、截断的输出摘要 */
outputSummary: string;
recovery: boolean;
}
| { type: "tool_call_rejected"; toolCallId: string; toolName: string; capabilityId?: string; reason: string }
| {
type: "compaction";
phase: "start" | "end";
reason: "manual" | "threshold" | "overflow";
status?: "completed" | "declined" | "aborted" | "failed";
}
| { type: "run_completed"; finalText: string; usage: AgentUsage }
| { type: "run_failed"; reason: AgentFailureReason; message: string; usage: AgentUsage; budget?: BudgetKind };
export type BudgetKind = "tool_calls" | "duration" | "tokens" | "cost";
export type AgentEvent = AgentEventBody & { runId: string; seq: number; at: string };
/** 调用能力时使用的业务身份;授权回调据此到 automation_grant / approval 查询 */
export interface AgentActor {
actorId: string;
/** 持久任务 ID(agent_run 所属任务) */
taskId: string;
scopeIds: string[];
/** 本次运行允许触达的作用域上限;"先给方案"模式传 ['read','preview','propose'] */
allowedScopes?: CapabilityScope[];
}
export interface CreateRunSpec {
/** = agent_run.id,同时作为 Pi 会话 ID 与 JSONL 文件名 */
runId: string;
goal: string;
systemPrompt: string;
/** 本次允许的能力 ID 白名单 */
capabilities: string[];
settings: AgentSettings;
/** 应用数据目录下的会话目录;会话文件为 sessionDir/<runId>.jsonl */
sessionDir: string;
actor: AgentActor;
}
export interface RunOutcome {
status: "completed" | "failed";
reason?: AgentFailureReason;
budget?: BudgetKind;
message?: string;
finalText: string;
usage: AgentUsage;
}
export interface AgentRun {
readonly runId: string;
readonly sessionPath: string;
/** 打开已有会话时发现的未完成操作数;>0 时应调用 resume() */
readonly pendingOperations: number;
/** 从 fromSeq(含)开始回放并持续推送事件;close() 后结束 */
events(fromSeq?: number): AsyncIterable<AgentEvent>;
/** 已产生的全部事件快照 */
history(): AgentEvent[];
/** 发送一条用户消息并驱动到本轮结束;text 省略时使用 goal */
prompt(text?: string): Promise<RunOutcome>;
/** 崩溃/重启后继续未完成的操作 */
resume(): Promise<RunOutcome>;
abort(reason?: string): Promise<void>;
compact(customInstructions?: string): Promise<void>;
close(): Promise<void>;
}
/** 领域/服务层只依赖这个端口,不直接接触 Pi SDK 类型 */
export interface AgentRuntimePort {
createRun(spec: CreateRunSpec): Promise<AgentRun>;
}
export function emptyUsage(): AgentUsage {
return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, costUsd: 0 };
}