@springbrand/agent-runtime 0.1.1 → 0.1.3-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.1.1",
3
+ "version": "0.1.3-alpha.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -759,6 +759,7 @@ export interface RuntimeBrowserPort {
759
759
  */
760
760
  export type RuntimeModelProtocol =
761
761
  | "openai-chat"
762
+ | "openrouter-chat"
762
763
  | "anthropic-messages"
763
764
  | "google-generative-ai"
764
765
  | "openai-codex-responses";
@@ -26,6 +26,9 @@ import {
26
26
  import {
27
27
  openaiProvider,
28
28
  } from "@earendil-works/pi-ai/providers/openai";
29
+ import {
30
+ openrouterProvider,
31
+ } from "@earendil-works/pi-ai/providers/openrouter";
29
32
  import {
30
33
  googleProvider,
31
34
  } from "@earendil-works/pi-ai/providers/google";
@@ -40,6 +43,7 @@ import type {
40
43
 
41
44
  const CATALOGS = {
42
45
  "openai-chat": openaiProvider().getModels(),
46
+ "openrouter-chat": openrouterProvider().getModels(),
43
47
  "anthropic-messages": anthropicProvider().getModels(),
44
48
  "google-generative-ai": googleProvider().getModels(),
45
49
  "openai-codex-responses": openaiCodexProvider().getModels(),
@@ -126,6 +130,7 @@ function providerId(endpoint: RuntimeModelEndpoint, index: number): string {
126
130
  function apiFor(protocol: RuntimeModelProtocol): Api {
127
131
  switch (protocol) {
128
132
  case "openai-chat":
133
+ case "openrouter-chat":
129
134
  return "openai-completions";
130
135
  case "anthropic-messages":
131
136
  return "anthropic-messages";
@@ -139,6 +144,7 @@ function apiFor(protocol: RuntimeModelProtocol): Api {
139
144
  function piApiFor(protocol: RuntimeModelProtocol) {
140
145
  switch (protocol) {
141
146
  case "openai-chat":
147
+ case "openrouter-chat":
142
148
  return openAICompletionsApi();
143
149
  case "anthropic-messages":
144
150
  return anthropicMessagesApi();
@@ -165,12 +171,11 @@ function configuredModel(
165
171
  `Model is not in Pi's built-in catalog for ${endpoint.protocol}: ${modelId}`,
166
172
  );
167
173
  }
168
- // 待确认:当前统一丢弃 catalogModel.compat;现有提交历史没有说明这样做对 Anthropic 兼容参数的影响,修改前需先验证 Pi 的流式请求行为。
169
174
  const {
170
175
  api: _api,
171
176
  provider: _provider,
172
177
  baseUrl: _baseUrl,
173
- compat: _compat,
178
+ compat,
174
179
  headers: catalogHeaders,
175
180
  ...metadata
176
181
  } = catalogModel;
@@ -183,6 +188,9 @@ function configuredModel(
183
188
  api: apiFor(endpoint.protocol),
184
189
  provider: providerId(endpoint, index),
185
190
  baseUrl: endpoint.baseURL,
191
+ ...(endpoint.protocol === "openrouter-chat" && compat
192
+ ? { compat }
193
+ : {}),
186
194
  ...(Object.keys(headers).length > 0 ? { headers } : {}),
187
195
  };
188
196
  }
@@ -228,10 +236,10 @@ export function resolvePiApiKey(
228
236
  * 用当前部署端点完整重建一个已有的 Pi 模型集合。
229
237
  *
230
238
  * PiRuntimeAdapter 激活新运行快照时调用它;需要复用 MutableModels 的调用方也可以调用,并继续使用返回的同一实例。
231
- * 该函数会先清空集合,所以调用方必须把传入对象视为由 Runtime 独占管理,不能期待自定义 provider 被保留。
239
+ * 该函数在全部新 provider 校验通过后才清空并替换集合,所以失败时旧集合保持可用。
232
240
  *
233
241
  * Pi 当前用 provider.id 在 Map 中注册和路由请求,因此每个部署端点都必须同时注册唯一 provider、对应模型、认证解析器和协议实现。
234
- * clearProviders 是为了避免配置更新后旧端点继续可用;所有模型在注册时都会经过 catalog 校验,因此任一未知 modelId 会让整次配置失败。
242
+ * 替换时先构建新 provider,确保任一未知 modelId 都不会让旧端点失效;校验通过后再清空旧集合,避免配置更新后旧端点继续可用。
235
243
  *
236
244
  * @returns 传入并已重建完成的 `models` 实例。
237
245
  * @throws 当任一部署模型不在 Pi 内置 OpenAI、Anthropic catalog 中时抛出错误。
@@ -240,9 +248,8 @@ export function configurePiModels(
240
248
  models: MutableModels,
241
249
  provider: RuntimeProviderPort,
242
250
  ): MutableModels {
243
- models.clearProviders();
244
- provider.endpoints.forEach((endpoint, index) => {
245
- models.setProvider(createProvider({
251
+ const configuredProviders = provider.endpoints.map((endpoint, index) =>
252
+ createProvider({
246
253
  id: providerId(endpoint, index),
247
254
  name: endpoint.protocol,
248
255
  baseUrl: endpoint.baseURL,
@@ -259,8 +266,12 @@ export function configurePiModels(
259
266
  configuredModel(endpoint, index, modelId),
260
267
  ),
261
268
  api: piApiFor(endpoint.protocol),
262
- }));
263
- });
269
+ })
270
+ );
271
+ models.clearProviders();
272
+ configuredProviders.forEach((configuredProvider) =>
273
+ models.setProvider(configuredProvider)
274
+ );
264
275
  return models;
265
276
  }
266
277
 
@@ -66,9 +66,9 @@ function candidate<T extends TSchema>(tool: AgentTool<T>): PiToolCandidate {
66
66
  };
67
67
  }
68
68
 
69
- /** Runtime-permanent Pi tools, independent of optional Host plugins. */
69
+ /** Runtime base tools, including native web search when the model supports it. */
70
70
  export function basePiToolCandidates(
71
- webSearch: WebSearch,
71
+ webSearch?: WebSearch,
72
72
  ): PiToolCandidate[] {
73
73
  return [
74
74
  candidate({
@@ -105,6 +105,6 @@ export function basePiToolCandidates(
105
105
  });
106
106
  },
107
107
  }),
108
- webSearchPiToolCandidate(webSearch),
108
+ ...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
109
109
  ];
110
110
  }
@@ -14,6 +14,8 @@ import type { PiToolCandidate } from "./compiler";
14
14
 
15
15
  // 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
16
16
 
17
+ const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
18
+
17
19
  /**
18
20
  * 把宿主的 Worker Loader、出站网络和 Workspace 组装成 Pi 代码执行工具候选项。
19
21
  *
@@ -32,6 +34,8 @@ export function workspaceCodeExecutionPiToolCandidate(options: {
32
34
  executor: new DynamicWorkerExecutor({
33
35
  loader: options.loader,
34
36
  globalOutbound: options.outbound,
37
+ // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
38
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
35
39
  }),
36
40
  connectors: [
37
41
  new StateConnector(
@@ -205,6 +205,7 @@ const executeParameters = Type.Object({
205
205
  "Plain JavaScript async function. TypeScript annotations are not supported.",
206
206
  }),
207
207
  });
208
+ const CODEMODE_EXECUTE_TIMEOUT_MS = 60_000;
208
209
 
209
210
  /**
210
211
  * 把 Cloudflare Codemode Runtime handle 包装为高风险 Pi 代码执行工具候选项。
@@ -235,7 +236,33 @@ export function codeExecutionPiToolCandidate(
235
236
  // 必须通过 Runtime handle 而不是直接调用 executor,因为 Cloudflare Codemode 把重放、审批和执行日志放在持久化 Runtime 层。
236
237
  async execute(_toolCallId, input, signal) {
237
238
  signal?.throwIfAborted();
238
- return result(await runtime.execute(input));
239
+ // ponytail: 外层截止只保证 Agent 继续;Codemode 支持 AbortSignal 或宿主 dispose 后再终止底层执行。
240
+ let timeout: ReturnType<typeof setTimeout> | undefined;
241
+ const deadline = new Promise<{
242
+ status: "error";
243
+ code: "timeout";
244
+ error: string;
245
+ retryable: false;
246
+ outcome: "unknown";
247
+ }>((resolve) => {
248
+ timeout = setTimeout(() => resolve({
249
+ status: "error",
250
+ code: "timeout",
251
+ error:
252
+ `Code Mode execute exceeded ${CODEMODE_EXECUTE_TIMEOUT_MS} ms; ` +
253
+ "use existing results and state the missing evidence.",
254
+ retryable: false,
255
+ outcome: "unknown",
256
+ }), CODEMODE_EXECUTE_TIMEOUT_MS);
257
+ });
258
+ try {
259
+ return result(await Promise.race([
260
+ runtime.execute(input),
261
+ deadline,
262
+ ]));
263
+ } finally {
264
+ if (timeout !== undefined) clearTimeout(timeout);
265
+ }
239
266
  },
240
267
  };
241
268
  return {
@@ -1106,6 +1106,8 @@ export interface WebSearchOptions {
1106
1106
 
1107
1107
  function apiFor(endpoint: RuntimeModelEndpoint): NativeApi {
1108
1108
  switch (endpoint.protocol) {
1109
+ case "openrouter-chat":
1110
+ throw new Error("Native web search is unavailable for openrouter-chat");
1109
1111
  case "google-generative-ai":
1110
1112
  return "google-generative-ai";
1111
1113
  case "openai-chat":
package/src/plugins.ts CHANGED
@@ -863,12 +863,22 @@ class RuntimeBuilder implements RuntimeContributionContext {
863
863
  );
864
864
  }
865
865
  const resolvedModel = resolvePiModel(provider, profile.model);
866
- const webSearch = createWebSearch({
867
- endpoint: endpoints[0]!,
868
- model: profile.model,
869
- maxTokens: resolvedModel.maxTokens,
870
- reasoning: resolvedModel.reasoning,
871
- });
866
+ const endpoint = endpoints[0]!;
867
+ const webSearch = endpoint.protocol === "openrouter-chat"
868
+ ? undefined
869
+ : createWebSearch({
870
+ endpoint,
871
+ model: profile.model,
872
+ maxTokens: resolvedModel.maxTokens,
873
+ reasoning: resolvedModel.reasoning,
874
+ });
875
+ if (!webSearch) {
876
+ this.reportDegradation({
877
+ capability: "web_search",
878
+ reason: "unavailable",
879
+ detail: "Native web search is unavailable for openrouter-chat",
880
+ });
881
+ }
872
882
  for (const candidate of basePiToolCandidates(webSearch)) {
873
883
  this.addPiTool(candidate);
874
884
  }