Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ a2a-slimrpc = { package = "a2a-slimrpc", path = "a2a-slimrpc", version = "0.2.3"
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
pbjson = "0.9"
pbjson-types = "0.9"

Expand Down
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,31 @@ HTTP+JSON, prints responses as JSON, and manages task push notification
configs.

```sh
cargo run --bin a2acli -- card
cargo run --bin a2acli -- send "hello from rust"
cargo run --bin a2acli -- stream "hello from rust"
cargo run --bin a2acli -- list-tasks
cargo run --bin a2acli -- push-config list task-123
# Inspect an agent card
a2a discover http://localhost:3000

# Send a one-shot message
a2a send http://localhost:3000 "hello from rust"

# Send a streaming message
a2a stream http://localhost:3000 "hello from rust"

# List tasks
a2a task list http://localhost:3000

# Manage push notification configs
a2a push-config list http://localhost:3000 task-123
```

By default the CLI targets `http://localhost:3000`, which matches the bundled
hello world server. Override the target with `--base-url https://host` for any
compatible A2A server, use `--binding jsonrpc` or `--binding http-json` to pin
Each command takes an agent reference as its first argument: a base URL
(`http://host` → card fetched from `/.well-known/agent-card.json`), a direct
URL to a card JSON file (`https://host/path/agent.json`), or a local file path.
Use `--enabled-binding jsonrpc` or `--enabled-binding http-json` to pin
transport selection, and pass `--bearer-token` or repeated `--header Name:Value`
arguments when the server requires authentication.

See [`a2acli/README.md`](a2acli/README.md) for the full command reference.

## Depending On The Workspace

The crates are published on crates.io. To depend on the latest development version directly from Git:
Expand Down
96 changes: 60 additions & 36 deletions a2a-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct A2AClient<T: Transport> {
transport: T,
interceptors: Vec<Arc<dyn CallInterceptor>>,
default_params: ServiceParams,
tenant: Option<String>,
}

impl<T: Transport> A2AClient<T> {
Expand All @@ -24,6 +25,7 @@ impl<T: Transport> A2AClient<T> {
transport,
interceptors: Vec::new(),
default_params,
tenant: None,
}
}

Expand All @@ -32,6 +34,11 @@ impl<T: Transport> A2AClient<T> {
self
}

pub fn with_tenant(mut self, tenant: Option<String>) -> Self {
self.tenant = tenant;
self
}

fn params(&self) -> ServiceParams {
self.default_params.clone()
}
Expand Down Expand Up @@ -72,92 +79,109 @@ impl<T: Transport> A2AClient<T> {

pub async fn send_message(
&self,
req: &SendMessageRequest,
mut req: SendMessageRequest,
) -> Result<SendMessageResponse, A2AError> {
let params = self.apply_before(methods::SEND_MESSAGE).await?;
let result = self.transport.send_message(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.send_message(&params, &req).await;
self.finish_call(methods::SEND_MESSAGE, result).await
}

pub async fn send_streaming_message(
&self,
req: &SendMessageRequest,
mut req: SendMessageRequest,
) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
let params = self.apply_before(methods::SEND_STREAMING_MESSAGE).await?;
let result = self.transport.send_streaming_message(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.send_streaming_message(&params, &req).await;
self.finish_call(methods::SEND_STREAMING_MESSAGE, result)
.await
}

pub async fn get_task(&self, req: &GetTaskRequest) -> Result<Task, A2AError> {
pub async fn get_task(&self, mut req: GetTaskRequest) -> Result<Task, A2AError> {
let params = self.apply_before(methods::GET_TASK).await?;
let result = self.transport.get_task(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.get_task(&params, &req).await;
self.finish_call(methods::GET_TASK, result).await
}

pub async fn list_tasks(&self, req: &ListTasksRequest) -> Result<ListTasksResponse, A2AError> {
pub async fn list_tasks(
&self,
mut req: ListTasksRequest,
) -> Result<ListTasksResponse, A2AError> {
let params = self.apply_before(methods::LIST_TASKS).await?;
let result = self.transport.list_tasks(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.list_tasks(&params, &req).await;
self.finish_call(methods::LIST_TASKS, result).await
}

pub async fn cancel_task(&self, req: &CancelTaskRequest) -> Result<Task, A2AError> {
pub async fn cancel_task(&self, mut req: CancelTaskRequest) -> Result<Task, A2AError> {
let params = self.apply_before(methods::CANCEL_TASK).await?;
let result = self.transport.cancel_task(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.cancel_task(&params, &req).await;
self.finish_call(methods::CANCEL_TASK, result).await
}

pub async fn subscribe_to_task(
&self,
req: &SubscribeToTaskRequest,
mut req: SubscribeToTaskRequest,
) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
let params = self.apply_before(methods::SUBSCRIBE_TO_TASK).await?;
let result = self.transport.subscribe_to_task(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.subscribe_to_task(&params, &req).await;
self.finish_call(methods::SUBSCRIBE_TO_TASK, result).await
}

pub async fn create_push_config(
&self,
req: &TaskPushNotificationConfig,
mut req: TaskPushNotificationConfig,
) -> Result<TaskPushNotificationConfig, A2AError> {
let params = self.apply_before(methods::CREATE_PUSH_CONFIG).await?;
let result = self.transport.create_push_config(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.create_push_config(&params, &req).await;
self.finish_call(methods::CREATE_PUSH_CONFIG, result).await
}

pub async fn get_push_config(
&self,
req: &GetTaskPushNotificationConfigRequest,
mut req: GetTaskPushNotificationConfigRequest,
) -> Result<TaskPushNotificationConfig, A2AError> {
let params = self.apply_before(methods::GET_PUSH_CONFIG).await?;
let result = self.transport.get_push_config(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.get_push_config(&params, &req).await;
self.finish_call(methods::GET_PUSH_CONFIG, result).await
}

pub async fn list_push_configs(
&self,
req: &ListTaskPushNotificationConfigsRequest,
mut req: ListTaskPushNotificationConfigsRequest,
) -> Result<ListTaskPushNotificationConfigsResponse, A2AError> {
let params = self.apply_before(methods::LIST_PUSH_CONFIGS).await?;
let result = self.transport.list_push_configs(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.list_push_configs(&params, &req).await;
self.finish_call(methods::LIST_PUSH_CONFIGS, result).await
}

pub async fn delete_push_config(
&self,
req: &DeleteTaskPushNotificationConfigRequest,
mut req: DeleteTaskPushNotificationConfigRequest,
) -> Result<(), A2AError> {
let params = self.apply_before(methods::DELETE_PUSH_CONFIG).await?;
let result = self.transport.delete_push_config(&params, req).await;
req.tenant = self.tenant.clone();
let result = self.transport.delete_push_config(&params, &req).await;
self.finish_call(methods::DELETE_PUSH_CONFIG, result).await
}

pub async fn get_extended_agent_card(
&self,
req: &GetExtendedAgentCardRequest,
mut req: GetExtendedAgentCardRequest,
) -> Result<AgentCard, A2AError> {
let params = self.apply_before(methods::GET_EXTENDED_AGENT_CARD).await?;
let result = self.transport.get_extended_agent_card(&params, req).await;
req.tenant = self.tenant.clone();
let result = self
.transport
.get_extended_agent_card(&params, &req)
.await;
self.finish_call(methods::GET_EXTENDED_AGENT_CARD, result)
.await
}
Expand Down Expand Up @@ -189,7 +213,7 @@ impl<T: Transport> SendMessageExt for A2AClient<T> {
metadata: None,
tenant: None,
};
self.send_message(&req).await
self.send_message(req).await
}
}

Expand Down Expand Up @@ -476,7 +500,7 @@ mod tests {
metadata: None,
tenant: None,
};
let resp = client.send_message(&req).await.unwrap();
let resp = client.send_message(req).await.unwrap();
assert!(matches!(resp, SendMessageResponse::Task(_)));
}

Expand All @@ -502,7 +526,7 @@ mod tests {
tenant: None,
};

client.send_message(&req).await.unwrap();
client.send_message(req).await.unwrap();

let calls = state.calls.lock().unwrap();
let params = &calls[0].1;
Expand Down Expand Up @@ -541,7 +565,7 @@ mod tests {
tenant: None,
};

let err = client.send_message(&req).await.unwrap_err();
let err = client.send_message(req).await.unwrap_err();
assert_eq!(err.message, "boom");

let events = events.lock().unwrap().clone();
Expand All @@ -561,7 +585,7 @@ mod tests {
metadata: None,
tenant: None,
};
let mut stream = client.send_streaming_message(&req).await.unwrap();
let mut stream = client.send_streaming_message(req).await.unwrap();
let item = stream.next().await.unwrap().unwrap();
assert!(matches!(item, StreamResponse::StatusUpdate(_)));
}
Expand All @@ -574,7 +598,7 @@ mod tests {
history_length: None,
tenant: None,
};
let task = client.get_task(&req).await.unwrap();
let task = client.get_task(req).await.unwrap();
assert_eq!(task.id, "t1");
}

Expand All @@ -591,7 +615,7 @@ mod tests {
include_artifacts: None,
tenant: None,
};
let resp = client.list_tasks(&req).await.unwrap();
let resp = client.list_tasks(req).await.unwrap();
assert!(resp.tasks.is_empty());
}

Expand All @@ -603,7 +627,7 @@ mod tests {
metadata: None,
tenant: None,
};
let task = client.cancel_task(&req).await.unwrap();
let task = client.cancel_task(req).await.unwrap();
assert_eq!(task.status.state, TaskState::Canceled);
}

Expand All @@ -614,7 +638,7 @@ mod tests {
id: "t1".into(),
tenant: None,
};
let _stream = client.subscribe_to_task(&req).await.unwrap();
let _stream = client.subscribe_to_task(req).await.unwrap();
}

#[tokio::test]
Expand All @@ -628,7 +652,7 @@ mod tests {
authentication: None,
tenant: None,
};
let resp = client.create_push_config(&req).await.unwrap();
let resp = client.create_push_config(req).await.unwrap();
assert_eq!(resp.task_id, "t1");
}

Expand All @@ -640,7 +664,7 @@ mod tests {
id: "cfg1".into(),
tenant: None,
};
let resp = client.get_push_config(&req).await.unwrap();
let resp = client.get_push_config(req).await.unwrap();
assert_eq!(resp.id, Some("cfg1".into()));
}

Expand All @@ -653,7 +677,7 @@ mod tests {
page_token: None,
tenant: None,
};
let resp = client.list_push_configs(&req).await.unwrap();
let resp = client.list_push_configs(req).await.unwrap();
assert!(resp.configs.is_empty());
}

Expand All @@ -665,14 +689,14 @@ mod tests {
id: "cfg1".into(),
tenant: None,
};
client.delete_push_config(&req).await.unwrap();
client.delete_push_config(req).await.unwrap();
}

#[tokio::test]
async fn test_get_extended_agent_card() {
let client = make_client();
let req = GetExtendedAgentCardRequest { tenant: None };
let card = client.get_extended_agent_card(&req).await.unwrap();
let card = client.get_extended_agent_card(req).await.unwrap();
assert_eq!(card.name, "Test");
}

Expand Down
6 changes: 3 additions & 3 deletions a2a-client/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ impl A2AClientFactory {
for (_prio, iface, factory) in &candidates {
match factory.create(card, iface).await {
Ok(transport) => {
return Ok(
A2AClient::new(transport).with_interceptors(self.interceptors.clone())
);
return Ok(A2AClient::new(transport)
.with_tenant(iface.tenant.clone())
.with_interceptors(self.interceptors.clone()));
}
Err(e) => {
tracing::debug!(
Expand Down
Loading
Loading