Skip to content

LCORE-2914: A2A client support: enable LCS to discover and call external A2A agents#2162

Open
are-ces wants to merge 1 commit into
lightspeed-core:mainfrom
are-ces:lcore-2914-a2a-client-discovery
Open

LCORE-2914: A2A client support: enable LCS to discover and call external A2A agents#2162
are-ces wants to merge 1 commit into
lightspeed-core:mainfrom
are-ces:lcore-2914-a2a-client-discovery

Conversation

@are-ces

@are-ces are-ces commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Description

Enable LCS to act as an A2A client — discovering and delegating tasks to external A2A-compliant agents during inference. Previously LCS only supported the A2A protocol as a server. This PR adds both the client-side infrastructure and refactors the A2A server to use pydantic-ai agents.

A2A Server Refactor

  • Refactor A2AAgentExecutor to use build_agent() + agent.run_stream_events() instead of raw client.responses.create(), aligning the A2A path with query/streaming endpoints
  • Add _build_a2a_parts_from_agent_result() and _dispatch_agent_event() for pydantic-ai → A2A event mapping

A2A Client

  • A2AAgentsConfiguration: YAML config for remote agent endpoints with timeout, max_retries, and optional auth_token
  • A2AClientManager: singleton that resolves agent cards at startup, holds Client instances with httpx timeout/retry transport
  • A2ADelegationCapability: pydantic-ai capability with list_agents and delegate_to_agent tools, auto-injected into build_agent() when agents are configured
  • Startup/shutdown lifecycle wired into FastAPI lifespan
  • Graceful degradation: unavailable agents are skipped at startup; delegation failures return error strings to the LLM

Docs & Config

  • docs/a2a_protocol.md updated with A2A client documentation
  • examples/lightspeed-stack-a2a-agents.yaml example config
  • OpenAPI schema regenerated

Type of change

  • Refactor
  • New feature
  • Bug fix
  • CVE fix
  • Optimization
  • Documentation Update
  • Configuration Update
  • Bump-up service version
  • Bump-up dependent library
  • Bump-up library or tool used for development (does not change the final image)
  • CI configuration change
  • Konflux configuration change
  • Unit tests improvement
  • Integration tests improvement
  • End to end tests improvement
  • Benchmarks improvement

Tools used to create PR

  • Assisted-by: Claude Opus 4.6
  • Generated by: Claude Opus 4.6

Related Tickets & Documents

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

  • uv run make verify — all linters pass
  • uv run make test-unit — 2992 tests pass, 0 failures
  • Manual testing with 4-instance demo (orchestrator + 3 A2A server agents): delegation, parallel tool calls, error handling verified

Summary by CodeRabbit

  • New Features

    • Added support for connecting to and delegating tasks to external A2A agents.
    • Added configurable agent URLs, authentication tokens, timeouts, and retry settings.
    • Added tools for discovering available agents and delegating tasks during inference.
    • Added streaming responses, concurrent delegations, and clear handling of remote failures.
    • Added OpenAPI documentation and an example configuration for remote agents.
  • Documentation

    • Expanded A2A documentation to cover both client and server capabilities, delegation, authentication, and references.
  • Tests

    • Added coverage for agent connections, delegation, streaming responses, failures, authentication, and cleanup.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds external A2A agent configuration, startup discovery and caching, Pydantic-AI delegation tools, lifecycle cleanup, OpenAPI schemas, tests, an example configuration, and updated A2A protocol documentation.

Changes

A2A delegation

Layer / File(s) Summary
A2A configuration contracts
src/models/config.py, src/configuration.py, docs/openapi.json, examples/..., tests/unit/models/config/test_dump_configuration.py
Adds typed external-agent endpoint settings, the top-level a2a_agents field, OpenAPI definitions, an example configuration, and serialized-configuration assertions.
Remote-agent connection lifecycle
src/a2a_client/manager.py, src/app/main.py, tests/unit/a2a_client/test_manager.py
Connects to configured agents at startup, applies bearer authentication and retry settings, caches agent cards and clients, handles connection failures, and closes clients during shutdown.
Delegation capability and agent wiring
src/a2a_client/*, src/utils/pydantic_ai.py, tests/unit/a2a_client/test_capability.py
Adds list_agents and delegate_to_agent tools, collects streamed responses and failures, and conditionally includes the capability in Pydantic-AI agents.
A2A protocol documentation
docs/a2a_protocol.md
Documents LCS A2A client/server roles, the updated executor flow, remote delegation configuration, authentication, runtime behavior, and references.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FastAPI lifespan
  participant A2AClientManager
  participant Remote A2A agent
  participant Pydantic-AI agent
  FastAPI lifespan->>A2AClientManager: Initialize configured agents
  A2AClientManager->>Remote A2A agent: Connect and fetch AgentCard
  Pydantic-AI agent->>A2AClientManager: Call list_agents or delegate_to_agent
  A2AClientManager->>Remote A2A agent: Send delegated user message
  Remote A2A agent-->>Pydantic-AI agent: Stream response events
Loading

Possibly related PRs

Suggested reviewers: tisnik


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Security And Secret Handling ❌ Error FAIL: capability.py:102-103 logs user tasks, capability.py:133-142 uses untrusted agent-card prose in system prompts, and manager.py:55-57 mutates caller headers with bearer tokens. Remove task text from logs, treat remote agent metadata as untrusted data or exclude it from system prompts, and copy request/header mappings before adding Authorization.
✅ Passed checks (6 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 92.45% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Performance And Algorithmic Complexity ✅ Passed No O(n^2), N+1, or unbounded-growth hotspots found; A2A init/teardown and delegation loops are linear and bounded by configured agents/stream events.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding A2A client support for discovering and delegating to external agents.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Enable LCS to discover and delegate tasks to external A2A-compliant
agents. The delegation is exposed as a pydantic-ai capability, so any
agent-powered endpoint (query, streaming, A2A) can use it.

- A2AAgentsConfiguration: YAML config for remote agent endpoints
  with timeout, max_retries, and optional auth_token
- A2AClientManager: singleton that resolves agent cards at startup
  and holds Client instances with httpx timeout/retry transport
- A2ADelegationCapability: pydantic-ai tools (list_agents,
  delegate_to_agent) auto-injected into build_agent()
- Startup/shutdown lifecycle wired into FastAPI lifespan
- A2A protocol docs updated with client documentation
- OpenAPI schema regenerated
- Example config: examples/lightspeed-stack-a2a-agents.yaml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@are-ces
are-ces force-pushed the lcore-2914-a2a-client-discovery branch from 71f1074 to 60029d6 Compare July 17, 2026 14:37
Comment thread src/a2a_client/manager.py
Comment on lines +98 to +99
httpx_client = httpx.AsyncClient(
timeout=httpx.Timeout(agent_config.timeout),

@coderabbitai coderabbitai 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.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/main.py (1)

145-153: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guarantee A2A cleanup when another shutdown step fails.

If either preceding cleanup raises, Line 149 is skipped and remote connections remain open. Put A2AClientManager().close() in an independent finally block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/main.py` around lines 145 - 153, Update the shutdown cleanup sequence
around shutdown_background_topic_summary_tasks, A2AStorageFactory.cleanup, and
A2AClientManager().close so the client close operation runs in its own finally
block even when an earlier cleanup step raises. Preserve the outer Sentry flush
behavior after all cleanup attempts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/a2a_client/__init__.py`:
- Around line 3-9: Update the package initializer by removing the
A2ADelegationCapability and A2AClientManager imports and deleting __all__; move
those imports into each consuming module so this __init__.py contains only a
brief package description.

In `@src/a2a_client/capability.py`:
- Around line 87-89: Update the documentation section header in the affected
function docstring from “Args:” to “Parameters:”, while preserving the existing
agent_name and task descriptions.
- Around line 130-142: Update the agent-list prompt construction in the
capability method around self._manager.list_agents() so remote card.description
and skill names are not inserted as trusted system-instruction prose. Restrict
displayed metadata to validated identifiers, or clearly encode the description
and skills as untrusted data while preserving the delegation list and agent
names.
- Around line 51-52: Replace the Any annotation on the _manager field with
A2AClientManager, and update _send_and_collect.client to use the concrete Client
interface. Add or reuse the appropriate imports, preserving the existing runtime
behavior while ensuring both class attributes and parameters are statically
checked against the SDK contracts.
- Around line 101-103: Update the delegation debug log in the try block around
create_text_message_object to remove the user-provided task from logger.debug,
retaining only the target agent name and non-sensitive diagnostic context.
- Around line 170-181: Update the terminal-state handling in the capability
delegation logic around the existing failed-state branch to treat
TaskState.failed, TaskState.rejected, and TaskState.canceled uniformly,
preserving the existing failure-message extraction and delegation error
response. In tests/unit/a2a_client/test_capability.py, extend the relevant unit
test coverage to verify all three terminal states produce the delegation failure
result.

In `@src/a2a_client/manager.py`:
- Around line 128-148: Update get_client and get_card in
src/a2a_client/manager.py to use modern union return annotations with | None
instead of Optional. Apply the same annotation change to the helper at
src/utils/pydantic_ai.py line 193; preserve each function’s existing return
behavior.
- Around line 97-113: Update the exception path surrounding
ClientFactory.connect and client.get_card in the connection manager to close any
partially initialized client and its httpx_client before handling or logging the
error. Ensure cleanup is conditional and safe when either resource was never
created, while preserving caching only after successful card retrieval.
- Around line 55-58: Update the request-kwargs preparation around the headers
assignment to copy the incoming mapping and its headers mapping before adding
the Bearer Authorization value. Preserve all existing request options while
ensuring the original input and caller-owned headers remain unchanged, and
assign the copied headers back to the copied kwargs before returning.

In `@src/configuration.py`:
- Around line 431-437: Update the a2a_agents property docstring to use the
Google-style format, adding a Returns section for its A2AAgentsConfiguration |
None result and a Raises section documenting LogicError when _configuration is
not loaded.

---

Outside diff comments:
In `@src/app/main.py`:
- Around line 145-153: Update the shutdown cleanup sequence around
shutdown_background_topic_summary_tasks, A2AStorageFactory.cleanup, and
A2AClientManager().close so the client close operation runs in its own finally
block even when an earlier cleanup step raises. Preserve the outer Sentry flush
behavior after all cleanup attempts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e733f60c-2ce1-4f05-898b-ecf8346ba5f3

📥 Commits

Reviewing files that changed from the base of the PR and between 938dcac and 71f1074.

📒 Files selected for processing (14)
  • docs/a2a_protocol.md
  • docs/openapi.json
  • examples/lightspeed-stack-a2a-agents.yaml
  • src/a2a_client/__init__.py
  • src/a2a_client/capability.py
  • src/a2a_client/manager.py
  • src/app/main.py
  • src/configuration.py
  • src/models/config.py
  • src/utils/pydantic_ai.py
  • tests/unit/a2a_client/__init__.py
  • tests/unit/a2a_client/test_capability.py
  • tests/unit/a2a_client/test_manager.py
  • tests/unit/models/config/test_dump_configuration.py
📜 Review details
⏰ Context from checks skipped due to timeout. (7)
  • GitHub Check: E2E Tests for Lightspeed Evaluation job
  • GitHub Check: E2E: server mode / ci / group 2
  • GitHub Check: E2E: library mode / ci / group 2
  • GitHub Check: E2E: library mode / ci / group 1
  • GitHub Check: E2E: server mode / ci / group 1
  • GitHub Check: E2E: server mode / ci / group 3
  • GitHub Check: E2E: library mode / ci / group 3
🧰 Additional context used
📓 Path-based instructions (8)
**/*

📄 CodeRabbit inference engine (Custom checks)

**/*: Flag meaningful O(n^2)+ algorithms on non-trivial inputs, including handlers and Kubernetes list operations.
Flag N+1 patterns that list items and then query once per item, including Kubernetes API and database access.
Flag expensive work inside loops, including API calls, JSON parsing, and regex compilation.
Flag unbounded growth in caches, watchers, or buffers when eviction or limits are missing.
Flag missing pagination or limits on list operations and API endpoints.
Flag secrets or tokens logged in plaintext or hardcoded in source.
Flag API endpoints missing authentication or authorization.
Flag injection vulnerabilities, including SQL injection, command injection, and path traversal.
Flag sensitive data leaked in API responses, WebSocket messages, or logs.
Flag Kubernetes Secrets and Red Hat secrets missing OwnerReferences.

Files:

  • tests/unit/a2a_client/__init__.py
  • src/a2a_client/__init__.py
  • examples/lightspeed-stack-a2a-agents.yaml
  • src/a2a_client/capability.py
  • docs/openapi.json
  • src/configuration.py
  • src/a2a_client/manager.py
  • src/app/main.py
  • src/utils/pydantic_ai.py
  • src/models/config.py
  • tests/unit/models/config/test_dump_configuration.py
  • tests/unit/a2a_client/test_manager.py
  • tests/unit/a2a_client/test_capability.py
  • docs/a2a_protocol.md
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use absolute imports for internal Python modules.
Start every module with a descriptive docstring and use logger = get_logger(__name__) for module logging.
Define shared constants in the central constants.py module, use descriptive comments, and annotate constants with Final[type].
Use complete type annotations for function parameters and return values, modern union syntax, and typing_extensions.Self for model validators.
Document all modules, classes, and functions using Google-style Python docstrings, including applicable Parameters, Returns, Raises, and Attributes sections.
Use descriptive snake_case, action-oriented function names such as get_, validate_, and check_.
Avoid modifying input parameters in place; return a newly created data structure instead.
Use async def for I/O operations and external API calls.
Use standard logger levels appropriately: debug for diagnostics, info for normal execution, warning for unexpected conditions, and error for serious failures.
Name classes with PascalCase and use descriptive suffixes such as Configuration, Error/Exception, Resolver, and Interface where applicable.
Use ABC and @abstractmethod for abstract interfaces; provide complete, specific type annotations for class attributes and avoid Any.

Files:

  • tests/unit/a2a_client/__init__.py
  • src/a2a_client/__init__.py
  • src/a2a_client/capability.py
  • src/configuration.py
  • src/a2a_client/manager.py
  • src/app/main.py
  • src/utils/pydantic_ai.py
  • src/models/config.py
  • tests/unit/models/config/test_dump_configuration.py
  • tests/unit/a2a_client/test_manager.py
  • tests/unit/a2a_client/test_capability.py
**/__init__.py

📄 CodeRabbit inference engine (AGENTS.md)

Keep package __init__.py files limited to brief package descriptions.

Files:

  • tests/unit/a2a_client/__init__.py
  • src/a2a_client/__init__.py
tests/unit/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use pytest for unit tests, shared fixtures in conftest.py, pytest-mock for mocks, and pytest.mark.asyncio for asynchronous tests; do not use unittest.

Files:

  • tests/unit/a2a_client/__init__.py
  • tests/unit/models/config/test_dump_configuration.py
  • tests/unit/a2a_client/test_manager.py
  • tests/unit/a2a_client/test_capability.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Maintain at least 60% unit-test coverage and 10% integration-test coverage.

Files:

  • tests/unit/a2a_client/__init__.py
  • tests/unit/models/config/test_dump_configuration.py
  • tests/unit/a2a_client/test_manager.py
  • tests/unit/a2a_client/test_capability.py
**/*.{py,yaml,yml,json,toml}

📄 CodeRabbit inference engine (AGENTS.md)

Never commit secrets or keys; use environment variables for sensitive data.

Files:

  • tests/unit/a2a_client/__init__.py
  • src/a2a_client/__init__.py
  • examples/lightspeed-stack-a2a-agents.yaml
  • src/a2a_client/capability.py
  • docs/openapi.json
  • src/configuration.py
  • src/a2a_client/manager.py
  • src/app/main.py
  • src/utils/pydantic_ai.py
  • src/models/config.py
  • tests/unit/models/config/test_dump_configuration.py
  • tests/unit/a2a_client/test_manager.py
  • tests/unit/a2a_client/test_capability.py
src/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Configuration models must extend ConfigurationBase, whose extra="forbid" behavior rejects unknown fields; use types such as Optional[FilePath], PositiveInt, and SecretStr where appropriate.

Files:

  • src/a2a_client/__init__.py
  • src/a2a_client/capability.py
  • src/configuration.py
  • src/a2a_client/manager.py
  • src/app/main.py
  • src/utils/pydantic_ai.py
  • src/models/config.py
src/models/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use ConfigurationBase for configuration Pydantic models and BaseModel for data models; use @model_validator and @field_validator for validation.

Files:

  • src/models/config.py
🧠 Learnings (5)
📚 Learning: 2026-06-24T13:45:37.249Z
Learnt from: Jdubrick
Repo: lightspeed-core/lightspeed-stack PR: 1971
File: src/utils/markdown_repair.py:31-36
Timestamp: 2026-06-24T13:45:37.249Z
Learning: In the lightspeed-stack repository, docstrings must use the section header name "Parameters:" (not "Args:") for function arguments, even if the project references Google Python docstring conventions. Ensure docstrings follow the project’s established "Parameters:" header format for any documented function parameters.

Applied to files:

  • tests/unit/a2a_client/__init__.py
  • src/a2a_client/__init__.py
  • src/a2a_client/capability.py
  • src/configuration.py
  • src/a2a_client/manager.py
  • src/app/main.py
  • src/utils/pydantic_ai.py
  • src/models/config.py
  • tests/unit/models/config/test_dump_configuration.py
  • tests/unit/a2a_client/test_manager.py
  • tests/unit/a2a_client/test_capability.py
📚 Learning: 2026-07-06T15:26:18.398Z
Learnt from: Jdubrick
Repo: lightspeed-core/lightspeed-stack PR: 2071
File: src/models/config.py:2416-2422
Timestamp: 2026-07-06T15:26:18.398Z
Learning: In this repo’s Python code under src/**, don’t treat differences in string concatenation style as a style inconsistency when Black has effectively forced (or made clearer) use of explicit `+` string concatenation in multi-line logger/string expressions. If adjacent-literal implicit concatenation is avoided/changed specifically to accommodate Black’s formatting in these call sites, accept the `+` usage and don’t recommend converting it solely for consistency with nearby blocks that use implicit concatenation.

Applied to files:

  • src/a2a_client/__init__.py
  • src/a2a_client/capability.py
  • src/configuration.py
  • src/a2a_client/manager.py
  • src/app/main.py
  • src/utils/pydantic_ai.py
  • src/models/config.py
📚 Learning: 2026-05-20T08:09:30.641Z
Learnt from: max-svistunov
Repo: lightspeed-core/lightspeed-stack PR: 1580
File: docs/design/llama-stack-config-merge/poc-results/library-mode/synthesized-run.yaml:107-110
Timestamp: 2026-05-20T08:09:30.641Z
Learning: In Llama-stack config YAMLs, when defining a Llama Guard safety shield entry, set `provider_shield_id` to the *guard model identifier* (e.g., `meta-llama/Llama-Guard-3-8B`). Do not use a chat/generative model id (e.g., `openai/gpt-4o-mini`): a chat-model id (or `native_override`) indicates only an override landed and does **not** mean the safety shield is actually gating queries. Ensure any E2E coverage for the related implementation (JIRA/E2E tests) exercises a real Llama Guard model to verify that the shield is effective.

Applied to files:

  • examples/lightspeed-stack-a2a-agents.yaml
📚 Learning: 2026-01-12T10:58:40.230Z
Learnt from: blublinsky
Repo: lightspeed-core/lightspeed-stack PR: 972
File: src/models/config.py:459-513
Timestamp: 2026-01-12T10:58:40.230Z
Learning: In lightspeed-core/lightspeed-stack, for Python files under src/models, when a user claims a fix is done but the issue persists, verify the current code state before accepting the fix. Steps: review the diff, fetch the latest changes, run relevant tests, reproduce the issue, search the codebase for lingering references to the original problem, confirm the fix is applied and not undone by subsequent commits, and validate with local checks to ensure the issue is resolved.

Applied to files:

  • src/models/config.py
📚 Learning: 2026-02-25T07:46:33.545Z
Learnt from: asimurka
Repo: lightspeed-core/lightspeed-stack PR: 1211
File: src/models/responses.py:8-16
Timestamp: 2026-02-25T07:46:33.545Z
Learning: In the Python codebase, requests.py should use OpenAIResponseInputTool as Tool while responses.py uses OpenAIResponseTool as Tool. This difference is intentional due to differing schemas for input vs output tools in llama-stack-api. Apply this distinction consistently to other models under src/models (e.g., ensure request-related tools use the InputTool variant and response-related tools use the ResponseTool variant). If adding new tools, choose the corresponding InputTool or Tool class based on whether the tool represents input or output, and document the rationale in code comments.

Applied to files:

  • src/models/config.py
🪛 Checkov (3.3.8)
docs/openapi.json

[medium] 10768-10775: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🔇 Additional comments (11)
tests/unit/a2a_client/__init__.py (1)

1-1: LGTM!

src/models/config.py (1)

1981-2034: LGTM!

Also applies to: 2727-2732

docs/openapi.json (2)

10714-10781: LGTM!


12230-12241: LGTM!

examples/lightspeed-stack-a2a-agents.yaml (1)

1-42: LGTM!

tests/unit/models/config/test_dump_configuration.py (1)

212-212: LGTM!

Also applies to: 436-436, 811-811, 1070-1070, 1309-1309, 1528-1528, 1907-1907, 2132-2132, 2357-2357, 2589-2589

docs/a2a_protocol.md (1)

7-10: LGTM!

Also applies to: 316-317, 792-819

src/app/main.py (1)

17-17: LGTM!

Also applies to: 132-136

src/utils/pydantic_ai.py (1)

15-16: LGTM!

Also applies to: 199-202, 219-222

src/a2a_client/manager.py (1)

102-108: 🎯 Functional Correctness

ClientFactory.connect is part of the supported a2a-sdk API.

			> Likely an incorrect or invalid review comment.
tests/unit/a2a_client/test_manager.py (1)

72-75: 🎯 Functional Correctness

No change needed for ClientFactory.connect This test matches src/a2a_client/manager.py's use of ClientFactory.connect, so the API mismatch is not present.

			> Likely an incorrect or invalid review comment.

Comment on lines +3 to +9
from a2a_client.capability import A2ADelegationCapability
from a2a_client.manager import A2AClientManager

__all__ = [
"A2AClientManager",
"A2ADelegationCapability",
]

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep this package initializer description-only.

Move these imports to their consumers and remove __all__.

As per coding guidelines, package __init__.py files must be limited to brief package descriptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/__init__.py` around lines 3 - 9, Update the package
initializer by removing the A2ADelegationCapability and A2AClientManager imports
and deleting __all__; move those imports into each consuming module so this
__init__.py contains only a brief package description.

Source: Coding guidelines

Comment on lines +51 to +52
_manager: Any = field(repr=False)
_toolset: FunctionToolset[object] = field(init=False, repr=False)

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace Any with the concrete manager and client interfaces.

Type _manager as A2AClientManager and _send_and_collect.client as Client so SDK contract changes are caught statically.

As per coding guidelines, class attributes and function parameters require specific annotations and should avoid Any.

Also applies to: 150-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/capability.py` around lines 51 - 52, Replace the Any
annotation on the _manager field with A2AClientManager, and update
_send_and_collect.client to use the concrete Client interface. Add or reuse the
appropriate imports, preserving the existing runtime behavior while ensuring
both class attributes and parameters are statically checked against the SDK
contracts.

Source: Coding guidelines

Comment on lines +87 to +89
Args:
agent_name: Name of the agent to delegate to (from list_agents).
task: The task description or question to send to the agent.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use Parameters: instead of Args:.

Based on learnings, this repository requires the section header Parameters: for documented function arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/capability.py` around lines 87 - 89, Update the documentation
section header in the affected function docstring from “Args:” to “Parameters:”,
while preserving the existing agent_name and task descriptions.

Source: Learnings

Comment on lines +101 to +103
try:
logger.debug("Delegating to agent '%s': %s", agent_name, task)
message = create_text_message_object(role=Role.user, content=task)

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not log the delegated user task.

task may contain prompts, credentials, or personal data. Log only the target agent and non-sensitive diagnostics.

Proposed fix
-                logger.debug("Delegating to agent '%s': %s", agent_name, task)
+                logger.debug("Delegating task to agent '%s'", agent_name)

As per coding guidelines, sensitive data must not be logged in plaintext.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
logger.debug("Delegating to agent '%s': %s", agent_name, task)
message = create_text_message_object(role=Role.user, content=task)
try:
logger.debug("Delegating task to agent '%s'", agent_name)
message = create_text_message_object(role=Role.user, content=task)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/capability.py` around lines 101 - 103, Update the delegation
debug log in the try block around create_text_message_object to remove the
user-provided task from logger.debug, retaining only the target agent name and
non-sensitive diagnostic context.

Source: Coding guidelines

Comment on lines +130 to +142
agents = self._manager.list_agents()
if not agents:
return None
lines = ["You can delegate tasks to these external agents when appropriate:"]
for name, card in agents.items():
skills_text = ""
if card.skills:
skill_names = [s.name for s in card.skills if s.name]
if skill_names:
skills_text = f" (skills: {', '.join(skill_names)})"
lines.append(
f"- {name}: {card.description or 'No description'}{skills_text}"
)

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.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not place untrusted agent-card prose directly in system instructions.

A remote agent controls its description and skill names, allowing a compromised endpoint to inject model instructions. Restrict this prompt to validated identifiers or encode remote metadata as explicitly untrusted data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/capability.py` around lines 130 - 142, Update the agent-list
prompt construction in the capability method around self._manager.list_agents()
so remote card.description and skill names are not inserted as trusted
system-instruction prose. Restrict displayed metadata to validated identifiers,
or clearly encode the description and skills as untrusted data while preserving
the delegation list and agent names.

Comment on lines +170 to +181
elif (
update_event is not None
and hasattr(update_event, "status")
and update_event.status.state == TaskState.failed
):
fail_msg = "Agent task failed"
if update_event.status.message:
for part in update_event.status.message.parts:
if isinstance(part.root, TextPart):
fail_msg = part.root.text
break
return f"Delegation failed: {fail_msg}"

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== capability.py slice ==\n'
sed -n '130,220p' src/a2a_client/capability.py

printf '\n== test_capability.py slice ==\n'
sed -n '150,260p' tests/unit/a2a_client/test_capability.py

printf '\n== TaskState definitions/usages ==\n'
rg -n "class TaskState|TaskState\.(failed|rejected|canceled)|rejected|canceled" src tests -g '!**/__pycache__/**'

Repository: lightspeed-core/lightspeed-stack

Length of output: 50388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== TaskState definition ==\n'
rg -n "class TaskState|TaskState =|enum.*TaskState" src tests -g '*.py' | head -n 40

printf '\n== a2a endpoint handling slice ==\n'
sed -n '130,210p' src/app/endpoints/a2a.py

printf '\n== a2a client capability surrounding file top ==\n'
sed -n '1,240p' src/a2a_client/capability.py

Repository: lightspeed-core/lightspeed-stack

Length of output: 9923


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== TaskState definition ==\n'
rg -n "class TaskState|TaskState =" src tests -g '*.py' | head -n 20

printf '\n== a2a endpoint handling slice ==\n'
sed -n '130,210p' src/app/endpoints/a2a.py

printf '\n== capability file with line numbers ==\n'
cat -n src/a2a_client/capability.py | sed -n '1,260p'

Repository: lightspeed-core/lightspeed-stack

Length of output: 11188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== capability.py imports ==\n'
sed -n '1,120p' src/a2a_client/capability.py

printf '\n== app/endpoints/a2a.py imports and handler slice ==\n'
sed -n '1,120p' src/app/endpoints/a2a.py
sed -n '150,190p' src/app/endpoints/a2a.py

printf '\n== files mentioning TaskState in source tree (limited) ==\n'
rg -n "TaskState" src -g '*.py' | head -n 80

Repository: lightspeed-core/lightspeed-stack

Length of output: 12098


🌐 Web query:

A2A protocol TaskState failed rejected canceled task status update enum

💡 Result:

In the A2A protocol, the TaskState enum defines the lifecycle states of a task, and the states failed, rejected, and canceled are officially classified as terminal states [1][2][3]. TaskState Enum Definitions The TaskState enum includes the following terminal states [1][2][3]: TASK_STATE_FAILED (4): Indicates the task finished with an error [1][2]. TASK_STATE_CANCELED (5): Indicates the task was canceled before completion (often by the client) [1][2][4]. TASK_STATE_REJECTED (7): Indicates the agent decided not to perform the task, which may occur at initial creation or later [1][2]. Task Status Updates When a task transitions into any of these terminal states, the protocol typically requires a final status update [5][3]. These updates are handled via the TaskStatusUpdateEvent message [1][6][7]. Key components of the update process include: TaskStatusUpdateEvent: This event notifies the client of a status change [1][6]. It contains the task ID, context ID, and the new status [1][6]. Terminal Marker: The TaskStatusUpdateEvent includes a boolean field (final) that is set to true when the task reaches a terminal state, such as failed, rejected, or canceled [6][8][7]. Implementation: SDKs often provide helper methods (e.g., cancel, failed, reject) within a task_updater module that automatically mark the task as having reached a terminal state and publish the necessary final status update [5][7]. Once a task reaches a terminal state, subsequent status updates are generally prohibited or will raise an error [7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== capability.py imports ==\n'
sed -n '1,120p' src/a2a_client/capability.py

printf '\n== a2a.py imports and status handling ==\n'
sed -n '1,190p' src/app/endpoints/a2a.py

printf '\n== TaskState mentions in src ==\n'
rg -n "TaskState" src -g '*.py' | head -n 120

Repository: lightspeed-core/lightspeed-stack

Length of output: 13048


🌐 Web query:

A2A protocol TaskState values rejected canceled failed terminal states

💡 Result:

In the A2A protocol, the TaskState values rejected, canceled, failed, and completed are all defined as terminal states [1][2][3]. A terminal state indicates that a task has permanently ceased execution and cannot be restarted [4][5][6]. The specific terminal TaskState values are: - TASK_STATE_COMPLETED: Indicates the task finished successfully [1][2]. - TASK_STATE_FAILED: Indicates the task finished with an error [1][2]. - TASK_STATE_CANCELED: Indicates the task was canceled before completion [1][2]. - TASK_STATE_REJECTED: Indicates the agent decided not to perform the task, which may occur at creation or later during processing [1][2]. Once a task reaches any of these terminal states, it is no longer capable of further progression [4][6]. Attempting to send messages to a task in a terminal state will result in an UnsupportedOperationError [1][7][8]. Additionally, stream subscriptions for tasks will terminate upon reaching these states [1][7][8]. Other states, such as TASK_STATE_INPUT_REQUIRED and TASK_STATE_AUTH_REQUIRED, are classified as interrupted states rather than terminal states [1][2][3].

Citations:


Handle all terminal task states from A2A. TaskState.rejected and TaskState.canceled are terminal too; they currently fall through to "Agent returned no response." instead of surfacing a delegation error. Add those states here and cover failed/rejected/canceled in the unit test.

📍 Affects 2 files
  • src/a2a_client/capability.py#L170-L181 (this comment)
  • tests/unit/a2a_client/test_capability.py#L178-L210
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/capability.py` around lines 170 - 181, Update the
terminal-state handling in the capability delegation logic around the existing
failed-state branch to treat TaskState.failed, TaskState.rejected, and
TaskState.canceled uniformly, preserving the existing failure-message extraction
and delegation error response. In tests/unit/a2a_client/test_capability.py,
extend the relevant unit test coverage to verify all three terminal states
produce the delegation failure result.

Comment thread src/a2a_client/manager.py
Comment on lines +55 to +58
headers = http_kwargs.get("headers", {})
headers["Authorization"] = f"Bearer {self._token}"
http_kwargs["headers"] = headers
return request_payload, http_kwargs

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Copy request kwargs before injecting the credential.

This mutates both the input mapping and potentially its caller-owned headers mapping. Build new dictionaries so authorization state cannot leak across reused request options.

Proposed fix
-        headers = http_kwargs.get("headers", {})
+        http_kwargs = dict(http_kwargs)
+        headers = dict(http_kwargs.get("headers", {}))
         headers["Authorization"] = f"Bearer {self._token}"
         http_kwargs["headers"] = headers

As per coding guidelines, input parameters must not be modified in place.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
headers = http_kwargs.get("headers", {})
headers["Authorization"] = f"Bearer {self._token}"
http_kwargs["headers"] = headers
return request_payload, http_kwargs
http_kwargs = dict(http_kwargs)
headers = dict(http_kwargs.get("headers", {}))
headers["Authorization"] = f"Bearer {self._token}"
http_kwargs["headers"] = headers
return request_payload, http_kwargs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/manager.py` around lines 55 - 58, Update the request-kwargs
preparation around the headers assignment to copy the incoming mapping and its
headers mapping before adding the Bearer Authorization value. Preserve all
existing request options while ensuring the original input and caller-owned
headers remain unchanged, and assign the copied headers back to the copied
kwargs before returning.

Source: Coding guidelines

Comment thread src/a2a_client/manager.py
Comment on lines +97 to +113
transport = httpx.AsyncHTTPTransport(retries=agent_config.max_retries)
httpx_client = httpx.AsyncClient(
timeout=httpx.Timeout(agent_config.timeout),
transport=transport,
)
client = await ClientFactory.connect(
url,
client_config=ClientConfig(
streaming=True, httpx_client=httpx_client
),
interceptors=interceptors or None,
)
card = await client.get_card()
self._clients[name] = client
self._cards[name] = card
logger.info("Connected to A2A agent '%s' at %s", name, url)
except (A2AClientError, httpx.HTTPError, OSError) as e:

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close partially created connections when discovery fails.

If connection or card retrieval raises, httpx_client and possibly client are neither cached nor closed. Explicitly close the partially initialized resource in the exception path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/manager.py` around lines 97 - 113, Update the exception path
surrounding ClientFactory.connect and client.get_card in the connection manager
to close any partially initialized client and its httpx_client before handling
or logging the error. Ensure cleanup is conditional and safe when either
resource was never created, while preserving caching only after successful card
retrieval.

Comment thread src/a2a_client/manager.py
Comment on lines +128 to +148
def get_client(self, agent_name: str) -> Optional[Client]:
"""Return the Client for a named agent.

Parameters:
agent_name: Name of the agent as configured.

Returns:
Client instance, or None if the agent is not available.
"""
return self._clients.get(agent_name)

def get_card(self, agent_name: str) -> Optional[AgentCard]:
"""Return the cached AgentCard for a named agent.

Parameters:
agent_name: Name of the agent as configured.

Returns:
AgentCard, or None if the agent is not available.
"""
return self._cards.get(agent_name)

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use modern union syntax consistently in the new A2A surface.

  • src/a2a_client/manager.py#L128-L148: replace both Optional[...] return annotations with ... | None.
  • src/utils/pydantic_ai.py#L193-L193: replace the helper’s Optional[...] return annotation with ... | None.

As per coding guidelines, functions must use modern union syntax.

📍 Affects 2 files
  • src/a2a_client/manager.py#L128-L148 (this comment)
  • src/utils/pydantic_ai.py#L193-L193
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a_client/manager.py` around lines 128 - 148, Update get_client and
get_card in src/a2a_client/manager.py to use modern union return annotations
with | None instead of Optional. Apply the same annotation change to the helper
at src/utils/pydantic_ai.py line 193; preserve each function’s existing return
behavior.

Source: Coding guidelines

Comment thread src/configuration.py
Comment on lines +431 to +437
@property
def a2a_agents(self) -> A2AAgentsConfiguration | None:
"""Return A2A clients configuration."""
if self._configuration is None:
raise LogicError("logic error: configuration is not loaded")
return self._configuration.a2a_agents

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Include Returns: and Raises: in the property docstring.

As per coding guidelines, all functions and properties must be documented using Google-style Python docstrings, including applicable Returns: and Raises: sections. Since this property dynamically raises a LogicError and returns a specific type, these details should be explicitly documented to match the surrounding properties (like database_configuration).

📝 Proposed docstring fix
     `@property`
     def a2a_agents(self) -> A2AAgentsConfiguration | None:
-        """Return A2A clients configuration."""
+        """Return A2A clients configuration.
+
+        Returns:
+            A2AAgentsConfiguration | None: The configured A2A agents or None if not set.
+
+        Raises:
+            LogicError: If the configuration has not been loaded.
+        """
         if self._configuration is None:
             raise LogicError("logic error: configuration is not loaded")
         return self._configuration.a2a_agents
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@property
def a2a_agents(self) -> A2AAgentsConfiguration | None:
"""Return A2A clients configuration."""
if self._configuration is None:
raise LogicError("logic error: configuration is not loaded")
return self._configuration.a2a_agents
`@property`
def a2a_agents(self) -> A2AAgentsConfiguration | None:
"""Return A2A clients configuration.
Returns:
A2AAgentsConfiguration | None: The configured A2A agents or None if not set.
Raises:
LogicError: If the configuration has not been loaded.
"""
if self._configuration is None:
raise LogicError("logic error: configuration is not loaded")
return self._configuration.a2a_agents
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/configuration.py` around lines 431 - 437, Update the a2a_agents property
docstring to use the Google-style format, adding a Returns section for its
A2AAgentsConfiguration | None result and a Raises section documenting LogicError
when _configuration is not loaded.

Source: Coding guidelines

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.

2 participants