From 542f1cb17e800f30cb9bd624ab7a27f048f203c4 Mon Sep 17 00:00:00 2001 From: Federico Kamelhar Date: Sat, 2 May 2026 12:39:58 -0400 Subject: [PATCH] chore(types): mypy strict for locus.loop.* and locus.reasoning.* (#34 batch 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts ``locus.loop.*`` (~1.3k LOC across runner / router / nodes / react) and ``locus.reasoning.*`` (~3k LOC across causal / gsar / gsar_judge / gsar_evaluator / grounding / reflexion) out of the mypy ``ignore_errors`` blanket so they are now type-checked under the project's strict defaults. Specific fixes: - ``reasoning.causal._detect_bidirectional``: build the pair tuple with explicit ``(str, str)`` typing so ``set[tuple[str, str]].add`` type-checks (was inferred as the wider ``tuple[str, ...]``). - ``reasoning.gsar_judge.StructuredOutputGSARJudge.judge``: cast the ``StructuredOutput.parsed`` field (typed as ``BaseModel | None``) back to ``JudgeOutput`` since the dynamic type is pinned by the schema we passed to ``parse_structured``. - ``loop.router.RouterWithCustomConditions.route``: bind the ``model_copy`` result to a ``RouteDecision`` local so the return isn't ``Any``. - ``loop.nodes.ToolNode``: declare the local ``events`` list with the same union as ``NodeResult.events`` so ``list``-invariance doesn't reject the otherwise-correct return. - ``loop.runner``: drop a stale ``# type: ignore[assignment]``. ``locus.hooks.builtin.*`` is intentionally held back. The migration surfaces a real correctness gap — the built-in hooks (LoggingHook / GuardrailsHook / TelemetryHook) carry method signatures from a pre-event ``HookProvider`` API and would TypeError when dispatched through ``HookRegistry.emit_*``. That is a behavioural fix and is tracked on its own follow-up PR. Remaining batches: ``locus.rag.*``, ``locus.memory.*``, ``locus.hooks.builtin.*``. Signed-off-by: Federico Kamelhar --- pyproject.toml | 8 ++++++-- src/locus/loop/nodes.py | 5 ++++- src/locus/loop/router.py | 3 ++- src/locus/loop/runner.py | 2 +- src/locus/reasoning/causal.py | 3 ++- src/locus/reasoning/gsar_judge.py | 6 ++++-- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 88dd508a..664ce424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -530,9 +530,13 @@ ignore_missing_imports = true module = [ "locus.rag.*", "locus.memory.*", - "locus.loop.*", + # ``locus.hooks.builtin.*`` is held back: the module-level migration + # surfaces a real runtime bug — the LoggingHook / GuardrailsHook / + # TelemetryHook signatures pre-date the event-based ``HookProvider`` + # Protocol and would TypeError when called through ``HookRegistry``. + # That is a behavioural fix, not a typing one, so it's tracked on + # its own issue and migrated in a follow-up PR. "locus.hooks.builtin.*", - "locus.reasoning.*", ] ignore_errors = true diff --git a/src/locus/loop/nodes.py b/src/locus/loop/nodes.py index 8e93049e..5a2e22cd 100644 --- a/src/locus/loop/nodes.py +++ b/src/locus/loop/nodes.py @@ -164,7 +164,10 @@ async def execute(self, state: AgentState) -> NodeResult: from locus.tools.executor import ToolResult tool_calls = state.last_tool_calls - events: list[ToolStartEvent | ToolCompleteEvent] = [] + # ``NodeResult.events`` is invariantly typed as the broader event + # union; declare the local list with that union so the eventual + # ``return NodeResult(events=events)`` type-checks. + events: list[ThinkEvent | ToolStartEvent | ToolCompleteEvent | ReflectEvent] = [] if not tool_calls: return NodeResult(state=state, events=[]) diff --git a/src/locus/loop/router.py b/src/locus/loop/router.py index a24990ed..cef446ef 100644 --- a/src/locus/loop/router.py +++ b/src/locus/loop/router.py @@ -229,9 +229,10 @@ def route(self, current_node: NodeType, state: AgentState) -> RouteDecision: try: result = condition(state) if result is not None: - return result.model_copy( + updated: RouteDecision = result.model_copy( update={"metadata": {**result.metadata, "custom_condition": name}} ) + return updated except Exception: # noqa: BLE001 # Custom condition failed, continue with others continue diff --git a/src/locus/loop/runner.py b/src/locus/loop/runner.py index 69d9bb09..7707c14e 100644 --- a/src/locus/loop/runner.py +++ b/src/locus/loop/runner.py @@ -238,7 +238,7 @@ def collect(self, event: LoopEvent) -> None: elif event.event_type == "reflect": self.reflect_events.append(event) elif event.event_type == "terminate": - self.terminate_event = event # type: ignore[assignment] + self.terminate_event = event @property def is_complete(self) -> bool: diff --git a/src/locus/reasoning/causal.py b/src/locus/reasoning/causal.py index 16f4e87b..1f3674c4 100644 --- a/src/locus/reasoning/causal.py +++ b/src/locus/reasoning/causal.py @@ -504,7 +504,8 @@ def _detect_bidirectional(self) -> list[CausalConflict]: if not edge.is_causal: continue - pair = tuple(sorted([edge.source_id, edge.target_id])) + ordered = sorted([edge.source_id, edge.target_id]) + pair: tuple[str, str] = (ordered[0], ordered[1]) if pair in checked: continue checked.add(pair) diff --git a/src/locus/reasoning/gsar_judge.py b/src/locus/reasoning/gsar_judge.py index 10833dce..aec137d9 100644 --- a/src/locus/reasoning/gsar_judge.py +++ b/src/locus/reasoning/gsar_judge.py @@ -26,7 +26,7 @@ from __future__ import annotations -from typing import Any, Protocol, runtime_checkable +from typing import Any, Protocol, cast, runtime_checkable from pydantic import BaseModel, Field, model_validator @@ -317,7 +317,9 @@ async def judge( content = response.message.content or "{}" parsed = parse_structured(content, JudgeOutput, strict=False) if parsed.success and parsed.parsed is not None: - return parsed.parsed + # ``parse_structured`` returns ``BaseModel | None``; the schema + # we passed in pins the dynamic type to ``JudgeOutput``. + return cast("JudgeOutput", parsed.parsed) return safe_default_judge_output(f"judge output parse failed: {parsed.error or 'unknown'}")