{"owner":"exo-explore","repo":"exo","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md",".cursorrules","RULES.md"],"files":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to AI coding agents when working with code in this repository.\n\n## Project Overview\n\nexo is a distributed AI inference system that connects multiple devices into a cluster. It enables running large language models across multiple machines using MLX as the inference backend and zenoh for peer-to-peer networking.\n\n## Build & Run Commands\n\n```bash\n# Build the dashboard (required before running exo)\ncd dashboard && npm install && npm run build && cd ..\n\n# Run exo (starts both master and worker with API at http://localhost:52415)\nuv run exo\n\n# Run with verbose logging\nuv run exo -v   # or -vv for more verbose\n\n# Run tests (excludes slow tests by default)\nuv run pytest\n\n# Run all tests including slow tests\nuv run pytest -m \"\"\n\n# Run a specific test file\nuv run pytest src/exo/shared/tests/test_election.py\n\n# Run a specific test function\nuv run pytest src/exo/shared/tests/test_election.py::test_function_name\n\n# Type checking (strict mode)\nuv run basedpyright\n\n# Linting\nuv run ruff check\n\n# Format code (using nix)\nnix fmt\n```\n\n## Pre-Commit Checks (REQUIRED)\n\n**IMPORTANT: Always run these checks before committing code. CI will fail if these don't pass.**\n\n```bash\n# 1. Type checking - MUST pass with 0 errors\nuv run basedpyright\n\n# 2. Linting - MUST pass\nuv run ruff check\n\n# 3. Formatting - MUST be applied\nnix fmt\n\n# 4. Tests - MUST pass\nuv run pytest\n```\n\nRun all checks in sequence:\n```bash\nuv run basedpyright && uv run ruff check && nix fmt && uv run pytest\n```\n\nIf `nix fmt` changes any files, stage them before committing. The CI runs `nix flake check` which verifies formatting, linting, and runs Rust tests.\n\n## Architecture\n\n### Node Composition\nA single exo `Node` (src/exo/main.py) runs multiple components:\n- **Router**: zenoh-based pub/sub messaging via Rust bindings (exo_rs)\n- **Worker**: Handles inference tasks, downloads models, manages runner processes\n- **Master**: Coordinates cluster state, places model instances across nodes\n- **Election**: Bully algorithm for master election\n- **API**: FastAPI server for OpenAI-compatible chat completions\n\n### Message Flow\nComponents communicate via typed pub/sub topics (src/exo/routing/topics.py):\n- `GLOBAL_EVENTS`: Master broadcasts indexed events to all workers\n- `LOCAL_EVENTS`: Workers send events to master for indexing\n- `COMMANDS`: Workers/API send commands to master\n- `ELECTION_MESSAGES`: Election protocol messages\n- `CONNECTION_MESSAGES`: zenoh connection updates\n\n### Event Sourcing\nThe system uses event sourcing for state management:\n- `State` (src/exo/shared/types/state.py): Immutable state object\n- `apply()` (src/exo/shared/apply.py): Pure function that applies events to state\n- Master indexes events and broadcasts; workers apply indexed events\n\n### Key Type Hierarchy\n- `src/exo/shared/types/`: Pydantic models for all shared types\n  - `events.py`: Event types (discriminated union)\n  - `commands.py`: Command types\n  - `tasks.py`: Task types for worker execution\n  - `state.py`: Cluster state model\n\n### Rust Components\nRust code in `rust/` provides:\n- `networking`: zenoh networking (gossipsub, peer discovery)\n- `exo_rs`: PyO3 bindings exposing Rust to Python\n- `system_custodian`: System-level operations\n\n### Dashboard\nSvelte 5 + TypeScript frontend in `dashboard/`. Build output goes to `dashboard/build/` and is served by the API.\n\n## Code Style Requirements\n\nFrom .cursorrules:\n- Strict, exhaustive typing - never bypass the type-checker\n- Use `Literal[...]` for enum-like sets, `typing.NewType` for primitives\n- Pydantic models with `frozen=True` and `strict=True`\n- Pure functions with injectable effect handlers for side-effects\n- Descriptive names - no abbreviations or 3-letter acronyms\n- Catch exceptions only where you can handle them meaningfully\n- Use `@final` and immutability wherever applicable\n\n## Testing\n\nTests use pytest-asyncio with `asyncio_mode = \"auto\"`. Tests are in `tests/` subdirectories alongside the code they test. The `EXO_TESTS=1` env var is set during tests.\n\n## Dashboard UI Testing & Screenshots\n\n### Building and Running the Dashboard\n```bash\n# Build the dashboard (must be done before running exo)\ncd dashboard && npm install && npm run build && cd ..\n\n# Start exo (serves the dashboard at http://localhost:52415)\nuv run exo &\nsleep 8  # Wait for server to start\n```\n\n### Taking Headless Screenshots with Playwright\nUse Playwright with headless Chromium for programmatic screenshots — no manual browser interaction needed.\n\n**Setup (one-time):**\n```bash\nnpx --yes playwright install chromium\ncd /tmp && npm init -y && npm install playwright\n```\n\n**Taking screenshots:**\n```javascript\n// Run from /tmp where playwright is installed: cd /tmp && node -e \"...\"\nconst { chromium } = require('playwright');\n(async () => {\n  const browser = await chromium.launch({ headless: true });\n  const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });\n  await page.goto('http://localhost:52415', { waitUntil: 'networkidle' });\n  await page.waitForTimeout(2000);\n\n  // Inject test data into localStorage if needed (e.g., recent models)\n  await page.evaluate(() => {\n    localStorage.setItem('exo-recent-models', JSON.stringify([\n      { modelId: 'mlx-community/Qwen3-30B-A3B-4bit', launchedAt: Date.now() },\n    ]));\n  });\n  await page.reload({ waitUntil: 'networkidle' });\n  await page.waitForTimeout(2000);\n\n  // Interact with UI elements\n  await page.locator('text=SELECT MODEL').click();\n  await page.waitForTimeout(1000);\n\n  // Take screenshot\n  await page.screenshot({ path: '/tmp/screenshot.png', fullPage: false });\n  await browser.close();\n})();\n```\n\n### Uploading Images to GitHub PRs\nGitHub's API doesn't support direct image upload for PR comments. Workaround:\n\n1. **Commit images to the branch** (temporarily):\n   ```bash\n   cp /tmp/screenshot.png .\n   git add screenshot.png\n   git commit -m \"temp: add screenshots for PR\"\n   git push origin <branch>\n   COMMIT_SHA=$(git rev-parse HEAD)\n   ```\n\n2. **Post PR comment** referencing the raw image URL (uses permanent commit SHA so images survive deletion):\n   ```bash\n   gh pr comment <PR_NUMBER> --body \"![Screenshot](https://raw.githubusercontent.com/exo-explore/exo/${COMMIT_SHA}/screenshot.png)\"\n   ```\n\n3. **Remove the images** from the branch:\n   ```bash\n   git rm screenshot.png\n   git commit -m \"chore: remove temporary screenshot files\"\n   git push origin <branch>\n   ```\n   The images still render in the PR comment because they reference the permanent commit SHA.\n",".cursorrules":"# follow **every** rule exactly; report any violation instead of silently fixing it.\n\nYou must prioritize straightforward code semantics, well-named types, clear function signatures, and robust, carefully-chosen abstractions. Think about how your decisions might impact these aspects of code quality before proposing any changes.\n\nYou can use the advanced features of `typing`. You have access to all of the new features from Python 3.13, 3.12, 3.11...\n\n**When you're done making your changes, remove any redundant comments that you may have left; the comments that remain should only apply to complex segments of code, adding relevant context.**\n\n## 1. Code Discipline\n\n* Eliminate superfluous `try` / `catch` and `if` branches through strict typing and static analysis.\n* Use pure functions unless you must mutate fixed state—then wrap that state in a class.\n* Every function is **referentially transparent**: same inputs ⇒ same outputs, no hidden state, no unintended I/O.\n* Put side-effects in injectable “effect handlers”; keep core logic pure.\n\n## 2. Naming\n\n* Choose descriptive, non-abbreviated names—no 3-letter acronyms or non-standard contractions.\n* Anyone reading a function’s type signature alone should grasp its purpose without extra context.\n\n## 3. Typing\n\n* Maintain **strict, exhaustive** typing; never bypass the type-checker.\n* Default to `Literal[...]` when an enum-like set is needed.\n* Prefer built-in types; when two values share structure but differ in meaning, enforce separation:\n  * Use `typing.NewType` for primitives (zero runtime cost).\n  * For serialisable objects, add a `type: str` field that states the object’s identity.\n\n## 4. Pydantic\n\n* Read, respect, and rely on Pydantic docs.\n* Centralise a common `ConfigDict` with `frozen=True` and `strict=True` (or stricter) and reuse it everywhere.\n* For hierarchies of `BaseModel` variants, declare a discriminated union with `typing.Annotated[Base, Field(discriminator='variant')]`; publish a single `TypeAdapter[Base]` so all variants share one strict validator.\n\n## 5. IDs & UUIDs\n\n* Subclass Pydantic’s `UUID4` for custom ID types.\n* Generate fresh IDs with `uuid.uuid4()`.\n* Create idempotency keys by hashing *persisted* state plus a **function-specific salt** to avoid collisions after crashes.\n\n## 6. Error Handling\n\n* Catch an exception **only** where you can handle or transform it meaningfully.\n* State in the docstring **where** each exception is expected to be handled and **why**.\n\n## 7. Dependencies\n\n* Introduce new external dependencies only after approval.\n* Request only libraries common in production environments.\n\n## 8. Use of `@final` & Freezing\n\n* Mark classes, methods, and variables as `@final` or otherwise immutable wherever applicable.\n\n## 9. Repository Workflow\n\nIf you spot a rule violation within code that you've not been asked to work on directly, inform the user rather than patching it ad-hoc.\n\n\n---\n\n### One-Sentence Summary\n\nWrite strictly-typed, pure, self-describing Python that uses Pydantic, well-scoped side-effects, immutable state, approved dependencies, and explicit error handling\n","RULES.md":"# Repository Rules\n\n* if you see any code that violates these rules, raise it with me directly rather than trying to fix.\n  * where applicable, file a GitHub Issue.\n* adhere to these rules strictly.\n\n## General Rules\n\n* if its possible to eliminate an extra try-catch or if-statement at runtime using type-level discipline, do it!\n* name your types, functions, and classes appropriately.\n  * no three-letter acronyms.\n  * no non-standard contractions.\n  * each data type has a meaning, pick a name which is accurate and descriptive.\n  * the average layman should be able to easily understand what your function does using the function signature alone!\n    * sometimes, there will be exceptions. eg, when you're using specific technical terms that are well understood (saga, event, etc).\n    * usually, you'll think that your code is an exception to the rules, but it won't be.\n\n## State, Functions and Classes\n\n* every function, given the same inputs, should produce the same outputs. ie, no hidden state.\n* use classes to prevent fixed state from being mutated arbitrarily (unsafely); methods provide a safe way of interfacing with state.\n* if your logic doesn't mutate fixed state, it probably belongs in a standalone function rather than a class.\n* functions shouldn't usually produce side-effects (they should be computationally pure).\n  * if, for example, you're updating a state using an event (computationally pure), and you want to trigger a saga (computational side-effect), store the logic for triggering the saga into an effect handler (a function, capable of producing side-effects, that you pass into an otherwise computationally pure function, so that it may trigger side-effects safely).\n\n## Pydantic\n\n* read the Pydantic docs.\n* respect the Pydantic docs.\n* pydantic is all you need.\n* declare and re-use a central `ConfigDict` for your use-case, you'll usually want `frozen` and `strict` to be `True`.\n\n## Unique ID (UUID) Generation\n\n* inherit from Pydantic's `UUID4` class to create your own UUID class.\n* use `uuid.uuid4()` to initialize your class with a fresh UUID where possible.\n* ensure that idempotency tags are generated by taking the salted hash of persisted state.\n  * rationale: if a node crashes and resumes from an older state, it should not accidentally re-publish the same event twice under different idempotency tags. \n  * every distinct function should feature a unique salt, so that there are no accidental collisions in idempotency tags.\n\n## Type Wrappers\n\n* reuse types that already exist in the Python standard library.\n* when two distinct data types are structurally identical (for example, different IDs which are both UUIDs but shouldn't never mixed up), make sure they can't be conflated by the type system.\n  * if you're working with a primitive data type (`str`, `int`, etc), use `NewType` (it has zero runtime overhead).\n  * if you're working with serializable data objects, consider adding a field (type `str`) that states its type.\n\n## Type Discipline\n\n* do not bypass the type-checker, preserve strict typing by any means necessary.\n* by default, use literal types (like `Literal['one', 'two']`) where an enum seems appropriate.\n\npro-tip: Python's type system is quite complex and feature-rich, so reading the documentation is often advisable; Matt discovered that Python `typing` library allows you to check that you've implemented a `match` exhaustively using `Literal` and `get_args(type)` after reading the docs.\n\n## Use of `@final`, Freezing\n\n* use wherever applicable.\n\n## Error Handling\n\n* don't try-catch for no reason.\n* make sure that you always know where and when the exceptions your code produces are meant to be handled, so that it's never a nasty surprise.\n  * always write the rationale for your error-handling down in the docstring!\n  * communicate the details to your colleagues when appropriate.\n\n## Dependencies\n\n* don't introduce any new dependencies without asking.\n* don't ask for any dependencies that aren't ubiquitous within production environments.\n\n## Commit Messages\n\n*   use the imperative mood in the subject line.\n*   prefix the subject line with a change type. our change types are:\n    *   `documentation`: documentation changes.\n    *   `feature`: a new feature.\n    *   `refactor`: a code change that neither fixes a bug nor adds a feature.\n    *   `bugfix`: a bug fix.\n    *   `chore`: routine tasks, maintenance, or tooling changes.\n    *   `test`: adding or correcting tests.\n*   restrict the subject line to fifty characters or less.\n*   capitalize the subject line.\n*   do not end the subject line with a period.\n*   separate subject from body with a blank line."}}