All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Protocol diagnostics now redact credential-shaped values in request and response bodies as well as headers. Workflow failure and callback logs also redact credential-shaped exception text; registry search no longer logs raw intent.
- Standard A2A HTTP and SSE error envelopes retain their status, reason,
retry hint and safe details. An empty response stream fails immediately with
a2a.empty_streaminstead of returning an unattributed unsuccessful result. - A malformed subtask
input, an activated extension missing from AgentCard, or an unavailable explicitly preferred protocol now fails before dispatch. Explicit TLS/mTLS/CRL options are rejected for unsupported non-HTTP transports instead of being silently ignored. - Hosts may inject
credential_encryption_keyintoA2ATransportrather than depending on a process-wideA2AT_CRED_KEY; the environment remains the compatibility fallback. MessageContent.with_extension(uri)offers an immutable, Java-aligned way to activate one extension for a single request.- Registry and PSOP HTTP helpers accept separate connect/read timeout settings instead of hard-coding both to 30 seconds.
- A notification stream that died was never released, and a healthy one could be
reported as dead. Liveness was judged from decoded business events only, but an SSE
comment heartbeat never reaches the protocol layer, so a subscription receiving only
heartbeats looked identical to a dead one --
is_healthy()returned false and a caller acting on it tore down a live stream, while a genuinely dead stream leaked its connection and task forever. Transport activity is now observed per body chunk and recorded separately from business events. - Sensitive header names were only redacted in the exact spelling the pattern
happened to list:
api_key,x_api_key,access_session,accessSession,pwdandpasswdall leaked into the opt-in protocol log, whileapi-keyandApiKeywere caught. Names are now matched after stripping-and_, and a header registered throughregister_sensitive_headermatches in any spelling. - A merge step could start before its own predecessor had run. Step readiness was
judged from direct predecessors, and a predecessor that had not been activated yet
was filtered out entirely -- so a merge whose branches became ready in different
batches ran early, with only part of its inputs, and still reported success. Readiness
is now judged from all active ancestors, matching the Java engine
(
WorkflowExecutor#getAllPredecessors): an ancestor that is activated but has not produced output is the signal that a branch is still running. - A Negotiation-T Propose carried on a taskless bare message (A2A-T pre-task
negotiation, where the remote proposes before creating any task) was returned to
the host as if it were the final business answer:
SendMessageResult.is_successreported success andoutputs()exposed the negotiation prompt as the result. Such a message now opens a negotiation, correlated by contextId, and the follow-up send carries no taskId. A bare message whose Negotiation-T metadata is malformed now fails loudly instead of silently passing as a result. - When an intermediary (API gateway, load balancer) closed an idle SSE stream
while the remote task was still running, the engine cancelled that task. A
transport fault during stream consumption now falls back to polling
get_taskeverytask_poll_interval_secondsuntil the task leaves a non-terminal state or the deadline expires, so a healthy task is awaited instead of destroyed. Cancellation for a genuinely local interaction failure is unchanged. - A
Notification-Tsubscription whose remote never acknowledged stayed pending forever, pinning the caller's coroutine and the connection. The first acknowledgement is now bounded bynotification_ack_timeout_seconds(default 300s); SSE idleness after the acknowledgement is deliberately not bounded. sse_normalizationpatchedjson_format.Parsewith a duplicate keyword argument, so everyStreamResponsetext parse raisedTypeError. The patch now covers onlyParseDict, the entry point that actually performs the normalization, which leavesParseuntouched for every other caller.- A
taskstream event carries a full task snapshot, yet its artifact text was appended on every update, duplicating the response text. Text is now assembled once from the merged artifact map, which also gives a non-empty result to streams that deliver content only throughartifact_updateevents. subscribe_to_taskreplaced its accumulated message list on everytaskevent, silently dropping standalone messages seen earlier. It now merges, and additionally reports status and artifact updates and terminal failure codes, matchingconsume_stream.create_ssl_contextsilently discarded the CA, mTLS and CRL arguments whenverify_server=False. A client identity is now still presented, while a trust store or CRL without verification is rejected rather than ignored.- Protocol logging redacted headers by name pattern alone, so a credential in a
custom
auth_headerwas logged in clear. Authentication interceptors now register the exact header names they inject. failure_mappinglooked the A2A error mapping up by exact type, so a raised subclass fell through to the generic failure code. It now walks the MRO.env_file_loaderwrote any key from a.envfile into the process environment; keys must now be valid environment-variable names.
- Remote A2A
error_detailsnow uses Java-compatiblehttpStatus,code,status,reason,domain,detailsandretryAfterfields. Callers using the formerhttp_statusor top-level SDK metadata fields must migrate. NotificationHeartbeat.last_event_atnow reports the last transport activity, including an SSE comment heartbeat, andevent_countcounts only decoded A2A business events. The last business event is available through the newlast_business_event_at. This matches the Java engine'sNotificationSubscription.NotificationSubscription.is_healthy()judges liveness from transport activity, so a subscription that is only receiving heartbeats is no longer reported as dead. A freshly opened subscription counts as healthy, since opening the stream is activity.- A notification stream is now released after
send_timeout_secondswithout any transport activity (configurable throughExtensionSender(notification_idle_timeout_seconds=...)), mirroring the Java engine's idle budget.await subscription.completionraisesTimeoutErrorwhen that happens. A stream that keeps receiving heartbeats is never released. Hosts that relied on a subscription staying open forever must be updated. A2ATransportacceptstask_poll_interval_seconds(default 20s, floor 0.1s) andnotification_ack_timeout_seconds(default 300s), andexecute_psopforwards the former. These mirror the Java engine'staskPollIntervalMillisandnotificationAckTimeoutSeconds; the poll interval is used only after a stream interruption, never while the SSE stream is alive.consume_streamincludestask_idin theAGENT_STATUS_UPDATEintermediate event it forwards, so a caller can learn the remote task identity before a stream ends and recover from a dropped connection.A2ATransportcaches one A2A client per agent through the newclient_for(agent_name)instead of rebuilding it for every send, task poll and cancellation.update_agent_cardsdrops the cache.A2ATransport.get_extension_urisis the public form of the former private_get_extensions, whichExtensionSenderreached into across modules.WorkflowEngineClient.stream_messageacceptstimeout_secondsand bounds the whole stream rather than relying on the idle read timeout alone.credential_cryptoaccepts an optionalaadthat binds a ciphertext to its context. It defaults toNoneto stay wire-compatible with the Java SDK.- Registry helpers accept a custom CA, mTLS identity and CRL, and
RegistryClientaccepts an access token, so the registry path no longer has less TLS capability than the transport path. workflow_engine.__version__is the single source of the distribution version (pyproject.tomlreads it through[tool.setuptools.dynamic]), and thea2a-t-sdklower bound now matches whatverify_sdk.pyasserts.
- Dead
A2ATransport._text_from_metadata, and a staleextension_handlers.cpython-312.pycwhose source was deleted in 0.0.9.
- Publish PyPI releases through the tag-triggered GitHub Actions workflow.
- Upgrade GitHub Actions dependencies for checkout, Python setup, and dependency review.
- Align the business callback contract with the Java engine: callbacks prepare final content and never receive the transport client.
- Replace rendered Markdown predecessor context with typed
WorkflowInputand ordered task outputs. - Evaluate every conditional outgoing edge independently; unconditional edges always pass and a node may activate zero through N successors.
- Integrate
a2a-t-sdk>=1.0.9,<2core metadata types while keeping content generation and semantic validation in host code; remove implicit Task-T handlers and legacy negotiation schemas. - Install development dependencies directly through the editable dev extras and remove the temporary bootstrap file.
- Structured response evidence, protocol-to-business result mapping, safe
BusinessFailure, and standard A2A error mapping. - Remote
get_task,list_tasks,cancel_task, andsubscribe_to_taskoperations. - Explicit
NotificationSubscriptionacknowledgement, completion, close, heartbeat, and health lifecycle. - mTLS/CRL transport configuration and fail-closed TLS file validation.
- Reusable per-agent credential profiles with nested overrides and fail-fast configuration checks.
- Preserve message, task, and artifact metadata at their original levels and process status/artifact stream updates.
- Detect duplicate graph edges, invalid history sources, duplicate AgentCards, and conflicting authentication headers.
- Use the current A2A interceptor contract so activated extension and complete authentication requirement headers reach the wire.
- Close only resources owned by the high-level runner.
- Read canonical
negotiationContextmetadata and enforce current Propose/Accept/Reject/Abort context and round rules. - Serialize protobuf message parts as their JSON fields in workflow events instead of leaking descriptor internals.
- NegotiationTHandler metadata overwrite bug: Use local metadata dict to prevent cross-handler contamination
- Negotiation concern extraction: Add fallback in
engine_clientfor concern extraction when metadata is missing - needResponse=false handling: Fix negotiation flow to properly handle terminal negotiation states
- Task-T prompt caching:
TaskTHandlernow caches generated prompts bymessage_text, so identical task descriptions sent to multiple agents only call the LLM once (subsequent agents get cache hit, saving ~20s each)
- Per-handler timing logs:
_run_before_send_handlersnow logs each handler's execution time individually (e.g.TaskTHandler.before_send for AgentX: 0.01s)
Initial release of workflow-exec-engine (renamed from internal a2at-engine).
A2ATransport: shared wire layer with httpx client, auth manager, agent-card map, and SSE stream consumerWorkflowEngineClient: workflow send facade with Task-T prompt generation, Negotiation-T auto-loop, event callbackExtensionSender: one-shot pre-positioning facade for Authorization-T and Notification-TControlPoint: flow decision interface (on_task/on_self_task/on_route/on_negotiation)NegotiationStrategy: pluggable clarification strategySELF_LOOPstep type for local task handling without A2A-T messageANY_SUCCESSstep policy with early cancellation of remaining subtasks- Parallel DAG step dispatch and context assembly (
ContextBuilder) - Agent authentication from AgentCard
securitySchemes(Bearer, custom headers) - SSE response normalization for non-standard server responses
RegistryClientfor fetching AgentCards and PSOP workflows