Skip to content

Commit a234fa9

Browse files
chore(deps): bump the minor-and-patch group across 1 directory with 3 updates (#55)
* chore(deps): bump the minor-and-patch group across 1 directory with 3 updates --- updated-dependencies: - dependency-name: agent-client-protocol dependency-version: 0.10.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: aiohttp dependency-version: 3.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: typer dependency-version: 0.26.5 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> * 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. * fix(acp): validate session and mode in set_session_mode; align auth test Address CodeRabbit review on #55: - set_session_mode now returns invalid_params for an unknown session or a non-default mode instead of relying on an assert (which is stripped under python -O and silently reported success). - Align the auth-error test fixture's terminal-auth args with the real payload the server now advertises (['login']). --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mohamed Elkholy <melkholy@techmatrix.com>
1 parent 27578a6 commit a234fa9

10 files changed

Lines changed: 297 additions & 186 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",

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ classifiers = [
2222
"Topic :: Utilities",
2323
]
2424
dependencies = [
25-
"agent-client-protocol==0.8.0",
25+
"agent-client-protocol==0.10.1",
2626
"aiofiles>=24.0,<26.0",
27-
"aiohttp==3.13.5",
28-
"typer==0.21.1",
27+
"aiohttp==3.14.0",
28+
"typer==0.26.5",
2929
"pythinker-core[contrib]==1.2.0",
3030
# notify-py (via batrachian-toad) caps loguru at <=0.6.0 on 3.14+.
3131
"loguru>=0.6.0,<0.7",

src/pythinker_code/acp/server.py

Lines changed: 84 additions & 43 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,29 @@ 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:
352-
assert mode_id == "default", "Only default mode is supported"
367+
async def set_session_mode(
368+
self, mode_id: str, session_id: str, **kwargs: Any
369+
) -> acp.schema.SetSessionModeResponse | None:
370+
if session_id not in self.sessions:
371+
raise acp.RequestError.invalid_params({"session_id": "Session not found"})
372+
if mode_id != "default":
373+
raise acp.RequestError.invalid_params({"mode_id": "Only `default` mode is supported"})
374+
return None
375+
376+
async def close_session(
377+
self, session_id: str, **kwargs: Any
378+
) -> acp.schema.CloseSessionResponse | None:
379+
"""Drop a session from the in-memory registry (ACP 0.10 session/close)."""
380+
logger.info("Closing session: {id}", id=session_id)
381+
self.sessions.pop(session_id, None)
382+
return None
383+
384+
async def set_config_option(
385+
self, config_id: str, session_id: str, value: str | bool, **kwargs: Any
386+
) -> acp.schema.SetSessionConfigOptionResponse | None:
387+
"""Pythinker advertises no session config options, so none can be set."""
388+
logger.warning("Unsupported session config option: {id}", id=config_id)
389+
raise acp.RequestError.invalid_params({"config_id": "Unknown config option"})
353390

354391
async def set_session_model(self, model_id: str, session_id: str, **kwargs: Any) -> None:
355392
logger.info(
@@ -440,7 +477,11 @@ async def authenticate(self, method_id: str, **kwargs: Any) -> acp.AuthenticateR
440477
raise acp.RequestError.invalid_params({"method_id": "Unknown auth method"})
441478

442479
async def prompt(
443-
self, prompt: list[ACPContentBlock], session_id: str, **kwargs: Any
480+
self,
481+
prompt: list[ACPContentBlock],
482+
session_id: str,
483+
message_id: str | None = None,
484+
**kwargs: Any,
444485
) -> acp.PromptResponse:
445486
logger.info("Received prompt request for session: {id}", id=session_id)
446487
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

0 commit comments

Comments
 (0)