Skip to content
Merged
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
40 changes: 27 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,22 @@ public class WeatherAgentExecutorProducer {
}
```

### 4. Configure Executor Settings (Optional)
### 4. Configuration System

The A2A Java SDK uses a dedicated executor for handling asynchronous operations like streaming subscriptions. By default, this executor is configured with a core pool size of 5 threads and a maximum pool size of 50 threads, optimized for I/O-bound operations.
The A2A Java SDK uses a flexible configuration system that works across different frameworks.

You can customize the executor settings in your `application.properties`:
**Default behavior:** Configuration values come from `META-INF/a2a-defaults.properties` files on the classpath (provided by core modules and extras). These defaults work out of the box without any additional setup.

**Customizing configuration:**
- **Quarkus/MicroProfile Config users**: Add the [`microprofile-config`](integrations/microprofile-config/README.md) integration to override defaults via `application.properties`, environment variables, or system properties
- **Spring/other frameworks**: See the [integration module README](integrations/microprofile-config/README.md#custom-config-providers) for how to implement a custom `A2AConfigProvider`
- **Reference implementations**: Already include the MicroProfile Config integration

#### Configuration Properties

**Executor Settings** (Optional)

The SDK uses a dedicated executor for async operations like streaming. Default: 5 core threads, 50 max threads.

```properties
# Core thread pool size for the @Internal executor (default: 5)
Expand All @@ -249,20 +260,23 @@ a2a.executor.max-pool-size=50
a2a.executor.keep-alive-seconds=60
```

**Why this matters:**
- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load.
- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool used by other async tasks.
- **Concurrency**: In production environments with high concurrent streaming requests, increase the pool sizes accordingly.
**Blocking Call Timeouts** (Optional)

**Default Configuration:**
```properties
# These are the defaults - no need to set unless you want different values
a2a.executor.core-pool-size=5
a2a.executor.max-pool-size=50
a2a.executor.keep-alive-seconds=60
# Timeout for agent execution in blocking calls (default: 30 seconds)
a2a.blocking.agent.timeout.seconds=30

# Timeout for event consumption in blocking calls (default: 5 seconds)
a2a.blocking.consumption.timeout.seconds=5
```

**Note:** The reference server implementations automatically configure this executor. If you're creating a custom server integration, ensure you provide an `@Internal Executor` bean for optimal streaming performance.
**Why this matters:**
- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load.
- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool.
- **Concurrency**: In production with high concurrent streaming, increase pool sizes accordingly.
- **Agent Timeouts**: LLM-based agents may need longer timeouts (60-120s) compared to simple agents.

**Note:** The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in `application.properties`.

## A2A Client

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.time.Duration;
import java.time.Instant;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
Expand All @@ -14,10 +15,10 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import io.a2a.extras.common.events.TaskFinalizedEvent;
import io.a2a.server.config.A2AConfigProvider;
import io.a2a.server.tasks.TaskStateProvider;
import io.a2a.server.tasks.TaskStore;
import io.a2a.spec.Task;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -35,9 +36,24 @@ public class JpaDatabaseTaskStore implements TaskStore, TaskStateProvider {
Event<TaskFinalizedEvent> taskFinalizedEvent;

@Inject
@ConfigProperty(name = "a2a.replication.grace-period-seconds", defaultValue = "15")
A2AConfigProvider configProvider;

/**
* Grace period for task finalization in replicated scenarios (seconds).
* After a task reaches a final state, this is the minimum time to wait before cleanup
* to allow replicated events to arrive and be processed.
* <p>
* Property: {@code a2a.replication.grace-period-seconds}<br>
* Default: 15<br>
* Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath.
*/
long gracePeriodSeconds;

@PostConstruct
void initConfig() {
gracePeriodSeconds = Long.parseLong(configProvider.getValue("a2a.replication.grace-period-seconds"));
}
Comment on lines 38 to +55
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To improve maintainability and avoid magic strings, it's best to declare the configuration key as a private static final String constant. This prevents typos and makes it easier to manage keys.

Suggested change
@Inject
@ConfigProperty(name = "a2a.replication.grace-period-seconds", defaultValue = "15")
A2AConfigProvider configProvider;
/**
* Grace period for task finalization in replicated scenarios (seconds).
* After a task reaches a final state, this is the minimum time to wait before cleanup
* to allow replicated events to arrive and be processed.
* <p>
* Property: {@code a2a.replication.grace-period-seconds}<br>
* Default: 15<br>
* Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath.
*/
long gracePeriodSeconds;
@PostConstruct
void initConfig() {
gracePeriodSeconds = Long.parseLong(configProvider.getValue("a2a.replication.grace-period-seconds"));
}
@Inject
A2AConfigProvider configProvider;
private static final String REPLICATION_GRACE_PERIOD_SECONDS_KEY = "a2a.replication.grace-period-seconds";
/**
* Grace period for task finalization in replicated scenarios (seconds).
* After a task reaches a final state, this is the minimum time to wait before cleanup
* to allow replicated events to arrive and be processed.
* <p>
* Property: {@code a2a.replication.grace-period-seconds}<br>
* Default: 15<br>
* Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath.
*/
long gracePeriodSeconds;
@PostConstruct
void initConfig() {
gracePeriodSeconds = Long.parseLong(configProvider.getValue(REPLICATION_GRACE_PERIOD_SECONDS_KEY));
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will fix upstream. It was already reviewed and merged there


@Transactional
@Override
public void save(Task task) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# A2A JPA Database Task Store Default Configuration

# Grace period for task finalization in replicated scenarios (seconds)
# After a task reaches a final state, this is the minimum time to wait before cleanup
# to allow replicated events to arrive and be processed
a2a.replication.grace-period-seconds=15
148 changes: 148 additions & 0 deletions integrations/microprofile-config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# A2A Java SDK - MicroProfile Config Integration

This optional integration module provides MicroProfile Config support for the A2A Java SDK configuration system.

## Overview

The A2A Java SDK core uses the `A2AConfigProvider` interface for configuration, with default values loaded from `META-INF/a2a-defaults.properties` files on the classpath.

This module provides `MicroProfileConfigProvider`, which integrates with MicroProfile Config to allow configuration via:
- `application.properties`
- Environment variables
- System properties (`-D` flags)
- Custom ConfigSources

## Quick Start

### 1. Add Dependency

```xml
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-microprofile-config</artifactId>
<version>${io.a2a.sdk.version}</version>
</dependency>
```

### 2. Configure Properties

Once the dependency is added, you can override any A2A configuration property:

**application.properties:**
```properties
# Executor configuration
a2a.executor.core-pool-size=10
a2a.executor.max-pool-size=100

# Timeout configuration
a2a.blocking.agent.timeout.seconds=60
a2a.blocking.consumption.timeout.seconds=10
```

**Environment variables:**
```bash
export A2A_EXECUTOR_CORE_POOL_SIZE=10
export A2A_BLOCKING_AGENT_TIMEOUT_SECONDS=60
```

**System properties:**
```bash
java -Da2a.executor.core-pool-size=10 -jar your-app.jar
```

## How It Works

The `MicroProfileConfigProvider` implementation:

1. **First tries MicroProfile Config** - Checks `application.properties`, environment variables, system properties, and custom ConfigSources
2. **Falls back to defaults** - If not found, uses values from `META-INF/a2a-defaults.properties` provided by core modules and extras
3. **Priority 50** - Can be overridden by custom providers with higher priority

## Configuration Fallback Chain

```
MicroProfile Config Sources (application.properties, env vars, -D flags)
↓ (not found?)
DefaultValuesConfigProvider
→ Scans classpath for ALL META-INF/a2a-defaults.properties files
→ Merges all discovered properties together
→ Throws exception if duplicate keys found
↓ (property exists?)
Return merged default value
↓ (not found?)
IllegalArgumentException
```

**Note**: All `META-INF/a2a-defaults.properties` files (from server-common, extras modules, etc.) are loaded and merged together by `DefaultValuesConfigProvider` at startup. This is not a sequential fallback chain, but a single merged set of defaults.

## Available Configuration Properties

See the [main README](../../README.md#configuration-system) for a complete list of configuration properties.

## Framework Compatibility

This module works with any MicroProfile Config implementation:

- **Quarkus** - Built-in MicroProfile Config support
- **Helidon** - Built-in MicroProfile Config support
- **Open Liberty** - Built-in MicroProfile Config support
- **WildFly/JBoss EAP** - Add `smallrye-config` dependency
- **Other Jakarta EE servers** - Add MicroProfile Config implementation

## Custom Config Providers

If you're using a different framework (Spring, Micronaut, etc.), you can implement your own `A2AConfigProvider`:

```java
import io.a2a.server.config.A2AConfigProvider;
import io.a2a.server.config.DefaultValuesConfigProvider;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;

@ApplicationScoped
@Alternative
@Priority(100) // Higher than MicroProfileConfigProvider's priority of 50
public class OtherEnvironmentConfigProvider implements A2AConfigProvider {

@Inject
Environment env;

@Inject
DefaultValuesConfigProvider defaultValues;

@Override
public String getValue(String name) {
String value = env.getProperty(name);
if (value != null) {
return value;
}
// Fallback to defaults
return defaultValues.getValue(name);
}

@Override
public Optional<String> getOptionalValue(String name) {
String value = env.getProperty(name);
if (value != null) {
return Optional.of(value);
}
return defaultValues.getOptionalValue(name);
}
}
```

## Implementation Details

- **Package**: `io.a2a.integrations.microprofile`
- **Class**: `MicroProfileConfigProvider`
- **Priority**: 50 (can be overridden)
- **Scope**: `@ApplicationScoped`
- **Dependencies**: MicroProfile Config API, A2A SDK server-common

## Reference Implementations

The A2A Java SDK reference implementations (Quarkus-based) automatically include this integration module, so MicroProfile Config properties work out of the box.

If you're building a custom server implementation, add this dependency to enable property-based configuration.
59 changes: 59 additions & 0 deletions integrations/microprofile-config/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-parent</artifactId>
<version>0.3.3.Beta1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>a2a-java-sdk-microprofile-config</artifactId>

<packaging>jar</packaging>

<name>A2A Java SDK - MicroProfile Config Integration</name>
<description>MicroProfile Config integration for A2A Java SDK - provides A2AConfigProvider implementation</description>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>a2a-java-sdk-server-common</artifactId>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Loading