Skip to content

feat: adaptive transport selection (bio-inspired)#385

Open
AliceLJY wants to merge 4 commits into
a2aproject:mainfrom
AliceLJY:feat/adaptive-transport
Open

feat: adaptive transport selection (bio-inspired)#385
AliceLJY wants to merge 4 commits into
a2aproject:mainfrom
AliceLJY:feat/adaptive-transport

Conversation

@AliceLJY

@AliceLJY AliceLJY commented Apr 2, 2026

Copy link
Copy Markdown

Summary

Adds bio-inspired adaptive transport selection as a pluggable CallInterceptor. Inspired by cellular signal pathway selection — cells preferentially activate signaling cascades with higher efficacy and faster transduction speed.

  • TransportStats: Tracks per-transport success rate and latency in a sliding window. Computes composite score: successRate × (1 / (1 + avgLatency / normalizer)). Unknown transports score 1.0 (explore-first).
  • AdaptiveTransportInterceptor: CallInterceptor that automatically feeds TransportStats from every transport call.
  • preferredOrder(): Returns transport names ranked by score, ready to feed into ClientFactoryOptions.preferredTransports.

Usage

import { TransportStats, AdaptiveTransportInterceptor, ClientFactory, ClientFactoryOptions } from '@a2a-js/sdk/client';

const stats = new TransportStats();
const factory = new ClientFactory(
  ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
    clientConfig: { interceptors: [new AdaptiveTransportInterceptor(stats)] },
    preferredTransports: stats.preferredOrder(),
  })
);

Motivation

Standard A2A transport selection uses static priority ordering. When a transport starts failing, clients keep hitting it until timeout. This module adds automatic adaptation:

  • Transports that fail get deprioritized (lower score)
  • Transports that recover get re-promoted (sliding window forgets old failures)
  • Reliable-but-slow beats fast-but-flaky (success rate × latency factor)

Benchmark from our production implementation (openclaw-a2a-gateway, 475 tests):

  • Automatic failover when primary transport degrades
  • Recovery detection when transport comes back online
  • Score correctly ranks reliable > flaky despite latency differences

Design decisions

  • CallInterceptor, not TransportFactory: Keeps existing transport factories untouched. Stats collection is orthogonal to transport creation.
  • Explore-first for unknown: Score 1.0 for untested transports encourages trying all paths before settling.
  • Sliding window: Old failures are forgotten, allowing natural recovery (analogous to receptor recycling in cell biology).

Test plan

  • 11 new tests covering: scoring math, sliding window, success rate tracking, latency tracking, preferred ordering, explore-first default, reliable-vs-flaky ranking
  • All 420 existing tests pass (zero regression)
  • Build passes (npm run build)

Refs #384

🤖 Generated with Claude Code

Add TransportStats and AdaptiveTransportInterceptor for bio-inspired
adaptive transport selection. Inspired by cellular signal pathway
selection (Kholodenko 2006): transports with higher success rates and
lower latency are preferred, while failing transports are deprioritized
and allowed to recover.

TransportStats tracks per-transport success rate and latency in a
sliding window, computing a composite score analogous to signal pathway
strength. AdaptiveTransportInterceptor implements CallInterceptor to
automatically feed stats from every transport call.

- TransportStats: sliding window, composite scoring, preferredOrder()
- AdaptiveTransportInterceptor: CallInterceptor integration
- 11 tests, all passing, zero regression on existing 420 tests
- Fully backward-compatible: no changes to existing behavior

Refs: a2aproject#384

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@AliceLJY
AliceLJY requested a review from a team as a code owner April 2, 2026 10:50
@github-actions

github-actions Bot commented Apr 2, 2026

Copy link
Copy Markdown

🧪 Code Coverage

⬇️ Download Full Report

Base PR Delta
src/client/transports/adaptive_transport.ts (new) 67.44%
Total 81.12% 80.86% 🔴 -0.26%

Generated by coverage-comment.yml

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an adaptive transport selection mechanism, including a TransportStats class for tracking performance metrics and an AdaptiveTransportInterceptor for recording call outcomes. Feedback highlights several critical issues: the preferredOrder method should accept all available transports to properly support explore-first logic; the current timer implementation using a class-level Map is prone to memory leaks and key collisions, and should instead store timing data directly in the request options; the method for identifying the active transport is unreliable; and the unused timestamp field in TransportRecord should be removed to optimize memory usage.

Comment on lines +112 to +120
/**
* Returns transport names ordered by descending score.
* Only includes transports that have at least one recorded outcome.
*/
preferredOrder(): string[] {
return [...this.records.keys()].sort(
(a, b) => this.getScore(b) - this.getScore(a)
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of preferredOrder() only returns transports that have already been recorded. This prevents the 'explore-first' strategy for untried transports because they will never appear in the preferred list. To ensure untried transports are prioritized (since they default to a score of 1.0), this method should accept the list of all available transport names to be ranked. Note: This is a breaking change to the public interface, which is permissible per repository rules if documented in the PR description.

Suggested change
/**
* Returns transport names ordered by descending score.
* Only includes transports that have at least one recorded outcome.
*/
preferredOrder(): string[] {
return [...this.records.keys()].sort(
(a, b) => this.getScore(b) - this.getScore(a)
);
}
/**
* Returns transport names ordered by descending score.
* Accepts a list of available transports to ensure untried ones are included
* in the ranking (explore-first).
*/
preferredOrder(availableTransports: string[]): string[] {
return [...availableTransports].sort(
(a, b) => this.getScore(b) - this.getScore(a)
);
}
References
  1. Breaking changes to public interfaces are permissible if they are expected and explicitly documented in the pull request description.

Comment on lines +173 to +180
const key = (args.options as Record<string, unknown> | undefined)?.[
'_adaptiveTimerKey'
] as string | undefined;
if (!key) return;

const startTime = this.timers.get(key);
this.timers.delete(key);
if (startTime === undefined) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Retrieve the start time from the options object. This eliminates the need for the timers Map and the risk of key collisions.

Suggested change
const key = (args.options as Record<string, unknown> | undefined)?.[
'_adaptiveTimerKey'
] as string | undefined;
if (!key) return;
const startTime = this.timers.get(key);
this.timers.delete(key);
if (startTime === undefined) return;
const startTime = (args.options as Record<string, unknown> | undefined)?.[
'_adaptiveStartTime'
] as number | undefined;
if (startTime === undefined) return;

list = [];
this.records.set(transport, list);
}
list.push({ ok, latencyMs: Math.max(0, latencyMs), timestamp: Date.now() });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Remove the unused timestamp from the record creation.

Suggested change
list.push({ ok, latencyMs: Math.max(0, latencyMs), timestamp: Date.now() });
list.push({ ok, latencyMs: Math.max(0, latencyMs) });

AliceLJY and others added 3 commits April 2, 2026 20:20
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add null checks for args.input and args.result in interceptor
- Use Array.from() instead of spread on Map iterator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant