@springbrand/agent-runtime 0.2.0-alpha.51 → 0.2.0-alpha.53

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.2.0-alpha.51",
3
+ "version": "0.2.0-alpha.53",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -20,7 +20,7 @@
20
20
  "@cloudflare/codemode": "0.5.1",
21
21
  "@cloudflare/sandbox": "0.12.4",
22
22
  "@cloudflare/shell": "0.4.3",
23
- "@cloudflare/think": "0.15.1",
23
+ "@cloudflare/think": "0.17.0",
24
24
  "@earendil-works/pi-agent-core": "0.83.0",
25
25
  "@earendil-works/pi-ai": "0.83.0",
26
26
  "@types/lodash-es": "^4.17.12",
@@ -59,6 +59,7 @@ export interface RuntimeQueuedSubmission {
59
59
  export interface RuntimeTurnState {
60
60
  activeSubmissionId?: string;
61
61
  activeRequestId?: string;
62
+ activeMessageId?: string;
62
63
  recoveryAttempt?: number;
63
64
  recoveryMax?: number;
64
65
  recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
package/src/lib/prompt.ts CHANGED
@@ -113,9 +113,9 @@ export const INTERACTION =
113
113
  "a bounded choice; for required free text, omit the options field entirely — never pass an empty or single-item options array; " +
114
114
  "continue the same turn when its result arrives. " +
115
115
  "Don't use it when a sensible default lets you proceed. " +
116
- "After completing a substantive task (report, analysis, multi-step job): write your full answer " +
117
- "FIRST, then call suggest_followups (2-4 directions) as the very last action and end the turn " +
118
- "write no text after it (the tool renders its own closing block). Never for small talk or while a " +
116
+ "After completing a substantive task (report, analysis, multi-step job), finish all work and other Tool calls, " +
117
+ "then call suggest_followups (2-4 directions) as the last Tool call before your final answer. " +
118
+ "After it returns, write your complete final answer and make no further Tool calls. Never for small talk or while a " +
119
119
  "task is still in progress; it is non-blocking, unlike ask_user.";
120
120
 
121
121
  // Separates short always-on memory from indexed long-tail workspace files.
@@ -300,7 +300,7 @@ export function basePiToolCandidates(
300
300
  name: "suggest_followups",
301
301
  label: "Suggest follow-ups",
302
302
  description:
303
- "Offer the user 2-4 optional follow-up directions after completing a substantive task (a report, an analysis, a multi-step job). Call this at most once, and it MUST be the very last thing you do: finish all of your prose FIRST, then call this tool and END the turn immediately do NOT write any text after calling it. Do NOT call it for small talk, quick answers, or while a task is still in progress.",
303
+ "Offer the user 2-4 optional follow-up directions after completing a substantive task (a report, an analysis, a multi-step job). Call this at most once, after all work and other Tool calls are complete. It MUST be the last Tool call before the final answer. After it returns, write the complete final answer and make no further Tool calls. Do NOT call it for small talk, quick answers, or while a task is still in progress.",
304
304
  parameters: suggestFollowupsParameters,
305
305
  async execute(_toolCallId, input) {
306
306
  return result({ noted: true, count: input.items.length });
package/src/runtime.ts CHANGED
@@ -442,6 +442,7 @@ export abstract class AgentRuntimeKernel<
442
442
  private readonly approvals: ApprovalLifecycle<StoredSubmission>;
443
443
  private readonly interactions: InteractionLifecycle<StoredSubmission>;
444
444
  private runtimeLoadTracker?: RuntimeLoadTracker;
445
+ private runtimeEventDrainTail?: Promise<void>;
445
446
  private readonly streamBySubmission = new Map<string, string>();
446
447
  private migratedSubmissionIds?: Set<string>;
447
448
  private db!: RuntimeDatabase;
@@ -1153,7 +1154,19 @@ export abstract class AgentRuntimeKernel<
1153
1154
  );
1154
1155
  }
1155
1156
 
1156
- private async drainRuntimeEvents(idempotentRetry = true): Promise<void> {
1157
+ private drainRuntimeEvents(idempotentRetry = true): Promise<void> {
1158
+ const drain = (this.runtimeEventDrainTail ?? Promise.resolve())
1159
+ .catch(() => undefined)
1160
+ .then(() => this.deliverRuntimeEvents(idempotentRetry));
1161
+ this.runtimeEventDrainTail = drain;
1162
+ return drain.finally(() => {
1163
+ if (this.runtimeEventDrainTail === drain) {
1164
+ this.runtimeEventDrainTail = undefined;
1165
+ }
1166
+ });
1167
+ }
1168
+
1169
+ private async deliverRuntimeEvents(idempotentRetry: boolean): Promise<void> {
1157
1170
  const turnEvents = this.turnEventsPort();
1158
1171
  if (!turnEvents) return;
1159
1172
 
@@ -3411,15 +3424,15 @@ export abstract class AgentRuntimeKernel<
3411
3424
  * @remarks
3412
3425
  * Cloudflare Agents SDK 为每条新 WebSocket 连接调用。
3413
3426
  *
3414
- * 先发送 transcript,再让恢复基类通知正在续传的流,
3415
- * 使普通历史和恢复握手保持各自的协议帧。
3427
+ * 先发送已完成 transcript,再让恢复基类独自重建 active assistant,
3428
+ * 使持久化历史和 resumable stream 只有一个生产者。
3416
3429
  */
3417
3430
  async onConnect(connection: Connection): Promise<void> {
3418
3431
  sendIfOpen(
3419
3432
  connection,
3420
3433
  json({
3421
3434
  type: MessageType.CF_AGENT_CHAT_MESSAGES,
3422
- messages: await this.getMessages(),
3435
+ messages: await this.hydrationMessages(),
3423
3436
  }),
3424
3437
  );
3425
3438
  await super.onConnect(connection);
@@ -3435,8 +3448,13 @@ export abstract class AgentRuntimeKernel<
3435
3448
  * 避免在这里复制 Agents SDK 的默认协议行为。
3436
3449
  */
3437
3450
  async onRequest(request: Request): Promise<Response> {
3438
- if (new URL(request.url).pathname.endsWith("/get-messages")) {
3439
- return Response.json(await this.getMessages());
3451
+ const url = new URL(request.url);
3452
+ if (url.pathname.endsWith("/get-messages")) {
3453
+ return Response.json(
3454
+ url.searchParams.get("view") === "hydration"
3455
+ ? await this.hydrationMessages()
3456
+ : await this.getMessages(),
3457
+ );
3440
3458
  }
3441
3459
  return super.onRequest(request);
3442
3460
  }
@@ -3577,12 +3595,20 @@ export abstract class AgentRuntimeKernel<
3577
3595
  * @remarks
3578
3596
  * 新连接、HTTP 读取、Turn 完成投影和 RPC 调用方需要消息快照时调用。
3579
3597
  *
3580
- * 统一从 transcript 生成完整视图;活跃 assistant 也必须随首帧恢复,后续流按相同消息 ID 接管。
3598
+ * 统一从 transcript 生成完整视图;HTTP fallback、fork RPC 读取保留 active assistant。
3581
3599
  */
3582
3600
  async getMessages(): Promise<UIMessage[]> {
3583
3601
  return this.transcript.browserMessages();
3584
3602
  }
3585
3603
 
3604
+ private async hydrationMessages(): Promise<UIMessage[]> {
3605
+ const messages = await this.getMessages();
3606
+ const running = this.db.submissions.findRunning();
3607
+ return running?.assistantMessageId
3608
+ ? messages.filter(({ id }) => id !== running.assistantMessageId)
3609
+ : messages;
3610
+ }
3611
+
3586
3612
  /** 读取当前 canonical transcript 中最后一条助手文本。 */
3587
3613
  protected async latestAssistantText(): Promise<string | undefined> {
3588
3614
  const message = [...await this.transcript.canonicalMessages()]
@@ -4620,6 +4646,7 @@ export abstract class AgentRuntimeKernel<
4620
4646
  ...(current
4621
4647
  ? {
4622
4648
  activeSubmissionId: current.submissionId,
4649
+ activeMessageId: current.assistantMessageId,
4623
4650
  ...(current.requestId
4624
4651
  ? { activeRequestId: current.requestId }
4625
4652
  : {}),