Skip to content

Conversation

@rayokamoto
Copy link
Contributor

@rayokamoto rayokamoto commented Feb 12, 2026

Summary by CodeRabbit

  • Type System Enhancements

    • Centralized domain types, enums, and typed payloads introduced for consistent identifiers and data shapes.
  • API / Backward Compatibility

    • Public APIs, DTOs, and route responses now use explicit domain types while preserving existing behavior.
  • Environments & Robotics

    • Environment/step/observation/task payloads and generators use richer typed structures and enums (images, step info, object/task types).
  • Documentation

    • AGENTS guide expanded with setup, commands, services, style, and examples.
  • CLI / Tests

    • CLI flows adjusted for async/robust outputs; tests updated to use new types and annotations.

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Centralizes domain/types in kinitro/types.py and propagates strongly-typed NewType/TypedDict/Enum aliases across the codebase, updating many public signatures, DTOs, storage, scheduler/scoring, environments, RL interface, CLI, executor/validator, chain/weights, and tests to use these types and small control-flow updates where necessary.

Changes

Cohort / File(s) Summary
Type System Foundation
kinitro/types.py
New centralized public types: NewType aliases (MinerUID, EnvironmentId, BlockNumber, TaskUUID, Hotkey, Seed), TypedDicts (EncodedImage, StepInfo, ParsedCommitment, MinerScoreData, TaskCreateData, etc.), Enums (EnvironmentFamily, ObjectType, SubsetWeightScheme), NamedTuples (FeasibilityResult, EligibilityResult), Protocol (AffinetesEnv), and helper env_family_from_id.
Public API Models & Storage
kinitro/backend/models.py, kinitro/backend/storage.py
Replaced primitive fields with domain types (MinerUID, EnvironmentId, TaskUUID, Seed, Hotkey) across Pydantic models and storage method signatures; TaskPool stats and weight payload shapes updated to typed structures.
API Routes
kinitro/api/routes/health.py, kinitro/api/routes/miners.py, kinitro/api/routes/scores.py, kinitro/api/routes/tasks.py, kinitro/api/routes/weights.py
Added explicit return types and moved route payload construction to use domain wrappers (MinerUID, Hotkey, EnvironmentId, TaskUUID, Seed, TaskStatus).
Scheduler & Scoring
kinitro/scheduler/*, kinitro/scoring/*
Aggregate/weighting logic migrated to MinerScores/MinerThresholds/MinerFirstBlocks types; functions now use MinerUID keys and EnvironmentId env ids; subset weighting uses enum SubsetWeightScheme; compute_weights/aggregate signatures changed.
Chain & Weights
kinitro/chain/commitments.py, kinitro/chain/weights.py
Commitment parsing and MinerCommitment dataclass now use typed aliases; weight conversion, eligibility checks, and set_weights return/accept typed payloads and EligibilityResult.
Environments & RL
kinitro/environments/*, kinitro/rl_interface.py
Step/observation and robot-state types switched to StepInfo, EncodedImage, RobotStateDict; TaskSpec/SceneObject use ObjectType enum; multiple env templates and generators updated to typed signatures and some match-case refactors.
Executor, Validator & Worker
kinitro/executor/*, kinitro/validator/*, kinitro/scheduler/main.py
Executor/worker/validator updated to use AffinetesEnv, EnvironmentId, MinerUID, Hotkey; subtensor/wallet initialization moved to eager paths in scheduler/validator; fetch_tasks/create_tasks/use typed IDs.
Task generation & Scheduler tasks
kinitro/scheduler/task_generator.py
generate_seed, generate_tasks and return payloads use Seed, BlockNumber, EnvironmentId and produce typed TaskCreateData.
CLI & Utilities
kinitro/cli/*, pyproject.toml
Replaced some lazy bittensor imports with direct Subtensor/Wallet imports; small CLI string-splitting tweaks (rsplit) and dependency bound updates in pyproject.
Executor helpers & env loader
kinitro/executor/env_loader.py, kinitro/executor/config.py, kinitro/executor/worker.py, kinitro/executor/verification.py
Return/param types and internal annotations switched to AffinetesEnv, EnvironmentId, MinerUID, Hotkey, VerificationDetails; typed assertions added for env initialization.
Environments templates
environments/_template/env.py, environments/genesis/env.py, environments/metaworld/env.py, environments/*
list_environments/evaluate/_get_env signatures changed to use EnvironmentId and RoboticsEnvironment; evaluate now requires/propagates base_url in some envs.
Genesis family updates
kinitro/environments/genesis/*
Many genesis internals updated: RobotStateDict, EncodedImage, StepInfo, ObjectType usage, match-case dispatch, and typing of scene/task generators updated.
CLI commands & miner tools
kinitro/cli/*
Commands updated to pass EnvironmentId where appropriate; crypto/miner CLI now uses direct Subtensor/Wallet imports.
Tests
tests/unit/*
Massive test updates: explicit return type annotations and test data migrated to new types (MinerUID, EnvironmentId, BlockNumber, ObjectType, ProceduralTaskResult, etc.).
Docs / Agents
AGENTS.md
Extensive documentation and developer guidance additions (setup, typing guidance, APIs, multi-worktree notes, services, validators, scoring/incentives, change hygiene).

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • Shr1ftyy
  • rishiad

Poem

🐰
I hopped through types with nimble paws,
Wrapped UIDs and envs in tidy laws.
Enums and dicts now safe and bright,
Typed carrots glowing in the night,
A rabbit applauds — code feels right!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: improve typing throughout the codebase' accurately summarizes the main objective of the changeset, which introduces comprehensive typing improvements across multiple modules.
Docstring Coverage ✅ Passed Docstring coverage is 94.78% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/typing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
kinitro/environments/genesis/scene_generator.py (1)

130-143: ⚠️ Potential issue | 🔴 Critical

ObjectType enum stringifies as "ObjectType.BOX", not "box" — breaks object_id format.

ObjectType extends Enum (not str, Enum), so f"{obj_type}" produces "ObjectType.BOX" instead of "box". The generated object_id will change from e.g. "obj_00_red_box" to "obj_00_red_ObjectType.BOX", which may break downstream consumers that parse or match on the id.

Use obj_type.value in the f-string:

🐛 Proposed fix
             objects.append(
                 SceneObjectConfig(
-                    object_id=f"obj_{i:02d}_{color_name}_{obj_type}",
+                    object_id=f"obj_{i:02d}_{color_name}_{obj_type.value}",
                     object_type=obj_type,
kinitro/environments/registry.py (1)

106-116: ⚠️ Potential issue | 🔴 Critical

Use .value to handle EnvironmentFamily enum members correctly in f-string formatting.

When family is an EnvironmentFamily enum member, f"{family}/" produces "EnvironmentFamily.METAWORLD/" in Python 3.12 (not "metaworld/"), causing the startswith() check to fail. This breaks the type contract since the function signature explicitly accepts str | EnvironmentFamily. Use .value to ensure enum members are properly converted:

Proposed fix
 def get_environments_by_family(family: str | EnvironmentFamily) -> list[EnvironmentId]:
-    return [EnvironmentId(env_id) for env_id in ENVIRONMENTS if env_id.startswith(f"{family}/")]
+    family_prefix = family.value if isinstance(family, EnvironmentFamily) else family
+    return [EnvironmentId(env_id) for env_id in ENVIRONMENTS if env_id.startswith(f"{family_prefix}/")]
🤖 Fix all issues with AI agents
In `@kinitro/chain/commitments.py`:
- Line 322: committed_block can be None when passed into BlockNumber, which
silently allows None into an int-typed field; update the code that constructs
BlockNumber(committed_block) (from the _query_commitment_by_hotkey result or
neuron.last_update) to guard against None: check the returned committed_block
for None and either skip adding that commitment record or supply a safe
default/raise, e.g., only call BlockNumber(...) and use the commitment if
committed_block is not None, otherwise omit the commitment entry (or explicitly
handle the None case according to domain semantics).

In `@kinitro/environments/genesis/task_types.py`:
- Line 129: The error message uses the ObjectType enum directly so f-string
interpolation shows the enum repr (e.g., "ObjectType.BOX"); update the message
construction in the FeasibilityResult return(s) to use target.object_type.value
instead of target.object_type (and similarly for the other occurrence around
line 137) so the message reads the plain value (e.g., "box") — adjust the
f-strings where FeasibilityResult(False, f"Object {target.object_type} is not
{prop}") appears to use .value.

In `@kinitro/scoring/winners_take_all.py`:
- Around line 178-191: Hoist the conversion of subset_weight_scheme to a
SubsetWeightScheme once before the for loop (avoid re-creating `scheme` each
iteration) and move only the computation that depends on `subset_size` into the
loop; use the `SubsetWeightScheme` enum (or symbol `subset_weight_scheme`) to
resolve the scheme once, then inside the loop compute `subset_weight` based on
`subset_size`. Also make the match/case exhaustive by adding a wildcard `case
_:` that assigns a safe default or raises a clear error so `subset_weight` is
always defined (refer to the names `SubsetWeightScheme`, `subset_weight_scheme`,
`scheme`, `subset_size`, and `subset_weight` to locate the change).

In `@kinitro/types.py`:
- Around line 91-103: TaskCreateData currently uses TypedDict with total=False
which makes every key optional; change TaskCreateData to use the default
total=True and mark only optional fields with NotRequired (import NotRequired
from typing), i.e. keep required keys (cycle_id, miner_uid, miner_hotkey,
miner_endpoint, env_id, seed) as regular annotations and mark task_uuid,
miner_repo, miner_revision as NotRequired[...] so create_tasks_bulk (which
indexes required keys directly and only .get() for the three optional fields)
matches the type definition.
🧹 Nitpick comments (14)
kinitro/environments/genesis/envs/g1_humanoid.py (1)

61-92: Missing default case _ branch in _compute_reward.

_check_success (line 129) has a case _: return False fallback, but _compute_reward silently returns only the alive bonus for an unrecognized TaskType. Adding a default case (even if it's just a pass or a log) would keep both methods consistent and make it easier to catch new task types that haven't been wired up yet.

Suggested fix
             case TaskType.PUSH:
                 obj_pos = object_states.get(task_spec.target_object_id)
                 dest_pos = task_spec.destination_position
                 if obj_pos is not None and dest_pos is not None:
                     dist_to_dest = float(np.linalg.norm(obj_pos[:2] - np.array(dest_pos[:2])))
                     reward += max(0.0, 1.0 - dist_to_dest / 5.0) * 0.1
+
+            case _:
+                pass  # Unknown task type; only alive bonus applies
 
         return reward
kinitro/environments/metaworld_env.py (1)

584-603: Type mismatch: MetaWorld's info dict is assigned directly to StepInfo.

On Line 590, env.step(mw_action) returns MetaWorld's own info dict (a plain dict[str, Any]), which is unpacked into info: StepInfo. While this works at runtime (TypedDicts are just dicts), MetaWorld's info may contain keys not declared in StepInfo. Since the env is already accessed via cast(Any, ...), the type checker won't catch this, but it's worth noting that the returned StepInfo may carry extra undeclared keys.

Not blocking — just a heads-up for future type-strictness efforts.

kinitro/executor/api_client.py (1)

39-43: Consider simplifying the union type.

list[str] | list[EnvironmentId] is redundant since EnvironmentId is a NewType over str — a list[EnvironmentId] already satisfies list[str] at runtime. You could simplify to list[EnvironmentId] | None to encourage callers to adopt the new type.

That said, keeping both during a transitional period is understandable.

environments/metaworld/env.py (1)

62-66: Consider typing env_id parameters as EnvironmentId for consistency.

list_environments now returns list[EnvironmentId], but evaluate, _run_evaluation, _get_env, and _build_error_result still annotate env_id as str. Since EnvironmentId is a NewType(str), this is not a bug, but applying it here would be more consistent with the PR's goals.

Also applies to: 99-111, 179-191, 317-325

kinitro/scoring/threshold.py (1)

43-43: episodes_per_env dict variant still typed as dict[str, int] rather than dict[EnvironmentId, int].

Since env_id keys in miner_scores are now EnvironmentId, the dict form of episodes_per_env should match for consistency.

Suggested fix
 def compute_miner_thresholds(
     miner_scores: MinerScores,
-    episodes_per_env: int | dict[str, int],
+    episodes_per_env: int | dict[EnvironmentId, int],
     z_score: float = 1.5,
     min_gap: float = 0.02,
     max_gap: float = 0.10,
 ) -> MinerThresholds:
kinitro/rl_interface.py (1)

153-154: Consider narrowing the list type in the union.

EncodedImage | list leaves the list element type unspecified. If the legacy format is always nested numeric lists, a narrower annotation (e.g., list[list[list[int]]] or at minimum list[Any]) would be more self-documenting, though this is a minor nit given the backward-compat intent.

kinitro/scheduler/main.py (1)

257-257: Redundant EnvironmentId() wrapping — self.env_ids is already list[EnvironmentId].

self.env_ids is assigned as list[EnvironmentId] (line 44) or from get_all_environment_ids() which also returns list[EnvironmentId] (line 66). The comprehension [EnvironmentId(e) for e in self.env_ids] is a no-op re-wrap.

♻️ Simplify
             weights, weights_u16 = compute_weights(
                 miner_scores=miner_scores,
-                env_ids=[EnvironmentId(e) for e in self.env_ids],
+                env_ids=self.env_ids,
                 episodes_per_env=self.config.episodes_per_env,
kinitro/api/routes/miners.py (1)

61-63: Minor: env_ids is already list[EnvironmentId], so the wrapping in the comprehension is a no-op.

get_all_environment_ids() returns list[EnvironmentId], making EnvironmentId(env_id) on line 62 (and again on lines 79, 84) redundant. Harmless at runtime since NewType is transparent, but it adds noise.

kinitro/scoring/pareto.py (1)

102-106: n_samples_per_env dict key type inconsistent with EnvironmentId.

env_ids is now list[EnvironmentId], but n_samples_per_env accepts dict[str, int]. For type-checker consistency, this should be dict[EnvironmentId, int] to match the keys used in samples.get(env_ids[j], 50) on line 152.

Proposed fix
 def compute_pareto_frontier(
     miner_scores: MinerScores,
     env_ids: list[EnvironmentId],
-    n_samples_per_env: int | dict[str, int],
+    n_samples_per_env: int | dict[EnvironmentId, int],
 ) -> ParetoResult:
kinitro/scheduler/scoring.py (1)

171-175: miners_by_uid value type should be Hotkey instead of str.

The comment says uid -> hotkey, and the values are used to populate the hotkey field of MinerScoreData (which expects Hotkey). For consistency with the broader typing effort:

Proposed fix
 def convert_to_scores_data(
     miner_scores: MinerScores,
-    miners_by_uid: dict[MinerUID, str],  # uid -> hotkey
+    miners_by_uid: dict[MinerUID, Hotkey],  # uid -> hotkey
     episodes_per_env: int,
 ) -> list[MinerScoreData]:

This would also require adding Hotkey to the import on line 14.

kinitro/environments/genesis/base.py (1)

751-751: Narrow the local type annotations to match actual usage.

The rgb and depth dicts in _build_observation only store EncodedImage values from encode_image() calls; the | list part is unnecessary here. While the Observation class itself defines these fields with | list, the local variables can safely use a narrower type that reflects what's actually stored.

Proposed simplification
-        rgb: dict[str, EncodedImage | list] = {}
+        rgb: dict[str, EncodedImage] = {}
-        depth: dict[str, EncodedImage | list] = {}
+        depth: dict[str, EncodedImage] = {}
kinitro/types.py (2)

1-14: Well-structured centralized type module.

Clean separation with zero intra-project imports, clear docstring, and proper import grouping. One minor note: importing numpy at the module level means every consumer of even simple aliases like MinerUID or EnvironmentId transitively imports NumPy. If startup time or lightweight consumers ever become a concern, the NumPy-dependent TypedDicts (RobotStateDict, ProceduralTaskResult) could be split into a separate sub-module.


155-161: dict[str, object] is overly restrictive for a randomization bag.

object is the top type but disallows attribute access and most operations without casting. If these values are truly heterogeneous (floats, lists, bools, etc.), Any is the pragmatic choice here — or a float | bool | list[float] union if the set of possible types is known. As per coding guidelines, Any should include a comment explaining why.

kinitro/scoring/winners_take_all.py (1)

152-153: Consider using the enum member as the default instead of a raw string.

The default "linear" works because of the isinstance + conversion logic, but using SubsetWeightScheme.LINEAR directly communicates intent more clearly and avoids the conversion path when the default is used.

Proposed fix
-    subset_weight_scheme: str | SubsetWeightScheme = "linear",
+    subset_weight_scheme: str | SubsetWeightScheme = SubsetWeightScheme.LINEAR,

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
kinitro/environments/genesis/scene_generator.py (1)

130-151: ⚠️ Potential issue | 🟠 Major

Bug: ObjectType enum renders as "ObjectType.BOX" in the f-string, changing object_id format.

ObjectType extends Enum (not str, Enum), so f"{obj_type}" produces "ObjectType.BOX" instead of the previous "box". This changes the generated object_id (e.g., "obj_00_red_ObjectType.BOX" instead of "obj_00_red_box"), which may break downstream logic that parses or matches these IDs.

🐛 Proposed fix: use `.value` for the string representation
             objects.append(
                 SceneObjectConfig(
-                    object_id=f"obj_{i:02d}_{color_name}_{obj_type}",
+                    object_id=f"obj_{i:02d}_{color_name}_{obj_type.value}",
                     object_type=obj_type,
🤖 Fix all issues with AI agents
In `@kinitro/chain/commitments.py`:
- Around line 466-468: The code assumes subtensor.set_commitment() returns an
object with a .success attribute but in bittensor 8.0.0 it returns a boolean;
update the handling in set_commitment (look for the variable result and the line
success = result.success) to treat result as a bool (e.g., set success = result)
and remove any subsequent attribute access on result so the function correctly
processes the boolean return value.

In `@kinitro/chain/weights.py`:
- Around line 81-95: subtensor.set_weights returns a tuple (success, message)
not an object, so change the code that assigns and checks result to unpack the
tuple (e.g., success, message = subtensor.set_weights(...)) instead of accessing
result.success; then use success for the if/return and include message in the
failure log (e.g., logger.error("weights_set_failed", message=message)) so the
actual error message surfaces rather than being swallowed.

In `@kinitro/cli/crypto_commands.py`:
- Line 8: Update the import so Wallet is imported from the separate package and
Subtensor remains from bittensor: replace the combined import of Wallet and
Subtensor with an import that gets Wallet from the bittensor_wallet package and
Subtensor from bittensor (or use bittensor as bt and access bt.Subtensor);
update any references in this module to use the new import if namespaced
differently; and add bittensor-wallet to the project dependencies (e.g.,
requirements or pyproject) so Wallet is available at runtime.
- Line 242: The code assumes subtensor.set_commitment() returns an object with a
.success attribute but in bittensor 8.0.0 it returns a boolean; update the
assignment where result is set by subtensor.set_commitment(...) so that you use
the boolean directly (replace accessing result.success with success = result) —
adjust the handling around the result variable in the function that calls
subtensor.set_commitment to remove the .success access and treat result as a
bool.

In `@kinitro/cli/miner/deploy.py`:
- Line 7: The import line using "from bittensor import Subtensor, Wallet" is
incompatible with bittensor v8.0.0; replace it by importing bittensor as bt and
importing Wallet from the separate bittensor_wallet package, i.e., use "import
bittensor as bt" and "from bittensor_wallet import Wallet", then update any
direct references to Subtensor to bt.Subtensor (e.g., where Subtensor(...) is
used) and ensure the project dependencies include bittensor-wallet so the new
Wallet import resolves.

In `@kinitro/validator/main.py`:
- Around line 42-46: Validator.__init__ currently instantiates Subtensor
synchronously (self.subtensor = Subtensor(...)), which blocks the event loop
because run_validator calls the constructor from async code; replace this by
deferring the blocking work: either instantiate AsyncSubtensor and call its
awaitable initialize() from run_validator (or an async factory on Validator), or
move the Subtensor(...) call onto a worker thread with asyncio.to_thread and
assign the result to self.subtensor so the event loop is not blocked; update
Validator.__init__/run_validator to use the chosen approach and ensure any
subsequent uses of self.subtensor await initialize() or run after the to_thread
completion.
🧹 Nitpick comments (12)
kinitro/environments/genesis/envs/g1_humanoid.py (1)

61-91: Missing default case in _compute_reward match-case.

_check_success (line 129) includes a case _: return False default branch, but _compute_reward has no corresponding default. If a new TaskType is added to the enum, this match will silently skip all task-specific reward terms without any signal. Adding a default here for symmetry — even if it just passes — improves clarity and helps catch future omissions.

Suggested fix
             case TaskType.PUSH:
                 obj_pos = object_states.get(task_spec.target_object_id)
                 dest_pos = task_spec.destination_position
                 if obj_pos is not None and dest_pos is not None:
                     dist_to_dest = float(np.linalg.norm(obj_pos[:2] - np.array(dest_pos[:2])))
                     reward += max(0.0, 1.0 - dist_to_dest / 5.0) * 0.1
+
+            case _:
+                pass  # Only alive bonus for unknown task types
 
         return reward
environments/genesis/env.py (1)

103-105: Consider tightening the AffinetesEnv protocol return type.

The AffinetesEnv protocol in kinitro/types.py (line 172) declares list_environments as -> Any, but all concrete implementations (genesis/env.py, metaworld/env.py, template) now return list[EnvironmentId]. Updating the protocol to match would provide better type safety.

kinitro/executor/api_client.py (1)

42-42: Consider simplifying the union type for env_ids.

list[str] | list[EnvironmentId] prevents mixed lists. If callers might pass a heterogeneous list, list[str | EnvironmentId] | None is more permissive. If the intent is to eventually migrate all callers to EnvironmentId, consider just list[EnvironmentId] | None — since EnvironmentId is a NewType over str, callers can wrap with EnvironmentId(...) at the boundary.

environments/metaworld/env.py (1)

343-345: Missing -> None return type on cleanup.

Public method is missing its return type annotation. As per coding guidelines, "Add return types to public functions and methods."

Proposed fix
-    async def cleanup(self):
+    async def cleanup(self) -> None:
kinitro/scheduler/task_generator.py (1)

95-107: Task dict values are not wrapped with the new typed constructors, unlike every other file in this PR.

The return type is list[TaskCreateData], which expects MinerUID, Hotkey, Seed, EnvironmentId, and TaskUUID typed values. However, the dict is constructed with raw values (miner.uid, miner.hotkey, seed as int, etc.) without wrapping them (e.g., MinerUID(miner.uid), Seed(seed), TaskUUID(task_uuid)).

While this works at runtime since NewType is erased, it defeats the purpose of this PR's type-safety improvements and a type checker like mypy --strict or pyright will flag these as errors.

♻️ Proposed fix: wrap values with typed constructors

You'd need to add Hotkey, MinerUID, Seed, TaskUUID to the import on line 9, then:

                 tasks.append(
                     {
-                        "task_uuid": task_uuid,
+                        "task_uuid": TaskUUID(task_uuid),
                         "cycle_id": cycle_id,
-                        "miner_uid": miner.uid,
-                        "miner_hotkey": miner.hotkey,
+                        "miner_uid": MinerUID(miner.uid),
+                        "miner_hotkey": Hotkey(miner.hotkey),
                         "miner_endpoint": endpoint,
                         "miner_repo": miner.huggingface_repo,
                         "miner_revision": miner.revision_sha,
                         "env_id": env_id,
-                        "seed": seed,
+                        "seed": Seed(seed),
                     }
                 )
kinitro/api/routes/miners.py (1)

61-84: Redundant EnvironmentId() wrapping — env_ids are already EnvironmentId.

get_all_environment_ids() already returns list[EnvironmentId], so env_id in the loop is already typed as EnvironmentId. The re-wrapping on lines 62, 79, and 84 is unnecessary.

♻️ Remove redundant wrapping
     env_stats: dict[EnvironmentId, EnvStatsEntry] = {
-        EnvironmentId(env_id): EnvStatsEntry(count=0, total_sr=0.0) for env_id in env_ids
+        env_id: EnvStatsEntry(count=0, total_sr=0.0) for env_id in env_ids
     }
-        stats = env_stats[EnvironmentId(env_id)]
+        stats = env_stats[env_id]
         result.append(
             EnvironmentInfo(
-                env_id=EnvironmentId(env_id),
+                env_id=env_id,
kinitro/scoring/pareto.py (1)

102-106: n_samples_per_env dict variant still uses str keys instead of EnvironmentId.

The env_ids parameter is now list[EnvironmentId], and on line 152 the code does samples.get(env_ids[j], 50) where env_ids[j] is EnvironmentId. The dict type should match for type-checker consistency.

♻️ Align the dict key type
 def compute_pareto_frontier(
     miner_scores: MinerScores,
     env_ids: list[EnvironmentId],
-    n_samples_per_env: int | dict[str, int],
+    n_samples_per_env: int | dict[EnvironmentId, int],
 ) -> ParetoResult:
kinitro/scheduler/main.py (1)

248-248: Redundant EnvironmentId wrapping — self.env_ids is already list[EnvironmentId].

self.env_ids is typed and populated as list[EnvironmentId] (lines 44, 53, 66), so re-wrapping each element is a no-op at runtime (NewType is the identity function). You can pass self.env_ids directly.

Suggested simplification
             weights, weights_u16 = compute_weights(
                 miner_scores=miner_scores,
-                env_ids=[EnvironmentId(e) for e in self.env_ids],
+                env_ids=self.env_ids,
                 episodes_per_env=self.config.episodes_per_env,
kinitro/backend/storage.py (1)

264-282: weights dict has MinerUID (int) keys but the ORM column is typed dict[str, float].

save_weights accepts dict[MinerUID, float] (line 269), but ComputedWeightsORM.weights_json is annotated as Mapped[dict[str, float]] (models.py line 124). JSONB serialization will stringify the int keys, so this works at runtime, but the type mismatch may confuse static analysis and readers.

Consider either:

  • Explicitly converting keys to strings before storing, or
  • Updating the ORM annotation to dict[int, float] to match the semantic intent.
kinitro/scheduler/scoring.py (1)

171-205: Return type annotation says list[MinerScoreData] but the function appends plain dict literals.

This works at runtime because TypedDict is structurally typed, but static type checkers (mypy/pyright) will flag the plain dict literal as incompatible with MinerScoreData unless you explicitly annotate or construct it. Consider annotating the dict literal to help the checker:

entry: MinerScoreData = { ... }
scores_data.append(entry)

This is minor since it's already correct at runtime.

kinitro/types.py (2)

164-175: Any usage in AffinetesEnv Protocol is acceptable but worth a brief inline comment.

The Any annotations on list_environments, evaluate, and **kwargs are justified since this is a structural interface for an external package (affinetes) not available at type-check time. A short # External package – exact types unknown note would satisfy the coding guideline to explain Any usage. As per coding guidelines: "Avoid Any type annotations unless required and explain why in code comments."


155-162: dict[str, object] for domain_randomization — intentional or should this be Any?

object is more restrictive than Any (no attribute access or method calls without casting). If consumers need to access values without type narrowing, Any with a comment may be more practical. If the intent is to force explicit narrowing, object is correct.

@rayokamoto rayokamoto force-pushed the feat/typing branch 2 times, most recently from 7ed57b6 to 8b42a0e Compare February 12, 2026 13:46
@rayokamoto
Copy link
Contributor Author

@coderabbitai please re-review

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

@rayokamoto I'll conduct a complete re-review of all the changes in this PR.

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
kinitro/environments/genesis/scene_generator.py (1)

130-143: ⚠️ Potential issue | 🟠 Major

Use obj_type.value in the f-string to preserve the object_id format.

ObjectType is a plain Enum, not a StrEnum. When you use f"{obj_type}" where obj_type = ObjectType.BOX, Python's __str__() returns "ObjectType.BOX" rather than "box". This changes the object_id format from the expected obj_00_red_box to obj_00_red_ObjectType.BOX.

Test fixtures hardcode the old format (object_id="obj_00_red_box", "obj_01_green_sphere", etc.) and will fail with the current code. Use obj_type.value to preserve the original format:

Proposed fix
             objects.append(
                 SceneObjectConfig(
-                    object_id=f"obj_{i:02d}_{color_name}_{obj_type}",
+                    object_id=f"obj_{i:02d}_{color_name}_{obj_type.value}",
                     object_type=obj_type,
kinitro/backend/storage.py (1)

264-282: ⚠️ Potential issue | 🟡 Minor

weights dict keys are MinerUID (int) but the JSONB column is typed dict[str, float].

ComputedWeightsORM.weights_json is Mapped[dict[str, float]] (models.py line 124), but weights here is dict[MinerUID, float] where MinerUID is NewType(..., int). JSON serialization will stringify the keys, so it works at runtime, but static type checkers will flag the assignment weights_json=weights. Consider converting keys explicitly:

Suggested fix
         weights_orm = ComputedWeightsORM(
             cycle_id=cycle_id,
             block_number=block_number,
-            weights_json=weights,
+            weights_json={str(k): v for k, v in weights.items()},
             weights_u16_json=weights_u16,
             created_at=datetime.now(timezone.utc),
         )
🤖 Fix all issues with AI agents
In `@AGENTS.md`:
- Line 121: Update the American English spelling by changing the phrase
"Centralise shared newtypes, type aliases, enums, and `TypedDict` definitions in
`kinitro/types.py`. Import from there rather than re-defining types locally." to
use "Centralize" instead of "Centralise" in AGENTS.md so it matches the rest of
the document's spelling.

In `@pyproject.toml`:
- Line 11: The dependency line currently lists "bittensor>=10.1.0", which is
unsatisfiable because 10.1.0 doesn't exist on PyPI; update the requirement in
pyproject.toml where the string "bittensor>=10.1.0" appears to a valid
constraint such as "bittensor>=10.0.1,<11.0.0" (or pin to "bittensor==10.0.1")
so installations succeed.
- Line 30: Replace the unsatisfiable dependency "typer>=0.23.0" in
pyproject.toml with a real Typer release; change the requirement to a valid
version such as "typer>=0.22.0" or pin to "typer==0.22.0" (or use a bounded
range like "typer>=0.22.0,<0.23") so dependency resolution succeeds — update the
string "typer>=0.23.0" accordingly.
🧹 Nitpick comments (17)
pyproject.toml (1)

59-65: Duplicate dev dependency sections with diverging versions.

Both [project.optional-dependencies].dev (Lines 59–65) and [dependency-groups].dev (Lines 115–121) declare dev dependencies, but with mismatched version bounds:

Package optional-dependencies dependency-groups
pytest >=9.0.0 >=9.0.2
pytest-asyncio >=0.21.0 >=1.3.0
ruff >=0.1.0 >=0.14.13
pytest-cov >=4.0.0 (missing)
ty >=0.0.14 >=0.0.14

This will cause confusion about which versions are actually required. Consider consolidating into a single source of truth — likely [dependency-groups] if your tooling supports PEP 735, or [project.optional-dependencies] otherwise.

Also applies to: 115-121

kinitro/scoring/threshold.py (1)

43-43: Consider typing episodes_per_env dict key as EnvironmentId for consistency.

Since miner_scores now uses EnvironmentId keys (via MinerScores), the dict[str, int] variant of episodes_per_env could be dict[EnvironmentId, int] to keep the domain typing consistent throughout this function's signature.

Proposed fix
 def compute_miner_thresholds(
     miner_scores: MinerScores,
-    episodes_per_env: int | dict[str, int],
+    episodes_per_env: int | dict[EnvironmentId, int],
     z_score: float = 1.5,
     min_gap: float = 0.02,
     max_gap: float = 0.10,
 ) -> MinerThresholds:
kinitro/environments/genesis/envs/g1_humanoid.py (1)

61-92: Minor: _compute_reward lacks a case _ branch, unlike _check_success.

If a new TaskType variant is added, _compute_reward silently returns only the alive bonus (0.01) without any indication, while _check_success explicitly handles the unknown case with case _: return False. Adding a symmetric case _ (even if just a pass/comment) would make the intent clear and keep the two methods consistent.

Proposed fix
             case TaskType.PUSH:
                 obj_pos = object_states.get(task_spec.target_object_id)
                 dest_pos = task_spec.destination_position
                 if obj_pos is not None and dest_pos is not None:
                     dist_to_dest = float(np.linalg.norm(obj_pos[:2] - np.array(dest_pos[:2])))
                     reward += max(0.0, 1.0 - dist_to_dest / 5.0) * 0.1
+
+            case _:
+                pass  # Unknown task type — alive bonus only
 
         return reward
environments/_template/env.py (1)

36-44: Template still has several untyped signatures — consider aligning with the genesis counterpart.

Since this PR is about improving typing, the template (__init__, _get_env, _build_error_result, evaluate) could benefit from the same treatment applied to environments/genesis/env.py (e.g., -> None, -> RoboticsEnvironment, -> EvalResult). Template code tends to be copied verbatim into new environments, so stronger types here pay forward.

kinitro/types.py (2)

164-175: AffinetesEnv.list_environments and evaluate use Any without the required justification comment.

Per the coding guidelines and AGENTS.md Line 119: "avoid Any unless required and explain why in code." All concrete implementations return list[EnvironmentId] and dict[str, Any] respectively, so at minimum list_environments could be tightened. If Any is intentional (e.g., to keep the protocol loose for third-party environments), a brief comment explaining why would satisfy the guideline.

Proposed fix (tighten where possible, comment where not)
 class AffinetesEnv(Protocol):
     """Structural interface for affinetes-managed evaluation environments.
 
     Used by the executor subsystem to interact with Docker/Basilica
     environments without depending on the ``affinetes`` package at
     type-checking time.
     """
 
     def is_ready(self) -> bool: ...
-    async def list_environments(self) -> Any: ...
-    async def evaluate(self, **kwargs: Any) -> dict[str, Any]: ...
+    async def list_environments(self) -> list[EnvironmentId]: ...
+    # evaluate() uses Any because affinetes actors accept heterogeneous kwargs
+    async def evaluate(self, **kwargs: Any) -> dict[str, Any]: ...
     async def cleanup(self) -> None: ...

As per coding guidelines: "Avoid Any type annotations unless required and explain why in code."


23-25: Consider using TypeAlias (or the Python 3.12 type statement) for the composite aliases.

Currently MinerScores, MinerThresholds, and MinerFirstBlocks are bare assignments. Type checkers handle these, but using TypeAlias (or type MinerScores = ... in 3.12) makes the intent explicit and avoids potential confusion with runtime values.

Example using Python 3.12 `type` statement
-MinerScores = dict[MinerUID, dict[EnvironmentId, float]]  # uid -> env_id -> score
-MinerThresholds = dict[MinerUID, dict[EnvironmentId, float]]  # uid -> env_id -> threshold
-MinerFirstBlocks = dict[MinerUID, BlockNumber]  # uid -> block_number
+type MinerScores = dict[MinerUID, dict[EnvironmentId, float]]  # uid -> env_id -> score
+type MinerThresholds = dict[MinerUID, dict[EnvironmentId, float]]  # uid -> env_id -> threshold
+type MinerFirstBlocks = dict[MinerUID, BlockNumber]  # uid -> block_number

Note: The type statement requires removing from __future__ import annotations or using it only in Python 3.12+. If you stay with from __future__ import annotations, use TypeAlias from typing instead.

kinitro/executor/api_client.py (1)

42-42: Consider simplifying the union type.

list[str] | list[EnvironmentId] | None could be simplified to list[EnvironmentId] | None if all callers have been migrated to use EnvironmentId. If backward compatibility with plain strings is needed, list[str | EnvironmentId] | None is more precise than two separate list types.

kinitro/environments/metaworld_env.py (1)

584-590: Minor type imprecision: MetaWorld's env.step() returns a broader dict than StepInfo.

On line 590, info is reassigned from MetaWorld's native env.step() which returns its own info dict containing keys not defined in StepInfo (e.g., near_object, grasp_success, etc.). The StepInfo annotation on line 584 is immediately overwritten and the actual runtime value won't strictly conform to StepInfo.

This is pragmatically fine since TypedDict with total=False doesn't enforce at runtime, but a # type: ignore[assignment] or a cast would make the intent explicit for type checkers.

kinitro/environments/registry.py (1)

78-78: get_environment parameter remains str while sibling functions now use EnvironmentId.

Minor inconsistency: get_all_environment_ids() and get_environments_by_family() return EnvironmentId, but get_environment() accepts plain str. Since EnvironmentId is a NewType(str), this works at runtime, but updating the signature to env_id: EnvironmentId would make the public API consistent.

kinitro/scoring/pareto.py (1)

102-106: n_samples_per_env dict key type is still str instead of EnvironmentId.

env_ids is now list[EnvironmentId] and miner_scores uses EnvironmentId keys, but n_samples_per_env on line 105 is int | dict[str, int]. For consistency (and correct type-checking when used with samples.get(env_ids[j], 50) on line 152), the dict variant should use EnvironmentId keys.

Suggested fix
 def compute_pareto_frontier(
     miner_scores: MinerScores,
     env_ids: list[EnvironmentId],
-    n_samples_per_env: int | dict[str, int],
+    n_samples_per_env: int | dict[EnvironmentId, int],
 ) -> ParetoResult:
kinitro/environments/genesis/base.py (1)

744-775: _build_observation types are mostly clean, but dict[str, EncodedImage | list] is imprecise.

Lines 751 and 755 use dict[str, EncodedImage | list] for rgb and depth. The | list arm is vague — if this is for the case where the Observation model expects an empty list as default, it would be clearer to let the dict simply be empty. If the Observation model genuinely requires list as an alternative value type, adding a brief comment would help clarify intent.

kinitro/scheduler/task_generator.py (1)

89-107: Task dict values not wrapped with corresponding NewTypes.

The return type is list[TaskCreateData], where TaskCreateData expects seed: Seed and task_uuid: TaskUUID (both NewTypes). However, generate_seed returns a plain int and str(uuid.uuid4()) returns a plain str. For consistency with the typing effort in this PR, wrap these values:

Suggested fix
+from kinitro.types import BlockNumber, EnvironmentId, Seed, TaskCreateData, TaskUUID
...
-                task_uuid = str(uuid.uuid4())
+                task_uuid = TaskUUID(str(uuid.uuid4()))
...
-                seed = generate_seed(task_uuid)
+                seed = Seed(generate_seed(task_uuid))
kinitro/scheduler/main.py (1)

248-248: Redundant EnvironmentId wrapping.

self.env_ids is already list[EnvironmentId] (assigned at line 53 or 66), so [EnvironmentId(e) for e in self.env_ids] is a no-op copy. Pass self.env_ids directly.

Suggested fix
-                env_ids=[EnvironmentId(e) for e in self.env_ids],
+                env_ids=self.env_ids,
kinitro/api/routes/miners.py (2)

61-63: Redundant EnvironmentId wrapping — values are already EnvironmentId.

get_all_environment_ids() returns list[EnvironmentId], so EnvironmentId(env_id) in the dict comprehension is unnecessary. Same applies on lines 79 and 84.

Suggested fix
     env_stats: dict[EnvironmentId, EnvStatsEntry] = {
-        EnvironmentId(env_id): EnvStatsEntry(count=0, total_sr=0.0) for env_id in env_ids
+        env_id: EnvStatsEntry(count=0, total_sr=0.0) for env_id in env_ids
     }
-        stats = env_stats[EnvironmentId(env_id)]
+        stats = env_stats[env_id]
-                env_id=EnvironmentId(env_id),
+                env_id=env_id,

29-46: O(n²) score aggregation per miner.

Lines 42-45 re-scan the full scores list for each miner to compute the average. Consider accumulating totals during the initial loop to avoid the quadratic cost as the number of miners/scores grows.

kinitro/scheduler/scoring.py (2)

171-175: miners_by_uid value type should be Hotkey, not str.

The parameter is typed dict[MinerUID, str] but MinerScoreData expects hotkey: Hotkey. The caller (main.py line 258) already passes m.hotkey which is Hotkey. Update the annotation and the fallback:

Suggested fix
 def convert_to_scores_data(
     miner_scores: MinerScores,
-    miners_by_uid: dict[MinerUID, str],  # uid -> hotkey
+    miners_by_uid: dict[MinerUID, Hotkey],  # uid -> hotkey
     episodes_per_env: int,
 ) -> list[MinerScoreData]:
-        hotkey = miners_by_uid.get(uid, "unknown")
+        hotkey = miners_by_uid.get(uid, Hotkey("unknown"))

198-204: TODO comments for mean_reward and episodes_completed/episodes_failed.

These are hardcoded to 0.0 / episodes_per_env / 0. Would you like me to generate an implementation that computes these from the actual task results, or open an issue to track it?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
kinitro/environments/registry.py (1)

106-116: ⚠️ Potential issue | 🟡 Minor

Stale docstring: "currently only 'metaworld'" — Genesis is also a registered family.

Line 111 references only 'metaworld' but the registry also contains genesis/g1-v0.

Suggested fix
-        family: Environment family (currently only 'metaworld')
+        family: Environment family (e.g., 'metaworld', 'genesis')
kinitro/environments/genesis/scene_generator.py (1)

141-151: ⚠️ Potential issue | 🔴 Critical

Bug: obj_type in f-string produces ObjectType.BOX instead of box.

str(ObjectType.BOX) yields "ObjectType.BOX", not "box". This changes object_id from e.g. "obj_00_red_box" to "obj_00_red_ObjectType.BOX", which will break any downstream code that parses or matches object IDs.

Use .value to preserve the original string representation:

🐛 Proposed fix
             objects.append(
                 SceneObjectConfig(
-                    object_id=f"obj_{i:02d}_{color_name}_{obj_type}",
+                    object_id=f"obj_{i:02d}_{color_name}_{obj_type.value}",
                     object_type=obj_type,
kinitro/scheduler/task_generator.py (1)

95-107: ⚠️ Potential issue | 🟡 Minor

Only task_uuid and seed need to be wrapped with their domain-type constructors; miner_uid and miner_hotkey are already correctly typed.

The return type is list[TaskCreateData], but the dict construction uses raw primitives for task_uuid (a str) and seed (an int). However, miner.uid and miner.hotkey are already typed as MinerUID and Hotkey respectively in the MinerCommitment class, so they do not need wrapping.

♻️ Suggested fix
+from kinitro.types import BlockNumber, EnvironmentId, Seed, TaskCreateData, TaskUUID

...
                 tasks.append(
                     {
-                        "task_uuid": task_uuid,
+                        "task_uuid": TaskUUID(task_uuid),
                         "cycle_id": cycle_id,
                         "miner_uid": miner.uid,
                         "miner_hotkey": miner.hotkey,
                         "miner_endpoint": endpoint,
                         "miner_repo": miner.huggingface_repo,
                         "miner_revision": miner.revision_sha,
                         "env_id": env_id,
-                        "seed": seed,
+                        "seed": Seed(seed),
                     }
                 )
🤖 Fix all issues with AI agents
In `@kinitro/scoring/pareto.py`:
- Around line 102-106: The type annotation for the function
compute_pareto_frontier is inconsistent: n_samples_per_env is typed as dict[str,
int] but is used with env_ids (EnvironmentId), e.g. samples.get(env_ids[j], 50);
update the annotation to use dict[EnvironmentId, int] (or Mapping[EnvironmentId,
int]) so n_samples_per_env, the local samples variable, and any related lookups
match EnvironmentId keys and satisfy type checkers.

In `@pyproject.toml`:
- Line 24: The inline comment on the "numpy>=2.0.1,<2.3" dependency incorrectly
references a non-existent bittensor version; update the comment to reference the
correct bittensor version (the actual version declared in pyproject.toml
elsewhere) and make the comment accurate and concise (e.g., replace "lower bound
from bittensor >=10.1" with the real version constraint that provides the lower
bound), leaving the dependency spec itself unchanged.
🧹 Nitpick comments (18)
tests/unit/test_crypto.py (1)

34-34: Consider adding a type annotation for the uuid_str parameter.

Since this PR is about improving typing, the parametrized argument could also be annotated for consistency.

-    def test_uuid_to_bytes(self, uuid_str) -> None:
+    def test_uuid_to_bytes(self, uuid_str: str) -> None:
kinitro/environments/registry.py (1)

78-98: get_environment still accepts str rather than EnvironmentId.

For consistency with the typed return values of get_all_environment_ids and get_environments_by_family, consider updating the env_id parameter to accept EnvironmentId. Since EnvironmentId is a NewType over str, this is a zero-cost annotation change that improves type-checker coverage.

kinitro/cli/miner/commitment.py (1)

4-5: Minor inconsistency: from bittensor import Subtensor here vs import bittensor as bt in deploy.py.

Both work, but the codebase now uses two different import styles for the same package across CLI modules. Consider aligning on one style for consistency.

kinitro/executor/api_client.py (1)

42-42: The union list[str] | list[EnvironmentId] is redundant if EnvironmentId is a NewType of str.

Since EnvironmentId = NewType("EnvironmentId", str), a list[EnvironmentId] is already assignable where list[str] is expected at runtime (NewType doesn't create a distinct runtime type). The union was likely added for backward compatibility with existing callers, which is fine, but consider simplifying to list[EnvironmentId] | None to encourage callers to adopt the stronger type.

kinitro/environments/metaworld_env.py (1)

515-518: StepInfo return type is a reasonable approximation but not fully accurate for MetaWorld's info dict.

MetaWorld's env.step() returns an info dict with extra keys beyond what StepInfo declares (e.g., near_object, grasp_success, grasp_reward). Since StepInfo uses total=False and the runtime dict is a plain dict, this works at runtime and the key callers actually use (success) is declared in StepInfo. Just be aware that strict type checkers (pyright in strict mode) may flag the extra keys if the return type is narrowed elsewhere.

kinitro/scheduler/task_generator.py (1)

14-32: generate_seed returns a plain int, not a Seed.

Since the PR introduces Seed as a NewType, consider updating the return type of generate_seed to Seed for consistency.

♻️ Suggested fix
-def generate_seed(task_uuid: str) -> int:
+def generate_seed(task_uuid: str) -> Seed:
     ...
-    return int.from_bytes(hash_bytes, byteorder="big") & 0x7FFFFFFF
+    return Seed(int.from_bytes(hash_bytes, byteorder="big") & 0x7FFFFFFF)
kinitro/environments/genesis/envs/g1_humanoid.py (1)

61-92: Consider adding a wildcard case to _compute_reward for symmetry with _check_success.

_check_success (line 129) has a case _: return False wildcard, but _compute_reward silently falls through for unknown task types, returning only the alive bonus. Adding a wildcard (even if it just passes or logs a warning) would make intent explicit and prevent silent bugs when new TaskType variants are added.

kinitro/validator/main.py (1)

70-73: Sync RPC calls (verify_weight_setting_eligibility, set_weights) still block the event loop.

Both functions perform synchronous network I/O (chain RPC) and are called directly from async methods. Consider wrapping them with asyncio.to_thread(...) for consistency with the Subtensor initialization fix. This is a pre-existing concern, not introduced by this PR.

As per coding guidelines, "Use async def for I/O and DB operations."

Also applies to: 129-134

kinitro/rl_interface.py (1)

154-155: Consider narrowing the legacy list type annotation.

EncodedImage | list uses a bare list which is very broad. If the legacy format is always a nested list of integers (pixel data), consider list[list[list[int]]] or at minimum list[Any] to be explicit. Low priority since this is a transitional type for backward compatibility.

kinitro/api/routes/miners.py (1)

61-63: Redundant EnvironmentId() wrapping — env_ids already contains EnvironmentId values.

get_all_environment_ids() returns list[EnvironmentId] (see kinitro/environments/registry.py, lines 100-102), so env_id in the loop is already an EnvironmentId. The extra EnvironmentId(env_id) calls on lines 62, 79, and 84 are no-ops at runtime but add noise.

♻️ Suggested cleanup
     env_stats: dict[EnvironmentId, EnvStatsEntry] = {
-        EnvironmentId(env_id): EnvStatsEntry(count=0, total_sr=0.0) for env_id in env_ids
+        env_id: EnvStatsEntry(count=0, total_sr=0.0) for env_id in env_ids
     }
 ...
-        stats = env_stats[EnvironmentId(env_id)]
+        stats = env_stats[env_id]
 ...
             EnvironmentInfo(
-                env_id=EnvironmentId(env_id),
+                env_id=env_id,

Also applies to: 79-79, 84-84

kinitro/scheduler/main.py (2)

248-248: Redundant EnvironmentId re-wrapping.

self.env_ids is already list[EnvironmentId] (set at line 53 or 66), so [EnvironmentId(e) for e in self.env_ids] creates a new list of identical values. You can pass self.env_ids directly.

Suggested fix
-                env_ids=[EnvironmentId(e) for e in self.env_ids],
+                env_ids=self.env_ids,

68-68: Eager Subtensor initialization performs blocking network I/O in __init__.

The previous lazy-loaded property deferred this cost. Now Subtensor(network=...) blocks during construction, which could stall the event loop if Scheduler.__init__ is ever called from an async context. Consider whether this should be deferred to start() or wrapped in asyncio.to_thread. As per coding guidelines, kinitro/**/*.py should "Use async def for I/O and DB operations."

kinitro/scoring/pareto.py (2)

7-8: Extraneous blank line after import.

Line 8 is an empty changed line between the import and the existing blank line at line 9, resulting in a double blank line.

Suggested fix
 from kinitro.types import EnvironmentId, MinerScores, MinerUID
-

 

176-192: Consider O(1) UID lookup instead of list.index().

uid_mapping.index(uid) is O(n). If these functions are called frequently (e.g., for every miner), building a reverse mapping {uid: idx} once would be more efficient. Not critical at current scale, but worth noting.

Also applies to: 195-211

kinitro/scheduler/scoring.py (1)

188-205: "unknown" default should be wrapped as Hotkey("unknown").

On Line 190, the fallback "unknown" is a plain str, but it ends up as a value in a MinerScoreData dict where the hotkey field is typed as Hotkey. Strict type checkers will flag this.

Proposed fix
-        hotkey = miners_by_uid.get(uid, "unknown")
+        hotkey = miners_by_uid.get(uid, Hotkey("unknown"))
kinitro/types.py (2)

16-25: NewType aliases and composite type aliases are clear and useful.

MinerScores, MinerThresholds, and MinerFirstBlocks as plain type aliases are convenient. For stricter type-checker support, you could annotate them with TypeAlias (e.g., MinerScores: TypeAlias = dict[...]), but this is not required.


155-161: Consider Any instead of object for domain_randomization values.

dict[str, object] prevents attribute access on values without casting, which may be cumbersome for consumers. If the contents are truly heterogeneous, Any is the pragmatic choice (with a justifying comment per coding guidelines). If the intent is to disallow arbitrary attribute access, object is fine.

kinitro/environments/genesis/base.py (1)

751-757: Bare list type annotation lacks element type information.

The type dict[str, EncodedImage | list] uses bare list, which is overly broad. The Observation model defines the same type for backward compatibility with legacy nested list formats (see decode_image() function). However, consider narrowing the local variable to dict[str, EncodedImage | list[Any]] for clarity, or drop the | list union entirely if this method never assigns list values—currently only EncodedImage objects are assigned via encode_image().

@rayokamoto rayokamoto force-pushed the feat/typing branch 3 times, most recently from 11617f5 to 1bd4324 Compare February 12, 2026 14:15
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
environments/_template/env.py (1)

206-213: ⚠️ Potential issue | 🟡 Minor

_build_error_result still uses env_id: str — should be EnvironmentId.

Every call site passes an EnvironmentId, and the rest of this file has been migrated. This parameter was missed.

Proposed fix
     def _build_error_result(
         self,
-        env_id: str,
+        env_id: EnvironmentId,
         task_id: int,
         seed: int,
         start_time: float,
         error: str,
     ) -> dict:
kinitro/environments/genesis/scene_generator.py (1)

143-143: ⚠️ Potential issue | 🟠 Major

Use .value to preserve the object_id format.

When obj_type is an ObjectType enum, f"{obj_type}" produces "ObjectType.BOX" instead of "box". This changes the object_id from "obj_00_red_box" to "obj_00_red_ObjectType.BOX", breaking downstream lookups in object_states dict and reward/success computation functions.

Proposed fix
             objects.append(
                 SceneObjectConfig(
-                    object_id=f"obj_{i:02d}_{color_name}_{obj_type}",
+                    object_id=f"obj_{i:02d}_{color_name}_{obj_type.value}",
                     object_type=obj_type,
kinitro/backend/storage.py (1)

264-282: ⚠️ Potential issue | 🟡 Minor

Type annotation mismatch: weights parameter should be converted before storing in JSON column.

weights: dict[MinerUID, float] is assigned directly to weights_json=weights (line 276), but the ORM model defines weights_json: Mapped[dict[str, float]] (models.py line 124). Since MinerUID keys are int at runtime, they serialize to JSON strings {"1": 0.5}. This works at runtime and the API route handles it via int(k) on deserialization (routes/weights.py line 27), but it creates a type annotation mismatch.

Convert the dict before storing to match the declared type:

weights_json={str(k): v for k, v in weights.items()}
🤖 Fix all issues with AI agents
In `@kinitro/scheduler/scoring.py`:
- Around line 172-176: The parameter type for miners_by_uid in
convert_to_scores_data is incorrect: change its annotation from dict[MinerUID,
str] to dict[MinerUID, Hotkey] (and update any imports) so it aligns with how
the map is used and with MinerScoreData.hotkey; also ensure the
default/placeholder usage (e.g., Hotkey("unknown")) remains a Hotkey instance
and update any related type hints or docstrings; verify call sites like the one
in kinitro/scheduler/main.py (which passes m.hotkey) still match the updated
signature.

In `@kinitro/validator/main.py`:
- Around line 166-171: The Subtensor websocket opened by Subtensor(network=...)
must be explicitly closed to avoid leaks: in the try/finally where you set
validator.subtensor and await validator.run(), update the finally block to first
check if validator.subtensor is set (not None) and then await its async close
method (e.g., await validator.subtensor.close() or the appropriate async cleanup
call provided by the Bittensor SDK), handling any exceptions, before calling
await validator.close(); reference the Validator instance and its
validator.subtensor attribute to locate where to add this explicit shutdown.
🧹 Nitpick comments (7)
environments/_template/env.py (1)

40-44: Missing return type annotation on _get_env.

The other implementations (e.g., environments/genesis/env.py:66) annotate the return as -> RoboticsEnvironment. Consider adding the return type here for consistency with the template's role as a reference implementation. As per coding guidelines, "Add return types to public functions and methods."

kinitro/executor/worker.py (1)

59-61: Consider extracting the family-from-env_id logic into a shared utility.

The env_id.split("/", maxsplit=1)[0] if "/" in env_id else env_id pattern is now duplicated in Worker._get_family (here) and ExecutorConfig.get_image_for_env (config.py line 218). A small helper (e.g., in kinitro/types.py) would reduce duplication.

environments/metaworld/env.py (2)

317-319: _build_error_result uses env_id: str instead of EnvironmentId.

All other methods in this class (_get_env, evaluate, _run_evaluation) now use EnvironmentId, but _build_error_result still declares env_id: str. This should be updated for consistency with the rest of the typing migration.

Proposed fix
     def _build_error_result(
         self,
-        env_id: str,
+        env_id: EnvironmentId,
         task_id: int,

62-66: Missing return type annotation on _get_env.

As per the updated AGENTS.md guideline: "Add type hints to all function signatures (parameters and return types), not just public ones." The return type should be annotated here. The same applies to cleanup on line 343.

environments/genesis/env.py (2)

324-327: _build_error_result still uses str for env_id — inconsistent with the rest of the file.

Every other method in this file was updated to use EnvironmentId, but _build_error_result at Line 326 still declares env_id: str. Since all callers pass an EnvironmentId, this should match.

Suggested fix
     def _build_error_result(
         self,
-        env_id: str,
+        env_id: EnvironmentId,
         task_id: int,

62-62: _env_locks dict key type should be EnvironmentId for consistency.

The keys stored in _env_locks are EnvironmentId values (see Line 162: self._env_locks.setdefault(env_id, ...)), but the annotation still says dict[str, asyncio.Lock].

Suggested fix
-        self._env_locks: dict[str, asyncio.Lock] = {}
+        self._env_locks: dict[EnvironmentId, asyncio.Lock] = {}
kinitro/types.py (1)

146-163: Consider whether np.ndarray annotations are sufficient here.

RobotStateDict and ProceduralTaskResult use bare np.ndarray without shape or dtype hints. This is standard practice (numpy's type stubs don't carry that info ergonomically yet), but worth noting that consumers must rely on conventions for expected shapes (e.g., base_pos is 3D, base_quat is 4D). A brief inline comment on expected shapes could help future readers.

@rayokamoto
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
pyproject.toml (1)

59-65: ⚠️ Potential issue | 🟡 Minor

Duplicate dev dependency groups with inconsistent version constraints.

[project.optional-dependencies] dev (lines 59-65) and [dependency-groups] dev (lines 116-121) declare overlapping packages with diverging lower bounds:

Package optional-dep dependency-group
pytest >=9.0.0 >=9.0.2
pytest-asyncio >=0.21.0 >=1.3.0
ruff >=0.1.0 >=0.14.13

This can cause different resolved environments depending on which mechanism a developer uses (pip install -e ".[dev]" vs. PEP 735 dependency-groups). Consolidate into one source of truth or remove the redundant section.

Also applies to: 116-121

kinitro/executor/verification.py (1)

584-603: ⚠️ Potential issue | 🟡 Minor

Potential type mismatch: info from env.step() is MetaWorld's dict, not StepInfo.

At Line 590, env.step(mw_action) returns MetaWorld's native info dict (which may contain keys like "success", "near_object", etc. that don't match StepInfo). The info variable is initialized as StepInfo = {} on Line 584 but then overwritten with MetaWorld's return value on Line 590. This is then returned directly on Line 603.

Since StepInfo is a TypedDict(total=False), assigning an arbitrary dict from MetaWorld to it won't cause a runtime error, but it's a type-safety gap — the returned dict may contain keys outside the StepInfo schema.

kinitro/environments/metaworld_env.py (1)

584-603: ⚠️ Potential issue | 🟡 Minor

info is annotated as StepInfo but overwritten with MetaWorld's native info dict.

On Line 584, info is typed as StepInfo = {}, but on Line 590 it's reassigned from env.step() which returns MetaWorld's own info dict containing keys outside the StepInfo schema (e.g., near_object, grasp_success, etc.). The returned value on Line 603 is this MetaWorld dict, not a conforming StepInfo.

Consider either:

  1. Constructing a proper StepInfo from MetaWorld's info before returning, or
  2. Keeping the annotation as dict[str, Any] for the internal variable and converting at the return site.
Suggested approach: construct StepInfo at return
-        done: bool = terminated or truncated
-
-        if full_obs is None:
-            raise RuntimeError("MetaWorld step returned no observation")
-        return self._build_observation(full_obs), float(total_reward), done, info
+        done: bool = terminated or truncated
+
+        if full_obs is None:
+            raise RuntimeError("MetaWorld step returned no observation")
+
+        step_info: StepInfo = {
+            "success": self._episode_success,
+        }
+        return self._build_observation(full_obs), float(total_reward), done, step_info
kinitro/scheduler/main.py (1)

351-367: ⚠️ Potential issue | 🟠 Major

Subtensor connection is never closed — websocket leak.

run_scheduler initializes Subtensor via scheduler.start() (line 115), but the finally block only calls scheduler.stop() (sets a flag) and storage.close(). The Subtensor websocket connection is never cleaned up. The validator (run_validator) already handles this correctly with an explicit subtensor.close() in its finally block.

Suggested fix
     try:
         await scheduler.start()
     finally:
+        if scheduler.subtensor is not None:
+            try:
+                await asyncio.to_thread(scheduler.subtensor.close)
+            except Exception:
+                logger.warning("subtensor_close_failed", exc_info=True)
         await scheduler.stop()
         await storage.close()
🤖 Fix all issues with AI agents
In `@kinitro/scheduler/main.py`:
- Line 275: The dict comprehension is needlessly unwrapping and rewrapping
MinerUIDs; since compute_weights returns dict[ MinerUID, float ], change the
comprehension that builds weights (currently using MinerUID(int(k))) to simply
preserve the existing MinerUID keys and cast values to float (i.e., use the
existing weights variable from compute_weights and map v -> float(v)); update
the line building weights in the function where weights is used so keys remain
MinerUID and values are floats.
🧹 Nitpick comments (6)
tests/unit/test_crypto.py (1)

63-116: Consider annotating the keypair fixture parameter.

Since this PR is about improving typing, the keypair parameter on these test methods (e.g., Line 63, 68, 76, 84, 93, 102, 110) could also be annotated as keypair: BackendKeypair for consistency. The same applies to keypair params in TestEncryptDecrypt and TestIntegration.

kinitro/scoring/threshold.py (1)

43-43: Consider updating episodes_per_env dict type to use EnvironmentId.

The dict[str, int] branch of episodes_per_env could be dict[EnvironmentId, int] for consistency with the rest of the typing migration. Minor nit since NewType is transparent at runtime.

Suggested change
-    episodes_per_env: int | dict[str, int],
+    episodes_per_env: int | dict[EnvironmentId, int],

This also requires adding EnvironmentId to the import on line 5.

kinitro/scheduler/main.py (1)

162-164: Prefer RuntimeError over assert for runtime initialization guard.

assert statements are stripped when Python runs with -O (optimized mode). The validator (kinitro/validator/main.py lines 67–68) uses raise RuntimeError(...) for the identical check. Using the same pattern here would be more robust and consistent.

Suggested fix
     async def _run_evaluation_cycle(self) -> None:
         """Run a single evaluation cycle."""
-        assert self.subtensor is not None, "subtensor not initialized; call start() first"
+        if self.subtensor is None:
+            raise RuntimeError("subtensor not initialized; call start() first")
         start_time = time.time()
kinitro/api/routes/miners.py (1)

29-29: Minor: miners_dict key type could be MinerUID for consistency.

The dict key uses raw s.uid (int from ORM) while the rest of the file wraps UIDs with MinerUID(...). Consider dict[MinerUID, MinerInfo] and keying by MinerUID(s.uid).

kinitro/scheduler/scoring.py (1)

167-167: Minor: list(uids) preserves MinerUID type but dict is annotated list[int].

weights_to_u16 returns list[MinerUID] for UIDs. list(uids) is still list[MinerUID], but the annotation dict[str, list[int]] says list[int]. Since this dict appears to be a serialization payload for the chain, consider either annotating as dict[str, list[int] | list[MinerUID]] or explicitly casting: "uids": [int(u) for u in uids].

tests/unit/test_pareto.py (1)

123-139: Consider extracting repeated environment ID lists to module-level constants.

Patterns like [EnvironmentId("a"), EnvironmentId("b")] and (EnvironmentId("a"), EnvironmentId("b")) are repeated in almost every test. A module-level constant (e.g., ENV_AB = [EnvironmentId("a"), EnvironmentId("b")]) would reduce noise and make the tests easier to scan.

@rayokamoto rayokamoto force-pushed the feat/typing branch 3 times, most recently from a80aa7c to fd76206 Compare February 12, 2026 15:13
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@kinitro/chain/weights.py`:
- Around line 3-9: The third‑party imports are not sorted per Ruff I and the
local import is mixed in; reorder the imports so third‑party packages are
grouped and alphabetized (bittensor, bittensor_wallet, numpy, structlog) and
place the local import from kinitro.types after a blank line as the local group,
i.e. import Subtensor and Wallet first, then numpy as np, then structlog,
followed by the local EligibilityResult and MinerUID import.

In `@kinitro/cli/crypto_commands.py`:
- Around line 7-9: The third‑party imports in this module are not sorted per
Ruff I rules; reorder the third‑party block alphabetically (e.g., import
bittensor.Subtensor, bittensor_wallet.Wallet, typer or equivalent alphabetical
order for the actual import names) and ensure import groups follow: standard
library first, then third‑party, then local, with all imports at the top of the
file; update the import statements referencing typer, Subtensor, and Wallet
accordingly so they follow Ruff I ordering.

In `@kinitro/cli/miner/commitment.py`:
- Around line 3-5: Reorder the third-party imports at the top of commitment.py
to satisfy Ruff I: group and sort them alphabetically so bittensor (Subtensor)
comes first, then bittensor_wallet (Wallet), then typer; ensure imports remain
at the module top (after any docstring) and preserve the referenced symbols
Subtensor, Wallet, and typer.
🧹 Nitpick comments (3)
kinitro/cli/testing_commands.py (1)

21-25: Consider adding return type annotation.

Since this PR focuses on improving typing, consider adding -> None to test_scoring (and mock_miner below) to complete the typing coverage for public CLI commands.

 def test_scoring(
     n_miners: int = typer.Option(5, help="Number of simulated miners"),
     n_envs: int = typer.Option(3, help="Number of environments"),
     episodes_per_env: int = typer.Option(50, help="Simulated episodes per environment"),
-):
+) -> None:

As per coding guidelines: "Add return types to public functions and methods."

kinitro/api/routes/miners.py (1)

29-46: Consider caching the MinerUID conversion to avoid repeated wrapping.

Line 44 creates a new MinerUID(s.uid) for each score when calculating averages. While functionally correct, this could be optimized by reusing the already-converted UIDs from the aggregation loop.

Suggested optimization
     # Calculate average success rate per miner
     for uid, miner in miners_dict.items():
-        miner_scores = [s.success_rate for s in scores if MinerUID(s.uid) == uid]
+        miner_scores = [s.success_rate for s in scores if s.uid == int(uid)]
         if miner_scores:
             miner.avg_success_rate = sum(miner_scores) / len(miner_scores)
kinitro/types.py (1)

165-172: Consider adding a comment explaining the Any usage.

Per coding guidelines, Any should be avoided unless required, with an explanation in code comments. Here domain_randomization uses Any because the randomization parameters are open-ended and environment-specific.

Suggested documentation
 class ProceduralTaskResult(TypedDict):
     """Result of ProceduralTaskGenerator.generate()."""

     object_positions: np.ndarray  # (n_objects, 3)
     target_positions: np.ndarray  # (n_objects, 3)
     physics_params: dict[str, float]
-    domain_randomization: dict[str, Any]
+    domain_randomization: dict[str, Any]  # Any: env-specific randomization params vary by implementation

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
kinitro/rl_interface.py (1)

106-115: ⚠️ Potential issue | 🟡 Minor

Avoid Any/raw list in legacy image typings.
list and list[Any] bypass the repo’s typing rules and make downstream validation harder. Consider a concrete legacy image alias (or add a brief justification if Any is truly required).

♻️ Suggested typing tweak
+LegacyImage = list[list[int]] | list[list[list[int]]]
+
-def decode_image(encoded: EncodedImage | list) -> np.ndarray:
+def decode_image(encoded: EncodedImage | LegacyImage) -> np.ndarray:
@@
-    rgb: dict[str, EncodedImage | list[Any]] = Field(default_factory=dict)
-    depth: dict[str, EncodedImage | list[Any]] = Field(default_factory=dict)
+    rgb: dict[str, EncodedImage | LegacyImage] = Field(default_factory=dict)
+    depth: dict[str, EncodedImage | LegacyImage] = Field(default_factory=dict)
As per coding guidelines, avoid Any type annotations unless required and explain why in code comments.

Also applies to: 154-155

🤖 Fix all issues with AI agents
In `@kinitro/cli/miner/commitment.py`:
- Around line 106-107: The synchronous Subtensor construction
(Subtensor(network=network)) in the commit and show_commitment flows must be
replaced with async usage: refactor the logic inside the functions that perform
I/O (the commit flow and show_commitment) into async helpers (e.g., async def
_commit_async(...), async def _show_commitment_async(...)) that create
AsyncSubtensor (or use SubtensorApi(async_subtensor=True)) as an async context
manager, await the necessary calls, and return results; then invoke these
helpers from the CLI entry points using asyncio.run(...). Update references to
Subtensor in the commit routine and show_commitment to use AsyncSubtensor
context manager and ensure Wallet creation remains compatible with async calls
if needed.

In `@kinitro/cli/miner/deploy.py`:
- Around line 5-9: Reorder the third-party imports to follow Ruff I ordering and
grouping: keep all third-party imports together and sort them alphabetically
(basilica, bittensor, bittensor_wallet, huggingface_hub, typer) so that typer
follows the other third‑party modules; ensure imports remain at the top of the
file and no extra grouping or blank lines split these third‑party imports.
- Around line 466-468: The code uses synchronous Subtensor/Wallet in
commit_model(); refactor commit_model() into an async def that uses the async
on-chain APIs (either AsyncSubtensor or SubtensorApi(async_subtensor=True))
instead of Subtensor, create the Wallet/Hotkey using the async-compatible API,
and perform the commit calls via the async client; then invoke this from the CLI
by either wrapping the call in asyncio.run(commit_model(...)) inside the
existing miner_deploy() or convert miner_deploy() to async and use typer's async
support to await commit_model(). Update references to the old synchronous
symbols (Subtensor, commit_model, miner_deploy) accordingly so all network I/O
uses the async APIs.

In `@kinitro/types.py`:
- Around line 175-186: Change the AffinetesEnv Protocol so list_environments()
returns list[EnvironmentId] instead of Any and add an explanatory comment on
evaluate()’s use of Any to document that payloads are environment-specific;
update the method signature for list_environments in the AffinetesEnv class and
add a short comment above async def evaluate(...) explaining why its
kwargs/return use Any are intentional and cannot be tightened.

In `@pyproject.toml`:
- Around line 60-63: The new dev dependency minimums ("pytest>=9.0.2",
"pytest-asyncio>=1.3.0", "pytest-cov>=4.0.0", "ruff>=0.14.13") may be
incompatible with our CI and test/coverage configs; verify CI Python runtime is
>=3.10 (or lower the pytest-asyncio pin), confirm pytest 9's path-argument
behavior won't break test invocations, ensure coverage measurement still works
given pytest-cov 4 removal of multiprocessing support (or revert to a compatible
pytest-cov/coverage combo), and check ruff 0.14.13/isort ordering changes
against our import-order expectations — then update the version strings in
pyproject.toml (or adjust CI/test configs) accordingly before merging.
🧹 Nitpick comments (2)
tests/unit/test_cycle_isolation.py (1)

54-54: Consider adding parameter type annotation for consistency.

Since the fixture returns AsyncMock, you could add the type annotation to the parameter as well for full consistency:

async def test_no_incomplete_cycles(self, mock_session: AsyncMock) -> None:

Same applies to the other test methods using this fixture.

kinitro/scoring/winners_take_all.py (1)

180-186: Add a wildcard case for exhaustiveness.

Since subset_weight_scheme is now a SubsetWeightScheme enum (no longer a string), the match statement should include a case _: branch to guard against future enum variants being added. Without it, subset_weight would be unbound if a new variant is introduced.

Proposed fix
         match subset_weight_scheme:
             case SubsetWeightScheme.LINEAR:
                 subset_weight = float(subset_size)
             case SubsetWeightScheme.EXPONENTIAL:
                 subset_weight = float(2 ** (subset_size - 1))
             case SubsetWeightScheme.EQUAL:
                 subset_weight = 1.0
+            case _:
+                raise ValueError(f"Unknown subset weight scheme: {subset_weight_scheme}")

@rayokamoto rayokamoto force-pushed the feat/typing branch 2 times, most recently from 3356570 to cba093e Compare February 13, 2026 00:49
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/test_genesis.py (1)

57-76: ⚠️ Potential issue | 🟡 Minor

Use dict[str, Any] | None to match TaskSpec's type signature.

The bare dict annotation implicitly means dict[Any, Any]. However, since TaskSpec.initial_state is explicitly typed as dict[str, Any], the helper should use the same type for consistency. Using Any here is required because TaskSpec mandates it.

🔧 Suggested update
 def _make_task_spec(
     task_type: TaskType = TaskType.NAVIGATE,
     target_object_id: str = "obj_00_red_box",
     target_object_type: ObjectType = ObjectType.BOX,
     target_position: list[float] | None = None,
     destination_object_id: str | None = None,
     destination_position: list[float] | None = None,
-    initial_state: dict | None = None,
+    initial_state: dict[str, Any] | None = None,
 ) -> TaskSpec:
🤖 Fix all issues with AI agents
In `@kinitro/chain/commitments.py`:
- Around line 159-166: In _parse_commitment_result, replace the unexplained Any
with a narrower annotation (e.g., Union[dict, object, None] or a
TypedDict/Protocol representing the expected shapes) or, if Any is unavoidable,
add a concise comment above the function that explains the substrate result’s
possible shapes (e.g., a dict with keys like "commitment"/"block" or an object
exposing a .value attribute) and why a broad type is required; update the
function signature from _parse_commitment_result(result: Any) to the chosen
narrower type and ensure the comment mentions the exact fields/properties the
function reads so future reviewers understand the assumptions.
- Around line 13-25: Reorder the import block to follow Ruff I: group standard
library imports first (dataclasses, typing), then third‑party packages sorted
alphabetically (structlog, bittensor imports
AsyncSubtensor/NeuronInfo/Subtensor, bittensor_wallet.Wallet,
cryptography.x25519), and finally local kinitro imports (kinitro.crypto
decrypt_deployment_id/encrypt_deployment_id and kinitro.types
BlockNumber/Hotkey/MinerUID/ParsedCommitment); keep existing from ... import
forms and preserve names like decrypt_deployment_id, AsyncSubtensor, Wallet,
x25519 and ParsedCommitment so the rest of the module (e.g., any references to
these symbols) continues to work.

In `@kinitro/cli/miner/commitment.py`:
- Around line 52-62: The function _get_neurons_hotkey_async currently allows a
negative uid which will index from the end of neurons; add a guard that returns
None when uid is out of bounds by checking if uid is less than 0 or uid >=
len(neurons) before accessing neurons[uid]. Update the conditional that
currently only checks uid >= len(neurons) to instead check (uid < 0 or uid >=
len(neurons)) and keep the rest of the logic (using AsyncSubtensor, neurons, and
returning neurons[uid].hotkey) unchanged.
🧹 Nitpick comments (3)
kinitro/cli/service_commands.py (1)

16-28: Consider adding return type annotations to CLI command functions.

Since this PR focuses on typing improvements, the CLI command functions (api, scheduler, executor) could benefit from explicit -> None return type annotations to align with the coding guidelines. As per coding guidelines, "Add return types to public functions and methods."

💡 Example for the `api` function
 def api(
     host: str = typer.Option("0.0.0.0", help="Host to bind to"),
     port: int = typer.Option(8000, help="Port to bind to"),
     database_url: str = typer.Option(
         "postgresql://postgres:postgres@localhost:5432/kinitro",
         help="PostgreSQL connection URL",
     ),
     no_auth: bool = typer.Option(
         False,
         "--no-auth",
         help="Disable API key authentication for task endpoints.",
     ),
     log_level: str = typer.Option("INFO", help="Logging level"),
-):
+) -> None:

Same pattern would apply to scheduler and executor functions.

kinitro/api/routes/miners.py (1)

31-40: Minor redundancy: MinerUID(s.uid) used twice.

Line 31 creates uid = MinerUID(s.uid), but line 34 wraps it again with uid=MinerUID(s.uid). Consider reusing the existing uid variable for consistency.

♻️ Suggested simplification
     for s in scores:
         uid = MinerUID(s.uid)
         if uid not in miners_dict:
             miners_dict[uid] = MinerInfo(
-                uid=MinerUID(s.uid),
+                uid=uid,
                 hotkey=Hotkey(s.hotkey),
                 last_evaluated_block=cycle.block_number,
                 avg_success_rate=0.0,
                 environments_evaluated=[],
             )
kinitro/chain/commitments.py (1)

258-263: Consider an async variant for chain I/O.
read_miner_commitments performs network calls but is synchronous; per project guidance, prefer async def with AsyncSubtensor to avoid blocking event loops.

As per coding guidelines: “kinitro/**/*.py: Use async def for I/O and DB operations. Use asyncio.run(...) at CLI boundaries. Use asynccontextmanager and session managers for DB work.”

Copy link
Collaborator

@Shr1ftyy Shr1ftyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm.

@rayokamoto rayokamoto merged commit 223f362 into main Feb 13, 2026
2 checks passed
@rayokamoto rayokamoto deleted the feat/typing branch February 13, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants