@springbrand/agent-runtime 0.2.0-alpha.61 → 0.2.0-alpha.62

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.61",
3
+ "version": "0.2.0-alpha.62",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -11,6 +11,7 @@ export type SubmissionStatus =
11
11
  | "error";
12
12
  export type SubmissionRecoveryReason =
13
13
  | "no_meaningful_model_progress"
14
+ | "server_tool_timeout"
14
15
  | "transient_model_error"
15
16
  | "context_overflow";
16
17
 
@@ -62,7 +62,10 @@ export interface RuntimeTurnState {
62
62
  activeMessageId?: string;
63
63
  recoveryAttempt?: number;
64
64
  recoveryMax?: number;
65
- recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
65
+ recoveryReason?:
66
+ | "no_meaningful_model_progress"
67
+ | "server_tool_timeout"
68
+ | "transient_model_error";
66
69
  steerable: boolean;
67
70
  hasPendingSteer: boolean;
68
71
  queued: RuntimeQueuedSubmission[];
@@ -83,6 +83,8 @@ const MAX_WRAP_UP_TURNS = 2;
83
83
 
84
84
  export class RetryableModelError extends Error {}
85
85
 
86
+ export class RetryableModelSessionError extends RetryableModelError {}
87
+
86
88
  export class ContextOverflowError extends RetryableModelError {}
87
89
 
88
90
  function normalizePlanToolCalls(message: AgentMessage): void {
@@ -610,7 +612,6 @@ export type PiCanonicalMessageCommit =
610
612
  export interface PiTurnTerminalIntent {
611
613
  readonly outcome: "succeeded" | "failed" | "aborted";
612
614
  readonly message?: string;
613
- readonly resetModelSession?: boolean;
614
615
  }
615
616
 
616
617
  export interface PiPreparedTurnRunOptions {
@@ -1058,7 +1059,8 @@ export class PreparedPiTurnAdapter {
1058
1059
  ) || isModelStreamStallMessage(
1059
1060
  event.assistantMessageEvent.error.errorMessage,
1060
1061
  ) ||
1061
- isRecoverableAssistantError(event.assistantMessageEvent.error))
1062
+ isRecoverableAssistantError(event.assistantMessageEvent.error) ||
1063
+ isOpenRouterServerToolTimeout(event.assistantMessageEvent.error))
1062
1064
  ) {
1063
1065
  return;
1064
1066
  }
@@ -1073,10 +1075,11 @@ export class PreparedPiTurnAdapter {
1073
1075
  }
1074
1076
  const stalled = message.stopReason === "error" &&
1075
1077
  isModelStreamStallMessage(message.errorMessage);
1078
+ const resetModelSession = isOpenRouterServerToolTimeout(message);
1076
1079
  const retryable = isRecoverableAssistantError(message);
1077
- if (stalled || retryable) {
1080
+ if (stalled || retryable || resetModelSession) {
1078
1081
  const content = visibleAssistantContent(message);
1079
- if (content.length > 0) {
1082
+ if (!resetModelSession && content.length > 0) {
1080
1083
  this.assistantOrdinal += 1;
1081
1084
  await this.options.onCanonicalMessage({
1082
1085
  kind: "commit-turn",
@@ -1090,6 +1093,9 @@ export class PreparedPiTurnAdapter {
1090
1093
  if (stalled) {
1091
1094
  throw new ChatStreamStalledError(message.errorMessage!);
1092
1095
  }
1096
+ if (resetModelSession) {
1097
+ throw new RetryableModelSessionError(message.errorMessage);
1098
+ }
1093
1099
  throw new RetryableModelError(message.errorMessage);
1094
1100
  }
1095
1101
  }
@@ -1136,9 +1142,6 @@ export class PreparedPiTurnAdapter {
1136
1142
  );
1137
1143
  this.terminalIntent = {
1138
1144
  outcome: terminal.outcome,
1139
- ...(isOpenRouterServerToolTimeout(authoritativeMessage)
1140
- ? { resetModelSession: true }
1141
- : {}),
1142
1145
  ...(authoritativeMessage.errorMessage
1143
1146
  ? { message: authoritativeMessage.errorMessage }
1144
1147
  : terminal.message
@@ -2,7 +2,11 @@ import {
2
2
  PreparedPiTurnAdapter,
3
3
  type CreatePreparedPiTurnOptions,
4
4
  } from "./execution";
5
- export { ContextOverflowError, RetryableModelError } from "./execution";
5
+ export {
6
+ ContextOverflowError,
7
+ RetryableModelError,
8
+ RetryableModelSessionError,
9
+ } from "./execution";
6
10
  import {
7
11
  uiUserMessageToPi,
8
12
  } from "../message";
@@ -215,6 +215,7 @@ export function isRecoverableAssistantError(
215
215
  message.stopReason === "error" &&
216
216
  /thought[_ ]signature/i.test(message.errorMessage ?? "")
217
217
  ) return false;
218
+ // Runtime resets the model Session and gives this timeout its own one-retry budget.
218
219
  if (isOpenRouterServerToolTimeout(message)) return false;
219
220
  return isRetryableAssistantError(message) ||
220
221
  (message.stopReason === "error" &&
package/src/runtime.ts CHANGED
@@ -71,6 +71,7 @@ import {
71
71
  MODEL_STREAM_STALL_TIMEOUT_MS,
72
72
  readModelStreamStallDetails,
73
73
  RetryableModelError,
74
+ RetryableModelSessionError,
74
75
  type PiCanonicalUserInput,
75
76
  type PiCanonicalTranscriptSnapshot,
76
77
  type PiChatRecoveryData,
@@ -145,6 +146,7 @@ const CONTINUATION_WATCHDOG_SECONDS = 300;
145
146
  // Host 侧的一次抖动会把 Session 永久卡在「队列已满」。
146
147
  const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
147
148
  export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
149
+ const SERVER_TOOL_TIMEOUT_MAX_ATTEMPTS = 2;
148
150
  // stall 单独收窄。理由不是「stall 更不值得救」,而是它的重试**期望值和别的错不一样**:
149
151
  // 瞬时错(5xx / 断流)重跑一次往往就好了;stall 的重跑是拿同一份 transcript 让模型
150
152
  // 重新想同样久,如果它本来就超预算,再跑几次也一样超。把看门狗放宽到 240s 之后,
@@ -177,10 +179,7 @@ interface StoredSubmission extends SubmissionReceipt {
177
179
  userMessageId?: string | null;
178
180
  regenerateMessageId?: string | null;
179
181
  recoveryErrorCount: number;
180
- recoveryReason?:
181
- | "no_meaningful_model_progress"
182
- | "transient_model_error"
183
- | "context_overflow";
182
+ recoveryReason?: SubmissionRecoveryReason;
184
183
  runId: string | null;
185
184
  accountId: string | null;
186
185
  rateVersion: number | null;
@@ -893,7 +892,6 @@ export abstract class AgentRuntimeKernel<
893
892
  terminal: {
894
893
  outcome: "succeeded" | "failed" | "aborted";
895
894
  message?: string;
896
- resetModelSession?: boolean;
897
895
  },
898
896
  ) => void | Promise<void>;
899
897
  } = {},
@@ -3062,7 +3060,6 @@ export abstract class AgentRuntimeKernel<
3062
3060
  | {
3063
3061
  outcome: "succeeded" | "failed" | "aborted";
3064
3062
  message?: string;
3065
- resetModelSession?: boolean;
3066
3063
  }
3067
3064
  | undefined;
3068
3065
  const startedAt = ready.createdAt;
@@ -3082,13 +3079,6 @@ export abstract class AgentRuntimeKernel<
3082
3079
  intent.outcome,
3083
3080
  intent.message,
3084
3081
  );
3085
- if (intent.resetModelSession) {
3086
- this.modelSessionId = crypto.randomUUID();
3087
- await this.ctx.storage.put(
3088
- MODEL_SESSION_ID_STORAGE_KEY,
3089
- this.modelSessionId,
3090
- );
3091
- }
3092
3082
  },
3093
3083
  });
3094
3084
  for (const pending of this.db.steers.listForSubmission(submissionId)) {
@@ -3212,13 +3202,23 @@ export abstract class AgentRuntimeKernel<
3212
3202
  }
3213
3203
  const stalled = error instanceof ChatStreamStalledError;
3214
3204
  const contextOverflow = error instanceof ContextOverflowError;
3205
+ const resetModelSession = error instanceof RetryableModelSessionError;
3215
3206
  const abortReason = this.readSubmission(submissionId)?.abortReason;
3216
3207
  if (!abortReason && (stalled || error instanceof RetryableModelError)) {
3208
+ if (resetModelSession) {
3209
+ this.modelSessionId = crypto.randomUUID();
3210
+ await this.ctx.storage.put(
3211
+ MODEL_SESSION_ID_STORAGE_KEY,
3212
+ this.modelSessionId,
3213
+ );
3214
+ }
3217
3215
  const recoveryReason: SubmissionRecoveryReason = stalled
3218
3216
  ? "no_meaningful_model_progress"
3219
- : contextOverflow
3220
- ? "context_overflow"
3221
- : "transient_model_error";
3217
+ : resetModelSession
3218
+ ? "server_tool_timeout"
3219
+ : contextOverflow
3220
+ ? "context_overflow"
3221
+ : "transient_model_error";
3222
3222
  let contextOverflowRecoveryClaimed = false;
3223
3223
  const recoveryErrorCount = this.db.transaction(() => {
3224
3224
  if (contextOverflow) {
@@ -3253,7 +3253,11 @@ export abstract class AgentRuntimeKernel<
3253
3253
  ? contextOverflowRecoveryClaimed &&
3254
3254
  recoveryErrorCount < CHAT_RECOVERY_MAX_ATTEMPTS
3255
3255
  : recoveryErrorCount <
3256
- (stalled ? CHAT_STALL_MAX_ATTEMPTS : CHAT_RECOVERY_MAX_ATTEMPTS);
3256
+ (stalled
3257
+ ? CHAT_STALL_MAX_ATTEMPTS
3258
+ : resetModelSession
3259
+ ? SERVER_TOOL_TIMEOUT_MAX_ATTEMPTS
3260
+ : CHAT_RECOVERY_MAX_ATTEMPTS);
3257
3261
  if (
3258
3262
  !stoppedDuringRecovery &&
3259
3263
  retryAllowed
@@ -4710,7 +4714,9 @@ export abstract class AgentRuntimeKernel<
4710
4714
  : current.recoveryReason ===
4711
4715
  "no_meaningful_model_progress"
4712
4716
  ? CHAT_STALL_MAX_ATTEMPTS
4713
- : CHAT_RECOVERY_MAX_ATTEMPTS,
4717
+ : current.recoveryReason === "server_tool_timeout"
4718
+ ? SERVER_TOOL_TIMEOUT_MAX_ATTEMPTS
4719
+ : CHAT_RECOVERY_MAX_ATTEMPTS,
4714
4720
  ...(current.recoveryReason &&
4715
4721
  current.recoveryReason !== "context_overflow"
4716
4722
  ? { recoveryReason: current.recoveryReason }