Release boundary: Maven Central currently publishes
0.1.1(tag v0.1.1, 952cde2). This guide follows current source. Independent center HTTP authentication/TLS, PSOP account login and task-state-independent Notification-T handling were added after that tag and are unreleased. Build an exact reviewed source revision with a unique unpublished version for those capabilities; do not overwrite a released coordinate. Source revision=0.1.0-SNAPSHOT is a development coordinate, not a newer release.
The A2A-T Workflow Execution Engine is a Java SDK for orchestrating multi-agent workflows using the A2A protocol with A2A-T telecom extensions.
The engine schedules A2A tasks, envelopes final content, manages authentication/transport and waits for results. Host callbacks own A2A-T generation, semantic validation, schemas and any LLM calls.
| Requirement | Version |
|---|---|
| JDK | 17+ |
| Maven | 3.6+ |
<dependency>
<groupId>net.openan.workflow.sdk</groupId>
<artifactId>workflow-engine</artifactId>
<version>0.1.1</version>
</dependency>Version 0.1.1 is published to Maven Central. The repository currently builds 0.1.0-SNAPSHOT.
Four steps: define workflow -> load AgentCard -> implement ControlPoint -> execute.
Complete runnable source: HostQuickStart.java. It is compiled and its remote-task/local-aggregation flow is tested by HostQuickStartTest. Copy that source into your host project or run it from samples in IDEA (registry URL, target agent name, credentials path). The snippets below explain the same API; use one AgentCard loading option, and implement domain-specific content for Task-T. Each nonblank business condition requires an onRoute decision; this minimal workflow uses unconditional edges.
Workflow workflow = Workflow.builder()
.name("Service Analysis")
.steps(List.of(
WorkflowStep.builder()
.name("analyze")
.subtasks(List.of(
Task.builder()
.agent("Dispatched Agent")
.skill("analysis")
.description("Analyze the request")
.build()))
.next(List.of(
JumpCondition.builder()
.step("merge")
.condition("")
.build()))
.layer(0)
.build(),
WorkflowStep.builder()
.name("merge")
.stepType(StepType.SELF_LOOP) // host-agent local step; no A2A-T send
.subtasks(List.of(
Task.builder()
.agent("Host Agent")
.skill("aggregate")
.description("Merge results")
.build()))
.next(List.of(
JumpCondition.builder()
.step("end")
.condition("")
.build()))
.layer(1)
.contextFrom(List.of("*"))
.build()
))
.build();// Option A: From JSON files
ObjectMapper mapper = new ObjectMapper()
.registerModule(new AgentCardJacksonModule());
AgentCard card = mapper.readValue(
new File("agentcard/my_agent.json"), AgentCard.class);
// Option B: From Registry Center
HttpAuthProvider registryAuth = HttpAuthProvider.bearer(() -> System.getenv("REGISTRY_API_TOKEN"));
RegistryClient registry = new RegistryClient("https://127.0.0.1:5000", registryAuth, TlsPolicy.strict());
ObjectMapper cardMapper = new ObjectMapper().registerModule(new AgentCardJacksonModule());
List<AgentCard> cards = registry.fetchAgentCards().stream()
.map(raw -> cardMapper.convertValue(raw, AgentCard.class)).toList();RegistryClient defaults to a 30-second deadline including response-body consumption. For another budget, use
new RegistryClient(url, registryAuth, TlsPolicy.strict(), Duration.ofSeconds(15)); only positive
durations are accepted. All discovery and registration requests apply credentials immediately before sending.
Credentials are sent in headers, never in URL query parameters. Missing/invalid Bearer tokens fail locally.
interface ControlPoint {
CompletableFuture<MessageContent> onTask(TaskRequest request);
CompletableFuture<TaskResult> onSelfTask(TaskRequest request);
CompletableFuture<RouteDecision> onRoute(RouteRequest request);
CompletableFuture<NegotiationReply> onNegotiation(NegotiationRequest request);
}onTask returns final parts/metadata/extensions; the engine sends them without generating or rewriting content. onSelfTask returns local TaskResult, onRoute independently allows or denies one conditional edge, and onNegotiation returns Send or Stop. Unconditional edges bypass onRoute and always run. Unimplemented callbacks fail explicitly. No echo-success, implicit route approval or automatic consent. See Business callback contract for fields and working examples.
ControlPoint callbacks = ControlPoint.builder()
.onTask(request -> CompletableFuture.completedFuture(
MessageContent.text(request.getInstruction())))
.onSelfTask(request -> CompletableFuture.completedFuture(
TaskResult.success(List.of(Map.of(
"sourceResults", request.getWorkflowInput().upstreamResults())))))
.onRoute(request -> CompletableFuture.completedFuture(
RouteDecision.deny("Replace with host condition evaluation")))
.onNegotiation(request -> CompletableFuture.completedFuture(
new NegotiationReply.Stop("manual.required", "Manual confirmation required")))
.build();CompletableFuture<ExecutionResult> execution = ExecutePsop.builder()
.psop(workflow)
.agentCards(cards)
.controlPoint(callbacks)
.runtimeIntent("Analyze the service request")
.lang("zh")
.credentialsConfigPath("credentials.json")
.sslVerify(true)
.onFinish((r, history) -> {
System.out.println("Result: " + r.isSuccess());
})
.execute();
try {
ExecutionResult result = execution.get(10, TimeUnit.MINUTES);
System.out.println(result.getStepOutputs());
} finally {
if (!execution.isDone()) execution.cancel(true);
}Required: psop, controlPoint. All other config items have defaults.
When the host supplies a configured engineClient, that client owns AgentCard, runtime, TLS, and credentials
construction. Do not also set the corresponding ExecutePsop.Builder options; conflicting settings fail before
execution instead of being silently ignored.
The engine does not read A2A-T .env files or create LLM clients. If host-agent callbacks use A2A-T, initialize A2ATClient/A2ATServer with a host-owned environment file containing provider/model/key/base URL and A2AT_LANGUAGE. A sample-specific a2atEnvPath is not an engine builder option. Keep dispatched-agent credential decryption separate from LLM configuration: pass the secret explicitly through WorkflowEngineClientConfig.builder() .credentialEncryptionKey (key) when using encrypted built-in credentials, then pass that configured engineClient to ExecutePsop. Custom AuthProvider owns its own token/configuration. Tests use the current SDK SPI with an offline provider, not template overrides or production fallbacks.
For agents requiring authentication, provide a JSON credentials file:
{
"Dispatched Agent": {
"bearerAuth": {
"login_url": "https://agent.example.com/oauth/token",
"method": "POST",
"request_fields": {
"username": "service-account",
"password": "enc:<base64-iv>:<base64-ciphertext>"
},
"token_field": "accessSession",
"token_ttl": 3600
}
}
}- Encrypted passwords use
enc:<iv>:<ciphertext>format, key fromA2AT_CRED_KEY - Plaintext values (no
enc:prefix) are also accepted - Tokens are cached and refreshed automatically
Passwords in the credentials file support encrypted storage to avoid plaintext exposure.
Generate a key
openssl rand -hex 32Example output:
4f8a2b1c3d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b
Supply the key explicitly as credentialEncryptionKey, or set the OS environment variable below.
Resolution order is explicit instance key > OS environment > JVM property. The engine does not load .env automatically:
A2AT_CRED_KEY=4f8a2b1c3d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b
For instance-local encryption, call CredentialCrypto.encrypt(plaintext, keyHex). An explicit key overrides OS/JVM
configuration without modifying system properties. Never log plaintext or the key.
Encrypt a password
Build the jar with mvn -pl workflow-engine -am "-Drevision=0.1.0" package; commands below run from the repository root.
set is Windows cmd syntax (PowerShell: $env:A2AT_CRED_KEY='...'). This CLI needs only the SDK jar and JDK.
Use disposable example values here: command-line passwords/keys can appear in shell history and process listings.
For production, obtain secrets securely in the host and use the Java encryption API.
# Option 1: set env var first
set A2AT_CRED_KEY=4f8a2b1c3d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b
java -cp workflow-engine/target/workflow-engine-0.1.0.jar dev.openan.workflow.engine.client.CredentialCrypto "Admin@123"
# Option 2: pass key as second argument
java -cp workflow-engine/target/workflow-engine-0.1.0.jar dev.openan.workflow.engine.client.CredentialCrypto "Admin@123" 4f8a2b1c3d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2bOutput:
enc:uHQcTeKZMVNRM9Ga:o5vm4weRozBXBs04phrLq7j7+/yRVyDsrw==
Paste the output into the value field of the credentials JSON.
Rotating the key
- Generate a new key:
openssl rand -hex 32 - Update the host secret store / explicit
credentialEncryptionKey, or its OS/JVMA2AT_CRED_KEY - Re-encrypt all passwords:
java -cp workflow-engine/target/workflow-engine-0.1.0.jar dev.openan.workflow.engine.client.CredentialCrypto "plaintext" new-key - Update the
enc:...results in the credentials JSON file
The
.envfile should not be committed to version control. Add it to.gitignore.
When tokens are obtained by the integrator or an external identity service, or the mechanism is non-standard, implement
the AuthProvider interface. It has a single method:
public interface AuthProvider {
void applyAuth(String agentName, AgentCard agentCard, Map<String, String> headers);
}applyAuth is called before every message send. The implementation adds auth headers to the mutable headers map.
Scenario 1: Enterprise SSO / External Token Service
public class SsoAuthProvider implements AuthProvider {
private final SsoClient ssoClient;
public SsoAuthProvider(SsoClient ssoClient) {
this.ssoClient = ssoClient;
}
@Override
public void applyAuth(String agentName, AgentCard agentCard, Map<String, String> headers) {
String token = ssoClient.getToken(agentName);
headers.put("Authorization", "Bearer " + token);
}
}
// Register
WorkflowEngineClientConfig config = WorkflowEngineClientConfig.builder()
.authProvider(new SsoAuthProvider(mySsoClient))
.sslVerify(true)
.build();Scenario 2: AgentCard has no securitySchemes, but server requires auth
WorkflowEngineClientConfig config = WorkflowEngineClientConfig.builder()
.authProvider((agentName, agentCard, headers) -> {
headers.put("X-API-Key", "static-api-key-value");
})
.build();Scenario 3: Custom header name (non-standard Authorization)
WorkflowEngineClientConfig config = WorkflowEngineClientConfig.builder()
.authProvider((agentName, agentCard, headers) -> {
String token = refreshTokenIfNeeded(agentName);
headers.put("X-Auth-Token", token);
headers.put("X-Tenant-Id", "tenant-001");
})
.build();Notes:
applyAuthis called on every message send; implement token caching/refresh logic insidesecuritySchemeslists authentication methods the agent supports;securityRequirementsmarks the methods required by this integration. EmptysecurityRequirementsdisables built-in credential authentication, butAuthProvideris still calledAuthProvidercan be the sole authentication source even whensecurityRequirementsis non-empty- If both credentials and
AuthProviderare configured, their headers are generated independently and merged; different values for the same header fail fast - On auth failure(e.g. token retrieval throws), the exception propagates to
send()and the request is blocked
AgentCards declare extensions via capabilities.extensions:
{
"name": "Dispatched Agent",
"capabilities": {
"streaming": true,
"extensions": [
{
"uri": "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Task-T/v1",
"description": "Structured task prompt",
"required": false
},
{
"uri": "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Negotiation-T/v1",
"description": "Negotiation text exchange",
"required": false
},
{
"uri": "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Authorization-T/v1",
"description": "Authorization operation",
"required": false
},
{
"uri": "https://projects.tmforum.org/a2aproject/telecommunication/extensions/Notification-T/v1",
"description": "Notification subscription",
"required": false
}
]
},
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer"
}
},
"securityRequirements": [
{
"schemes": {
"bearerAuth": []
}
}
],
"supportedInterfaces": [
{
"protocolBinding": "HTTP+JSON",
"protocolVersion": "1.0",
"url": "https://127.0.0.1:26335/a2a/json"
}
]
}Extension URIs must match the A2A-T definitions exactly.
Both securitySchemes and securityRequirements are optional. The former lists authentication methods the agent
supports; the latter marks methods required by this integration. securityRequirements: [] disables built-in credential
authentication.
Declaring Negotiation-T in the AgentCard means the target supports it; it does not enable negotiation for every task. The host business selects it on the initial final content:
MessageContent outgoing = A2atMessages.from(generatedTask, parts)
.withExtension(A2ATExtension.NEGOTIATION_T.uri());The engine writes this extension set to both the A2A Message and the A2A-Extensions request header. Initial metadata
still contains only the Task-T body; do not manufacture Negotiation-T metadata before receiving a Propose. Configuring
a negotiation callback alone does not activate the extension, and tasks that disallow negotiation must not call
withExtension.
Only a remote INPUT_REQUIRED or a taskless bare message carrying a valid Negotiation-T Propose enters
onNegotiation (the latter is the A2A-T pre-task mode: correlated by contextId and negotiation id, with taskId-less
follow-up sends). Terminal responses never restart negotiation; ordinary INPUT_REQUIRED fails explicitly. Invalid
negotiation metadata on a bare message also fails explicitly instead of being treated as a normal response. The host validates/interprets the proposal and
generates the final Accept/Reject/Abort with its own A2A-T client. Use A2atMessages.contextOf(request.received()) to
obtain the received context; reply with the same id, round and maxRounds. The last allowed round can still be answered.
Do not call nextRound for an ending reply or return a new Propose.
Return new NegotiationReply.Send(content) to send that exact content. Return new NegotiationReply.Stop(code, reason)
to stop without generating Abort content; the engine then cancels the known non-final A2A task as lifecycle cleanup.
Repeated task/session/round events do not repeat the callback or submission.
Unchanged waiting state is observed with getTask.
maxNegotiationExchanges (default 3) bounds local interactions, independently of the SDK context's maxRounds. Timeout,
exhausted budget or a missing handler fails locally; no implicit Accept or synthesized Abort. Accept/Reject ACKs in
SUBMITTED/WORKING remain pending and are observed without resending the command. A business-sent Abort is never
task success, even if the dispatched agent acknowledges it with COMPLETED.
For a task outside a workflow DAG, call the task client directly. It uses a fresh context but keeps the same remote task identity through waiting and negotiation. The optional per-call strategy does not change the client-wide ControlPoint. Plain A2A content is allowed; activated Task-T content must include Task-T metadata and the target AgentCard must declare the extension.
CompletableFuture<SendMessageResult> sendTask(String agentName, MessageContent content);
CompletableFuture<SendMessageResult> sendTask(String agentName, MessageContent content,
NegotiationStrategy negotiationStrategy);If a known remote task is still non-final when local interaction cannot continue, the client makes a best-effort cancellation before completing exceptionally. Standard A2A request errors remain exceptional results and are not converted into business task failures.
CompletableFuture<SendMessageResult> sendAuthorization(String agentName, MessageContent content);
NotificationSubscription openNotification(String agentName, MessageContent content,
BiConsumer<NotificationSubscription, ReceivedMessage> listener);The host agent generates final Authorization-T/Notification-T content. Authorization and notification each use a transport/runtime/context independent of the task client and of one another. The listener receives the handle and complete ReceivedMessage. Task/result terminal statuses do not close the subscription; the host explicitly closes the handle when cancelling the subscription or shutting down. Stream exit/error and the configured idle timeout also end the subscription. acknowledgement() and completion() separately represent ACK and actual stream exit; neither is a workflow prerequisite.
heartbeat().lastEventAt() is the last transport activity. A standard SSE comment heartbeat such as : heartbeat
refreshes it without increasing eventCount, which counts decoded A2A business events only. Use
lastBusinessEventAt() for the last business-message time. Cancelling the Future returned by subscribeToTask
only stops the local wait; close NotificationSubscription for an independent subscription, or close its owning
client/runtime for a client-managed task subscription.
WorkflowEngineClientConfig.notificationAckTimeoutSeconds controls the initial subscription ACK wait and defaults to
300 seconds. The Spring sample exposes it as a2a.notification-ack-timeout-seconds and
A2A_NOTIFICATION_ACK_TIMEOUT_SECONDS. It does not control SSE idle time after acknowledgement.
// Controlled local diagnostics only: skip chain validation, but still verify the host name
.sslVerify(false)
// Production: enable verification + custom CA certs
.sslVerify(true)
.caCertsPath("/path/to/ca-certs.pem")
// Optional mTLS and CRL. Private keys support PKCS#8 PEM/DER; encrypted keys require a password
.clientCertPath("/path/to/client-cert.pem")
.clientKeyPath("/path/to/client-key.pem")
.clientKeyPassword("change-me")
.crlPath("/path/to/revocations.crl")For HTTP/JSON-RPC, TLS policy is scoped to the current client and never changes JVM-wide hostname verification;
disabling chain verification still loads the mTLS client identity. Keep sslVerify(true) in production and trust
self-signed certificates through caCertsPath. The default gRPC runtime uses plaintext when sslVerify(false) is set,
so mTLS and crlPath cannot be combined with that mode and fail fast instead of being ignored.
The PROTOCOL logger at DEBUG records observations at the actual transport boundary. HTTP/JSON-RPC logs preserve the
serialized body and application headers after A2A SDK processing, including A2A-Version when actually present. gRPC
records actual metadata and a protobuf JSON view; that view is not an HTTP JSON body. The engine never adds a missing
header just to make logs look uniform. Automatic network headers, HTTP/2 frames, TLS records and server-side bytes are
not captured.
MODEL_PREVIEW is optional, disabled by default, and never wire proof.
logger.protocol.name=PROTOCOL
logger.protocol.level=DEBUG
logger.protocol.additivity=true
WORKFLOW_ENGINE_PROTOCOL_INCLUDE_BODY=true
WORKFLOW_ENGINE_PROTOCOL_MAX_BODY_CHARS=100000Body observation defaults to enabled when DEBUG is enabled; disable it explicitly for sensitive deployments. JVM
properties take precedence over same-named environment variables. Header credentials/cookies/tokens and recognized
secret body fields are always redacted; this cannot be disabled. This is field-based redaction, not a classifier for all
personal/business-sensitive content. Bodies are bounded (raw collectors use the configured numeric limit as bytes;
emitted text uses characters). Oversized SSE frames are dropped whole until the next delimiter and marked
dropped-capacity; disabled, truncated and interrupted observations are labeled. UTF-8 is decoded after assembling
chunks. Observers cannot fail delivery. File references are recorded as references and are never downloaded for logging.
requestId correlates each call; workflow calls additionally carry executionId/logicalTaskId/attempt,
agent/contextId/channel and remoteTaskId when known. These are local log fields, not wire metadata.
Use a controlled test in which one dispatched agent returns INPUT_REQUIRED with a valid Negotiation-T Propose. The
expected sequence is request → Propose → onNegotiation → final Send/Stop → terminal task result. The PROTOCOL logger
must show the observed request/response boundaries and correlation fields without exposing credentials. A declared
Negotiation-T capability alone does not force negotiation.
Subscribe to execution events for real-time monitoring:
EventCallback callback = new EventCallback() {
@Override
public void onEvent(String eventType, Map<String, Object> data) {
switch (eventType) {
case EventType.STEP_START -> System.out.println("Step started: " + data.get("step"));
case EventType.TASK_STATUS_CHANGED -> System.out.println(
data.get("agent") + " state: " + data.get("state"));
case EventType.NEGOTIATION_REQUEST -> System.out.println(
"Negotiation from " + data.get("agent"));
case EventType.COMPLETE -> System.out.println("Workflow complete");
}
}
};
ExecutePsop.builder()
.eventCallback(callback)
// ...Common event types: START, STEP_START, TASK_REQUEST,
TASK_RESPONSE, TASK_STATUS_CHANGED, STEP_COMPLETE,
NEGOTIATION_REQUEST, NEGOTIATION_RESOLVED, NEGOTIATION_FAILED,
ROUTE_DECISION, WORKFLOW_COMPLETE, COMPLETE, ERROR, CLOSE.
// Center credentials are independent of A2A AuthProvider and agent credentials.
TlsPolicy orchTls = TlsPolicy.strict(); // Or trustedCa("/external/certs/ca.pem").
HttpAuthProvider orchAuth = new OrchestrationAccountAuth(
"https://orchestration.example.com", System.getenv("ORCH_USERNAME"),
System.getenv("ORCH_PASSWORD"), orchTls);
// Search by intent
List<WorkflowSearchResult> results = LoadPsop.search(
"https://orchestration.example.com", "analyze service request", 5, orchAuth, orchTls);
// Load full workflow by ID
Workflow workflow = LoadPsop.load(
"https://orchestration.example.com", results.get(0).getWorkflowId(), orchAuth, orchTls);Construct final MessageContent(parts, metadata, extensions), with host-owned content generation/validation. No engine handler or SDK instance registration. A2atMessages.from copies A2A-T metadata; other extensions can directly supply metadata and activation URIs. AgentCard declarations never cause implicit generation.
TlsPolicy.strict() verifies certificate trust and hostname. Use TlsPolicy.trustedCa(caPemPath)
for a private/self-signed CA. TlsPolicy.insecure() explicitly skips both checks for the current
management client only (Python verify=False equivalent) and emits an INSECURE_TLS warning.
It does not modify JVM-wide TLS defaults or bypass Bearer authentication.
The HTTPS server must still present a certificate, and client-certificate requirements (mTLS) still apply.
This mode cannot authenticate the server and is vulnerable to interception; do not use it in production.
Keep verification enabled in production, configure matching server SANs and trust its CA in the running JVM.
For mTLS, use TlsPolicy.mutualTls(ca, clientCert, pkcs8Key, password); the insecure overload
TlsPolicy.insecure(clientCert, pkcs8Key, password) retains client identity.
The dispatched-agent caCertsPath setting is not forwarded to LoadPsop.
LoadPsop never changes JVM-wide SSLContext, SocketFactory or HostnameVerifier defaults.
Dispatched-agent HTTP/JSON-RPC retains hostname checks when chain verification is disabled.
Existing LoadPsop methods retain their arguments, but now send accessToken in the Bearer header, not the URL. Existing RegistryClient boolean constructors remain anonymous; their false setting still disables only certificate-chain checks. Prefer the explicit authentication/TLS constructors.
HttpAuthProvider.bearer(tokenSupplier) reads the current token once per request. The application/IAM
client owns OAuth2 acquisition, caching and refresh; the SDK does not retry business writes on 401.
Custom authentication implements headers(URI) and returns business-defined header names/values.
Duplicate case-insensitive or reserved transport headers are rejected. Use HttpAuthProvider.none()
only when the service deliberately allows anonymous access or uses mTLS identity instead.
SpringSpnDemo uses ORCH_USERNAME and ORCH_PASSWORD (the login-page account),
ORCH_VERIFY_SERVER (default true), and optional ORCH_CA_CERTS_PATH, independently
of A2A_SSL_VERIFY and agent/Order credentials. Supply the password through an external
secret, never in committed YAML or command-line arguments. The shared account provider
logs in lazily at POST /api/v1/auth/token, sends {username, password} as JSON, and
caches access_token according to expires_in. Expiry is handled by logging in again;
this first-party endpoint is not an OAuth2 password grant or refresh-token server.
Only PSOP load/search may retry once after 401 with refreshed account credentials.
403, 429, service errors, TLS/network failures and rejected logins are not retried.
Credentials are scoped to the configured center's PSOP API, never query strings/cookies
or another origin. Login and PSOP calls must use the same center TLS policy; skipping
server verification does not skip account authentication. HTTP is supported for isolated
development, but exposes the login password; prefer verified HTTPS in deployments.
Share one OrchestrationAccountAuth instance across executions to reuse its thread-safe
session cache. No center credentials affect Task-T or Notification-T streams.
Alternatively set ORCH_API_TOKEN alone for an externally managed Bearer; do not combine
it with account credentials. Static/supplier Bearer providers do not opt into retries.
For dynamic/custom credentials, provide a bean named orchestrationHttpAuthProvider.
For mTLS or an application-owned TLS policy, provide a TlsPolicy bean named orchestrationTlsPolicy.
HostQuickStart requires REGISTRY_API_TOKEN for registry discovery.
Center failures (including 401/403, TLS/network errors and empty search results) never select a
different workflow. --a2a.local-workflow-enabled=true explicitly runs the offline sample workflow
without contacting the center; it is disabled by default and is not a production fallback.
The engine consumes AgentCards and never starts dispatched-agent servers. The host agent owns AgentCard discovery, endpoint reachability checks, and any development fixtures. When using externally managed dispatched agents, do not start local fixtures on the same endpoints. Keep production AgentCards and credentials outside tracked sample resources.
The demo queries each dispatched agent for SUBMITTED, WORKING, INPUT_REQUIRED and AUTH_REQUIRED tasks before
opening extension channels or starting a workflow, then cancels every visible result through the standard A2A task API.
It follows nextPageToken, de-duplicates tasks that change state during the scan, and accepts the race where a task
becomes terminal before cancellation. Query and cancellation use a dedicated short-lived authenticated transport; they
never reuse workflow, authorization or notification lifecycles.
Cleanup is enabled and fail-fast by default so stale tasks cannot silently lead to a capacity error. Configure it with
A2A_TASK_CLEANUP_ENABLED, A2A_TASK_CLEANUP_FAIL_FAST, A2A_TASK_CLEANUP_PAGE_SIZE (1–100), and
A2A_TASK_CLEANUP_MAX_TASKS. The list operation is authorization-scoped. If several installations share one identity,
the demo may cancel active tasks created by another installation; use an isolated identity or disable cleanup after
providing an equivalent ownership-aware policy.
A2A HTTP+JSON defines action endpoints such as message:send, message:stream, tasks/{id}:cancel and
tasks/{id}:subscribe. Keep these canonical routes enabled. Prefer configuring the API gateway to publish slash-style
paths and rewrite them to the canonical backend paths, for example:
/gateway/service/a2a/json/message/send -> /a2a/json/message:send
/gateway/service/a2a/json/message/stream -> /a2a/json/message:stream
If the gateway cannot rewrite the backend path, enable the starter's aliases:
a2at:
server:
enabled: true
path-prefix: /a2a/json
slash-action-aliases-enabled: trueThe gateway can then forward /message/send and /message/stream unchanged. The same option also exposes
/tasks/{id}/cancel and /tasks/{id}/subscribe. Do not put the gateway-owned public prefix into path-prefix when the
gateway removes it before forwarding; add it only when the application actually receives that prefix. The A2A-T SDK
does not send HTTP requests, and the A2A Java REST transport appends canonical operation suffixes to the AgentCard base
URL. A caller that must emit slash-style paths therefore needs gateway rewriting or a transport adapter; changing the
AgentCard base URL alone is insufficient.
Keep failures before and after task creation separate. A request rejected before a task is created uses a
non-2xx HTTP response with the standard A2A google.rpc.Status JSON envelope:
{"error":{"code":400,"status":"INVALID_ARGUMENT","message":"A required parameter is missing","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS","domain":"a2a-protocol.org","metadata":{"field":"port"}}]}}RemoteA2AErrorException preserves the observed HTTP status, envelope code/status/message, typed details,
ErrorInfo reason/domain and safe response headers. findIn(Throwable) also projects typed A2A Java SDK
errors. The direct transport checks ordinary responses and defensively detects a top-level error envelope if
an SDK exposes a pre-stream rejection as SSE data. Nested error objects inside a task, message or artifact
remain business content.
If an SSE call closes before delivering any A2A event, the transport fails immediately instead of waiting for the workflow send timeout. The underlying SDK does not expose the HTTP status of an empty streaming response, so this case is reported as a transport/protocol failure rather than a fabricated HTTP error code.
Workflow history and TASK_RESPONSE expose a stable code such as a2a.invalid_params; unknown HTTP errors
fall back to a2a.http.<status>. errorDetails retains protocol facts and retryAfter when observed. Such an
error creates no successful output, does not invoke onNegotiation and is not retried automatically.
After a task has been created, a business execution failure is not an HTTP protocol error. The agent returns
HTTP 200 with a Task or status update in TASK_STATE_FAILED; the TaskStatus message, task metadata and
artifacts carry the available failure evidence. The engine marks the workflow task failed, keeps that evidence
in receivedMessages, and does not interpret an extension-specific result schema. Independent authorization
and subscription failures remain independent of workflow outcome.
For an ALL_SUCCESS step, one failed task does not retroactively cancel a peer call that was already dispatched: the
peer result or timeout is collected before the workflow returns success=false. Downstream steps are not invoked;
successful peer results remain in history rather than being turned into a fabricated aggregate. There is no automatic
retry, queuing, or partial-success merge. ANY_SUCCESS nodes retain their declared first-success semantics.
Observe failures through TASK_RESPONSE or inspect result.history in onFinish(result, events);
onTask prepares outbound content and is not an error-retry callback.
Executor node events include executionId; TASK_RESPONSE and history also include the logical taskId.
The host agent decides how to expose a failed execution to its caller. It must not convert a failed execution into a
success artifact. A persistent host owns notification subscriptions independently of workflow results.
Logging responsibilities: PROTOCOL captures observed traffic, correlated by requestId rather than adjacent log entries; A2A_ERROR reports a protocol rejection; TASK_FAILED identifies the execution, step, logical task, agent, code and reason; WORKFLOW_STOPPED lists unexecuted steps. Sample TASK_RESPONSE logs bridge contextId and executionId. Recognized A2A errors use WARN summaries, while unexpected exceptions retain ERROR stack traces. Known credentials and Bearer/Basic values are redacted. Logging configuration, pretty display and observer callbacks do not determine task outcomes.
| Interface/Class | Purpose |
|---|---|
ExecutePsop.Builder |
Workflow execution entry point |
ControlPoint / DefaultControlPoint |
Business decisions (onTask, onSelfTask, onRoute, onNegotiation, etc.) |
WorkflowEngineClient / DefaultWorkflowEngineClient |
Workflow and standalone task interaction |
ExtensionSender / DefaultExtensionSender |
Independent Authorization-T operations and Notification-T subscriptions |
A2ATransport |
Shared wire layer (A2A Java client runtime, auth, SSE consumer) |
WorkflowEngineClientConfig |
Configuration (TLS, auth, deadlines, executor limits, negotiation exchange budget) |
AuthProvider |
Custom authentication |
EventCallback / EventType |
Event callback |
LoadPsop / RegistryClient |
Workflow loading / AgentCard fetching |
Workflow / WorkflowStep / Task / JumpCondition |
Workflow definition |
ExecutionResult |
Execution result |
SendMessageResult / TaskResult |
Message/task response |