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,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();
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user