Skip to content

Commit efdd391

Browse files
committed
fix(deps): adapt to agent-client-protocol 0.10 and typer 0.26
The minor-and-patch bump pulls in two effectively-breaking 0.x upgrades: - Align the pythinker-review typer pin to 0.26.5 so the uv workspace resolves (root required 0.26.5 while the member still pinned 0.21.1). - Migrate the ACP server to the 0.10 auth schema: AuthMethod is gone, replaced by the typed TerminalAuthMethod. ACP 0.10 drops the per-method command field by design (the client invokes the agent binary directly for security), so only args/env/type are advertised. - Conform to the expanded 0.10 Agent protocol: additional_directories on the session methods, message_id on prompt, new close_session and set_config_option, and updated return types. - Restore optional-value parsing for --session/--resume. Typer 0.26 reimplemented option parsing with a parser that always consumes the next token as the value, so the old click _flag_needs_value trick was dead code; normalise argv before parsing instead. - Track typer's vendored click Command type in the lazy command group.
1 parent ed6b80f commit efdd391

8 files changed

Lines changed: 192 additions & 99 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ GitHub Releases page; `0.8.0` is the new starting line.
1818
- **Release packaging keeps SDK/core pins in lockstep.** The SDK's `pythinker-core`
1919
dependency is now updated by release automation and checked by CI/release validation,
2020
preventing no-sources binary builds from resolving against a stale core pin.
21+
- **Routine dependency bumps with the breaking-change fallout fixed.** Upgrades `agent-client-protocol` to 0.10.1, `aiohttp` to 3.14.0, and `typer` to 0.26.5. Aligns the `pythinker-review` `typer` pin so the uv workspace resolves; migrates the ACP server to the 0.10 auth schema (`TerminalAuthMethod`) and expanded `Agent` protocol (`additional_directories`, `close_session`, session config options); and restores the optional-value behaviour of `--session`/`--resume` (interactive picker when used without an ID) under Typer 0.26's new argument parser.
2122

2223
## 0.29.0 (2026-06-01)
2324

docs/en/release-notes/changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ GitHub Releases page; `0.8.0` is the new starting line.
1717

1818
## Unreleased
1919

20+
- **Release packaging keeps SDK/core pins in lockstep.** The SDK's `pythinker-core`
21+
dependency is now updated by release automation and checked by CI/release validation,
22+
preventing no-sources binary builds from resolving against a stale core pin.
23+
- **Routine dependency bumps with the breaking-change fallout fixed.** Upgrades `agent-client-protocol` to 0.10.1, `aiohttp` to 3.14.0, and `typer` to 0.26.5. Aligns the `pythinker-review` `typer` pin so the uv workspace resolves; migrates the ACP server to the 0.10 auth schema (`TerminalAuthMethod`) and expanded `Agent` protocol (`additional_directories`, `close_session`, session config options); and restores the optional-value behaviour of `--session`/`--resume` (interactive picker when used without an ID) under Typer 0.26's new argument parser.
24+
2025
## 0.29.0 (2026-06-01)
2126

2227
### What changed in this release

packages/pythinker-review/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ classifiers = [
2020
"Topic :: Security",
2121
]
2222
dependencies = [
23-
"typer==0.21.1",
23+
"typer==0.26.5",
2424
"pydantic>=2.12.5",
2525
"pyyaml==6.0.3",
2626
"rich==15.0.0",

src/pythinker_code/acp/server.py

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import asyncio
4-
import sys
54
import time
65
from datetime import datetime
76
from pathlib import Path
@@ -27,14 +26,19 @@
2726
from pythinker_code.thinking import DEFAULT_THINKING_EFFORT, effective_config_thinking_effort
2827
from pythinker_code.utils.logging import logger
2928

29+
# ACP 0.10 types `auth_methods` as a discriminated union of auth-method variants.
30+
ACPAuthMethod = (
31+
acp.schema.EnvVarAuthMethod | acp.schema.TerminalAuthMethod | acp.schema.AuthMethodAgent
32+
)
33+
3034

3135
class ACPServer:
3236
def __init__(self) -> None:
3337
self.client_capabilities: acp.schema.ClientCapabilities | None = None
3438
self.conn: acp.Client | None = None
3539
self.sessions: dict[str, tuple[ACPSession, _ModelIDConv]] = {}
3640
self.negotiated_version: ACPVersionSpec | None = None
37-
self._auth_methods: list[acp.schema.AuthMethod] = []
41+
self._auth_methods: list[ACPAuthMethod] = []
3842

3943
def on_connect(self, conn: acp.Client) -> None:
4044
logger.info("ACP client connected")
@@ -67,32 +71,22 @@ async def initialize(
6771
version=getattr(client_info, "version", None),
6872
)
6973

70-
# get command and args of current process for terminal-auth
71-
command = sys.argv[0]
72-
args: list[str] = []
73-
74-
# Build terminal auth data for error response
75-
terminal_args = args + ["login"]
74+
# Build the terminal-auth args; the client re-runs the agent command
75+
# with these args to complete login in the terminal.
76+
terminal_args = ["login"]
7677

7778
# Build and cache auth methods for reuse in AUTH_REQUIRED errors
7879
self._auth_methods = [
79-
acp.schema.AuthMethod(
80+
acp.schema.TerminalAuthMethod(
8081
id="login",
8182
name="Login with Pythinker account",
8283
description=(
8384
"Run `pythinker login` command in the terminal, "
8485
"then follow the instructions to finish login."
8586
),
86-
# Store auth data in field_meta for building AUTH_REQUIRED error
87-
field_meta={
88-
"terminal-auth": {
89-
"command": command,
90-
"args": terminal_args,
91-
"label": "Pythinker Login",
92-
"env": {},
93-
"type": "terminal",
94-
}
95-
},
87+
type="terminal",
88+
args=terminal_args,
89+
env={},
9690
),
9791
]
9892

@@ -155,26 +149,28 @@ def _check_auth(self, config: Config | None = None) -> None:
155149
self._check_config_auth(config) if config is not None else self._check_token_usable()
156150
)
157151
if reason:
158-
auth_methods_data: list[dict[str, Any]] = []
159-
for m in self._auth_methods:
160-
if m.field_meta and "terminal-auth" in m.field_meta:
161-
terminal_auth = m.field_meta["terminal-auth"]
162-
auth_methods_data.append(
163-
{
164-
"id": m.id,
165-
"name": m.name,
166-
"description": m.description,
167-
"type": terminal_auth.get("type", "terminal"),
168-
"args": terminal_auth.get("args", []),
169-
"env": terminal_auth.get("env", {}),
170-
}
171-
)
152+
auth_methods_data: list[dict[str, Any]] = [
153+
{
154+
"id": m.id,
155+
"name": m.name,
156+
"description": m.description,
157+
"type": m.type,
158+
"args": m.args or [],
159+
"env": m.env or {},
160+
}
161+
for m in self._auth_methods
162+
if isinstance(m, acp.schema.TerminalAuthMethod)
163+
]
172164

173165
logger.warning("Authentication required, {reason}", reason=reason)
174166
raise acp.RequestError.auth_required({"authMethods": auth_methods_data})
175167

176168
async def new_session(
177-
self, cwd: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
169+
self,
170+
cwd: str,
171+
additional_directories: list[str] | None = None,
172+
mcp_servers: list[MCPServer] | None = None,
173+
**kwargs: Any,
178174
) -> acp.NewSessionResponse:
179175
logger.info("Creating new session for working directory: {cwd}", cwd=cwd)
180176
assert self.conn is not None, "ACP client not connected"
@@ -281,22 +277,33 @@ async def _setup_session(
281277
return acp_session, model_id_conv
282278

283279
async def load_session(
284-
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
285-
) -> None:
280+
self,
281+
cwd: str,
282+
session_id: str,
283+
additional_directories: list[str] | None = None,
284+
mcp_servers: list[MCPServer] | None = None,
285+
**kwargs: Any,
286+
) -> acp.schema.LoadSessionResponse | None:
286287
logger.info("Loading session: {id} for working directory: {cwd}", id=session_id, cwd=cwd)
287288

288289
if session_id in self.sessions:
289290
logger.warning("Session already loaded: {id}", id=session_id)
290-
return
291+
return None
291292

292293
# Check authentication before loading session
293294
self._check_auth(load_config())
294295

295296
await self._setup_session(cwd, session_id, mcp_servers)
296297
# TODO: replay session history?
298+
return None
297299

298300
async def resume_session(
299-
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
301+
self,
302+
cwd: str,
303+
session_id: str,
304+
additional_directories: list[str] | None = None,
305+
mcp_servers: list[MCPServer] | None = None,
306+
**kwargs: Any,
300307
) -> acp.schema.ResumeSessionResponse:
301308
logger.info("Resuming session: {id} for working directory: {cwd}", id=session_id, cwd=cwd)
302309

@@ -323,12 +330,21 @@ async def resume_session(
323330
)
324331

325332
async def fork_session(
326-
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
333+
self,
334+
cwd: str,
335+
session_id: str,
336+
additional_directories: list[str] | None = None,
337+
mcp_servers: list[MCPServer] | None = None,
338+
**kwargs: Any,
327339
) -> acp.schema.ForkSessionResponse:
328340
raise NotImplementedError
329341

330342
async def list_sessions(
331-
self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
343+
self,
344+
additional_directories: list[str] | None = None,
345+
cursor: str | None = None,
346+
cwd: str | None = None,
347+
**kwargs: Any,
332348
) -> acp.schema.ListSessionsResponse:
333349
logger.info("Listing sessions for working directory: {cwd}", cwd=cwd)
334350
if cwd is None:
@@ -348,8 +364,26 @@ async def list_sessions(
348364
next_cursor=None,
349365
)
350366

351-
async def set_session_mode(self, mode_id: str, session_id: str, **kwargs: Any) -> None:
367+
async def set_session_mode(
368+
self, mode_id: str, session_id: str, **kwargs: Any
369+
) -> acp.schema.SetSessionModeResponse | None:
352370
assert mode_id == "default", "Only default mode is supported"
371+
return None
372+
373+
async def close_session(
374+
self, session_id: str, **kwargs: Any
375+
) -> acp.schema.CloseSessionResponse | None:
376+
"""Drop a session from the in-memory registry (ACP 0.10 session/close)."""
377+
logger.info("Closing session: {id}", id=session_id)
378+
self.sessions.pop(session_id, None)
379+
return None
380+
381+
async def set_config_option(
382+
self, config_id: str, session_id: str, value: str | bool, **kwargs: Any
383+
) -> acp.schema.SetSessionConfigOptionResponse | None:
384+
"""Pythinker advertises no session config options, so none can be set."""
385+
logger.warning("Unsupported session config option: {id}", id=config_id)
386+
raise acp.RequestError.invalid_params({"config_id": "Unknown config option"})
353387

354388
async def set_session_model(self, model_id: str, session_id: str, **kwargs: Any) -> None:
355389
logger.info(
@@ -440,7 +474,11 @@ async def authenticate(self, method_id: str, **kwargs: Any) -> acp.AuthenticateR
440474
raise acp.RequestError.invalid_params({"method_id": "Unknown auth method"})
441475

442476
async def prompt(
443-
self, prompt: list[ACPContentBlock], session_id: str, **kwargs: Any
477+
self,
478+
prompt: list[ACPContentBlock],
479+
session_id: str,
480+
message_id: str | None = None,
481+
**kwargs: Any,
444482
) -> acp.PromptResponse:
445483
logger.info("Received prompt request for session: {id}", id=session_id)
446484
if session_id not in self.sessions:

src/pythinker_code/cli/_lazy_group.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import click
88
import typer
99
from click.core import HelpFormatter
10+
from typer._click.core import Command as _TyperCommand # typer 0.26 vendors its own click
1011
from typer.main import get_command
1112

1213

@@ -62,30 +63,46 @@ class LazySubcommandGroup(typer.core.TyperGroup):
6263
"web",
6364
)
6465

65-
# Click options that support optional values. When the flag is present
66-
# without a following argument the parser returns the mapped *flag_value*
67-
# instead of raising "requires an argument".
68-
_optional_value_options: dict[str, str] = {
69-
"session_id": "", # --session / --resume without value → picker mode
70-
}
66+
# `--session`/`--resume` accept an *optional* value: with an ID they resume
67+
# that session, without one they open the interactive picker. Typer 0.26
68+
# reimplemented option parsing with a parser that always consumes the next
69+
# token as the value (no optional-value support), so we normalise argv before
70+
# parsing: when one of these flags is used without a usable value (it is the
71+
# last token, or is followed by another option) we inject an empty-string
72+
# sentinel that the root callback maps to picker mode.
73+
_optional_value_flags: frozenset[str] = frozenset({"--session", "--resume", "-S", "-r"})
7174

7275
def make_context(
7376
self, info_name: str | None, args: list[str], parent: click.Context | None = None, **extra
7477
) -> click.Context:
75-
for param in self.params:
76-
if isinstance(param, click.Option) and param.name in self._optional_value_options:
77-
param._flag_needs_value = True
78-
param.flag_value = self._optional_value_options[param.name]
78+
args = self._inject_optional_value_sentinels(args)
7979
return super().make_context(info_name, args, parent=parent, **extra)
8080

81+
def _inject_optional_value_sentinels(self, args: list[str]) -> list[str]:
82+
"""Insert an empty-string value after optional-value flags used without one."""
83+
result: list[str] = []
84+
seen_terminator = False
85+
for i, arg in enumerate(args):
86+
result.append(arg)
87+
if seen_terminator:
88+
continue
89+
if arg == "--":
90+
seen_terminator = True
91+
continue
92+
if arg in self._optional_value_flags:
93+
nxt = args[i + 1] if i + 1 < len(args) else None
94+
if nxt is None or nxt.startswith("-"):
95+
result.append("")
96+
return result
97+
8198
def list_commands(self, ctx: click.Context) -> list[str]:
8299
commands = list(super().list_commands(ctx))
83100
for name in self.lazy_command_order:
84101
if name not in commands:
85102
commands.append(name)
86103
return commands
87104

88-
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
105+
def get_command(self, ctx: click.Context, cmd_name: str) -> _TyperCommand | None:
89106
command = super().get_command(ctx, cmd_name)
90107
if command is not None:
91108
return command

src/pythinker_code/ui/acp/__init__.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,50 @@ async def initialize(
3535
self._raise()
3636

3737
async def new_session(
38-
self, cwd: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
38+
self,
39+
cwd: str,
40+
additional_directories: list[str] | None = None,
41+
mcp_servers: list[MCPServer] | None = None,
42+
**kwargs: Any,
3943
) -> acp.NewSessionResponse:
4044
self._raise()
4145

4246
async def load_session(
43-
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
44-
) -> None:
47+
self,
48+
cwd: str,
49+
session_id: str,
50+
additional_directories: list[str] | None = None,
51+
mcp_servers: list[MCPServer] | None = None,
52+
**kwargs: Any,
53+
) -> acp.schema.LoadSessionResponse | None:
4554
self._raise()
4655

4756
async def resume_session(
48-
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
57+
self,
58+
cwd: str,
59+
session_id: str,
60+
additional_directories: list[str] | None = None,
61+
mcp_servers: list[MCPServer] | None = None,
62+
**kwargs: Any,
4963
) -> acp.schema.ResumeSessionResponse:
5064
self._raise()
5165

5266
async def fork_session(
53-
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
67+
self,
68+
cwd: str,
69+
session_id: str,
70+
additional_directories: list[str] | None = None,
71+
mcp_servers: list[MCPServer] | None = None,
72+
**kwargs: Any,
5473
) -> acp.schema.ForkSessionResponse:
5574
self._raise()
5675

5776
async def list_sessions(
58-
self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
77+
self,
78+
additional_directories: list[str] | None = None,
79+
cursor: str | None = None,
80+
cwd: str | None = None,
81+
**kwargs: Any,
5982
) -> acp.schema.ListSessionsResponse:
6083
self._raise()
6184

@@ -64,6 +87,16 @@ async def set_session_mode(
6487
) -> acp.SetSessionModeResponse | None:
6588
self._raise()
6689

90+
async def close_session(
91+
self, session_id: str, **kwargs: Any
92+
) -> acp.schema.CloseSessionResponse | None:
93+
self._raise()
94+
95+
async def set_config_option(
96+
self, config_id: str, session_id: str, value: str | bool, **kwargs: Any
97+
) -> acp.schema.SetSessionConfigOptionResponse | None:
98+
self._raise()
99+
67100
async def set_session_model(
68101
self, model_id: str, session_id: str, **kwargs: Any
69102
) -> acp.SetSessionModelResponse | None:
@@ -73,7 +106,11 @@ async def authenticate(self, method_id: str, **kwargs: Any) -> acp.AuthenticateR
73106
self._raise()
74107

75108
async def prompt(
76-
self, prompt: list[ACPContentBlock], session_id: str, **kwargs: Any
109+
self,
110+
prompt: list[ACPContentBlock],
111+
session_id: str,
112+
message_id: str | None = None,
113+
**kwargs: Any,
77114
) -> acp.PromptResponse:
78115
self._raise()
79116

0 commit comments

Comments
 (0)