@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.
@@ -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
- /** Validate before loading capabilities, then reuse or atomically persist the first-run pin. */
214
- async pin(
210
+ /** Resolve the saved Agent identity against its current Config before every execution slice. */
211
+ async prepareCurrentRuntime(
215
212
  submission: StoredSubmission,
216
- preparePin: () => Promise<RuntimeAssemblyPin>,
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
- const hasPin = await validateRuntimeAssemblyPin(current.runtimeKey, {
222
- revision: current.assemblyRevision,
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 before Runtime assembly was pinned");
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
- this.options.submissions.pinAssembly(current.submissionId, pin.revision, pin.descriptor);
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` 必须分开:前者的进程内配置已经丢失,需重新装配原 runtime key 再打开 Session;
134
- * 后者是本轮主动让出执行片,持久 operation 和已固定配置保持完好。
135
- * 区分两者可避免主动让出时重复加载原 runtime key。
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
- const admitted = await this.admit(
279
- input,
280
- await input.prepareAdmission(),
281
- );
282
- if (admitted.admitted) this.pump();
283
- return {
284
- receipt: admitted.submission,
285
- completion: admitted.rejected
286
- ? Promise.resolve(admitted.submission)
287
- : this.waitForTerminal(admitted.submission.submissionId),
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, assembly_revision,
176
- assembly_descriptor, runtime_key, assistant_message_id, abort_reason,
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, assembly_revision, assembly_descriptor, runtime_key,
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, assembly_revision,
340
- assembly_descriptor, runtime_key, assistant_message_id, abort_reason,
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}, '', '', '', ${s.assistantMessageId ?? s.submissionId}, NULL,
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
@@ -56,7 +56,6 @@ export interface TelemetryPayloadByType {
56
56
  readonly idempotencyKey?: string;
57
57
  };
58
58
  readonly "turn.started": PayloadBase & {
59
- readonly assemblyRevision?: string;
60
59
  readonly input?: TelemetryValue;
61
60
  };
62
61
  readonly "turn.steered": PayloadBase & {
@@ -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: "ask_user",
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
- * 候选项捕获准备阶段的只读列表,这与 pinned Runtime 组装保持一致,不能在执行时重新扫描出另一份 Extension 集合。
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
- // 返回这个 pinned Runtime 已加载的 Extension 快照。
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;