@springbrand/agent-runtime 0.2.0-alpha.68 → 0.2.0-alpha.70
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 +3 -3
- package/src/adapter/cloudflare/subagent/runner.ts +23 -22
- package/src/adapter/cloudflare/subagent/tools.ts +1 -1
- package/src/adapter/cloudflare/universal-agent/preparation.ts +0 -7
- package/src/db/schema.ts +0 -12
- package/src/db/submission-admission.repo.ts +5 -20
- package/src/db/submission.repo.ts +5 -21
- package/src/kernel/bindings.ts +8 -7
- package/src/kernel/durable-lifecycle.ts +0 -9
- package/src/kernel/profile.ts +3 -0
- package/src/kernel/public-contracts.ts +0 -1
- package/src/kernel/subagent-runtime.ts +2 -12
- package/src/kernel/submission-authority.ts +4 -24
- package/src/model-usage.ts +52 -0
- package/src/pi/assembly/context.ts +26 -60
- package/src/pi/runtime-adapter/assembly.ts +6 -2
- package/src/pi/runtime-adapter/execution.ts +78 -114
- package/src/pi/runtime-adapter/index.ts +3 -4
- package/src/pi/runtime-adapter/transcript.ts +31 -46
- package/src/pi/session/index.ts +22 -22
- package/src/pi/session/migration.ts +646 -0
- package/src/pi/session/storage.ts +523 -311
- package/src/pi/tool/compiler.ts +2 -2
- package/src/pi/tool/core-host.ts +6 -0
- package/src/pi/tool/mcp.ts +23 -15
- package/src/pi/tool/subagent.ts +0 -9
- package/src/pi/tool/workspace-state-connector.ts +7 -6
- package/src/runtime-agent-context.ts +2 -0
- package/src/runtime-agent.ts +24 -0
- package/src/runtime-assembler.ts +13 -2
- package/src/runtime-definition.ts +2 -0
- package/src/runtime.ts +30 -83
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { summarizeModelUsage } from "../../model-usage";
|
|
2
|
+
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/pi-agent-core/harness/context";
|
|
1
3
|
import {
|
|
2
4
|
buildSessionContext,
|
|
3
5
|
compact as compactPiTranscript,
|
|
@@ -7,7 +9,7 @@ import {
|
|
|
7
9
|
prepareCompaction,
|
|
8
10
|
type AgentMessage,
|
|
9
11
|
type CompactionEntry,
|
|
10
|
-
type
|
|
12
|
+
type Entry,
|
|
11
13
|
} from "@earendil-works/pi-agent-core";
|
|
12
14
|
import type {
|
|
13
15
|
AssistantMessage,
|
|
@@ -45,31 +47,9 @@ const COMPACTION_RETRY: RetryPolicy = {
|
|
|
45
47
|
const COLD_MEMORY_INDEX = "/userspace/memories/MEMORY.md";
|
|
46
48
|
const TRUNCATION_MARKER = "\n[truncated to memory budget]";
|
|
47
49
|
|
|
48
|
-
function
|
|
49
|
-
const first = responses
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const total = structuredClone(first.usage);
|
|
53
|
-
for (const response of responses.slice(1)) {
|
|
54
|
-
const usage = response.usage;
|
|
55
|
-
total.input += usage.input;
|
|
56
|
-
total.output += usage.output;
|
|
57
|
-
total.cacheRead += usage.cacheRead;
|
|
58
|
-
total.cacheWrite += usage.cacheWrite;
|
|
59
|
-
if (usage.cacheWrite1h !== undefined) {
|
|
60
|
-
total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
|
|
61
|
-
}
|
|
62
|
-
if (usage.reasoning !== undefined) {
|
|
63
|
-
total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
|
|
64
|
-
}
|
|
65
|
-
total.totalTokens += usage.totalTokens;
|
|
66
|
-
total.cost.input += usage.cost.input;
|
|
67
|
-
total.cost.output += usage.cost.output;
|
|
68
|
-
total.cost.cacheRead += usage.cost.cacheRead;
|
|
69
|
-
total.cost.cacheWrite += usage.cost.cacheWrite;
|
|
70
|
-
total.cost.total += usage.cost.total;
|
|
71
|
-
}
|
|
72
|
-
return total;
|
|
50
|
+
function compactionUsage(responses: readonly AssistantMessage[], requested: Model<any>) {
|
|
51
|
+
const [first, ...remaining] = responses;
|
|
52
|
+
return first ? { modelUsage: summarizeModelUsage(requested, [first, ...remaining]) } : {};
|
|
73
53
|
}
|
|
74
54
|
|
|
75
55
|
// #region Transcript compaction
|
|
@@ -85,7 +65,7 @@ function sumUsage(responses: readonly AssistantMessage[]): Usage | undefined {
|
|
|
85
65
|
* 核心术语见包入口 `index.ts`。
|
|
86
66
|
*/
|
|
87
67
|
export async function compactPiContext(input: Readonly<{
|
|
88
|
-
branch: readonly
|
|
68
|
+
branch: readonly Entry[];
|
|
89
69
|
compactAfterTokens: number;
|
|
90
70
|
force?: boolean;
|
|
91
71
|
models: Models;
|
|
@@ -95,7 +75,7 @@ export async function compactPiContext(input: Readonly<{
|
|
|
95
75
|
messages: AgentMessage[];
|
|
96
76
|
compactionEntry?: Omit<
|
|
97
77
|
CompactionEntry,
|
|
98
|
-
"id" | "parentId" | "timestamp"
|
|
78
|
+
"id" | "parentId" | "seq" | "timestamp"
|
|
99
79
|
>;
|
|
100
80
|
modelUsage?: {
|
|
101
81
|
api: string;
|
|
@@ -108,12 +88,14 @@ export async function compactPiContext(input: Readonly<{
|
|
|
108
88
|
didCompact: boolean;
|
|
109
89
|
degraded: boolean;
|
|
110
90
|
}> {
|
|
91
|
+
const responses: AssistantMessage[] = [];
|
|
111
92
|
const branch = [...input.branch];
|
|
93
|
+
const context = input.signal ? withAbortSignal(input.signal, BACKGROUND_CONTEXT) : BACKGROUND_CONTEXT;
|
|
112
94
|
let currentMessages = branch.flatMap((entry) =>
|
|
113
95
|
entry.type === "message" ? [entry.message] : []
|
|
114
96
|
);
|
|
115
97
|
try {
|
|
116
|
-
currentMessages = buildSessionContext(branch)
|
|
98
|
+
currentMessages = await buildSessionContext(branch, undefined, context);
|
|
117
99
|
const settings = DEFAULT_COMPACTION_SETTINGS;
|
|
118
100
|
if (
|
|
119
101
|
!settings.enabled ||
|
|
@@ -133,6 +115,7 @@ export async function compactPiContext(input: Readonly<{
|
|
|
133
115
|
messages: currentMessages,
|
|
134
116
|
didCompact: false,
|
|
135
117
|
degraded: true,
|
|
118
|
+
...compactionUsage(responses, input.model),
|
|
136
119
|
};
|
|
137
120
|
}
|
|
138
121
|
if (!preparation.value) {
|
|
@@ -143,7 +126,6 @@ export async function compactPiContext(input: Readonly<{
|
|
|
143
126
|
};
|
|
144
127
|
}
|
|
145
128
|
|
|
146
|
-
const responses: AssistantMessage[] = [];
|
|
147
129
|
const models = {
|
|
148
130
|
completeSimple: async (
|
|
149
131
|
model: Model<any>,
|
|
@@ -152,6 +134,7 @@ export async function compactPiContext(input: Readonly<{
|
|
|
152
134
|
) => {
|
|
153
135
|
const response = await input.models.completeSimple(model, context, options);
|
|
154
136
|
responses.push(response);
|
|
137
|
+
input.signal?.throwIfAborted();
|
|
155
138
|
return response;
|
|
156
139
|
},
|
|
157
140
|
} as Models;
|
|
@@ -160,46 +143,38 @@ export async function compactPiContext(input: Readonly<{
|
|
|
160
143
|
models,
|
|
161
144
|
input.model,
|
|
162
145
|
undefined,
|
|
163
|
-
input.signal,
|
|
164
146
|
undefined,
|
|
165
147
|
COMPACTION_RETRY,
|
|
148
|
+
undefined,
|
|
149
|
+
context,
|
|
166
150
|
);
|
|
167
151
|
if (!compacted.ok) {
|
|
168
152
|
return {
|
|
169
153
|
messages: currentMessages,
|
|
170
154
|
didCompact: false,
|
|
171
155
|
degraded: true,
|
|
156
|
+
...compactionUsage(responses, input.model),
|
|
172
157
|
};
|
|
173
158
|
}
|
|
174
159
|
|
|
175
160
|
const compactionEntry = {
|
|
176
161
|
type: "compaction",
|
|
162
|
+
fromHook: false,
|
|
177
163
|
...compacted.value,
|
|
178
164
|
} satisfies Omit<
|
|
179
165
|
CompactionEntry,
|
|
180
|
-
"id" | "parentId" | "timestamp"
|
|
166
|
+
"id" | "parentId" | "seq" | "timestamp"
|
|
181
167
|
>;
|
|
182
168
|
const previewEntry: CompactionEntry = {
|
|
183
169
|
...compactionEntry,
|
|
184
170
|
id: `compaction-preview:${branch[branch.length - 1]?.id ?? "root"}`,
|
|
185
171
|
parentId: branch[branch.length - 1]?.id ?? null,
|
|
186
|
-
timestamp:
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
seq: (branch.at(-1)?.seq ?? 0) + 1,
|
|
187
174
|
};
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
192
|
-
? firstResponse.responseModel
|
|
193
|
-
: undefined;
|
|
194
|
-
const stopReason = firstResponse && responses.every(
|
|
195
|
-
(response) => response.stopReason === firstResponse.stopReason,
|
|
196
|
-
)
|
|
197
|
-
? firstResponse.stopReason
|
|
198
|
-
: undefined;
|
|
199
|
-
const usage = sumUsage(responses);
|
|
200
|
-
const messages = buildSessionContext(
|
|
201
|
-
[...branch, previewEntry],
|
|
202
|
-
).messages;
|
|
175
|
+
const messages = await buildSessionContext(
|
|
176
|
+
[...branch, previewEntry], undefined, context,
|
|
177
|
+
);
|
|
203
178
|
if (
|
|
204
179
|
input.force &&
|
|
205
180
|
messages.reduce((total, message) => total + estimateTokens(message), 0) >=
|
|
@@ -212,23 +187,13 @@ export async function compactPiContext(input: Readonly<{
|
|
|
212
187
|
messages: currentMessages,
|
|
213
188
|
didCompact: false,
|
|
214
189
|
degraded: true,
|
|
190
|
+
...compactionUsage(responses, input.model),
|
|
215
191
|
};
|
|
216
192
|
}
|
|
217
193
|
return {
|
|
218
194
|
messages,
|
|
219
195
|
compactionEntry,
|
|
220
|
-
...(
|
|
221
|
-
? {
|
|
222
|
-
modelUsage: {
|
|
223
|
-
api: firstResponse.api,
|
|
224
|
-
provider: firstResponse.provider,
|
|
225
|
-
model: firstResponse.model,
|
|
226
|
-
...(responseModel ? { responseModel } : {}),
|
|
227
|
-
...(stopReason ? { stopReason } : {}),
|
|
228
|
-
usage,
|
|
229
|
-
},
|
|
230
|
-
}
|
|
231
|
-
: {}),
|
|
196
|
+
...compactionUsage(responses, input.model),
|
|
232
197
|
didCompact: true,
|
|
233
198
|
degraded: false,
|
|
234
199
|
};
|
|
@@ -237,6 +202,7 @@ export async function compactPiContext(input: Readonly<{
|
|
|
237
202
|
messages: currentMessages,
|
|
238
203
|
didCompact: false,
|
|
239
204
|
degraded: true,
|
|
205
|
+
...compactionUsage(responses, input.model),
|
|
240
206
|
};
|
|
241
207
|
}
|
|
242
208
|
}
|
|
@@ -51,6 +51,7 @@ interface PiAssemblySnapshot {
|
|
|
51
51
|
readonly bindings: {
|
|
52
52
|
readonly platform: Pick<RuntimePlatformPort, "loader">;
|
|
53
53
|
readonly workspace?: RuntimeBindings["workspace"];
|
|
54
|
+
readonly mcp?: RuntimeBindings["mcp"];
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -390,10 +391,13 @@ async function preparePiAssembly(
|
|
|
390
391
|
...(options.gatewaySession
|
|
391
392
|
? createPiGatewayToolCandidates(options.gatewaySession)
|
|
392
393
|
: []),
|
|
393
|
-
...createPiMcpToolCandidates(
|
|
394
|
+
...await createPiMcpToolCandidates(
|
|
394
395
|
options.mcpHost,
|
|
395
|
-
snapshot.profile.mcpServers,
|
|
396
|
+
snapshot.profile.mcpServers.filter(server => !server.hostManaged),
|
|
396
397
|
),
|
|
398
|
+
...(snapshot.bindings.mcp
|
|
399
|
+
? await createPiMcpToolCandidates(snapshot.bindings.mcp, snapshot.bindings.mcp.servers)
|
|
400
|
+
: []),
|
|
397
401
|
]);
|
|
398
402
|
return Object.freeze({
|
|
399
403
|
loaded: extensions.loaded,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
runAgentLoopContinue,
|
|
3
3
|
convertToLlm,
|
|
4
4
|
type AgentEvent,
|
|
5
5
|
type AgentMessage,
|
|
6
|
-
type
|
|
6
|
+
type AgentLoopConfig,
|
|
7
7
|
type ThinkingLevel,
|
|
8
8
|
} from "@earendil-works/pi-agent-core";
|
|
9
9
|
import type {
|
|
@@ -114,7 +114,7 @@ interface PiTurnAdapterOptions {
|
|
|
114
114
|
readonly models: Models;
|
|
115
115
|
readonly modelSessionId: string;
|
|
116
116
|
// 读取这次执行真正要交给 Pi 的 canonical transcript。
|
|
117
|
-
// PiTurnAdapter.run 在创建
|
|
117
|
+
// PiTurnAdapter.run 在创建 Pi Loop 前调用它,宿主应返回当时最新的 transcript。
|
|
118
118
|
// 这里保留为延迟读取,是为了避免适配器创建后新增的恢复消息被旧快照漏掉。
|
|
119
119
|
readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
|
|
120
120
|
// 把工具的最终结果交回宿主持久化。
|
|
@@ -124,10 +124,9 @@ interface PiTurnAdapterOptions {
|
|
|
124
124
|
call: SettledPiToolCall,
|
|
125
125
|
) => void | Promise<void>;
|
|
126
126
|
readonly onGeneration?: PiGenerationLifecycleObserver;
|
|
127
|
-
//
|
|
128
|
-
// PiCore 通过 AgentOptions.transformContext 调用它,Runtime 用它接入现有上下文处理。
|
|
127
|
+
// 在模型调用前通过 AgentLoopConfig.transformContext 调整上下文。
|
|
129
128
|
// 直接复用 Pi 的回调类型可避免这里维护另一套上下文转换约定。
|
|
130
|
-
readonly transformContext: NonNullable<
|
|
129
|
+
readonly transformContext: NonNullable<AgentLoopConfig["transformContext"]>;
|
|
131
130
|
readonly workspace?: SpillWorkspace;
|
|
132
131
|
// 之前的执行片已经用掉的模型回合数。首次执行传 0。
|
|
133
132
|
readonly modelTurnsConsumed: number;
|
|
@@ -137,7 +136,7 @@ interface PiTurnAdapterOptions {
|
|
|
137
136
|
* 收集那些结果已经由工具自己收口的 Tool 名字。
|
|
138
137
|
*
|
|
139
138
|
* @remarks
|
|
140
|
-
* `run` 在建
|
|
139
|
+
* `run` 在建 Pi Loop 前调用,把结果交给 `projectToolResultsForModel`。
|
|
141
140
|
*
|
|
142
141
|
* `outputBudget: "structure"` 的语义就是“这个结果不需要 Runtime 再替它省”:read
|
|
143
142
|
* 按页返回并在页脚写明覆盖的行号,activate_skill 返回的是模型接下来要照做的指令,
|
|
@@ -216,8 +215,6 @@ function orderLateToolResultsBeforeTurnAbortMarker(
|
|
|
216
215
|
}
|
|
217
216
|
|
|
218
217
|
class PiTurnAdapter {
|
|
219
|
-
private piInstance?: PiCore;
|
|
220
|
-
private abortRequested = false;
|
|
221
218
|
private readonly pendingSteers: UserMessage[] = [];
|
|
222
219
|
private readonly governance: PiToolGovernance;
|
|
223
220
|
|
|
@@ -255,25 +252,16 @@ class PiTurnAdapter {
|
|
|
255
252
|
return true;
|
|
256
253
|
}
|
|
257
254
|
|
|
258
|
-
// 在 Pi 运行实例可用后尽快停止这个 Turn。
|
|
259
|
-
// Runtime 可能在 run() 之前或期间发出取消,因此尚未创建 PiCore 时先记住请求。
|
|
260
|
-
// canonical transcript 读取是异步的,其间没有可中止的 Pi 实例,所以不能删掉启动前标记。
|
|
261
|
-
abort(): void {
|
|
262
|
-
if (this.piInstance) this.piInstance.abort();
|
|
263
|
-
else this.abortRequested = true;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
255
|
// 把一条用户消息排入当前 Turn。
|
|
267
256
|
// 提交仍在运行时收到扩展宿主消息,Runtime 会调用这里。
|
|
268
|
-
//
|
|
257
|
+
// Pi Loop 创建前先缓冲消息,可覆盖异步启动窗口,同时不引入第二份 transcript 来源。
|
|
269
258
|
steer(message: UserMessage): void {
|
|
270
|
-
|
|
271
|
-
else this.pendingSteers.push(message);
|
|
259
|
+
this.pendingSteers.push(message);
|
|
272
260
|
}
|
|
273
261
|
|
|
274
262
|
// 从当前 canonical transcript 继续运行 Pi,并把生命周期事件交给监听器。
|
|
275
263
|
// Submission 的首次执行或恢复执行取得 Pinned Runtime 后,PreparedPiTurnAdapter 调用这里。
|
|
276
|
-
//
|
|
264
|
+
// 每次运行持有独立上下文;Loop 等待事件 sink 完成后才继续。
|
|
277
265
|
async run(
|
|
278
266
|
opts: {
|
|
279
267
|
readonly systemPrompt: string;
|
|
@@ -285,6 +273,7 @@ class PiTurnAdapter {
|
|
|
285
273
|
const { systemPrompt, signal } = opts;
|
|
286
274
|
signal?.throwIfAborted();
|
|
287
275
|
const canonicalMessages = await this.opts.canonicalMessages();
|
|
276
|
+
signal?.throwIfAborted();
|
|
288
277
|
const tools = this.compile(opts.tools);
|
|
289
278
|
const selfBounded = selfBoundedToolNames(opts.tools);
|
|
290
279
|
// `modelTurns` 是跨执行片的累计数,`sliceTurns` 只数本片 —— 前者管任务预算,
|
|
@@ -292,16 +281,14 @@ class PiTurnAdapter {
|
|
|
292
281
|
let modelTurns = this.opts.modelTurnsConsumed;
|
|
293
282
|
let sliceTurns = 0;
|
|
294
283
|
let reachedModelTurnLimit = false;
|
|
295
|
-
let
|
|
284
|
+
let wrapUpMessage: UserMessage | undefined;
|
|
296
285
|
let wrapUpTurns = 0;
|
|
297
|
-
const
|
|
286
|
+
const config: AgentLoopConfig = {
|
|
287
|
+
model: this.opts.pi.model,
|
|
288
|
+
reasoning: this.opts.pi.thinkingLevel === "off" ? undefined : this.opts.pi.thinkingLevel,
|
|
289
|
+
toolExecution: "parallel",
|
|
298
290
|
convertToLlm,
|
|
299
|
-
|
|
300
|
-
this.opts.models.streamSimple.bind(this.opts.models),
|
|
301
|
-
0,
|
|
302
|
-
this.opts.modelSessionId,
|
|
303
|
-
this.opts.onGeneration,
|
|
304
|
-
),
|
|
291
|
+
getSteeringMessages: async () => this.pendingSteers.splice(0, 1),
|
|
305
292
|
// transformMessages 在宿主上下文变换之后运行,确保跨 provider 的 tool call ID 格式兼容。
|
|
306
293
|
// OpenAI Responses API 生成含 `|` 的 450+ 字符 ID,Anthropic 只接受 ^[a-zA-Z0-9_-]+$(64 字符上限);
|
|
307
294
|
// 切换 provider 或消息跨 provider 回放时若不规范化,provider 会静默拒绝。
|
|
@@ -309,50 +296,36 @@ class PiTurnAdapter {
|
|
|
309
296
|
const ctx = await this.opts.transformContext(messages, signal);
|
|
310
297
|
return transformMessages(
|
|
311
298
|
orderLateToolResultsBeforeTurnAbortMarker(
|
|
312
|
-
projectToolResultsForModel(
|
|
299
|
+
projectToolResultsForModel(
|
|
300
|
+
wrapUpMessage ? [...ctx, wrapUpMessage] : ctx,
|
|
301
|
+
selfBounded,
|
|
302
|
+
),
|
|
313
303
|
) as Message[],
|
|
314
304
|
this.opts.pi.model,
|
|
315
305
|
) as AgentMessage[];
|
|
316
306
|
},
|
|
317
307
|
afterToolCall: this.governance.afterToolCall,
|
|
318
|
-
// Pi
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
prepareNextTurnWithContext: ({ context }) => {
|
|
322
|
-
modelTurns += 1;
|
|
323
|
-
sliceTurns += 1;
|
|
324
|
-
if (wrappingUp) {
|
|
325
|
-
wrapUpTurns += 1;
|
|
326
|
-
return undefined;
|
|
327
|
-
}
|
|
308
|
+
// Pi calls this only when another model turn is needed; counting belongs to turn_end.
|
|
309
|
+
prepareNextTurn: ({ context }) => {
|
|
310
|
+
if (wrapUpMessage) return undefined;
|
|
328
311
|
const toolFailureLimitReached = this.governance.wrapUpRequested();
|
|
329
312
|
if (
|
|
330
313
|
!toolFailureLimitReached &&
|
|
331
314
|
modelTurns < MAX_MODEL_TURNS_PER_SUBMISSION - 1
|
|
332
315
|
) return undefined;
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
role: "user" as const,
|
|
345
|
-
content: [{
|
|
346
|
-
type: "text" as const,
|
|
347
|
-
text: toolFailureLimitReached
|
|
348
|
-
? TOOL_FAILURE_WRAP_UP_MESSAGE
|
|
349
|
-
: WRAP_UP_MESSAGE,
|
|
350
|
-
}],
|
|
351
|
-
timestamp: Date.now(),
|
|
352
|
-
},
|
|
353
|
-
],
|
|
354
|
-
},
|
|
316
|
+
// Control instructions enter only the model projection, after official compaction.
|
|
317
|
+
// Keeping them out of Loop history preserves its canonical transcript boundary.
|
|
318
|
+
wrapUpMessage = {
|
|
319
|
+
role: "user",
|
|
320
|
+
content: [{
|
|
321
|
+
type: "text",
|
|
322
|
+
text: toolFailureLimitReached
|
|
323
|
+
? TOOL_FAILURE_WRAP_UP_MESSAGE
|
|
324
|
+
: WRAP_UP_MESSAGE,
|
|
325
|
+
}],
|
|
326
|
+
timestamp: Date.now(),
|
|
355
327
|
};
|
|
328
|
+
return { context: { ...context, tools: [] } };
|
|
356
329
|
},
|
|
357
330
|
shouldStopAfterTurn: () => {
|
|
358
331
|
if (signal?.aborted === true) return true;
|
|
@@ -360,46 +333,34 @@ class PiTurnAdapter {
|
|
|
360
333
|
// 不等于「不再有上限」:Pi 在这之后还会取 steering 和 follow-up 消息,
|
|
361
334
|
// 一直有新消息进来就会一直发模型请求,而且全在同一个 invocation 里 ——
|
|
362
335
|
// 那正是 MAX_MODEL_TURNS_PER_SLICE 要挡的 CPU 驱逐。
|
|
363
|
-
if (
|
|
336
|
+
if (wrapUpMessage) return wrapUpTurns >= MAX_WRAP_UP_TURNS;
|
|
337
|
+
if (this.governance.wrapUpRequested() || modelTurns >= MAX_MODEL_TURNS_PER_SUBMISSION - 1) return false;
|
|
364
338
|
reachedModelTurnLimit = sliceTurns >= MAX_MODEL_TURNS_PER_SLICE;
|
|
365
339
|
return reachedModelTurnLimit;
|
|
366
340
|
},
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
341
|
+
};
|
|
342
|
+
await runAgentLoopContinue(
|
|
343
|
+
{ systemPrompt, messages: [...canonicalMessages], tools: [...tools] },
|
|
344
|
+
config,
|
|
345
|
+
async (event) => {
|
|
346
|
+
await listener(event);
|
|
347
|
+
if (event.type === "turn_end") {
|
|
348
|
+
modelTurns += 1;
|
|
349
|
+
sliceTurns += 1;
|
|
350
|
+
if (wrapUpMessage) wrapUpTurns += 1;
|
|
351
|
+
}
|
|
375
352
|
},
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
388
|
-
try {
|
|
389
|
-
const running = pi.continue();
|
|
390
|
-
if (this.abortRequested) {
|
|
391
|
-
this.abortRequested = false;
|
|
392
|
-
pi.abort();
|
|
393
|
-
}
|
|
394
|
-
await running;
|
|
395
|
-
return reachedModelTurnLimit
|
|
396
|
-
? { kind: "yielded", modelTurns }
|
|
397
|
-
: { kind: "terminal", modelTurns };
|
|
398
|
-
} finally {
|
|
399
|
-
signal?.removeEventListener("abort", abort);
|
|
400
|
-
unsubscribe();
|
|
401
|
-
if (this.piInstance === pi) this.piInstance = undefined;
|
|
402
|
-
}
|
|
353
|
+
signal,
|
|
354
|
+
withProviderRetry(
|
|
355
|
+
this.opts.models.streamSimple.bind(this.opts.models),
|
|
356
|
+
0,
|
|
357
|
+
this.opts.modelSessionId,
|
|
358
|
+
this.opts.onGeneration,
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
return reachedModelTurnLimit
|
|
362
|
+
? { kind: "yielded", modelTurns }
|
|
363
|
+
: { kind: "terminal", modelTurns };
|
|
403
364
|
}
|
|
404
365
|
}
|
|
405
366
|
|
|
@@ -429,7 +390,7 @@ export interface PiToolInputRecord {
|
|
|
429
390
|
*
|
|
430
391
|
* @remarks
|
|
431
392
|
* Runtime 为每个 Submission 实现这个端口,PreparedPiTurnAdapter 会在审批和 Tool 执行前后调用它。
|
|
432
|
-
* 持久化留在
|
|
393
|
+
* 持久化留在 Pi Loop 之外,恢复流程才能从 Runtime 数据库重建决定,而不是依赖已丢失的内存状态。
|
|
433
394
|
*/
|
|
434
395
|
export interface PiTurnDurability {
|
|
435
396
|
/**
|
|
@@ -487,7 +448,7 @@ export interface PiTurnDurability {
|
|
|
487
448
|
*
|
|
488
449
|
* @remarks
|
|
489
450
|
* Submission 首次执行或恢复时,Runtime 把它传给 PiRuntimeAdapter.createTurn。
|
|
490
|
-
* 这些回调让 transcript、持久化状态、流投影和终态仍由 Runtime 负责,避免在
|
|
451
|
+
* 这些回调让 transcript、持久化状态、流投影和终态仍由 Runtime 负责,避免在 Pi Loop 内复制一份。
|
|
491
452
|
* Runtime、Submission、Turn、Prepared Runtime、Pinned Runtime、Tool candidate 和 transcript 沿用 `./index.ts` 的统一定义。
|
|
492
453
|
*/
|
|
493
454
|
export interface CreatePreparedPiTurnOptions {
|
|
@@ -508,7 +469,7 @@ export interface CreatePreparedPiTurnOptions {
|
|
|
508
469
|
/**
|
|
509
470
|
* 在 Pi 启动前读取最新 canonical transcript。
|
|
510
471
|
*
|
|
511
|
-
* PiTurnAdapter.run 在创建
|
|
472
|
+
* PiTurnAdapter.run 在创建 Pi Loop 前调用它,Runtime 应返回 transcript 当时已提交的消息。
|
|
512
473
|
* 延迟读取可纳入适配器创建后才物化的恢复结果,不能提前固化为数组快照。
|
|
513
474
|
*/
|
|
514
475
|
readonly canonicalMessages: () => Promise<readonly AgentMessage[]>;
|
|
@@ -557,10 +518,10 @@ export interface CreatePreparedPiTurnOptions {
|
|
|
557
518
|
/**
|
|
558
519
|
* 调整 Pi 即将发送给模型的消息上下文。
|
|
559
520
|
*
|
|
560
|
-
*
|
|
561
|
-
* 直接沿用
|
|
521
|
+
* Pi Loop 在每次模型调用前使用它,Runtime 通过这里接入现有上下文处理。
|
|
522
|
+
* 直接沿用 AgentLoopConfig 的类型可确保参数和取消信号与 Pi 保持一致。
|
|
562
523
|
*/
|
|
563
|
-
readonly transformContext: NonNullable<
|
|
524
|
+
readonly transformContext: NonNullable<AgentLoopConfig["transformContext"]>;
|
|
564
525
|
/**
|
|
565
526
|
* 在提交终止助手消息前读取已保存的取消原因。
|
|
566
527
|
*
|
|
@@ -681,7 +642,7 @@ export class PreparedPiTurnAdapter {
|
|
|
681
642
|
tool: {
|
|
682
643
|
...candidate.tool,
|
|
683
644
|
// 只在 Runtime 的持久化和策略门禁都放行后执行一个 Tool candidate。
|
|
684
|
-
// 实时运行由
|
|
645
|
+
// 实时运行由 Pi Loop 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
|
|
685
646
|
// 顺序不能随便调整:门禁先于结果复用,审批可能生成结果,不确定的非幂等工作不能重放。
|
|
686
647
|
execute: async (toolCallId, input, signal, onUpdate) => {
|
|
687
648
|
if (toolCallIdPrefix) {
|
|
@@ -915,11 +876,10 @@ export class PreparedPiTurnAdapter {
|
|
|
915
876
|
*
|
|
916
877
|
* @remarks
|
|
917
878
|
* 活动提交被中止时 Runtime 会调用它,包括仍在异步启动的阶段。
|
|
918
|
-
*
|
|
879
|
+
* 同一取消信号覆盖历史读取、Loop 和工具重试。
|
|
919
880
|
*/
|
|
920
881
|
abort(): void {
|
|
921
882
|
this.abortController.abort();
|
|
922
|
-
this.turn.abort();
|
|
923
883
|
}
|
|
924
884
|
|
|
925
885
|
/**
|
|
@@ -941,7 +901,7 @@ export class PreparedPiTurnAdapter {
|
|
|
941
901
|
*
|
|
942
902
|
* @remarks
|
|
943
903
|
* 当前提交仍活动时收到消息,Runtime 的扩展宿主会调用它。
|
|
944
|
-
*
|
|
904
|
+
* 启动前与运行中都进入同一队列,Loop 每次消费一条;空闲时调用方直接追加 transcript。
|
|
945
905
|
*/
|
|
946
906
|
steer(
|
|
947
907
|
message: UserMessage,
|
|
@@ -992,7 +952,7 @@ export class PreparedPiTurnAdapter {
|
|
|
992
952
|
}
|
|
993
953
|
|
|
994
954
|
/**
|
|
995
|
-
* 运行这个 Turn,直到
|
|
955
|
+
* 运行这个 Turn,直到 Pi Loop 和所有等待中的事件回调结束。
|
|
996
956
|
*
|
|
997
957
|
* @remarks
|
|
998
958
|
* Submission 首次执行和续跑时,Runtime 都会在可恢复聊天任务中调用它。
|
|
@@ -1002,13 +962,16 @@ export class PreparedPiTurnAdapter {
|
|
|
1002
962
|
options: PiPreparedTurnRunOptions,
|
|
1003
963
|
): Promise<PiTurnRunResult> {
|
|
1004
964
|
this.terminalIntent = undefined;
|
|
965
|
+
const signal = options.signal
|
|
966
|
+
? AbortSignal.any([options.signal, this.abortController.signal])
|
|
967
|
+
: this.abortController.signal;
|
|
1005
968
|
const result = await this.turn.run(
|
|
1006
969
|
{
|
|
1007
970
|
systemPrompt: this.systemPrompt,
|
|
1008
971
|
tools: this.candidates,
|
|
1009
|
-
signal
|
|
972
|
+
signal,
|
|
1010
973
|
},
|
|
1011
|
-
(event) => this.handleEvent(event),
|
|
974
|
+
(event) => this.handleEvent(event, signal),
|
|
1012
975
|
);
|
|
1013
976
|
if (this.terminalIntent) {
|
|
1014
977
|
await this.options.onTerminal(this.terminalIntent);
|
|
@@ -1018,9 +981,9 @@ export class PreparedPiTurnAdapter {
|
|
|
1018
981
|
}
|
|
1019
982
|
|
|
1020
983
|
// 提交完成的 Pi 消息、推导终态意图,并把事件投影给可恢复流。
|
|
1021
|
-
//
|
|
984
|
+
// Loop 等待每个事件的 Promise,canonical 提交失败时直接拒绝本次执行。
|
|
1022
985
|
// canonical message 先于流投影提交,恢复才不会依赖仅供浏览器消费的记录;调整顺序必须复核 recovery effect。
|
|
1023
|
-
private async handleEvent(event: AgentEvent): Promise<void> {
|
|
986
|
+
private async handleEvent(event: AgentEvent, signal: AbortSignal): Promise<void> {
|
|
1024
987
|
if (
|
|
1025
988
|
event.type === "tool_execution_start" &&
|
|
1026
989
|
!this.candidates.some(({ tool }) => tool.name === event.toolName)
|
|
@@ -1068,6 +1031,7 @@ export class PreparedPiTurnAdapter {
|
|
|
1068
1031
|
if (event.type === "message_end") {
|
|
1069
1032
|
const message = event.message;
|
|
1070
1033
|
if (message.role === "assistant") {
|
|
1034
|
+
if (message.stopReason === "deferred") throw new Error("Deferred Pi responses are not supported by this Runtime");
|
|
1071
1035
|
if (isContextOverflow(message, this.contextWindow)) {
|
|
1072
1036
|
throw new ContextOverflowError(
|
|
1073
1037
|
message.errorMessage ?? "Model context window overflow",
|
|
@@ -1109,7 +1073,7 @@ export class PreparedPiTurnAdapter {
|
|
|
1109
1073
|
message.role === "assistant" ||
|
|
1110
1074
|
message.role === "toolResult"
|
|
1111
1075
|
) {
|
|
1112
|
-
const abortReason = this.options.abortReason?.();
|
|
1076
|
+
const abortReason = this.options.abortReason?.() ?? (signal.aborted ? "Turn aborted" : undefined);
|
|
1113
1077
|
const authoritativeMessage =
|
|
1114
1078
|
message.role === "assistant" &&
|
|
1115
1079
|
message.stopReason !== "toolUse" &&
|
|
@@ -98,12 +98,12 @@ export class PiRuntimeAdapter {
|
|
|
98
98
|
* 不要把 RuntimeDatabase 直接传进 Pi 层;持久化职责应继续通过这里的窄回调提供。
|
|
99
99
|
*/
|
|
100
100
|
createTranscript(options: {
|
|
101
|
-
readonly
|
|
101
|
+
readonly storage: DurableObjectStorage;
|
|
102
102
|
readonly durability: PiTranscriptDurability;
|
|
103
103
|
readonly hasActiveTurn: () => boolean;
|
|
104
104
|
}): PiRuntimeTranscript {
|
|
105
105
|
return new PiRuntimeTranscript(
|
|
106
|
-
options.
|
|
106
|
+
options.storage,
|
|
107
107
|
options.durability,
|
|
108
108
|
this.models,
|
|
109
109
|
options.hasActiveTurn,
|
|
@@ -224,8 +224,7 @@ export class PiRuntimeAdapter {
|
|
|
224
224
|
* @remarks
|
|
225
225
|
* 调用方:Runtime 在创建 Turn 前调用一次,并把结果作为 `createTurn` 的 `toolExecutors` 传入。
|
|
226
226
|
*
|
|
227
|
-
* 实现理由:SubAgent Tool 的名称、Schema 和执行语义属于 Pi Tool
|
|
228
|
-
* rateVersion 和 slot identity 这些计费事实属于 Submission。两者在这里汇合:Pi 提供工具,
|
|
227
|
+
* 实现理由:SubAgent Tool 的名称、Schema 和执行语义属于 Pi Tool 层,而父子执行身份属于 Submission。两者在这里汇合:Pi 提供工具,
|
|
229
228
|
* Runtime 提供本次 Submission 的 lifecycle 端口,因此 Runtime 不需要认识 Tool 模块本身。
|
|
230
229
|
* 没有可用 SubAgent 类型时返回 `undefined`,让 Turn 继续使用装配期的无 lifecycle 执行器。
|
|
231
230
|
*/
|