@springbrand/agent-runtime 0.2.0-alpha.43 → 0.2.0-alpha.45

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/src/runtime.ts CHANGED
@@ -66,6 +66,7 @@ import {
66
66
  } from "./telemetry";
67
67
  import { connectConfiguredMcpServers } from "./lib/mcp";
68
68
  import {
69
+ ContextOverflowError,
69
70
  PiRuntimeAdapter,
70
71
  MODEL_STREAM_STALL_TIMEOUT_MS,
71
72
  readModelStreamStallDetails,
@@ -109,6 +110,7 @@ import {
109
110
  isTerminalSubmissionStatus,
110
111
  RuntimeDatabase,
111
112
  type StoredSubmissionAdmission,
113
+ type SubmissionRecoveryReason,
112
114
  type SubmissionStatus,
113
115
  } from "./db/index";
114
116
  import {
@@ -150,6 +152,8 @@ export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
150
152
  export const CHAT_STALL_MAX_ATTEMPTS = 3;
151
153
  const CHAT_RECOVERY_TERMINAL_MESSAGE =
152
154
  "Recovery failed after multiple attempts. This generation has stopped and your progress has been saved. Send a new message to continue.";
155
+ const CONTEXT_OVERFLOW_TERMINAL_MESSAGE =
156
+ "The model context is still too large after forced compaction. Start a new conversation or reduce the attached context.";
153
157
 
154
158
  type RuntimeEventOutboxPayload =
155
159
  | { readonly type: "model-usage"; readonly event: RuntimeModelUsageEvent }
@@ -172,7 +176,10 @@ interface StoredSubmission extends SubmissionReceipt {
172
176
  userMessageId?: string | null;
173
177
  regenerateMessageId?: string | null;
174
178
  recoveryErrorCount: number;
175
- recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
179
+ recoveryReason?:
180
+ | "no_meaningful_model_progress"
181
+ | "transient_model_error"
182
+ | "context_overflow";
176
183
  runId: string | null;
177
184
  accountId: string | null;
178
185
  rateVersion: number | null;
@@ -532,6 +539,8 @@ export abstract class AgentRuntimeKernel<
532
539
  this.db.submissions.findRunning() as StoredSubmission | null,
533
540
  findNextPending: () =>
534
541
  this.db.submissions.findNextPending() as StoredSubmission | null,
542
+ listPending: () =>
543
+ this.db.submissions.listPending() as StoredSubmission[],
535
544
  updateAbortReason: (submissionId, reason) =>
536
545
  this.db.submissions.updateAbortReason(submissionId, reason),
537
546
  };
@@ -999,6 +1008,26 @@ export abstract class AgentRuntimeKernel<
999
1008
  });
1000
1009
  this.projectNestedToolSettlement(submission.submissionId, event);
1001
1010
  },
1011
+ onUnknownToolStarted: (event) => {
1012
+ this.telemetry.capture("toolStarted", {
1013
+ submissionId: submission.submissionId,
1014
+ toolCallId: event.toolCallId,
1015
+ toolName: event.toolName,
1016
+ input: event.input,
1017
+ occurredAt: event.occurredAt,
1018
+ });
1019
+ },
1020
+ onUnknownToolFinished: (event) => {
1021
+ this.telemetry.capture("toolFinished", {
1022
+ submissionId: submission.submissionId,
1023
+ toolCallId: event.toolCallId,
1024
+ toolName: event.toolName,
1025
+ outcome: event.outcome,
1026
+ output: event.output,
1027
+ outputBytes: json(event.output).length,
1028
+ occurredAt: event.occurredAt,
1029
+ });
1030
+ },
1002
1031
  ...(toolExecutors && Object.keys(toolExecutors).length > 0
1003
1032
  ? { toolExecutors }
1004
1033
  : {}),
@@ -1662,7 +1691,7 @@ export abstract class AgentRuntimeKernel<
1662
1691
 
1663
1692
  // 作用:按当前模型输入预算压缩 Pi 上下文。
1664
1693
  // 调用:Pi Turn 适配器在发模型请求前通过 `transformContext` 回调。
1665
- // 原因:模型窗口必须预留最大输出,压缩也必须使用本 Snapshot 的模型和密钥。
1694
+ // 原因:模型窗口必须预留最大输出,压缩也必须使用本 Snapshot 的模型。
1666
1695
  private async transformPiContext(
1667
1696
  submissionId: string,
1668
1697
  messages: Parameters<PiRuntimeTranscript["compactContext"]>[0],
@@ -1671,13 +1700,12 @@ export abstract class AgentRuntimeKernel<
1671
1700
  const snapshot = this.assembly();
1672
1701
  const inputBudget = snapshot.pi.model.contextWindow -
1673
1702
  snapshot.pi.model.maxTokens;
1703
+ const force = this.readSubmission(submissionId)?.recoveryReason ===
1704
+ "context_overflow";
1674
1705
  const compacted = await this.transcript.compactContext(messages, {
1675
1706
  compactAfterTokens: inputBudget * 0.85,
1707
+ force,
1676
1708
  model: snapshot.pi.model,
1677
- apiKey: this.pi.resolveApiKey(
1678
- snapshot.bindings.provider,
1679
- snapshot.pi.model.id,
1680
- ),
1681
1709
  signal,
1682
1710
  submissionId,
1683
1711
  onCompactionPersisted: (event) => {
@@ -1710,6 +1738,7 @@ export abstract class AgentRuntimeKernel<
1710
1738
  });
1711
1739
  },
1712
1740
  });
1741
+ if (force) this.db.submissions.clearRecoveryReason(submissionId);
1713
1742
  await this.drainRuntimeEvents();
1714
1743
  return compacted;
1715
1744
  }
@@ -2840,7 +2869,9 @@ export abstract class AgentRuntimeKernel<
2840
2869
  reason: submission.recoveryReason ?? "runtime_restart",
2841
2870
  attempt: Math.max(1, submission.recoveryErrorCount),
2842
2871
  });
2843
- this.db.submissions.clearRecoveryReason(submissionId);
2872
+ if (submission.recoveryReason !== "context_overflow") {
2873
+ this.db.submissions.clearRecoveryReason(submissionId);
2874
+ }
2844
2875
  await this.broadcastApprovals();
2845
2876
  }
2846
2877
  if (mode !== "fresh") await this.ensureRuntimeReady();
@@ -3135,15 +3166,25 @@ export abstract class AgentRuntimeKernel<
3135
3166
  return this.readSubmission(submissionId)!;
3136
3167
  }
3137
3168
  const stalled = error instanceof ChatStreamStalledError;
3169
+ const contextOverflow = error instanceof ContextOverflowError;
3138
3170
  const abortReason = this.readSubmission(submissionId)?.abortReason;
3139
3171
  if (!abortReason && (stalled || error instanceof RetryableModelError)) {
3140
- const recoveryErrorCount = this.db.submissions
3141
- .incrementRecoveryErrorCount(
3172
+ const recoveryReason: SubmissionRecoveryReason = stalled
3173
+ ? "no_meaningful_model_progress"
3174
+ : contextOverflow
3175
+ ? "context_overflow"
3176
+ : "transient_model_error";
3177
+ let contextOverflowRecoveryClaimed = false;
3178
+ const recoveryErrorCount = this.db.transaction(() => {
3179
+ if (contextOverflow) {
3180
+ contextOverflowRecoveryClaimed = this.db.submissions
3181
+ .claimContextOverflowRecovery(submissionId);
3182
+ }
3183
+ return this.db.submissions.incrementRecoveryErrorCount(
3142
3184
  submissionId,
3143
- stalled
3144
- ? "no_meaningful_model_progress"
3145
- : "transient_model_error",
3185
+ recoveryReason,
3146
3186
  );
3187
+ });
3147
3188
  if (stalled) {
3148
3189
  const stallDetails = readModelStreamStallDetails(errorText(error)) ?? {
3149
3190
  lastMeaningfulActivityAt:
@@ -3163,10 +3204,14 @@ export abstract class AgentRuntimeKernel<
3163
3204
  await this.broadcastApprovals();
3164
3205
  const stoppedDuringRecovery = this.readSubmission(submissionId)
3165
3206
  ?.abortReason;
3207
+ const retryAllowed = contextOverflow
3208
+ ? contextOverflowRecoveryClaimed &&
3209
+ recoveryErrorCount < CHAT_RECOVERY_MAX_ATTEMPTS
3210
+ : recoveryErrorCount <
3211
+ (stalled ? CHAT_STALL_MAX_ATTEMPTS : CHAT_RECOVERY_MAX_ATTEMPTS);
3166
3212
  if (
3167
3213
  !stoppedDuringRecovery &&
3168
- recoveryErrorCount <
3169
- (stalled ? CHAT_STALL_MAX_ATTEMPTS : CHAT_RECOVERY_MAX_ATTEMPTS)
3214
+ retryAllowed
3170
3215
  ) {
3171
3216
  const recoveryOutcome = await this.scheduleChatRecoveryRetry(
3172
3217
  {
@@ -3207,7 +3252,9 @@ export abstract class AgentRuntimeKernel<
3207
3252
  } else if (!stoppedDuringRecovery) {
3208
3253
  terminalIntent = {
3209
3254
  outcome: "failed",
3210
- message: CHAT_RECOVERY_TERMINAL_MESSAGE,
3255
+ message: contextOverflow
3256
+ ? CONTEXT_OVERFLOW_TERMINAL_MESSAGE
3257
+ : CHAT_RECOVERY_TERMINAL_MESSAGE,
3211
3258
  };
3212
3259
  }
3213
3260
  }
@@ -4105,6 +4152,35 @@ export abstract class AgentRuntimeKernel<
4105
4152
  return result;
4106
4153
  }
4107
4154
 
4155
+ /** Stops every running, queued, or steered user request in this Session. */
4156
+ async stopAllSubmissions(reason?: string): Promise<{ ok: boolean }> {
4157
+ const current = this.db.submissions.findRunning() ??
4158
+ this.db.submissions.findNextPending();
4159
+ if (current) {
4160
+ this.db.transaction(() => {
4161
+ for (const steer of this.db.steers.listForSubmission(
4162
+ current.submissionId,
4163
+ )) {
4164
+ const message = JSON.parse(
4165
+ steer.canonicalJson,
4166
+ ) as PiCanonicalUserInput;
4167
+ const userMessage = steer.uiMessageJson
4168
+ ? JSON.parse(steer.uiMessageJson) as UIMessage & { role: "user" }
4169
+ : undefined;
4170
+ this.transcript.append(steer.messageId, message, {
4171
+ submissionId: current.submissionId,
4172
+ createdAt: steer.createdAt,
4173
+ ...(userMessage ? { userMessage } : {}),
4174
+ });
4175
+ this.db.steers.deleteByMessageId(steer.messageId);
4176
+ }
4177
+ });
4178
+ }
4179
+ const result = await this.submissions.stopAll(reason ?? "Stopped");
4180
+ await this.broadcastApprovals();
4181
+ return result;
4182
+ }
4183
+
4108
4184
  /** 返回当前已装配 User Agent 的执行档位,不提供 Session 覆盖。 */
4109
4185
  executionLevel(): ExecutionLevel {
4110
4186
  return this.assembly().profile.executionLevel;
@@ -4553,13 +4629,20 @@ export abstract class AgentRuntimeKernel<
4553
4629
  ...(current.recoveryErrorCount === undefined
4554
4630
  ? {}
4555
4631
  : {
4556
- recoveryAttempt: current.recoveryErrorCount,
4632
+ recoveryAttempt:
4633
+ current.recoveryReason === "context_overflow"
4634
+ ? 1
4635
+ : current.recoveryErrorCount,
4557
4636
  // 上限按当前恢复原因取,否则 stall 会显示 "2/5" 却在第 3 次就终止。
4558
4637
  recoveryMax:
4559
- current.recoveryReason === "no_meaningful_model_progress"
4560
- ? CHAT_STALL_MAX_ATTEMPTS
4561
- : CHAT_RECOVERY_MAX_ATTEMPTS,
4562
- ...(current.recoveryReason
4638
+ current.recoveryReason === "context_overflow"
4639
+ ? 1
4640
+ : current.recoveryReason ===
4641
+ "no_meaningful_model_progress"
4642
+ ? CHAT_STALL_MAX_ATTEMPTS
4643
+ : CHAT_RECOVERY_MAX_ATTEMPTS,
4644
+ ...(current.recoveryReason &&
4645
+ current.recoveryReason !== "context_overflow"
4563
4646
  ? { recoveryReason: current.recoveryReason }
4564
4647
  : {}),
4565
4648
  }),