feat(cli): redesign a2a CLI — transport flag, .a2a.yaml config, tenant injection#101
feat(cli): redesign a2a CLI — transport flag, .a2a.yaml config, tenant injection#101Tehsmash wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
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.
| #[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, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| #[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", | |
| } | |
| } | |
| } |
| let patched = SendMessageRequest { | ||
| tenant: self.tenant.clone(), | ||
| ..req.clone() | ||
| }; | ||
| let result = self.transport.send_message(¶ms, &patched).await; |
There was a problem hiding this comment.
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(¶ms, req).await
} else {
let patched = SendMessageRequest {
tenant: self.tenant.clone(),
..req.clone()
};
self.transport.send_message(¶ms, &patched).await
};| 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()) | ||
| } |
There was a problem hiding this comment.
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())
})
}| } else { | ||
| let json = std::fs::read_to_string(agent_ref)?; | ||
| serde_json::from_str(&json).map_err(|e| CliError::InvalidAgentCard { | ||
| reason: e.to_string(), | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| } 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(), | |
| }) | |
| } |
- 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>
9c20a5e to
7ef636b
Compare
Summary
Redesigns the
a2aCLI and thea2a-clientlibrary per the proposal in issue #1929.Changes
--transportflag (replaces--binding)--transportis now a repeatable, ordered flag (e.g.--transport jsonrpc --transport http-json)[jsonrpc, http-json]slimrpcfeature--bindingsingle-value flag.a2a.yamlconfig file.a2a.yamltransports,bearer_token,headers,outputheadersare additive (config first, then flags)a2acli/src/config.rswith unit tests usingtempfileTenant auto-injection from
AgentInterface--tenantCLI flag and anytenantfield from the config fileA2AClientnow storestenant: Option<String>(set via.with_tenant()builder)tenantfield with the client's stored value — callers cannot set tenant per-requestA2AClientFactory::create_from_cardchains.with_tenant(iface.tenant.clone())so the tenant from the selectedAgentInterfaceis automatically applied to every requestOutput format
--output/-ois nowOption<OutputFormat>(no clap default); config file fills it in if absent; final fallback isprettyFiles changed
a2acli/src/lib.rsa2acli/src/config.rsa2acli/Cargo.tomlserde_yaml,tempfiledev-depCargo.tomlserde_yaml = "0.9"to workspace depsa2a-client/src/client.rstenantfield,with_tenant(), patching in all 11 methodsa2a-client/src/factory.rs.with_tenant(iface.tenant.clone())a2acli/tests/e2e.rs--transport, remove--tenant, add config-file output test