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
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,152 @@
import io.a2a.server.events.EventQueue;
import io.a2a.spec.A2AError;

/**
* Core business logic interface for implementing A2A agent functionality.
* <p>
* This is the primary extension point where agent developers implement their agent's behavior -
* LLM interactions, data processing, external API calls, or any custom logic. Along with an
* {@link io.a2a.spec.AgentCard}, implementing this interface is the minimum requirement to
* create a functioning A2A agent.
* </p>
*
* <h2>Lifecycle</h2>
* The {@link io.a2a.server.requesthandlers.DefaultRequestHandler} executes AgentExecutor methods
* asynchronously in a background thread pool when requests arrive from transport layers.
* Your implementation should:
* <ul>
* <li>Use the {@link EventQueue} to enqueue task status updates and artifacts</li>
* <li>Use {@link io.a2a.server.tasks.TaskUpdater} helper for common lifecycle operations</li>
* <li>Handle cancellation via the {@link #cancel(RequestContext, EventQueue)} method</li>
* <li>Be thread-safe if maintaining state across invocations</li>
* </ul>
*
* <h2>Threading Model</h2>
* <ul>
* <li>{@code execute()} runs in the agent-executor thread pool (background thread)</li>
* <li>Events are consumed by Vert.x worker threads that return responses to clients</li>
* <li>Don't block waiting for events to be consumed - enqueue and return</li>
* <li>Multiple {@code execute()} calls may run concurrently for different tasks</li>
* </ul>
*
* <h2>CDI Integration</h2>
* Provide your AgentExecutor via CDI producer:
* <pre>{@code
* @ApplicationScoped
* public class MyAgentExecutorProducer {
* @Inject
* MyService myService; // Your business logic
*
* @Produces
* public AgentExecutor agentExecutor() {
* return new MyAgentExecutor(myService);
* }
* }
* }</pre>
*
* <h2>Example Implementation</h2>
* <pre>{@code
* public class WeatherAgentExecutor implements AgentExecutor {
* private final WeatherService weatherService;
*
* public WeatherAgentExecutor(WeatherService weatherService) {
* this.weatherService = weatherService;
* }
*
* @Override
* public void execute(RequestContext context, EventQueue eventQueue) {
* TaskUpdater updater = new TaskUpdater(context, eventQueue);
*
* // Initialize task if this is a new conversation
* if (context.getTask() == null) {
* updater.submit();
* }
* updater.startWork();
*
* // Extract user input from the message
* String userMessage = context.getUserInput("\n");
*
* // Process request (your business logic)
* String weatherData = weatherService.getWeather(userMessage);
*
* // Return result as artifact
* updater.addArtifact(List.of(new TextPart(weatherData, null)));
* updater.complete();
* }
*
* @Override
* public void cancel(RequestContext context, EventQueue eventQueue) {
* // Clean up resources and mark as canceled
* new TaskUpdater(context, eventQueue).cancel();
* }
* }
* }</pre>
*
* <h2>Streaming Results</h2>
* For long-running operations or LLM streaming, enqueue multiple artifacts:
* <pre>{@code
* updater.startWork();
* for (String chunk : llmService.stream(userInput)) {
* updater.addArtifact(List.of(new TextPart(chunk, null)));
* }
* updater.complete(); // Final event closes the queue
* }</pre>
*
* @see RequestContext
* @see io.a2a.server.tasks.TaskUpdater
* @see io.a2a.server.events.EventQueue
* @see io.a2a.server.requesthandlers.DefaultRequestHandler
* @see io.a2a.spec.AgentCard
*/
public interface AgentExecutor {
/**
* Executes the agent's business logic for a message.
* <p>
* Called asynchronously by {@link io.a2a.server.requesthandlers.DefaultRequestHandler}
* in a background thread when a client sends a message. Enqueue events to the queue as
* processing progresses. The queue remains open until you enqueue a final event
* (COMPLETED, FAILED, or CANCELED state).
* </p>
* <p>
* <b>Important:</b> Don't throw exceptions for business logic errors. Instead, use
* {@code updater.fail(errorMessage)} to communicate failures to the client gracefully.
* Only throw {@link A2AError} for truly exceptional conditions.
* </p>
*
* @param context the request context containing the message, task state, and configuration
* @param eventQueue the queue for enqueueing status updates and artifacts
* @throws A2AError if execution fails catastrophically (exception propagates to client)
*/
void execute(RequestContext context, EventQueue eventQueue) throws A2AError;

/**
* Cancels an ongoing agent execution.
* <p>
* Called when a client requests task cancellation via the cancelTask operation.
* You should:
* <ul>
* <li>Stop any ongoing work (interrupt LLM calls, cancel API requests)</li>
* <li>Enqueue a CANCELED status event (typically via {@code TaskUpdater.cancel()})</li>
* <li>Clean up resources (close connections, release locks)</li>
* </ul>
* <p>
* <b>Note:</b> The {@link #execute(RequestContext, EventQueue)} method may still be
* running on another thread. Use appropriate synchronization or interruption mechanisms
* if your agent maintains cancellable state.
* <p>
* <b>Error Handling:</b>
* <ul>
* <li>Throw {@link io.a2a.spec.TaskNotCancelableError} if your agent does not support
* cancellation at all (e.g., fire-and-forget agents)</li>
* <li>Throw {@link A2AError} if cancellation is supported but failed to execute
* (e.g., unable to interrupt running operation)</li>
* <li>Return normally after enqueueing CANCELED event if cancellation succeeds</li>
* </ul>
*
* @param context the request context for the task being canceled
* @param eventQueue the queue for enqueueing the cancellation event
* @throws io.a2a.spec.TaskNotCancelableError if this agent does not support cancellation
* @throws A2AError if cancellation is supported but failed to execute
*/
void cancel(RequestContext context, EventQueue eventQueue) throws A2AError;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,62 @@
import io.a2a.spec.TextPart;
import org.jspecify.annotations.Nullable;

/**
* Container for request parameters and task state provided to {@link AgentExecutor}.
* <p>
* This class encapsulates all the information an agent needs to process a request:
* the user's message, existing task state (for continuing conversations), configuration,
* and server call context. It's the primary way agents access request data.
* </p>
*
* <h2>Key Components</h2>
* <ul>
* <li><b>Message:</b> The user's input message with parts (text, images, etc.)</li>
* <li><b>Task:</b> Existing task state for continuing conversations (null for new conversations)</li>
* <li><b>TaskId/ContextId:</b> Identifiers for the task and conversation (auto-generated if not provided)</li>
* <li><b>Configuration:</b> Request settings (blocking mode, push notifications, etc.)</li>
* <li><b>Related Tasks:</b> Other tasks in the same conversation context</li>
* </ul>
*
* <h2>Common Usage Patterns</h2>
* <pre>{@code
* public void execute(RequestContext context, EventQueue queue) {
* // Check if this is a new conversation or continuation
* Task existingTask = context.getTask();
* if (existingTask == null) {
* // New conversation - initialize
* } else {
* // Continuing conversation - access history
* List<Message> history = existingTask.history();
* }
*
* // Extract user input
* String userMessage = context.getUserInput("\n");
*
* // Access configuration if needed
* MessageSendConfiguration config = context.getConfiguration();
* boolean isBlocking = config != null && config.blocking();
*
* // Process and respond...
* }
* }</pre>
*
* <h2>Text Extraction Helper</h2>
* The {@link #getUserInput(String)} method is a convenient way to extract text from
* message parts:
* <pre>{@code
* // Get all text parts joined with newlines
* String text = context.getUserInput("\n");
*
* // Get all text parts joined with spaces
* String text = context.getUserInput(" ");
* }</pre>
*
* @see AgentExecutor
* @see Message
* @see Task
* @see MessageSendConfiguration
*/
public class RequestContext {

private @Nullable MessageSendParams params;
Expand Down Expand Up @@ -54,38 +110,128 @@ public RequestContext(
}
}

public @Nullable MessageSendParams getParams() {
return params;
}

/**
* Returns the task identifier.
* <p>
* This is auto-generated (UUID) if not provided by the client in the message parameters.
* It can be null if the context was not created from message parameters.
* </p>
*
* @return the task ID
*/
public @Nullable String getTaskId() {
return taskId;
}

/**
* Returns the conversation context identifier.
* <p>
* Conversation contexts group related tasks together (e.g., multiple tasks
* in the same user session). This is auto-generated (UUID) if not provided by the client
* in the message parameters. It can be null if the context was not created from message parameters.
* </p>
*
* @return the context ID
*/
public @Nullable String getContextId() {
return contextId;
}

/**
* Returns the existing task state, if this is a continuation of a conversation.
* <p>
* For new conversations, this is null. For continuing conversations, contains
* the full task state including history, artifacts, and status.
* <p>
* <b>Common Pattern:</b>
* <pre>{@code
* if (context.getTask() == null) {
* // New conversation - initialize state
* } else {
* // Continuing - access previous messages
* List<Message> history = context.getTask().history();
* }
* }</pre>
*
* @return the existing task, or null if this is a new conversation
*/
public @Nullable Task getTask() {
return task;
}

/**
* Returns other tasks in the same conversation context.
* <p>
* Useful for multi-task conversations where the agent needs to access
* state from related tasks.
* </p>
*
* @return unmodifiable list of related tasks (empty if none)
*/
public List<Task> getRelatedTasks() {
return Collections.unmodifiableList(relatedTasks);
}

/**
* Returns the user's message.
* <p>
* Contains the message parts (text, images, etc.) sent by the client.
* Use {@link #getUserInput(String)} for convenient text extraction.
* </p>
*
* @return the message, or null if not available
* @see #getUserInput(String)
*/
public @Nullable Message getMessage() {
return params != null ? params.message() : null;
}

/**
* Returns the request configuration.
* <p>
* Contains settings like blocking mode, push notification config, etc.
* </p>
*
* @return the configuration, or null if not provided
*/
public @Nullable MessageSendConfiguration getConfiguration() {
return params != null ? params.configuration() : null;
}

/**
* Returns the server call context.
* <p>
* Contains transport-specific information like authentication, headers, etc.
* Most agents don't need this.
* </p>
*
* @return the call context, or null if not available
*/
public @Nullable ServerCallContext getCallContext() {
return callContext;
}

/**
* Extracts all text content from the message and joins with the specified delimiter.
* <p>
* This is a convenience method for getting text input from messages that may contain
* multiple text parts. Non-text parts (images, etc.) are ignored.
* <p>
* <b>Examples:</b>
* <pre>{@code
* // Join with newlines (common for multi-paragraph input)
* String text = context.getUserInput("\n");
*
* // Join with spaces (common for single-line input)
* String text = context.getUserInput(" ");
*
* // Default delimiter is newline
* String text = context.getUserInput(null); // uses "\n"
* }</pre>
*
* @param delimiter the string to insert between text parts (null defaults to "\n")
* @return all text parts joined with delimiter, or empty string if no message
*/
public String getUserInput(String delimiter) {
if (params == null) {
return "";
Expand Down
Loading