@coreplane/switchboard 1.260.4 → 1.260.5
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/dist/assets/deploy/cloudflare/package.json +2 -2
- package/dist/assets/deploy/cloudflare-docs/package.json +1 -1
- package/dist/assets/deploy/cloudflare-memory/package.json +2 -2
- package/dist/assets/deploy/cloudflare-memory/worker.ts +205 -8
- package/dist/assets/deploy/cloudflare-resident/package.json +2 -2
- package/dist/assets/deploy/cloudflare-resident/worker.ts +124 -40
- package/dist/assets/deploy/cloudflare-sandbox/package.json +2 -2
- package/dist/assets/package-lock.json +712 -97
- package/dist/assets/package.json +4 -4
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/budgets.ts +5 -0
- package/dist/assets/src/core/coordinator/contract.ts +5 -0
- package/dist/assets/src/core/coordinator/driver.ts +114 -25
- package/dist/assets/src/core/pipelineStanding.ts +2 -0
- package/dist/assets/src/core/plane/decide.ts +19 -2
- package/dist/assets/src/core/provider.ts +380 -0
- package/dist/assets/src/core/refusal.ts +1 -0
- package/dist/assets/src/core/runEvents.ts +8 -0
- package/dist/assets/src/core/runLedger/types.ts +10 -1
- package/dist/assets/src/core/runRecord.ts +31 -0
- package/dist/assets/src/core/ship/contract.ts +2 -0
- package/dist/assets/src/core/ship/coordinator.ts +487 -25
- package/dist/assets/src/core/types.ts +6 -0
- package/dist/assets/src/execution/residentRebind.ts +15 -0
- package/dist/assets/src/execution/residentReuse.ts +23 -2
- package/dist/assets/web/dist/.vite/manifest.json +67 -67
- package/dist/assets/web/dist/assets/{DeliveryPage-BaemWKW8.js → DeliveryPage-Cf1joUyA.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-fgKQHLaO.js → HomePage-DZEAnCCv.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-vuWiDzvz.js → PendingTurnRow-4B8JwETU.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-DXMJf8FB.js → PlanePage-CHU2szQs.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DdC8yU_S.js → ResidentDetailPage-L5SRcN_O.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-CSM-KODv.js → ResidentsIndexPage-BIFf8qyE.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DzPIshJx.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-jo6NZxsP.js → RunRoutePage-Cru5IVux.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-Dx1Y4sSq.js → RunsIndexPage-CBkdPAdj.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BuB_NZyR.js → ScheduledPage-D_R55BDk.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-Cn96jB6Q.js → SettingsPage-DlmZvAvf.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-D2cJfpEq.js → SilentTurn-PWMDMbMG.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-DDHUCPVJ.js → StatusDot-Bq0mYMFo.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-B4VIHkAI.js → Tooltip-Bs6zd7yE.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-C-2DfjLI.js → UnitRoutePage-BMGEZCvn.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-0szUBIMv.js +1 -0
- package/dist/assets/web/dist/assets/{dist-Cf1CmCSK.js → dist-EkKVbIM-.js} +11 -11
- package/dist/assets/web/dist/assets/{indexRow-Dxg7C8s9.js → indexRow-5s3hNzZ9.js} +1 -1
- package/dist/assets/web/dist/assets/{main-DxPqaFu9.js → main-Disot0n_.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-BzoeVxZB.js → sseReplay-9gcMpylx.js} +1 -1
- package/dist/cli.js +1855 -401
- package/package.json +4 -4
- package/dist/assets/web/dist/assets/RunFoldRow-Csik3wD6.js +0 -1
- package/dist/assets/web/dist/assets/budgets-KNNkT4PZ.js +0 -1
|
@@ -98,6 +98,386 @@ export interface Provider {
|
|
|
98
98
|
complete(req: CompletionRequest): Promise<CompletionResult>;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** Every model-call failure crosses this one seam. The cause is deliberately
|
|
102
|
+
* small and wire-neutral: adapters classify status plus structured body once;
|
|
103
|
+
* callers decide from `cause` and render only `renderProviderFailure(cause)`.
|
|
104
|
+
* `operatorUrl` is retained for an admin surface and never appears in the
|
|
105
|
+
* error's message or the user-facing renderer. */
|
|
106
|
+
export const PROVIDER_FAILURE_CAUSES = [
|
|
107
|
+
"transient",
|
|
108
|
+
"rate-limited",
|
|
109
|
+
"credit-or-quota-exhausted",
|
|
110
|
+
"key-absent",
|
|
111
|
+
"key-invalid",
|
|
112
|
+
"model-unknown",
|
|
113
|
+
"request-rejected",
|
|
114
|
+
"permanent",
|
|
115
|
+
] as const;
|
|
116
|
+
export type ProviderFailureCause = (typeof PROVIDER_FAILURE_CAUSES)[number];
|
|
117
|
+
|
|
118
|
+
export interface ProviderFailureAnswer {
|
|
119
|
+
status?: number;
|
|
120
|
+
body?: unknown;
|
|
121
|
+
error?: unknown;
|
|
122
|
+
provider?: string;
|
|
123
|
+
model?: string;
|
|
124
|
+
/** Only adapters that verified the model proxy's response-specific HMAC
|
|
125
|
+
* marker may set this. Provider response bodies are never trusted to name
|
|
126
|
+
* their own disposition. */
|
|
127
|
+
trustedEnvelope?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ProviderSchemaRejection {
|
|
131
|
+
tool: string;
|
|
132
|
+
keyword: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ProviderFailureDetails extends Pick<ProviderFailureAnswer, "status" | "provider" | "model"> {
|
|
136
|
+
operatorUrl?: string;
|
|
137
|
+
/** The configured variable involved in a key failure. It is operator-safe
|
|
138
|
+
* diagnostic context, never part of the requester-facing renderer. */
|
|
139
|
+
keyVariable?: string;
|
|
140
|
+
/** Vouched evidence minted at the provider boundary when its response names
|
|
141
|
+
* one offered tool and one schema keyword. Callers may repair only on
|
|
142
|
+
* this pair, never on a generic 400 or provider-controlled prose. */
|
|
143
|
+
schemaRejection?: ProviderSchemaRejection;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function providerFailureDiagnostic(cause: ProviderFailureCause, details: ProviderFailureDetails): string {
|
|
147
|
+
if (details.provider !== undefined && details.keyVariable !== undefined) {
|
|
148
|
+
if (cause === "key-absent") return `Provider "${details.provider}": ${details.keyVariable} is not set`;
|
|
149
|
+
if (cause === "key-invalid") return `Provider "${details.provider}": ${details.keyVariable} was refused`;
|
|
150
|
+
}
|
|
151
|
+
return renderProviderFailure(cause);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export class ProviderFailure extends Error {
|
|
155
|
+
override readonly name = "ProviderFailure";
|
|
156
|
+
override readonly cause: ProviderFailureCause;
|
|
157
|
+
readonly status: number | undefined;
|
|
158
|
+
readonly provider: string | undefined;
|
|
159
|
+
readonly model: string | undefined;
|
|
160
|
+
readonly operatorUrl: string | undefined;
|
|
161
|
+
readonly keyVariable: string | undefined;
|
|
162
|
+
readonly schemaRejection: ProviderSchemaRejection | undefined;
|
|
163
|
+
|
|
164
|
+
constructor(cause: ProviderFailureCause, details: ProviderFailureDetails = {}) {
|
|
165
|
+
super(providerFailureDiagnostic(cause, details));
|
|
166
|
+
this.cause = cause;
|
|
167
|
+
this.status = details.status;
|
|
168
|
+
this.provider = details.provider;
|
|
169
|
+
this.model = details.model;
|
|
170
|
+
this.operatorUrl = details.operatorUrl;
|
|
171
|
+
this.keyVariable = details.keyVariable;
|
|
172
|
+
this.schemaRejection = details.schemaRejection;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type ProviderFailureSurface = "parked" | "ended";
|
|
177
|
+
|
|
178
|
+
/** One cause, one sentence for the surface that owns the disposition. Only a
|
|
179
|
+
* live leased turn may promise continuation; every pre-run door defaults to
|
|
180
|
+
* an ending-safe sentence. No wire payload, provider URL or recovery
|
|
181
|
+
* instruction is accepted as an input. */
|
|
182
|
+
export function renderProviderFailure(cause: ProviderFailureCause, surface: ProviderFailureSurface = "ended"): string {
|
|
183
|
+
switch (cause) {
|
|
184
|
+
case "transient":
|
|
185
|
+
return surface === "parked"
|
|
186
|
+
? "The model provider is temporarily unavailable; your work is kept and will continue when service recovers."
|
|
187
|
+
: "The model provider is temporarily unavailable; this request did not start.";
|
|
188
|
+
case "rate-limited":
|
|
189
|
+
return surface === "parked"
|
|
190
|
+
? "The model provider is rate-limited; your work is kept and will continue when capacity returns."
|
|
191
|
+
: "The model provider is rate-limited; this request did not start.";
|
|
192
|
+
case "credit-or-quota-exhausted":
|
|
193
|
+
return surface === "parked"
|
|
194
|
+
? "The model provider's credit or quota is exhausted; your work is kept and will continue when service recovers."
|
|
195
|
+
: "The model provider's credit or quota is exhausted; this request did not start.";
|
|
196
|
+
case "key-absent":
|
|
197
|
+
return "The model provider key is not configured; this request cannot start until the service is restored.";
|
|
198
|
+
case "key-invalid":
|
|
199
|
+
return "The model provider key was refused; this request cannot start until the service is restored.";
|
|
200
|
+
case "model-unknown":
|
|
201
|
+
return "The configured model is unavailable from its provider; this request cannot start until the service is restored.";
|
|
202
|
+
case "request-rejected":
|
|
203
|
+
return "The model provider rejected the request shape; no work was started.";
|
|
204
|
+
case "permanent":
|
|
205
|
+
return "The model provider refused the call; the request ended without exposing the provider's response.";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const providerFailureRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
210
|
+
typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
211
|
+
|
|
212
|
+
function providerSchemaRejectionRecord(value: unknown): ProviderSchemaRejection | undefined {
|
|
213
|
+
const row = providerFailureRecord(value);
|
|
214
|
+
return typeof row?.tool === "string" &&
|
|
215
|
+
row.tool.length > 0 &&
|
|
216
|
+
typeof row.keyword === "string" &&
|
|
217
|
+
row.keyword.length > 0
|
|
218
|
+
? { tool: row.tool, keyword: row.keyword }
|
|
219
|
+
: undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function providerFailureBody(body: unknown): unknown {
|
|
223
|
+
if (typeof body !== "string") return body;
|
|
224
|
+
const text = body.trim();
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(text) as unknown;
|
|
227
|
+
} catch {
|
|
228
|
+
const at = text.indexOf("{");
|
|
229
|
+
if (at < 0) return undefined;
|
|
230
|
+
try {
|
|
231
|
+
return JSON.parse(text.slice(at)) as unknown;
|
|
232
|
+
} catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function providerFailureSignals(value: unknown, into: string[] = []): string[] {
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
for (const item of value) providerFailureSignals(item, into);
|
|
241
|
+
return into;
|
|
242
|
+
}
|
|
243
|
+
const row = providerFailureRecord(value);
|
|
244
|
+
if (!row) return into;
|
|
245
|
+
for (const [key, item] of Object.entries(row)) {
|
|
246
|
+
const normalizedKey = key.toLowerCase();
|
|
247
|
+
if (typeof item === "string" && ["code", "type", "error_type", "reason", "limit_source"].includes(normalizedKey))
|
|
248
|
+
into.push(item.toLowerCase());
|
|
249
|
+
else if (typeof item === "number" && normalizedKey === "code") into.push(String(item));
|
|
250
|
+
providerFailureSignals(item, into);
|
|
251
|
+
}
|
|
252
|
+
return into;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function providerFailureStatus(input: ProviderFailureAnswer, text: string): number | undefined {
|
|
256
|
+
if (input.status !== undefined) return input.status;
|
|
257
|
+
const match = /(?:^|\b(?:http|status(?: code)?|error|api error)[^0-9]{0,8})([1-5]\d\d)\b/i.exec(text);
|
|
258
|
+
return match ? Number(match[1]) : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function providerFailureText(input: ProviderFailureAnswer): string {
|
|
262
|
+
const error = input.error;
|
|
263
|
+
if (error instanceof Error) return error.message;
|
|
264
|
+
if (typeof error === "string") return error;
|
|
265
|
+
if (typeof input.body === "string") return input.body;
|
|
266
|
+
try {
|
|
267
|
+
return input.body === undefined ? "" : JSON.stringify(input.body);
|
|
268
|
+
} catch {
|
|
269
|
+
return "";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function quotedWord(text: string, word: string): boolean {
|
|
274
|
+
return [`'${word}'`, `"${word}"`, `\`${word}\``].some((candidate) => text.includes(candidate));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function toolSchemaKeywords(value: unknown, into = new Set<string>()): Set<string> {
|
|
278
|
+
if (Array.isArray(value)) {
|
|
279
|
+
for (const item of value) toolSchemaKeywords(item, into);
|
|
280
|
+
return into;
|
|
281
|
+
}
|
|
282
|
+
const schema = providerFailureRecord(value);
|
|
283
|
+
if (!schema) return into;
|
|
284
|
+
for (const [keyword, item] of Object.entries(schema)) {
|
|
285
|
+
into.add(keyword);
|
|
286
|
+
if ((keyword === "properties" || keyword === "$defs" || keyword === "definitions") && providerFailureRecord(item)) {
|
|
287
|
+
for (const child of Object.values(providerFailureRecord(item)!)) toolSchemaKeywords(child, into);
|
|
288
|
+
} else {
|
|
289
|
+
toolSchemaKeywords(item, into);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return into;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The provider adapter's evidence for one repairable schema rejection. The
|
|
296
|
+
* response must prove schema validation failed and name exactly one offered
|
|
297
|
+
* tool plus exactly one keyword present in that tool's sent schema. */
|
|
298
|
+
export function providerSchemaRejectionOf(
|
|
299
|
+
error: unknown,
|
|
300
|
+
tools: readonly ToolDef[] | undefined,
|
|
301
|
+
): ProviderSchemaRejection | undefined {
|
|
302
|
+
if (!tools || tools.length === 0) return undefined;
|
|
303
|
+
const text = providerFailureText({ error });
|
|
304
|
+
const parsed = providerFailureBody(providerFailureRecord(error) ?? text);
|
|
305
|
+
const prose = providerFailureProse(parsed ?? text);
|
|
306
|
+
if (
|
|
307
|
+
!/(?:invalid|unsupported|refused|rejected)[^.!?]{0,80}(?:json )?schema|(?:json )?schema[^.!?]{0,80}(?:invalid|unsupported|not (?:permitted|supported))/i.test(
|
|
308
|
+
prose,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
return undefined;
|
|
312
|
+
const namedTools = tools.filter((tool) => quotedWord(prose, tool.name));
|
|
313
|
+
if (namedTools.length !== 1) return undefined;
|
|
314
|
+
const [tool] = namedTools;
|
|
315
|
+
const namedKeywords = [...toolSchemaKeywords(tool.inputSchema)].filter((keyword) => quotedWord(prose, keyword));
|
|
316
|
+
if (namedKeywords.length !== 1) return undefined;
|
|
317
|
+
return { tool: tool.name, keyword: namedKeywords[0]! };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function providerFailureProse(value: unknown, into: string[] = []): string {
|
|
321
|
+
if (typeof value === "string") {
|
|
322
|
+
into.push(value);
|
|
323
|
+
return into.join(" ");
|
|
324
|
+
}
|
|
325
|
+
if (Array.isArray(value)) {
|
|
326
|
+
for (const item of value) providerFailureProse(item, into);
|
|
327
|
+
return into.join(" ");
|
|
328
|
+
}
|
|
329
|
+
const row = providerFailureRecord(value);
|
|
330
|
+
if (!row) return into.join(" ");
|
|
331
|
+
for (const [key, item] of Object.entries(row)) {
|
|
332
|
+
if (key.toLowerCase() !== "cause") providerFailureProse(item, into);
|
|
333
|
+
}
|
|
334
|
+
return into.join(" ");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function signalIncludes(signals: readonly string[], words: readonly string[]): boolean {
|
|
338
|
+
return signals.some((signal) => words.some((word) => signal === word || signal.includes(word)));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isProviderFailureCause(value: unknown): value is ProviderFailureCause {
|
|
342
|
+
return typeof value === "string" && (PROVIDER_FAILURE_CAUSES as readonly string[]).includes(value);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function trustedProviderFailureEnvelope(
|
|
346
|
+
value: unknown,
|
|
347
|
+
): { cause: ProviderFailureCause; schemaRejection?: ProviderSchemaRejection } | undefined {
|
|
348
|
+
if (Array.isArray(value)) {
|
|
349
|
+
for (const item of value) {
|
|
350
|
+
const envelope = trustedProviderFailureEnvelope(item);
|
|
351
|
+
if (envelope !== undefined) return envelope;
|
|
352
|
+
}
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
const row = providerFailureRecord(value);
|
|
356
|
+
if (!row) return undefined;
|
|
357
|
+
if (row.type === "provider_failure" && isProviderFailureCause(row.cause)) {
|
|
358
|
+
const schemaRejection = providerSchemaRejectionRecord(row.schemaRejection);
|
|
359
|
+
return { cause: row.cause, ...(schemaRejection !== undefined ? { schemaRejection } : {}) };
|
|
360
|
+
}
|
|
361
|
+
for (const item of Object.values(row)) {
|
|
362
|
+
const envelope = trustedProviderFailureEnvelope(item);
|
|
363
|
+
if (envelope !== undefined) return envelope;
|
|
364
|
+
}
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Status plus structured body → one typed failure. Text is consulted only at
|
|
369
|
+
* this adapter boundary for transports that expose no structured error; no
|
|
370
|
+
* consumer owns a word list. Mandatory provider statuses and transport facts
|
|
371
|
+
* precede every untrusted body signal. Unknown answers fail closed as
|
|
372
|
+
* `permanent`. */
|
|
373
|
+
export function classifyProviderFailure(input: ProviderFailureAnswer): ProviderFailure {
|
|
374
|
+
if (input.error instanceof ProviderFailure) return input.error;
|
|
375
|
+
const text = providerFailureText(input);
|
|
376
|
+
const parsed = providerFailureBody(input.body ?? providerFailureRecord(input.error) ?? text);
|
|
377
|
+
const signals = providerFailureSignals(parsed);
|
|
378
|
+
const trusted = input.trustedEnvelope === true ? trustedProviderFailureEnvelope(parsed) : undefined;
|
|
379
|
+
const status = providerFailureStatus(input, text);
|
|
380
|
+
// A parsed provider object contributes its structured signals and prose,
|
|
381
|
+
// but never its untrusted `cause` value. That preserves message-only
|
|
382
|
+
// adapters without re-reading `cause: rate-limited` through the regex.
|
|
383
|
+
const unstructuredText =
|
|
384
|
+
input.body !== undefined && (providerFailureRecord(parsed) !== undefined || Array.isArray(parsed))
|
|
385
|
+
? providerFailureProse(parsed)
|
|
386
|
+
: text;
|
|
387
|
+
const transientTransport =
|
|
388
|
+
status === 408 ||
|
|
389
|
+
status === 425 ||
|
|
390
|
+
(input.error instanceof Error && (input.error.name === "AbortError" || input.error.name === "TimeoutError")) ||
|
|
391
|
+
(status !== undefined && status >= 500) ||
|
|
392
|
+
/stream ended before message_stop|stream ended without finish_reason|ended before completion/i.test(
|
|
393
|
+
unstructuredText,
|
|
394
|
+
) ||
|
|
395
|
+
/^(?:(?:AbortError:\s*)?(?:This|The) operation was aborted|Request aborted)\.?$/i.test(unstructuredText.trim()) ||
|
|
396
|
+
/ECONNRESET|ETIMEDOUT|EPIPE|socket hang up|fetch failed|other side closed|network (?:error|failure)|(?:connection|stream) (?:reset|closed|terminated)|timed? ?out/i.test(
|
|
397
|
+
unstructuredText,
|
|
398
|
+
) ||
|
|
399
|
+
/^terminated$/i.test(unstructuredText.trim()) ||
|
|
400
|
+
/\boverloaded\b/i.test(unstructuredText) ||
|
|
401
|
+
/<html[\s>][\s\S]{0,4000}\b(?:bad gateway|service unavailable|gateway timeout)\b/i.test(unstructuredText);
|
|
402
|
+
let cause: ProviderFailureCause;
|
|
403
|
+
if (trusted !== undefined) cause = trusted.cause;
|
|
404
|
+
else if (status === 402) cause = "credit-or-quota-exhausted";
|
|
405
|
+
else if (status === 429) cause = "rate-limited";
|
|
406
|
+
else if (transientTransport) cause = "transient";
|
|
407
|
+
else if (
|
|
408
|
+
signalIncludes(signals, [
|
|
409
|
+
"limit_source",
|
|
410
|
+
"insufficient_quota",
|
|
411
|
+
"quota_exceeded",
|
|
412
|
+
"credit_limit",
|
|
413
|
+
"billing_hard_limit",
|
|
414
|
+
"payment_required",
|
|
415
|
+
]) ||
|
|
416
|
+
(providerFailureRecord(parsed)?.metadata !== undefined &&
|
|
417
|
+
providerFailureRecord(providerFailureRecord(parsed)?.metadata)?.limit_source !== undefined)
|
|
418
|
+
)
|
|
419
|
+
cause = "credit-or-quota-exhausted";
|
|
420
|
+
else if (
|
|
421
|
+
signalIncludes(signals, ["rate_limit", "too_many_requests"]) ||
|
|
422
|
+
/\brate[- ]limit(?:ed)?\b/i.test(unstructuredText)
|
|
423
|
+
)
|
|
424
|
+
cause = "rate-limited";
|
|
425
|
+
else if (signalIncludes(signals, ["provider_key_missing", "key_absent", "missing_api_key"])) cause = "key-absent";
|
|
426
|
+
else if (
|
|
427
|
+
status === 401 ||
|
|
428
|
+
signalIncludes(signals, ["authentication_error", "invalid_api_key", "invalid_key", "unauthorized"])
|
|
429
|
+
)
|
|
430
|
+
cause = "key-invalid";
|
|
431
|
+
else if (signalIncludes(signals, ["model_not_found", "unknown_model", "model_unknown"])) cause = "model-unknown";
|
|
432
|
+
else if (
|
|
433
|
+
status === 400 ||
|
|
434
|
+
status === 422 ||
|
|
435
|
+
signalIncludes(signals, ["invalid_request_error", "invalid_json_schema", "bad_request", "unprocessable_entity"])
|
|
436
|
+
)
|
|
437
|
+
cause = "request-rejected";
|
|
438
|
+
else cause = "permanent";
|
|
439
|
+
const operatorUrl = /https?:\/\/[^\s"'<>]+/.exec(text)?.[0];
|
|
440
|
+
return new ProviderFailure(cause, {
|
|
441
|
+
...(status !== undefined ? { status } : {}),
|
|
442
|
+
...(input.provider !== undefined ? { provider: input.provider } : {}),
|
|
443
|
+
...(input.model !== undefined ? { model: input.model } : {}),
|
|
444
|
+
...(operatorUrl !== undefined ? { operatorUrl } : {}),
|
|
445
|
+
...(trusted?.schemaRejection !== undefined ? { schemaRejection: trusted.schemaRejection } : {}),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function typedProviderFailureOf(error: unknown, seen = new Set<object>()): ProviderFailure | undefined {
|
|
450
|
+
if (error instanceof ProviderFailure) return error;
|
|
451
|
+
const row = providerFailureRecord(error);
|
|
452
|
+
if (!row || seen.has(row)) return undefined;
|
|
453
|
+
seen.add(row);
|
|
454
|
+
if (row.name === "ProviderFailure" && isProviderFailureCause(row.cause)) {
|
|
455
|
+
return new ProviderFailure(row.cause, {
|
|
456
|
+
...(typeof row.status === "number" ? { status: row.status } : {}),
|
|
457
|
+
...(typeof row.provider === "string" ? { provider: row.provider } : {}),
|
|
458
|
+
...(typeof row.model === "string" ? { model: row.model } : {}),
|
|
459
|
+
...(typeof row.operatorUrl === "string" ? { operatorUrl: row.operatorUrl } : {}),
|
|
460
|
+
...(typeof row.keyVariable === "string" ? { keyVariable: row.keyVariable } : {}),
|
|
461
|
+
...(providerSchemaRejectionRecord(row.schemaRejection) !== undefined
|
|
462
|
+
? { schemaRejection: providerSchemaRejectionRecord(row.schemaRejection)! }
|
|
463
|
+
: {}),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
// StructuredAskError and other boundary wrappers preserve the original
|
|
467
|
+
// throw as `cause`. Follow typed causes through those wrappers before the
|
|
468
|
+
// outer error's copied message can be classified without its status/body.
|
|
469
|
+
return typedProviderFailureOf(row.cause, seen);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function providerFailureOf(error: unknown): ProviderFailure {
|
|
473
|
+
return typedProviderFailureOf(error) ?? classifyProviderFailure({ error });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Causes that make the provider unavailable rather than ending one call. */
|
|
477
|
+
export function providerFailureParks(cause: ProviderFailureCause): boolean {
|
|
478
|
+
return cause === "transient" || cause === "rate-limited" || cause === "credit-or-quota-exhausted";
|
|
479
|
+
}
|
|
480
|
+
|
|
101
481
|
/** The three wire shapes a provider block may declare (record 0052):
|
|
102
482
|
* Anthropic's Messages API, OpenAI's Chat Completions and OpenAI's Responses
|
|
103
483
|
* API. Each is a proxy route of its own (`PROXY_PATHS`): an `openai-responses`
|
|
@@ -103,6 +103,7 @@ const CAUSE_OF = {
|
|
|
103
103
|
plan_runner_state_unread: "system",
|
|
104
104
|
plan_units_merged: "request",
|
|
105
105
|
plan_history_unavailable: "system",
|
|
106
|
+
decision_record_store_unavailable: "system",
|
|
106
107
|
plan_runner_conflict: "system",
|
|
107
108
|
plan_instance_orphaned: "system",
|
|
108
109
|
plan_start_failed: "system",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { DescriptionIssue, PrDescription, RecordedJson, RenderedPointer } from "./prDescriptionTypes.js";
|
|
5
5
|
import type { HarnessScope } from "./harness/scope.js";
|
|
6
6
|
import type { ModelCard } from "./modelCard.js";
|
|
7
|
+
import type { ProviderFailureCause } from "./provider.js";
|
|
7
8
|
|
|
8
9
|
/** The `pr_description` review artifact minus the event envelope
|
|
9
10
|
* (docs/reference/specs/reading-diff.md item 7). */
|
|
@@ -752,6 +753,10 @@ export type RunEvent =
|
|
|
752
753
|
agentSource?: AgentSource;
|
|
753
754
|
/** Absent on a command run, which resolves no model. */
|
|
754
755
|
model?: string;
|
|
756
|
+
/** Decision-record reservation carried in a coding child's brief. */
|
|
757
|
+
record?: string;
|
|
758
|
+
/** Stable direct-task key used to recover that reservation on a re-issue. */
|
|
759
|
+
recordTaskKey?: string;
|
|
755
760
|
/** The request's trace id (docs/reference/specs/tracing.md), once the root exists. */
|
|
756
761
|
traceId?: string;
|
|
757
762
|
/** The harness the run is driven by (`Harness.name`; docs/reference/specs/harness.md
|
|
@@ -1118,6 +1123,9 @@ export type RunEvent =
|
|
|
1118
1123
|
request?: string;
|
|
1119
1124
|
refusalCause?: string;
|
|
1120
1125
|
refusalText?: string;
|
|
1126
|
+
/** The typed cause when the operator model call failed at the provider
|
|
1127
|
+
* boundary. Its refusal text is always the cause-owned renderer. */
|
|
1128
|
+
providerFailure?: ProviderFailureCause;
|
|
1121
1129
|
attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
|
|
1122
1130
|
intake?: { verdict: string; reason: string };
|
|
1123
1131
|
latencyMs?: number;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// bot and by `deploy/cloudflare-memory/worker.ts` alike, the way runRecord.ts is.
|
|
5
5
|
|
|
6
6
|
import type { ChatMessage } from "../chatMessage.js";
|
|
7
|
-
import type
|
|
7
|
+
import { PROVIDER_FAILURE_CAUSES, type ProviderFailureCause, type ToolDef } from "../provider.js";
|
|
8
8
|
import type { ChannelVisibility } from "../authz/types.js";
|
|
9
9
|
import type { RunProfile } from "../../config/profile.js";
|
|
10
10
|
import type { RunEvent } from "../runEvents.js";
|
|
@@ -59,6 +59,10 @@ export interface LiveRunMeta {
|
|
|
59
59
|
/** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
|
|
60
60
|
postedBy?: string;
|
|
61
61
|
effort?: string;
|
|
62
|
+
/** Decision-record reservation assigned before the attach, so a restart of
|
|
63
|
+
* the admitted task keeps the same brief and process environment. */
|
|
64
|
+
record?: string;
|
|
65
|
+
recordTaskKey?: string;
|
|
62
66
|
/** A ship pipeline's parent (record 0060): claimed under the host key
|
|
63
67
|
* (`hostKey.ts`) while `threadKey` here names the thread itself, so every
|
|
64
68
|
* record, notice and rebuilt handle files by the metadata's thread and the
|
|
@@ -296,6 +300,8 @@ export interface IntakeReceipt {
|
|
|
296
300
|
verdict: "addressed" | "silent";
|
|
297
301
|
reason: string;
|
|
298
302
|
source: "model" | "mode" | "question" | "error" | "timeout";
|
|
303
|
+
/** Present only when `source: error` is a failed provider call. */
|
|
304
|
+
providerFailure?: ProviderFailureCause;
|
|
299
305
|
/** The structured seam's attempts (docs/decisions/0067): what each answer
|
|
300
306
|
* violated, or that it was accepted; absent when no model was asked. */
|
|
301
307
|
attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
|
|
@@ -336,6 +342,9 @@ export function isIntakeReceipt(v: unknown): v is IntakeReceipt {
|
|
|
336
342
|
r.source === "question" ||
|
|
337
343
|
r.source === "error" ||
|
|
338
344
|
r.source === "timeout") &&
|
|
345
|
+
(r.providerFailure === undefined ||
|
|
346
|
+
(typeof r.providerFailure === "string" &&
|
|
347
|
+
(PROVIDER_FAILURE_CAUSES as readonly string[]).includes(r.providerFailure))) &&
|
|
339
348
|
(r.mode === "mention" || r.mode === "classify") &&
|
|
340
349
|
typeof r.model === "string" &&
|
|
341
350
|
typeof r.gen === "number" &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChannelVisibility, Predicate } from "./authz/types.js";
|
|
2
2
|
import type { BoundaryScope, Identity, MachineClass, RunProfile } from "../config/profile.js";
|
|
3
3
|
import type { RunEvent } from "./runEvents.js";
|
|
4
|
+
import type { ProviderFailureCause } from "./provider.js";
|
|
4
5
|
import { isHeadMaterial, isSpanRecord } from "./runEvents.js";
|
|
5
6
|
import { isRunUsage, type RunUsage } from "./runUsage.js";
|
|
6
7
|
import type { PushedBranch } from "../execution/residentRebind.js";
|
|
@@ -153,6 +154,16 @@ export interface RunRecord {
|
|
|
153
154
|
/** The thread that started the run (`IncomingMessage.sourceUrl`), for the
|
|
154
155
|
* index's hover link. Optional as above. */
|
|
155
156
|
sourceUrl?: string;
|
|
157
|
+
/** The decision-record number the runner reserved and passed in this coding
|
|
158
|
+
* run's brief. Absent when the task writes no record. */
|
|
159
|
+
record?: string;
|
|
160
|
+
/** Stable text-free key for a direct task's re-issue. */
|
|
161
|
+
recordTaskKey?: string;
|
|
162
|
+
/** The final workspace head the run loop independently observed (7 to 40
|
|
163
|
+
* lowercase hex), after every tail step and mechanical salvage. Present
|
|
164
|
+
* only when the workspace had a readable Git HEAD; absent on older records
|
|
165
|
+
* and runs without a Git workspace. */
|
|
166
|
+
headSha?: string;
|
|
156
167
|
/** The typed handoff a coding child submitted (docs/reference/specs/agent-ship.md
|
|
157
168
|
* item 14): its deviations from the plan unit, its follow-ups and the
|
|
158
169
|
* criteria it could not prove — redacted like every stored string. Present
|
|
@@ -327,6 +338,20 @@ export function instanceIdOfEvents(events: readonly RunEvent[]): string | undefi
|
|
|
327
338
|
return id;
|
|
328
339
|
}
|
|
329
340
|
|
|
341
|
+
/** The reservation carried by the last run_meta that names one. */
|
|
342
|
+
export function decisionRecordOfEvents(
|
|
343
|
+
events: readonly RunEvent[],
|
|
344
|
+
): { record: string; recordTaskKey?: string } | undefined {
|
|
345
|
+
let reservation: { record: string; recordTaskKey?: string } | undefined;
|
|
346
|
+
for (const event of events)
|
|
347
|
+
if (event.type === "run_meta" && event.record !== undefined)
|
|
348
|
+
reservation = {
|
|
349
|
+
record: event.record,
|
|
350
|
+
...(event.recordTaskKey !== undefined ? { recordTaskKey: event.recordTaskKey } : {}),
|
|
351
|
+
};
|
|
352
|
+
return reservation;
|
|
353
|
+
}
|
|
354
|
+
|
|
330
355
|
/** The pull request a run's events say it opened or edited — the last
|
|
331
356
|
* `pr_opened` wins, as an edit after an open names the same PR — or nothing. */
|
|
332
357
|
export function prOfEvents(events: readonly RunEvent[]): RunPullRequest | undefined {
|
|
@@ -453,6 +478,7 @@ export interface RunOperatorDecision {
|
|
|
453
478
|
request?: string;
|
|
454
479
|
refusalCause?: string;
|
|
455
480
|
refusalText?: string;
|
|
481
|
+
providerFailure?: ProviderFailureCause;
|
|
456
482
|
/** The structured seam's attempts (record 0067): what each answer violated,
|
|
457
483
|
* or that it was accepted. */
|
|
458
484
|
attempts?: { outcome: "accepted" | "violation"; violation?: string }[];
|
|
@@ -488,6 +514,7 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
|
|
|
488
514
|
...(e.request !== undefined ? { request: e.request } : {}),
|
|
489
515
|
...(e.refusalCause !== undefined ? { refusalCause: e.refusalCause } : {}),
|
|
490
516
|
...(e.refusalText !== undefined ? { refusalText: e.refusalText } : {}),
|
|
517
|
+
...(e.providerFailure !== undefined ? { providerFailure: e.providerFailure } : {}),
|
|
491
518
|
...(e.attempts
|
|
492
519
|
? {
|
|
493
520
|
attempts: e.attempts.map((a) => ({
|
|
@@ -1011,6 +1038,10 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
1011
1038
|
)
|
|
1012
1039
|
return false;
|
|
1013
1040
|
if (!isOptionalString(r.activity) || !isOptionalString(r.sourceUrl) || !isOptionalString(r.userName)) return false;
|
|
1041
|
+
if (r.record !== undefined && (typeof r.record !== "string" || !/^\d{4}$/.test(r.record))) return false;
|
|
1042
|
+
if (r.recordTaskKey !== undefined && (typeof r.recordTaskKey !== "string" || !/^[0-9a-f]{16}$/.test(r.recordTaskKey)))
|
|
1043
|
+
return false;
|
|
1044
|
+
if (r.headSha !== undefined && (typeof r.headSha !== "string" || !REVIEW_HEAD_PATTERN.test(r.headSha))) return false;
|
|
1014
1045
|
// The handoff is checked for shape, not bounds (docs/reference/specs/agent-ship.md
|
|
1015
1046
|
// item 14): redaction may lengthen a stored string past the tool's limit.
|
|
1016
1047
|
if (r.handoff !== undefined && !isHandoffShape(r.handoff)) return false;
|
|
@@ -65,6 +65,8 @@ export interface Guard {
|
|
|
65
65
|
|
|
66
66
|
export interface ChildContract {
|
|
67
67
|
unit: ContractUnit;
|
|
68
|
+
/** The decision-record number reserved for this unit at admission. */
|
|
69
|
+
record?: string;
|
|
68
70
|
specRows: ContractSpecRow[];
|
|
69
71
|
/** Absent when the repository has neither rules file. */
|
|
70
72
|
agentRules: AgentRules | undefined;
|