- PiAgentRuntime 改用 pi-durable Harness:每个 run 一个 JSONL 存储目录 sessionDir/<runId>/ + root 对话; 系统提示 / 能力工具 / 白名单与预算钩子装在每次运行自己的扩展里;watchEvents 翻译成 AgentRunEvent - 挂起(工具内 close)立即封口,调用保持未完成,重开后 resume 重放;replayGate 用调用级 memo - 旧格式 <runId>.jsonl 打开时抛 LegacySessionError;skills 加载与 <skill> 块格式内置(源自 pi-agent-core,MIT) - tool_call_end 新增可选 durationMs;用量取 pi.usage 合计(整段会话总量) - Azure provider 改名 azure,迁移 0093 改存量;移除 pi-agent-core,锁 pi-durable/pi-ai/chord 1.1.0 - 文档:ADR-003、pi-providers、runbook、agent README、CHANGELOG Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
78 lines
3.6 KiB
TypeScript
78 lines
3.6 KiB
TypeScript
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 { CompactionPolicy } from "@earendil-works/pi-durable";
|
|
import type { ResolvedAgentSettings } from "../settings.ts";
|
|
|
|
/** Pi 侧需要的模型装配结果(仅在 src/pi/* 内部流转) */
|
|
export interface ResolvedPiModel {
|
|
models: MutableModels;
|
|
model: Model<Api>;
|
|
/** 夹到模型支持范围后的思考档位;缺省用设置值 */
|
|
thinkingLevel?: ResolvedAgentSettings["thinkingLevel"];
|
|
}
|
|
|
|
export type PiModelResolver = (settings: ResolvedAgentSettings) => ResolvedPiModel | Promise<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 {
|
|
if (settings.provider !== "anthropic" && settings.provider !== "openai-compatible")
|
|
throw new Error(`provider ${settings.provider} 需要按模型服务装配(PiAgentRuntimeOptions.upstream)`);
|
|
if (!settings.apiKey) throw new Error("未配置模型服务密钥");
|
|
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()。
|
|
* backgroundTokens=0:不做 pi-durable 的后台提前压缩,行为与 pi-agent-core 0.99 的阈值压缩一致。
|
|
*/
|
|
export function compactionFromSettings(settings: ResolvedAgentSettings, contextWindow: number): CompactionPolicy {
|
|
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)),
|
|
backgroundTokens: 0,
|
|
};
|
|
}
|