From 9b38a3b3335bbb06fdb8e42599a0c35ae3737a11 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 18 Jul 2026 14:10:04 +0800 Subject: [PATCH] ci/lint: prek commit gateway (ruff check/format + basedpyright) Add prek.toml so git commit runs ruff-pre-commit (fix then format) and pdm-based basedpyright. Document install/install-hook; clear type warnings that made basedpyright exit non-zero under recommended mode. --- CLAUDE.md | 7 ++++++- README.md | 10 +++++++++- prek.toml | 27 ++++++++++++++++++++++++++ src/plyngent/agent/compact.py | 8 +++----- src/plyngent/agent/responses_bridge.py | 15 ++++---------- src/plyngent/agent/todo_stack.py | 3 ++- src/plyngent/cli/slash.py | 22 ++++++--------------- tests/test_agent/test_loop.py | 5 +++-- 8 files changed, 60 insertions(+), 37 deletions(-) create mode 100644 prek.toml diff --git a/CLAUDE.md b/CLAUDE.md index 8f5e0d3..6c86d20 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,14 @@ pdm run ruff format . # apply formatting pdm run ruff format --check . # CI: fail if unformatted (do not skip) pdm run basedpyright . # type checking (basedpyright, "recommended" strictness) pdm run pytest # tests (pytest-asyncio auto mode) + +# Commit gateway (prek — https://prek.j178.dev/): ruff check/format + basedpyright +uv tool install prek # once +prek install # wire git pre-commit hook (once per clone) +prek run --all-files # run all hooks on demand ``` -GitHub Actions runs `ruff check`, `ruff format --check`, `basedpyright`, then `pytest`. Format locally with `pdm run ruff format .` so CI does not fail after publish. +GitHub Actions runs `ruff check`, `ruff format --check`, `basedpyright`, then `pytest`. Local commits run the same checks via `prek.toml` so `ruff format` is not forgotten. ## Architecture diff --git a/README.md b/README.md index 00dfafc..6503bb6 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,15 @@ pdm run basedpyright . pdm run pytest ``` -CI fails if sources are not Ruff-formatted (`ruff format --check`). Run `pdm run ruff format .` before push. +**Commit gateway** ([prek](https://prek.j178.dev/)): runs ruff check + format and basedpyright on `git commit` so format is not forgotten. + +```bash +uv tool install prek # once +prek install # once per clone (installs .git/hooks/pre-commit) +prek run --all-files # run all hooks on demand +``` + +Config: `prek.toml`. CI still runs the same checks in GitHub Actions. ## Basic usage diff --git a/prek.toml b/prek.toml new file mode 100644 index 0000000..18a32a0 --- /dev/null +++ b/prek.toml @@ -0,0 +1,27 @@ +# Commit gateway via prek (https://prek.j178.dev/) — drop-in pre-commit compatible. +# Install: uv tool install prek (or: pipx install prek) +# Wire git: prek install +# Manual: prek run --all-files + +default_install_hook_types = ["pre-commit"] +default_stages = ["pre-commit"] +default_language_version = { python = "python3.14" } + +# Ruff: lint (with autofix) then format — order matters (fix before format). +# https://docs.astral.sh/ruff/integrations/#pre-commit +[[repos]] +repo = "https://github.com/astral-sh/ruff-pre-commit" +rev = "v0.15.22" +hooks = [ + { id = "ruff-check", args = ["--fix"], types_or = ["python", "pyi"] }, + { id = "ruff-format", types_or = ["python", "pyi"] }, +] + +# basedpyright: full-project typecheck (same as CI `pdm run basedpyright .`). +# Mirror hook: https://docs.basedpyright.com/latest/installation/prek-hook/ +# Uses system+PDM so imports resolve from the project venv (isolated hook envs lack deps). +[[repos]] +repo = "local" +hooks = [ + { id = "basedpyright", name = "basedpyright", language = "system", entry = "pdm run basedpyright .", pass_filenames = false, files = "\\.pyi?$" }, +] diff --git a/src/plyngent/agent/compact.py b/src/plyngent/agent/compact.py index 942a893..fcc14e8 100644 --- a/src/plyngent/agent/compact.py +++ b/src/plyngent/agent/compact.py @@ -10,7 +10,6 @@ from plyngent.lmproto.openai_compatible.model import ( ChatCompletionsParam, DeveloperChatMessage, SystemChatMessage, - ToolChatMessage, UserChatMessage, ) @@ -44,7 +43,7 @@ _SEED_MESSAGE_TEMPLATE = ( ) -def format_transcript(messages: Sequence[AnyChatMessage]) -> str: # noqa: C901 +def format_transcript(messages: Sequence[AnyChatMessage]) -> str: """Render messages as plain text for a summarization prompt.""" lines: list[str] = [] for msg in messages: @@ -65,10 +64,9 @@ def format_transcript(messages: Sequence[AnyChatMessage]) -> str: # noqa: C901 lines.append(f"[assistant tool_call] {call.function.name}({call.function.arguments})") else: lines.append(f"[assistant tool_call] custom id={call.id}") - elif isinstance(msg, ToolChatMessage): - lines.append(f"[tool {msg.tool_call_id}] {msg.content}") else: - lines.append(f"[message] {msg!r}") + # ToolChatMessage (remaining AnyChatMessage arm) + lines.append(f"[tool {msg.tool_call_id}] {msg.content}") return "\n".join(lines) diff --git a/src/plyngent/agent/responses_bridge.py b/src/plyngent/agent/responses_bridge.py index 7b1ac40..e2342b7 100644 --- a/src/plyngent/agent/responses_bridge.py +++ b/src/plyngent/agent/responses_bridge.py @@ -33,7 +33,6 @@ from plyngent.lmproto.openai_compatible.model import ( StreamFunctionDelta, StreamToolCallDelta, SystemChatMessage, - ToolChatMessage, ToolFunctionItem, UserChatMessage, ) @@ -106,17 +105,14 @@ def chat_messages_to_responses_input( items.append(ResponseEasyInputMessage(role="user", content=message.content)) elif isinstance(message, AssistantChatMessage): items.extend(_assistant_to_input_items(message)) - elif isinstance(message, ToolChatMessage): + else: + # ToolChatMessage (remaining AnyChatMessage arm) items.append( ResponseFunctionToolCallOutput( call_id=message.tool_call_id, output=message.content, ) ) - else: - content = getattr(message, "content", None) - if isinstance(content, str) and content: - items.append(ResponseEasyInputMessage(role="user", content=content)) instructions = "\n\n".join(instructions_parts) if instructions_parts else None return instructions, items @@ -173,13 +169,10 @@ def responses_status_to_finish_reason( status_s = status if isinstance(status, str) else None if status_s == "incomplete": details = response.incomplete_details - reason = None if details is not UNSET and details is not None: raw_reason = details.reason - if raw_reason is not UNSET and isinstance(raw_reason, str): - reason = raw_reason - if reason == "content_filter": - return "content_filter" + if raw_reason is not UNSET and raw_reason == "content_filter": + return "content_filter" return "length" if status_s in {"failed", "cancelled"}: return status_s diff --git a/src/plyngent/agent/todo_stack.py b/src/plyngent/agent/todo_stack.py index 3c528d5..b8de99b 100644 --- a/src/plyngent/agent/todo_stack.py +++ b/src/plyngent/agent/todo_stack.py @@ -95,7 +95,8 @@ class TodoStack: for frame in cast("list[object]", frames_raw): if not isinstance(frame, dict): continue - frame_items = msgspec.convert(frame.get("items"), type=list[TodoItem]) + frame_map = cast("dict[str, object]", frame) + frame_items = msgspec.convert(frame_map.get("items"), type=list[TodoItem]) items.extend(frame_items) return cls(TodoStackData(items=items, next_id=next_id)) except msgspec.ValidationError, TypeError, ValueError: diff --git a/src/plyngent/cli/slash.py b/src/plyngent/cli/slash.py index b4aacea..03aac9b 100644 --- a/src/plyngent/cli/slash.py +++ b/src/plyngent/cli/slash.py @@ -17,7 +17,6 @@ from plyngent.lmproto.openai_compatible.model import ( AssistantFunctionToolCall, DeveloperChatMessage, SystemChatMessage, - ToolChatMessage, UserChatMessage, ) from plyngent.runtime import ProviderNotSupportedError @@ -36,7 +35,7 @@ _YOLO_MODE_CHOICES = ("on", "off", "once") _EXPORT_FORMAT_CHOICES = ("md", "json") -class HistoryLimitType(click.ParamType): +class HistoryLimitType(click.ParamType[int]): """``N`` (int >= 1) or the shortcut ``last`` (equivalent to ``1``).""" name: str = "history_limit" @@ -1100,15 +1099,9 @@ def _print_history_message_full(index: int, message: AnyChatMessage) -> None: if isinstance(message, AssistantChatMessage): _print_history_assistant_full(index, message) return - if isinstance(message, ToolChatMessage): - click.secho(f"{index}. tool({message.tool_call_id}):", fg="magenta") - click.echo(message.content or "") - click.echo() - return - role = getattr(message, "role", type(message).__name__) - content = getattr(message, "content", "") - click.echo(f"{index}. {role}:") - click.echo(str(content)) + # ToolChatMessage (remaining AnyChatMessage arm) + click.secho(f"{index}. tool({message.tool_call_id}):", fg="magenta") + click.echo(message.content or "") click.echo() @@ -1134,11 +1127,8 @@ def _format_history_message(index: int, message: AnyChatMessage) -> str: parts.append(f"tool_calls=[{', '.join(names)}]") body = " ".join(parts) if parts else "(empty)" return f"{index}. assistant: {body}" - if isinstance(message, ToolChatMessage): - return f"{index}. tool({message.tool_call_id}): {_preview_content(message.content)}" - role = getattr(message, "role", type(message).__name__) - content = getattr(message, "content", "") - return f"{index}. {role}: {_preview_content(str(content))}" + # ToolChatMessage + return f"{index}. tool({message.tool_call_id}): {_preview_content(message.content)}" def _run_slash_argv(args: Sequence[str], state: ReplState) -> None: diff --git a/tests/test_agent/test_loop.py b/tests/test_agent/test_loop.py index 1462384..2e9b212 100644 --- a/tests/test_agent/test_loop.py +++ b/tests/test_agent/test_loop.py @@ -30,6 +30,7 @@ from plyngent.lmproto.openai_compatible.model import ( ChatCompletionsParam, ChunkChoice, DeltaMessage, + FinishReason, StreamFunctionDelta, StreamToolCallDelta, ToolChatMessage, @@ -174,8 +175,8 @@ class ScriptedClient: def _response( message: AssistantChatMessage, *, - usage: dict[str, int] | None = None, - finish_reason: str | None = "stop", + usage: dict[str, object] | None = None, + finish_reason: FinishReason | None = "stop", ) -> ChatCompletionResponse: return ChatCompletionResponse( id="1",