Skip to content

feat(pydantic-acp): auth recovery, public session bootstrap, and clean agent-error propagation#22

Open
arthrod wants to merge 1 commit into
vcoderun:mainfrom
arthrod:feat/pydantic-acp-session-hardening
Open

feat(pydantic-acp): auth recovery, public session bootstrap, and clean agent-error propagation#22
arthrod wants to merge 1 commit into
vcoderun:mainfrom
arthrod:feat/pydantic-acp-session-hardening

Conversation

@arthrod

@arthrod arthrod commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

Hardens the pydantic-acp ACP client/provider so an unusable ACP agent fails legibly instead of silently, and adds the session-bootstrap surface the bridge was missing. Single commit off main; ships as synchronized release 1.5.0 → 1.6.0.

Motivation: driving real stdio ACP agents (Claude Code, GLM, dirac, vibe, grok, …) surfaced three gaps where a broken/unauthenticated/throttled agent produced an empty turn or an opaque ExceptionGroup with no indication of the real cause, and no public way to bring a session up before prompting.

Added

  • Auth recovery. AcpProvider captures initialize's auth_methods and, when session/new is rejected with auth_required (-32000), runs the ACP authenticate flow — an advertised method, or an explicit auth_method_id= — and retries session creation once. Agents that gate session/new on authentication were previously unrecoverable.
  • Public session bootstrap. AcpProvider.ensure_session() and AcpProvider.set_session_mode() bootstrap a session and select a session mode without sending a prompt turn, so callers no longer reach into the private _ensure_session.
  • Empty-turn diagnostic. Opt-in AcpProvider(raise_on_empty_turn=True) raises UnexpectedModelBehavior with an ACP-specific message when a text-output turn yields no visible text. Default False preserves the existing empty-response contract (and its test).

Fixed

  • Error propagation. request_prompt propagates the agent's real error (rate limit, auth rejection, upstream API failure) by unwrapping single-child anyio TaskGroup BaseExceptionGroups and dropping TaskGroup __context__ noise, instead of surfacing an opaque ExceptionGroup: unhandled errors in a TaskGroup. asyncio.CancelledError is re-raised untouched.

Fixes # (n/a)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist:

  • My code follows the style guidelines of this project (ruff format & check)
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation (CHANGELOG.md [1.6.0])
  • My changes generate no new warnings (basedpyright & ty)
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes (pytest)

Verification

  • make check — ruff, ty, basedpyright all clean
  • make tests694 passed
  • make check-coverage — thresholds satisfied; pydantic_acp/client.py at 100% line + branch (added an authenticate/ensure_session/set_session_mode/raise_on_empty_turn/unwrap/CancelledError test set)
  • uv run pre-commit run --all-files — all hooks pass, including version-ahead-of-PyPI
  • Synchronized 1.6.0 bump via bump.sh (version files + root extras) + CHANGELOG entry

🤖 Generated with Claude Code

…agate real agent errors

Hardens the pydantic-acp ACP client/provider so an unusable ACP agent fails
legibly instead of silently, and adds the session-bootstrap surface the bridge
was missing.

Added
- Auth recovery: AcpProvider captures initialize's auth_methods and, when
  session/new is rejected with auth_required (-32000), runs the ACP
  authenticate flow (an advertised method, or an explicit auth_method_id=) and
  retries session creation once. Such agents were previously unrecoverable.
- Public session bootstrap: AcpProvider.ensure_session() and
  AcpProvider.set_session_mode() bootstrap a session and select a session mode
  without sending a prompt turn, so callers no longer reach into the private
  _ensure_session.
- Opt-in AcpProvider(raise_on_empty_turn=True) raises UnexpectedModelBehavior
  with an ACP-specific diagnostic when a text-output turn yields no visible
  text. Default False preserves the existing empty-response contract.

Fixed
- request_prompt propagates the agent's real error (rate limit, auth rejection,
  upstream API failure) by unwrapping single-child anyio TaskGroup
  BaseExceptionGroups and dropping TaskGroup __context__ noise, instead of
  surfacing an opaque "ExceptionGroup: unhandled errors in a TaskGroup".

Synchronized workspace bump 1.5.0 -> 1.6.0 with CHANGELOG entry. pydantic-acp
client.py is at 100% line and branch coverage; make check (ruff + ty +
basedpyright), make tests, make check-coverage, and pre-commit all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CweysdtvG97d28xrbeF9BM
@fswair

fswair commented Jul 24, 2026

Copy link
Copy Markdown
Member

I'll check today, thanks for contribution.

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.

2 participants