@springbrand/agent-runtime 0.3.0-alpha.20 → 0.3.0-alpha.21
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/adapter/cloudflare/agent.ts +79 -80
- package/src/adapter/cloudflare/define-agent.ts +220 -93
- package/src/adapter/cloudflare/schema.ts +6 -2
- package/src/config/activation.ts +13 -15
- package/src/config/harness-runtime.ts +26 -36
- package/src/config/index.ts +1 -1
- package/src/config/{pinning.ts → prepared-runtime.ts} +20 -118
- package/src/config/runtime-config.ts +2 -1
- package/src/conversation/message/contract.ts +16 -4
- package/src/conversation/message/conversion.ts +43 -2
- package/src/harness/session-harness.ts +21 -6
- package/src/submission/admission.ts +8 -29
- package/src/submission/lifecycle.ts +28 -15
- package/src/submission/repository.ts +39 -30
- package/src/telemetry/contract.ts +0 -1
- package/src/telemetry/runtime-telemetry.ts +0 -4
- package/src/tools/assistant/ask-user.ts +3 -1
- package/src/tools/extension/index.ts +2 -2
- package/src/tools/web/search/api.ts +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import { validateRuntimeAssemblyPin, type RuntimeAssemblyPin } from "../config/pinning";
|
|
3
2
|
import type {
|
|
4
3
|
RuntimeSubmissionAdmissionHook,
|
|
5
4
|
RuntimeSubmissionAdmissionResult,
|
|
@@ -17,7 +16,7 @@ import type { RuntimeUnattendedExecutionGrant } from "./contracts";
|
|
|
17
16
|
import type { ExecutionLevel } from "../lib/execution-level";
|
|
18
17
|
|
|
19
18
|
// Pending records retain identity indefinitely; only recent attempts reserve capacity.
|
|
20
|
-
const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
|
|
19
|
+
export const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
|
|
21
20
|
|
|
22
21
|
export interface SubmitMessageOptions {
|
|
23
22
|
requestId: string;
|
|
@@ -177,8 +176,6 @@ export class SubmissionAdmission {
|
|
|
177
176
|
requestId,
|
|
178
177
|
idempotencyKey: idempotencyKey ?? null,
|
|
179
178
|
createdAt: userMessage.timestamp,
|
|
180
|
-
assemblyRevision: "",
|
|
181
|
-
assemblyDescriptor: "",
|
|
182
179
|
runtimeKey: runtime.runtimeKey,
|
|
183
180
|
assistantMessageId: crypto.randomUUID(),
|
|
184
181
|
queuedInputJson: JSON.stringify(userMessage),
|
|
@@ -210,38 +207,20 @@ export class SubmissionAdmission {
|
|
|
210
207
|
};
|
|
211
208
|
}
|
|
212
209
|
|
|
213
|
-
/**
|
|
214
|
-
async
|
|
210
|
+
/** Resolve the saved Agent identity against its current Config before every execution slice. */
|
|
211
|
+
async prepareCurrentRuntime(
|
|
215
212
|
submission: StoredSubmission,
|
|
216
|
-
|
|
217
|
-
restoreRuntime?: (runtimeKey: string) => Promise<void>,
|
|
213
|
+
restoreRuntime: (runtimeKey: string) => Promise<void>,
|
|
218
214
|
): Promise<StoredSubmission> {
|
|
219
215
|
const current = this.options.submissions.find(submission.submissionId);
|
|
220
216
|
if (!current) throw new Error(`Unknown SpringBrand submission: ${submission.submissionId}`);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
descriptor: current.assemblyDescriptor,
|
|
224
|
-
});
|
|
225
|
-
await restoreRuntime?.(current.runtimeKey);
|
|
217
|
+
if (!current.runtimeKey) throw new Error("Cannot resume a Submission without its Agent Config ID");
|
|
218
|
+
await restoreRuntime(current.runtimeKey);
|
|
226
219
|
const restored = this.options.submissions.find(current.submissionId);
|
|
227
220
|
if (!restored || isTerminalSubmissionStatus(restored.status)) {
|
|
228
|
-
throw new Error("Submission ended
|
|
229
|
-
}
|
|
230
|
-
if (hasPin) return restored;
|
|
231
|
-
const pin = await preparePin();
|
|
232
|
-
if (!await validateRuntimeAssemblyPin(current.runtimeKey, pin)) {
|
|
233
|
-
throw new Error("Submission assembly pin is incomplete");
|
|
221
|
+
throw new Error("Submission ended while loading current Agent Config");
|
|
234
222
|
}
|
|
235
|
-
|
|
236
|
-
const pinned = this.options.submissions.find(current.submissionId);
|
|
237
|
-
if (!pinned?.assemblyRevision || !pinned.assemblyDescriptor || isTerminalSubmissionStatus(pinned.status)) {
|
|
238
|
-
throw new Error("Submission ended before Runtime assembly was pinned");
|
|
239
|
-
}
|
|
240
|
-
await validateRuntimeAssemblyPin(pinned.runtimeKey, {
|
|
241
|
-
revision: pinned.assemblyRevision,
|
|
242
|
-
descriptor: pinned.assemblyDescriptor,
|
|
243
|
-
});
|
|
244
|
-
return pinned;
|
|
223
|
+
return restored;
|
|
245
224
|
}
|
|
246
225
|
|
|
247
226
|
/** The caller shares this synchronous write with its admission/timeout transaction. */
|
|
@@ -130,9 +130,9 @@ export type SubmissionOutcome = "succeeded" | "failed" | "aborted";
|
|
|
130
130
|
* @remarks
|
|
131
131
|
* `SubmissionLifecycle` 把它交给 `execute`,Runtime 据此选择执行前置动作。
|
|
132
132
|
*
|
|
133
|
-
* `recovery` 和 `continuation`
|
|
134
|
-
*
|
|
135
|
-
*
|
|
133
|
+
* `recovery` 和 `continuation` 都以持久化的 Agent 身份重新读取当前配置,
|
|
134
|
+
* 然后继续同一个持久 operation;前者处理进程恢复,后者处理本轮主动让出执行片。
|
|
135
|
+
* 两者都不沿用旧执行片的固定配置。
|
|
136
136
|
*/
|
|
137
137
|
export type SubmissionExecutionMode =
|
|
138
138
|
/** 首次执行一条新接收的提交。 */
|
|
@@ -244,6 +244,7 @@ export class SubmissionLifecycle<
|
|
|
244
244
|
private readonly executions = new Map<string, Promise<TSubmission>>();
|
|
245
245
|
private readonly activeBySubmission = new Map<string, TActive>();
|
|
246
246
|
private readonly terminalCommits = new Map<string, Promise<TSubmission>>();
|
|
247
|
+
private preparingAdmissions = 0;
|
|
247
248
|
|
|
248
249
|
// #region 提交与恢复
|
|
249
250
|
|
|
@@ -262,6 +263,13 @@ export class SubmissionLifecycle<
|
|
|
262
263
|
>,
|
|
263
264
|
) {}
|
|
264
265
|
|
|
266
|
+
/** Serialize an explicit Runtime change with this Chat's execution queue. */
|
|
267
|
+
async runConfigControl<T>(run: () => Promise<T>): Promise<T> {
|
|
268
|
+
const result = await this.queue.enqueue(`runtime-config:${crypto.randomUUID()}`, run);
|
|
269
|
+
if (result.status === "stale") throw new Error("Runtime configuration control was reset");
|
|
270
|
+
return result.value;
|
|
271
|
+
}
|
|
272
|
+
|
|
265
273
|
/**
|
|
266
274
|
* 接收新提交,或让重复请求加入已有提交。
|
|
267
275
|
*
|
|
@@ -275,17 +283,22 @@ export class SubmissionLifecycle<
|
|
|
275
283
|
): Promise<SubmissionHandle<TSubmission>> {
|
|
276
284
|
const duplicate = this.findDuplicate(input);
|
|
277
285
|
if (duplicate) return this.join(duplicate);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
await
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
:
|
|
288
|
-
|
|
286
|
+
this.preparingAdmissions += 1;
|
|
287
|
+
try {
|
|
288
|
+
const admitted = await this.admit(
|
|
289
|
+
input,
|
|
290
|
+
await input.prepareAdmission(),
|
|
291
|
+
);
|
|
292
|
+
if (admitted.admitted) this.pump();
|
|
293
|
+
return {
|
|
294
|
+
receipt: admitted.submission,
|
|
295
|
+
completion: admitted.rejected
|
|
296
|
+
? Promise.resolve(admitted.submission)
|
|
297
|
+
: this.waitForTerminal(admitted.submission.submissionId),
|
|
298
|
+
};
|
|
299
|
+
} finally {
|
|
300
|
+
this.preparingAdmissions -= 1;
|
|
301
|
+
}
|
|
289
302
|
}
|
|
290
303
|
|
|
291
304
|
/**
|
|
@@ -551,7 +564,7 @@ export class SubmissionLifecycle<
|
|
|
551
564
|
* 读取持久层而不是只看 `executions`,才能覆盖重启后尚未恢复的提交。
|
|
552
565
|
*/
|
|
553
566
|
isBusy(): boolean {
|
|
554
|
-
return this.options.store.countUnfinished() > 0;
|
|
567
|
+
return this.preparingAdmissions > 0 || this.options.store.countUnfinished() > 0;
|
|
555
568
|
}
|
|
556
569
|
|
|
557
570
|
// 作用:在有限时间内轮询 Runtime 是否已无活动提交。
|
|
@@ -46,8 +46,6 @@ export interface StoredSubmission {
|
|
|
46
46
|
error: string | null;
|
|
47
47
|
createdAt: number;
|
|
48
48
|
completedAt: number | null;
|
|
49
|
-
assemblyRevision: string;
|
|
50
|
-
assemblyDescriptor: string;
|
|
51
49
|
runtimeKey: string;
|
|
52
50
|
assistantMessageId: string;
|
|
53
51
|
abortReason: string | null;
|
|
@@ -63,6 +61,17 @@ export interface StoredSubmission {
|
|
|
63
61
|
retryable?: boolean;
|
|
64
62
|
}
|
|
65
63
|
|
|
64
|
+
/** Saved key and whether its Submission can still execute. */
|
|
65
|
+
export interface StoredRuntimeKey {
|
|
66
|
+
readonly runtimeKey: string;
|
|
67
|
+
readonly unfinished: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RuntimeKeyRewrite {
|
|
71
|
+
readonly from: string;
|
|
72
|
+
readonly to: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
/** Reuse durable stop intent when transferring work out of the source Submission. */
|
|
67
76
|
export function submissionAbortReason(submission: StoredSubmission | null): string | undefined {
|
|
68
77
|
return submission?.abortReason ||
|
|
@@ -74,8 +83,6 @@ export interface NewSubmission {
|
|
|
74
83
|
requestId: string;
|
|
75
84
|
idempotencyKey: string | null;
|
|
76
85
|
createdAt: number;
|
|
77
|
-
assemblyRevision: string;
|
|
78
|
-
assemblyDescriptor: string;
|
|
79
86
|
runtimeKey?: string;
|
|
80
87
|
assistantMessageId: string;
|
|
81
88
|
queuedInputJson?: string | null;
|
|
@@ -97,8 +104,6 @@ type SubmissionRow = {
|
|
|
97
104
|
error: string | null;
|
|
98
105
|
created_at: number;
|
|
99
106
|
completed_at: number | null;
|
|
100
|
-
assembly_revision: string;
|
|
101
|
-
assembly_descriptor: string;
|
|
102
107
|
runtime_key: string;
|
|
103
108
|
assistant_message_id: string;
|
|
104
109
|
abort_reason: string | null;
|
|
@@ -129,8 +134,6 @@ function mapRow(row: SubmissionRow): StoredSubmission {
|
|
|
129
134
|
error: row.error,
|
|
130
135
|
createdAt: row.created_at,
|
|
131
136
|
completedAt: row.completed_at,
|
|
132
|
-
assemblyRevision: row.assembly_revision,
|
|
133
|
-
assemblyDescriptor: row.assembly_descriptor,
|
|
134
137
|
runtimeKey: row.runtime_key,
|
|
135
138
|
assistantMessageId: row.assistant_message_id,
|
|
136
139
|
abortReason: row.abort_reason,
|
|
@@ -166,14 +169,36 @@ export class SubmissionRepository {
|
|
|
166
169
|
return this.transact(run);
|
|
167
170
|
}
|
|
168
171
|
|
|
172
|
+
/** Return saved identities for a one-time Host-owned key migration. */
|
|
173
|
+
listRuntimeKeys(): StoredRuntimeKey[] {
|
|
174
|
+
return this.sql<{ runtime_key: string; unfinished: number }>`
|
|
175
|
+
SELECT runtime_key,
|
|
176
|
+
MAX(CASE WHEN status IN ('pending', 'running') THEN 1 ELSE 0 END) AS unfinished
|
|
177
|
+
FROM pi_submissions
|
|
178
|
+
WHERE runtime_key <> ''
|
|
179
|
+
GROUP BY runtime_key
|
|
180
|
+
ORDER BY runtime_key
|
|
181
|
+
`.map((row) => ({ runtimeKey: row.runtime_key, unfinished: row.unfinished === 1 }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Rewrite each saved identity atomically after the Host validates active work. */
|
|
185
|
+
rewriteRuntimeKeys(rewrites: readonly RuntimeKeyRewrite[]): void {
|
|
186
|
+
this.transact(() => {
|
|
187
|
+
for (const { from, to } of rewrites) {
|
|
188
|
+
if (from === to) continue;
|
|
189
|
+
this.sql`UPDATE pi_submissions SET runtime_key = ${to} WHERE runtime_key = ${from}`;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
169
194
|
// 按 submissionId 读取一条完整 Submission,不存在时返回 null。
|
|
170
195
|
// Runtime 和 SubmissionLifecycle 在准入、执行、取消、结束与恢复时调用,调用方必须处理 null。
|
|
171
196
|
// 所有列在这里一次投影并统一映射,不能让不同调用方各自拼出不完整的 Submission。
|
|
172
197
|
find(id: string): StoredSubmission | null {
|
|
173
198
|
const row = this.sql<SubmissionRow>`
|
|
174
199
|
SELECT submission_id, request_id, idempotency_key, status, accepted,
|
|
175
|
-
error, created_at, completed_at,
|
|
176
|
-
|
|
200
|
+
error, created_at, completed_at,
|
|
201
|
+
runtime_key, assistant_message_id, abort_reason,
|
|
177
202
|
queued_input_json, queued_ui_message_json, user_message_id,
|
|
178
203
|
regenerate_message_id,
|
|
179
204
|
run_id,
|
|
@@ -279,7 +304,7 @@ export class SubmissionRepository {
|
|
|
279
304
|
this.sql`
|
|
280
305
|
INSERT INTO pi_submissions (
|
|
281
306
|
submission_id, request_id, idempotency_key, status, accepted,
|
|
282
|
-
created_at,
|
|
307
|
+
created_at, runtime_key,
|
|
283
308
|
assistant_message_id, queued_input_json, queued_ui_message_json,
|
|
284
309
|
user_message_id, regenerate_message_id,
|
|
285
310
|
run_id, origin, scheduled_max_execution_level, unattended_execution_grant_json
|
|
@@ -290,8 +315,6 @@ export class SubmissionRepository {
|
|
|
290
315
|
'pending',
|
|
291
316
|
1,
|
|
292
317
|
${s.createdAt},
|
|
293
|
-
${s.assemblyRevision},
|
|
294
|
-
${s.assemblyDescriptor},
|
|
295
318
|
${s.runtimeKey ?? ""},
|
|
296
319
|
${s.assistantMessageId},
|
|
297
320
|
${s.queuedInputJson ?? null},
|
|
@@ -336,31 +359,17 @@ export class SubmissionRepository {
|
|
|
336
359
|
this.sql`
|
|
337
360
|
INSERT INTO pi_submissions (
|
|
338
361
|
submission_id, request_id, idempotency_key, status, accepted,
|
|
339
|
-
error, created_at, completed_at,
|
|
340
|
-
|
|
362
|
+
error, created_at, completed_at,
|
|
363
|
+
runtime_key, assistant_message_id, abort_reason,
|
|
341
364
|
run_id, admission_retryable, user_message_id
|
|
342
365
|
) VALUES (
|
|
343
366
|
${s.submissionId}, ${s.requestId}, ${s.idempotencyKey}, 'skipped', 0,
|
|
344
|
-
${s.code}, ${s.createdAt}, ${s.createdAt}, '',
|
|
367
|
+
${s.code}, ${s.createdAt}, ${s.createdAt}, '', ${s.assistantMessageId ?? s.submissionId}, NULL,
|
|
345
368
|
${s.submissionId}, ${s.retryable ? 1 : 0}, ${s.userMessageId ?? null}
|
|
346
369
|
)
|
|
347
370
|
`;
|
|
348
371
|
}
|
|
349
372
|
|
|
350
|
-
pinAssembly(id: string, revision: string, descriptor: string): boolean {
|
|
351
|
-
const updated = this.sql<{ submission_id: string }>`
|
|
352
|
-
UPDATE pi_submissions
|
|
353
|
-
SET assembly_revision = ${revision},
|
|
354
|
-
assembly_descriptor = ${descriptor}
|
|
355
|
-
WHERE submission_id = ${id}
|
|
356
|
-
AND assembly_revision = ''
|
|
357
|
-
AND assembly_descriptor = ''
|
|
358
|
-
AND status IN ('pending', 'running')
|
|
359
|
-
RETURNING submission_id
|
|
360
|
-
`;
|
|
361
|
-
return updated.length === 1;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
373
|
recordUnattendedFailure(id: string, reason: string): void {
|
|
365
374
|
this.sql`
|
|
366
375
|
UPDATE pi_submissions
|
|
@@ -71,7 +71,6 @@ export class RuntimeTelemetry {
|
|
|
71
71
|
|
|
72
72
|
turnStarted(input: Readonly<{
|
|
73
73
|
submissionId: string;
|
|
74
|
-
assemblyRevision?: string;
|
|
75
74
|
input?: unknown;
|
|
76
75
|
occurredAt?: number;
|
|
77
76
|
}>): TelemetryRecordResult {
|
|
@@ -80,9 +79,6 @@ export class RuntimeTelemetry {
|
|
|
80
79
|
input.submissionId,
|
|
81
80
|
);
|
|
82
81
|
return this.record("turn.started", operationId, {
|
|
83
|
-
...(input.assemblyRevision
|
|
84
|
-
? { assemblyRevision: input.assemblyRevision }
|
|
85
|
-
: {}),
|
|
86
82
|
...(input.input === undefined ? {} : { input: input.input as never }),
|
|
87
83
|
}, input);
|
|
88
84
|
}
|
|
@@ -3,6 +3,8 @@ import { modelAttachmentText } from "../../conversation/message/conversion";
|
|
|
3
3
|
import type { PiToolCandidate } from "../runtime/contracts";
|
|
4
4
|
import { candidate } from "./candidate";
|
|
5
5
|
|
|
6
|
+
export const ASK_USER_TOOL_NAME = "ask_user";
|
|
7
|
+
|
|
6
8
|
const askUserResponseTypes = [
|
|
7
9
|
"text",
|
|
8
10
|
"single_select",
|
|
@@ -240,7 +242,7 @@ function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
|
240
242
|
export function askUserPiToolCandidate(): PiToolCandidate {
|
|
241
243
|
return {
|
|
242
244
|
...candidate({
|
|
243
|
-
name:
|
|
245
|
+
name: ASK_USER_TOOL_NAME,
|
|
244
246
|
label: "Ask user",
|
|
245
247
|
description:
|
|
246
248
|
"Ask all 1-10 user questions needed to continue in one call. Required input for every question: question and responseType; required defaults to false, so set it to true only when progress cannot continue without an answer. Use responseType text for free text, single_select or multi_select with 2-6 distinct options for choices, and attachment for files. At most one attachment question is allowed and it accepts multiple files. Do not add file-specific fields such as accept or tell the user to use the normal chat composer. The answers return as this tool's result, so continue the same turn after calling it.",
|
|
@@ -12,7 +12,7 @@ const noParameters = Type.Object({}, { additionalProperties: false });
|
|
|
12
12
|
*
|
|
13
13
|
* Runtime 组装层在 Extension 装载完成后调用,模型需要发现当前可用扩展时使用该工具。
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* 候选项捕获当前配置准备阶段的只读列表,执行时不重新扫描 Extension 集合。
|
|
16
16
|
*/
|
|
17
17
|
export function listExtensionsPiToolCandidate(
|
|
18
18
|
extensions: readonly PiLoadedExtension[],
|
|
@@ -22,7 +22,7 @@ export function listExtensionsPiToolCandidate(
|
|
|
22
22
|
label: "List extensions",
|
|
23
23
|
description: "List all currently loaded extensions and their tools.",
|
|
24
24
|
parameters: noParameters,
|
|
25
|
-
//
|
|
25
|
+
// 返回当前 Runtime 已加载的 Extension 快照。
|
|
26
26
|
// Pi 工具循环在模型选择 `list_extensions` 时调用,调用前允许 Turn 取消。
|
|
27
27
|
// 同时返回 count 和原列表,使模型既能快速判空又不丢失 Extension 细节。
|
|
28
28
|
async execute(_toolCallId, _input, signal) {
|
|
@@ -1115,7 +1115,7 @@ export function applyCitations(text: string, groundingMetadata: any): { text: st
|
|
|
1115
1115
|
const PROVIDER_ERROR_MAX_LENGTH = 2_048;
|
|
1116
1116
|
const NATIVE_TIMEOUT_MS = 60_000;
|
|
1117
1117
|
const OPENROUTER_MAX_OUTPUT_TOKENS = 4_096;
|
|
1118
|
-
export const WEB_SEARCH_MODEL = "deepseek/deepseek-v4-flash";
|
|
1118
|
+
export const WEB_SEARCH_MODEL = "deepseek/deepseek-v4.1-flash";
|
|
1119
1119
|
|
|
1120
1120
|
export interface WebSearchOptions {
|
|
1121
1121
|
endpoint: RuntimeModelEndpoint;
|