@mcoda/agents 0.1.28 → 0.1.29
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/AgentService/AgentService.d.ts +6 -0
- package/dist/AgentService/AgentService.d.ts.map +1 -1
- package/dist/AgentService/AgentService.js +307 -71
- package/dist/AgentService/InvocationFailureParser.d.ts +15 -0
- package/dist/AgentService/InvocationFailureParser.d.ts.map +1 -0
- package/dist/AgentService/InvocationFailureParser.js +57 -0
- package/package.json +3 -3
|
@@ -4,6 +4,8 @@ import { AgentAdapter, InvocationRequest, InvocationResult } from "../adapters/A
|
|
|
4
4
|
interface AgentServiceOptions {
|
|
5
5
|
now?: () => number;
|
|
6
6
|
sleep?: (ms: number) => Promise<void>;
|
|
7
|
+
checkInternetReachable?: () => Promise<boolean>;
|
|
8
|
+
connectivityPollIntervalMs?: number;
|
|
7
9
|
}
|
|
8
10
|
export declare class AgentService {
|
|
9
11
|
private repo;
|
|
@@ -22,6 +24,10 @@ export declare class AgentService {
|
|
|
22
24
|
private nowMs;
|
|
23
25
|
private sleepMs;
|
|
24
26
|
private sleepUntil;
|
|
27
|
+
private getConnectivityPollIntervalMs;
|
|
28
|
+
private isInternetReachable;
|
|
29
|
+
private waitForInternetRecovery;
|
|
30
|
+
private isOfflineCapable;
|
|
25
31
|
private metric;
|
|
26
32
|
private estimateWindowResetMs;
|
|
27
33
|
private estimateResetMsFromWindowTypes;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentService.d.ts","sourceRoot":"","sources":["../../src/AgentService/AgentService.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,EACL,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EAKpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAY7C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"AgentService.d.ts","sourceRoot":"","sources":["../../src/AgentService/AgentService.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,EACL,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EAKpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAY7C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AA2LhG,UAAU,mBAAmB;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,sBAAsB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,OAAO;gBADP,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,mBAAwB;WAG9B,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAKtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAUhD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAIrE,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAInD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;YAIpD,kBAAkB;YAMlB,kBAAkB;IA6BhC,OAAO,CAAC,kBAAkB;IA+BpB,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA4C/E,OAAO,CAAC,KAAK;YAIC,OAAO;YASP,UAAU;IAQxB,OAAO,CAAC,6BAA6B;YAOvB,mBAAmB;YAgCnB,uBAAuB;YAiBvB,gBAAgB;IAM9B,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,8BAA8B;IAStC,OAAO,CAAC,iBAAiB;YAOX,oBAAoB;YA6BpB,oBAAoB;YA6CpB,sBAAsB;YAkBtB,mBAAmB;YAUnB,4BAA4B;IAkCpC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAmBlD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA6H9E,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;YAwP5F,mBAAmB;YAkBnB,uBAAuB;YAwBvB,mBAAmB;CAYlC"}
|
|
@@ -14,9 +14,10 @@ import { OpenAiCliAdapter } from "../adapters/openai/OpenAiCliAdapter.js";
|
|
|
14
14
|
import { ZhipuApiAdapter } from "../adapters/zhipu/ZhipuApiAdapter.js";
|
|
15
15
|
import { QaAdapter } from "../adapters/qa/QaAdapter.js";
|
|
16
16
|
import { ClaudeAdapter } from "../adapters/claude/ClaudeAdapter.js";
|
|
17
|
-
import {
|
|
17
|
+
import { parseInvocationFailure } from "./InvocationFailureParser.js";
|
|
18
18
|
const CLI_BASED_ADAPTERS = new Set(["codex-cli", "gemini-cli", "openai-cli", "ollama-cli", "codali-cli", "claude-cli"]);
|
|
19
19
|
const LOCAL_ADAPTERS = new Set(["local-model"]);
|
|
20
|
+
const OFFLINE_CAPABLE_ADAPTERS = new Set(["local-model", "ollama-cli", "qa-cli"]);
|
|
20
21
|
const SUPPORTED_ADAPTERS = new Set([
|
|
21
22
|
"openai-api",
|
|
22
23
|
"codex-cli",
|
|
@@ -46,6 +47,9 @@ const DOCDEX_JSON_ONLY_MARKERS = [/output json only/i, /return json only/i, /no
|
|
|
46
47
|
const HANDOFF_END_MARKERS = [/^\s*END OF FILE\s*$/i, /^\s*\*\*\* End of File\s*$/i];
|
|
47
48
|
const EQUIVALENCE_THRESHOLD = 1.5;
|
|
48
49
|
const MAX_SLEEP_CHUNK_MS = 60 * 60 * 1000;
|
|
50
|
+
const DEFAULT_CONNECTIVITY_POLL_INTERVAL_MS = 5000;
|
|
51
|
+
const INTERNET_CHECK_TIMEOUT_MS = 2500;
|
|
52
|
+
const INTERNET_CHECK_TARGETS = ["https://clients3.google.com/generate_204", "https://www.cloudflare.com/cdn-cgi/trace"];
|
|
49
53
|
const WINDOW_RESET_FALLBACK_MS = {
|
|
50
54
|
rolling_5h: 5 * 60 * 60 * 1000,
|
|
51
55
|
daily: 24 * 60 * 60 * 1000,
|
|
@@ -343,6 +347,68 @@ export class AgentService {
|
|
|
343
347
|
await this.sleepMs(Math.min(remaining, MAX_SLEEP_CHUNK_MS));
|
|
344
348
|
}
|
|
345
349
|
}
|
|
350
|
+
getConnectivityPollIntervalMs() {
|
|
351
|
+
const configured = this.options.connectivityPollIntervalMs;
|
|
352
|
+
if (!Number.isFinite(configured))
|
|
353
|
+
return DEFAULT_CONNECTIVITY_POLL_INTERVAL_MS;
|
|
354
|
+
const normalized = Math.floor(Number(configured));
|
|
355
|
+
return normalized > 0 ? normalized : DEFAULT_CONNECTIVITY_POLL_INTERVAL_MS;
|
|
356
|
+
}
|
|
357
|
+
async isInternetReachable() {
|
|
358
|
+
if (this.options.checkInternetReachable) {
|
|
359
|
+
try {
|
|
360
|
+
return Boolean(await this.options.checkInternetReachable());
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const globalFetch = globalThis.fetch;
|
|
367
|
+
if (typeof globalFetch !== "function")
|
|
368
|
+
return false;
|
|
369
|
+
for (const target of INTERNET_CHECK_TARGETS) {
|
|
370
|
+
const controller = new AbortController();
|
|
371
|
+
const timeout = setTimeout(() => controller.abort(), INTERNET_CHECK_TIMEOUT_MS);
|
|
372
|
+
try {
|
|
373
|
+
const response = await globalFetch(target, {
|
|
374
|
+
method: "HEAD",
|
|
375
|
+
cache: "no-store",
|
|
376
|
+
signal: controller.signal,
|
|
377
|
+
});
|
|
378
|
+
if (response.status >= 200 && response.status < 500) {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// keep probing next endpoint.
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
clearTimeout(timeout);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
async waitForInternetRecovery() {
|
|
392
|
+
const pollMs = this.getConnectivityPollIntervalMs();
|
|
393
|
+
const startedAtMs = this.nowMs();
|
|
394
|
+
for (;;) {
|
|
395
|
+
const reachable = await this.isInternetReachable();
|
|
396
|
+
if (reachable) {
|
|
397
|
+
const recoveredAtMs = this.nowMs();
|
|
398
|
+
return {
|
|
399
|
+
startedAtMs,
|
|
400
|
+
recoveredAtMs,
|
|
401
|
+
durationMs: Math.max(0, recoveredAtMs - startedAtMs),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
await this.sleepMs(pollMs);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async isOfflineCapable(agent, adapterOverride) {
|
|
408
|
+
const secret = await this.getDecryptedSecret(agent.id);
|
|
409
|
+
const adapterType = this.resolveAdapterType(agent, secret, adapterOverride);
|
|
410
|
+
return OFFLINE_CAPABLE_ADAPTERS.has(adapterType);
|
|
411
|
+
}
|
|
346
412
|
metric(value, fallback = 5) {
|
|
347
413
|
return Number.isFinite(value) ? Number(value) : fallback;
|
|
348
414
|
}
|
|
@@ -447,10 +513,15 @@ export class AgentService {
|
|
|
447
513
|
});
|
|
448
514
|
return candidates.map((entry) => entry.agent);
|
|
449
515
|
}
|
|
450
|
-
async findNextAvailableAgent(candidates, attemptedAgentIds, nowMs) {
|
|
516
|
+
async findNextAvailableAgent(candidates, attemptedAgentIds, nowMs, options) {
|
|
451
517
|
for (const candidate of candidates) {
|
|
452
518
|
if (attemptedAgentIds.has(candidate.id))
|
|
453
519
|
continue;
|
|
520
|
+
if (options?.offlineCapableOnly) {
|
|
521
|
+
const offlineCapable = await this.isOfflineCapable(candidate, options.adapterOverride);
|
|
522
|
+
if (!offlineCapable)
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
454
525
|
const availability = await this.getAgentAvailability(candidate, nowMs);
|
|
455
526
|
if (availability.available)
|
|
456
527
|
return candidate;
|
|
@@ -552,38 +623,90 @@ export class AgentService {
|
|
|
552
623
|
}
|
|
553
624
|
catch (error) {
|
|
554
625
|
await this.recordInvocationFailure(activeAgent, error);
|
|
555
|
-
const
|
|
556
|
-
|
|
626
|
+
const nowMs = this.nowMs();
|
|
627
|
+
const parsedFailure = parseInvocationFailure(error, nowMs);
|
|
628
|
+
if (!parsedFailure) {
|
|
557
629
|
throw error;
|
|
558
630
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
631
|
+
const candidates = [baseAgent, ...equivalentAgents];
|
|
632
|
+
if (parsedFailure.kind === "usage_limit") {
|
|
633
|
+
await this.persistUsageLimitObservation(activeAgent, parsedFailure.usageLimit);
|
|
634
|
+
const nextAgent = await this.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs);
|
|
635
|
+
if (nextAgent) {
|
|
636
|
+
failoverEvents.push({
|
|
637
|
+
type: "switch_agent",
|
|
638
|
+
at: new Date(nowMs).toISOString(),
|
|
639
|
+
fromAgentId: activeAgent.id,
|
|
640
|
+
fromAgentSlug: activeAgent.slug,
|
|
641
|
+
toAgentId: nextAgent.id,
|
|
642
|
+
toAgentSlug: nextAgent.slug,
|
|
643
|
+
reason: parsedFailure.kind,
|
|
644
|
+
});
|
|
645
|
+
activeAgent = nextAgent;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const earliestResetMs = await this.findEarliestResetMs(candidates, nowMs);
|
|
649
|
+
const fallbackResetMs = this.estimateResetMsFromWindowTypes(parsedFailure.usageLimit.windowTypes, nowMs);
|
|
650
|
+
const waitUntilMs = earliestResetMs && earliestResetMs > nowMs ? earliestResetMs : Math.max(fallbackResetMs, nowMs + 1000);
|
|
651
|
+
const durationMs = waitUntilMs - nowMs;
|
|
563
652
|
failoverEvents.push({
|
|
564
|
-
type: "
|
|
653
|
+
type: "sleep_until_reset",
|
|
565
654
|
at: new Date(nowMs).toISOString(),
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
toAgentId: nextAgent.id,
|
|
569
|
-
toAgentSlug: nextAgent.slug,
|
|
655
|
+
until: new Date(waitUntilMs).toISOString(),
|
|
656
|
+
durationMs,
|
|
570
657
|
});
|
|
571
|
-
|
|
658
|
+
await this.sleepUntil(waitUntilMs);
|
|
659
|
+
attemptedAgentIds.clear();
|
|
660
|
+
activeAgent = baseAgent;
|
|
572
661
|
continue;
|
|
573
662
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
663
|
+
if (parsedFailure.kind === "connectivity_issue") {
|
|
664
|
+
const internetReachable = await this.isInternetReachable();
|
|
665
|
+
if (!internetReachable) {
|
|
666
|
+
const localFallback = await this.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs, {
|
|
667
|
+
offlineCapableOnly: true,
|
|
668
|
+
adapterOverride: request.adapterType,
|
|
669
|
+
});
|
|
670
|
+
if (localFallback) {
|
|
671
|
+
failoverEvents.push({
|
|
672
|
+
type: "switch_agent",
|
|
673
|
+
at: new Date(nowMs).toISOString(),
|
|
674
|
+
fromAgentId: activeAgent.id,
|
|
675
|
+
fromAgentSlug: activeAgent.slug,
|
|
676
|
+
toAgentId: localFallback.id,
|
|
677
|
+
toAgentSlug: localFallback.slug,
|
|
678
|
+
reason: parsedFailure.kind,
|
|
679
|
+
});
|
|
680
|
+
activeAgent = localFallback;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const recovery = await this.waitForInternetRecovery();
|
|
684
|
+
failoverEvents.push({
|
|
685
|
+
type: "wait_for_internet",
|
|
686
|
+
at: new Date(recovery.startedAtMs).toISOString(),
|
|
687
|
+
until: new Date(recovery.recoveredAtMs).toISOString(),
|
|
688
|
+
durationMs: recovery.durationMs,
|
|
689
|
+
pollIntervalMs: this.getConnectivityPollIntervalMs(),
|
|
690
|
+
});
|
|
691
|
+
attemptedAgentIds.clear();
|
|
692
|
+
activeAgent = baseAgent;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const nextAgent = await this.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs);
|
|
697
|
+
if (!nextAgent) {
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
578
700
|
failoverEvents.push({
|
|
579
|
-
type: "
|
|
701
|
+
type: "switch_agent",
|
|
580
702
|
at: new Date(nowMs).toISOString(),
|
|
581
|
-
|
|
582
|
-
|
|
703
|
+
fromAgentId: activeAgent.id,
|
|
704
|
+
fromAgentSlug: activeAgent.slug,
|
|
705
|
+
toAgentId: nextAgent.id,
|
|
706
|
+
toAgentSlug: nextAgent.slug,
|
|
707
|
+
reason: parsedFailure.kind,
|
|
583
708
|
});
|
|
584
|
-
|
|
585
|
-
attemptedAgentIds.clear();
|
|
586
|
-
activeAgent = baseAgent;
|
|
709
|
+
activeAgent = nextAgent;
|
|
587
710
|
}
|
|
588
711
|
}
|
|
589
712
|
}
|
|
@@ -610,37 +733,89 @@ export class AgentService {
|
|
|
610
733
|
}
|
|
611
734
|
catch (error) {
|
|
612
735
|
await self.recordInvocationFailure(activeAgent, error);
|
|
613
|
-
const parsedLimit = parseUsageLimitError(error, self.nowMs());
|
|
614
|
-
if (!parsedLimit)
|
|
615
|
-
throw error;
|
|
616
|
-
await self.persistUsageLimitObservation(activeAgent, parsedLimit);
|
|
617
736
|
const nowMs = self.nowMs();
|
|
618
|
-
const
|
|
619
|
-
if (
|
|
737
|
+
const parsedFailure = parseInvocationFailure(error, nowMs);
|
|
738
|
+
if (!parsedFailure)
|
|
739
|
+
throw error;
|
|
740
|
+
const candidates = [baseAgent, ...equivalentAgents];
|
|
741
|
+
if (parsedFailure.kind === "usage_limit") {
|
|
742
|
+
await self.persistUsageLimitObservation(activeAgent, parsedFailure.usageLimit);
|
|
743
|
+
const nextAgent = await self.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs);
|
|
744
|
+
if (nextAgent) {
|
|
745
|
+
failoverEvents.push({
|
|
746
|
+
type: "switch_agent",
|
|
747
|
+
at: new Date(nowMs).toISOString(),
|
|
748
|
+
fromAgentId: activeAgent.id,
|
|
749
|
+
fromAgentSlug: activeAgent.slug,
|
|
750
|
+
toAgentId: nextAgent.id,
|
|
751
|
+
toAgentSlug: nextAgent.slug,
|
|
752
|
+
reason: parsedFailure.kind,
|
|
753
|
+
});
|
|
754
|
+
activeAgent = nextAgent;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const earliestResetMs = await self.findEarliestResetMs(candidates, nowMs);
|
|
758
|
+
const fallbackResetMs = self.estimateResetMsFromWindowTypes(parsedFailure.usageLimit.windowTypes, nowMs);
|
|
759
|
+
const waitUntilMs = earliestResetMs && earliestResetMs > nowMs ? earliestResetMs : Math.max(fallbackResetMs, nowMs + 1000);
|
|
760
|
+
const durationMs = waitUntilMs - nowMs;
|
|
620
761
|
failoverEvents.push({
|
|
621
|
-
type: "
|
|
762
|
+
type: "sleep_until_reset",
|
|
622
763
|
at: new Date(nowMs).toISOString(),
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
toAgentId: nextAgent.id,
|
|
626
|
-
toAgentSlug: nextAgent.slug,
|
|
764
|
+
until: new Date(waitUntilMs).toISOString(),
|
|
765
|
+
durationMs,
|
|
627
766
|
});
|
|
628
|
-
|
|
767
|
+
await self.sleepUntil(waitUntilMs);
|
|
768
|
+
attemptedAgentIds.clear();
|
|
769
|
+
activeAgent = baseAgent;
|
|
629
770
|
continue;
|
|
630
771
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
772
|
+
if (parsedFailure.kind === "connectivity_issue") {
|
|
773
|
+
const internetReachable = await self.isInternetReachable();
|
|
774
|
+
if (!internetReachable) {
|
|
775
|
+
const localFallback = await self.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs, {
|
|
776
|
+
offlineCapableOnly: true,
|
|
777
|
+
adapterOverride: request.adapterType,
|
|
778
|
+
});
|
|
779
|
+
if (localFallback) {
|
|
780
|
+
failoverEvents.push({
|
|
781
|
+
type: "switch_agent",
|
|
782
|
+
at: new Date(nowMs).toISOString(),
|
|
783
|
+
fromAgentId: activeAgent.id,
|
|
784
|
+
fromAgentSlug: activeAgent.slug,
|
|
785
|
+
toAgentId: localFallback.id,
|
|
786
|
+
toAgentSlug: localFallback.slug,
|
|
787
|
+
reason: parsedFailure.kind,
|
|
788
|
+
});
|
|
789
|
+
activeAgent = localFallback;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const recovery = await self.waitForInternetRecovery();
|
|
793
|
+
failoverEvents.push({
|
|
794
|
+
type: "wait_for_internet",
|
|
795
|
+
at: new Date(recovery.startedAtMs).toISOString(),
|
|
796
|
+
until: new Date(recovery.recoveredAtMs).toISOString(),
|
|
797
|
+
durationMs: recovery.durationMs,
|
|
798
|
+
pollIntervalMs: self.getConnectivityPollIntervalMs(),
|
|
799
|
+
});
|
|
800
|
+
attemptedAgentIds.clear();
|
|
801
|
+
activeAgent = baseAgent;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const nextAgent = await self.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs);
|
|
806
|
+
if (!nextAgent) {
|
|
807
|
+
throw error;
|
|
808
|
+
}
|
|
635
809
|
failoverEvents.push({
|
|
636
|
-
type: "
|
|
810
|
+
type: "switch_agent",
|
|
637
811
|
at: new Date(nowMs).toISOString(),
|
|
638
|
-
|
|
639
|
-
|
|
812
|
+
fromAgentId: activeAgent.id,
|
|
813
|
+
fromAgentSlug: activeAgent.slug,
|
|
814
|
+
toAgentId: nextAgent.id,
|
|
815
|
+
toAgentSlug: nextAgent.slug,
|
|
816
|
+
reason: parsedFailure.kind,
|
|
640
817
|
});
|
|
641
|
-
|
|
642
|
-
attemptedAgentIds.clear();
|
|
643
|
-
activeAgent = baseAgent;
|
|
818
|
+
activeAgent = nextAgent;
|
|
644
819
|
continue;
|
|
645
820
|
}
|
|
646
821
|
const streamIo = ioEnabled ? createStreamIoRenderer() : undefined;
|
|
@@ -673,45 +848,106 @@ export class AgentService {
|
|
|
673
848
|
}
|
|
674
849
|
catch (error) {
|
|
675
850
|
await self.recordInvocationFailure(activeAgent, error);
|
|
676
|
-
const parsedLimit = parseUsageLimitError(error, self.nowMs());
|
|
677
|
-
if (!parsedLimit)
|
|
678
|
-
throw error;
|
|
679
851
|
const nowMs = self.nowMs();
|
|
680
|
-
|
|
852
|
+
const parsedFailure = parseInvocationFailure(error, nowMs);
|
|
853
|
+
if (!parsedFailure)
|
|
854
|
+
throw error;
|
|
855
|
+
const candidates = [baseAgent, ...equivalentAgents];
|
|
856
|
+
if (parsedFailure.kind === "usage_limit") {
|
|
857
|
+
if (emitted) {
|
|
858
|
+
failoverEvents.push({
|
|
859
|
+
type: "stream_restart_after_limit",
|
|
860
|
+
at: new Date(nowMs).toISOString(),
|
|
861
|
+
fromAgentId: activeAgent.id,
|
|
862
|
+
fromAgentSlug: activeAgent.slug,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
await self.persistUsageLimitObservation(activeAgent, parsedFailure.usageLimit);
|
|
866
|
+
const nextAgent = await self.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs);
|
|
867
|
+
if (nextAgent) {
|
|
868
|
+
failoverEvents.push({
|
|
869
|
+
type: "switch_agent",
|
|
870
|
+
at: new Date(nowMs).toISOString(),
|
|
871
|
+
fromAgentId: activeAgent.id,
|
|
872
|
+
fromAgentSlug: activeAgent.slug,
|
|
873
|
+
toAgentId: nextAgent.id,
|
|
874
|
+
toAgentSlug: nextAgent.slug,
|
|
875
|
+
reason: parsedFailure.kind,
|
|
876
|
+
});
|
|
877
|
+
activeAgent = nextAgent;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const earliestResetMs = await self.findEarliestResetMs(candidates, nowMs);
|
|
881
|
+
const fallbackResetMs = self.estimateResetMsFromWindowTypes(parsedFailure.usageLimit.windowTypes, nowMs);
|
|
882
|
+
const waitUntilMs = earliestResetMs && earliestResetMs > nowMs ? earliestResetMs : Math.max(fallbackResetMs, nowMs + 1000);
|
|
883
|
+
const durationMs = waitUntilMs - nowMs;
|
|
681
884
|
failoverEvents.push({
|
|
682
|
-
type: "
|
|
885
|
+
type: "sleep_until_reset",
|
|
683
886
|
at: new Date(nowMs).toISOString(),
|
|
684
|
-
|
|
685
|
-
|
|
887
|
+
until: new Date(waitUntilMs).toISOString(),
|
|
888
|
+
durationMs,
|
|
686
889
|
});
|
|
890
|
+
await self.sleepUntil(waitUntilMs);
|
|
891
|
+
attemptedAgentIds.clear();
|
|
892
|
+
activeAgent = baseAgent;
|
|
893
|
+
continue;
|
|
687
894
|
}
|
|
688
|
-
|
|
689
|
-
const nextAgent = await self.findNextAvailableAgent([baseAgent, ...equivalentAgents], attemptedAgentIds, nowMs);
|
|
690
|
-
if (nextAgent) {
|
|
895
|
+
if (emitted) {
|
|
691
896
|
failoverEvents.push({
|
|
692
|
-
type: "
|
|
897
|
+
type: "stream_restart_after_failure",
|
|
693
898
|
at: new Date(nowMs).toISOString(),
|
|
694
899
|
fromAgentId: activeAgent.id,
|
|
695
900
|
fromAgentSlug: activeAgent.slug,
|
|
696
|
-
|
|
697
|
-
toAgentSlug: nextAgent.slug,
|
|
901
|
+
reason: parsedFailure.kind,
|
|
698
902
|
});
|
|
699
|
-
activeAgent = nextAgent;
|
|
700
|
-
continue;
|
|
701
903
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
904
|
+
if (parsedFailure.kind === "connectivity_issue") {
|
|
905
|
+
const internetReachable = await self.isInternetReachable();
|
|
906
|
+
if (!internetReachable) {
|
|
907
|
+
const localFallback = await self.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs, {
|
|
908
|
+
offlineCapableOnly: true,
|
|
909
|
+
adapterOverride: request.adapterType,
|
|
910
|
+
});
|
|
911
|
+
if (localFallback) {
|
|
912
|
+
failoverEvents.push({
|
|
913
|
+
type: "switch_agent",
|
|
914
|
+
at: new Date(nowMs).toISOString(),
|
|
915
|
+
fromAgentId: activeAgent.id,
|
|
916
|
+
fromAgentSlug: activeAgent.slug,
|
|
917
|
+
toAgentId: localFallback.id,
|
|
918
|
+
toAgentSlug: localFallback.slug,
|
|
919
|
+
reason: parsedFailure.kind,
|
|
920
|
+
});
|
|
921
|
+
activeAgent = localFallback;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const recovery = await self.waitForInternetRecovery();
|
|
925
|
+
failoverEvents.push({
|
|
926
|
+
type: "wait_for_internet",
|
|
927
|
+
at: new Date(recovery.startedAtMs).toISOString(),
|
|
928
|
+
until: new Date(recovery.recoveredAtMs).toISOString(),
|
|
929
|
+
durationMs: recovery.durationMs,
|
|
930
|
+
pollIntervalMs: self.getConnectivityPollIntervalMs(),
|
|
931
|
+
});
|
|
932
|
+
attemptedAgentIds.clear();
|
|
933
|
+
activeAgent = baseAgent;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
const nextAgent = await self.findNextAvailableAgent(candidates, attemptedAgentIds, nowMs);
|
|
938
|
+
if (!nextAgent) {
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
706
941
|
failoverEvents.push({
|
|
707
|
-
type: "
|
|
942
|
+
type: "switch_agent",
|
|
708
943
|
at: new Date(nowMs).toISOString(),
|
|
709
|
-
|
|
710
|
-
|
|
944
|
+
fromAgentId: activeAgent.id,
|
|
945
|
+
fromAgentSlug: activeAgent.slug,
|
|
946
|
+
toAgentId: nextAgent.id,
|
|
947
|
+
toAgentSlug: nextAgent.slug,
|
|
948
|
+
reason: parsedFailure.kind,
|
|
711
949
|
});
|
|
712
|
-
|
|
713
|
-
attemptedAgentIds.clear();
|
|
714
|
-
activeAgent = baseAgent;
|
|
950
|
+
activeAgent = nextAgent;
|
|
715
951
|
}
|
|
716
952
|
}
|
|
717
953
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ParsedUsageLimitError } from "./UsageLimitParser.js";
|
|
2
|
+
export type ParsedInvocationFailure = {
|
|
3
|
+
kind: "usage_limit";
|
|
4
|
+
usageLimit: ParsedUsageLimitError;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "connectivity_issue";
|
|
7
|
+
message: string;
|
|
8
|
+
rawText: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "technical_issue";
|
|
11
|
+
message: string;
|
|
12
|
+
rawText: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const parseInvocationFailure: (error: unknown, nowMs?: number) => ParsedInvocationFailure | null;
|
|
15
|
+
//# sourceMappingURL=InvocationFailureParser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InvocationFailureParser.d.ts","sourceRoot":"","sources":["../../src/AgentService/InvocationFailureParser.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,qBAAqB,EACtB,MAAM,uBAAuB,CAAC;AAyC/B,MAAM,MAAM,uBAAuB,GAC/B;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,qBAAqB,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChE;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,eAAO,MAAM,sBAAsB,GACjC,OAAO,OAAO,EACd,cAAkB,KACjB,uBAAuB,GAAG,IAwB5B,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { extractUsageLimitErrorText, parseUsageLimitError, } from "./UsageLimitParser.js";
|
|
2
|
+
const NON_RETRYABLE_AUTH_PATTERNS = [
|
|
3
|
+
/invalid[_\s-]*api[_\s-]*key/i,
|
|
4
|
+
/api key.*invalid/i,
|
|
5
|
+
/authentication failed/i,
|
|
6
|
+
/missing api key/i,
|
|
7
|
+
/\bunauthorized\b/i,
|
|
8
|
+
/\bforbidden\b/i,
|
|
9
|
+
];
|
|
10
|
+
const CONNECTIVITY_PATTERNS = [
|
|
11
|
+
/\boffline\b/i,
|
|
12
|
+
/\bnetwork (?:is )?unreachable\b/i,
|
|
13
|
+
/\binternet (?:is )?(?:down|offline|unavailable)\b/i,
|
|
14
|
+
/\bfetch failed\b/i,
|
|
15
|
+
/\bfailed to fetch\b/i,
|
|
16
|
+
/\bgetaddrinfo\b/i,
|
|
17
|
+
/\bENOTFOUND\b/i,
|
|
18
|
+
/\bEAI_AGAIN\b/i,
|
|
19
|
+
/\bECONNRESET\b/i,
|
|
20
|
+
/\bECONNREFUSED\b/i,
|
|
21
|
+
/\bETIMEDOUT\b/i,
|
|
22
|
+
/\bsocket hang up\b/i,
|
|
23
|
+
/\bconnection (?:reset|refused|closed|failed)\b/i,
|
|
24
|
+
];
|
|
25
|
+
const TECHNICAL_PATTERNS = [
|
|
26
|
+
/\b(?:500|501|502|503|504)\b/,
|
|
27
|
+
/\binternal server error\b/i,
|
|
28
|
+
/\bbad gateway\b/i,
|
|
29
|
+
/\bservice unavailable\b/i,
|
|
30
|
+
/\bgateway timeout\b/i,
|
|
31
|
+
/\btemporar(?:y|ily) unavailable\b/i,
|
|
32
|
+
/\boverload(?:ed)?\b/i,
|
|
33
|
+
/\bupstream .*?(?:error|failed|timeout)\b/i,
|
|
34
|
+
/\bretry later\b/i,
|
|
35
|
+
];
|
|
36
|
+
const normalizeSpace = (value) => value.replace(/\s+/g, " ").trim();
|
|
37
|
+
export const parseInvocationFailure = (error, nowMs = Date.now()) => {
|
|
38
|
+
const usageLimit = parseUsageLimitError(error, nowMs);
|
|
39
|
+
if (usageLimit) {
|
|
40
|
+
return { kind: "usage_limit", usageLimit };
|
|
41
|
+
}
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
43
|
+
const rawText = normalizeSpace(extractUsageLimitErrorText(error));
|
|
44
|
+
const merged = normalizeSpace([message, rawText].filter(Boolean).join(" "));
|
|
45
|
+
if (!merged)
|
|
46
|
+
return null;
|
|
47
|
+
if (NON_RETRYABLE_AUTH_PATTERNS.some((pattern) => pattern.test(merged))) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (CONNECTIVITY_PATTERNS.some((pattern) => pattern.test(merged))) {
|
|
51
|
+
return { kind: "connectivity_issue", message, rawText: merged };
|
|
52
|
+
}
|
|
53
|
+
if (TECHNICAL_PATTERNS.some((pattern) => pattern.test(merged))) {
|
|
54
|
+
return { kind: "technical_issue", message, rawText: merged };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcoda/agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "Agent registry and capabilities for mcoda.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@mcoda/shared": "0.1.
|
|
34
|
-
"@mcoda/db": "0.1.
|
|
33
|
+
"@mcoda/shared": "0.1.29",
|
|
34
|
+
"@mcoda/db": "0.1.29"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc -p tsconfig.json",
|