@mcoda/agents 0.1.27 → 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.
@@ -1,9 +1,16 @@
1
1
  import { Agent, AgentAuthMetadata, AgentHealth, AgentPromptManifest } from "@mcoda/shared";
2
2
  import { GlobalRepository } from "@mcoda/db";
3
3
  import { AgentAdapter, InvocationRequest, InvocationResult } from "../adapters/AdapterTypes.js";
4
+ interface AgentServiceOptions {
5
+ now?: () => number;
6
+ sleep?: (ms: number) => Promise<void>;
7
+ checkInternetReachable?: () => Promise<boolean>;
8
+ connectivityPollIntervalMs?: number;
9
+ }
4
10
  export declare class AgentService {
5
11
  private repo;
6
- constructor(repo: GlobalRepository);
12
+ private options;
13
+ constructor(repo: GlobalRepository, options?: AgentServiceOptions);
7
14
  static create(): Promise<AgentService>;
8
15
  close(): Promise<void>;
9
16
  resolveAgent(identifier: string): Promise<Agent>;
@@ -14,6 +21,22 @@ export declare class AgentService {
14
21
  private buildAdapterConfig;
15
22
  private resolveAdapterType;
16
23
  getAdapter(agent: Agent, adapterOverride?: string): Promise<AgentAdapter>;
24
+ private nowMs;
25
+ private sleepMs;
26
+ private sleepUntil;
27
+ private getConnectivityPollIntervalMs;
28
+ private isInternetReachable;
29
+ private waitForInternetRecovery;
30
+ private isOfflineCapable;
31
+ private metric;
32
+ private estimateWindowResetMs;
33
+ private estimateResetMsFromWindowTypes;
34
+ private normalizeLimitKey;
35
+ private getAgentAvailability;
36
+ private listEquivalentAgents;
37
+ private findNextAvailableAgent;
38
+ private findEarliestResetMs;
39
+ private persistUsageLimitObservation;
17
40
  healthCheck(agentId: string): Promise<AgentHealth>;
18
41
  invoke(agentId: string, request: InvocationRequest): Promise<InvocationResult>;
19
42
  invokeStream(agentId: string, request: InvocationRequest): Promise<AsyncGenerator<InvocationResult>>;
@@ -21,4 +44,5 @@ export declare class AgentService {
21
44
  private recordInvocationFailure;
22
45
  private applyDocdexGuidance;
23
46
  }
47
+ export {};
24
48
  //# sourceMappingURL=AgentService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AgentService.d.ts","sourceRoot":"","sources":["../../src/AgentService/AgentService.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAY7C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AA6KhG,qBAAa,YAAY;IACX,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,gBAAgB;WAE7B,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;IA4CzE,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;IAyB9E,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;YAyC5F,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"}
@@ -1,7 +1,7 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { CryptoHelper } from "@mcoda/shared";
4
+ import { CryptoHelper, } from "@mcoda/shared";
5
5
  import { GlobalRepository } from "@mcoda/db";
6
6
  import { CodexAdapter } from "../adapters/codex/CodexAdapter.js";
7
7
  import { GeminiAdapter } from "../adapters/gemini/GeminiAdapter.js";
@@ -14,8 +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 { parseInvocationFailure } from "./InvocationFailureParser.js";
17
18
  const CLI_BASED_ADAPTERS = new Set(["codex-cli", "gemini-cli", "openai-cli", "ollama-cli", "codali-cli", "claude-cli"]);
18
19
  const LOCAL_ADAPTERS = new Set(["local-model"]);
20
+ const OFFLINE_CAPABLE_ADAPTERS = new Set(["local-model", "ollama-cli", "qa-cli"]);
19
21
  const SUPPORTED_ADAPTERS = new Set([
20
22
  "openai-api",
21
23
  "codex-cli",
@@ -43,6 +45,17 @@ let docdexGuidanceCache;
43
45
  let docdexGuidanceLoaded = false;
44
46
  const DOCDEX_JSON_ONLY_MARKERS = [/output json only/i, /return json only/i, /no prose, no analysis/i];
45
47
  const HANDOFF_END_MARKERS = [/^\s*END OF FILE\s*$/i, /^\s*\*\*\* End of File\s*$/i];
48
+ const EQUIVALENCE_THRESHOLD = 1.5;
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"];
53
+ const WINDOW_RESET_FALLBACK_MS = {
54
+ rolling_5h: 5 * 60 * 60 * 1000,
55
+ daily: 24 * 60 * 60 * 1000,
56
+ weekly: 7 * 24 * 60 * 60 * 1000,
57
+ other: 60 * 60 * 1000,
58
+ };
46
59
  const isIoEnabled = () => {
47
60
  const raw = process.env[IO_ENV];
48
61
  if (!raw)
@@ -179,8 +192,9 @@ const normalizeDocdexGuidanceInput = (input, prefix) => {
179
192
  return `${prefix}${remainder}`;
180
193
  };
181
194
  export class AgentService {
182
- constructor(repo) {
195
+ constructor(repo, options = {}) {
183
196
  this.repo = repo;
197
+ this.options = options;
184
198
  }
185
199
  static async create() {
186
200
  const repo = await GlobalRepository.create();
@@ -313,6 +327,247 @@ export class AgentService {
313
327
  }
314
328
  throw new Error(`Unsupported adapter type: ${adapterType}`);
315
329
  }
330
+ nowMs() {
331
+ return this.options.now ? this.options.now() : Date.now();
332
+ }
333
+ async sleepMs(ms) {
334
+ if (ms <= 0)
335
+ return;
336
+ if (this.options.sleep) {
337
+ await this.options.sleep(ms);
338
+ return;
339
+ }
340
+ await new Promise((resolve) => setTimeout(resolve, ms));
341
+ }
342
+ async sleepUntil(timestampMs) {
343
+ while (true) {
344
+ const remaining = timestampMs - this.nowMs();
345
+ if (remaining <= 0)
346
+ return;
347
+ await this.sleepMs(Math.min(remaining, MAX_SLEEP_CHUNK_MS));
348
+ }
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
+ }
412
+ metric(value, fallback = 5) {
413
+ return Number.isFinite(value) ? Number(value) : fallback;
414
+ }
415
+ estimateWindowResetMs(windowType, nowMs) {
416
+ return nowMs + (WINDOW_RESET_FALLBACK_MS[windowType] ?? WINDOW_RESET_FALLBACK_MS.other);
417
+ }
418
+ estimateResetMsFromWindowTypes(windowTypes, nowMs) {
419
+ const normalized = windowTypes.length ? windowTypes : ["other"];
420
+ let earliest = Number.POSITIVE_INFINITY;
421
+ for (const windowType of normalized) {
422
+ earliest = Math.min(earliest, this.estimateWindowResetMs(windowType, nowMs));
423
+ }
424
+ return Number.isFinite(earliest) ? earliest : this.estimateWindowResetMs("other", nowMs);
425
+ }
426
+ normalizeLimitKey(agent) {
427
+ const model = (agent.defaultModel ?? "").trim().toLowerCase();
428
+ if (model.includes("codex-spark"))
429
+ return "codex-spark";
430
+ if (model.includes("codex"))
431
+ return "codex-main";
432
+ return model || (agent.slug ?? agent.id).trim().toLowerCase();
433
+ }
434
+ async getAgentAvailability(agent, nowMs) {
435
+ const limits = await this.repo.listAgentUsageLimits(agent.id);
436
+ if (!limits.length)
437
+ return { available: true };
438
+ const limitKey = this.normalizeLimitKey(agent);
439
+ const relevant = limits.filter((entry) => {
440
+ if (entry.limitScope === "model") {
441
+ return entry.limitKey === limitKey;
442
+ }
443
+ return true;
444
+ });
445
+ if (!relevant.length)
446
+ return { available: true };
447
+ let earliestResetMs;
448
+ for (const entry of relevant) {
449
+ if (entry.status !== "exhausted")
450
+ continue;
451
+ let resetMs = entry.resetAt ? Date.parse(entry.resetAt) : Number.NaN;
452
+ if (!Number.isFinite(resetMs)) {
453
+ resetMs = this.estimateWindowResetMs(entry.windowType, nowMs);
454
+ }
455
+ if (resetMs > nowMs) {
456
+ earliestResetMs = earliestResetMs === undefined ? resetMs : Math.min(earliestResetMs, resetMs);
457
+ }
458
+ }
459
+ if (earliestResetMs !== undefined)
460
+ return { available: false, earliestResetMs };
461
+ return { available: true };
462
+ }
463
+ async listEquivalentAgents(baseAgent) {
464
+ const baseCapabilities = await this.repo.getAgentCapabilities(baseAgent.id);
465
+ const allAgents = await this.repo.listAgents();
466
+ const healthRows = await this.repo.listAgentHealthSummary();
467
+ const healthByAgentId = new Map(healthRows.map((entry) => [entry.agentId, entry]));
468
+ const baseRating = this.metric(baseAgent.rating);
469
+ const baseReasoning = this.metric(baseAgent.reasoningRating, baseRating);
470
+ const baseComplexity = this.metric(baseAgent.maxComplexity);
471
+ const candidates = [];
472
+ for (const candidate of allAgents) {
473
+ if (candidate.id === baseAgent.id)
474
+ continue;
475
+ const health = healthByAgentId.get(candidate.id);
476
+ if (health?.status === "unreachable")
477
+ continue;
478
+ const candidateCapabilities = await this.repo.getAgentCapabilities(candidate.id);
479
+ const hasAllCapabilities = baseCapabilities.every((capability) => candidateCapabilities.includes(capability));
480
+ if (!hasAllCapabilities)
481
+ continue;
482
+ const candidateRating = this.metric(candidate.rating);
483
+ const candidateReasoning = this.metric(candidate.reasoningRating, candidateRating);
484
+ const candidateComplexity = this.metric(candidate.maxComplexity);
485
+ const ratingDiff = Math.abs(candidateRating - baseRating);
486
+ const reasoningDiff = Math.abs(candidateReasoning - baseReasoning);
487
+ const complexityDiff = Math.abs(candidateComplexity - baseComplexity);
488
+ if (ratingDiff > EQUIVALENCE_THRESHOLD)
489
+ continue;
490
+ if (reasoningDiff > EQUIVALENCE_THRESHOLD)
491
+ continue;
492
+ if (complexityDiff > EQUIVALENCE_THRESHOLD)
493
+ continue;
494
+ candidates.push({
495
+ agent: candidate,
496
+ distance: ratingDiff + reasoningDiff + complexityDiff,
497
+ });
498
+ }
499
+ candidates.sort((left, right) => {
500
+ if (left.distance !== right.distance)
501
+ return left.distance - right.distance;
502
+ const leftRating = this.metric(left.agent.rating);
503
+ const rightRating = this.metric(right.agent.rating);
504
+ if (rightRating !== leftRating)
505
+ return rightRating - leftRating;
506
+ const leftCost = Number.isFinite(left.agent.costPerMillion) ? Number(left.agent.costPerMillion) : Number.POSITIVE_INFINITY;
507
+ const rightCost = Number.isFinite(right.agent.costPerMillion)
508
+ ? Number(right.agent.costPerMillion)
509
+ : Number.POSITIVE_INFINITY;
510
+ if (leftCost !== rightCost)
511
+ return leftCost - rightCost;
512
+ return (left.agent.slug ?? left.agent.id).localeCompare(right.agent.slug ?? right.agent.id);
513
+ });
514
+ return candidates.map((entry) => entry.agent);
515
+ }
516
+ async findNextAvailableAgent(candidates, attemptedAgentIds, nowMs, options) {
517
+ for (const candidate of candidates) {
518
+ if (attemptedAgentIds.has(candidate.id))
519
+ continue;
520
+ if (options?.offlineCapableOnly) {
521
+ const offlineCapable = await this.isOfflineCapable(candidate, options.adapterOverride);
522
+ if (!offlineCapable)
523
+ continue;
524
+ }
525
+ const availability = await this.getAgentAvailability(candidate, nowMs);
526
+ if (availability.available)
527
+ return candidate;
528
+ }
529
+ return undefined;
530
+ }
531
+ async findEarliestResetMs(candidates, nowMs) {
532
+ let earliest;
533
+ for (const candidate of candidates) {
534
+ const availability = await this.getAgentAvailability(candidate, nowMs);
535
+ if (!availability.earliestResetMs)
536
+ continue;
537
+ earliest = earliest === undefined ? availability.earliestResetMs : Math.min(earliest, availability.earliestResetMs);
538
+ }
539
+ return earliest;
540
+ }
541
+ async persistUsageLimitObservation(agent, observation) {
542
+ const observedAt = new Date(this.nowMs()).toISOString();
543
+ const limitKey = this.normalizeLimitKey(agent);
544
+ const windowTypes = observation.windowTypes.length
545
+ ? observation.windowTypes
546
+ : ["other"];
547
+ const fallbackResetMs = this.estimateResetMsFromWindowTypes(windowTypes, this.nowMs());
548
+ const resolvedResetAt = observation.resetAt ?? new Date(fallbackResetMs).toISOString();
549
+ const resetAtSource = observation.resetAt
550
+ ? observation.resetAtSource ?? "unknown"
551
+ : "estimated_window_fallback";
552
+ const records = windowTypes.map((windowType) => ({
553
+ agentId: agent.id,
554
+ limitScope: "model",
555
+ limitKey,
556
+ windowType,
557
+ status: "exhausted",
558
+ resetAt: resolvedResetAt,
559
+ observedAt,
560
+ source: "invoke_error_parse",
561
+ details: {
562
+ message: observation.message,
563
+ rawText: observation.rawText.slice(0, 4000),
564
+ resetAtSource,
565
+ resetAtProvided: Boolean(observation.resetAt),
566
+ estimatedResetAt: observation.resetAt ? undefined : resolvedResetAt,
567
+ },
568
+ }));
569
+ return this.repo.upsertAgentUsageLimits(records);
570
+ }
316
571
  async healthCheck(agentId) {
317
572
  const agent = await this.resolveAgent(agentId);
318
573
  try {
@@ -333,71 +588,370 @@ export class AgentService {
333
588
  }
334
589
  }
335
590
  async invoke(agentId, request) {
336
- const agent = await this.resolveAgent(agentId);
337
- const adapter = await this.getAdapter(agent, request.adapterType);
338
- if (!adapter.invoke) {
339
- throw new Error("Adapter does not support invoke");
340
- }
341
- const withDocdex = await this.applyDocdexGuidance(request);
342
- const enriched = await this.applyGatewayHandoff(withDocdex);
343
- const ioEnabled = isIoEnabled();
344
- if (ioEnabled) {
345
- renderIoHeader(agent, enriched, "invoke");
346
- }
347
- try {
348
- const result = await adapter.invoke(enriched);
349
- if (ioEnabled) {
350
- renderIoChunk(result);
351
- renderIoEnd();
591
+ const baseAgent = await this.resolveAgent(agentId);
592
+ const equivalentAgents = await this.listEquivalentAgents(baseAgent);
593
+ const attemptedAgentIds = new Set();
594
+ const failoverEvents = [];
595
+ let activeAgent = baseAgent;
596
+ for (;;) {
597
+ attemptedAgentIds.add(activeAgent.id);
598
+ const adapter = await this.getAdapter(activeAgent, request.adapterType);
599
+ if (!adapter.invoke) {
600
+ throw new Error("Adapter does not support invoke");
352
601
  }
353
- return result;
354
- }
355
- catch (error) {
356
- await this.recordInvocationFailure(agent, error);
357
- throw error;
358
- }
359
- }
360
- async invokeStream(agentId, request) {
361
- const agent = await this.resolveAgent(agentId);
362
- const adapter = await this.getAdapter(agent, request.adapterType);
363
- if (!adapter.invokeStream) {
364
- throw new Error("Adapter does not support streaming");
365
- }
366
- const withDocdex = await this.applyDocdexGuidance(request);
367
- const enriched = await this.applyGatewayHandoff(withDocdex);
368
- const ioEnabled = isIoEnabled();
369
- let generator;
370
- try {
371
- generator = await adapter.invokeStream(enriched);
372
- }
373
- catch (error) {
374
- await this.recordInvocationFailure(agent, error);
375
- throw error;
376
- }
377
- const recordFailure = (error) => this.recordInvocationFailure(agent, error);
378
- async function* wrap() {
379
- const streamIo = ioEnabled ? createStreamIoRenderer() : undefined;
602
+ const withDocdex = await this.applyDocdexGuidance(request);
603
+ const enriched = await this.applyGatewayHandoff(withDocdex);
604
+ const ioEnabled = isIoEnabled();
380
605
  if (ioEnabled) {
381
- renderIoHeader(agent, enriched, "stream");
606
+ renderIoHeader(activeAgent, enriched, "invoke");
382
607
  }
383
608
  try {
384
- for await (const chunk of generator) {
385
- if (ioEnabled) {
386
- streamIo?.push(chunk);
387
- }
388
- yield chunk;
609
+ const result = await adapter.invoke(enriched);
610
+ if (ioEnabled) {
611
+ renderIoChunk(result);
612
+ renderIoEnd();
389
613
  }
614
+ if (!failoverEvents.length)
615
+ return result;
616
+ return {
617
+ ...result,
618
+ metadata: {
619
+ ...(result.metadata ?? {}),
620
+ failoverEvents,
621
+ },
622
+ };
390
623
  }
391
624
  catch (error) {
392
- await recordFailure(error);
393
- throw error;
625
+ await this.recordInvocationFailure(activeAgent, error);
626
+ const nowMs = this.nowMs();
627
+ const parsedFailure = parseInvocationFailure(error, nowMs);
628
+ if (!parsedFailure) {
629
+ throw error;
630
+ }
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;
652
+ failoverEvents.push({
653
+ type: "sleep_until_reset",
654
+ at: new Date(nowMs).toISOString(),
655
+ until: new Date(waitUntilMs).toISOString(),
656
+ durationMs,
657
+ });
658
+ await this.sleepUntil(waitUntilMs);
659
+ attemptedAgentIds.clear();
660
+ activeAgent = baseAgent;
661
+ continue;
662
+ }
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
+ }
700
+ failoverEvents.push({
701
+ type: "switch_agent",
702
+ at: new Date(nowMs).toISOString(),
703
+ fromAgentId: activeAgent.id,
704
+ fromAgentSlug: activeAgent.slug,
705
+ toAgentId: nextAgent.id,
706
+ toAgentSlug: nextAgent.slug,
707
+ reason: parsedFailure.kind,
708
+ });
709
+ activeAgent = nextAgent;
394
710
  }
395
- if (ioEnabled) {
396
- streamIo?.flush();
397
- renderIoEnd();
711
+ }
712
+ }
713
+ async invokeStream(agentId, request) {
714
+ const baseAgent = await this.resolveAgent(agentId);
715
+ const equivalentAgents = await this.listEquivalentAgents(baseAgent);
716
+ const attemptedAgentIds = new Set();
717
+ const self = this;
718
+ async function* run() {
719
+ let activeAgent = baseAgent;
720
+ const failoverEvents = [];
721
+ for (;;) {
722
+ attemptedAgentIds.add(activeAgent.id);
723
+ const adapter = await self.getAdapter(activeAgent, request.adapterType);
724
+ if (!adapter.invokeStream) {
725
+ throw new Error("Adapter does not support streaming");
726
+ }
727
+ const withDocdex = await self.applyDocdexGuidance(request);
728
+ const enriched = await self.applyGatewayHandoff(withDocdex);
729
+ const ioEnabled = isIoEnabled();
730
+ let generator;
731
+ try {
732
+ generator = await adapter.invokeStream(enriched);
733
+ }
734
+ catch (error) {
735
+ await self.recordInvocationFailure(activeAgent, error);
736
+ const nowMs = self.nowMs();
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;
761
+ failoverEvents.push({
762
+ type: "sleep_until_reset",
763
+ at: new Date(nowMs).toISOString(),
764
+ until: new Date(waitUntilMs).toISOString(),
765
+ durationMs,
766
+ });
767
+ await self.sleepUntil(waitUntilMs);
768
+ attemptedAgentIds.clear();
769
+ activeAgent = baseAgent;
770
+ continue;
771
+ }
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
+ }
809
+ failoverEvents.push({
810
+ type: "switch_agent",
811
+ at: new Date(nowMs).toISOString(),
812
+ fromAgentId: activeAgent.id,
813
+ fromAgentSlug: activeAgent.slug,
814
+ toAgentId: nextAgent.id,
815
+ toAgentSlug: nextAgent.slug,
816
+ reason: parsedFailure.kind,
817
+ });
818
+ activeAgent = nextAgent;
819
+ continue;
820
+ }
821
+ const streamIo = ioEnabled ? createStreamIoRenderer() : undefined;
822
+ if (ioEnabled) {
823
+ renderIoHeader(activeAgent, enriched, "stream");
824
+ }
825
+ let emitted = false;
826
+ try {
827
+ for await (const chunk of generator) {
828
+ emitted = true;
829
+ const chunkWithMetadata = failoverEvents.length > 0
830
+ ? {
831
+ ...chunk,
832
+ metadata: {
833
+ ...(chunk.metadata ?? {}),
834
+ failoverEvents: failoverEvents.map((event) => ({ ...event })),
835
+ },
836
+ }
837
+ : chunk;
838
+ if (ioEnabled) {
839
+ streamIo?.push(chunkWithMetadata);
840
+ }
841
+ yield chunkWithMetadata;
842
+ }
843
+ if (ioEnabled) {
844
+ streamIo?.flush();
845
+ renderIoEnd();
846
+ }
847
+ return;
848
+ }
849
+ catch (error) {
850
+ await self.recordInvocationFailure(activeAgent, error);
851
+ const nowMs = self.nowMs();
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;
884
+ failoverEvents.push({
885
+ type: "sleep_until_reset",
886
+ at: new Date(nowMs).toISOString(),
887
+ until: new Date(waitUntilMs).toISOString(),
888
+ durationMs,
889
+ });
890
+ await self.sleepUntil(waitUntilMs);
891
+ attemptedAgentIds.clear();
892
+ activeAgent = baseAgent;
893
+ continue;
894
+ }
895
+ if (emitted) {
896
+ failoverEvents.push({
897
+ type: "stream_restart_after_failure",
898
+ at: new Date(nowMs).toISOString(),
899
+ fromAgentId: activeAgent.id,
900
+ fromAgentSlug: activeAgent.slug,
901
+ reason: parsedFailure.kind,
902
+ });
903
+ }
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
+ }
941
+ failoverEvents.push({
942
+ type: "switch_agent",
943
+ at: new Date(nowMs).toISOString(),
944
+ fromAgentId: activeAgent.id,
945
+ fromAgentSlug: activeAgent.slug,
946
+ toAgentId: nextAgent.id,
947
+ toAgentSlug: nextAgent.slug,
948
+ reason: parsedFailure.kind,
949
+ });
950
+ activeAgent = nextAgent;
951
+ }
398
952
  }
399
953
  }
400
- return wrap();
954
+ return run();
401
955
  }
402
956
  async applyGatewayHandoff(request) {
403
957
  if (request.metadata?.command === "gateway-agent") {
@@ -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
+ };
@@ -0,0 +1,13 @@
1
+ import { AgentUsageLimitWindowType } from "@mcoda/shared";
2
+ export type UsageLimitResetSource = "header" | "relative" | "absolute";
3
+ export interface ParsedUsageLimitError {
4
+ isUsageLimit: true;
5
+ message: string;
6
+ rawText: string;
7
+ windowTypes: AgentUsageLimitWindowType[];
8
+ resetAt?: string;
9
+ resetAtSource?: UsageLimitResetSource;
10
+ }
11
+ export declare const extractUsageLimitErrorText: (error: unknown) => string;
12
+ export declare const parseUsageLimitError: (error: unknown, nowMs?: number) => ParsedUsageLimitError | null;
13
+ //# sourceMappingURL=UsageLimitParser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UsageLimitParser.d.ts","sourceRoot":"","sources":["../../src/AgentService/UsageLimitParser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAqC1D,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,yBAAyB,EAAE,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,qBAAqB,CAAC;CACvC;AAkJD,eAAO,MAAM,0BAA0B,GAAI,OAAO,OAAO,KAAG,MAI3D,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,OAAO,OAAO,EAAE,cAAkB,KAAG,qBAAqB,GAAG,IA6BjG,CAAC"}
@@ -0,0 +1,197 @@
1
+ const LIMIT_PATTERNS = [
2
+ /usage[_\s-]*limit/i,
3
+ /usage_limit_reached/i,
4
+ /rate[_\s-]*limit/i,
5
+ /too many requests/i,
6
+ /\b429\b/,
7
+ /quota exceeded/i,
8
+ /retry after/i,
9
+ ];
10
+ const NON_LIMIT_AUTH_PATTERNS = [
11
+ /invalid[_\s-]*api[_\s-]*key/i,
12
+ /api key.*invalid/i,
13
+ /authentication failed/i,
14
+ /missing api key/i,
15
+ /unauthorized/i,
16
+ ];
17
+ const WINDOW_HINTS = [
18
+ { pattern: /\b(?:5\s*(?:h|hr|hrs|hour|hours)|five[-\s]?hour)\b/i, windowType: "rolling_5h" },
19
+ { pattern: /\b(?:daily|24\s*(?:h|hr|hrs|hour|hours))\b/i, windowType: "daily" },
20
+ { pattern: /\b(?:weekly|7\s*(?:d|day|days))\b/i, windowType: "weekly" },
21
+ ];
22
+ const RELATIVE_RESET_PATTERNS = [
23
+ /retry after\s+([^\n.;]+)/i,
24
+ /try again in\s+([^\n.;]+)/i,
25
+ /resets?\s+in\s+([^\n.;]+)/i,
26
+ ];
27
+ const ISO_LIKE_PATTERN = /\b(20\d{2}-\d{2}-\d{2}[tT ]\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,3})?(?:Z|[+-]\d{2}:?\d{2}| ?UTC| ?GMT)?)\b/g;
28
+ const EPOCH_RESET_PATTERN = /\bx-ratelimit-reset(?:-at)?\s*[:=]\s*(\d{10,13})\b/i;
29
+ const SHORT_RESET_PATTERN = /\bx-ratelimit-reset-after\s*[:=]\s*(\d+)\b/i;
30
+ const normalizeSpace = (value) => value.replace(/\s+/g, " ").trim();
31
+ const collectText = (value, out, depth = 0) => {
32
+ if (depth > 4 || value === null || value === undefined)
33
+ return;
34
+ if (typeof value === "string") {
35
+ const trimmed = value.trim();
36
+ if (trimmed)
37
+ out.push(trimmed);
38
+ return;
39
+ }
40
+ if (typeof value === "number" || typeof value === "boolean") {
41
+ out.push(String(value));
42
+ return;
43
+ }
44
+ if (value instanceof Error) {
45
+ collectText(value.message, out, depth + 1);
46
+ const extra = value;
47
+ for (const [key, nested] of Object.entries(extra)) {
48
+ if (key === "message" || key === "name" || key === "stack")
49
+ continue;
50
+ collectText(nested, out, depth + 1);
51
+ }
52
+ return;
53
+ }
54
+ if (Array.isArray(value)) {
55
+ for (const item of value)
56
+ collectText(item, out, depth + 1);
57
+ return;
58
+ }
59
+ if (typeof value === "object") {
60
+ for (const [key, nested] of Object.entries(value)) {
61
+ if (/(error|message|reason|stderr|stdout|detail|details|body)/i.test(key)) {
62
+ collectText(nested, out, depth + 1);
63
+ continue;
64
+ }
65
+ if (depth < 2) {
66
+ collectText(nested, out, depth + 1);
67
+ }
68
+ }
69
+ }
70
+ };
71
+ const parseDurationMs = (input) => {
72
+ let totalMs = 0;
73
+ let matched = false;
74
+ const pattern = /(\d+)\s*(weeks?|w|days?|d|hours?|hrs?|h|minutes?|mins?|m|seconds?|secs?|s)\b/gi;
75
+ let match = pattern.exec(input);
76
+ while (match) {
77
+ const amount = Number.parseInt(match[1] ?? "", 10);
78
+ const unit = (match[2] ?? "").toLowerCase();
79
+ if (Number.isFinite(amount) && amount > 0) {
80
+ matched = true;
81
+ if (unit.startsWith("w"))
82
+ totalMs += amount * 7 * 24 * 60 * 60 * 1000;
83
+ else if (unit.startsWith("d"))
84
+ totalMs += amount * 24 * 60 * 60 * 1000;
85
+ else if (unit.startsWith("h"))
86
+ totalMs += amount * 60 * 60 * 1000;
87
+ else if (unit.startsWith("m"))
88
+ totalMs += amount * 60 * 1000;
89
+ else if (unit.startsWith("s"))
90
+ totalMs += amount * 1000;
91
+ }
92
+ match = pattern.exec(input);
93
+ }
94
+ return matched && totalMs > 0 ? totalMs : undefined;
95
+ };
96
+ const parseIsoLikeDate = (candidate) => {
97
+ const normalized = candidate.replace(/\bUTC\b/i, "Z").replace(/\bGMT\b/i, "Z").replace(" ", "T");
98
+ const parsed = Date.parse(normalized);
99
+ return Number.isFinite(parsed) ? parsed : undefined;
100
+ };
101
+ const resolveResetTimestamp = (text, nowMs) => {
102
+ const candidates = [];
103
+ const epochMatch = EPOCH_RESET_PATTERN.exec(text);
104
+ if (epochMatch?.[1]) {
105
+ const raw = Number.parseInt(epochMatch[1], 10);
106
+ if (Number.isFinite(raw) && raw > 0) {
107
+ candidates.push({
108
+ timestampMs: raw > 1000000000000 ? raw : raw * 1000,
109
+ source: "header",
110
+ });
111
+ }
112
+ }
113
+ const shortMatch = SHORT_RESET_PATTERN.exec(text);
114
+ if (shortMatch?.[1]) {
115
+ const seconds = Number.parseInt(shortMatch[1], 10);
116
+ if (Number.isFinite(seconds) && seconds > 0) {
117
+ candidates.push({
118
+ timestampMs: nowMs + seconds * 1000,
119
+ source: "header",
120
+ });
121
+ }
122
+ }
123
+ for (const pattern of RELATIVE_RESET_PATTERNS) {
124
+ const match = pattern.exec(text);
125
+ const durationText = match?.[1];
126
+ if (!durationText)
127
+ continue;
128
+ const durationMs = parseDurationMs(durationText);
129
+ if (durationMs && durationMs > 0) {
130
+ candidates.push({
131
+ timestampMs: nowMs + durationMs,
132
+ source: "relative",
133
+ });
134
+ }
135
+ }
136
+ ISO_LIKE_PATTERN.lastIndex = 0;
137
+ let isoMatch = ISO_LIKE_PATTERN.exec(text);
138
+ while (isoMatch) {
139
+ const ts = parseIsoLikeDate(isoMatch[1] ?? "");
140
+ if (Number.isFinite(ts)) {
141
+ candidates.push({
142
+ timestampMs: ts,
143
+ source: "absolute",
144
+ });
145
+ }
146
+ isoMatch = ISO_LIKE_PATTERN.exec(text);
147
+ }
148
+ const future = candidates.filter((candidate) => Number.isFinite(candidate.timestampMs) && candidate.timestampMs > nowMs);
149
+ if (!future.length)
150
+ return undefined;
151
+ future.sort((left, right) => left.timestampMs - right.timestampMs);
152
+ return future[0];
153
+ };
154
+ const inferWindowTypes = (text) => {
155
+ const set = new Set();
156
+ for (const hint of WINDOW_HINTS) {
157
+ if (hint.pattern.test(text)) {
158
+ set.add(hint.windowType);
159
+ }
160
+ }
161
+ if (!set.size) {
162
+ set.add("other");
163
+ }
164
+ return Array.from(set);
165
+ };
166
+ export const extractUsageLimitErrorText = (error) => {
167
+ const parts = [];
168
+ collectText(error, parts);
169
+ return normalizeSpace(parts.join(" "));
170
+ };
171
+ export const parseUsageLimitError = (error, nowMs = Date.now()) => {
172
+ const message = error instanceof Error ? error.message : String(error ?? "");
173
+ const rawText = extractUsageLimitErrorText(error);
174
+ const messageLower = message.trim().toLowerCase();
175
+ const rawLower = rawText.trim().toLowerCase();
176
+ const text = normalizeSpace(rawText && rawLower.includes(messageLower) ? rawText : `${message} ${rawText}`);
177
+ if (!text)
178
+ return null;
179
+ const looksLikeLimit = LIMIT_PATTERNS.some((pattern) => pattern.test(text));
180
+ if (!looksLikeLimit)
181
+ return null;
182
+ const looksLikeOnlyNonLimitAuth = NON_LIMIT_AUTH_PATTERNS.some((pattern) => pattern.test(text)) &&
183
+ !/(usage[_\s-]*limit|rate[_\s-]*limit|too many requests|\b429\b)/i.test(text);
184
+ if (looksLikeOnlyNonLimitAuth) {
185
+ return null;
186
+ }
187
+ const reset = resolveResetTimestamp(text, nowMs);
188
+ const windowTypes = inferWindowTypes(text);
189
+ return {
190
+ isUsageLimit: true,
191
+ message,
192
+ rawText: text,
193
+ windowTypes,
194
+ resetAt: reset ? new Date(reset.timestampMs).toISOString() : undefined,
195
+ resetAtSource: reset?.source,
196
+ };
197
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcoda/agents",
3
- "version": "0.1.27",
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.27",
34
- "@mcoda/db": "0.1.27"
33
+ "@mcoda/shared": "0.1.29",
34
+ "@mcoda/db": "0.1.29"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc -p tsconfig.json",