diff --git a/docs/design/a2a-implementation-plan.md b/docs/design/a2a-implementation-plan.md new file mode 100644 index 00000000..6239f0e2 --- /dev/null +++ b/docs/design/a2a-implementation-plan.md @@ -0,0 +1,303 @@ +# A2A staged implementation plan + +> Proposed delivery for [FRD 0009](../frds/0009-a2a-server.md). +> [Architecture and invariants](a2a-server.md) govern each slice. +> This first review PR is documentation-only; no implementation or dependency +> changes are included. Human architecture sign-off precedes product work. + +## Review strategy + +One observable outcome per PR, with code, tests, and immediately affected docs +together. A few hundred lines can be a useful review budget, not a hard quota: +split by coherent behavior, never by leaving failing tests, unused public APIs, +dead flags, or broken intermediate states. Each product PR follows the existing +ruff/mypy/pytest gate; host/package isolation gates are added where relevant. + +**Priority: D0 -> P3 -> P4 -> P5a -> P5b -> P1 -> P2 -> P6a onward.** +Keep the existing PR IDs as stable references; their numbers no longer imply +execution order. First build the working non-SSE A2A implementation and sample +in P3, use them to validate/discuss the design, then prioritize basic SSE with +only its necessary P4/P5a preparation. Durable import cleanup and packaging +isolation must not block these learning milestones. Until P2, the existing +mandatory Durable package and eager imports remain; simple/basic use no Durable +execution or bindings when workflows are disabled, but are not yet Durable-free +installations. P1/P2 establish that separate acceptance criterion afterward. +Basic SSE requires startup rejection of deployed/multi-worker use; if that cannot +be enforced reliably, it remains an internal test profile. All durable mode +configuration and cards remain internal/experimental through P9b; P10 is the +public release gate. Private components must have real contract-test/harness +consumers. Bounds and security ship with each first affected surface, not at P10. + +| PR | Outcome | Depends on | Requirements | +| --- | --- | --- | --- | +| D0 | Approve FRD, architecture, library tuple, and staged contract | None | R1-R12 | +| P3 | Deliver working non-SSE MAF A2A server/sample for design validation | D0 + library gate within P3 | R1-R3, R5, R11 | +| P4 | Extract private typed runner events with unchanged chat output | P3 | R5 | +| P5a | Deliver bounded local task lifecycle with internal tests | P4 | R4, R11 | +| P5b | Deliver local SSE adapter and deployment guard | P5a | R4-R5, R11-R12 | +| P1 | Remove eager Durable import coupling, preserving chat/A2A behavior | P5b | R5-R7 | +| P2 | Make Durable installation optional and migrate workflow/A2A installs | P1 | R6-R7 | +| P6a | Implement atomic shared projection/journal store and contract tests | P2 + storage gate | R8-R11 | +| P6b | Implement scoped admission, deduplication, and recoverable dispatch | P6a | R8, R11 | +| P6c | Wire managed execution and scoped Get/List polling | P6b | R7-R8, R11 | +| P7 | Harden execution ownership, context ordering, and retry recovery | P6c | R8, R10-R11 | +| P8 | Deliver any-worker ordered subscriptions and streaming | P7 | R4, R9 | +| P9a | Deliver distributed cancellation | P8 | R9-R11 | +| P9b | Deliver interrupted continuation | P9a | R10-R11 | +| P10 | Qualify/tune existing bounds and release distributed profile | P9b | R11-R12 | + +This is a sequencing proposal, not simultaneous branches. Branch each slice +in its own worktree; normally wait for its dependency to merge. Stack only when +explicitly approved. Reverting a later capability must leave earlier modes and +existing workflow/chat behavior usable. + +## D0 - Design and compatibility approval + +Review these three documents; record human sign-off in the FRD Decisions log. +Approve the recommended Alpha hosting-a2a/hosting `1.0.0a260730` + published SDK +`1.1.2` candidate against the existing MAF pins, including any narrow HTTP extra. +Resolve native-1.0 versus legacy model namespaces and identify the public hooks +for converters, app-owned `RequestHandler`, async cards, and the Functions HTTP +response bridge. Approve the approach for preserving runtime policy and later +substituting distributed handling without private patches, plus the local SSE +contract and authentication scope. P3's working sample provides the executable +integration evidence and the next design-discussion checkpoint; do not make +Durable dependency cleanup a prerequisite for that evidence. + +**Acceptance:** reviewers can name the public hosting entry points, supported +wire methods, model namespace, optional closure, and upgrade/compatibility choice. +No declaration that A2A is available; no code or dependency changes in this slice. + +## P3 - Simple non-SSE server + +**Scope:** `[a2a]` dependency tuple, typed config/merge/validation, +`registration/a2a.py`, MAF adapter, app registration, card/JSON-RPC routes, auth, +and one runnable minimal sample with reproducible startup/client request steps. +Include generated reference and authoring/onboarding docs. +Reuse existing non-streaming runner policy rather than build a parallel agent path. + +**Acceptance:** the sample serves an authorized direct Message through the real +MAF hosting/SDK and Functions response bridge, using a fresh `[a2a]` installation +that still includes the baseline mandatory Durable dependency. Without workflows, +it neither executes Durable work nor registers Durable bindings. Durable-free +installation/import validation belongs to P1/P2, not this PR. Verify +version negotiation, explicitly configured card URL/SDK `card_path` (not automatic +domain-root discovery), JSON-RPC correlation and SDK error shapes, +supported input parts, unknown IDs, oversized requests, unsupported task methods, +and direct Message responses with either value of `returnImmediately` (no effect). +A2A-only agents validate; disabled A2A and +`builtin_endpoints: true` preserve current routes. Combined workflows+A2A uses one +`DFApp` with unchanged workflow bindings. + +**Design checkpoint:** demonstrate the working sample, discuss the authoring/API, +card/client interoperability and runtime adapter, and record resulting decisions +in the FRD before P4/P5. Validate assumptions with this implementation rather +than front-loading dependency refactors. + +**Non-goals / exposure:** streaming false; no Task persistence, subscribe/cancel, +outbound A2A client tools, REST, or durable execution. Pinning changes to the +existing MAF trio require their own compatibility explanation and regression gate. + +## P4 - Chat-preserving typed execution events + +**Scope:** private runner event types and existing chat serialization consumer; +minimal extraction around current streaming logic. No new public Python API. + +**Acceptance:** characterization tests preserve all current chat event kinds, +text/tool ordering, model/harness/tool policy, history/session handling, deadlines, +usage attribution, and failure behavior. Exercise cancellation while waiting, +timeout before the next item, GeneratorExit while suspended at yield, and nested +MAF cleanup exactly as current runner tests require. Preserve owned Durable +client lifetime in workflow-enabled streams. + +**Non-goals / exposure:** no A2A SSE endpoint/capability yet, no change to debug +UI, no conversion of chat payloads into A2A messages by reparsing SSE. + +## P5a - Local task lifecycle + +**Scope:** private local task owner/store, scoped get/cancel/subscription behavior, +and real lifecycle contract tests. Bound active tasks, reader memory, output, +retention, and event batching from the start. + +**Acceptance:** task execution outlives an individual reader; two local readers +receive the same ordered events independently. Test authorization, terminal +transitions, cancel races, expiry, and cleanup without a Durable execution +backend. The mandatory package may still be installed/imported until P1/P2. + +**Non-goals / exposure:** no public SSE or configuration yet, no cross-worker or +restart recovery. The private backend has an executable internal test consumer. + +## P5b - Local SSE adapter + +**Scope:** MAF typed-event mapping, SDK/Functions SSE bridge, local-mode routes, +and required startup guard rejecting deployed or multi-worker configurations. +Extend the P3 sample with a streaming client to demonstrate incremental output +and support the next design discussion. + +**Acceptance:** SDK parsing proves Task-first output, status/artifact ordering, +stable artifact IDs and append/last-chunk semantics; Message-only output is one +Message. Test reader disconnect independence, get/cancel/subscribe integration, +EOF/error cleanup, and guard rejection. If local-only operation cannot be reliably +enforced, keep this mode internal/test-only rather than expose configuration or +`streaming: true`. + +**Non-goals / exposure:** at most guarded local-development streaming; never a +production/distributed capability or a crash-recovery promise. + +## P1 - Import-boundary refactor + +**Scope:** after the P3/P5 working samples, update `__init__.py`, `app.py`, +`registration/endpoints.py`, `workflows/__init__.py`, `integration.py`, +`context.py`, `tools.py`, and directly required import edges, including the new +A2A path. Preserve pure schema/metadata access and public workflow exports. +Keep the mandatory dependency during this refactor for separate packaging review. + +**Acceptance:** import-blocking subprocess tests prove package/runner imports, +no-workflow startup, and simple/basic A2A never load Durable. Existing workflow +registration, real handler annotations, helper compatibility, and stream-owned +client close semantics remain intact. Preserve the P3/P5 samples and inspect +generated bindings, not just class names. + +**Non-goals / exposure:** no new A2A semantics, extra names, or broad workflow +restructuring. Review focus is the import graph and lifetime. + +## P2 - Optional dependency and workflow migration + +**Scope:** `pyproject.toml`, `uv.lock`, workflow and A2A sample/E2E requirements, +contributor setup, migration docs, and missing-extra checks. Move the Durable pin +to `[workflows]`; provision development workflow coverage explicitly. Retain the +`[a2a]` extra introduced by P3 and update its installation guidance. + +**Acceptance:** build/install actual wheels into base, base+monitor, `[a2a]`, +workflows, and combined workflows+A2A environments. Base/monitor/simple/basic +without workflows exclude Durable from the closure and work without importing +it or registering Durable bindings. Run the existing P3/P5 samples in these clean +environments. Enabled workflows fail fast with +`install azurefunctions-agents-runtime[workflows]` guidance when absent and retain +behavior with the extra. A broken installed SDK raises its original failure, +not a misleading missing-extra error. Python 3.13/3.14 remain supported. + +**Non-goals / exposure:** no new A2A behavior or MAF upgrade unless separately +required and approved. This PR completes R6's installation/import isolation, +which was explicitly deferred from P3/P5. + +## P6a - Atomic shared store + +**Scope:** private shared projection/journal contracts and concrete transactional +store, with real backend contract tests/internal harness. Define partition keys, +ETag/CAS invariants, initial owner-epoch fields, and atomic state/event publication. + +**Acceptance:** failure injection cannot expose a projection without its committed +events or vice versa. Exercise payload/transaction limits, expiry, bounded journal +reads, large-artifact references and authorization. Document cross-partition +index consistency; the index is not the admission/authorization authority. + +**Non-goals / exposure:** no public durable mode, dispatch, or HTTP wiring. + +## P6b - Admission and recoverable dispatch + +**Scope:** private scoped deduplication, admission transaction, outbox, dispatcher, +and reconciliation; internal harness exercises actual store and scheduling seam. + +**Acceptance:** initial context-less requests deduplicate by authenticated scope + +agent + client message ID **before** generating context/task IDs. Retrying without +context returns the original mapping; payload-hash conflicts fail. Existing-context +dedup is separately scoped. Accepted work survives disconnect, scheduling failure, +and lost acknowledgement; reconcile both accepted-but-unscheduled and +scheduled-but-unacknowledged windows. Bound admission, retry/backoff, and retention. + +**Non-goals / exposure:** no public mode, agent execution, or detached success +based on best-effort dual writes. + +## P6c - Managed execution and polling wiring + +**Scope:** `[a2a-durable]`, internal backend configuration, Durable registration, +one execution activity, and scoped GetTask/ListTasks wiring. Coexist with workflows +without duplicating its engine. Keep durable SSE disabled. + +**Acceptance:** default SendMessage waits as specified; explicit early return +acknowledges durable admission only. Auth filtering precedes bounded pagination; +state/results survive replacement workers. Test execution deadlines, output +limits, missing-extra errors, and workflows+A2A-durable package/binding matrix. + +**Non-goals / exposure:** internal/experimental only until P9b and P10 pass; no +public durable config/card, exactly-once claim, or blanket activity retries. + +## P7 - Execution ownership and recovery + +**Scope:** owner epochs/CAS publication, per-context distributed coordination, +retry policy, idempotent activity dispatch, and partial-output recovery policy. +Include failure-injection integration tests against the selected backend. + +**Acceptance:** crash after admission, before/after scheduling, during execution, +and after a side effect has specified outcomes. Old attempts cannot append or +complete after takeover; two tasks in one context cannot corrupt shared history. +Stable logical tool idempotency keys are available where tools support them. +Partial artifacts are resumed or explicitly superseded, never duplicated by +blind retry append. Orchestrator replay never invokes an LLM directly. + +**Non-goals / exposure:** no exactly-once external operations and no subscriber +transport changes. Review failure windows independently from HTTP streaming. + +## P8 - Distributed ordered subscriptions + +**Scope:** independent cursor readers over P6a's already atomic projection/journal; +wire durable execution into SendStreamingMessage and SubscribeToTask. Add snapshot +watermark/concurrency handling, bounded reader memory/cursor reads, slow-reader +gap/expiry policy, and teardown; preserve existing publication/batching bounds. + +**Acceptance:** task execution on worker A and subscription on worker B works. +Force an event between snapshot acquisition and live reading; deliver it exactly +once within that subscription's sequence. Two independent subscribers receive +the same ordered events, terminal output follows all artifact chunks, and a +disconnected reader does not stop execution. Terminal-task subscribe is rejected +according to the selected protocol; reconnect starts from current Task state. + +**Non-goals / exposure:** no full historic replay/Last-Event-ID contract, queue +competing-consumer fanout, host affinity, or infinite HTTP duration promise. +Keep production exposure gated until lifecycle and operational requirements pass. + +## P9a - Distributed cancellation + +**Scope:** authorized cancellation intent, executor signaling/acknowledgement, +and terminal races, with bounded cancellation waits and reader notification. + +**Acceptance:** cancellation from worker B reaches execution on A and produces +honest current/terminal state. Already running activities are not claimed killed +by orchestration termination; stale output is fenced. Race completion/cancel, +repeat cancel, owner changes, and unauthorized cancel. + +**Non-goals / exposure:** no compensation of completed tools; durable mode remains +internal/experimental. + +## P9b - Interrupted continuation + +**Scope:** authorized continuation for supported interrupted states using existing +context coordination, message deduplication, and bounded retention/turn limits. + +**Acceptance:** INPUT_REQUIRED and AUTH_REQUIRED retain resumable state; terminal +tasks reject new messages. Test scope/context mismatch, duplicate continuation, +expiry, and concurrent turns. Do not universally close AUTH_REQUIRED streams in +conflict with protocol behavior. Unsupported interaction types fail explicitly. + +**Non-goals / exposure:** no cross-agent transfer; public durable mode still waits +for P10 qualification. + +## P10 - Aggregate qualification and production capability gate + +**Scope:** qualify and tune existing retention, reader/quotas, artifact security, +batching, and sanitized telemetry; do not introduce safety bounds for the first +time here. Complete deployment guidance, supported profile matrix, migration guide, +and independent coverage review. + +**Acceptance:** automated multi-worker restart/fanout/cancel scenarios, expired +task/cursor responses, bounded memory under slow subscribers, backpressure, +artifact authorization, list pagination, and missing-extra matrix all pass. +Measure host/proxy behavior in an explicitly authorized later test environment; +distinguish idle and total duration limits. Heartbeats remain SSE comments. +Perform a separate coverage review before enabling production distributed +streaming in the card/configuration docs. + +**Non-goals / exposure:** no push notifications, REST/gRPC, full historical replay, +or performance superiority claim without evidence. Finalize implementation status +only after the last accepted slice merges; design approval alone is not shipment. diff --git a/docs/design/a2a-server.md b/docs/design/a2a-server.md new file mode 100644 index 00000000..b21e069f --- /dev/null +++ b/docs/design/a2a-server.md @@ -0,0 +1,379 @@ +# Proposed A2A server architecture + +> Design for [FRD 0009](../frds/0009-a2a-server.md), not current functionality. +> Delivery and acceptance gates: [implementation plan](a2a-implementation-plan.md). +> Baseline: `08b9d762d95f451ce926b89c166a14a2d345d433` (2026-09-08). + +## 1. Existing behavior and dependency evidence + +| Baseline surface | Evidence / consequence | +| --- | --- | +| App composition | [`app.py`](https://github.com/Azure/azure-functions-agents-runtime/blob/08b9d762d95f451ce926b89c166a14a2d345d433/src/azure_functions_agents/app.py#L208-L227) freezes catalogs, creates `DFApp` only for workflow agents, otherwise `FunctionApp`. Importing the module still eagerly imports Durable. | +| Chat SSE | [`endpoints.py`](https://github.com/Azure/azure-functions-agents-runtime/blob/08b9d762d95f451ce926b89c166a14a2d345d433/src/azure_functions_agents/registration/endpoints.py#L379-L459) returns HTTP Streams `StreamingResponse` on POST. No separate subscribe GET or A2A task lifecycle exists. | +| Model stream | [`runner.py`](https://github.com/Azure/azure-functions-agents-runtime/blob/08b9d762d95f451ce926b89c166a14a2d345d433/src/azure_functions_agents/runner.py#L1325-L1400) consumes `agent.run(stream=True)` and formats chat SSE strings directly. | +| Lifetime | The stream creates/closes its own `DurableFunctionsClient` because injected invocation-scoped clients close before the response generator finishes. Preserve this in the dependency refactor. | +| Concurrency/history | Runner `_SESSION_LOCKS` is process-local; `BlobHistoryProvider` stores transcripts, not durable A2A tasks, execution ownership, or event cursors. Atomic blob append does not serialize whole turns across workers. | +| Dependencies | [`pyproject.toml`](https://github.com/Azure/azure-functions-agents-runtime/blob/08b9d762d95f451ce926b89c166a14a2d345d433/pyproject.toml#L12-L39) already requires `azure-functions-durable==2.0.0b2`. Optionalizing is removal of an existing requirement, not avoidance of a new one. | +| Locked MAF graph | [`uv.lock`](https://github.com/Azure/azure-functions-agents-runtime/blob/08b9d762d95f451ce926b89c166a14a2d345d433/uv.lock#L12-L55): core `1.13.0`, openai `1.10.2`, foundry `1.10.3`. Traversing their locked dependency edges yields closures of 11, 24, and 48 packages respectively, none named Durable. The runtime is the only direct parent of `azure-functions-durable`; Durable brings `durabletask[opentelemetry]`. This is locked-graph evidence, not a fresh-wheel installation result. | + +### Eager-import blockers + +`__init__.py -> app.py -> workflows.integration` and +`__init__.py -> workflows.context` both reach Durable. Importing any workflow +submodule first executes `workflows/__init__.py`, which imports `context`, +`engine`, and `integration`. Additional edges: + +| Module | Required separation | +| --- | --- | +| `app.py` | Move `azure.durable_functions` construction behind an enabled-feature gate, after config validation. | +| `registration/endpoints.py` | Remove unconditional `DurableFunctionsClient` import while keeping real binding annotations on enabled workflow handlers. | +| `workflows/__init__.py` | Make lightweight schema/metadata imports safe; preserve existing public exports without eagerly loading the engine. | +| `workflows/integration.py` | Separate pure catalog/policy work from imports of `engine` and `tools`. | +| `workflows/context.py`, `workflows/tools.py` | Context types must not drag the client SDK into ordinary package import; client construction/use remains on the enabled path. | +| `workflows/engine.py`, `workflows/native_retry.py` | Keep Durable and `durabletask.task` dependencies feature-local. | +| Root `__init__.py`, `_function_tool.py`, discovery | Keep public `workflow_tool` metadata and pure workflow exception/context exports usable without registering or importing the Durable engine. | + +Type-checking imports alone are insufficient when Functions or tests use runtime +`get_type_hints()`: enabled binding wrappers must retain resolvable SDK types. +Do not solve this with `Any` annotations everywhere, missing SDK stubs, or broad +`ImportError` suppression. Catch only the absent optional dependency at the +feature boundary; dependency-internal import failures must remain visible. + +## 2. Library and protocol compatibility gate + +The requested integration is **MAF's A2A library** with an app-owned hosting +boundary. Source review distinguishes two server-capable choices: + +| MAF option | Verified behavior | Decision | +| --- | --- | --- | +| `agent-framework-a2a==1.0.0b260821`, `A2AExecutor` | Creates/updates Task, creates a MAF session from task context, directly calls its agent, forbids `session`/`stream` in run kwargs, and its cancel method updates task status rather than implementing this design's distributed cancellation. Requires core >=1.15,<2. | Not a drop-in for direct Message MVP, existing runtime policy, or durable ownership. Do not confuse its name with `A2AAgentExecutor`. | +| `agent-framework-hosting-a2a==1.0.0a260730` | Alpha public `AgentA2AAdapter`, `a2a_to_run`, `a2a_from_run`, and workflow converters. Deliberately supplies no executor, task lifecycle/store, queues, HTTP routes, sessions, auth, or deployment. Requires core >=1.13,<2, SDK >=1,<2 and hosting `1.0.0a260730`. | Recommended: use its actual conversions while the runtime owns policy and the SDK handles protocol dispatch/models. Alpha adoption needs explicit approval. | + +The published candidate tuple is hosting-a2a `1.0.0a260730`, hosting +`1.0.0a260730`, SDK `1.1.2`, and the existing core/openai/foundry pins. Hosting +helpers are metadata-compatible with core `1.13.0`; this is not proof of runtime +compatibility. The MAF source release train `python-1.17.0` is **not** the version +to substitute into all distribution requirements. No need to install both MAF +A2A options or upgrade the base core merely to use the hosting helpers. + +Source and publication evidence, verified 2026-09-08: + +- [MAF hosting-a2a README](https://github.com/microsoft/agent-framework/blob/4507512f95effaae4518d658e86e9afc0ccb4514/python/packages/hosting-a2a/README.md) + defines application ownership, flat Parts, card construction, and conversion modes. +- [Conversion helpers](https://github.com/microsoft/agent-framework/blob/4507512f95effaae4518d658e86e9afc0ccb4514/python/packages/hosting-a2a/agent_framework_hosting_a2a/_conversion.py#L168-L190): + `a2a_to_run` returns `AgentRunArgs` without creating sessions; + [`a2a_from_run`](https://github.com/microsoft/agent-framework/blob/4507512f95effaae4518d658e86e9afc0ccb4514/python/packages/hosting-a2a/agent_framework_hosting_a2a/_conversion.py#L258-L318) + accepts `AgentResponse | Message | AgentResponseUpdate` and returns `list[Part]`. +- [Opinionated executor](https://github.com/microsoft/agent-framework/blob/4507512f95effaae4518d658e86e9afc0ccb4514/python/packages/a2a/agent_framework_a2a/_a2a_executor.py#L94-L176) + is the alternative above, not the selected lifecycle implementation. +- [Hosting-a2a manifest](https://github.com/microsoft/agent-framework/blob/4507512f95effaae4518d658e86e9afc0ccb4514/python/packages/hosting-a2a/pyproject.toml), + [published hosting-a2a metadata](https://pypi.org/pypi/agent-framework-hosting-a2a/1.0.0a260730/json), + [hosting metadata](https://pypi.org/pypi/agent-framework-hosting/1.0.0a260730/json), + and [SDK 1.1.2 metadata](https://pypi.org/pypi/a2a-sdk/1.1.2/json) + distinguish available distributions from source-only research. +- [Official MAF A2A guidance](https://learn.microsoft.com/en-us/agent-framework/integrations/a2a) + describes the opinionated versus application-owned integration choices. + +No Durable dependency appears in the selected helper/SDK manifests. Prove the +resolved wheel closure separately. Use SDK `[http-server]` only if reusing its +HTTP dispatcher needs Starlette/SSE dependencies; `[fastapi]` adds FastAPI too. +Do not default to `[all]`. GitHub SDK tag `v1.1.3` existed during research but its +public PyPI version endpoint returned 404; **1.1.2 is the published candidate**, +not an unverified promise to install 1.1.3. + +The protocol target is [A2A v1.0.1](https://github.com/a2aproject/A2A/tree/3303592588e388e62e0f69f701af531d2f4e3991), +whose wire version is `1.0`. These are three independent values: normative spec +tag, wire version, and Python distribution version. + +Pinned normative references for the contracts below: + +| Contract | Specification v1.0.1 | +| --- | --- | +| Task-first or single-Message streaming | [Streaming responses](https://github.com/a2aproject/A2A/blob/3303592588e388e62e0f69f701af531d2f4e3991/docs/specification.md#L182-L212) | +| Current Task snapshot first | [SubscribeToTask](https://github.com/a2aproject/A2A/blob/3303592588e388e62e0f69f701af531d2f4e3991/docs/specification.md#L287-L311) | +| Execution mode / no-effect `returnImmediately` | [SendMessageConfiguration](https://github.com/a2aproject/A2A/blob/3303592588e388e62e0f69f701af531d2f4e3991/docs/specification.md#L431-L456) | +| Stream lifecycle, ordering, and replay scope | [Multiple streams](https://github.com/a2aproject/A2A/blob/3303592588e388e62e0f69f701af531d2f4e3991/docs/specification.md#L679-L694); [no complete-replay guarantee](https://github.com/a2aproject/A2A/blob/3303592588e388e62e0f69f701af531d2f4e3991/docs/specification.md#L747-L764) | + +| Semantic operation | 1.0 JSON-RPC method | 0.3 compatibility name | +| --- | --- | --- | +| Send / stream | `SendMessage` / `SendStreamingMessage` | `message/send` / `message/stream` | +| Inspect | `GetTask` / `ListTasks` | `tasks/get` / not assumed equivalent | +| Cancel | `CancelTask` | `tasks/cancel` | +| Subscribe | `SubscribeToTask` | `tasks/resubscribe` | + +An absent `A2A-Version` means 0.3; a 1.0-only service rejects it rather than +guessing. AgentCard interfaces declare the actual version. Do not mix 0.3 fields +such as status-event `final` into 1.0. Set push notifications false unless shipped. +Keep REST out of the first chain, including the spec/proto disagreement over +subscribe's verb, until a separate binding-conformance decision. + +**Gate:** approve the Alpha tuple and resolve its optional dependencies; exercise +actual public native-1.0 models, handler interfaces, serialization, and card output. +If the package only supports 0.3, either approve a separately labeled 0.3 first +release or upgrade/extend through supported upstream APIs for 1.0. A hand-written +version field cannot make incompatible models compliant. The Functions HTTP +Streams bridge must preserve SDK status codes, headers, streaming iteration, and +error envelopes without mounting an ASGI application that replaces unrelated +Functions routes. Validate this bridge in P3's first working sample before +exposing the interface, and discuss the evidence before P4/P5. If it fails, stop +for a design decision; do not silently substitute a different library. Durable +import/package isolation is not a prerequisite for this executable validation. + +## 3. Composition and execution seams + +Discovery remains read-only. Translation produces typed A2A config and +capabilities. Registration receives `ResolvedAgent`/`AgentCapabilities` plus the +immutable `AgentCatalog`, and owns Functions decorators, inbound auth, and HTTP +request/response adaptation. It must not parse YAML again. + +The app-owned SDK `RequestHandler` uses MAF conversion helpers and invokes the existing runtime execution policy, including +client selection, tools, skills, harness configuration, delegation restrictions, +and session/history management. Replacing it with a minimally constructed MAF +agent would bypass runtime behavior. Conversely, parsing the existing chat SSE +strings back into A2A events would couple two independent wire protocols. + +For the simple text-only profile, `a2a_to_run` performs protocol-to-MAF conversion; +the adapter then validates the supported text subset and supplies the existing +runner prompt. Reject unsupported structured/file input instead of silently +flattening it. The runner's public `AgentResult` is not a MAF `AgentResponse`: +adapt its final content into a supported MAF assistant Message for `a2a_from_run`, +then group returned Parts into one A2A Message. Do not pass `AgentResult` to the +converter as if the types matched. + +Introduce a **private typed execution-event seam** consumed by existing chat +formatting and the A2A adapter. Events distinguish text output, tool lifecycle, +completion, and execution failure without HTTP encoding. Keep reasoning and raw +tool arguments/results private by default on A2A; they are not automatically +public artifacts. The seam preserves deadlines, usage accounting, nested MAF +stream finalization, session cleanup, and exception propagation. Preserve +allowlisted native MAF updates at this boundary for the existing +`a2a_from_run(AgentResponseUpdate)` converter; do not invent a new token converter +or leak arbitrary update metadata. Group Parts into task artifacts/status events +in the app adapter; the helper deliberately does not own that lifecycle. + +The initial simple adapter can reuse non-streaming execution; the event seam is +introduced only with a real chat consumer and characterization coverage, not as +unused public scaffolding. Card conversion through `AgentA2AAdapter` is async; +validate how it fits synchronous app composition (for example lazy async card +materialization from frozen config), without calling `asyncio.run` in a running +worker loop. Card capabilities are supplied explicitly from shipped behavior, +not inferred from model streaming support. The library gate covers this bridge. +The FRD's per-agent card route requires an explicitly configured card URL or SDK +`card_path`; it is not domain-root well-known auto-discovery. A root singleton or +agent-selection convention is deferred. + +## 4. Capability profiles and protocol mapping + +| Profile | Behavior | Honest boundary | +| --- | --- | --- | +| `simple` | Authorized text -> runtime result -> one direct Message. Streaming false. | No Task backend, async admission, subscribe, or cancel. Reject supplied task continuation; `returnImmediately` has no effect for direct Message. | +| `basic` | Task-first SSE, bounded process-local task projection and ordered local events. | P5b must reject deployed/multi-worker startup; if that cannot be reliably enforced, test-only. No restart/cross-worker or production claim. | +| `durable` | Task admission/execution independent of HTTP; shared projection/journal and per-subscriber cursors. | Internal/experimental through P9b; public config/cards only after P10. No exactly-once model/tool execution or unlimited stream duration. | + +Basic SSE is not permission to equate disconnect with CancelTask. A local +supervisor owns execution independently of each response generator; local +subscriptions, get/cancel support, and bounded retention must work within that +process. P5a implements that private lifecycle with real contract tests; P5b adds +the SSE adapter and enforced deployment guard. A host restart loses state. +Without a reliable local-only startup guard this stays internal/test-only, not a +public configuration choice or `streaming: true` capability. + +For task-producing 1.0 `SendMessage`, omitted/false +`configuration.returnImmediately` waits for a terminal or interrupted result. +Specification section 3.2.2 says that flag has **no effect** for a direct Message +or streaming. Simple mode returns its Message normally even if the flag is true; +it must not promise asynchronous Task scheduling. Durable non-streaming mode may +acknowledge a non-final Task early only when the request permits it and admission +is durably recorded. + +Incremental output uses a Task first, then `TaskStatusUpdateEvent` and +`TaskArtifactUpdateEvent`; stable `artifactId`, `append`, and `lastChunk` describe +artifact assembly. Coalesce text into bounded batches, not one storage write or +Durable history item per token. Finish artifact output before terminal status. +A message-only streaming response is exactly **one Message**, not token Messages. +Use SDK domain errors and JSON-RPC envelopes, not chat `{"type":"error"}` events. + +Terminal `completed`, `failed`, `canceled`, and `rejected` tasks cannot accept more +messages or new subscriptions. `INPUT_REQUIRED` and `AUTH_REQUIRED` are +interrupted, retain resumable state, and are not generic terminal failures. +Do not universally close every interrupted stream: apply the spec's +authentication-required keep-stream recommendation in the selected profile. + +### Identity, mapping, and authorization + +The server generates opaque `taskId`; `contextId` groups related tasks. Persist +the mapping from authenticated owner scope + agent slug + context to a safe +runtime session ID. Do not accept arbitrary task/context IDs as runner filenames. +A supplied task must belong to the same principal/agent/context; reject mismatches +before model execution. For an initial request **without contextId**, deduplicate +by authenticated owner + agent + client message ID before generating context/task +IDs; retries without context return the original mapping. For existing contexts, +include context in the dedup scope. Store canonical payload hashes and reject +conflicting reuse in either case; generating a fresh context before dedup would +incorrectly create a new task on each retry. + +Reuse `_auth.py` for inbound authentication, but add task-level authorization: +get/list/subscribe/cancel/continuation and artifact reads must all enforce owner +scope. Function keys identify an application access boundary, not individual +users; shared-key/anonymous modes must be explicitly single-trust-domain profiles +or denied for multi-user durable tasks. Tenant equality and ID possession are +not sufficient. Listing filters scope before pagination; use non-enumerating +not-found responses for unauthorized task IDs. Never trust arbitrary principal +headers without the existing Easy Auth enforcement checks. + +## 5. Durable distributed architecture + +```mermaid +flowchart LR + Client --> HTTP["Functions A2A routes on any worker"] + HTTP --> Admission["Authorized admission / scoped dedup"] + Admission --> Store["Shared task projection + ordered event journal"] + Admission --> Outbox["Recoverable dispatch intent"] + Outbox --> Durable["Durable orchestration"] + Durable --> Activity["MAF execution activity"] + Activity --> Store + Store --> Readers["Independent snapshot + cursor readers"] + Readers --> HTTP +``` + +No frontend, .NET host, or Python worker affinity is required. The executor may +run elsewhere; each HTTP stream is only a reader of shared state. Durable owns +durable scheduling and coarse lifecycle, while LLM calls and tools run in +activities, never deterministic orchestrator code. + +### State, admission, and consistency + +Persist a task projection containing identity scope, context mapping, status, +artifacts/references, last sequence, execution attempt/owner epoch, cancellation +intent, expiry, and orchestration linkage. Keep internal sequence/ownership +metadata out of standard protocol fields unless a negotiated extension uses it. +Events are append-only per task with a unique increasing sequence. + +P6a implements/tests atomic state transition and event append (or log-derived +snapshots), before admission or streaming wiring. P8 adds readers, not publication +correctness: a reader obtains the current Task and watermark N +consistently, emits that Task first, and then reads N+1 onward. Do not perform an +uncoordinated snapshot read followed by a new live-only listener. + +Admission atomically records deduplication, initial Task, and recoverable dispatch +intent. An outbox dispatcher retries scheduling with stable orchestration identity +and verifies duplicate/start outcomes. Specify reconciliation for both "accepted +but unscheduled" and "scheduled but acknowledgement lost"; never use best-effort +dual writes that can orphan tasks. Storage Queue may wake the dispatcher, but is +not the source of accepted-task truth. + +The tentative first store is Azure Table for compact metadata/events plus Blob +for large artifacts. It is **not finalized**: the implementation gate must define +partition keys and prove the actual atomic transaction boundaries for admission, +deduplication, projection, and publication. Table transactions cannot span +partitions and have operation/payload limits; a cross-partition list index is a +rebuildable projection, not an authorization or admission authority. Choose +another transactional store if these invariants cannot be met cleanly. +Durable's `host.json` backend selection and the A2A journal store are separate +concerns; DTS does not automatically supply a token-event journal. + +### Execution, ownership, and retry + +Use conditional writes (ETag/CAS) and owner epochs/fencing for each execution +attempt. Reject stale activity publications after cancellation, takeover, or +terminal transition. Lease renewal alone is insufficient: every write validates +the current epoch. Apply equivalent distributed coordination to shared context +turns; the existing process-local chat lock cannot protect them. + +Do not replay live LLM calls inside orchestrators or put each token in orchestration +history. Activity retries may rerun the model or tools. Reuse stable per-logical +tool-operation idempotency keys where supported; document non-idempotent side +effects and do not enable indiscriminate retries. Define whether a retry resumes +from a checkpoint or supersedes a partial artifact; never append a regenerated +answer blindly onto output from a failed attempt. + +Separate cancellation intent from confirmed `canceled` state. `CancelTask` records +a distributed request, wakes/signals the executor, and returns the actual current +state according to protocol rules. The executor checks cancellation at safe +boundaries, closes MAF/client resources, and publishes the terminal transition. +Orchestration termination does not kill an already running activity or undo tools +([Durable instance management](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-instance-management#terminate-instances)). +Race completion against cancellation through the same fenced state transition. + +### Subscriptions, retention, and operations + +Subscribers use independent cursors over the same journal: they must not consume +each other's events. Neither a competing-consumer Storage Queue nor a Redis +consumer group implements broadcast delivery by itself. One subscriber's +disconnect releases that reader, not execution or other readers. + +Full historic replay and `Last-Event-ID` are not required by the chosen baseline +contract. The protocol allows missed transient statuses; current Task state plus +ordered continuation is the supported default. A journal enables durable +continuation, not an unlimited replay promise. Define event and artifact retention, +task expiry, authorization after expiry, pagination limits, and behavior when a +slow reader falls behind retained data. Never silently skip gaps. Reconnect with +a fresh snapshot or return an explicit expiry/resync error as appropriate. + +Bound active tasks, stream count, per-subscriber memory, output size, batching +latency, and idle periods when each surface first ships (P3, P5a/b, P6a/b/c, P8). +Retention, artifact authorization, and cursor-gap handling are not deferred to +P10; that gate aggregates/tunes existing safeguards. Heartbeats are SSE comments, +not A2A domain events. +Model deadlines, function execution timeouts, proxy idle limits, and total HTTP +connection limits are distinct; neither an exact universal 230-second SSE cap +nor "heartbeats make streams unlimited" is a valid guarantee. Document measured +hosting-plan behavior only after a separate deployment acceptance exercise. +Emit sanitized task/cursor/retry/cancel metrics without prompts, tokens, secrets, +or raw principal/session IDs. + +## 6. Dependency profiles and release gates + +Delivery prioritizes the working P3 simple server/sample and P4/P5 basic SSE +before P1/P2 dependency isolation. The table describes the **post-P2 target**, +not the initial P3/P5 installation: until P2, the existing mandatory Durable +dependency remains, and until P1 its eager imports may remain. With workflows +disabled, those samples use no Durable execution/bindings; P1/P2 later prove +Durable-free imports/installations while preserving the same working samples. + +| Proposed install | Durable installed? | Startup with no workflows | Enabled capability | +| --- | --- | --- | --- | +| Base | No | Plain `FunctionApp`, no Durable imports/bindings | Existing non-workflow runtime | +| `[monitor]` | No | Same, plus optional exporter | Monitoring only | +| `[a2a]` | No | Plain `FunctionApp`; MAF A2A imported only when configured | Simple; basic only after its gate | +| `[workflows]` | Yes | Plain app if workflows disabled | Existing workflows when enabled | +| `[a2a-durable]` | Yes | Plain app unless durable A2A/workflows enabled | Durable A2A after its gates | +| `[workflows,a2a]` | Yes | Workflow-driven `DFApp` only when workflows enabled | Workflows plus simple/basic A2A | +| `[workflows,a2a-durable]` | Yes | One `DFApp` when either durable feature enabled | Both engines without duplicate registration | + +Test actual built wheels in fresh environments, not only a development checkout +that already has Durable installed. Check resolved `Requires-Dist` closure, package +import, direct runner import, app creation, generated binding metadata, and plain +chat execution with a fake inference client. Add an import blocker test to detect +accidental `azure.durable_functions`/`durabletask` imports even in fully provisioned +developer environments. Test missing extras and broken installed dependencies +separately. The selected MAF hosting distribution's transitive closure must also +pass: moving only the runtime's direct dependency is not sufficient. + +## 7. Rationale and remaining decisions + +MCP legacy SSE's owner backplane forwards **inbound** JSON-RPC arriving at a +different .NET host to a queue named `mcp-backplane-{instanceId}`; matching local +sessions bypass it. It is not a token-output journal, and MCP Streamable HTTP's +stateless path is a separate design. Do not infer fixed latency or constant-cost +advantages when the traffic differs. If owner routing were chosen, a random +process-unique Python owner ID would suffice; mandatory host-plus-PID routing is +not an inherent requirement. This design instead needs recoverable execution and +any-worker reads, so it chooses shared state and events. + +A shared SDK `TaskStore` is not a distributed executor, ordered event bus, or +exact-replay engine. SDK 1.1.2's +[`DefaultRequestHandlerV2`](https://github.com/a2aproject/a2a-python/blob/3e6fa6a41d64f0581202df214a0515a0b0194832/src/a2a/server/request_handlers/default_request_handler_v2.py#L79-L113) +creates a local `ActiveTaskRegistry`; its `queue_manager` argument is retained +for compatibility, not distributed execution. Use the SDK's public +`RequestHandler` boundary with the MAF hosting helpers instead of patching this +registry. SDK 1.1.2 also registers +[both GET and POST REST subscribe](https://github.com/a2aproject/a2a-python/blob/3e6fa6a41d64f0581202df214a0515a0b0194832/src/a2a/server/routes/rest_routes.py#L76-L81); +this is evidence of a binding compatibility choice, not a reason to broaden the +initial JSON-RPC scope. + +Before human finalization: settle the library/wire tuple, supported local SSE +operations and deployment guard, trusted principal scope and card URL policy, +storage atomicity/partition design, retry-artifact policy, and concrete resource +limits. These decisions constrain implementation; they are not reasons to change +dependencies or advertise capabilities in this documentation-only work. diff --git a/docs/frds/0009-a2a-server.md b/docs/frds/0009-a2a-server.md new file mode 100644 index 00000000..603c367c --- /dev/null +++ b/docs/frds/0009-a2a-server.md @@ -0,0 +1,226 @@ +--- +frd: "0009" +title: A2A server with optional Durable execution +status: In review +author: tsuyoshiushio / Copilot +created: 2026-09-08 +updated: 2026-09-08 +issues: [] +pull_requests: [] +branch: tsuyoshiushio-a2a-frd-and-design +--- + +# FRD 0009 - A2A server with optional Durable execution + +## 1. Summary + +Add an opt-in Agent2Agent (A2A) server surface to markdown-authored agents using +Microsoft Agent Framework (MAF)'s A2A library. Deliver a simple non-streaming +server first, a deliberately limited basic SSE server next, and durable, +distributed task execution afterward. Separately remove the runtime's existing +mandatory Durable dependency for applications that use neither workflows nor +durable A2A. This is a **design proposal, not an available feature**. + +The [proposed architecture](../design/a2a-server.md) defines integration and +correctness boundaries; the [implementation plan](../design/a2a-implementation-plan.md) +defines independently reviewable PRs and capability gates. Existing behavior +remains documented in [architecture.md](../architecture.md). + +## 2. Motivation / problem + +The current chat SSE endpoint streams one POST response; it is not an A2A task +service. Its process-local session locks and conversation history cannot provide +cross-worker task lookup, cancellation, or subscriptions. Copying a connection +backplane would route requests but would not solve task execution recovery. + +At inspected baseline `08b9d762d95f451ce926b89c166a14a2d345d433`, +`azure-functions-durable==2.0.0b2` is mandatory even for plain chat applications, +and eager imports prevent simply moving that requirement to an extra. The pinned +MAF packages' locked dependency closures do not themselves require Durable. +Decoupling must address installation, imports, startup, and registration together. + +Users need an incremental adoption path without accidentally treating a +single-process SSE prototype as a production distributed service. Reviewers need +separate changes for dependency migration, protocol semantics, execution, and +distributed delivery rather than a single large feature PR. + +## 3. Goals / Non-goals + +**Goals** + +| ID | Requirement and observable acceptance criterion | +| --- | --- | +| R1 | Opt-in authoring follows discover -> translate -> register -> execute. Existing configurations produce the same routes; `builtin_endpoints: true` does not silently expose A2A. | +| R2 | Use the MAF A2A integration through public extension points, with one tested protocol/version tuple. AgentCard, request validation, result/event serialization, and errors match that tuple; no hand-written competing protocol stack. | +| R3 | Simple mode completes an authorized text request with one direct A2A `Message`, without Task persistence, SSE, or Durable execution. Initial package/import coupling remains until P1/P2. Unsupported task operations fail explicitly; `returnImmediately` has no effect for a direct Message and does not promise detached execution. | +| R4 | Basic SSE emits legal A2A domain events, not chat `data:` payloads. Incremental output is Task-first with ordered artifact updates; a message-only stream contains exactly one Message. Local-only limitations are explicit. | +| R5 | Existing chat wire output, public runner APIs, tool authorization, model options, session handling, deadlines, usage records, and cleanup remain unchanged. | +| R6 | At P2, fresh base, base+monitor, and simple/basic A2A installations do not install or import Durable, and register no Durable bindings when workflows are disabled. Earlier P3/P5 samples retain the baseline mandatory dependency; isolation is not their prerequisite. Enabling a feature without its extra fails at startup with an actionable error. | +| R7 | Existing workflow applications retain behavior after installing the documented workflow extra. Workflows and durable A2A coexist with one `DFApp`, independently named registrations, and no duplicated workflow engine. | +| R8 | Durable admission returns stable task/context identities and eventually schedules accepted work despite disconnect or scheduling failures. Context-less retries deduplicate by owner + agent + client message ID before server IDs are generated; conflicting payloads fail. | +| R9 | An authorized request on any worker can get/list/subscribe/cancel a durable task. Subscribe starts with the current snapshot at sequence N, then events after N without a race gap; multiple subscribers receive the same ordered events independently. | +| R10 | Execution ownership, retries, continuation, and cancellation have explicit state rules. Stale attempts cannot publish; cancellation is cooperative; retryable activities do not imply exactly-once tool side effects. | +| R11 | Every operation enforces owner/agent/context scope, bounded input and output, retention, and resource limits. Tenant ID or possession of a task ID alone is not authorization. | +| R12 | Each PR carries its own tests, bounds, and affected docs. Basic SSE requires an enforced local-only startup guard or stays test-only; durable config/cards remain internal through P9b and become public only after P10 qualification. | + +**Non-goals** + +Do not replace chat/UI/MCP endpoints or the workflow plan engine; add remote A2A +agents as tools; implement push notifications, gRPC, or REST in the initial +chain; guarantee full historic replay, unlimited SSE lifetime, exactly-once LLM +execution, or exactly-once external side effects. Do not deploy experimental +infrastructure as part of this design work. A2A Task identity is not a Durable +instance ID and is not a chat session ID. + +## 4. Proposed design + +| Pipeline stage | Existing / proposed modules | Change | +| --- | --- | --- | +| Discover | `discovery/*` | Keep read-only inventories. A2A reuses the existing agent/tool/skill inventory; no new discovery format. | +| Translate | `config/schema.py`, `merge.py`, `validation.py` | Proposed typed A2A options and strict mode validation; compose auth and capabilities before app mutation. | +| Register | `app.py`, `registration/capabilities.py`, `registration/a2a.py` (new), `_auth.py` | Freeze mode-specific capabilities; register per-agent card and JSON-RPC routes. Select `DFApp` only for enabled workflows or durable A2A. | +| Execute | `runner.py`, `a2a/adapter.py` (new) | Reuse MAF execution and runtime policy. Introduce a private typed event seam when basic SSE consumes it; retain existing chat serialization. | +| Durable execution | `a2a/durable.py`, `a2a/store.py` (new) | Separate task lifecycle from HTTP and use a shared task projection plus ordered journal. Azure binding/registration code remains in registration or the composition root. | + +Names for new internal modules are proposed ownership boundaries, not promises of +new public Python APIs. The recommended integration is the published Alpha +`agent-framework-hosting-a2a==1.0.0a260730` with +`agent-framework-hosting==1.0.0a260730` and candidate `a2a-sdk==1.1.2`. +Use its public conversion helpers with an app-owned SDK `RequestHandler`, not the +opinionated MAF `A2AExecutor`. The [library gate](../design/a2a-server.md#2-library-and-protocol-compatibility-gate) +covers Alpha approval, native models, optional HTTP extras, and the Functions +response bridge. Detailed storage selection is a separate pre-durable gate. + +### Authoring / API surface + +Proposed example, **not valid in the current schema**: + +```yaml +builtin_endpoints: + chat_api: true + a2a: + mode: simple + http_auth: entra +``` + +`a2a` is absent/disabled by default. `mode` is initially only `simple`; `basic` +requires the P5b local-only startup guard (otherwise test-only). `durable` is not +accepted as public configuration or advertised in cards until P10; earlier slices +use internal harnesses/experimental configuration. `builtin_endpoints: true` +keeps its current chat/UI/MCP meaning. An explicit +A2A declaration inherits the resolved built-in HTTP auth policy; no secret, +connection string, task hub, or executor routing ID goes into front matter. +An A2A-only agent must satisfy the endpoint-or-trigger validation rule. + +Proposed per-agent routes, relative to the configured Functions route prefix: +`agents/{slug}/a2a` (JSON-RPC POST) and +`agents/{slug}/.well-known/agent-card.json` (GET). The per-agent base avoids an +ambiguous root card in a multi-agent app, but is **not domain-root well-known +auto-discovery**: clients configure the explicit card URL or SDK `card_path`. +A root singleton/routing convention is deferred. Published card URLs use a trusted +external base URL, not an untrusted forwarded Host header. Card discovery/auth and +SDK route mounting are part of the first server acceptance gate. + +Target semantics are A2A specification v1.0.1, **wire version `1.0`**. Never +advertise 1.0 based on a Python package's version number. A 0.3 SDK surface requires +an explicitly approved compatibility profile, not mixed method names or fields. +See the design's version gate. + +### Compatibility + +The final simple/basic A2A installation must not require Durable unless the same +app enables workflows. This is P2's acceptance gate after the P3/P5 working +samples: those earlier slices retain existing mandatory Durable installation and +eager imports but do not use Durable execution/bindings when workflows are off. +Proposed extras are `[workflows]`, `[a2a]`, and `[a2a-durable]`; the last includes +the dependencies needed for both A2A and Durable execution. `[monitor]` remains +independent. These names are proposals until dependency review. + +Moving an existing mandatory dependency is a packaging compatibility change: +update workflow samples, E2E requirements, development installs, and migration +guidance in the same PR. Do not silently disable workflows when Durable is absent. +Preserve supported public workflow helper imports using lazy exports/type-safe +boundaries where necessary; invoke an actionable missing-extra error only when +Durable functionality is actually requested. + +### Delivery sequence + +The [plan](../design/a2a-implementation-plan.md) prioritizes **D0 -> P3 -> P4 -> +P5a -> P5b -> P1 -> P2 -> P6a onward**, retaining existing PR IDs for reference. +After the FRD, P3 delivers a working simple A2A implementation/sample for design +validation and discussion; record feedback before the P4 event seam and P5a/P5b +local lifecycle/SSE work. P1/P2 import cleanup and dependency migration follow, +then P6a/P6b/P6c atomic storage/admission/managed polling, P7 recovery, +P8 distributed readers/SSE, P9a cancellation, P9b continuation, and P10 qualification. +Atomic publication belongs to P6a; bounds ship with each affected surface. +Internal tested components do not create unused public APIs or early card claims. + +## 5. Decisions log + +Entries marked Agent are recommendations, not human architecture approval. + +| # | Decision | Options considered | Choice | Decided by | Date | +| - | -------- | ------------------ | ------ | ---------- | ---- | +| 1 | Scope of this work | Implement / design only | Documentation only; no production code, dependency change, commit, push, or PR | Human, relayed by coordinating session | 2026-09-08 | +| 2 | Delivery order | All-at-once / incremental | Non-SSE simple server, basic SSE, then durable/distributed capabilities; reviewer-friendly PRs | Human, relayed by coordinating session | 2026-09-08 | +| 3 | A2A integration | Custom protocol implementation / MAF A2A | Use MAF A2A library; verify its server and SDK boundaries before implementation | Human, relayed by coordinating session | 2026-09-08 | +| 4 | Durable installation | Mandatory / optional | Optional when neither workflows nor durable A2A is enabled | Human, relayed by coordinating session | 2026-09-08 | +| 5 | Initial protocol binding | JSON-RPC / REST / both | JSON-RPC first; exact supported version determined by the library gate | Agent | 2026-09-08 | +| 6 | Simple response model | Invent transient Task / direct Message | Direct single Message; no fabricated persistence or nonblocking acceptance | Agent | 2026-09-08 | +| 7 | Distributed transport | Owner-routed backplane / shared state and events | Any-worker readers over shared projection/journal; execution separate from HTTP | Agent | 2026-09-08 | +| 8 | Replay promise | Full token history / current snapshot plus ordered continuation | Snapshot plus continuation; retention-defined journal, no full historic replay promise | Agent | 2026-09-08 | +| 9 | Storage baseline | Table+Blob / another transactional store | Tentatively Table+Blob; transaction, admission, fencing, and retention gate before implementation | Agent | 2026-09-08 | +| 10 | Authoring and extras | New top-level format / existing built-in config | Extend typed built-in options; propose workflows/a2a/a2a-durable extras; keep old shorthand unchanged | Agent | 2026-09-08 | +| 11 | MAF server integration | `A2AExecutor` / hosting conversion helpers | Recommend published Alpha hosting-a2a helpers plus app-owned SDK RequestHandler to preserve runtime policy; reviewer acceptance and wheel/HTTP bridge gate required | Agent, source review by coordinating session | 2026-09-08 | +| 12 | Direct Message nonblocking flag | Reject `returnImmediately=true` / spec no-effect behavior | Honor the specification: flag has no effect for direct Message or streaming; do not invent asynchronous Task support | Agent, protocol correction from coordinating session | 2026-09-08 | +| 13 | Review granularity and release gates | Large lifecycle PRs / bounded sub-PRs | Mandatory P5a/b, P6a/b/c, P9a/b split; enforce local-only basic mode; durable public gate at P10; safety bounds accompany first use | Agent architecture review, coordinating session | 2026-09-08 | +| 14 | Prioritize executable design feedback | Dependency cleanup first / simple server and SSE first | D0 -> P3 working implementation/sample and discussion -> P4 -> P5a/b -> P1/P2 -> durable stages. Preserve IDs; defer Durable-free install/import acceptance to P2, not the initial sample | Human, explicit user direction; sequence approval only, not full FRD sign-off | 2026-09-08 | +| 15 | Publish the first design review PR | Keep local drafts / open the bottom PR | Publish only the English FRD, architecture, plan, and index as a signed draft PR on main. Japanese copies stay local; P3 and native stack registration follow separately. This supersedes decision 1's publication restriction, not the implementation/sign-off gate | Human, relayed by coordinating session | 2026-09-08 | + +## 6. Test plan + +Every implementing PR includes tests; this documentation-only change does not +claim these tests exist or pass. Requirement IDs map to PRs in the plan. + +- [ ] Extend `test_app.py`, `test_app_routes.py`, `test_registration_endpoints.py` + and config-scenario fixtures for route opt-in, auth inheritance, validation, + missing extras, and workflows/A2A coexistence. +- [ ] Add clean-wheel environment coverage for base, monitor, A2A, durable A2A, + workflows, and combined extras; assert installed closure, imported modules, + successful startup, and actual generated binding metadata. +- [ ] Add protocol fixtures using the selected MAF/SDK tuple for card discovery, + negotiation, direct Message, Task-first SSE, status/artifact ordering, errors, + unsupported task methods/input parts, no-effect `returnImmediately` behavior + for direct Message/streaming, and JSON-RPC correlation. +- [ ] Keep `test_runner_streaming.py`, `test_runner_usage.py`, + `test_runner_harness.py`, and delegation tests as characterization gates for + the private event refactor, including timeout/cancel/GeneratorExit cleanup. +- [ ] Add backend contract and multi-process tests for atomic publication, + admission outbox recovery, duplicate IDs, ownership fencing, subscription races, + two subscribers, cancellation races, restart, interrupted continuation, scoped + listing, retention expiry, and slow readers. +- [ ] Run existing lint/type/test gates on each product PR. Run host E2E coverage + for binding annotations and streaming lifetime; mocks alone cannot prove these. + +## 7. Docs impact + +This design change adds the FRD, companion design/plan, and FRD index entry. +It does not describe A2A as implemented in the current architecture or onboarding. +Implementation PRs must update `docs/architecture.md`, `docs/triggers.md`, +`docs/front-matter-spec.md`, `docs/workflows.md`, `README.md`, +`docs/index.md`, and `docs/getting-started.md` as each surface ships. +Schema-changing PRs regenerate `docs/front-matter-reference.md` and run the +`update-schema-docs` skill. Extras migration updates sample and contributor +installation instructions together with package metadata. + +## 8. Status & sign-off + +- **Architecture review (phase 2):** Coordinating session reviewed all three + documents; requested smaller lifecycle slices, deterministic release gates, + early atomicity/bounds, context-less dedup, and explicit card discovery. + Revisions incorporated; protocol/library compatibility, + concrete storage transaction design, identity policy, and local SSE release + restrictions are explicit review gates in the companion design. +- **Human sign-off:** Not yet recorded. Authorization to write this proposal is + not approval to implement it. Keep `status: In review` until explicit sign-off. +- **Implementation:** Not started; no feature or production-readiness claim. diff --git a/docs/frds/README.md b/docs/frds/README.md index 40cc5765..62780f62 100644 --- a/docs/frds/README.md +++ b/docs/frds/README.md @@ -37,6 +37,7 @@ The full lifecycle that produces an FRD lives in [`AGENTS.md`](https://github.co | [0006](0006-endpoint-authentication.md) | Endpoint & HTTP trigger authentication (API key / Entra ID) | Finalized | | [0007](0007-multi-agent-delegation.md) | Multi-agent delegation (agent-as-tool) | In review | | [0008](0008-harness-only-agent-configuration.md) | Harness-only agent configuration | Finalized | +| [0009](0009-a2a-server.md) | A2A server with optional Durable execution | In review | > `_template.md` is the template, not an FRD — the leading underscore keeps it > sorted first and excludes it from numbering.