Skip to content

feat(cli): redesign a2a CLI — transport flag, .a2a.yaml config, tenant injection#101

Open
Tehsmash wants to merge 7 commits into
mainfrom
feat/cli-redesign-1929
Open

feat(cli): redesign a2a CLI — transport flag, .a2a.yaml config, tenant injection#101
Tehsmash wants to merge 7 commits into
mainfrom
feat/cli-redesign-1929

Conversation

@Tehsmash

@Tehsmash Tehsmash commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Redesigns the a2a CLI and the a2a-client library per the proposal in issue #1929.

Changes

--transport flag (replaces --binding)

  • --transport is now a repeatable, ordered flag (e.g. --transport jsonrpc --transport http-json)
  • Default when omitted: [jsonrpc, http-json]
  • SlimRPC must be explicitly listed even when compiled in with the slimrpc feature
  • Removes the old --binding single-value flag

.a2a.yaml config file

  • New hierarchical config file: walks from CWD up to (inclusive) the home directory looking for .a2a.yaml
  • Supports: transports, bearer_token, headers, output
  • CLI flags always take precedence; headers are additive (config first, then flags)
  • Config discovery logic is in a2acli/src/config.rs with unit tests using tempfile

Tenant auto-injection from AgentInterface

  • Removed --tenant CLI flag and any tenant field from the config file
  • A2AClient now stores tenant: Option<String> (set via .with_tenant() builder)
  • All 11 request-forwarding methods unconditionally override the request's tenant field with the client's stored value — callers cannot set tenant per-request
  • A2AClientFactory::create_from_card chains .with_tenant(iface.tenant.clone()) so the tenant from the selected AgentInterface is automatically applied to every request

Output format

  • --output / -o is now Option<OutputFormat> (no clap default); config file fills it in if absent; final fallback is pretty
  • Enables the config file to control output format without conflicting with clap's default

Files changed

File Change
a2acli/src/lib.rs CLI struct, command parsing, request builders, output logic
a2acli/src/config.rs New: config file discovery, loading, and merging
a2acli/Cargo.toml Add serde_yaml, tempfile dev-dep
Cargo.toml Add serde_yaml = "0.9" to workspace deps
a2a-client/src/client.rs tenant field, with_tenant(), patching in all 11 methods
a2a-client/src/factory.rs Chain .with_tenant(iface.tenant.clone())
a2acli/tests/e2e.rs Update tests: --transport, remove --tenant, add config-file output test

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

Copy link
Copy Markdown

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 refactors the A2A CLI binary to 'a2a', restructures subcommands (such as grouping task operations under a 'task' subcommand and renaming 'card' to 'discover'), adds support for reading agent cards from local files, and introduces a '.a2a.yaml' configuration file to load default CLI options. Feedback on these changes highlights several issues: a missing 'Slimrpc' variant in the 'Transport' enum causes compilation failures when the 'slimrpc' feature is enabled; the configuration search fails to find the home directory config when run from an unrelated directory tree; cloning requests on every client call is inefficient; and using synchronous file I/O inside an async function blocks the executor thread.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread a2acli/src/lib.rs
Comment on lines 293 to 306
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Binding {
pub enum Transport {
Jsonrpc,
HttpJson,
}

impl Binding {
impl Transport {
fn protocol(self) -> &'static str {
match self {
Binding::Jsonrpc => TRANSPORT_PROTOCOL_JSONRPC,
Binding::HttpJson => TRANSPORT_PROTOCOL_HTTP_JSON,
Transport::Jsonrpc => TRANSPORT_PROTOCOL_JSONRPC,
Transport::HttpJson => TRANSPORT_PROTOCOL_HTTP_JSON,
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The Transport enum is missing the Slimrpc variant, which causes a compilation error in a2acli/src/config.rs when the slimrpc feature is enabled. Add the Slimrpc variant under the #[cfg(feature = "slimrpc")] attribute to match its usage in the configuration parsing logic.

Suggested change
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Binding {
pub enum Transport {
Jsonrpc,
HttpJson,
}
impl Binding {
impl Transport {
fn protocol(self) -> &'static str {
match self {
Binding::Jsonrpc => TRANSPORT_PROTOCOL_JSONRPC,
Binding::HttpJson => TRANSPORT_PROTOCOL_HTTP_JSON,
Transport::Jsonrpc => TRANSPORT_PROTOCOL_JSONRPC,
Transport::HttpJson => TRANSPORT_PROTOCOL_HTTP_JSON,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Transport {
Jsonrpc,
HttpJson,
#[cfg(feature = "slimrpc")]
Slimrpc,
}
impl Transport {
fn protocol(self) -> &'static str {
match self {
Transport::Jsonrpc => TRANSPORT_PROTOCOL_JSONRPC,
Transport::HttpJson => TRANSPORT_PROTOCOL_HTTP_JSON,
#[cfg(feature = "slimrpc")]
Transport::Slimrpc => "slimrpc",
}
}
}

Comment thread a2a-client/src/client.rs Outdated
Comment on lines +85 to +89
let patched = SendMessageRequest {
tenant: self.tenant.clone(),
..req.clone()
};
let result = self.transport.send_message(&params, &patched).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Cloning the entire SendMessageRequest on every client call can be highly inefficient, especially for large message payloads. Since most requests do not require overriding the tenant (or the tenant already matches), we can avoid cloning entirely by checking if self.tenant == req.tenant before cloning. This optimization can also be applied to the other request-forwarding methods in this file.

        let result = if self.tenant == req.tenant {
            self.transport.send_message(&params, req).await
        } else {
            let patched = SendMessageRequest {
                tenant: self.tenant.clone(),
                ..req.clone()
            };
            self.transport.send_message(&params, &patched).await
        };

Comment thread a2acli/src/config.rs
Comment on lines +37 to +41
pub fn find_config_file() -> Option<PathBuf> {
let cwd = std::env::current_dir().ok()?;
let home = home_dir();
find_config_file_from(&cwd, home.as_deref())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When the CLI is run from a directory outside the home directory tree (e.g., /tmp or another drive), walking up the directory tree will never reach the home directory. As a result, the global configuration file at ~/.a2a.yaml is completely ignored. Fall back to checking the home directory directly if the hierarchical search from the current working directory yields no config file.

pub fn find_config_file() -> Option<PathBuf> {
    let cwd = std::env::current_dir().ok()?;
    let home = home_dir();
    find_config_file_from(&cwd, home.as_deref()).or_else(|| {
        home.as_ref()
            .map(|h| h.join(CONFIG_FILENAME))
            .filter(|p| p.is_file())
    })
}

Comment thread a2acli/src/lib.rs
Comment on lines +656 to +661
} else {
let json = std::fs::read_to_string(agent_ref)?;
serde_json::from_str(&json).map_err(|e| CliError::InvalidAgentCard {
reason: e.to_string(),
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using std::fs::read_to_string inside an async fn blocks the async executor thread, which can degrade performance. Since tokio is already a dependency with the full feature set, use tokio::fs::read_to_string instead to perform non-blocking asynchronous file I/O.

Suggested change
} else {
let json = std::fs::read_to_string(agent_ref)?;
serde_json::from_str(&json).map_err(|e| CliError::InvalidAgentCard {
reason: e.to_string(),
})
}
} else {
let json = tokio::fs::read_to_string(agent_ref).await?;
serde_json::from_str(&json).map_err(|e| CliError::InvalidAgentCard {
reason: e.to_string(),
})
}

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

LGTM ✅

Tehsmash added 7 commits July 16, 2026 15:01
- Rename binary from `a2acli` to `a2a`
- Replace `--base-url` global with positional `<agent-ref>` on every command
- Group task commands under `task get/list/cancel/subscribe`
- Replace `card`/`extended-card` with `discover [--extended]`
- Add `-o/--output pretty|json` replacing `--compact`
- Replace `--binding` with `--transport` (repeatable, ordered); defaults to
  jsonrpc + http-json so SLIMRPC is opt-in even when compiled with the feature
- Show meaningful parse error when agent card JSON is invalid
- Support local file paths as agent references

Signed-off-by: Sam Betts <1769706+Tehsmash@users.noreply.github.com>
- Add hierarchical .a2a.yaml discovery (CWD up to home dir) that
  configures transports, bearer_token, headers, and output format;
  CLI flags always take precedence over config file values
- Remove --tenant CLI flag and config option; tenant is now
  automatically injected by A2AClient from the selected AgentInterface,
  so callers never need to set it manually
- A2AClient.with_tenant() stores the interface tenant and overrides
  the tenant field in every outgoing request, making it non-overridable
  by the caller

Signed-off-by: Sam Betts <1769706+Tehsmash@users.noreply.github.com>
Signed-off-by: Sam Betts <1769706+Tehsmash@users.noreply.github.com>
Signed-off-by: Sam Betts <1769706+Tehsmash@users.noreply.github.com>
…nfig fallback and async file I/O

- A2AClient methods now take request structs by value (mut req) and
  assign req.tenant in place, eliminating the struct-spread clone on
  every call
- find_config_file() falls back to checking the home directory directly
  when CWD is outside the home directory tree (e.g. /tmp)
- resolve_agent_card uses tokio::fs::read_to_string instead of the
  blocking std::fs variant
- Remove the forward slimrpc stub from parse_binding_str; it will be
  re-added in the follow-up slimrpc PR

Signed-off-by: Sam Betts <1769706+Tehsmash@users.noreply.github.com>
Gate file I/O imports and config-loading functions with #[cfg(not(test))]
since they are only reachable from the non-test code path, and move the
mut rebinding of cli inside that block to silence the unused_mut warning.

Signed-off-by: Sam Betts <1769706+Tehsmash@users.noreply.github.com>
Replace the stub a2acli/README.md with a complete command reference
covering all subcommands, global options, config file, auth, output
formats, and transport negotiation. Document the AGENT_REF resolution
rules (base URL, direct .json URL, local file path) throughout.

Add a2acli-skill/SKILL.md as a concise skill reference for AI-assisted
CLI use. Update the root README's CLI section to use current command
names and link to the full reference.

Signed-off-by: Sam Betts <1769706+Tehsmash@users.noreply.github.com>
@Tehsmash
Tehsmash force-pushed the feat/cli-redesign-1929 branch from 9c20a5e to 7ef636b Compare July 16, 2026 14:02
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