@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.
@@ -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;AAsLhG,UAAU,mBAAmB;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;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,MAAM;IAId,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,8BAA8B;IAStC,OAAO,CAAC,iBAAiB;YAOX,oBAAoB;YA6BpB,oBAAoB;YA6CpB,sBAAsB;YAatB,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;IA2E9E,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;YA0I5F,mBAAmB;YAkBnB,uBAAuB;YAwBvB,mBAAmB;CAYlC"}
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 { parseUsageLimitError } from "./UsageLimitParser.js";
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 parsedLimit = parseUsageLimitError(error, this.nowMs());
556
- if (!parsedLimit) {
626
+ const nowMs = this.nowMs();
627
+ const parsedFailure = parseInvocationFailure(error, nowMs);
628
+ if (!parsedFailure) {
557
629
  throw error;
558
630
  }
559
- await this.persistUsageLimitObservation(activeAgent, parsedLimit);
560
- const nowMs = this.nowMs();
561
- const nextAgent = await this.findNextAvailableAgent([baseAgent, ...equivalentAgents], attemptedAgentIds, nowMs);
562
- if (nextAgent) {
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: "switch_agent",
653
+ type: "sleep_until_reset",
565
654
  at: new Date(nowMs).toISOString(),
566
- fromAgentId: activeAgent.id,
567
- fromAgentSlug: activeAgent.slug,
568
- toAgentId: nextAgent.id,
569
- toAgentSlug: nextAgent.slug,
655
+ until: new Date(waitUntilMs).toISOString(),
656
+ durationMs,
570
657
  });
571
- activeAgent = nextAgent;
658
+ await this.sleepUntil(waitUntilMs);
659
+ attemptedAgentIds.clear();
660
+ activeAgent = baseAgent;
572
661
  continue;
573
662
  }
574
- const earliestResetMs = await this.findEarliestResetMs([baseAgent, ...equivalentAgents], nowMs);
575
- const fallbackResetMs = this.estimateResetMsFromWindowTypes(parsedLimit.windowTypes, nowMs);
576
- const waitUntilMs = earliestResetMs && earliestResetMs > nowMs ? earliestResetMs : Math.max(fallbackResetMs, nowMs + 1000);
577
- const durationMs = waitUntilMs - nowMs;
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: "sleep_until_reset",
701
+ type: "switch_agent",
580
702
  at: new Date(nowMs).toISOString(),
581
- until: new Date(waitUntilMs).toISOString(),
582
- durationMs,
703
+ fromAgentId: activeAgent.id,
704
+ fromAgentSlug: activeAgent.slug,
705
+ toAgentId: nextAgent.id,
706
+ toAgentSlug: nextAgent.slug,
707
+ reason: parsedFailure.kind,
583
708
  });
584
- await this.sleepUntil(waitUntilMs);
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 nextAgent = await self.findNextAvailableAgent([baseAgent, ...equivalentAgents], attemptedAgentIds, nowMs);
619
- if (nextAgent) {
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: "switch_agent",
762
+ type: "sleep_until_reset",
622
763
  at: new Date(nowMs).toISOString(),
623
- fromAgentId: activeAgent.id,
624
- fromAgentSlug: activeAgent.slug,
625
- toAgentId: nextAgent.id,
626
- toAgentSlug: nextAgent.slug,
764
+ until: new Date(waitUntilMs).toISOString(),
765
+ durationMs,
627
766
  });
628
- activeAgent = nextAgent;
767
+ await self.sleepUntil(waitUntilMs);
768
+ attemptedAgentIds.clear();
769
+ activeAgent = baseAgent;
629
770
  continue;
630
771
  }
631
- const earliestResetMs = await self.findEarliestResetMs([baseAgent, ...equivalentAgents], nowMs);
632
- const fallbackResetMs = self.estimateResetMsFromWindowTypes(parsedLimit.windowTypes, nowMs);
633
- const waitUntilMs = earliestResetMs && earliestResetMs > nowMs ? earliestResetMs : Math.max(fallbackResetMs, nowMs + 1000);
634
- const durationMs = waitUntilMs - nowMs;
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: "sleep_until_reset",
810
+ type: "switch_agent",
637
811
  at: new Date(nowMs).toISOString(),
638
- until: new Date(waitUntilMs).toISOString(),
639
- durationMs,
812
+ fromAgentId: activeAgent.id,
813
+ fromAgentSlug: activeAgent.slug,
814
+ toAgentId: nextAgent.id,
815
+ toAgentSlug: nextAgent.slug,
816
+ reason: parsedFailure.kind,
640
817
  });
641
- await self.sleepUntil(waitUntilMs);
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
- if (emitted) {
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: "stream_restart_after_limit",
885
+ type: "sleep_until_reset",
683
886
  at: new Date(nowMs).toISOString(),
684
- fromAgentId: activeAgent.id,
685
- fromAgentSlug: activeAgent.slug,
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
- await self.persistUsageLimitObservation(activeAgent, parsedLimit);
689
- const nextAgent = await self.findNextAvailableAgent([baseAgent, ...equivalentAgents], attemptedAgentIds, nowMs);
690
- if (nextAgent) {
895
+ if (emitted) {
691
896
  failoverEvents.push({
692
- type: "switch_agent",
897
+ type: "stream_restart_after_failure",
693
898
  at: new Date(nowMs).toISOString(),
694
899
  fromAgentId: activeAgent.id,
695
900
  fromAgentSlug: activeAgent.slug,
696
- toAgentId: nextAgent.id,
697
- toAgentSlug: nextAgent.slug,
901
+ reason: parsedFailure.kind,
698
902
  });
699
- activeAgent = nextAgent;
700
- continue;
701
903
  }
702
- const earliestResetMs = await self.findEarliestResetMs([baseAgent, ...equivalentAgents], nowMs);
703
- const fallbackResetMs = self.estimateResetMsFromWindowTypes(parsedLimit.windowTypes, nowMs);
704
- const waitUntilMs = earliestResetMs && earliestResetMs > nowMs ? earliestResetMs : Math.max(fallbackResetMs, nowMs + 1000);
705
- const durationMs = waitUntilMs - nowMs;
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: "sleep_until_reset",
942
+ type: "switch_agent",
708
943
  at: new Date(nowMs).toISOString(),
709
- until: new Date(waitUntilMs).toISOString(),
710
- durationMs,
944
+ fromAgentId: activeAgent.id,
945
+ fromAgentSlug: activeAgent.slug,
946
+ toAgentId: nextAgent.id,
947
+ toAgentSlug: nextAgent.slug,
948
+ reason: parsedFailure.kind,
711
949
  });
712
- await self.sleepUntil(waitUntilMs);
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.28",
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.28",
34
- "@mcoda/db": "0.1.28"
33
+ "@mcoda/shared": "0.1.29",
34
+ "@mcoda/db": "0.1.29"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc -p tsconfig.json",