Skip to content

feat(plug): implement tasks/resubscribe - #106

Merged
maxekman merged 1 commit into
mainfrom
max/99-tasks-resubscribe
Sep 23, 2026
Merged

maxekman merged 1 commit into
mainfrom
max/99-tasks-resubscribe

Conversation

@maxekman

Copy link
Copy Markdown
Contributor

Summary

Implements tasks/resubscribe (SubscribeToTask), which answered -32004 for every request. This empties the TCK baseline — test/tck/expected-failures.txt now has no entries.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor / chore
  • Documentation

What changed

  • Subscribing opens an SSE stream: task snapshot first, then a status update per state change, ending at terminal state
  • Subscriber registry in agent state, monitored so a dropped connection deregisters itself
  • Error codes per requirement: -32001 unknown task, -32004 terminal task or undeclared streaming
  • :authorize_task now runs under a new :resubscribe operation — the arm ran no authorization at all before
  • A2A.Plug gains :resubscribe_timeout (default 60s) for streams that go idle
  • A2A.Client.resubscribe/3

Results

All four STREAM-SUB requirements pass, and the subscribe/multi-stream families un-skipped with them:

Before After
Overall 74.8% 78.6%
MUST 69 passed, 22 skipped 75 passed, 17 skipped
jsonrpc 86/95, 7 skipped 93/100, 7 skipped
Baseline 2 node ids (#99) empty

STREAM-SUB-001/002 and STREAM-ORDER-001..004 plus the three test_multi_stream tests all went from skipped to passing. mix test 652 passed, mix quality clean.

⚠️ The TCK job gets ~30s slower. The three multi-stream tests each spend a fixed 10s observation window collecting broadcast events. They pass — that's their design, not a timeout on our side.

The trap this had to avoid

A stored :working task still holds the agent's source enumerable in metadata[:stream], and get_task hands it back. Enumerating it replays from element one rather than attaching, and each consumer's Stream.transform after-fun casts its own {:stream_done, …} — duplicating artifacts, history, the terminal transition, and the push delivery.

So resubscribe never touches it: it reads the task for a snapshot and waits for events the agent pushes. There's a regression test asserting artifacts and history stay at one after a subscribe.

Design notes

Why the registry lives in agent state. AGENTS.md forbids a supervision tree in application/0, so there is nowhere else to hang one. The agent fans out from the same three transition seams push notifications already hook, and Process.monitor is the entire cleanup story.

Why an idle timeout. The relay blocks in a receive loop; a task that never terminates would pin its connection process indefinitely.

Deliberate limits

  • No replay of prior events — a subscriber sees the snapshot and everything after, not artifacts already produced. Pair with tasks/get for full history.
  • :rejected is not terminal to A2A.Task.terminal?/1 (it covers completed/canceled/failed), though STREAM-SUB-002's wording includes it. The runtime never produces :rejected, so it cannot bite today — widening @terminal_states would also change continue_task/5, so I left it alone rather than quietly expanding scope.

Closes #99. Part of #13.

Resubscribe answered unsupported-operation for every request, which is
the right answer for exactly one case and wrong for the rest: an
unknown task should report not-found, and a task still running should
get a stream.

Subscribing now opens an SSE stream whose first event is the task as
it stands, then one status update per state change, ending when the
task is terminal. Subscribers live in the agent's own state because
the library may not start a supervision tree, so there is nowhere else
to keep them; each is monitored, which makes a dropped connection
deregister itself without the agent being told. An idle timeout closes
a stream whose task never finishes, so it cannot pin a connection
process open forever.

The stored task still carries the agent's own source enumerable, and
that is a trap rather than a resume point: enumerating it replays from
the start instead of attaching, and each consumer casts its own
completion, duplicating the task's artifacts and history. A subscriber
therefore only reads the snapshot and waits for pushed events, which
also means nothing produced before it attached is replayed.

Clears the last baselined compliance failure, and un-skips the
subscribe and multi-stream families along with it.

Closes #99. Part of #13.
@github-actions

Copy link
Copy Markdown

TCK Compliance Results

Result: passed - failures match the tracked baseline.

TCK run complete — 0 known failure(s), matching test/tck/expected-failures.txt.

             A2A TCK Compatibility Report              
═══════════════════════════════════════════════════════
SUT: http://localhost:9999
Timestamp: 2026-09-23T08:22:11.820972+00:00

OVERALL COMPATIBILITY: 76.8%

┌─────────────┬────────┬────────┬─────────┬───────┐
│ Level       │ Passed │ Failed │ Skipped │ Total │
├─────────────┼────────┼────────┼─────────┼───────┤
│ MUST        │     75 │     22 │      17 │   114 │
│ SHOULD      │      7 │      4 │       0 │    11 │
│ MAY         │      4 │      0 │       0 │     4 │
└─────────────┴────────┴────────┴─────────┴───────┘

BY TRANSPORT:
  agent_card:    10/10 ✓
  grpc:          0/72 (72 skipped) ✓
  jsonrpc:       93/100 (7 skipped) ✓
  http_json:     3/83 (80 skipped) ✓

═══════════════════════════════════════════════════════


- Generated html report: file:///home/runner/work/a2a-elixir/a2a-elixir/.tck/reports/tck_report.html -
99 passed, 166 skipped in 31.58s
Command: /home/runner/work/a2a-elixir/a2a-elixir/.tck/.venv/bin/python -m pytest tests/compatibility/ --sut-host=http://localhost:9999 --tb=short --transport=jsonrpc -q --compatibility-report=reports/compatibility --html=reports/tck_report.html --self-contained-html --junitxml=reports/junitreport.xml


========================================
TCK run complete — 0 known failure(s), matching test/tck/expected-failures.txt.
Stopping server (PID 2683)...

@maxekman
maxekman merged commit 4b2af51 into main Sep 23, 2026
8 checks passed
@maxekman
maxekman deleted the max/99-tasks-resubscribe branch September 23, 2026 08:24
@claude

claude Bot commented Sep 23, 2026

Copy link
Copy Markdown

Reviewed — no high-confidence issues found. LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement tasks/resubscribe streaming

1 participant