ai-resilience 0.1.1 → 0.2.0
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/README.md +70 -2
- package/dist/index.cjs +402 -2
- package/dist/index.d.cts +135 -1
- package/dist/index.d.ts +135 -1
- package/dist/index.js +386 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ai-resilience
|
|
2
2
|
|
|
3
|
-
`ai-resilience` is an axios-retry++
|
|
3
|
+
`ai-resilience` is an axios-retry++ toolkit for modern AI and backend systems. It keeps axios compatibility while adding configurable retry strategies, advanced jitter, semantic recovery, JSON validation and repair, hooks, structured logging, and strong TypeScript types.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -12,7 +12,8 @@ npm install ai-resilience axios axios-retry
|
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
import axios from "axios";
|
|
15
|
-
import { applyAiResilience, ConsoleRetryLogger } from "ai-resilience";
|
|
15
|
+
import { applyAiResilience, ConsoleRetryLogger, requestWithSemanticRetry } from "ai-resilience";
|
|
16
|
+
import { z } from "zod";
|
|
16
17
|
|
|
17
18
|
const client = axios.create({ baseURL: "https://api.example.com" });
|
|
18
19
|
|
|
@@ -29,6 +30,13 @@ applyAiResilience(client, {
|
|
|
29
30
|
},
|
|
30
31
|
},
|
|
31
32
|
});
|
|
33
|
+
|
|
34
|
+
const result = await requestWithSemanticRetry(client, {
|
|
35
|
+
request: { method: "post", url: "/chat/completions", data: { prompt: "Return JSON" } },
|
|
36
|
+
schema: z.object({ answer: z.string() }),
|
|
37
|
+
repairJson: true,
|
|
38
|
+
requireJson: true,
|
|
39
|
+
});
|
|
32
40
|
```
|
|
33
41
|
|
|
34
42
|
## Features
|
|
@@ -40,6 +48,10 @@ applyAiResilience(client, {
|
|
|
40
48
|
- Async custom retry conditions
|
|
41
49
|
- EventEmitter-based hooks plus direct hook callbacks
|
|
42
50
|
- Structured logger interface and console logger
|
|
51
|
+
- Semantic retry for valid HTTP responses with invalid AI payloads
|
|
52
|
+
- JSON validation, Zod schema validation, and JSON repair
|
|
53
|
+
- AI-aware failure detection for empty responses, refusals, truncation, and schema mismatches
|
|
54
|
+
- AI retry policies and semantic lifecycle hooks
|
|
43
55
|
- TypeScript-first public API
|
|
44
56
|
|
|
45
57
|
## API
|
|
@@ -52,6 +64,42 @@ Installs retry behavior on an existing axios instance and returns `{ axios, hook
|
|
|
52
64
|
|
|
53
65
|
Creates a new axios instance with retry behavior already applied.
|
|
54
66
|
|
|
67
|
+
### `requestWithSemanticRetry(instance, config)`
|
|
68
|
+
|
|
69
|
+
Runs an axios request and retries when the response is semantically invalid, such as malformed JSON, a schema mismatch, an empty body, a refusal, or a truncated answer.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { requestWithSemanticRetry } from "ai-resilience";
|
|
73
|
+
import { z } from "zod";
|
|
74
|
+
|
|
75
|
+
const data = await requestWithSemanticRetry(client, {
|
|
76
|
+
request: { url: "/generate", method: "post", data: { prompt } },
|
|
77
|
+
schema: z.object({
|
|
78
|
+
title: z.string(),
|
|
79
|
+
tags: z.array(z.string()),
|
|
80
|
+
}),
|
|
81
|
+
repairJson: true,
|
|
82
|
+
requireJson: true,
|
|
83
|
+
hooks: {
|
|
84
|
+
onSemanticRetry: ({ issue, attempt }) => {
|
|
85
|
+
console.log(`semantic retry ${attempt}: ${issue.kind}`);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `semanticRetry(operation, policy)`
|
|
92
|
+
|
|
93
|
+
Wraps any async operation that returns either an axios response or raw data and retries until the payload passes semantic validation.
|
|
94
|
+
|
|
95
|
+
### `parseJsonResponse(data, options)`
|
|
96
|
+
|
|
97
|
+
Parses JSON strings, optionally repairs common model-output issues, and validates against a Zod schema.
|
|
98
|
+
|
|
99
|
+
### `applySemanticRecovery(instance, policy)`
|
|
100
|
+
|
|
101
|
+
Installs a response interceptor that validates and repairs successful axios responses. Use `requestWithSemanticRetry` when you also want semantic retries.
|
|
102
|
+
|
|
55
103
|
### Retry config
|
|
56
104
|
|
|
57
105
|
```ts
|
|
@@ -70,6 +118,26 @@ type AiResilienceRetryConfig = {
|
|
|
70
118
|
};
|
|
71
119
|
```
|
|
72
120
|
|
|
121
|
+
### Semantic policy
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
type AiRetryPolicy = {
|
|
125
|
+
maxSemanticRetries?: number;
|
|
126
|
+
retryOnFailureKinds?: Array<
|
|
127
|
+
"invalid_json" | "schema_mismatch" | "empty_response" | "refusal" | "truncated"
|
|
128
|
+
>;
|
|
129
|
+
repairJson?: boolean;
|
|
130
|
+
requireJson?: boolean;
|
|
131
|
+
detectRefusals?: boolean;
|
|
132
|
+
detectTruncation?: boolean;
|
|
133
|
+
schema?: z.ZodType;
|
|
134
|
+
validate?: (data, response) => SemanticValidationResult | Promise<SemanticValidationResult>;
|
|
135
|
+
hooks?: SemanticRetryHooks;
|
|
136
|
+
logger?: RetryLogger;
|
|
137
|
+
metadata?: Record<string, unknown>;
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
73
141
|
## Scripts
|
|
74
142
|
|
|
75
143
|
```sh
|
package/dist/index.cjs
CHANGED
|
@@ -32,18 +32,33 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ConsoleRetryLogger: () => ConsoleRetryLogger,
|
|
34
34
|
RetryHookEmitter: () => RetryHookEmitter,
|
|
35
|
+
SemanticRetryError: () => SemanticRetryError,
|
|
36
|
+
aiRetryPolicies: () => aiRetryPolicies,
|
|
35
37
|
applyAiResilience: () => applyAiResilience,
|
|
36
38
|
applyJitter: () => applyJitter,
|
|
39
|
+
applySemanticRecovery: () => applySemanticRecovery,
|
|
37
40
|
calculateRetryDelay: () => calculateRetryDelay,
|
|
41
|
+
classifyAiFailure: () => classifyAiFailure,
|
|
38
42
|
createAiResilienceClient: () => createAiResilienceClient,
|
|
39
43
|
createLogEntry: () => createLogEntry,
|
|
44
|
+
createSemanticRetryCondition: () => createSemanticRetryCondition,
|
|
40
45
|
defaultRetryConfig: () => defaultRetryConfig,
|
|
46
|
+
detectAiFailureSignals: () => detectAiFailureSignals,
|
|
41
47
|
isRetryableMethod: () => isRetryableMethod,
|
|
42
48
|
isRetryableStatus: () => isRetryableStatus,
|
|
49
|
+
normalizeAiRetryPolicy: () => normalizeAiRetryPolicy,
|
|
43
50
|
normalizeRetryConfig: () => normalizeRetryConfig,
|
|
51
|
+
parseJsonResponse: () => parseJsonResponse,
|
|
52
|
+
repairJsonString: () => repairJsonString,
|
|
53
|
+
requestWithSemanticRetry: () => requestWithSemanticRetry,
|
|
54
|
+
resolveAiRetryPolicy: () => resolveAiRetryPolicy,
|
|
44
55
|
resolveRetryStrategy: () => resolveRetryStrategy,
|
|
45
56
|
retryStrategies: () => retryStrategies,
|
|
46
|
-
|
|
57
|
+
semanticRetry: () => semanticRetry,
|
|
58
|
+
shouldRetry: () => shouldRetry,
|
|
59
|
+
shouldSemanticRetry: () => shouldSemanticRetry,
|
|
60
|
+
validateSchema: () => validateSchema,
|
|
61
|
+
validateSemanticResponse: () => validateSemanticResponse
|
|
47
62
|
});
|
|
48
63
|
module.exports = __toCommonJS(index_exports);
|
|
49
64
|
|
|
@@ -307,20 +322,405 @@ var retryStrategies = {
|
|
|
307
322
|
function resolveRetryStrategy(strategy) {
|
|
308
323
|
return typeof strategy === "string" ? retryStrategies[strategy] : strategy;
|
|
309
324
|
}
|
|
325
|
+
|
|
326
|
+
// src/ai-policies.ts
|
|
327
|
+
var aiRetryPolicies = {
|
|
328
|
+
strictJson: {
|
|
329
|
+
maxSemanticRetries: 2,
|
|
330
|
+
retryOnFailureKinds: ["invalid_json", "schema_mismatch", "empty_response", "truncated"],
|
|
331
|
+
repairJson: true,
|
|
332
|
+
requireJson: true,
|
|
333
|
+
detectRefusals: true,
|
|
334
|
+
detectTruncation: true
|
|
335
|
+
},
|
|
336
|
+
toolCall: {
|
|
337
|
+
maxSemanticRetries: 3,
|
|
338
|
+
retryOnFailureKinds: ["invalid_json", "schema_mismatch", "empty_response", "refusal", "truncated"],
|
|
339
|
+
repairJson: true,
|
|
340
|
+
requireJson: true,
|
|
341
|
+
detectRefusals: true,
|
|
342
|
+
detectTruncation: true
|
|
343
|
+
},
|
|
344
|
+
permissive: {
|
|
345
|
+
maxSemanticRetries: 1,
|
|
346
|
+
retryOnFailureKinds: ["invalid_json", "empty_response"],
|
|
347
|
+
repairJson: true,
|
|
348
|
+
requireJson: false,
|
|
349
|
+
detectRefusals: false,
|
|
350
|
+
detectTruncation: true
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
function resolveAiRetryPolicy(policy) {
|
|
354
|
+
return typeof policy === "string" ? aiRetryPolicies[policy] : policy;
|
|
355
|
+
}
|
|
356
|
+
function shouldSemanticRetry(issue, policy = {}) {
|
|
357
|
+
const retryKinds = policy.retryOnFailureKinds ?? aiRetryPolicies.strictJson.retryOnFailureKinds;
|
|
358
|
+
return retryKinds.includes(issue.kind);
|
|
359
|
+
}
|
|
360
|
+
function createSemanticRetryCondition(policy = {}) {
|
|
361
|
+
return (issue) => shouldSemanticRetry(issue, policy);
|
|
362
|
+
}
|
|
363
|
+
function classifyAiFailure(error) {
|
|
364
|
+
if (error.name === "SemanticRetryError") {
|
|
365
|
+
return error.result.issues[0]?.kind ?? "unknown";
|
|
366
|
+
}
|
|
367
|
+
if (!error.response) {
|
|
368
|
+
return "network";
|
|
369
|
+
}
|
|
370
|
+
if (error.response.status === 429) {
|
|
371
|
+
return "rate_limit";
|
|
372
|
+
}
|
|
373
|
+
if (error.response.status >= 500) {
|
|
374
|
+
return "server_error";
|
|
375
|
+
}
|
|
376
|
+
return "unknown";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/repair.ts
|
|
380
|
+
function repairJsonString(input) {
|
|
381
|
+
const candidates = buildCandidates(input);
|
|
382
|
+
const issues = [];
|
|
383
|
+
for (const candidate of candidates) {
|
|
384
|
+
try {
|
|
385
|
+
return {
|
|
386
|
+
ok: true,
|
|
387
|
+
value: JSON.parse(candidate),
|
|
388
|
+
repairedText: candidate,
|
|
389
|
+
issues
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
issues.push({
|
|
393
|
+
kind: "invalid_json",
|
|
394
|
+
message: error instanceof Error ? error.message : "Invalid JSON",
|
|
395
|
+
cause: error
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
ok: false,
|
|
401
|
+
issues: issues.length ? issues : [
|
|
402
|
+
{
|
|
403
|
+
kind: "invalid_json",
|
|
404
|
+
message: "Unable to repair JSON response"
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function buildCandidates(input) {
|
|
410
|
+
const trimmed = stripMarkdownFence(input.trim());
|
|
411
|
+
const extracted = extractJsonCandidate(trimmed);
|
|
412
|
+
const normalized = normalizeCommonJsonIssues(extracted);
|
|
413
|
+
const balanced = balanceJson(normalized);
|
|
414
|
+
return Array.from(new Set([trimmed, extracted, normalized, balanced].filter(Boolean)));
|
|
415
|
+
}
|
|
416
|
+
function stripMarkdownFence(input) {
|
|
417
|
+
const fenceMatch = input.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
418
|
+
return fenceMatch?.[1]?.trim() ?? input;
|
|
419
|
+
}
|
|
420
|
+
function extractJsonCandidate(input) {
|
|
421
|
+
const firstObject = input.indexOf("{");
|
|
422
|
+
const firstArray = input.indexOf("[");
|
|
423
|
+
const starts = [firstObject, firstArray].filter((index) => index >= 0);
|
|
424
|
+
if (!starts.length) {
|
|
425
|
+
return input;
|
|
426
|
+
}
|
|
427
|
+
const start = Math.min(...starts);
|
|
428
|
+
const lastObject = input.lastIndexOf("}");
|
|
429
|
+
const lastArray = input.lastIndexOf("]");
|
|
430
|
+
const end = Math.max(lastObject, lastArray);
|
|
431
|
+
return end > start ? input.slice(start, end + 1).trim() : input.slice(start).trim();
|
|
432
|
+
}
|
|
433
|
+
function normalizeCommonJsonIssues(input) {
|
|
434
|
+
return input.replace(/[\u201C\u201D]/g, '"').replace(/[\u2018\u2019]/g, "'").replace(/,\s*([}\]])/g, "$1");
|
|
435
|
+
}
|
|
436
|
+
function balanceJson(input) {
|
|
437
|
+
const stack = [];
|
|
438
|
+
let inString = false;
|
|
439
|
+
let escaped = false;
|
|
440
|
+
for (const char of input) {
|
|
441
|
+
if (escaped) {
|
|
442
|
+
escaped = false;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (char === "\\") {
|
|
446
|
+
escaped = true;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (char === '"') {
|
|
450
|
+
inString = !inString;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (inString) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (char === "{") stack.push("}");
|
|
457
|
+
if (char === "[") stack.push("]");
|
|
458
|
+
if ((char === "}" || char === "]") && stack.at(-1) === char) stack.pop();
|
|
459
|
+
}
|
|
460
|
+
return input + stack.reverse().join("");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/json-validation.ts
|
|
464
|
+
function parseJsonResponse(input, options = {}) {
|
|
465
|
+
const issues = [];
|
|
466
|
+
let data = input;
|
|
467
|
+
let repaired = false;
|
|
468
|
+
if (typeof input === "string") {
|
|
469
|
+
const trimmed = input.trim();
|
|
470
|
+
if (!trimmed) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
issues: [{ kind: "empty_response", message: "Response body is empty" }],
|
|
474
|
+
raw: input
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
data = JSON.parse(trimmed);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (!options.repair) {
|
|
481
|
+
return {
|
|
482
|
+
ok: false,
|
|
483
|
+
issues: [
|
|
484
|
+
{
|
|
485
|
+
kind: "invalid_json",
|
|
486
|
+
message: error instanceof Error ? error.message : "Invalid JSON",
|
|
487
|
+
cause: error
|
|
488
|
+
}
|
|
489
|
+
],
|
|
490
|
+
raw: input
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const repairedResult = repairJsonString(trimmed);
|
|
494
|
+
if (!repairedResult.ok) {
|
|
495
|
+
return {
|
|
496
|
+
ok: false,
|
|
497
|
+
issues: repairedResult.issues,
|
|
498
|
+
raw: input
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
data = repairedResult.value;
|
|
502
|
+
repaired = repairedResult.repairedText !== trimmed;
|
|
503
|
+
}
|
|
504
|
+
} else if (options.requireJson && (input === null || typeof input !== "object")) {
|
|
505
|
+
issues.push({
|
|
506
|
+
kind: "invalid_json",
|
|
507
|
+
message: "Response data is not a JSON object or array"
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (issues.length) {
|
|
511
|
+
return { ok: false, issues, raw: input };
|
|
512
|
+
}
|
|
513
|
+
if (!options.schema) {
|
|
514
|
+
return {
|
|
515
|
+
ok: true,
|
|
516
|
+
data,
|
|
517
|
+
issues: [],
|
|
518
|
+
repaired,
|
|
519
|
+
raw: input
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return validateSchema(data, options.schema, { repaired, raw: input });
|
|
523
|
+
}
|
|
524
|
+
function validateSchema(data, schema, metadata = {}) {
|
|
525
|
+
const parsed = schema.safeParse(data);
|
|
526
|
+
if (parsed.success) {
|
|
527
|
+
return {
|
|
528
|
+
ok: true,
|
|
529
|
+
data: parsed.data,
|
|
530
|
+
issues: [],
|
|
531
|
+
...metadata
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
537
|
+
kind: "schema_mismatch",
|
|
538
|
+
message: issue.message,
|
|
539
|
+
path: issue.path,
|
|
540
|
+
cause: issue
|
|
541
|
+
})),
|
|
542
|
+
...metadata
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async function validateSemanticResponse(data, policy = {}, response) {
|
|
546
|
+
const parsed = parseJsonResponse(data, {
|
|
547
|
+
schema: policy.schema,
|
|
548
|
+
repair: policy.repairJson,
|
|
549
|
+
requireJson: policy.requireJson
|
|
550
|
+
});
|
|
551
|
+
if (!parsed.ok) {
|
|
552
|
+
return parsed;
|
|
553
|
+
}
|
|
554
|
+
const intelligenceIssues = detectAiFailureSignals(parsed.data, policy);
|
|
555
|
+
if (intelligenceIssues.length) {
|
|
556
|
+
return {
|
|
557
|
+
...parsed,
|
|
558
|
+
ok: false,
|
|
559
|
+
issues: intelligenceIssues
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (policy.validate) {
|
|
563
|
+
return policy.validate(parsed.data, response);
|
|
564
|
+
}
|
|
565
|
+
return parsed;
|
|
566
|
+
}
|
|
567
|
+
function detectAiFailureSignals(data, policy = {}) {
|
|
568
|
+
const text = typeof data === "string" ? data : JSON.stringify(data ?? "");
|
|
569
|
+
const issues = [];
|
|
570
|
+
if (policy.detectRefusals !== false && /\b(i can't|i cannot|unable to comply|cannot assist|as an ai)\b/i.test(text)) {
|
|
571
|
+
issues.push({
|
|
572
|
+
kind: "refusal",
|
|
573
|
+
message: "Response appears to be an AI refusal instead of the requested payload"
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (policy.detectTruncation !== false && /\b(truncated|continues?|unfinished|cut off)\b/i.test(text)) {
|
|
577
|
+
issues.push({
|
|
578
|
+
kind: "truncated",
|
|
579
|
+
message: "Response appears to be incomplete or truncated"
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
return issues;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/semantic-types.ts
|
|
586
|
+
var SemanticRetryError = class extends Error {
|
|
587
|
+
constructor(message, result, response) {
|
|
588
|
+
super(message);
|
|
589
|
+
this.result = result;
|
|
590
|
+
this.response = response;
|
|
591
|
+
}
|
|
592
|
+
result;
|
|
593
|
+
response;
|
|
594
|
+
name = "SemanticRetryError";
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/semantic.ts
|
|
598
|
+
function normalizeAiRetryPolicy(policy = {}) {
|
|
599
|
+
return {
|
|
600
|
+
...aiRetryPolicies.strictJson,
|
|
601
|
+
...policy
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
async function semanticRetry(operation, policy = {}) {
|
|
605
|
+
const resolved = normalizeAiRetryPolicy(policy);
|
|
606
|
+
let lastResult;
|
|
607
|
+
let lastResponse;
|
|
608
|
+
let previousDelayMs;
|
|
609
|
+
for (let attempt = 0; attempt <= resolved.maxSemanticRetries; attempt += 1) {
|
|
610
|
+
const value = await operation();
|
|
611
|
+
const response = isAxiosResponse(value) ? value : void 0;
|
|
612
|
+
const data = response ? response.data : value;
|
|
613
|
+
const finalResult = await validateSemanticResponse(data, resolved, response);
|
|
614
|
+
lastResult = finalResult;
|
|
615
|
+
lastResponse = response;
|
|
616
|
+
if (finalResult.ok) {
|
|
617
|
+
await resolved.hooks?.onSemanticRecovery?.({ result: finalResult, response });
|
|
618
|
+
return finalResult.data;
|
|
619
|
+
}
|
|
620
|
+
const issue = finalResult.issues[0] ?? {
|
|
621
|
+
kind: "unknown",
|
|
622
|
+
message: "Semantic validation failed"
|
|
623
|
+
};
|
|
624
|
+
if (attempt >= resolved.maxSemanticRetries || !shouldSemanticRetry(issue, resolved)) {
|
|
625
|
+
await resolved.hooks?.onSemanticGiveUp?.({
|
|
626
|
+
attempts: attempt + 1,
|
|
627
|
+
result: finalResult,
|
|
628
|
+
response
|
|
629
|
+
});
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
const nextDelayMs = calculateRetryDelay(
|
|
633
|
+
{
|
|
634
|
+
retries: resolved.maxSemanticRetries,
|
|
635
|
+
strategy: "exponential",
|
|
636
|
+
baseDelayMs: 100,
|
|
637
|
+
maxDelayMs: 3e3,
|
|
638
|
+
jitter: "full"
|
|
639
|
+
},
|
|
640
|
+
{ attempt: attempt + 1, previousDelayMs }
|
|
641
|
+
);
|
|
642
|
+
previousDelayMs = nextDelayMs;
|
|
643
|
+
await resolved.hooks?.onSemanticRetry?.({
|
|
644
|
+
attempt: attempt + 1,
|
|
645
|
+
maxRetries: resolved.maxSemanticRetries,
|
|
646
|
+
issue,
|
|
647
|
+
result: finalResult,
|
|
648
|
+
response,
|
|
649
|
+
nextDelayMs
|
|
650
|
+
});
|
|
651
|
+
resolved.logger?.log(
|
|
652
|
+
createLogEntry("warn", "semantic.retry", "Semantic retry scheduled", {
|
|
653
|
+
attempt: attempt + 1,
|
|
654
|
+
maxRetries: resolved.maxSemanticRetries,
|
|
655
|
+
delayMs: nextDelayMs,
|
|
656
|
+
issue: issue.kind,
|
|
657
|
+
...resolved.metadata
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
await sleep(nextDelayMs);
|
|
661
|
+
}
|
|
662
|
+
throw new SemanticRetryError("Semantic retry attempts exhausted", lastResult ?? emptyFailure(), lastResponse);
|
|
663
|
+
}
|
|
664
|
+
async function requestWithSemanticRetry(instance, config) {
|
|
665
|
+
return semanticRetry(() => instance.request(config.request), config);
|
|
666
|
+
}
|
|
667
|
+
function applySemanticRecovery(instance, policy = {}) {
|
|
668
|
+
const resolved = normalizeAiRetryPolicy(policy);
|
|
669
|
+
instance.interceptors.response.use(async (response) => {
|
|
670
|
+
const finalResult = await validateSemanticResponse(response.data, resolved, response);
|
|
671
|
+
if (!finalResult.ok) {
|
|
672
|
+
throw new SemanticRetryError("Semantic response validation failed", finalResult, response);
|
|
673
|
+
}
|
|
674
|
+
response.data = finalResult.data;
|
|
675
|
+
await resolved.hooks?.onSemanticRecovery?.({ result: finalResult, response });
|
|
676
|
+
return response;
|
|
677
|
+
});
|
|
678
|
+
return {
|
|
679
|
+
axios: instance,
|
|
680
|
+
policy: resolved
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function isAxiosResponse(value) {
|
|
684
|
+
return Boolean(value && typeof value === "object" && "status" in value && "headers" in value && "config" in value);
|
|
685
|
+
}
|
|
686
|
+
function sleep(ms) {
|
|
687
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
688
|
+
}
|
|
689
|
+
function emptyFailure() {
|
|
690
|
+
return {
|
|
691
|
+
ok: false,
|
|
692
|
+
issues: [{ kind: "unknown", message: "Semantic validation failed before a response was captured" }]
|
|
693
|
+
};
|
|
694
|
+
}
|
|
310
695
|
// Annotate the CommonJS export names for ESM import in node:
|
|
311
696
|
0 && (module.exports = {
|
|
312
697
|
ConsoleRetryLogger,
|
|
313
698
|
RetryHookEmitter,
|
|
699
|
+
SemanticRetryError,
|
|
700
|
+
aiRetryPolicies,
|
|
314
701
|
applyAiResilience,
|
|
315
702
|
applyJitter,
|
|
703
|
+
applySemanticRecovery,
|
|
316
704
|
calculateRetryDelay,
|
|
705
|
+
classifyAiFailure,
|
|
317
706
|
createAiResilienceClient,
|
|
318
707
|
createLogEntry,
|
|
708
|
+
createSemanticRetryCondition,
|
|
319
709
|
defaultRetryConfig,
|
|
710
|
+
detectAiFailureSignals,
|
|
320
711
|
isRetryableMethod,
|
|
321
712
|
isRetryableStatus,
|
|
713
|
+
normalizeAiRetryPolicy,
|
|
322
714
|
normalizeRetryConfig,
|
|
715
|
+
parseJsonResponse,
|
|
716
|
+
repairJsonString,
|
|
717
|
+
requestWithSemanticRetry,
|
|
718
|
+
resolveAiRetryPolicy,
|
|
323
719
|
resolveRetryStrategy,
|
|
324
720
|
retryStrategies,
|
|
325
|
-
|
|
721
|
+
semanticRetry,
|
|
722
|
+
shouldRetry,
|
|
723
|
+
shouldSemanticRetry,
|
|
724
|
+
validateSchema,
|
|
725
|
+
validateSemanticResponse
|
|
326
726
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import * as axios from 'axios';
|
|
1
2
|
import { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
2
3
|
import EventEmitter from 'eventemitter3';
|
|
3
4
|
import { IAxiosRetryConfig } from 'axios-retry';
|
|
5
|
+
import * as zod from 'zod';
|
|
6
|
+
import { z } from 'zod';
|
|
4
7
|
|
|
5
8
|
type RetryHookEvents<T = unknown> = {
|
|
6
9
|
retry: (context: RetryAttemptContext<T>) => void;
|
|
@@ -141,4 +144,135 @@ declare const retryStrategies: {
|
|
|
141
144
|
type NamedRetryStrategy = keyof typeof retryStrategies;
|
|
142
145
|
declare function resolveRetryStrategy(strategy: NamedRetryStrategy | AiResilienceRetryConfig): AiResilienceRetryConfig;
|
|
143
146
|
|
|
144
|
-
|
|
147
|
+
type AiFailureKind = "invalid_json" | "schema_mismatch" | "empty_response" | "refusal" | "truncated" | "rate_limit" | "server_error" | "network" | "unknown";
|
|
148
|
+
interface SemanticValidationIssue {
|
|
149
|
+
kind: AiFailureKind;
|
|
150
|
+
message: string;
|
|
151
|
+
path?: Array<string | number>;
|
|
152
|
+
cause?: unknown;
|
|
153
|
+
}
|
|
154
|
+
interface SemanticValidationResult<T = unknown> {
|
|
155
|
+
ok: boolean;
|
|
156
|
+
data?: T;
|
|
157
|
+
issues: SemanticValidationIssue[];
|
|
158
|
+
repaired?: boolean;
|
|
159
|
+
raw?: unknown;
|
|
160
|
+
}
|
|
161
|
+
interface JsonRepairResult {
|
|
162
|
+
ok: boolean;
|
|
163
|
+
value?: unknown;
|
|
164
|
+
repairedText?: string;
|
|
165
|
+
issues: SemanticValidationIssue[];
|
|
166
|
+
}
|
|
167
|
+
interface SemanticRetryContext<T = unknown> {
|
|
168
|
+
attempt: number;
|
|
169
|
+
maxRetries: number;
|
|
170
|
+
issue: SemanticValidationIssue;
|
|
171
|
+
result: SemanticValidationResult<T>;
|
|
172
|
+
response?: AxiosResponse;
|
|
173
|
+
nextDelayMs: number;
|
|
174
|
+
}
|
|
175
|
+
interface SemanticRecoveryContext<T = unknown> {
|
|
176
|
+
result: SemanticValidationResult<T>;
|
|
177
|
+
response?: AxiosResponse;
|
|
178
|
+
}
|
|
179
|
+
interface SemanticGiveUpContext<T = unknown> {
|
|
180
|
+
attempts: number;
|
|
181
|
+
result: SemanticValidationResult<T>;
|
|
182
|
+
response?: AxiosResponse;
|
|
183
|
+
}
|
|
184
|
+
interface SemanticRetryHooks<T = unknown> {
|
|
185
|
+
onSemanticRetry?: (context: SemanticRetryContext<T>) => void | Promise<void>;
|
|
186
|
+
onSemanticRecovery?: (context: SemanticRecoveryContext<T>) => void | Promise<void>;
|
|
187
|
+
onSemanticGiveUp?: (context: SemanticGiveUpContext<T>) => void | Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
interface AiRetryPolicy<T = unknown> {
|
|
190
|
+
maxSemanticRetries?: number;
|
|
191
|
+
retryOnFailureKinds?: AiFailureKind[];
|
|
192
|
+
repairJson?: boolean;
|
|
193
|
+
requireJson?: boolean;
|
|
194
|
+
detectRefusals?: boolean;
|
|
195
|
+
detectTruncation?: boolean;
|
|
196
|
+
schema?: z.ZodType<T>;
|
|
197
|
+
validate?: (data: unknown, response?: AxiosResponse) => SemanticValidationResult<T> | Promise<SemanticValidationResult<T>>;
|
|
198
|
+
hooks?: SemanticRetryHooks<T>;
|
|
199
|
+
logger?: RetryLogger;
|
|
200
|
+
metadata?: Record<string, unknown>;
|
|
201
|
+
}
|
|
202
|
+
interface SemanticRequestConfig<T = unknown> extends AiRetryPolicy<T> {
|
|
203
|
+
request: AxiosRequestConfig;
|
|
204
|
+
}
|
|
205
|
+
interface SemanticResilienceClient<T = unknown> {
|
|
206
|
+
axios: AxiosInstance;
|
|
207
|
+
policy: Required<Pick<AiRetryPolicy<T>, "maxSemanticRetries" | "retryOnFailureKinds" | "repairJson" | "requireJson" | "detectRefusals" | "detectTruncation">> & AiRetryPolicy<T>;
|
|
208
|
+
}
|
|
209
|
+
declare class SemanticRetryError<T = unknown> extends Error {
|
|
210
|
+
readonly result: SemanticValidationResult<T>;
|
|
211
|
+
readonly response?: AxiosResponse | undefined;
|
|
212
|
+
readonly name = "SemanticRetryError";
|
|
213
|
+
constructor(message: string, result: SemanticValidationResult<T>, response?: AxiosResponse | undefined);
|
|
214
|
+
}
|
|
215
|
+
type SemanticRetryCondition<T = unknown> = (issue: SemanticValidationIssue, error?: AxiosError | SemanticRetryError<T>) => boolean;
|
|
216
|
+
|
|
217
|
+
declare const aiRetryPolicies: {
|
|
218
|
+
readonly strictJson: {
|
|
219
|
+
readonly maxSemanticRetries: 2;
|
|
220
|
+
readonly retryOnFailureKinds: ["invalid_json", "schema_mismatch", "empty_response", "truncated"];
|
|
221
|
+
readonly repairJson: true;
|
|
222
|
+
readonly requireJson: true;
|
|
223
|
+
readonly detectRefusals: true;
|
|
224
|
+
readonly detectTruncation: true;
|
|
225
|
+
};
|
|
226
|
+
readonly toolCall: {
|
|
227
|
+
readonly maxSemanticRetries: 3;
|
|
228
|
+
readonly retryOnFailureKinds: ["invalid_json", "schema_mismatch", "empty_response", "refusal", "truncated"];
|
|
229
|
+
readonly repairJson: true;
|
|
230
|
+
readonly requireJson: true;
|
|
231
|
+
readonly detectRefusals: true;
|
|
232
|
+
readonly detectTruncation: true;
|
|
233
|
+
};
|
|
234
|
+
readonly permissive: {
|
|
235
|
+
readonly maxSemanticRetries: 1;
|
|
236
|
+
readonly retryOnFailureKinds: ["invalid_json", "empty_response"];
|
|
237
|
+
readonly repairJson: true;
|
|
238
|
+
readonly requireJson: false;
|
|
239
|
+
readonly detectRefusals: false;
|
|
240
|
+
readonly detectTruncation: true;
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
type NamedAiRetryPolicy = keyof typeof aiRetryPolicies;
|
|
244
|
+
declare function resolveAiRetryPolicy<T = unknown>(policy: NamedAiRetryPolicy | AiRetryPolicy<T>): AiRetryPolicy<T>;
|
|
245
|
+
declare function shouldSemanticRetry<T = unknown>(issue: SemanticValidationIssue, policy?: AiRetryPolicy<T>): boolean;
|
|
246
|
+
declare function createSemanticRetryCondition<T = unknown>(policy?: AiRetryPolicy<T>): SemanticRetryCondition<T>;
|
|
247
|
+
declare function classifyAiFailure(error: AxiosError | SemanticRetryError): AiFailureKind;
|
|
248
|
+
|
|
249
|
+
interface JsonValidationOptions<T = unknown> {
|
|
250
|
+
schema?: z.ZodType<T>;
|
|
251
|
+
repair?: boolean;
|
|
252
|
+
requireJson?: boolean;
|
|
253
|
+
}
|
|
254
|
+
declare function parseJsonResponse<T = unknown>(input: unknown, options?: JsonValidationOptions<T>): SemanticValidationResult<T>;
|
|
255
|
+
declare function validateSchema<T>(data: unknown, schema: z.ZodType<T>, metadata?: Pick<SemanticValidationResult<T>, "repaired" | "raw">): SemanticValidationResult<T>;
|
|
256
|
+
declare function validateSemanticResponse<T = unknown>(data: unknown, policy?: AiRetryPolicy<T>, response?: axios.AxiosResponse): Promise<SemanticValidationResult<T>>;
|
|
257
|
+
declare function detectAiFailureSignals<T = unknown>(data: T | undefined, policy?: Pick<AiRetryPolicy<T>, "detectRefusals" | "detectTruncation">): SemanticValidationIssue[];
|
|
258
|
+
|
|
259
|
+
declare function repairJsonString(input: string): JsonRepairResult;
|
|
260
|
+
|
|
261
|
+
declare function normalizeAiRetryPolicy<T = unknown>(policy?: AiRetryPolicy<T>): {
|
|
262
|
+
maxSemanticRetries: number;
|
|
263
|
+
retryOnFailureKinds: AiFailureKind[];
|
|
264
|
+
repairJson: boolean;
|
|
265
|
+
requireJson: boolean;
|
|
266
|
+
detectRefusals: boolean;
|
|
267
|
+
detectTruncation: boolean;
|
|
268
|
+
schema?: zod.ZodType<T, zod.ZodTypeDef, T> | undefined;
|
|
269
|
+
validate?: ((data: unknown, response?: AxiosResponse) => SemanticValidationResult<T> | Promise<SemanticValidationResult<T>>) | undefined;
|
|
270
|
+
hooks?: SemanticRetryHooks<T> | undefined;
|
|
271
|
+
logger?: RetryLogger;
|
|
272
|
+
metadata?: Record<string, unknown>;
|
|
273
|
+
};
|
|
274
|
+
declare function semanticRetry<T = unknown>(operation: () => Promise<AxiosResponse | T>, policy?: AiRetryPolicy<T>): Promise<T>;
|
|
275
|
+
declare function requestWithSemanticRetry<T = unknown>(instance: AxiosInstance, config: SemanticRequestConfig<T>): Promise<T>;
|
|
276
|
+
declare function applySemanticRecovery<T = unknown>(instance: AxiosInstance, policy?: AiRetryPolicy<T>): SemanticResilienceClient<T>;
|
|
277
|
+
|
|
278
|
+
export { type AiFailureKind, type AiResilienceClient, type AiResilienceRetryConfig, type AiRetryPolicy, ConsoleRetryLogger, type JitterStrategy, type JsonRepairResult, type LogLevel, type NamedAiRetryPolicy, type RetryAttemptContext, type RetryGiveUpContext, RetryHookEmitter, type RetryHooks, type RetryLogger, type RetryStrategy, type RetrySuccessContext, type SemanticGiveUpContext, type SemanticRecoveryContext, type SemanticRequestConfig, type SemanticResilienceClient, type SemanticRetryCondition, type SemanticRetryContext, SemanticRetryError, type SemanticRetryHooks, type SemanticValidationIssue, type SemanticValidationResult, type StructuredLogEntry, aiRetryPolicies, applyAiResilience, applyJitter, applySemanticRecovery, calculateRetryDelay, classifyAiFailure, createAiResilienceClient, createLogEntry, createSemanticRetryCondition, defaultRetryConfig, detectAiFailureSignals, isRetryableMethod, isRetryableStatus, normalizeAiRetryPolicy, normalizeRetryConfig, parseJsonResponse, repairJsonString, requestWithSemanticRetry, resolveAiRetryPolicy, resolveRetryStrategy, retryStrategies, semanticRetry, shouldRetry, shouldSemanticRetry, validateSchema, validateSemanticResponse };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import * as axios from 'axios';
|
|
1
2
|
import { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
2
3
|
import EventEmitter from 'eventemitter3';
|
|
3
4
|
import { IAxiosRetryConfig } from 'axios-retry';
|
|
5
|
+
import * as zod from 'zod';
|
|
6
|
+
import { z } from 'zod';
|
|
4
7
|
|
|
5
8
|
type RetryHookEvents<T = unknown> = {
|
|
6
9
|
retry: (context: RetryAttemptContext<T>) => void;
|
|
@@ -141,4 +144,135 @@ declare const retryStrategies: {
|
|
|
141
144
|
type NamedRetryStrategy = keyof typeof retryStrategies;
|
|
142
145
|
declare function resolveRetryStrategy(strategy: NamedRetryStrategy | AiResilienceRetryConfig): AiResilienceRetryConfig;
|
|
143
146
|
|
|
144
|
-
|
|
147
|
+
type AiFailureKind = "invalid_json" | "schema_mismatch" | "empty_response" | "refusal" | "truncated" | "rate_limit" | "server_error" | "network" | "unknown";
|
|
148
|
+
interface SemanticValidationIssue {
|
|
149
|
+
kind: AiFailureKind;
|
|
150
|
+
message: string;
|
|
151
|
+
path?: Array<string | number>;
|
|
152
|
+
cause?: unknown;
|
|
153
|
+
}
|
|
154
|
+
interface SemanticValidationResult<T = unknown> {
|
|
155
|
+
ok: boolean;
|
|
156
|
+
data?: T;
|
|
157
|
+
issues: SemanticValidationIssue[];
|
|
158
|
+
repaired?: boolean;
|
|
159
|
+
raw?: unknown;
|
|
160
|
+
}
|
|
161
|
+
interface JsonRepairResult {
|
|
162
|
+
ok: boolean;
|
|
163
|
+
value?: unknown;
|
|
164
|
+
repairedText?: string;
|
|
165
|
+
issues: SemanticValidationIssue[];
|
|
166
|
+
}
|
|
167
|
+
interface SemanticRetryContext<T = unknown> {
|
|
168
|
+
attempt: number;
|
|
169
|
+
maxRetries: number;
|
|
170
|
+
issue: SemanticValidationIssue;
|
|
171
|
+
result: SemanticValidationResult<T>;
|
|
172
|
+
response?: AxiosResponse;
|
|
173
|
+
nextDelayMs: number;
|
|
174
|
+
}
|
|
175
|
+
interface SemanticRecoveryContext<T = unknown> {
|
|
176
|
+
result: SemanticValidationResult<T>;
|
|
177
|
+
response?: AxiosResponse;
|
|
178
|
+
}
|
|
179
|
+
interface SemanticGiveUpContext<T = unknown> {
|
|
180
|
+
attempts: number;
|
|
181
|
+
result: SemanticValidationResult<T>;
|
|
182
|
+
response?: AxiosResponse;
|
|
183
|
+
}
|
|
184
|
+
interface SemanticRetryHooks<T = unknown> {
|
|
185
|
+
onSemanticRetry?: (context: SemanticRetryContext<T>) => void | Promise<void>;
|
|
186
|
+
onSemanticRecovery?: (context: SemanticRecoveryContext<T>) => void | Promise<void>;
|
|
187
|
+
onSemanticGiveUp?: (context: SemanticGiveUpContext<T>) => void | Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
interface AiRetryPolicy<T = unknown> {
|
|
190
|
+
maxSemanticRetries?: number;
|
|
191
|
+
retryOnFailureKinds?: AiFailureKind[];
|
|
192
|
+
repairJson?: boolean;
|
|
193
|
+
requireJson?: boolean;
|
|
194
|
+
detectRefusals?: boolean;
|
|
195
|
+
detectTruncation?: boolean;
|
|
196
|
+
schema?: z.ZodType<T>;
|
|
197
|
+
validate?: (data: unknown, response?: AxiosResponse) => SemanticValidationResult<T> | Promise<SemanticValidationResult<T>>;
|
|
198
|
+
hooks?: SemanticRetryHooks<T>;
|
|
199
|
+
logger?: RetryLogger;
|
|
200
|
+
metadata?: Record<string, unknown>;
|
|
201
|
+
}
|
|
202
|
+
interface SemanticRequestConfig<T = unknown> extends AiRetryPolicy<T> {
|
|
203
|
+
request: AxiosRequestConfig;
|
|
204
|
+
}
|
|
205
|
+
interface SemanticResilienceClient<T = unknown> {
|
|
206
|
+
axios: AxiosInstance;
|
|
207
|
+
policy: Required<Pick<AiRetryPolicy<T>, "maxSemanticRetries" | "retryOnFailureKinds" | "repairJson" | "requireJson" | "detectRefusals" | "detectTruncation">> & AiRetryPolicy<T>;
|
|
208
|
+
}
|
|
209
|
+
declare class SemanticRetryError<T = unknown> extends Error {
|
|
210
|
+
readonly result: SemanticValidationResult<T>;
|
|
211
|
+
readonly response?: AxiosResponse | undefined;
|
|
212
|
+
readonly name = "SemanticRetryError";
|
|
213
|
+
constructor(message: string, result: SemanticValidationResult<T>, response?: AxiosResponse | undefined);
|
|
214
|
+
}
|
|
215
|
+
type SemanticRetryCondition<T = unknown> = (issue: SemanticValidationIssue, error?: AxiosError | SemanticRetryError<T>) => boolean;
|
|
216
|
+
|
|
217
|
+
declare const aiRetryPolicies: {
|
|
218
|
+
readonly strictJson: {
|
|
219
|
+
readonly maxSemanticRetries: 2;
|
|
220
|
+
readonly retryOnFailureKinds: ["invalid_json", "schema_mismatch", "empty_response", "truncated"];
|
|
221
|
+
readonly repairJson: true;
|
|
222
|
+
readonly requireJson: true;
|
|
223
|
+
readonly detectRefusals: true;
|
|
224
|
+
readonly detectTruncation: true;
|
|
225
|
+
};
|
|
226
|
+
readonly toolCall: {
|
|
227
|
+
readonly maxSemanticRetries: 3;
|
|
228
|
+
readonly retryOnFailureKinds: ["invalid_json", "schema_mismatch", "empty_response", "refusal", "truncated"];
|
|
229
|
+
readonly repairJson: true;
|
|
230
|
+
readonly requireJson: true;
|
|
231
|
+
readonly detectRefusals: true;
|
|
232
|
+
readonly detectTruncation: true;
|
|
233
|
+
};
|
|
234
|
+
readonly permissive: {
|
|
235
|
+
readonly maxSemanticRetries: 1;
|
|
236
|
+
readonly retryOnFailureKinds: ["invalid_json", "empty_response"];
|
|
237
|
+
readonly repairJson: true;
|
|
238
|
+
readonly requireJson: false;
|
|
239
|
+
readonly detectRefusals: false;
|
|
240
|
+
readonly detectTruncation: true;
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
type NamedAiRetryPolicy = keyof typeof aiRetryPolicies;
|
|
244
|
+
declare function resolveAiRetryPolicy<T = unknown>(policy: NamedAiRetryPolicy | AiRetryPolicy<T>): AiRetryPolicy<T>;
|
|
245
|
+
declare function shouldSemanticRetry<T = unknown>(issue: SemanticValidationIssue, policy?: AiRetryPolicy<T>): boolean;
|
|
246
|
+
declare function createSemanticRetryCondition<T = unknown>(policy?: AiRetryPolicy<T>): SemanticRetryCondition<T>;
|
|
247
|
+
declare function classifyAiFailure(error: AxiosError | SemanticRetryError): AiFailureKind;
|
|
248
|
+
|
|
249
|
+
interface JsonValidationOptions<T = unknown> {
|
|
250
|
+
schema?: z.ZodType<T>;
|
|
251
|
+
repair?: boolean;
|
|
252
|
+
requireJson?: boolean;
|
|
253
|
+
}
|
|
254
|
+
declare function parseJsonResponse<T = unknown>(input: unknown, options?: JsonValidationOptions<T>): SemanticValidationResult<T>;
|
|
255
|
+
declare function validateSchema<T>(data: unknown, schema: z.ZodType<T>, metadata?: Pick<SemanticValidationResult<T>, "repaired" | "raw">): SemanticValidationResult<T>;
|
|
256
|
+
declare function validateSemanticResponse<T = unknown>(data: unknown, policy?: AiRetryPolicy<T>, response?: axios.AxiosResponse): Promise<SemanticValidationResult<T>>;
|
|
257
|
+
declare function detectAiFailureSignals<T = unknown>(data: T | undefined, policy?: Pick<AiRetryPolicy<T>, "detectRefusals" | "detectTruncation">): SemanticValidationIssue[];
|
|
258
|
+
|
|
259
|
+
declare function repairJsonString(input: string): JsonRepairResult;
|
|
260
|
+
|
|
261
|
+
declare function normalizeAiRetryPolicy<T = unknown>(policy?: AiRetryPolicy<T>): {
|
|
262
|
+
maxSemanticRetries: number;
|
|
263
|
+
retryOnFailureKinds: AiFailureKind[];
|
|
264
|
+
repairJson: boolean;
|
|
265
|
+
requireJson: boolean;
|
|
266
|
+
detectRefusals: boolean;
|
|
267
|
+
detectTruncation: boolean;
|
|
268
|
+
schema?: zod.ZodType<T, zod.ZodTypeDef, T> | undefined;
|
|
269
|
+
validate?: ((data: unknown, response?: AxiosResponse) => SemanticValidationResult<T> | Promise<SemanticValidationResult<T>>) | undefined;
|
|
270
|
+
hooks?: SemanticRetryHooks<T> | undefined;
|
|
271
|
+
logger?: RetryLogger;
|
|
272
|
+
metadata?: Record<string, unknown>;
|
|
273
|
+
};
|
|
274
|
+
declare function semanticRetry<T = unknown>(operation: () => Promise<AxiosResponse | T>, policy?: AiRetryPolicy<T>): Promise<T>;
|
|
275
|
+
declare function requestWithSemanticRetry<T = unknown>(instance: AxiosInstance, config: SemanticRequestConfig<T>): Promise<T>;
|
|
276
|
+
declare function applySemanticRecovery<T = unknown>(instance: AxiosInstance, policy?: AiRetryPolicy<T>): SemanticResilienceClient<T>;
|
|
277
|
+
|
|
278
|
+
export { type AiFailureKind, type AiResilienceClient, type AiResilienceRetryConfig, type AiRetryPolicy, ConsoleRetryLogger, type JitterStrategy, type JsonRepairResult, type LogLevel, type NamedAiRetryPolicy, type RetryAttemptContext, type RetryGiveUpContext, RetryHookEmitter, type RetryHooks, type RetryLogger, type RetryStrategy, type RetrySuccessContext, type SemanticGiveUpContext, type SemanticRecoveryContext, type SemanticRequestConfig, type SemanticResilienceClient, type SemanticRetryCondition, type SemanticRetryContext, SemanticRetryError, type SemanticRetryHooks, type SemanticValidationIssue, type SemanticValidationResult, type StructuredLogEntry, aiRetryPolicies, applyAiResilience, applyJitter, applySemanticRecovery, calculateRetryDelay, classifyAiFailure, createAiResilienceClient, createLogEntry, createSemanticRetryCondition, defaultRetryConfig, detectAiFailureSignals, isRetryableMethod, isRetryableStatus, normalizeAiRetryPolicy, normalizeRetryConfig, parseJsonResponse, repairJsonString, requestWithSemanticRetry, resolveAiRetryPolicy, resolveRetryStrategy, retryStrategies, semanticRetry, shouldRetry, shouldSemanticRetry, validateSchema, validateSemanticResponse };
|
package/dist/index.js
CHANGED
|
@@ -258,19 +258,404 @@ var retryStrategies = {
|
|
|
258
258
|
function resolveRetryStrategy(strategy) {
|
|
259
259
|
return typeof strategy === "string" ? retryStrategies[strategy] : strategy;
|
|
260
260
|
}
|
|
261
|
+
|
|
262
|
+
// src/ai-policies.ts
|
|
263
|
+
var aiRetryPolicies = {
|
|
264
|
+
strictJson: {
|
|
265
|
+
maxSemanticRetries: 2,
|
|
266
|
+
retryOnFailureKinds: ["invalid_json", "schema_mismatch", "empty_response", "truncated"],
|
|
267
|
+
repairJson: true,
|
|
268
|
+
requireJson: true,
|
|
269
|
+
detectRefusals: true,
|
|
270
|
+
detectTruncation: true
|
|
271
|
+
},
|
|
272
|
+
toolCall: {
|
|
273
|
+
maxSemanticRetries: 3,
|
|
274
|
+
retryOnFailureKinds: ["invalid_json", "schema_mismatch", "empty_response", "refusal", "truncated"],
|
|
275
|
+
repairJson: true,
|
|
276
|
+
requireJson: true,
|
|
277
|
+
detectRefusals: true,
|
|
278
|
+
detectTruncation: true
|
|
279
|
+
},
|
|
280
|
+
permissive: {
|
|
281
|
+
maxSemanticRetries: 1,
|
|
282
|
+
retryOnFailureKinds: ["invalid_json", "empty_response"],
|
|
283
|
+
repairJson: true,
|
|
284
|
+
requireJson: false,
|
|
285
|
+
detectRefusals: false,
|
|
286
|
+
detectTruncation: true
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function resolveAiRetryPolicy(policy) {
|
|
290
|
+
return typeof policy === "string" ? aiRetryPolicies[policy] : policy;
|
|
291
|
+
}
|
|
292
|
+
function shouldSemanticRetry(issue, policy = {}) {
|
|
293
|
+
const retryKinds = policy.retryOnFailureKinds ?? aiRetryPolicies.strictJson.retryOnFailureKinds;
|
|
294
|
+
return retryKinds.includes(issue.kind);
|
|
295
|
+
}
|
|
296
|
+
function createSemanticRetryCondition(policy = {}) {
|
|
297
|
+
return (issue) => shouldSemanticRetry(issue, policy);
|
|
298
|
+
}
|
|
299
|
+
function classifyAiFailure(error) {
|
|
300
|
+
if (error.name === "SemanticRetryError") {
|
|
301
|
+
return error.result.issues[0]?.kind ?? "unknown";
|
|
302
|
+
}
|
|
303
|
+
if (!error.response) {
|
|
304
|
+
return "network";
|
|
305
|
+
}
|
|
306
|
+
if (error.response.status === 429) {
|
|
307
|
+
return "rate_limit";
|
|
308
|
+
}
|
|
309
|
+
if (error.response.status >= 500) {
|
|
310
|
+
return "server_error";
|
|
311
|
+
}
|
|
312
|
+
return "unknown";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/repair.ts
|
|
316
|
+
function repairJsonString(input) {
|
|
317
|
+
const candidates = buildCandidates(input);
|
|
318
|
+
const issues = [];
|
|
319
|
+
for (const candidate of candidates) {
|
|
320
|
+
try {
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
value: JSON.parse(candidate),
|
|
324
|
+
repairedText: candidate,
|
|
325
|
+
issues
|
|
326
|
+
};
|
|
327
|
+
} catch (error) {
|
|
328
|
+
issues.push({
|
|
329
|
+
kind: "invalid_json",
|
|
330
|
+
message: error instanceof Error ? error.message : "Invalid JSON",
|
|
331
|
+
cause: error
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
issues: issues.length ? issues : [
|
|
338
|
+
{
|
|
339
|
+
kind: "invalid_json",
|
|
340
|
+
message: "Unable to repair JSON response"
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function buildCandidates(input) {
|
|
346
|
+
const trimmed = stripMarkdownFence(input.trim());
|
|
347
|
+
const extracted = extractJsonCandidate(trimmed);
|
|
348
|
+
const normalized = normalizeCommonJsonIssues(extracted);
|
|
349
|
+
const balanced = balanceJson(normalized);
|
|
350
|
+
return Array.from(new Set([trimmed, extracted, normalized, balanced].filter(Boolean)));
|
|
351
|
+
}
|
|
352
|
+
function stripMarkdownFence(input) {
|
|
353
|
+
const fenceMatch = input.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
354
|
+
return fenceMatch?.[1]?.trim() ?? input;
|
|
355
|
+
}
|
|
356
|
+
function extractJsonCandidate(input) {
|
|
357
|
+
const firstObject = input.indexOf("{");
|
|
358
|
+
const firstArray = input.indexOf("[");
|
|
359
|
+
const starts = [firstObject, firstArray].filter((index) => index >= 0);
|
|
360
|
+
if (!starts.length) {
|
|
361
|
+
return input;
|
|
362
|
+
}
|
|
363
|
+
const start = Math.min(...starts);
|
|
364
|
+
const lastObject = input.lastIndexOf("}");
|
|
365
|
+
const lastArray = input.lastIndexOf("]");
|
|
366
|
+
const end = Math.max(lastObject, lastArray);
|
|
367
|
+
return end > start ? input.slice(start, end + 1).trim() : input.slice(start).trim();
|
|
368
|
+
}
|
|
369
|
+
function normalizeCommonJsonIssues(input) {
|
|
370
|
+
return input.replace(/[\u201C\u201D]/g, '"').replace(/[\u2018\u2019]/g, "'").replace(/,\s*([}\]])/g, "$1");
|
|
371
|
+
}
|
|
372
|
+
function balanceJson(input) {
|
|
373
|
+
const stack = [];
|
|
374
|
+
let inString = false;
|
|
375
|
+
let escaped = false;
|
|
376
|
+
for (const char of input) {
|
|
377
|
+
if (escaped) {
|
|
378
|
+
escaped = false;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (char === "\\") {
|
|
382
|
+
escaped = true;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (char === '"') {
|
|
386
|
+
inString = !inString;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (inString) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (char === "{") stack.push("}");
|
|
393
|
+
if (char === "[") stack.push("]");
|
|
394
|
+
if ((char === "}" || char === "]") && stack.at(-1) === char) stack.pop();
|
|
395
|
+
}
|
|
396
|
+
return input + stack.reverse().join("");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/json-validation.ts
|
|
400
|
+
function parseJsonResponse(input, options = {}) {
|
|
401
|
+
const issues = [];
|
|
402
|
+
let data = input;
|
|
403
|
+
let repaired = false;
|
|
404
|
+
if (typeof input === "string") {
|
|
405
|
+
const trimmed = input.trim();
|
|
406
|
+
if (!trimmed) {
|
|
407
|
+
return {
|
|
408
|
+
ok: false,
|
|
409
|
+
issues: [{ kind: "empty_response", message: "Response body is empty" }],
|
|
410
|
+
raw: input
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
data = JSON.parse(trimmed);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (!options.repair) {
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
issues: [
|
|
420
|
+
{
|
|
421
|
+
kind: "invalid_json",
|
|
422
|
+
message: error instanceof Error ? error.message : "Invalid JSON",
|
|
423
|
+
cause: error
|
|
424
|
+
}
|
|
425
|
+
],
|
|
426
|
+
raw: input
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const repairedResult = repairJsonString(trimmed);
|
|
430
|
+
if (!repairedResult.ok) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
issues: repairedResult.issues,
|
|
434
|
+
raw: input
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
data = repairedResult.value;
|
|
438
|
+
repaired = repairedResult.repairedText !== trimmed;
|
|
439
|
+
}
|
|
440
|
+
} else if (options.requireJson && (input === null || typeof input !== "object")) {
|
|
441
|
+
issues.push({
|
|
442
|
+
kind: "invalid_json",
|
|
443
|
+
message: "Response data is not a JSON object or array"
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (issues.length) {
|
|
447
|
+
return { ok: false, issues, raw: input };
|
|
448
|
+
}
|
|
449
|
+
if (!options.schema) {
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
data,
|
|
453
|
+
issues: [],
|
|
454
|
+
repaired,
|
|
455
|
+
raw: input
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return validateSchema(data, options.schema, { repaired, raw: input });
|
|
459
|
+
}
|
|
460
|
+
function validateSchema(data, schema, metadata = {}) {
|
|
461
|
+
const parsed = schema.safeParse(data);
|
|
462
|
+
if (parsed.success) {
|
|
463
|
+
return {
|
|
464
|
+
ok: true,
|
|
465
|
+
data: parsed.data,
|
|
466
|
+
issues: [],
|
|
467
|
+
...metadata
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
473
|
+
kind: "schema_mismatch",
|
|
474
|
+
message: issue.message,
|
|
475
|
+
path: issue.path,
|
|
476
|
+
cause: issue
|
|
477
|
+
})),
|
|
478
|
+
...metadata
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async function validateSemanticResponse(data, policy = {}, response) {
|
|
482
|
+
const parsed = parseJsonResponse(data, {
|
|
483
|
+
schema: policy.schema,
|
|
484
|
+
repair: policy.repairJson,
|
|
485
|
+
requireJson: policy.requireJson
|
|
486
|
+
});
|
|
487
|
+
if (!parsed.ok) {
|
|
488
|
+
return parsed;
|
|
489
|
+
}
|
|
490
|
+
const intelligenceIssues = detectAiFailureSignals(parsed.data, policy);
|
|
491
|
+
if (intelligenceIssues.length) {
|
|
492
|
+
return {
|
|
493
|
+
...parsed,
|
|
494
|
+
ok: false,
|
|
495
|
+
issues: intelligenceIssues
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (policy.validate) {
|
|
499
|
+
return policy.validate(parsed.data, response);
|
|
500
|
+
}
|
|
501
|
+
return parsed;
|
|
502
|
+
}
|
|
503
|
+
function detectAiFailureSignals(data, policy = {}) {
|
|
504
|
+
const text = typeof data === "string" ? data : JSON.stringify(data ?? "");
|
|
505
|
+
const issues = [];
|
|
506
|
+
if (policy.detectRefusals !== false && /\b(i can't|i cannot|unable to comply|cannot assist|as an ai)\b/i.test(text)) {
|
|
507
|
+
issues.push({
|
|
508
|
+
kind: "refusal",
|
|
509
|
+
message: "Response appears to be an AI refusal instead of the requested payload"
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (policy.detectTruncation !== false && /\b(truncated|continues?|unfinished|cut off)\b/i.test(text)) {
|
|
513
|
+
issues.push({
|
|
514
|
+
kind: "truncated",
|
|
515
|
+
message: "Response appears to be incomplete or truncated"
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return issues;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/semantic-types.ts
|
|
522
|
+
var SemanticRetryError = class extends Error {
|
|
523
|
+
constructor(message, result, response) {
|
|
524
|
+
super(message);
|
|
525
|
+
this.result = result;
|
|
526
|
+
this.response = response;
|
|
527
|
+
}
|
|
528
|
+
result;
|
|
529
|
+
response;
|
|
530
|
+
name = "SemanticRetryError";
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// src/semantic.ts
|
|
534
|
+
function normalizeAiRetryPolicy(policy = {}) {
|
|
535
|
+
return {
|
|
536
|
+
...aiRetryPolicies.strictJson,
|
|
537
|
+
...policy
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async function semanticRetry(operation, policy = {}) {
|
|
541
|
+
const resolved = normalizeAiRetryPolicy(policy);
|
|
542
|
+
let lastResult;
|
|
543
|
+
let lastResponse;
|
|
544
|
+
let previousDelayMs;
|
|
545
|
+
for (let attempt = 0; attempt <= resolved.maxSemanticRetries; attempt += 1) {
|
|
546
|
+
const value = await operation();
|
|
547
|
+
const response = isAxiosResponse(value) ? value : void 0;
|
|
548
|
+
const data = response ? response.data : value;
|
|
549
|
+
const finalResult = await validateSemanticResponse(data, resolved, response);
|
|
550
|
+
lastResult = finalResult;
|
|
551
|
+
lastResponse = response;
|
|
552
|
+
if (finalResult.ok) {
|
|
553
|
+
await resolved.hooks?.onSemanticRecovery?.({ result: finalResult, response });
|
|
554
|
+
return finalResult.data;
|
|
555
|
+
}
|
|
556
|
+
const issue = finalResult.issues[0] ?? {
|
|
557
|
+
kind: "unknown",
|
|
558
|
+
message: "Semantic validation failed"
|
|
559
|
+
};
|
|
560
|
+
if (attempt >= resolved.maxSemanticRetries || !shouldSemanticRetry(issue, resolved)) {
|
|
561
|
+
await resolved.hooks?.onSemanticGiveUp?.({
|
|
562
|
+
attempts: attempt + 1,
|
|
563
|
+
result: finalResult,
|
|
564
|
+
response
|
|
565
|
+
});
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
const nextDelayMs = calculateRetryDelay(
|
|
569
|
+
{
|
|
570
|
+
retries: resolved.maxSemanticRetries,
|
|
571
|
+
strategy: "exponential",
|
|
572
|
+
baseDelayMs: 100,
|
|
573
|
+
maxDelayMs: 3e3,
|
|
574
|
+
jitter: "full"
|
|
575
|
+
},
|
|
576
|
+
{ attempt: attempt + 1, previousDelayMs }
|
|
577
|
+
);
|
|
578
|
+
previousDelayMs = nextDelayMs;
|
|
579
|
+
await resolved.hooks?.onSemanticRetry?.({
|
|
580
|
+
attempt: attempt + 1,
|
|
581
|
+
maxRetries: resolved.maxSemanticRetries,
|
|
582
|
+
issue,
|
|
583
|
+
result: finalResult,
|
|
584
|
+
response,
|
|
585
|
+
nextDelayMs
|
|
586
|
+
});
|
|
587
|
+
resolved.logger?.log(
|
|
588
|
+
createLogEntry("warn", "semantic.retry", "Semantic retry scheduled", {
|
|
589
|
+
attempt: attempt + 1,
|
|
590
|
+
maxRetries: resolved.maxSemanticRetries,
|
|
591
|
+
delayMs: nextDelayMs,
|
|
592
|
+
issue: issue.kind,
|
|
593
|
+
...resolved.metadata
|
|
594
|
+
})
|
|
595
|
+
);
|
|
596
|
+
await sleep(nextDelayMs);
|
|
597
|
+
}
|
|
598
|
+
throw new SemanticRetryError("Semantic retry attempts exhausted", lastResult ?? emptyFailure(), lastResponse);
|
|
599
|
+
}
|
|
600
|
+
async function requestWithSemanticRetry(instance, config) {
|
|
601
|
+
return semanticRetry(() => instance.request(config.request), config);
|
|
602
|
+
}
|
|
603
|
+
function applySemanticRecovery(instance, policy = {}) {
|
|
604
|
+
const resolved = normalizeAiRetryPolicy(policy);
|
|
605
|
+
instance.interceptors.response.use(async (response) => {
|
|
606
|
+
const finalResult = await validateSemanticResponse(response.data, resolved, response);
|
|
607
|
+
if (!finalResult.ok) {
|
|
608
|
+
throw new SemanticRetryError("Semantic response validation failed", finalResult, response);
|
|
609
|
+
}
|
|
610
|
+
response.data = finalResult.data;
|
|
611
|
+
await resolved.hooks?.onSemanticRecovery?.({ result: finalResult, response });
|
|
612
|
+
return response;
|
|
613
|
+
});
|
|
614
|
+
return {
|
|
615
|
+
axios: instance,
|
|
616
|
+
policy: resolved
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function isAxiosResponse(value) {
|
|
620
|
+
return Boolean(value && typeof value === "object" && "status" in value && "headers" in value && "config" in value);
|
|
621
|
+
}
|
|
622
|
+
function sleep(ms) {
|
|
623
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
624
|
+
}
|
|
625
|
+
function emptyFailure() {
|
|
626
|
+
return {
|
|
627
|
+
ok: false,
|
|
628
|
+
issues: [{ kind: "unknown", message: "Semantic validation failed before a response was captured" }]
|
|
629
|
+
};
|
|
630
|
+
}
|
|
261
631
|
export {
|
|
262
632
|
ConsoleRetryLogger,
|
|
263
633
|
RetryHookEmitter,
|
|
634
|
+
SemanticRetryError,
|
|
635
|
+
aiRetryPolicies,
|
|
264
636
|
applyAiResilience,
|
|
265
637
|
applyJitter,
|
|
638
|
+
applySemanticRecovery,
|
|
266
639
|
calculateRetryDelay,
|
|
640
|
+
classifyAiFailure,
|
|
267
641
|
createAiResilienceClient,
|
|
268
642
|
createLogEntry,
|
|
643
|
+
createSemanticRetryCondition,
|
|
269
644
|
defaultRetryConfig,
|
|
645
|
+
detectAiFailureSignals,
|
|
270
646
|
isRetryableMethod,
|
|
271
647
|
isRetryableStatus,
|
|
648
|
+
normalizeAiRetryPolicy,
|
|
272
649
|
normalizeRetryConfig,
|
|
650
|
+
parseJsonResponse,
|
|
651
|
+
repairJsonString,
|
|
652
|
+
requestWithSemanticRetry,
|
|
653
|
+
resolveAiRetryPolicy,
|
|
273
654
|
resolveRetryStrategy,
|
|
274
655
|
retryStrategies,
|
|
275
|
-
|
|
656
|
+
semanticRetry,
|
|
657
|
+
shouldRetry,
|
|
658
|
+
shouldSemanticRetry,
|
|
659
|
+
validateSchema,
|
|
660
|
+
validateSemanticResponse
|
|
276
661
|
};
|