@springbrand/agent-runtime 0.1.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 +28 -0
- package/src/db/approval.repo.ts +291 -0
- package/src/db/ext-context.repo.ts +34 -0
- package/src/db/index.ts +83 -0
- package/src/db/message-ui.repo.ts +39 -0
- package/src/db/milestone.repo.ts +96 -0
- package/src/db/runtime-event-outbox.repo.ts +89 -0
- package/src/db/schema.ts +164 -0
- package/src/db/settlement.repo.ts +104 -0
- package/src/db/steer.repo.ts +73 -0
- package/src/db/submission.repo.ts +323 -0
- package/src/index.ts +133 -0
- package/src/kernel/approval-lifecycle.ts +552 -0
- package/src/kernel/bindings.ts +898 -0
- package/src/kernel/degradation.ts +15 -0
- package/src/kernel/extensions.ts +108 -0
- package/src/kernel/profile.ts +116 -0
- package/src/kernel/public-contracts.ts +17 -0
- package/src/kernel/receipts.ts +124 -0
- package/src/kernel/recoverable-chat-agent.ts +899 -0
- package/src/kernel/state.ts +76 -0
- package/src/kernel/submission-lifecycle.ts +600 -0
- package/src/layers/context/budget/gate.ts +88 -0
- package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
- package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
- package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
- package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
- package/src/layers/orchestration/temporary-agent/core.ts +152 -0
- package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
- package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
- package/src/lib/artifacts.ts +54 -0
- package/src/lib/egress.ts +44 -0
- package/src/lib/execution-level.ts +27 -0
- package/src/lib/extension-name.ts +18 -0
- package/src/lib/host-actions.ts +57 -0
- package/src/lib/mcp.ts +86 -0
- package/src/lib/model-catalog.ts +7 -0
- package/src/lib/prompt.ts +139 -0
- package/src/lib/telemetry-dev.ts +44 -0
- package/src/pi/assembly/context.ts +510 -0
- package/src/pi/assembly/extensions.ts +661 -0
- package/src/pi/assembly/index.ts +19 -0
- package/src/pi/assembly/snapshot.ts +200 -0
- package/src/pi/message/contract.ts +8 -0
- package/src/pi/message/conversion.ts +73 -0
- package/src/pi/message/index.ts +3 -0
- package/src/pi/message/projection.ts +604 -0
- package/src/pi/runtime-adapter/assembly.ts +552 -0
- package/src/pi/runtime-adapter/execution.ts +683 -0
- package/src/pi/runtime-adapter/index.ts +232 -0
- package/src/pi/runtime-adapter/models.ts +243 -0
- package/src/pi/runtime-adapter/recovery.ts +805 -0
- package/src/pi/runtime-adapter/transcript.ts +825 -0
- package/src/pi/session/index.ts +24 -0
- package/src/pi/session/storage.ts +353 -0
- package/src/pi/tool/ai-adapter.ts +100 -0
- package/src/pi/tool/base.ts +110 -0
- package/src/pi/tool/compiler.ts +444 -0
- package/src/pi/tool/core-host.ts +48 -0
- package/src/pi/tool/core.ts +251 -0
- package/src/pi/tool/index.ts +32 -0
- package/src/pi/tool/mcp.ts +319 -0
- package/src/pi/tool/schedule.ts +198 -0
- package/src/pi/tool/skill.ts +455 -0
- package/src/pi/tool/subagent.ts +148 -0
- package/src/pi/tool/web-search/api.ts +1292 -0
- package/src/pi/tool/web-search/index.ts +2 -0
- package/src/pi/tool/web-search/web-search.ts +127 -0
- package/src/pi/tool/workspace-sandbox.ts +664 -0
- package/src/pi/turn/approval.ts +181 -0
- package/src/pi/turn/index.ts +62 -0
- package/src/pi/turn/tool-recovery.ts +792 -0
- package/src/plugins.ts +1024 -0
- package/src/runtime-agent.ts +654 -0
- package/src/runtime.ts +2880 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSessionContext,
|
|
3
|
+
compact as compactPiTranscript,
|
|
4
|
+
DEFAULT_COMPACTION_SETTINGS,
|
|
5
|
+
estimateContextTokens,
|
|
6
|
+
prepareCompaction,
|
|
7
|
+
type AgentMessage,
|
|
8
|
+
type CompactionEntry,
|
|
9
|
+
type SessionTreeEntry,
|
|
10
|
+
} from "@earendil-works/pi-agent-core";
|
|
11
|
+
import type {
|
|
12
|
+
AssistantMessage,
|
|
13
|
+
Model,
|
|
14
|
+
Models,
|
|
15
|
+
RetryPolicy,
|
|
16
|
+
Usage,
|
|
17
|
+
} from "@earendil-works/pi-ai";
|
|
18
|
+
import { estimateStringTokens } from "agents/experimental/memory/utils";
|
|
19
|
+
import type { RuntimeMemoryPort } from "../../kernel/bindings";
|
|
20
|
+
import type { RuntimeDegradation } from "../../kernel/degradation";
|
|
21
|
+
import type { RuntimeMemoryProfile } from "../../kernel/profile";
|
|
22
|
+
import type { PiExtensionContextContribution } from "./extensions";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 压缩这一次 LLM 调用的重试策略。
|
|
26
|
+
*
|
|
27
|
+
* 为什么必须显式传:pi 的 `retryAssistantCall` 是 opt-in 的 —— d.ts 原话
|
|
28
|
+
* 「When `policy` is undefined or disabled, the first response is returned
|
|
29
|
+
* unchanged」。升到 0.83 不传这个参数,行为与 0.80.6 完全一样(零重试)。
|
|
30
|
+
*
|
|
31
|
+
* 为什么压缩值得重试:压缩失败不是「少一次摘要」,是**下一轮直接撞 provider
|
|
32
|
+
* 的上下文窗口上限、整个 turn 挂掉**。而它跑在 Workers 里,瞬时网络错误是常态。
|
|
33
|
+
*
|
|
34
|
+
* 为什么只有 2 次、退避从 500ms 起:压缩发生在 turn 的关键路径上,DO 的墙钟
|
|
35
|
+
* 是真金白银(等 LLM 不占 CPU 配额但占用户的等待)。2 次退避总计约 1.5s,
|
|
36
|
+
* 够穿过瞬时抖动,又不至于把一次失败拖成用户可感知的卡顿。
|
|
37
|
+
*/
|
|
38
|
+
const COMPACTION_RETRY: RetryPolicy = {
|
|
39
|
+
enabled: true,
|
|
40
|
+
maxRetries: 2,
|
|
41
|
+
baseDelayMs: 500,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const COLD_MEMORY_INDEX = "/shared/memories/MEMORY.md";
|
|
45
|
+
const TRUNCATION_MARKER = "\n[truncated to memory budget]";
|
|
46
|
+
|
|
47
|
+
function sumUsage(responses: readonly AssistantMessage[]): Usage | undefined {
|
|
48
|
+
const first = responses[0];
|
|
49
|
+
if (!first) return undefined;
|
|
50
|
+
|
|
51
|
+
const total = structuredClone(first.usage);
|
|
52
|
+
for (const response of responses.slice(1)) {
|
|
53
|
+
const usage = response.usage;
|
|
54
|
+
total.input += usage.input;
|
|
55
|
+
total.output += usage.output;
|
|
56
|
+
total.cacheRead += usage.cacheRead;
|
|
57
|
+
total.cacheWrite += usage.cacheWrite;
|
|
58
|
+
if (usage.cacheWrite1h !== undefined) {
|
|
59
|
+
total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
|
|
60
|
+
}
|
|
61
|
+
if (usage.reasoning !== undefined) {
|
|
62
|
+
total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
|
|
63
|
+
}
|
|
64
|
+
total.totalTokens += usage.totalTokens;
|
|
65
|
+
total.cost.input += usage.cost.input;
|
|
66
|
+
total.cost.output += usage.cost.output;
|
|
67
|
+
total.cost.cacheRead += usage.cost.cacheRead;
|
|
68
|
+
total.cost.cacheWrite += usage.cost.cacheWrite;
|
|
69
|
+
total.cost.total += usage.cost.total;
|
|
70
|
+
}
|
|
71
|
+
return total;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// #region Transcript compaction
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 把当前 Pi 会话分支压缩成下一轮可以直接使用的消息。
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Transcript 在每轮开始前传入当前分支、模型和 token 阈值,并在返回压缩条目后自行追加到 Session。
|
|
81
|
+
*
|
|
82
|
+
* 这里先用 Pi 的 Session 管线重建上下文,任何失败都退回未压缩消息,以避免压缩失败损坏持久化会话。
|
|
83
|
+
*
|
|
84
|
+
* 核心术语见包入口 `index.ts`。
|
|
85
|
+
*/
|
|
86
|
+
export async function compactPiContext(input: Readonly<{
|
|
87
|
+
branch: readonly SessionTreeEntry[];
|
|
88
|
+
compactAfterTokens: number;
|
|
89
|
+
models: Models;
|
|
90
|
+
model: Model<any>;
|
|
91
|
+
apiKey?: string;
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}>): Promise<{
|
|
94
|
+
messages: AgentMessage[];
|
|
95
|
+
compactionEntry?: Omit<
|
|
96
|
+
CompactionEntry,
|
|
97
|
+
"id" | "parentId" | "timestamp"
|
|
98
|
+
>;
|
|
99
|
+
modelUsage?: {
|
|
100
|
+
api: string;
|
|
101
|
+
provider: string;
|
|
102
|
+
model: string;
|
|
103
|
+
responseModel?: string;
|
|
104
|
+
stopReason?: string;
|
|
105
|
+
usage: Usage;
|
|
106
|
+
};
|
|
107
|
+
didCompact: boolean;
|
|
108
|
+
degraded: boolean;
|
|
109
|
+
}> {
|
|
110
|
+
const branch = [...input.branch];
|
|
111
|
+
let currentMessages = branch.flatMap((entry) =>
|
|
112
|
+
entry.type === "message" ? [entry.message] : []
|
|
113
|
+
);
|
|
114
|
+
try {
|
|
115
|
+
currentMessages = buildSessionContext(branch).messages;
|
|
116
|
+
const settings = DEFAULT_COMPACTION_SETTINGS;
|
|
117
|
+
if (
|
|
118
|
+
!settings.enabled ||
|
|
119
|
+
estimateContextTokens(currentMessages).tokens <=
|
|
120
|
+
input.compactAfterTokens
|
|
121
|
+
) {
|
|
122
|
+
return {
|
|
123
|
+
messages: currentMessages,
|
|
124
|
+
didCompact: false,
|
|
125
|
+
degraded: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const preparation = prepareCompaction(branch, settings);
|
|
130
|
+
if (!preparation.ok) {
|
|
131
|
+
return {
|
|
132
|
+
messages: currentMessages,
|
|
133
|
+
didCompact: false,
|
|
134
|
+
degraded: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (!preparation.value) {
|
|
138
|
+
return {
|
|
139
|
+
messages: currentMessages,
|
|
140
|
+
didCompact: false,
|
|
141
|
+
degraded: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const responses: AssistantMessage[] = [];
|
|
146
|
+
const models = {
|
|
147
|
+
completeSimple: async (
|
|
148
|
+
model: Model<any>,
|
|
149
|
+
context: Parameters<Models["completeSimple"]>[1],
|
|
150
|
+
options?: Parameters<Models["completeSimple"]>[2],
|
|
151
|
+
) => {
|
|
152
|
+
const response = await input.models.completeSimple(model, context, {
|
|
153
|
+
...options,
|
|
154
|
+
...(input.apiKey ? { apiKey: input.apiKey } : {}),
|
|
155
|
+
});
|
|
156
|
+
responses.push(response);
|
|
157
|
+
return response;
|
|
158
|
+
},
|
|
159
|
+
} as Models;
|
|
160
|
+
const compacted = await compactPiTranscript(
|
|
161
|
+
preparation.value,
|
|
162
|
+
models,
|
|
163
|
+
input.model,
|
|
164
|
+
undefined,
|
|
165
|
+
input.signal,
|
|
166
|
+
undefined,
|
|
167
|
+
COMPACTION_RETRY,
|
|
168
|
+
);
|
|
169
|
+
if (!compacted.ok) {
|
|
170
|
+
return {
|
|
171
|
+
messages: currentMessages,
|
|
172
|
+
didCompact: false,
|
|
173
|
+
degraded: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const compactionEntry = {
|
|
178
|
+
type: "compaction",
|
|
179
|
+
...compacted.value,
|
|
180
|
+
} satisfies Omit<
|
|
181
|
+
CompactionEntry,
|
|
182
|
+
"id" | "parentId" | "timestamp"
|
|
183
|
+
>;
|
|
184
|
+
const previewEntry: CompactionEntry = {
|
|
185
|
+
...compactionEntry,
|
|
186
|
+
id: `compaction-preview:${branch[branch.length - 1]?.id ?? "root"}`,
|
|
187
|
+
parentId: branch[branch.length - 1]?.id ?? null,
|
|
188
|
+
timestamp: new Date().toISOString(),
|
|
189
|
+
};
|
|
190
|
+
const firstResponse = responses[0];
|
|
191
|
+
const responseModel = firstResponse?.responseModel && responses.every(
|
|
192
|
+
(response) => response.responseModel === firstResponse.responseModel,
|
|
193
|
+
)
|
|
194
|
+
? firstResponse.responseModel
|
|
195
|
+
: undefined;
|
|
196
|
+
const stopReason = firstResponse && responses.every(
|
|
197
|
+
(response) => response.stopReason === firstResponse.stopReason,
|
|
198
|
+
)
|
|
199
|
+
? firstResponse.stopReason
|
|
200
|
+
: undefined;
|
|
201
|
+
const usage = sumUsage(responses);
|
|
202
|
+
return {
|
|
203
|
+
messages: buildSessionContext(
|
|
204
|
+
[...branch, previewEntry],
|
|
205
|
+
).messages,
|
|
206
|
+
compactionEntry,
|
|
207
|
+
...(firstResponse && usage
|
|
208
|
+
? {
|
|
209
|
+
modelUsage: {
|
|
210
|
+
api: firstResponse.api,
|
|
211
|
+
provider: firstResponse.provider,
|
|
212
|
+
model: firstResponse.model,
|
|
213
|
+
...(responseModel ? { responseModel } : {}),
|
|
214
|
+
...(stopReason ? { stopReason } : {}),
|
|
215
|
+
usage,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
219
|
+
didCompact: true,
|
|
220
|
+
degraded: false,
|
|
221
|
+
};
|
|
222
|
+
} catch {
|
|
223
|
+
return {
|
|
224
|
+
messages: currentMessages,
|
|
225
|
+
didCompact: false,
|
|
226
|
+
degraded: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 作用:把一段上下文裁到指定 token 预算内。
|
|
232
|
+
// 调用:内存和 Extension 上下文在拼入 system prompt 前调用。
|
|
233
|
+
// 原因:二分搜索每次都用同一个 token 估算器验证,改成字符数截断会突破真实预算。
|
|
234
|
+
function truncateToBudget(content: string, maxTokens: number): string {
|
|
235
|
+
if (maxTokens <= 0) return "";
|
|
236
|
+
if (estimateStringTokens(content) <= maxTokens) return content;
|
|
237
|
+
|
|
238
|
+
const marker =
|
|
239
|
+
estimateStringTokens(TRUNCATION_MARKER) <= maxTokens
|
|
240
|
+
? TRUNCATION_MARKER
|
|
241
|
+
: "";
|
|
242
|
+
const characters = [...content];
|
|
243
|
+
let low = 0;
|
|
244
|
+
let high = characters.length;
|
|
245
|
+
while (low < high) {
|
|
246
|
+
const middle = Math.ceil((low + high) / 2);
|
|
247
|
+
const candidate = `${characters.slice(0, middle).join("")}${marker}`;
|
|
248
|
+
if (estimateStringTokens(candidate) <= maxTokens) {
|
|
249
|
+
low = middle;
|
|
250
|
+
} else {
|
|
251
|
+
high = middle - 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return `${characters.slice(0, low).join("")}${marker}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 作用:给一段动态上下文加上人可读的来源标题。
|
|
259
|
+
// 调用:内存和 Extension 读取器在生成 system prompt 块时调用。
|
|
260
|
+
// 原因:固定分隔格式能保留来源边界,不能随意拼成无标记文本。
|
|
261
|
+
function renderBlock(
|
|
262
|
+
label: string,
|
|
263
|
+
description: string,
|
|
264
|
+
content: string,
|
|
265
|
+
): string {
|
|
266
|
+
const separator = "═".repeat(46);
|
|
267
|
+
return `${separator}\n${label} (${description})\n${separator}\n${content}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// #endregion
|
|
271
|
+
|
|
272
|
+
// #region System context sources
|
|
273
|
+
|
|
274
|
+
// 作用:同时收集当前 Session 内存和用户管理的冷内存索引。
|
|
275
|
+
// 调用:系统上下文装配在 Runtime 锁定当前配置时调用。
|
|
276
|
+
// 原因:内存是可选能力,各来源失败只记 degradation,不应阻断整个 Runtime。
|
|
277
|
+
async function readMemory(
|
|
278
|
+
profile: RuntimeMemoryProfile,
|
|
279
|
+
port: Pick<RuntimeMemoryPort, "get"> | undefined,
|
|
280
|
+
workspace: { readFile(path: string): Promise<string | null> } | undefined,
|
|
281
|
+
): Promise<{
|
|
282
|
+
blocks: string[];
|
|
283
|
+
degradations: RuntimeDegradation[];
|
|
284
|
+
}> {
|
|
285
|
+
if (!profile.enabled) return { blocks: [], degradations: [] };
|
|
286
|
+
|
|
287
|
+
const blocks: string[] = [];
|
|
288
|
+
const degradations: RuntimeDegradation[] = [];
|
|
289
|
+
const hot = port
|
|
290
|
+
? await Promise.allSettled([
|
|
291
|
+
port.get("memory"),
|
|
292
|
+
port.get("preferences"),
|
|
293
|
+
])
|
|
294
|
+
: [];
|
|
295
|
+
|
|
296
|
+
if (!port) {
|
|
297
|
+
degradations.push({
|
|
298
|
+
capability: "memory",
|
|
299
|
+
reason: "unavailable",
|
|
300
|
+
detail: "hot",
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
const memory = hot[0];
|
|
304
|
+
const preferences = hot[1];
|
|
305
|
+
let hotUnavailable = false;
|
|
306
|
+
if (memory.status === "fulfilled" && memory.value) {
|
|
307
|
+
blocks.push(
|
|
308
|
+
renderBlock(
|
|
309
|
+
"MEMORY",
|
|
310
|
+
"Working facts for the current Session only",
|
|
311
|
+
truncateToBudget(memory.value, profile.memoryTokens),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
} else if (memory.status === "rejected") {
|
|
315
|
+
hotUnavailable = true;
|
|
316
|
+
}
|
|
317
|
+
if (preferences.status === "fulfilled" && preferences.value) {
|
|
318
|
+
blocks.push(
|
|
319
|
+
renderBlock(
|
|
320
|
+
"PREFERENCES",
|
|
321
|
+
"Working preferences for the current Session only",
|
|
322
|
+
truncateToBudget(
|
|
323
|
+
preferences.value,
|
|
324
|
+
profile.preferencesTokens,
|
|
325
|
+
),
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
} else if (preferences.status === "rejected") {
|
|
329
|
+
hotUnavailable = true;
|
|
330
|
+
}
|
|
331
|
+
if (hotUnavailable) {
|
|
332
|
+
degradations.push({
|
|
333
|
+
capability: "memory",
|
|
334
|
+
reason: "unavailable",
|
|
335
|
+
detail: "hot",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!workspace) {
|
|
341
|
+
degradations.push({
|
|
342
|
+
capability: "memory",
|
|
343
|
+
reason: "unavailable",
|
|
344
|
+
detail: "cold",
|
|
345
|
+
});
|
|
346
|
+
} else {
|
|
347
|
+
try {
|
|
348
|
+
const index = await workspace.readFile(COLD_MEMORY_INDEX);
|
|
349
|
+
if (index) {
|
|
350
|
+
blocks.push(
|
|
351
|
+
renderBlock(
|
|
352
|
+
"MEMORY_INDEX",
|
|
353
|
+
"Index of user-managed cold memory under /shared/memories; entries are read on demand",
|
|
354
|
+
index,
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
degradations.push({
|
|
360
|
+
capability: "memory",
|
|
361
|
+
reason: "unavailable",
|
|
362
|
+
detail: "cold",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { blocks, degradations };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 作用:把已授权 Skill 的持久化 catalog 整理成提示词。
|
|
371
|
+
// 调用:系统上下文装配在需要告诉模型当前可用 Skill 时调用。
|
|
372
|
+
// 原因:catalog 已在发布和绑定时固定,装配期不得为此访问远端内容来源。
|
|
373
|
+
function renderSkillCatalog(
|
|
374
|
+
bindings: readonly {
|
|
375
|
+
name: string;
|
|
376
|
+
description: string;
|
|
377
|
+
}[],
|
|
378
|
+
): string | null {
|
|
379
|
+
if (bindings.length === 0) return null;
|
|
380
|
+
|
|
381
|
+
return [
|
|
382
|
+
"AUTHORIZED SKILLS",
|
|
383
|
+
"Use an authorized Skill when its description matches. Read its instructions or files only through the authorized Skill tool.",
|
|
384
|
+
"",
|
|
385
|
+
...bindings.map(
|
|
386
|
+
({ name, description }) => `- ${name}: ${description}`,
|
|
387
|
+
),
|
|
388
|
+
].join("\n");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 作用:读取 Extension 声明的上下文并按各自预算截断。
|
|
392
|
+
// 调用:系统上下文装配在已加载 Extension 提供了上下文声明时调用。
|
|
393
|
+
// 原因:各块使用 `Promise.allSettled` 隔离失败,不能让一个 Extension 拖垮整份 system prompt。
|
|
394
|
+
async function readExtensionContexts(
|
|
395
|
+
contributions: readonly PiExtensionContextContribution[],
|
|
396
|
+
read:
|
|
397
|
+
| ((
|
|
398
|
+
contribution: PiExtensionContextContribution,
|
|
399
|
+
) => Promise<string | null>)
|
|
400
|
+
| undefined,
|
|
401
|
+
): Promise<{
|
|
402
|
+
blocks: string[];
|
|
403
|
+
degradations: RuntimeDegradation[];
|
|
404
|
+
}> {
|
|
405
|
+
if (contributions.length === 0) {
|
|
406
|
+
return { blocks: [], degradations: [] };
|
|
407
|
+
}
|
|
408
|
+
if (!read) {
|
|
409
|
+
return {
|
|
410
|
+
blocks: [],
|
|
411
|
+
degradations: [{
|
|
412
|
+
capability: "extension",
|
|
413
|
+
reason: "unavailable",
|
|
414
|
+
detail: "context",
|
|
415
|
+
}],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const results = await Promise.allSettled(
|
|
420
|
+
contributions.map((contribution) => read(contribution)),
|
|
421
|
+
);
|
|
422
|
+
const blocks: string[] = [];
|
|
423
|
+
const degradations: RuntimeDegradation[] = [];
|
|
424
|
+
|
|
425
|
+
results.forEach((result, index) => {
|
|
426
|
+
const contribution = contributions[index]!;
|
|
427
|
+
if (result.status === "rejected") {
|
|
428
|
+
degradations.push({
|
|
429
|
+
capability: "extension",
|
|
430
|
+
reason: "unavailable",
|
|
431
|
+
detail: contribution.label,
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (!result.value) return;
|
|
436
|
+
const content = contribution.maxTokens === undefined
|
|
437
|
+
? result.value
|
|
438
|
+
: truncateToBudget(result.value, contribution.maxTokens);
|
|
439
|
+
if (!content) return;
|
|
440
|
+
blocks.push(
|
|
441
|
+
renderBlock(
|
|
442
|
+
contribution.label,
|
|
443
|
+
contribution.description
|
|
444
|
+
?? `Context contributed by ${contribution.extensionName}`,
|
|
445
|
+
content,
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
return { blocks, degradations };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// #endregion
|
|
454
|
+
|
|
455
|
+
// #region Public assembly
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 从已授权的 Runtime 能力重建 Pi 本轮使用的 system prompt。
|
|
459
|
+
*
|
|
460
|
+
* @remarks
|
|
461
|
+
* Runtime 在锁定每轮配置时调用,调用方只应传入 Snapshot 已选中的 Port、Skill 和 Extension 上下文。
|
|
462
|
+
*
|
|
463
|
+
* 上下文每次重建而不写入 Pi 消息,这使刷新可选能力不会污染持久化 transcript;各可选来源失败只通过 degradation 返回。
|
|
464
|
+
*
|
|
465
|
+
* 核心术语见包入口 `index.ts`。
|
|
466
|
+
*/
|
|
467
|
+
export async function assemblePiSystemContext(input: Readonly<{
|
|
468
|
+
baseSystemPrompt: string;
|
|
469
|
+
memory: RuntimeMemoryProfile;
|
|
470
|
+
memoryPort?: Pick<RuntimeMemoryPort, "get">;
|
|
471
|
+
workspace?: { readFile(path: string): Promise<string | null> };
|
|
472
|
+
skills: readonly {
|
|
473
|
+
name: string;
|
|
474
|
+
description: string;
|
|
475
|
+
}[];
|
|
476
|
+
extensionContexts?: readonly PiExtensionContextContribution[];
|
|
477
|
+
readExtensionContext?: (
|
|
478
|
+
contribution: PiExtensionContextContribution,
|
|
479
|
+
) => Promise<string | null>;
|
|
480
|
+
}>): Promise<Readonly<{
|
|
481
|
+
systemPrompt: string;
|
|
482
|
+
compactAfterTokens: number;
|
|
483
|
+
degradations: readonly RuntimeDegradation[];
|
|
484
|
+
}>> {
|
|
485
|
+
const [memory, extensions] = await Promise.all([
|
|
486
|
+
readMemory(input.memory, input.memoryPort, input.workspace),
|
|
487
|
+
readExtensionContexts(
|
|
488
|
+
input.extensionContexts ?? [],
|
|
489
|
+
input.readExtensionContext,
|
|
490
|
+
),
|
|
491
|
+
]);
|
|
492
|
+
const skills = renderSkillCatalog(input.skills);
|
|
493
|
+
const parts = [
|
|
494
|
+
input.baseSystemPrompt,
|
|
495
|
+
...memory.blocks,
|
|
496
|
+
...(skills ? [skills] : []),
|
|
497
|
+
...extensions.blocks,
|
|
498
|
+
].filter(Boolean);
|
|
499
|
+
|
|
500
|
+
return Object.freeze({
|
|
501
|
+
systemPrompt: parts.join("\n\n"),
|
|
502
|
+
compactAfterTokens: input.memory.compactAfterTokens,
|
|
503
|
+
degradations: Object.freeze([
|
|
504
|
+
...memory.degradations,
|
|
505
|
+
...extensions.degradations,
|
|
506
|
+
]),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// #endregion
|