feat: adaptive transport selection (bio-inspired)#385
Conversation
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>
🧪 Code Coverage
Generated by coverage-comment.yml |
There was a problem hiding this comment.
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.
| /** | ||
| * 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) | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| /** | |
| * 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
- Breaking changes to public interfaces are permissible if they are expected and explicitly documented in the pull request description.
| 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; |
There was a problem hiding this comment.
Retrieve the start time from the options object. This eliminates the need for the timers Map and the risk of key collisions.
| 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() }); |
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>
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:CallInterceptorthat automatically feedsTransportStatsfrom every transport call.preferredOrder(): Returns transport names ranked by score, ready to feed intoClientFactoryOptions.preferredTransports.Usage
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:
Benchmark from our production implementation (openclaw-a2a-gateway, 475 tests):
Design decisions
Test plan
npm run build)Refs #384
🤖 Generated with Claude Code