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

+68
View File
@@ -0,0 +1,68 @@
import type { CompactionSettings } from "@earendil-works/pi-agent-core";
import { type Api, createModels, createProvider, type Model, type MutableModels } from "@earendil-works/pi-ai";
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
import type { ResolvedAgentSettings } from "../settings";
/** Pi 侧需要的模型装配结果(仅在 src/pi/* 内部流转) */
export interface ResolvedPiModel {
models: MutableModels;
model: Model<Api>;
}
export type PiModelResolver = (settings: ResolvedAgentSettings) => ResolvedPiModel;
const ANTHROPIC_DEFAULT_BASE = "https://api.anthropic.com";
const PROVIDER_ID = { anthropic: "umt-anthropic", "openai-compatible": "umt-openai-compatible" } as const;
/**
* 设置 → pi-ai createProvider。apiKey 只在请求时经 auth.resolve 交给 SDK,不读环境变量。
* Anthropic baseUrl 不带 /v1;OpenAI 兼容 baseUrl 带 /v1(与 spike 抓包结论一致)。
*/
export function resolveModelFromSettings(settings: ResolvedAgentSettings): ResolvedPiModel {
const providerId = PROVIDER_ID[settings.provider];
const common = {
id: settings.model,
name: settings.model,
provider: providerId,
reasoning: settings.thinkingLevel !== "off",
input: ["text"] as ("text" | "image")[],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: settings.contextBudgetTokens,
maxTokens: Math.min(settings.maxOutputTokens, Math.floor(settings.contextBudgetTokens / 2)),
};
const apiKey = settings.apiKey;
const auth = { apiKey: { name: "umt-secret", resolve: async () => ({ auth: { apiKey } }) } };
const models = createModels();
let model: Model<Api>;
if (settings.provider === "anthropic") {
const m: Model<"anthropic-messages"> = {
...common,
api: "anthropic-messages",
baseUrl: (settings.baseUrl ?? ANTHROPIC_DEFAULT_BASE).replace(/\/+$/, "").replace(/\/v1$/, ""),
};
models.setProvider(createProvider({ id: providerId, auth, models: [m], api: anthropicMessagesApi() }));
model = m;
} else {
if (!settings.baseUrl) throw new Error("openai-compatible 上游必须配置 baseUrl(带 /v1)");
const m: Model<"openai-completions"> = {
...common,
api: "openai-completions",
baseUrl: settings.baseUrl.replace(/\/+$/, ""),
compat: { supportsDeveloperRole: false },
};
models.setProvider(createProvider({ id: providerId, auth, models: [m], api: openAICompletionsApi() }));
model = m;
}
return { models, model };
}
/** 压缩设置:threshold 模式下占用达到 threshold 比例时触发;manual 只允许手动 compact() */
export function compactionFromSettings(settings: ResolvedAgentSettings, contextWindow: number): CompactionSettings {
const reserveTokens = Math.max(256, Math.round(contextWindow * (1 - settings.compaction.threshold)));
return {
enabled: settings.compaction.mode === "threshold",
reserveTokens,
keepRecentTokens: Math.max(256, Math.round(contextWindow * 0.25)),
};
}