fix(types): declare AIAgent instance attributes as class-level annotations

init_agent() in agent/agent_init.py sets ~176 instance attributes on the
AIAgent instance, but ty cannot track cross-module attribute assignment
through a function that receives the instance as a plain `agent` parameter.
This caused 186 unresolved-attribute errors in run_agent.py alone.

Declaring the attributes as class-level annotations (PEP 526) tells ty
they exist, clearing 182 of 190 errors (190→8). The remaining 8 are:
- 2 duck-typed object.function accesses (hasattr-guarded, safe)
- 2 missing attrs (_current_tool, _api_call_count — added)
- 2 None-narrowing on iteration_budget (.used, .max_total)
- 2 None-narrowing on client (.close)

Overall ty diagnostics: 4,648 → 4,422 (−226)
Tests: 496 passed, 0 failed
This commit is contained in:
ethernet 2026-07-17 14:59:20 -04:00
parent 169bfe20e4
commit 0cb42c10a4
2 changed files with 281 additions and 0 deletions

View file

@ -174,3 +174,90 @@ the pattern is always identical: `param: Type = None` → `param: Type | None =
two runtime-breaking gotchas found during the sweep:
1. lowercase `callable` (builtin function) → must be `Callable` (typing)
2. string forward-refs can't use `| None` → must use `Optional["Ref"]`
---
## run_agent.py — unresolved-attribute analysis (190 errors)
### category 1: "Self@<method>" — init_agent() cross-module init (186/190)
these are ALL the same root cause: `AIAgent.__init__` in `run_agent.py` is a
thin forwarder that calls `init_agent(self, ...)` from `agent/agent_init.py`.
that function sets `self.model`, `self.provider`, `self.session_id`, etc. on
the instance — but ty can't see across the module boundary that these
attributes are being set. so every method that accesses `self.model`,
`self.provider`, etc. gets flagged.
**these are NOT bugs.** the attributes are correctly set at runtime by
`init_agent()`. this is a ty limitation — it can't track attribute assignments
made in a function defined in a different module that receives `self` as a
regular parameter (not as a method on the class).
example:
```python
# run_agent.py
class AIAgent:
def __init__(self, model: str = "", ...):
from agent.agent_init import init_agent
init_agent(self, model=model, ...) # sets self.model, self.provider, etc.
def _resolved_api_call_timeout(self):
return self.provider # ty: "Self@_resolved_api_call_timeout has no attribute provider"
```
**fix approach:** this is the single biggest cluster of ty errors in the
codebase (186 in run_agent.py alone, plus similar in other files). the cleanest
fix would be to declare the attributes as class-level annotations in the
`AIAgent` class body so ty knows they exist:
```python
class AIAgent:
# Instance attributes — set by init_agent() in agent/agent_init.py
model: str
provider: str | None
session_id: str | None
# ... etc
```
this is a one-time declaration that would clear ~186 errors instantly. it's
also good documentation — currently there's no single place that lists all
instance attributes; they're scattered across the 60-param init_agent() body.
### category 2: object.function — duck-typed tool_calls access (2 errors)
line 1968: `tc.function.name` and `tc.function.arguments` on objects typed as
`object`. this is because `msg.tool_calls` is checked via `hasattr` +
`isinstance(list)` but the list elements are `object` to ty.
```python
if hasattr(msg, "tool_calls") and isinstance(msg.tool_calls, list) and msg.tool_calls:
tool_calls_data = [
{"name": tc.function.name, "arguments": tc.function.arguments}
for tc in msg.tool_calls
]
```
**not a bug** — this is duck-typed access to OpenAI SDK objects. the `hasattr`
guard makes it safe at runtime. ty just can't infer the element type of a
list accessed via `hasattr`. could be fixed with a `type: ignore` or by
narrowing `msg` to a proper type.
### category 3: ~AlwaysFalsy.on_session_end — duck-typed context_compressor (2 errors)
lines 3416, 3441: `self.context_compressor.on_session_end(...)` after a
`hasattr(self, "context_compressor") and self.context_compressor` guard.
```python
if hasattr(self, "context_compressor") and self.context_compressor:
self.context_compressor.on_session_end(self.session_id or "", messages or [])
```
ty narrows `self.context_compressor` to `~AlwaysFalsy` (the truthy branch of
the `and`) but doesn't know it has `on_session_end`. **not a bug** — the
`hasattr` guard makes this safe. again a type-system limitation with
duck-typed access.
### verdict for run_agent.py: 0 real bugs, 190 type-system limitations
all 190 are ty being unable to track either cross-module attribute init
(186) or duck-typed hasattr-guarded access (4). no logic bugs found.

View file

@ -400,6 +400,200 @@ class AIAgent:
for AI models that support function calling.
"""
# -----------------------------------------------------------------------
# Instance attributes — set by init_agent() in agent/agent_init.py.
# Ideally, we could refactor this class into smaller parts so that
# we don't need to split its __init__ into a separate file.
# Declared here so type checkers know they exist (init_agent receives
# the instance as a plain `agent` parameter, not as a method receiver,
# so cross-module attribute assignment is invisible to static analysis).
# -----------------------------------------------------------------------
# scalars
_anthropic_api_key: str
_anthropic_base_url: str | None
_budget_exhausted_injected: bool
_budget_grace_call: bool
_cache_ttl: str
_cached_system_prompt: str | None
_chat_id: str | None
_chat_name: str | None
_chat_type: str | None
_client_kwargs: dict
_codex_reasoning_replay_enabled: bool
_compression_feasibility_checked: bool
_credits_latch: dict
_current_streamed_assistant_text: str
_delegate_depth: int
_end_session_on_close: bool
_executing_tools: bool
_fallback_activated: bool
_fallback_chain: list
_fallback_index: int
_force_ascii_payload: bool
_interrupt_requested: bool
_interrupt_thread_signal_pending: bool
_is_anthropic_oauth: bool
_is_user_initiated_turn: bool
_iters_since_skill: int
_last_flushed_db_idx: int
_memory_enabled: bool
_memory_nudge_interval: int
_memory_write_context: str
_memory_write_origin: str
_parent_session_id: str | None
_persist_disabled: bool
_primary_runtime: dict
_session_db_created: bool
_session_init_model_config: dict
_session_json_enabled: bool
_skill_nudge_interval: int
_skip_mcp_refresh: bool
_stream_needs_break: bool
_stream_writer_dropped: int
_stream_writer_token: int
_thread_id: str | None
_tool_snapshot_generation: int
_turns_since_memory: int
_user_name: str | None
_user_profile_enabled: bool
_user_turn_count: int
acp_command: str | None
api_key: str | None
api_mode: str | None
ephemeral_system_prompt: str | None
lmstudio_load_mode: str
load_soul_identity: bool
log_prefix: str
log_prefix_chars: int
max_iterations: int
max_tokens: int | None
memory_notifications: str
model: str
pass_session_id: bool
platform: str | None
provider: str | None
provider_data_collection: str | None
provider_require_parameters: bool
provider_sort: str | None
quiet_mode: bool
save_trajectories: bool
service_tier: str | None
session_api_calls: int
session_cache_read_tokens: int
session_cache_write_tokens: int
session_completion_tokens: int
session_cost_source: str
session_cost_status: str
session_estimated_cost_usd: float
session_id: str | None
session_input_tokens: int
session_output_tokens: int
session_prompt_tokens: int
session_reasoning_tokens: int
session_total_tokens: int
show_commentary: bool
skip_context_files: bool
suppress_status_output: bool
tool_delay: float
tool_progress_mode: str
verbose_logging: bool
# callbacks
clarify_callback: Callable | None
event_callback: Optional[Callable[[str, dict], None]]
interim_assistant_callback: Callable | None
notice_callback: Callable | None
notice_clear_callback: Callable | None
reaction_callback: Optional[Callable[[str], None]]
read_terminal_callback: Callable | None
reasoning_callback: Callable | None
status_callback: Callable | None
step_callback: Callable | None
stream_delta_callback: Callable | None
thinking_callback: Callable | None
tool_complete_callback: Callable | None
tool_gen_callback: Callable | None
tool_progress_callback: Callable | None
tool_start_callback: Callable | None
# collections
acp_args: list[str] | None
disabled_toolsets: List[str] | None
enabled_toolsets: List[str] | None
prefill_messages: List[Dict[str, Any]] | None
providers_allowed: List[str] | None
providers_ignored: List[str] | None
providers_order: List[str] | None
reasoning_config: Dict[str, Any] | None
request_overrides: Dict[str, Any] | None
# internal / runtime state
_active_children: list
_active_children_lock: Any
_anthropic_client: Any | None
_anthropic_image_fallback_cache: Any
_api_max_retries: Any
_aux_compression_context_length_config: Any | None
_base_url_hostname: Any
_bedrock_guardrail_config: Any | None
_bedrock_region: Any
_checkpoint_mgr: Any
_client_lock: Any
_compression_threshold_autoraised: Any | None
_compression_warning: Any | None
_config_context_length: Any
_credential_pool: Any
_credits_session_start_micros: Any | None
_credits_state: Any | None
_custom_providers: Any
_environment_probe: Any
_execution_thread_id: Any
_fallback_model: Any
_gateway_session_key: Any
_intent_ack_continuation: Any
_interrupt_message: Any
_kanban_worker_guidance: Any
_memory_manager: Any | None
_memory_store: Any | None
_ollama_num_ctx: Any
_parallel_tool_call_guidance: Any
_pending_cli_user_message: Any | None
_pending_steer_lock: Any
_persist_user_message_idx: Any | None
_persist_user_message_override: Any | None
_persist_user_message_timestamp: Any | None
_platform_hint_overrides: Any
_print_fn: Any | None
_session_db: Any
_session_persist_lock: Any
_stream_callback: Any | None
_stream_context_scrubber: Any
_stream_think_scrubber: Any
_stream_writer_lock: Any
_stream_writer_tls: Any
_subdirectory_hints: Any
_task_completion_guidance: Any
_todo_store: Any
_tool_guardrails: Any
_tool_use_enforcement: Any
_tool_worker_threads_lock: Any
_user_id: Any
_user_id_alt: Any
background_review_callback: Any
client: Any | None
codex_app_server_auto_compaction: Any
compression_enabled: Any
compression_in_place: Any
context_compressor: Any
iteration_budget: Optional[IterationBudget]
logs_dir: Any
openrouter_min_coding_score: Optional[float]
session_start: Any
tools: Any
valid_tool_names: Any
_current_tool: Any
_api_call_count: int
_TOOL_CALL_ARGUMENTS_CORRUPTION_MARKER = (
"[hermes-agent: tool call arguments were corrupted in this session and "
"have been dropped to keep the conversation alive. See issue #15236.]"