@springbrand/agent-runtime 0.2.0-alpha.60 → 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 +1 -1
- package/src/db/submission.repo.ts +1 -0
- package/src/kernel/state.ts +4 -1
- package/src/lib/prompt.ts +2 -2
- package/src/pi/runtime-adapter/execution.ts +10 -7
- package/src/pi/runtime-adapter/index.ts +5 -1
- package/src/pi/runtime-adapter/models.ts +1 -0
- package/src/pi/tool/base.ts +24 -5
- package/src/runtime.ts +24 -18
package/package.json
CHANGED
package/src/kernel/state.ts
CHANGED
|
@@ -62,7 +62,10 @@ export interface RuntimeTurnState {
|
|
|
62
62
|
activeMessageId?: string;
|
|
63
63
|
recoveryAttempt?: number;
|
|
64
64
|
recoveryMax?: number;
|
|
65
|
-
recoveryReason?:
|
|
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[];
|
package/src/lib/prompt.ts
CHANGED
|
@@ -111,8 +111,8 @@ export const INTERACTION =
|
|
|
111
111
|
"Interaction: When progress requires user decisions, put all 1-10 necessary questions in one ask_user call " +
|
|
112
112
|
"instead of asking each question separately or writing 'please choose A/B/C' as text. Every question must set exactly one responseType. " +
|
|
113
113
|
"Use responseType: \"single_select\" or responseType: \"multi_select\" with 2-6 options for choices; use responseType: \"text\" " +
|
|
114
|
-
"for
|
|
115
|
-
"request attachments. Do not add an accept field or tell the user to upload through the normal chat composer; " +
|
|
114
|
+
"for free text; use responseType: \"attachment\" for one or more files. Set required: true only when progress cannot continue " +
|
|
115
|
+
"without that answer; required defaults to false. At most one question may request attachments. Do not add an accept field or tell the user to upload through the normal chat composer; " +
|
|
116
116
|
"continue the same turn when its result arrives. " +
|
|
117
117
|
"Don't use it when a sensible default lets you proceed. " +
|
|
118
118
|
"After completing a substantive task (report, analysis, multi-step job), finish all work and other Tool calls, " +
|
|
@@ -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 {
|
|
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/pi/tool/base.ts
CHANGED
|
@@ -44,6 +44,11 @@ const askUserQuestion = Type.Object({
|
|
|
44
44
|
description:
|
|
45
45
|
"For single_select or multi_select, whether the user may also add free text. Defaults to false.",
|
|
46
46
|
})),
|
|
47
|
+
required: Type.Optional(Type.Boolean({
|
|
48
|
+
default: false,
|
|
49
|
+
description:
|
|
50
|
+
"Whether the user must answer this question to submit. Defaults to false.",
|
|
51
|
+
})),
|
|
47
52
|
}, { additionalProperties: false });
|
|
48
53
|
|
|
49
54
|
const askUserParameters = Type.Object({
|
|
@@ -98,6 +103,10 @@ function questionsOf(
|
|
|
98
103
|
if (!question || typeof question.question !== "string" || !question.question) {
|
|
99
104
|
return null;
|
|
100
105
|
}
|
|
106
|
+
if (
|
|
107
|
+
question.required !== undefined && typeof question.required !== "boolean"
|
|
108
|
+
) return null;
|
|
109
|
+
const required = question.required === true;
|
|
101
110
|
let responseType = question.responseType;
|
|
102
111
|
if (
|
|
103
112
|
typeof responseType !== "string" ||
|
|
@@ -118,6 +127,11 @@ function questionsOf(
|
|
|
118
127
|
return null;
|
|
119
128
|
}
|
|
120
129
|
}
|
|
130
|
+
const normalizedQuestion = {
|
|
131
|
+
question: question.question,
|
|
132
|
+
responseType: responseType as typeof askUserResponseTypes[number],
|
|
133
|
+
required,
|
|
134
|
+
};
|
|
121
135
|
if (responseType === "attachment") {
|
|
122
136
|
attachmentQuestions += 1;
|
|
123
137
|
if (
|
|
@@ -126,7 +140,7 @@ function questionsOf(
|
|
|
126
140
|
question.allowCustom !== undefined ||
|
|
127
141
|
(!allowLegacy && question.multiSelect !== undefined)
|
|
128
142
|
) return null;
|
|
129
|
-
normalized.push(
|
|
143
|
+
normalized.push(normalizedQuestion);
|
|
130
144
|
continue;
|
|
131
145
|
}
|
|
132
146
|
if (responseType === "text") {
|
|
@@ -137,7 +151,7 @@ function questionsOf(
|
|
|
137
151
|
) {
|
|
138
152
|
return null;
|
|
139
153
|
}
|
|
140
|
-
normalized.push(
|
|
154
|
+
normalized.push(normalizedQuestion);
|
|
141
155
|
continue;
|
|
142
156
|
}
|
|
143
157
|
if (
|
|
@@ -156,8 +170,7 @@ function questionsOf(
|
|
|
156
170
|
(question.kind !== undefined || question.multiSelect !== undefined)
|
|
157
171
|
) return null;
|
|
158
172
|
normalized.push({
|
|
159
|
-
|
|
160
|
-
responseType: responseType as typeof askUserResponseTypes[number],
|
|
173
|
+
...normalizedQuestion,
|
|
161
174
|
options: question.options,
|
|
162
175
|
...(question.allowCustom === undefined
|
|
163
176
|
? {}
|
|
@@ -223,6 +236,12 @@ function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
|
223
236
|
);
|
|
224
237
|
})) return false;
|
|
225
238
|
|
|
239
|
+
if (
|
|
240
|
+
answer.selections.length === 0 &&
|
|
241
|
+
!answer.text.trim() &&
|
|
242
|
+
answer.attachments.length === 0
|
|
243
|
+
) return question.required !== true;
|
|
244
|
+
|
|
226
245
|
if (question.responseType === "attachment") {
|
|
227
246
|
return answer.selections.length === 0 && !answer.text &&
|
|
228
247
|
answer.attachments.length >= 1 && answer.attachments.length <= 8;
|
|
@@ -367,7 +386,7 @@ export function basePiToolCandidates(
|
|
|
367
386
|
name: "ask_user",
|
|
368
387
|
label: "Ask user",
|
|
369
388
|
description:
|
|
370
|
-
"Ask all 1-10 user questions needed to continue in one call. Required input for every question: question and responseType. Use responseType text for free text, single_select or multi_select with 2-6 distinct options for choices, and attachment
|
|
389
|
+
"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.",
|
|
371
390
|
parameters: askUserParameters,
|
|
372
391
|
prepareArguments: prepareAskUserArguments,
|
|
373
392
|
// 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
|
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
|
-
:
|
|
3220
|
-
? "
|
|
3221
|
-
:
|
|
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
|
|
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
|
-
:
|
|
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 }
|