Skip to content

feat(minimal_pvc_agent): support A2A streaming - #1

Open
moonlight16 wants to merge 3 commits into
mainfrom
feature/agent-streaming
Open

feat(minimal_pvc_agent): support A2A streaming#1
moonlight16 wants to merge 3 commits into
mainfrom
feature/agent-streaming

Conversation

@moonlight16

Copy link
Copy Markdown
Owner

Summary

kagenti chat minimal-pvc-agent was failing with "Streaming is not supported by the agent" because the agent card declared streaming=false while the CLI's default path calls message/stream. This PR adds real streaming support so the CLI's default path just works.

Changes

  • AgentCapabilities(streaming=True) in the agent card.
  • Replaced graph.ainvoke(...) with graph.astream(..., stream_mode="updates") in MinimalPVCExecutor.execute so intermediate LangGraph node events are surfaced.
  • Emits an initial TaskState.working "thinking..." status before the graph runs, then one working-status per node event with output summaries truncated to 256 chars (same convention as weather_service).
  • Terminal artifact + input_required status logic unchanged, so non-streaming message/send clients still get the same response shape.
  • Persistence layer (SQLite-on-PVC checkpointer, task store) untouched.
  • Version bumped 0.2.0 → 0.3.0.

Testing

  • Local: uv run --with pytest --with pydantic-settings --python 3.11 python -m pytest tests/a2a/test_minimal_pvc_agent.py -q — 9 passed.
  • Local smoke test with the server running:
    • curl /.well-known/agent-card.json shows "streaming": true, "version": "0.3.0".
    • message/send returns the echo reply with the artifact (backward compatible).
    • message/stream returns SSE with initial status → intermediate node events → artifact → terminal status.

Related

Complements upstream Kagenti PR that teaches kagenti chat to fall back to message/send for agents that declare streaming=false. Either fix alone unblocks kagenti chat for this agent; both together are the belt-and-suspenders story.

A minimal A2A agent for validating durable agent context on Kagenti.
One-node LangGraph chat agent with two persistence paths:

- Per-turn JSONL appended to <CONTEXT_DIR>/<sanitized-context-id>.jsonl.
  Default CONTEXT_DIR is /shared/minimal-pvc-agent, the Kagenti
  StatefulSet PVC mount path. Best-effort: IO errors are logged and
  swallowed so the agent stays responsive when /shared is unavailable.

- LangGraph checkpointer keyed on the A2A context_id as thread_id.
  MemorySaver by default; AsyncPostgresSaver when CHECKPOINT_DB_URL is
  set, so conversation state can survive pod restarts.

A2A task storage mirrors the same shape: InMemoryTaskStore by default,
DatabaseTaskStore (a2a-sdk[sql], SQLAlchemy async engine) when
TASK_STORE_DB_URL is set.

Two read-only HTTP endpoints inspect persistence without `kubectl exec`:
- GET /history?context_id=...    returns the JSONL turn log
- GET /checkpoint?context_id=... returns the LangGraph MessagesState

Falls back to echo mode (returns "echo: <input> (turn N)") when no
valid LLM credentials are configured, so smoke tests work without an
LLM. Uses the same has_valid_api_key heuristic as weather_service:
dummy keys are accepted only when the LLM base URL is localhost.

a2a-sdk pinned to >=0.3.26,<1.0 to match the version running on the
target cluster (verified via kubectl exec against the live weather
agent).

Includes 24 unit tests covering configuration defaults and env
overrides, has_valid_api_key heuristic, context-id sanitization
(path traversal, length cap, unicode), and JSONL append/read
(append, separate contexts, missing root mkdir, swallowed OSError,
malformed-line skipping).

End-to-end verified locally: two-turn A2A conversation produced two
JSONL records, /history returned both turns, /checkpoint returned a
four-message LangGraph state proving cross-turn memory.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
Replace the custom JSONL writer with the harness's own persistence
primitives, both backed by SQLite files on the PVC:

- LangGraph AsyncSqliteSaver at CHECKPOINT_PATH (default
  /shared/checkpoints.db). Empty string falls back to MemorySaver.
- A2A DatabaseTaskStore via SQLAlchemy asyncio over a SQLite file at
  TASK_STORE_PATH (default /shared/tasks.db). Empty string falls back
  to InMemoryTaskStore.

The agent code no longer contains any persistence logic. Removing the
JSONL writer (persistence.py, /history endpoint, append_turn call in
execute()) also removes the burden of every agent author having to
reimplement basic session persistence: the harness already knows how
to do it, we just need to point it at durable storage.

Drops langgraph-checkpoint-postgres and asyncpg dependencies; adds
langgraph-checkpoint-sqlite and aiosqlite. Postgres support can be
reintroduced later when we need multi-writer semantics.

Smoke-tested locally: two turns in one context produced a checkpoints.db
containing four messages (2 human + 2 AI) via GET /checkpoint. The
turn counter correctly incremented to (turn 2), proving LangGraph saw
turn 1's messages via the checkpointer.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
The Kagenti CLI's `kagenti chat` calls `message/stream` by default, but
the agent card previously declared `streaming=false`, so the a2a-sdk
rejected the request with "Streaming is not supported by the agent".

Enable streaming by:
- Declaring `AgentCapabilities(streaming=True)` in the agent card.
- Replacing `graph.ainvoke(...)` with `graph.astream(..., stream_mode="updates")`
  so intermediate LangGraph node updates are visible to clients.
- Emitting an initial `TaskState.working` "thinking..." status before the
  graph runs so streaming clients see progress right away.
- Emitting one working-status message per node event, with node output
  summaries truncated to 256 chars (same convention as weather_service).

The final artifact + `input_required` status logic is unchanged, so
non-streaming `message/send` clients still get the same response shape
as before. The persistence layer (SQLite-on-PVC checkpointer, task
store) is untouched.

Bumps the agent version from 0.2.0 to 0.3.0.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jeremy Cohn <Jeremy.Cohn@ibm.com>
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.

1 participant