Skip to content

feat(filter): add A2A classifier following MCP routing pattern#368

Open
nerdalert wants to merge 1 commit into
praxis-proxy:mainfrom
nerdalert:brent-a2a-classifier
Open

feat(filter): add A2A classifier following MCP routing pattern#368
nerdalert wants to merge 1 commit into
praxis-proxy:mainfrom
nerdalert:brent-a2a-classifier

Conversation

@nerdalert
Copy link
Copy Markdown
Member

Summary

Adds the a2a HTTP payload-processing filter for static A2A JSON-RPC classification and routing. This gets parity with the existing MCP classifier functionality merged in #195

This follows the same classifier pattern as the existing MCP filter: reuse the shared JSON-RPC parser, inspect the buffered request body at EOS, extract protocol-specific metadata, promote bounded internal routing headers/filter results/durable metadata, then release to router/load_balancer for static routing.

This implements issue #346 by:

  • classifying canonical A2A methods such as SendMessage, SendStreamingMessage, and GetTask
  • supporting configured slash-delimited method aliases such as message/send
  • extracting task IDs from params.id and params.taskId
  • detecting streaming methods
  • promoting bounded x-praxis-a2a-* routing headers, filter results, and durable metadata
  • adding an A2A static-routing example config and docs

This is classifier/static routing support only. Stateful task routing, response parsing, and SSE task-event parsing remain out of scope for the later A2A task-routing work.

This does not include, focused on parity with the MCP classifier and its already overly large but no new patterns here:

  • Extraction of task state such as task.status.state.
  • Extraction of agent capabilities from an agent card payload, beyond classifying GetExtendedAgentCard as the agent_card family.

Tests

  • cargo +nightly fmt --check
  • cargo test -p praxis-proxy-filter a2a
  • cargo test -p praxis-tests-integration --test suite -- a2a
  • cargo test -p praxis-tests-schema --test suite -- all_example_configs_parse
  • cargo clippy --workspace --all-targets

Refs #346

@nerdalert nerdalert requested a review from a team May 19, 2026 03:23
@nerdalert nerdalert marked this pull request as draft May 19, 2026 03:23
@praxis-bot
Copy link
Copy Markdown
Collaborator

PR too large

This PR adds 2679 lines (limit: 500).

Large PRs are difficult to review and more likely to introduce subtle bugs. Please break this contribution into smaller, focused PRs that each address a single concern.

See our coding conventions for guidance.

If this PR legitimately requires a large change, a maintainer can add the skip/pr-hygiene label to skip this check.

@praxis-bot praxis-bot closed this May 19, 2026
@shaneutt shaneutt added the skip/pr-hygiene PR size and description bypass label May 19, 2026
@shaneutt shaneutt reopened this May 19, 2026
@shaneutt shaneutt moved this from Done to Review in AI Gateway May 19, 2026
@shaneutt shaneutt added this to the v0.4.0 milestone May 19, 2026
@shaneutt shaneutt self-assigned this May 19, 2026
@shaneutt shaneutt marked this pull request as ready for review May 19, 2026 11:50
shaneutt
shaneutt previously approved these changes May 19, 2026
Copy link
Copy Markdown
Member

@shaneutt shaneutt left a comment

Choose a reason for hiding this comment

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

I like how isolated to its module all the A2A stuff is, much like MCP.

I really like how more of the PR is tests and examples than code.

This makes it very easy for us to merge early, and iterate on it since it's well-isolated. Thank you @nerdalert 🖖

Comment thread filter/src/builtins/http/payload_processing/a2a/config.rs
@shaneutt shaneutt linked an issue May 19, 2026 that may be closed by this pull request
@shaneutt shaneutt requested review from twghu and removed request for twghu May 19, 2026 15:21
@praxis-bot praxis-bot marked this pull request as draft May 19, 2026 18:20
@praxis-bot
Copy link
Copy Markdown
Collaborator

Converted to draft: required checks failing.

@nerdalert nerdalert force-pushed the brent-a2a-classifier branch from 949a70f to b1144ab Compare May 19, 2026 19:47
@nerdalert nerdalert marked this pull request as ready for review May 19, 2026 20:34
@nerdalert nerdalert force-pushed the brent-a2a-classifier branch from b1144ab to 02e6b76 Compare May 21, 2026 03:21
@nerdalert nerdalert requested a review from a team May 21, 2026 03:21
Comment thread examples/configs/ai/a2a-classifier-routing.yaml Outdated
Comment thread filter/src/builtins/http/ai/agentic/a2a/config.rs
Comment thread filter/src/builtins/http/ai/agentic/a2a/config.rs
Comment thread filter/src/builtins/http/ai/agentic/a2a/envelope.rs
Comment thread filter/src/builtins/http/ai/agentic/a2a/envelope.rs
"ListTaskPushNotificationConfigs" => Self::ListTaskPushNotificationConfigs,
"DeleteTaskPushNotificationConfig" => Self::DeleteTaskPushNotificationConfig,
"GetExtendedAgentCard" => Self::GetExtendedAgentCard,
other => Self::Unknown(other.to_owned()),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does the protocol talk about case-sensitivity in methods? Right now "sendmessage" would fall through to unknown. 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A2A v1.0 method names are exact JSON-RPC method strings in PascalCase, so I’m treating classification as case-sensitive. Legacy v0.3 slash-delimited names are accepted only through explicit method_aliases. That means sendmessage intentionally falls through to unknown instead of being silently normalized into SendMessage.

Change:

  • Added docs in A2aMethod::from_method_str() explaining that A2A method strings are matched exactly.
  • Added a unit test confirming lowercase sendmessage classifies A2aMethod::Unknown("sendmessage").

Comment thread filter/src/builtins/http/ai/agentic/a2a/mod.rs
}

// -----------------------------------------------------------------------------
// Helpers
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// Helpers
// Test Utilities

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Maybe leave this as // Helpers? The file already has a separate // Test Utilities section, and this section contains production helpers like build_json_rpc_config, handle_parse_error, and metadata/header promotion functions.

WDYT?

Comment on lines +581 to +594
async fn batch_rejected_even_with_on_invalid_continue() {
let filter = make_filter(r#"{"on_invalid": "continue"}"#);
let body_str = r#"[{"jsonrpc":"2.0","id":1,"method":"SendMessage"}]"#;
let req = make_a2a_request(&[]);
let mut ctx = crate::test_utils::make_filter_context(&req);
let mut body = Some(Bytes::from(body_str));

let action = filter.on_request_body(&mut ctx, &mut body, true).await.unwrap();

assert!(
matches!(action, FilterAction::Reject(_)),
"batch should be rejected regardless of on_invalid"
);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we document anywhere why this is the behavior? 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Documented. A2A classification produces one static routing decision per HTTP request; JSON-RPC batches can mix methods, task IDs, and streaming semantics, so they are rejected even when invalid non-A2A input would otherwise continue.

Adds the `a2a` HTTP payload-processing filter for static
A2A JSON-RPC classification and routing.

This follows the same classifier pattern as the existing
 MCP filter: reuse the shared JSON-RPC parser, inspect the
 buffered request body at EOS, extract protocol-specific
metadata, promote bounded internal routing headers/filter
 results/durable metadata, then release to `router`/`load_balancer`
 for static routing.

Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
@nerdalert nerdalert force-pushed the brent-a2a-classifier branch from 02e6b76 to af27f40 Compare May 23, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip/pr-hygiene PR size and description bypass

Projects

Status: Review

Development

Successfully merging this pull request may close these issues.

A2A classifier: body classification filter

3 participants