@springbrand/agent-runtime 0.1.0 → 0.1.2
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 +1 -1
- package/src/kernel/bindings.ts +1 -0
- package/src/kernel/profile.ts +0 -2
- package/src/pi/runtime-adapter/assembly.ts +2 -2
- package/src/pi/runtime-adapter/models.ts +65 -14
- package/src/pi/tool/base.ts +3 -3
- package/src/pi/tool/web-search/api.ts +2 -0
- package/src/plugins.ts +16 -7
package/package.json
CHANGED
package/src/kernel/bindings.ts
CHANGED
package/src/kernel/profile.ts
CHANGED
|
@@ -84,7 +84,6 @@ export interface RuntimeProfile {
|
|
|
84
84
|
denyPolicy?: RuntimeDenyPolicy;
|
|
85
85
|
enabledSubagents: readonly string[];
|
|
86
86
|
mcpServers: readonly RuntimeMcpServer[];
|
|
87
|
-
enabledExtensions: readonly string[];
|
|
88
87
|
executionLevel: ExecutionLevel;
|
|
89
88
|
memory: RuntimeMemoryProfile;
|
|
90
89
|
}
|
|
@@ -109,7 +108,6 @@ export function freezeRuntimeProfile(profile: RuntimeProfile): RuntimeProfile {
|
|
|
109
108
|
: undefined,
|
|
110
109
|
enabledSubagents: Object.freeze([...profile.enabledSubagents]),
|
|
111
110
|
mcpServers: Object.freeze(profile.mcpServers.map((server) => Object.freeze({ ...server }))),
|
|
112
|
-
enabledExtensions: Object.freeze([...profile.enabledExtensions]),
|
|
113
111
|
memory: Object.freeze({ ...profile.memory }),
|
|
114
112
|
};
|
|
115
113
|
return Object.freeze(frozen);
|
|
@@ -45,7 +45,7 @@ interface PiAssemblySnapshot {
|
|
|
45
45
|
>;
|
|
46
46
|
readonly profile: Pick<
|
|
47
47
|
RuntimeProfile,
|
|
48
|
-
"denyPolicy" | "
|
|
48
|
+
"denyPolicy" | "mcpServers"
|
|
49
49
|
>;
|
|
50
50
|
readonly bindings: {
|
|
51
51
|
readonly platform: Pick<RuntimePlatformPort, "loader">;
|
|
@@ -453,7 +453,7 @@ async function preparePiAssembly(
|
|
|
453
453
|
const extensions = await assemblePiExtensions({
|
|
454
454
|
extensions: snapshot.pi.extensions,
|
|
455
455
|
published: extensionNames,
|
|
456
|
-
enabled:
|
|
456
|
+
enabled: extensionNames,
|
|
457
457
|
authorized: extensionNames,
|
|
458
458
|
load: async (extension) => {
|
|
459
459
|
const workspacePermission =
|
|
@@ -2,7 +2,9 @@ import type { StreamFn } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import {
|
|
3
3
|
createModels,
|
|
4
4
|
createProvider,
|
|
5
|
+
lazyStream,
|
|
5
6
|
type Api,
|
|
7
|
+
type AssistantMessageEvent,
|
|
6
8
|
type Model,
|
|
7
9
|
type MutableModels,
|
|
8
10
|
} from "@earendil-works/pi-ai";
|
|
@@ -24,6 +26,9 @@ import {
|
|
|
24
26
|
import {
|
|
25
27
|
openaiProvider,
|
|
26
28
|
} from "@earendil-works/pi-ai/providers/openai";
|
|
29
|
+
import {
|
|
30
|
+
openrouterProvider,
|
|
31
|
+
} from "@earendil-works/pi-ai/providers/openrouter";
|
|
27
32
|
import {
|
|
28
33
|
googleProvider,
|
|
29
34
|
} from "@earendil-works/pi-ai/providers/google";
|
|
@@ -38,25 +43,64 @@ import type {
|
|
|
38
43
|
|
|
39
44
|
const CATALOGS = {
|
|
40
45
|
"openai-chat": openaiProvider().getModels(),
|
|
46
|
+
"openrouter-chat": openrouterProvider().getModels(),
|
|
41
47
|
"anthropic-messages": anthropicProvider().getModels(),
|
|
42
48
|
"google-generative-ai": googleProvider().getModels(),
|
|
43
49
|
"openai-codex-responses": openaiCodexProvider().getModels(),
|
|
44
50
|
} satisfies Record<RuntimeModelProtocol, readonly Model<Api>[]>;
|
|
45
51
|
|
|
46
52
|
const PROVIDER_MAX_RETRIES = 2;
|
|
53
|
+
const MODEL_STREAM_STALL_TIMEOUT_MS = 120_000;
|
|
54
|
+
|
|
55
|
+
async function* stopStalledModelStream(
|
|
56
|
+
source: AsyncIterable<AssistantMessageEvent>,
|
|
57
|
+
watchdog: AbortController,
|
|
58
|
+
): AsyncGenerator<AssistantMessageEvent> {
|
|
59
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
60
|
+
while (true) {
|
|
61
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
62
|
+
const next = await Promise.race([
|
|
63
|
+
iterator.next(),
|
|
64
|
+
new Promise<never>((_, reject) => {
|
|
65
|
+
timer = setTimeout(() => {
|
|
66
|
+
const error = new Error(
|
|
67
|
+
`Model stream stalled for ${MODEL_STREAM_STALL_TIMEOUT_MS} ms`,
|
|
68
|
+
);
|
|
69
|
+
watchdog.abort(error);
|
|
70
|
+
reject(error);
|
|
71
|
+
}, MODEL_STREAM_STALL_TIMEOUT_MS);
|
|
72
|
+
}),
|
|
73
|
+
]).finally(() => {
|
|
74
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
75
|
+
});
|
|
76
|
+
if (next.done) {
|
|
77
|
+
throw new Error("Model stream ended without a terminal event");
|
|
78
|
+
}
|
|
79
|
+
yield next.value;
|
|
80
|
+
if (next.value.type === "done" || next.value.type === "error") return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
47
83
|
|
|
48
84
|
/**
|
|
49
|
-
* 给模型请求补上可中断的 provider
|
|
85
|
+
* 给模型请求补上可中断的 provider 瞬时错误重试和空闲终止边界。
|
|
50
86
|
*
|
|
51
87
|
* Runtime Turn 和 SubAgent 在把 `Models.streamSimple` 交给 Pi 前调用;显式传入的重试次数优先。
|
|
52
88
|
*
|
|
53
|
-
* Pi 0.83 默认 `maxRetries` 为 0
|
|
89
|
+
* Pi 0.83 默认 `maxRetries` 为 0,且流无事件时会无限等待。这里补 2 次默认重试,
|
|
90
|
+
* 并在连续 120 秒没有模型事件时中止 provider、返回明确失败终态。
|
|
54
91
|
*/
|
|
55
92
|
export function withProviderRetry(streamFn: StreamFn): StreamFn {
|
|
56
93
|
return (model, context, options) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
lazyStream(model, async () => {
|
|
95
|
+
const watchdog = new AbortController();
|
|
96
|
+
const source = await streamFn(model, context, {
|
|
97
|
+
...options,
|
|
98
|
+
signal: options?.signal
|
|
99
|
+
? AbortSignal.any([options.signal, watchdog.signal])
|
|
100
|
+
: watchdog.signal,
|
|
101
|
+
maxRetries: options?.maxRetries ?? PROVIDER_MAX_RETRIES,
|
|
102
|
+
});
|
|
103
|
+
return stopStalledModelStream(source, watchdog);
|
|
60
104
|
});
|
|
61
105
|
}
|
|
62
106
|
|
|
@@ -86,6 +130,7 @@ function providerId(endpoint: RuntimeModelEndpoint, index: number): string {
|
|
|
86
130
|
function apiFor(protocol: RuntimeModelProtocol): Api {
|
|
87
131
|
switch (protocol) {
|
|
88
132
|
case "openai-chat":
|
|
133
|
+
case "openrouter-chat":
|
|
89
134
|
return "openai-completions";
|
|
90
135
|
case "anthropic-messages":
|
|
91
136
|
return "anthropic-messages";
|
|
@@ -99,6 +144,7 @@ function apiFor(protocol: RuntimeModelProtocol): Api {
|
|
|
99
144
|
function piApiFor(protocol: RuntimeModelProtocol) {
|
|
100
145
|
switch (protocol) {
|
|
101
146
|
case "openai-chat":
|
|
147
|
+
case "openrouter-chat":
|
|
102
148
|
return openAICompletionsApi();
|
|
103
149
|
case "anthropic-messages":
|
|
104
150
|
return anthropicMessagesApi();
|
|
@@ -125,12 +171,11 @@ function configuredModel(
|
|
|
125
171
|
`Model is not in Pi's built-in catalog for ${endpoint.protocol}: ${modelId}`,
|
|
126
172
|
);
|
|
127
173
|
}
|
|
128
|
-
// 待确认:当前统一丢弃 catalogModel.compat;现有提交历史没有说明这样做对 Anthropic 兼容参数的影响,修改前需先验证 Pi 的流式请求行为。
|
|
129
174
|
const {
|
|
130
175
|
api: _api,
|
|
131
176
|
provider: _provider,
|
|
132
177
|
baseUrl: _baseUrl,
|
|
133
|
-
compat
|
|
178
|
+
compat,
|
|
134
179
|
headers: catalogHeaders,
|
|
135
180
|
...metadata
|
|
136
181
|
} = catalogModel;
|
|
@@ -143,6 +188,9 @@ function configuredModel(
|
|
|
143
188
|
api: apiFor(endpoint.protocol),
|
|
144
189
|
provider: providerId(endpoint, index),
|
|
145
190
|
baseUrl: endpoint.baseURL,
|
|
191
|
+
...(endpoint.protocol === "openrouter-chat" && compat
|
|
192
|
+
? { compat }
|
|
193
|
+
: {}),
|
|
146
194
|
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
147
195
|
};
|
|
148
196
|
}
|
|
@@ -188,10 +236,10 @@ export function resolvePiApiKey(
|
|
|
188
236
|
* 用当前部署端点完整重建一个已有的 Pi 模型集合。
|
|
189
237
|
*
|
|
190
238
|
* PiRuntimeAdapter 激活新运行快照时调用它;需要复用 MutableModels 的调用方也可以调用,并继续使用返回的同一实例。
|
|
191
|
-
*
|
|
239
|
+
* 该函数在全部新 provider 校验通过后才清空并替换集合,所以失败时旧集合保持可用。
|
|
192
240
|
*
|
|
193
241
|
* Pi 当前用 provider.id 在 Map 中注册和路由请求,因此每个部署端点都必须同时注册唯一 provider、对应模型、认证解析器和协议实现。
|
|
194
|
-
*
|
|
242
|
+
* 替换时先构建新 provider,确保任一未知 modelId 都不会让旧端点失效;校验通过后再清空旧集合,避免配置更新后旧端点继续可用。
|
|
195
243
|
*
|
|
196
244
|
* @returns 传入并已重建完成的 `models` 实例。
|
|
197
245
|
* @throws 当任一部署模型不在 Pi 内置 OpenAI、Anthropic catalog 中时抛出错误。
|
|
@@ -200,9 +248,8 @@ export function configurePiModels(
|
|
|
200
248
|
models: MutableModels,
|
|
201
249
|
provider: RuntimeProviderPort,
|
|
202
250
|
): MutableModels {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
models.setProvider(createProvider({
|
|
251
|
+
const configuredProviders = provider.endpoints.map((endpoint, index) =>
|
|
252
|
+
createProvider({
|
|
206
253
|
id: providerId(endpoint, index),
|
|
207
254
|
name: endpoint.protocol,
|
|
208
255
|
baseUrl: endpoint.baseURL,
|
|
@@ -219,8 +266,12 @@ export function configurePiModels(
|
|
|
219
266
|
configuredModel(endpoint, index, modelId),
|
|
220
267
|
),
|
|
221
268
|
api: piApiFor(endpoint.protocol),
|
|
222
|
-
})
|
|
223
|
-
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
models.clearProviders();
|
|
272
|
+
configuredProviders.forEach((configuredProvider) =>
|
|
273
|
+
models.setProvider(configuredProvider)
|
|
274
|
+
);
|
|
224
275
|
return models;
|
|
225
276
|
}
|
|
226
277
|
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -66,9 +66,9 @@ function candidate<T extends TSchema>(tool: AgentTool<T>): PiToolCandidate {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** Runtime
|
|
69
|
+
/** Runtime base tools, including native web search when the model supports it. */
|
|
70
70
|
export function basePiToolCandidates(
|
|
71
|
-
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
|
}
|
|
@@ -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
|
@@ -837,7 +837,6 @@ class RuntimeBuilder implements RuntimeContributionContext {
|
|
|
837
837
|
denyPolicy: this.denyPolicy,
|
|
838
838
|
enabledSubagents: [...this.enabledSubagents],
|
|
839
839
|
mcpServers: [...this.connectors.values()],
|
|
840
|
-
enabledExtensions: [...this.extensions.keys()],
|
|
841
840
|
executionLevel: this.profile.executionLevel,
|
|
842
841
|
memory: this.memoryProfile,
|
|
843
842
|
});
|
|
@@ -864,12 +863,22 @@ class RuntimeBuilder implements RuntimeContributionContext {
|
|
|
864
863
|
);
|
|
865
864
|
}
|
|
866
865
|
const resolvedModel = resolvePiModel(provider, profile.model);
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
+
}
|
|
873
882
|
for (const candidate of basePiToolCandidates(webSearch)) {
|
|
874
883
|
this.addPiTool(candidate);
|
|
875
884
|
}
|