@bastani/atomic 0.8.26-alpha.5 → 0.8.26-alpha.6
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/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +48 -10
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +30 -9
- package/dist/builtin/subagents/src/runs/shared/final-drain.ts +34 -0
- package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +416 -7
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +6 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +10 -2
- package/dist/builtin/workflows/src/extension/runtime.ts +35 -3
- package/dist/builtin/workflows/src/runs/background/status.ts +52 -6
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +441 -15
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +69 -8
- package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +402 -8
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +182 -6
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +76 -6
- package/dist/builtin/workflows/src/shared/stage-prompt.ts +33 -2
- package/dist/builtin/workflows/src/shared/store-types.ts +31 -0
- package/dist/builtin/workflows/src/shared/store.ts +99 -11
- package/dist/builtin/workflows/src/shared/workflow-failures.ts +758 -132
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +5 -5
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js +5 -5
- package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +16 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.js +21 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
- package/package.json +1 -1
|
@@ -62,7 +62,7 @@ export function currentModelFullId(model: { provider: string; id: string } | und
|
|
|
62
62
|
return `${String(model.provider)}/${model.id}`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
65
|
+
const RETRYABLE_MODEL_FAILURE_PATTERNS: readonly RegExp[] = [
|
|
66
66
|
/rate\s*limit/i,
|
|
67
67
|
/too many requests/i,
|
|
68
68
|
/\b429\b/,
|
|
@@ -71,6 +71,7 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
|
71
71
|
/credit/i,
|
|
72
72
|
/auth(?:entication)?/i,
|
|
73
73
|
/unauthori[sz]ed/i,
|
|
74
|
+
/\b40[13]\b/,
|
|
74
75
|
/forbidden/i,
|
|
75
76
|
/api key/i,
|
|
76
77
|
/token expired/i,
|
|
@@ -90,14 +91,422 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
|
90
91
|
/upstream/i,
|
|
91
92
|
/timed? out/i,
|
|
92
93
|
/timeout/i,
|
|
93
|
-
/\
|
|
94
|
-
/\b503\b/,
|
|
95
|
-
/\b504\b/,
|
|
94
|
+
/\b50[0-4]\b/,
|
|
96
95
|
];
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
const NON_RETRYABLE_FAILURE_PATTERNS: readonly RegExp[] = [
|
|
98
|
+
/command failed/i,
|
|
99
|
+
/tests? failed/i,
|
|
100
|
+
/shell/i,
|
|
101
|
+
/missing file/i,
|
|
102
|
+
/no such file/i,
|
|
103
|
+
/completion guard/i,
|
|
104
|
+
/cancel/i,
|
|
105
|
+
/abort/i,
|
|
106
|
+
/interrupted/i,
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const CANCELLED_FAILURE_PATTERNS: readonly RegExp[] = [
|
|
110
|
+
/cancel/i,
|
|
111
|
+
/abort/i,
|
|
112
|
+
/interrupted/i,
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export type ModelFallbackFailureKind =
|
|
116
|
+
| "auth_on_candidate_provider"
|
|
117
|
+
| "rate_limit"
|
|
118
|
+
| "provider_unavailable"
|
|
119
|
+
| "network_timeout"
|
|
120
|
+
| "model_unavailable"
|
|
121
|
+
| "cancelled"
|
|
122
|
+
| "task_failure"
|
|
123
|
+
| "unknown";
|
|
124
|
+
|
|
125
|
+
export type ModelFallbackFailureSource =
|
|
126
|
+
| "assistant_message"
|
|
127
|
+
| "diagnostic"
|
|
128
|
+
| "throw"
|
|
129
|
+
| "structured"
|
|
130
|
+
| "string_fallback";
|
|
131
|
+
|
|
132
|
+
export interface ModelFallbackFailureSignal {
|
|
133
|
+
readonly kind: ModelFallbackFailureKind;
|
|
134
|
+
readonly message: string;
|
|
135
|
+
readonly source: ModelFallbackFailureSource;
|
|
136
|
+
readonly stopReason?: string;
|
|
137
|
+
readonly status?: number;
|
|
138
|
+
readonly code?: string | number;
|
|
139
|
+
readonly name?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const FALLBACKABLE_FAILURE_KINDS: ReadonlySet<ModelFallbackFailureKind> = new Set([
|
|
143
|
+
"auth_on_candidate_provider",
|
|
144
|
+
"rate_limit",
|
|
145
|
+
"provider_unavailable",
|
|
146
|
+
"network_timeout",
|
|
147
|
+
"model_unavailable",
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
151
|
+
return value !== null && typeof value === "object" ? value as Record<string, unknown> : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function field(value: unknown, key: string): unknown {
|
|
155
|
+
return asRecord(value)?.[key];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stringField(value: unknown, key: string): string | undefined {
|
|
159
|
+
const raw = field(value, key);
|
|
160
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function errorName(value: unknown): string | undefined {
|
|
164
|
+
return value instanceof Error ? value.name : stringField(value, "name");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function directMessageFrom(value: unknown): string | undefined {
|
|
168
|
+
return stringField(value, "errorMessage")
|
|
169
|
+
?? stringField(value, "message")
|
|
170
|
+
?? stringField(value, "statusText");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function integerFrom(value: unknown): number | undefined {
|
|
174
|
+
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
175
|
+
if (typeof value !== "string" || value.trim().length === 0) return undefined;
|
|
176
|
+
const parsed = Number(value.trim());
|
|
177
|
+
return Number.isInteger(parsed) ? parsed : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function statusFrom(value: unknown): number | undefined {
|
|
181
|
+
return integerFrom(field(value, "status"))
|
|
182
|
+
?? integerFrom(field(value, "statusCode"))
|
|
183
|
+
?? integerFrom(field(value, "httpStatus"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function codeFrom(value: unknown): string | number | undefined {
|
|
187
|
+
const rawCode = field(value, "code");
|
|
188
|
+
return typeof rawCode === "string" || typeof rawCode === "number" ? rawCode : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function stopReasonFrom(value: unknown): string | undefined {
|
|
192
|
+
return stringField(value, "stopReason");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function finishReasonFrom(value: unknown): string | undefined {
|
|
196
|
+
return stringField(value, "finish_reason") ?? stringField(value, "finishReason");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function causeOf(value: unknown): unknown {
|
|
200
|
+
return value instanceof Error ? value.cause : field(value, "cause");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function diagnosticErrors(value: unknown): readonly unknown[] {
|
|
204
|
+
const diagnostics = field(value, "diagnostics");
|
|
205
|
+
if (!Array.isArray(diagnostics)) return [];
|
|
206
|
+
const errors: unknown[] = [];
|
|
207
|
+
for (const diagnostic of diagnostics) {
|
|
208
|
+
const diagnosticError = field(diagnostic, "error");
|
|
209
|
+
errors.push(diagnosticError ?? diagnostic);
|
|
210
|
+
}
|
|
211
|
+
return errors;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeCode(value: string | number | undefined): string | undefined {
|
|
215
|
+
if (value === undefined) return undefined;
|
|
216
|
+
const normalized = String(value)
|
|
217
|
+
.trim()
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
220
|
+
.replace(/^_+|_+$/g, "");
|
|
221
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function kindFromStatus(status: number | undefined): ModelFallbackFailureKind | undefined {
|
|
225
|
+
switch (status) {
|
|
226
|
+
case 401:
|
|
227
|
+
case 403:
|
|
228
|
+
return "auth_on_candidate_provider";
|
|
229
|
+
case 408:
|
|
230
|
+
return "network_timeout";
|
|
231
|
+
case 404:
|
|
232
|
+
return "model_unavailable";
|
|
233
|
+
case 429:
|
|
234
|
+
return "rate_limit";
|
|
235
|
+
default:
|
|
236
|
+
if (status !== undefined && status >= 500 && status <= 599) return "provider_unavailable";
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function refusalKindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
|
|
242
|
+
const normalizedCode = normalizeCode(code);
|
|
243
|
+
if (normalizedCode === undefined) return undefined;
|
|
244
|
+
if (normalizedCode.includes("content_filter") || normalizedCode.includes("contentfilter")) return "task_failure";
|
|
245
|
+
if (normalizedCode.includes("safety") || normalizedCode.includes("policy")) return "task_failure";
|
|
246
|
+
switch (normalizedCode) {
|
|
247
|
+
case "blocked":
|
|
248
|
+
case "blocked_by_provider":
|
|
249
|
+
case "blocked_by_safety":
|
|
250
|
+
case "blocked_by_policy":
|
|
251
|
+
case "provider_refusal":
|
|
252
|
+
case "refusal":
|
|
253
|
+
case "tool_refusal":
|
|
254
|
+
case "tool_call_refusal":
|
|
255
|
+
case "tool_use_refusal":
|
|
256
|
+
return "task_failure";
|
|
257
|
+
default:
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function kindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
|
|
263
|
+
const normalizedCode = normalizeCode(code);
|
|
264
|
+
if (normalizedCode === undefined) return undefined;
|
|
265
|
+
const refusalKind = refusalKindFromCode(code);
|
|
266
|
+
if (refusalKind !== undefined) return refusalKind;
|
|
267
|
+
const httpStatusKind = kindFromStatus(integerFrom(code));
|
|
268
|
+
if (httpStatusKind !== undefined) return httpStatusKind;
|
|
269
|
+
|
|
270
|
+
switch (normalizedCode) {
|
|
271
|
+
case "auth":
|
|
272
|
+
case "auth_required":
|
|
273
|
+
case "authentication_required":
|
|
274
|
+
case "unauthorized":
|
|
275
|
+
case "forbidden":
|
|
276
|
+
case "invalid_api_key":
|
|
277
|
+
case "missing_api_key":
|
|
278
|
+
case "invalid_key":
|
|
279
|
+
return "auth_on_candidate_provider";
|
|
280
|
+
case "etimedout":
|
|
281
|
+
case "econnreset":
|
|
282
|
+
case "econnrefused":
|
|
283
|
+
case "enotfound":
|
|
284
|
+
case "eai_again":
|
|
285
|
+
case "fetch_failed":
|
|
286
|
+
case "network_error":
|
|
287
|
+
case "timeout":
|
|
288
|
+
case "timeout_error":
|
|
289
|
+
case "und_err_connect_timeout":
|
|
290
|
+
return "network_timeout";
|
|
291
|
+
case "rate_limit":
|
|
292
|
+
case "rate_limit_exceeded":
|
|
293
|
+
case "too_many_requests":
|
|
294
|
+
case "quota_exceeded":
|
|
295
|
+
return "rate_limit";
|
|
296
|
+
case "aborterror":
|
|
297
|
+
case "aborted":
|
|
298
|
+
case "cancelled":
|
|
299
|
+
case "canceled":
|
|
300
|
+
return "cancelled";
|
|
301
|
+
case "model_not_found":
|
|
302
|
+
case "model_unavailable":
|
|
303
|
+
case "model_disabled":
|
|
304
|
+
case "unknown_model":
|
|
305
|
+
return "model_unavailable";
|
|
306
|
+
case "provider_error":
|
|
307
|
+
case "api_error":
|
|
308
|
+
case "service_unavailable":
|
|
309
|
+
case "temporarily_unavailable":
|
|
310
|
+
case "overloaded":
|
|
311
|
+
return "provider_unavailable";
|
|
312
|
+
default:
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const PROVIDER_REFUSAL_FAILURE_PATTERNS: readonly RegExp[] = [
|
|
318
|
+
/\bfinish[_\s-]?reason\b[^\n]*\bcontent[_\s-]?filter\b/i,
|
|
319
|
+
/\bcontent[_\s-]?filter(?:ed|ing)?\b/i,
|
|
320
|
+
/\b(?:safety|policy)\b[^\n]*\b(?:refus(?:e|al|ed|es|ing)?|block(?:ed|ing)?|filter(?:ed|ing)?|violat(?:e|ion|ed|ing)?|disallow(?:ed|ing)?|reject(?:ed|ion|ing)?)\b/i,
|
|
321
|
+
/\b(?:refus(?:e|al|ed|es|ing)?|block(?:ed|ing)?|filter(?:ed|ing)?|violat(?:e|ion|ed|ing)?|disallow(?:ed|ing)?|reject(?:ed|ion|ing)?)\b[^\n]*\b(?:safety|policy)\b/i,
|
|
322
|
+
/\btool[_\s-]?(?:call|use)?[_\s-]?refus(?:e|al|ed|es|ing)?\b/i,
|
|
323
|
+
/\btool(?:\s+call|\s+use)?\b[^\n]*\brefus(?:e|al|ed|es|ing)?\b/i,
|
|
324
|
+
/\brefus(?:e|al|ed|es|ing)?\b[^\n]*\btool(?:\s+call|\s+use)?\b/i,
|
|
325
|
+
/\bprovider[_\s-]?refus(?:e|al|ed|es|ing)?\b/i,
|
|
326
|
+
/\bprovider\b[^\n]*\brefus(?:e|al|ed|es|ing)?\b[^\n]*\b(?:prompt|request|content|policy|safety)\b/i,
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
function refusalKindFromMessage(message: string): ModelFallbackFailureKind | undefined {
|
|
330
|
+
if (CANCELLED_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "cancelled";
|
|
331
|
+
if (NON_RETRYABLE_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "task_failure";
|
|
332
|
+
if (PROVIDER_REFUSAL_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "task_failure";
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function fallbackKindFromMessage(message: string, name: string | undefined): ModelFallbackFailureKind | undefined {
|
|
337
|
+
const refusalKind = refusalKindFromMessage(message);
|
|
338
|
+
if (refusalKind !== undefined) return refusalKind;
|
|
339
|
+
const nameKind = kindFromCode(name);
|
|
340
|
+
if (nameKind !== undefined) return nameKind;
|
|
341
|
+
if (!RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return undefined;
|
|
342
|
+
if (/rate\s*limit|too many requests|\b429\b|quota|billing|credit/i.test(message)) return "rate_limit";
|
|
343
|
+
if (/auth|unauthori[sz]ed|\b40[13]\b|api key|token expired|forbidden|invalid key/i.test(message)) return "auth_on_candidate_provider";
|
|
344
|
+
if (/model.*(?:unavailable|disabled|not found|unknown)|(?:unavailable|disabled|not found|unknown).*model/i.test(message)) return "model_unavailable";
|
|
345
|
+
if (/network|fetch failed|socket|connection refused|timeout|timed? out/i.test(message)) return "network_timeout";
|
|
346
|
+
return "provider_unavailable";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function signalSource(value: unknown, fallback: ModelFallbackFailureSource | undefined): ModelFallbackFailureSource {
|
|
350
|
+
if (fallback !== undefined) return fallback;
|
|
351
|
+
if (stopReasonFrom(value) !== undefined || diagnosticErrors(value).length > 0) return "assistant_message";
|
|
352
|
+
if (value instanceof Error) return "throw";
|
|
353
|
+
return "structured";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function makeSignal(
|
|
357
|
+
kind: ModelFallbackFailureKind,
|
|
358
|
+
value: unknown,
|
|
359
|
+
source: ModelFallbackFailureSource | undefined,
|
|
360
|
+
): ModelFallbackFailureSignal {
|
|
361
|
+
const status = statusFrom(value);
|
|
362
|
+
const code = codeFrom(value);
|
|
363
|
+
const name = errorName(value);
|
|
364
|
+
const stopReason = stopReasonFrom(value);
|
|
365
|
+
return {
|
|
366
|
+
kind,
|
|
367
|
+
message: modelFailureMessage(value),
|
|
368
|
+
source: signalSource(value, source),
|
|
369
|
+
...(stopReason !== undefined ? { stopReason } : {}),
|
|
370
|
+
...(status !== undefined ? { status } : {}),
|
|
371
|
+
...(code !== undefined ? { code } : {}),
|
|
372
|
+
...(name !== undefined ? { name } : {}),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function fallbackSignalFromMessage(
|
|
377
|
+
value: unknown,
|
|
378
|
+
source: ModelFallbackFailureSource | undefined,
|
|
379
|
+
): ModelFallbackFailureSignal | undefined {
|
|
380
|
+
const message = modelFailureMessage(value);
|
|
381
|
+
if (!message.trim()) return undefined;
|
|
382
|
+
const kind = fallbackKindFromMessage(message, errorName(value));
|
|
383
|
+
return kind === undefined ? undefined : makeSignal(kind, value, source);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function classifyAssistantRefusalSignal(
|
|
387
|
+
value: unknown,
|
|
388
|
+
source: ModelFallbackFailureSource | undefined,
|
|
389
|
+
): ModelFallbackFailureSignal | undefined {
|
|
390
|
+
const codeRefusalKind = refusalKindFromCode(codeFrom(value))
|
|
391
|
+
?? refusalKindFromCode(errorName(value))
|
|
392
|
+
?? refusalKindFromCode(finishReasonFrom(value));
|
|
393
|
+
if (codeRefusalKind !== undefined) return makeSignal(codeRefusalKind, value, source);
|
|
394
|
+
|
|
395
|
+
const messageRefusalKind = refusalKindFromMessage(directMessageFrom(value) ?? "");
|
|
396
|
+
return messageRefusalKind === undefined ? undefined : makeSignal(messageRefusalKind, value, source);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isRefusalSignal(signal: ModelFallbackFailureSignal): boolean {
|
|
400
|
+
return signal.kind === "cancelled" || signal.kind === "task_failure";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function structuredSignal(
|
|
404
|
+
value: unknown,
|
|
405
|
+
seen: Set<unknown>,
|
|
406
|
+
source?: ModelFallbackFailureSource,
|
|
407
|
+
): ModelFallbackFailureSignal | undefined {
|
|
408
|
+
if (value === undefined || value === null || seen.has(value)) return undefined;
|
|
409
|
+
if (typeof value === "object") seen.add(value);
|
|
410
|
+
|
|
411
|
+
const stopReason = stopReasonFrom(value)?.toLowerCase();
|
|
412
|
+
if (stopReason === "aborted") return makeSignal("cancelled", value, source);
|
|
413
|
+
|
|
414
|
+
const directRefusalSignal = classifyAssistantRefusalSignal(value, source);
|
|
415
|
+
if (directRefusalSignal !== undefined) return directRefusalSignal;
|
|
416
|
+
|
|
417
|
+
const codeKind = kindFromCode(codeFrom(value));
|
|
418
|
+
const nameKind = kindFromCode(errorName(value));
|
|
419
|
+
if (codeKind === "cancelled" || nameKind === "cancelled") return makeSignal("cancelled", value, source);
|
|
420
|
+
|
|
421
|
+
let firstNestedFallbackSignal: ModelFallbackFailureSignal | undefined;
|
|
422
|
+
const nestedSeen = new Set(seen);
|
|
423
|
+
for (const diagnosticError of diagnosticErrors(value)) {
|
|
424
|
+
const diagnosticSignal = structuredSignal(diagnosticError, nestedSeen, "diagnostic")
|
|
425
|
+
?? fallbackSignalFromMessage(diagnosticError, "diagnostic");
|
|
426
|
+
if (diagnosticSignal === undefined) continue;
|
|
427
|
+
if (isRefusalSignal(diagnosticSignal)) return diagnosticSignal;
|
|
428
|
+
firstNestedFallbackSignal ??= diagnosticSignal;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const cause = causeOf(value);
|
|
432
|
+
const causeSignal = structuredSignal(cause, nestedSeen, source)
|
|
433
|
+
?? fallbackSignalFromMessage(cause, source);
|
|
434
|
+
if (causeSignal !== undefined) {
|
|
435
|
+
if (isRefusalSignal(causeSignal)) return causeSignal;
|
|
436
|
+
firstNestedFallbackSignal ??= causeSignal;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const statusKind = kindFromStatus(statusFrom(value));
|
|
440
|
+
if (statusKind !== undefined) return makeSignal(statusKind, value, source);
|
|
441
|
+
if (codeKind !== undefined) return makeSignal(codeKind, value, source);
|
|
442
|
+
if (nameKind !== undefined) return makeSignal(nameKind, value, source);
|
|
443
|
+
|
|
444
|
+
if (firstNestedFallbackSignal !== undefined) return firstNestedFallbackSignal;
|
|
445
|
+
|
|
446
|
+
if (stopReason === "error") return makeSignal("provider_unavailable", value, source);
|
|
447
|
+
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function messageFromUnknown(value: unknown, seen: Set<unknown>): string | undefined {
|
|
452
|
+
if (value === undefined || value === null || seen.has(value)) return undefined;
|
|
453
|
+
if (typeof value === "string") return value.trim().length > 0 ? value : undefined;
|
|
454
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
455
|
+
if (typeof value === "symbol" || typeof value === "function") return undefined;
|
|
456
|
+
seen.add(value);
|
|
457
|
+
|
|
458
|
+
if (value instanceof Error && value.message.trim().length > 0) return value.message;
|
|
459
|
+
const directMessage = directMessageFrom(value);
|
|
460
|
+
if (directMessage !== undefined) return directMessage;
|
|
461
|
+
|
|
462
|
+
for (const diagnosticError of diagnosticErrors(value)) {
|
|
463
|
+
const diagnosticMessage = messageFromUnknown(diagnosticError, seen);
|
|
464
|
+
if (diagnosticMessage !== undefined) return diagnosticMessage;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const causeMessage = messageFromUnknown(causeOf(value), seen);
|
|
468
|
+
if (causeMessage !== undefined) return causeMessage;
|
|
469
|
+
|
|
470
|
+
const stopReason = stopReasonFrom(value);
|
|
471
|
+
if (stopReason !== undefined) return `Assistant message ended with stopReason:${stopReason}`;
|
|
472
|
+
const finishReason = finishReasonFrom(value);
|
|
473
|
+
if (finishReason !== undefined) return `Model request finished with finish_reason:${finishReason}`;
|
|
474
|
+
const status = statusFrom(value);
|
|
475
|
+
if (status !== undefined) return `Model request failed with status ${status}`;
|
|
476
|
+
const code = codeFrom(value);
|
|
477
|
+
if (code !== undefined) return `Model request failed with code ${String(code)}`;
|
|
478
|
+
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function modelFailureMessage(error: unknown): string {
|
|
483
|
+
const structuredMessage = messageFromUnknown(error, new Set());
|
|
484
|
+
if (structuredMessage !== undefined) return structuredMessage;
|
|
485
|
+
const rendered = String(error);
|
|
486
|
+
return rendered === "[object Object]" ? "Model request failed" : rendered;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function normalizeModelFailureSignal(error: unknown): ModelFallbackFailureSignal {
|
|
490
|
+
const structured = structuredSignal(error, new Set());
|
|
491
|
+
if (structured !== undefined) return structured;
|
|
492
|
+
|
|
493
|
+
const message = modelFailureMessage(error);
|
|
494
|
+
const name = errorName(error);
|
|
495
|
+
const fallbackKind = message.trim().length > 0
|
|
496
|
+
? fallbackKindFromMessage(message, name)
|
|
497
|
+
: undefined;
|
|
498
|
+
return {
|
|
499
|
+
kind: fallbackKind ?? "unknown",
|
|
500
|
+
message,
|
|
501
|
+
source: "string_fallback",
|
|
502
|
+
...(name !== undefined ? { name } : {}),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function isRetryableModelFailure(error: unknown): boolean {
|
|
507
|
+
if (error === undefined) return false;
|
|
508
|
+
const signal = normalizeModelFailureSignal(error);
|
|
509
|
+
return FALLBACKABLE_FAILURE_KINDS.has(signal.kind);
|
|
101
510
|
}
|
|
102
511
|
|
|
103
512
|
export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
|
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.26-alpha.6] - 2026-06-06
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Bumped package version for the Atomic 0.8.26-alpha.6 prerelease.
|
|
12
|
+
|
|
7
13
|
## [0.8.26-alpha.5] - 2026-06-06
|
|
8
14
|
|
|
9
15
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.8.26-alpha.
|
|
3
|
+
"version": "0.8.26-alpha.6",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
|
|
6
6
|
"contributors": [
|
|
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.26-alpha.6] - 2026-06-06
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Bumped package version for the Atomic 0.8.26-alpha.6 prerelease.
|
|
14
|
+
|
|
9
15
|
## [0.8.26-alpha.5] - 2026-06-06
|
|
10
16
|
|
|
11
17
|
### Fixed
|
|
@@ -1795,7 +1795,11 @@ export function makeExecuteWorkflowTool(
|
|
|
1795
1795
|
const isPaused =
|
|
1796
1796
|
run?.status === "paused" ||
|
|
1797
1797
|
(run?.stages.some((s) => s.status === "paused") ?? false);
|
|
1798
|
-
|
|
1798
|
+
const isResumableContinuation = run !== undefined && !isPaused && (
|
|
1799
|
+
(run.status === "failed" && run.endedAt !== undefined && run.resumable !== false) ||
|
|
1800
|
+
(run.endedAt === undefined && run.resumable === true && run.failureRecoverability === "recoverable")
|
|
1801
|
+
);
|
|
1802
|
+
if (isResumableContinuation) {
|
|
1799
1803
|
const continuation = activeRuntime.resumeFailedRun(stageRunId, stage.stageId, { policy });
|
|
1800
1804
|
return {
|
|
1801
1805
|
action: "resume",
|
|
@@ -3133,7 +3137,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3133
3137
|
const isPaused =
|
|
3134
3138
|
run?.status === "paused" ||
|
|
3135
3139
|
(run?.stages.some((s) => s.status === "paused") ?? false);
|
|
3136
|
-
|
|
3140
|
+
const isResumableContinuation = run !== undefined && !isPaused && (
|
|
3141
|
+
(run.status === "failed" && run.endedAt !== undefined && run.resumable !== false) ||
|
|
3142
|
+
(run.endedAt === undefined && run.resumable === true && run.failureRecoverability === "recoverable")
|
|
3143
|
+
);
|
|
3144
|
+
if (isResumableContinuation) {
|
|
3137
3145
|
const continuation = runtimeForContext(ctx).resumeFailedRun(stageRunId, stageId, { policy });
|
|
3138
3146
|
if (continuation.ok) {
|
|
3139
3147
|
print(continuation.message);
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
import { validateWorkflowModels } from "../runs/shared/model-fallback.js";
|
|
45
45
|
import { runDetached } from "../runs/background/runner.js";
|
|
46
46
|
import type { JobTracker } from "../runs/background/job-tracker.js";
|
|
47
|
+
import { appendRunEnd } from "../shared/persistence-session-entries.js";
|
|
47
48
|
import { classifyWorkflowFailure } from "../shared/workflow-failures.js";
|
|
48
49
|
|
|
49
50
|
// ---------------------------------------------------------------------------
|
|
@@ -389,13 +390,39 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
389
390
|
return { ok: true, stageId: failedStageId };
|
|
390
391
|
}
|
|
391
392
|
|
|
393
|
+
function finalizeResumedActiveBlockedSourceRun(source: RunSnapshot, continuationRunId: string): void {
|
|
394
|
+
const errorMessage = source.error ?? source.failureMessage ?? `workflow resumed in new run ${continuationRunId}`;
|
|
395
|
+
const metadata = {
|
|
396
|
+
...(source.failureKind !== undefined ? { failureKind: source.failureKind } : {}),
|
|
397
|
+
...(source.failureCode !== undefined ? { failureCode: source.failureCode } : {}),
|
|
398
|
+
failureRecoverability: "non_recoverable",
|
|
399
|
+
failureDisposition: "terminal_killed",
|
|
400
|
+
...(source.failureMessage !== undefined ? { failureMessage: source.failureMessage } : {}),
|
|
401
|
+
...(source.failedStageId !== undefined ? { failedStageId: source.failedStageId } : {}),
|
|
402
|
+
resumable: false,
|
|
403
|
+
...(source.retryAfterMs !== undefined ? { retryAfterMs: source.retryAfterMs } : {}),
|
|
404
|
+
} as const;
|
|
405
|
+
const recorded = activeStore.recordRunEnd(source.id, "killed", undefined, errorMessage, metadata);
|
|
406
|
+
if (recorded && persistence !== undefined) {
|
|
407
|
+
appendRunEnd(persistence, {
|
|
408
|
+
runId: source.id,
|
|
409
|
+
status: "killed",
|
|
410
|
+
error: errorMessage,
|
|
411
|
+
...metadata,
|
|
412
|
+
ts: Date.now(),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
392
417
|
function resumeFailedRun(sourceRunId: string, stageId?: string, options?: RuntimeDispatchOptions): ResumeFailedRunResult {
|
|
393
418
|
const source = activeStore.runs().find((run) => run.id === sourceRunId);
|
|
394
419
|
if (source === undefined) {
|
|
395
420
|
return { ok: false, reason: "run_not_found", message: `run not found: ${sourceRunId}` };
|
|
396
421
|
}
|
|
397
|
-
|
|
398
|
-
|
|
422
|
+
const isTerminalFailedResumable = source.status === "failed" && source.endedAt !== undefined && source.resumable !== false;
|
|
423
|
+
const isActiveBlockedResumable = source.endedAt === undefined && source.resumable === true && source.failureRecoverability === "recoverable";
|
|
424
|
+
if (!isTerminalFailedResumable && !isActiveBlockedResumable) {
|
|
425
|
+
return { ok: false, reason: "not_resumable", message: `run ${sourceRunId} is not a resumable workflow run` };
|
|
399
426
|
}
|
|
400
427
|
const def = registry.get(source.name);
|
|
401
428
|
if (def === undefined) {
|
|
@@ -415,12 +442,17 @@ export function createExtensionRuntime(opts: ExtensionRuntimeOpts = {}): Extensi
|
|
|
415
442
|
...runOptions({ workflow: def.name, inputs: sourceInputs }, options?.policy),
|
|
416
443
|
continuation: { source, resumeFromStageId: resolvedStage.stageId },
|
|
417
444
|
});
|
|
445
|
+
if (isActiveBlockedResumable) {
|
|
446
|
+
finalizeResumedActiveBlockedSourceRun(source, accepted.runId);
|
|
447
|
+
}
|
|
418
448
|
return {
|
|
419
449
|
ok: true,
|
|
420
450
|
runId: accepted.runId,
|
|
421
451
|
sourceRunId: source.id,
|
|
422
452
|
resumeFromStageId: resolvedStage.stageId,
|
|
423
|
-
message:
|
|
453
|
+
message: isActiveBlockedResumable
|
|
454
|
+
? `Resuming blocked workflow "${def.name}" from run ${source.id.slice(0, 8)} at stage ${resolvedStage.stageId.slice(0, 8)} (new run ${accepted.runId}).`
|
|
455
|
+
: `Resuming failed workflow "${def.name}" from run ${source.id.slice(0, 8)} at stage ${resolvedStage.stageId.slice(0, 8)} (new run ${accepted.runId}).`,
|
|
424
456
|
};
|
|
425
457
|
}
|
|
426
458
|
|
|
@@ -82,6 +82,14 @@ export interface RunDetail {
|
|
|
82
82
|
readonly stages: readonly RunSnapshot["stages"][number][];
|
|
83
83
|
readonly result?: WorkflowOutputValues;
|
|
84
84
|
readonly error?: string;
|
|
85
|
+
readonly failureKind?: RunSnapshot["failureKind"];
|
|
86
|
+
readonly failureCode?: RunSnapshot["failureCode"];
|
|
87
|
+
readonly failureRecoverability?: RunSnapshot["failureRecoverability"];
|
|
88
|
+
readonly failureDisposition?: RunSnapshot["failureDisposition"];
|
|
89
|
+
readonly failedStageId?: string;
|
|
90
|
+
readonly resumable?: boolean;
|
|
91
|
+
readonly retryAfterMs?: number;
|
|
92
|
+
readonly blockedAt?: number;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
export type InspectRunResult =
|
|
@@ -146,11 +154,26 @@ export function killRun(
|
|
|
146
154
|
const previousStatus = run.status;
|
|
147
155
|
|
|
148
156
|
// Abort active executor (no-op if not registered)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
const errorMessage = "workflow killed";
|
|
158
|
+
opts?.cancellation?.abort(runId, errorMessage);
|
|
159
|
+
|
|
160
|
+
const metadata = {
|
|
161
|
+
failureKind: "cancelled",
|
|
162
|
+
failureCode: "cancelled",
|
|
163
|
+
failureRecoverability: "non_recoverable",
|
|
164
|
+
failureDisposition: "terminal_killed",
|
|
165
|
+
failureMessage: errorMessage,
|
|
166
|
+
resumable: false,
|
|
167
|
+
} as const;
|
|
168
|
+
const recorded = activeStore.recordRunEnd(runId, "killed", undefined, errorMessage, metadata);
|
|
152
169
|
if (recorded && opts?.persistence) {
|
|
153
|
-
appendRunEnd(opts.persistence, {
|
|
170
|
+
appendRunEnd(opts.persistence, {
|
|
171
|
+
runId,
|
|
172
|
+
status: "killed",
|
|
173
|
+
error: errorMessage,
|
|
174
|
+
...metadata,
|
|
175
|
+
ts: Date.now(),
|
|
176
|
+
});
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
return { ok: true, runId, previousStatus };
|
|
@@ -247,14 +270,29 @@ export function resumeRun(
|
|
|
247
270
|
// Return a deep copy of the snapshot for safe consumption
|
|
248
271
|
const snapshot = structuredClone(run);
|
|
249
272
|
const resumedCopy = structuredClone(resumed);
|
|
250
|
-
if (run.status === "
|
|
273
|
+
if (run.status === "killed" || run.resumable === false) {
|
|
251
274
|
return {
|
|
252
275
|
ok: true,
|
|
253
276
|
runId,
|
|
254
277
|
snapshot,
|
|
255
278
|
resumed: resumedCopy,
|
|
256
279
|
mode: "not_resumable",
|
|
257
|
-
message: "This
|
|
280
|
+
message: "This workflow is not resumable; inspect the snapshot and start a new workflow run when ready.",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
run.endedAt === undefined &&
|
|
285
|
+
run.resumable === true &&
|
|
286
|
+
run.failureRecoverability === "recoverable" &&
|
|
287
|
+
run.failedStageId !== undefined
|
|
288
|
+
) {
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
runId,
|
|
292
|
+
snapshot,
|
|
293
|
+
resumed: resumedCopy,
|
|
294
|
+
mode: resumedCopy.length > 0 ? "paused" : "snapshot",
|
|
295
|
+
message: `Workflow is blocked on a recoverable ${run.failureCode ?? run.failureKind ?? "workflow"} failure at stage ${run.failedStageId}; retry/resume after the issue clears.`,
|
|
258
296
|
};
|
|
259
297
|
}
|
|
260
298
|
return {
|
|
@@ -441,6 +479,14 @@ export function inspectRun(
|
|
|
441
479
|
stages: expandedStages.map((stage) => structuredClone(stage)),
|
|
442
480
|
result: copy.result,
|
|
443
481
|
error: copy.error,
|
|
482
|
+
failureKind: copy.failureKind,
|
|
483
|
+
failureCode: copy.failureCode,
|
|
484
|
+
failureRecoverability: copy.failureRecoverability,
|
|
485
|
+
failureDisposition: copy.failureDisposition,
|
|
486
|
+
failedStageId: copy.failedStageId,
|
|
487
|
+
resumable: copy.resumable,
|
|
488
|
+
retryAfterMs: copy.retryAfterMs,
|
|
489
|
+
blockedAt: copy.blockedAt,
|
|
444
490
|
};
|
|
445
491
|
|
|
446
492
|
return { ok: true, runId: copy.id, detail };
|