conductor(spec): Add 'Coordination with Pending Tracks' section (§10)
This track executes after startup_speedup, test_batching_refactor, and qwen_llama_grok_integration land. Section 10 documents the expected post-tracks codebase state and answers 6 critical coordination questions: - Q1: Existing _send_<vendor>() functions (returning str) are renamed to _send_<vendor>_result() and changed to return Result[str] (Option A: clean rename, contained to internal callers). - Q2: send_openai_compatible in src/openai_compatible.py STAYS as-is (it raises at the SDK boundary; correct per Fleury). The new _send_<vendor>_result() functions catch and convert to ErrorInfo. - Q3: Deprecation warning on send() will produce Python warnings in tests; filterwarnings in conftest.py silences them during transition. - Q4: The except ProviderError clauses in src/ai_client.py become dead code after the refactor and are removed in Phase 3. - Q5: ProviderError is FULLY REPLACED by ErrorInfo (a value, not an exception). ProviderError removed entirely; ErrorInfo is the new error type. - Q6: ProviderError.ui_message() moves to ErrorInfo.ui_message(). Phase 1 also adds a baseline verification task to confirm the 3 pending tracks have merged before proceeding. Also renumbered Out of Scope (11) and See Also (12) sections to preserve monotonic section numbers.
This commit is contained in:
@@ -0,0 +1,654 @@
|
||||
# Track: Data-Oriented Error Handling (Fleury Pattern)
|
||||
|
||||
**Status:** Active (spec approved 2026-06-06)
|
||||
**Initialized:** 2026-06-06
|
||||
**Owner:** Tier 2 Tech Lead
|
||||
**Priority:** High (foundational; unlocks incremental migration of the remaining `src/` in future tracks)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This track introduces a new project convention — **Data-Oriented Error Handling** — based on Ryan Fleury's "The Easiest Way To Handle Errors Is To Not Have Them" framework. The convention is codified in a new `conductor/code_styleguides/error_handling.md` reference, surfaced in `product-guidelines.md` and `workflow.md`, and applied to three high-value subsystems: `src/mcp_client.py`, `src/ai_client.py`, and `src/rag_engine.py` (~150 refactor sites).
|
||||
|
||||
The patterns applied: **Result dataclasses** with side-channel error lists instead of `Optional[T]` / exception-based control flow; **nil-sentinel dataclasses** instead of `None`; **zero-initialized fields** via `@dataclass` defaults; **fail-early** validation pushed to shallow stack frames; **AND-over-OR** return types (data + errors as parallel fields, not a sum type). These collapse the bifurcated codepaths that `if x is None` / `try/except` create, in the spirit of Fleury's argument that "errors are just cases."
|
||||
|
||||
A new **public `Result`-based API** (`ai_client.send_result()`) is introduced for new code; the existing `ai_client.send()` is **marked `@deprecated`** (warning emitted at runtime) so callers can migrate incrementally. The actual removal of the deprecated public API is **deferred to a separate follow-up track** (see §13.1) — this track only marks it deprecated and documents the migration path.
|
||||
|
||||
## 2. Goals (Priority Order)
|
||||
|
||||
| Priority | Goal | Rationale |
|
||||
|---|---|---|
|
||||
| **A (foundational)** | New `conductor/code_styleguides/error_handling.md` documenting the 5 patterns with Python mappings. | Establishes the convention as a first-class project standard. Future plans reference this file; new code follows it; the next comprehensive sweep uses it. |
|
||||
| **A (foundational)** | New `src/result_types.py` with `ErrorInfo` dataclass and `Result[T]` dataclass (generic over data only; errors are `list[ErrorInfo]`). | Provides the canonical building blocks. Re-used across the 3 refactored files and by future migrations. |
|
||||
| **A (primary value)** | `src/mcp_client.py` refactored: the `(p, err)` tuple returns + `if err or p is None: return err` pattern (~30 sites) and the `assert p is not None` chain (~30+ sites) become nil-sentinel `Path` + `Result` returns with side-channel errors. | Clearest, most-contained refactor target. The MCP tool layer is the "boundary" between the AI and the filesystem; errors here should be data, not exceptions, so the model can react. |
|
||||
| **A (primary value)** | `src/ai_client.py` refactored: `ProviderError` exception becomes `ErrorInfo` dataclass; internal `_send_<vendor>()` functions return `Result[str, ErrorInfo]`; SDK-exception catches become conversions to `ErrorInfo` (caught at the boundary, not propagated). | The provider layer is the highest-stakes refactor. Catches SDK exceptions at the boundary, converts to data, and lets the rest of the code work with a flat control flow. |
|
||||
| **A (primary value)** | `src/rag_engine.py` refactored: `RAGEngine._init_vector_store`, `_validate_collection_dim`, `is_empty`, `add_documents` return `Result` with side-channel errors instead of raising `ImportError` / `ValueError`. | The RAG engine has its own ad-hoc error class hierarchy that mirrors the patterns Fleury criticizes. Bringing it into the convention aligns it with the new vendor layer. |
|
||||
| **B (architectural)** | Existing public `ai_client.send()` is marked `@deprecated` with a runtime warning directing callers to `ai_client.send_result()`. | The public API is preserved (no breaking change) but signals the migration intent. The deprecation message includes a TODO reference to the follow-up track. |
|
||||
| **B (architectural)** | New public `ai_client.send_result()` returns `Result[str, ErrorInfo]`. The new vendor layer (Qwen/Llama/Grok from the prior track) calls `_send_<vendor>_result()` internally and `send_result()` is the public entry point. | New code uses the new API. Old code keeps working via the deprecated `send()`. |
|
||||
| **C (documentation)** | `conductor/product-guidelines.md` gets a new "Data-Oriented Error Handling" section summarizing the principles (referencing the code styleguide for details). | The convention is visible in the project-level guidance. |
|
||||
| **C (documentation)** | `conductor/workflow.md` gets a note in the Code Style section linking to the new styleguide. | The convention is visible in the workflow so all future plans reference it. |
|
||||
| **C (documentation)** | `docs/guide_*.md` updates: `guide_mcp_client.md` and `guide_ai_client.md` show the new patterns; the next refactor of `guide_rag.md` (or its creation if missing) does the same. | Guides stay in sync with the implementation. |
|
||||
| **D (forward-looking)** | A new follow-up track "Public API Result Migration" is **planned in this spec's §13.1** (not executed) so it's clear what work remains. | Future plans have a known destination. |
|
||||
|
||||
### 2.1 Non-Goals (this track)
|
||||
|
||||
- **Not** migrating the remaining `src/` files (`app_controller.py`, `models.py`, `project_manager.py`, `commands.py`, etc.). These are explicitly out of scope; the convention is established so future tracks can migrate them one at a time.
|
||||
- **Not** removing the public `ai_client.send()`. Only `@deprecated` markers are added. Removal is in a follow-up track.
|
||||
- **Not** changing the `multi_agent_conductor.py` MMA worker interface or the `app_controller.py` orchestrator interface. They continue to call the public `send()` (which still works) and migrate later.
|
||||
- **Not** introducing a generic `Result[T, E]` (with `E` as the error type). The Result is generic only over the success data; errors are always `list[ErrorInfo]`. Rationale: per Fleury, errors are a side-channel — they should accumulate, not be a single tagged value. This also avoids Python's `Union[T, E]` complexity.
|
||||
- **Not** introducing async-aware error propagation. Async / asyncio patterns are out of scope; the refactored code stays synchronous.
|
||||
- **Not** changing how `logging` works. Errors flow as data in `Result`; logging is the caller's choice (most callers will log via the existing comms_log_callback).
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
### 3.1 The 5 Patterns + Python Mappings
|
||||
|
||||
| # | Fleury pattern | Python mapping | Code location |
|
||||
|---|---|---|---|
|
||||
| 1 | **Nil struct pointer** (read-only sentinel) | `@dataclass(frozen=True) class Nil: pass`; module-level `NIL = Nil()` singleton. Frozen prevents runtime mutation; convention prevents writes. | `src/result_types.py:NilPath`, `NilRAGState`, etc. |
|
||||
| 2 | **Zero-initialization** | `@dataclass` with field defaults. `field(default_factory=list)` for mutables. | Used throughout `Result` and the refactored files. |
|
||||
| 3 | **Fail early** | Same principle: validation at the entry point; assert or early return. No `goto defer`, but `try/finally` is similar. | Applied to MCP `_resolve_and_check`, RAG `_init_*`, provider `_ensure_*_client`. |
|
||||
| 4 | **AND over OR (Result struct with side-channel errors)** | `@dataclass(frozen=True) class Result: data: T; errors: list[ErrorInfo]`. Caller: `r = fn(); if r.errors: handle(); else: use(r.data)`. Empty errors list = success. | `src/result_types.py:Result`; used by all 3 refactored files. |
|
||||
| 5 | **Error info as side-channel** | Per-context error list in the Result struct. The list accumulates all errors encountered, not just the first one. Simpler than C's `errno` (which is single-slot); richer than just raising one exception. | `src/result_types.py:ErrorInfo`; populated by error-classification helpers. |
|
||||
|
||||
### 3.2 Module Layout
|
||||
|
||||
```
|
||||
conductor/
|
||||
code_styleguides/
|
||||
error_handling.md # NEW: the canonical reference (5 patterns, Python mappings, examples)
|
||||
product-guidelines.md # MODIFIED: new "Data-Oriented Error Handling" section
|
||||
workflow.md # MODIFIED: note in Code Style section referencing the new styleguide
|
||||
tracks.md # MODIFIED: register this track; add the public_api_migration_20260606 placeholder
|
||||
|
||||
docs/
|
||||
guide_mcp_client.md # MODIFIED: new patterns (if doc exists; otherwise created in follow-up)
|
||||
guide_ai_client.md # MODIFIED: new patterns, deprecation note, Result API
|
||||
guide_rag.md # MODIFIED: new patterns (if doc exists)
|
||||
|
||||
src/
|
||||
result_types.py # NEW: ErrorInfo, Result[T], NilPath, NilRAGState
|
||||
mcp_client.py # MODIFIED: ~60 sites refactored
|
||||
ai_client.py # MODIFIED: ProviderError → ErrorInfo; _send_* returns Result; send() deprecated; send_result() added
|
||||
rag_engine.py # MODIFIED: ~20 sites refactored
|
||||
|
||||
tests/
|
||||
test_result_types.py # NEW: Result + ErrorInfo + nil-sentinel tests
|
||||
test_mcp_client_paths.py # NEW: verify MCP path resolution returns Result
|
||||
test_ai_client_result.py # NEW: verify _send_* return Result, send_result() public API, deprecation warning
|
||||
test_rag_engine_result.py # NEW: verify RAG methods return Result
|
||||
test_deprecation_warnings.py # NEW: verify send() emits DeprecationWarning
|
||||
```
|
||||
|
||||
### 3.3 The `Result[T]` and `ErrorInfo` Data Model
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Generic, TypeVar
|
||||
from enum import Enum
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class ErrorKind(str, Enum):
|
||||
NETWORK = "network"
|
||||
AUTH = "auth"
|
||||
QUOTA = "quota"
|
||||
RATE_LIMIT = "rate_limit"
|
||||
BALANCE = "balance"
|
||||
PERMISSION = "permission"
|
||||
NOT_FOUND = "not_found"
|
||||
INVALID_INPUT = "invalid_input"
|
||||
UNKNOWN = "unknown"
|
||||
CONFIG = "config"
|
||||
INTERNAL = "internal"
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ErrorInfo:
|
||||
kind: ErrorKind
|
||||
message: str
|
||||
source: str = "" # which subsystem produced it (e.g. "mcp.read_file", "ai_client.gemini")
|
||||
original: BaseException | None = None
|
||||
def ui_message(self) -> str:
|
||||
src = f"[{self.source}] " if self.source else ""
|
||||
return f"{src}{self.kind.value}: {self.message}"
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Result(Generic[T]):
|
||||
data: T
|
||||
errors: list[ErrorInfo] = field(default_factory=list)
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return not self.errors
|
||||
def with_error(self, err: ErrorInfo) -> "Result[T]":
|
||||
return Result(data=self.data, errors=[*self.errors, err])
|
||||
def with_data(self, new_data: T) -> "Result[T]":
|
||||
return Result(data=new_data, errors=list(self.errors))
|
||||
```
|
||||
|
||||
**Design notes:**
|
||||
- `Result` is generic over `T` (the success data type) but **not** over `E` (the error type). Per Fleury: errors are a side-channel list, not a tagged sum. This also avoids `Union[T, E]` complexity.
|
||||
- `data: T` is the happy-path result. The success case is `Result(data=X, errors=[])`. The failure case is `Result(data=zero_value, errors=[err1, err2])`.
|
||||
- `errors` is a `list[ErrorInfo]`, not a single error, so partial failures can be reported (e.g., "5 of 10 files failed; here are the 5 errors").
|
||||
- `Result` is `frozen=True` (no mutation); use `with_error` / `with_data` to produce modified copies.
|
||||
- `NilPath` is a `@dataclass(frozen=True)` singleton: `NIL_PATH = NilPath()`. Same for `NilRAGState` etc.
|
||||
|
||||
### 3.4 Nil-Sentinel Pattern
|
||||
|
||||
The nil sentinel is a `@dataclass(frozen=True)` with all-default values. Module-level singleton. Used when a function "would return None" in the old code; in the new code, it returns the nil sentinel of the right type.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class NilPath:
|
||||
exists: bool = False
|
||||
read_text: str = ""
|
||||
errors: list[ErrorInfo] = field(default_factory=list)
|
||||
|
||||
NIL_PATH = NilPath()
|
||||
```
|
||||
|
||||
`NIL_PATH` is the "empty Path" — it has all default values, can be safely read from (the `read_text` is `""`, no file I/O), and `errors` accumulates any deferred errors. Callers that need a real `pathlib.Path` for filesystem operations can check `if isinstance(result.data, NilPath): handle()` — but most callers just need the read text, and `NIL_PATH.read_text == ""` is fine for the AI model's purposes.
|
||||
|
||||
For the MCP client, the `(p, err)` tuple returns are replaced with `Result[Path]`:
|
||||
- Old: `def _resolve_and_check(path: str) -> tuple[Path | None, str]`
|
||||
- New: `def _resolve_and_check(path: str) -> Result[Path]` where `Path` is the real `pathlib.Path` on success or `NilPath()` on failure (the `data` field can be a `Path` or `NilPath`; the consumer checks `result.data.__class__` or relies on the duck-typed `read_text` field)
|
||||
|
||||
This is the same idea as Fleury's nil struct pointer: callers don't need to `if p is None:` check; they can call `p.read_text` and get `""` on the nil path.
|
||||
|
||||
### 3.5 Deprecation Strategy for the Public `send()` API
|
||||
|
||||
The public `ai_client.send()` is preserved (existing callers don't break) but marked deprecated:
|
||||
|
||||
```python
|
||||
import warnings
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("Use ai_client.send_result() instead. Will be removed in the public_api_migration_20260606 track. See conductor/tracks/data_oriented_error_handling_20260606/spec.md for the migration path.")
|
||||
def send(...) -> str:
|
||||
warnings.warn(
|
||||
"ai_client.send() is deprecated; use ai_client.send_result() instead. "
|
||||
"The deprecated function will be removed once callers migrate. "
|
||||
"See conductor/tracks/data_oriented_error_handling_20260606/spec.md §13.1.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _extract_text(_send_*_result(...))
|
||||
```
|
||||
|
||||
`@deprecated` is the `typing_extensions` backport (works on Python 3.11+; this project requires 3.11+). The decorator:
|
||||
- Emits a `DeprecationWarning` at the first call (cached after that to avoid log spam).
|
||||
- Updates type hints in IDEs and type checkers (mypy, pyright) to show the deprecation.
|
||||
- The `@deprecated` call is a no-op for the runtime; only the warning + type-checker effect.
|
||||
|
||||
The new public API:
|
||||
|
||||
```python
|
||||
def send_result(...) -> Result[str]:
|
||||
"""The Result-based public API. Returns Result[str, ErrorInfo] with text in .data and errors in .errors."""
|
||||
# Acquire _send_lock, route to provider, return Result
|
||||
...
|
||||
```
|
||||
|
||||
The `send_result()` function does the same routing as `send()` but returns `Result` instead of unwrapping it. The internal `_send_<vendor>_result()` functions are called from `send_result()`. The deprecated `send()` is a thin wrapper:
|
||||
|
||||
```python
|
||||
@deprecated(...)
|
||||
def send(...) -> str:
|
||||
result = send_result(...)
|
||||
if not result.ok:
|
||||
_append_comms("WARN", "deprecated_send_with_errors", [e.ui_message() for e in result.errors])
|
||||
return result.data
|
||||
return result.data
|
||||
```
|
||||
|
||||
This way, the deprecated `send()` keeps working (returning the text even if there were errors, matching today's behavior), and the comms log gets a warning entry so users can see that the old API is being used with errors.
|
||||
|
||||
## 4. Per-File Refactor Designs
|
||||
|
||||
### 4.1 `src/mcp_client.py`
|
||||
|
||||
**Current pattern (the "sum type as tuple"):**
|
||||
```python
|
||||
def _resolve_and_check(path: str) -> tuple[Path | None, str]:
|
||||
p, err = _resolve_path(path)
|
||||
if err: return None, err
|
||||
if not _is_in_allowed_base(p): return None, "ERROR: ..."
|
||||
if p.exists() and not p.is_file(): return None, "ERROR: ..."
|
||||
return p, ""
|
||||
|
||||
def read_file(path: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.exists(): return f"ERROR: file not found: {path}"
|
||||
...
|
||||
```
|
||||
|
||||
**Refactored pattern (Result + nil sentinel):**
|
||||
```python
|
||||
def _resolve_and_check(path: str) -> Result[Path]:
|
||||
"""Returns Result[Path]. On success, .data is a pathlib.Path. On failure, .data is NilPath() and .errors is populated."""
|
||||
try:
|
||||
p = _resolve_path(path)
|
||||
except _ResolutionError as e:
|
||||
return Result(data=NilPath(), errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=str(e), source="mcp._resolve_and_check")])
|
||||
if not _is_in_allowed_base(p):
|
||||
return Result(data=NilPath(), errors=[ErrorInfo(kind=ErrorKind.PERMISSION, message=f"path '{path}' not in allowed base", source="mcp._resolve_and_check")])
|
||||
return Result(data=p)
|
||||
|
||||
def read_file(path: str) -> Result[str]:
|
||||
"""Returns Result[str]. On success, .data is the file's text. On failure, .data is '' and .errors is populated."""
|
||||
resolved = _resolve_and_check(path)
|
||||
if not resolved.ok:
|
||||
return Result(data="").with_errors_from(resolved)
|
||||
p = resolved.data
|
||||
if not p.exists():
|
||||
return Result(data="", errors=[ErrorInfo(kind=ErrorKind.NOT_FOUND, message=f"file not found: {path}", source="mcp.read_file")])
|
||||
if not p.is_file():
|
||||
return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=f"not a file: {path}", source="mcp.read_file")])
|
||||
try:
|
||||
content = p.read_text(encoding="utf-8")
|
||||
return Result(data=content)
|
||||
except Exception as e:
|
||||
return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.read_file", original=e)])
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
- `_resolve_and_check` returns `Result[Path]` (or `Result[Path | NilPath]` for type clarity). The MCP layer never returns `None` or raises for the resolution step.
|
||||
- `read_file` and the other tool functions return `Result[str]`. The caller (`mcp_client.async_dispatch` or the tool-dispatch internals) extracts the text or formats the error.
|
||||
- The 30+ `assert p is not None` checks (lines 304-794) become "trust the Result and use `p.read_text`" — the Path is never None in the Result; it's either a real Path or `NilPath` (with a `read_text` field that's `""`).
|
||||
- Internal exceptions (`OSError`, `PermissionError`, etc.) are caught at the boundary and converted to `ErrorInfo` — they don't propagate as Python exceptions.
|
||||
|
||||
### 4.2 `src/ai_client.py`
|
||||
|
||||
**Current pattern (the `ProviderError` exception):**
|
||||
```python
|
||||
class ProviderError(Exception):
|
||||
kind: str
|
||||
provider: str
|
||||
original: Exception
|
||||
def ui_message(self) -> str: ...
|
||||
|
||||
def _send_gemini(...) -> str:
|
||||
try:
|
||||
resp = genai_client.models.generate_content(...)
|
||||
...
|
||||
except Exception as exc:
|
||||
raise _classify_gemini_error(exc) from exc
|
||||
```
|
||||
|
||||
**Refactored pattern (ErrorInfo + Result):**
|
||||
```python
|
||||
def _classify_gemini_error(exc: Exception, source: str) -> ErrorInfo:
|
||||
if isinstance(exc, genai_types.RateLimitError):
|
||||
return ErrorInfo(kind=ErrorKind.RATE_LIMIT, message=str(exc), source=source, original=exc)
|
||||
if isinstance(exc, genai_types.PermissionDeniedError):
|
||||
return ErrorInfo(kind=ErrorKind.AUTH, message=str(exc), source=source, original=exc)
|
||||
...
|
||||
return ErrorInfo(kind=ErrorKind.UNKNOWN, message=str(exc), source=source, original=exc)
|
||||
|
||||
def _send_gemini_result(...) -> Result[str]:
|
||||
try:
|
||||
resp = genai_client.models.generate_content(...)
|
||||
...
|
||||
return Result(data=text)
|
||||
except Exception as exc:
|
||||
return Result(data="", errors=[_classify_gemini_error(exc, source="ai_client.gemini")])
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
- `ProviderError` exception class becomes `ErrorInfo` dataclass (a value, not a control-flow primitive).
|
||||
- `_classify_<vendor>_error()` functions return `ErrorInfo` instead of raising `ProviderError`.
|
||||
- `_send_<vendor>()` becomes `_send_<vendor>_result()` returning `Result[str]`. SDK exceptions are caught at the boundary and converted to `ErrorInfo` (caught at the boundary, not propagated).
|
||||
- The public `send()` is preserved (marked `@deprecated`) for backward compat; it calls `send_result()` and unwraps.
|
||||
- The new public `send_result()` returns `Result[str]`.
|
||||
|
||||
**Migration note (for the follow-up track):**
|
||||
- The MMA worker interface in `multi_agent_conductor.py` calls `ai_client.send()`. Migration: call `ai_client.send_result()` and check `.ok` and `.errors`.
|
||||
- The orchestrator in `app_controller.py` calls `ai_client.send()`. Migration: same.
|
||||
- ~50+ test files call `ai_client.send()` or directly call `_send_<vendor>()`. Migration: most tests use the public `send()`; only `_send_*()` direct tests need to update.
|
||||
|
||||
### 4.3 `src/rag_engine.py`
|
||||
|
||||
**Current pattern (raises + ad-hoc error strings):**
|
||||
```python
|
||||
def _init_vector_store(self):
|
||||
vs_config = self.config.vector_store
|
||||
if vs_config.provider == 'chroma':
|
||||
db_path = os.path.abspath(...)
|
||||
os.makedirs(db_path, exist_ok=True)
|
||||
chroma_module = _get_chromadb()
|
||||
if chroma_module is None:
|
||||
raise ImportError("chromadb is not installed")
|
||||
chromadb, Settings = chroma_module
|
||||
self.client = chromadb.PersistentClient(path=db_path)
|
||||
self.collection = self.client.get_or_create_collection(...)
|
||||
self._validate_collection_dim()
|
||||
elif vs_config.provider == 'mock':
|
||||
self.client = "mock"
|
||||
self.collection = "mock"
|
||||
else:
|
||||
raise ValueError(f"Unknown vector store provider: {vs_config.provider}")
|
||||
```
|
||||
|
||||
**Refactored pattern (Result + nil sentinel):**
|
||||
```python
|
||||
def _init_vector_store_result(self) -> Result[None]:
|
||||
vs_config = self.config.vector_store
|
||||
if vs_config.provider == 'chroma':
|
||||
db_path = os.path.abspath(...)
|
||||
os.makedirs(db_path, exist_ok=True)
|
||||
chroma_module = _get_chromadb()
|
||||
if chroma_module is None:
|
||||
return Result(data=None, errors=[ErrorInfo(kind=ErrorKind.CONFIG, message="chromadb is not installed", source="rag._init_vector_store")])
|
||||
chromadb, Settings = chroma_module
|
||||
self.client = chromadb.PersistentClient(path=db_path)
|
||||
self.collection = self.client.get_or_create_collection(...)
|
||||
return _validate_collection_dim_result() # cascades the result
|
||||
elif vs_config.provider == 'mock':
|
||||
self.client = "mock"
|
||||
self.collection = "mock"
|
||||
return Result(data=None)
|
||||
else:
|
||||
return Result(data=None, errors=[ErrorInfo(kind=ErrorKind.CONFIG, message=f"Unknown vector store provider: {vs_config.provider}", source="rag._init_vector_store")])
|
||||
|
||||
def _validate_collection_dim_result(self) -> Result[None]:
|
||||
if self.collection is None or self.collection == "mock" or self.embedding_provider is None:
|
||||
return Result(data=None)
|
||||
try:
|
||||
res = self.collection.get(limit=1, include=["embeddings"])
|
||||
...
|
||||
except Exception as e:
|
||||
return Result(data=None, errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"Failed to validate collection dim: {e}", source="rag._validate_collection_dim", original=e)])
|
||||
return Result(data=None)
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
- `_init_vector_store` becomes `_init_vector_store_result` returning `Result[None]`. `ImportError` and `ValueError` raises become `ErrorInfo` entries in the result.
|
||||
- `_validate_collection_dim` becomes `_validate_collection_dim_result`. The catch-all `except Exception` becomes a `Result` with a single `ErrorInfo` (or success if the catch was a no-op).
|
||||
- The `RAGEngine.is_empty`, `add_documents`, and other public methods return `Result` (or stay as their current return type if no error path exists).
|
||||
- The `RAGEngine.__init__` itself stays as-is (it's a constructor; it sets `self.collection = NIL_COLLECTION` if init fails, deferring the error to the first operation).
|
||||
|
||||
**Nil sentinel for RAG:**
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class NilRAGState:
|
||||
enabled: bool = False
|
||||
is_empty_result: bool = True
|
||||
errors: list[ErrorInfo] = field(default_factory=list)
|
||||
|
||||
NIL_RAG_STATE = NilRAGState()
|
||||
```
|
||||
|
||||
Used when the RAG engine is in a "not configured" / "failed to init" state. Methods that would have raised now return `Result` with `data=NIL_RAG_STATE` and the error in `.errors`.
|
||||
|
||||
### 4.4 Convention Documentation
|
||||
|
||||
**`conductor/code_styleguides/error_handling.md`** (NEW, ~400 lines):
|
||||
|
||||
The canonical reference. Sections:
|
||||
1. The 5 patterns (with Python code examples for each)
|
||||
2. Decision tree: when to use Result vs Exception vs Optional
|
||||
3. Naming conventions (`*_result` for Result-returning functions; `_result` suffix on dataclasses)
|
||||
4. Error classification (the `ErrorKind` enum and when to use which)
|
||||
5. Migration playbook (how to convert an `Optional[T]` return to `Result[T]`)
|
||||
6. Anti-patterns (don't do these things)
|
||||
7. Examples (the 3 refactored subsystems as worked examples)
|
||||
|
||||
**`conductor/product-guidelines.md`** (MODIFIED, +1 section):
|
||||
|
||||
New top-level section "Data-Oriented Error Handling":
|
||||
|
||||
```markdown
|
||||
## Data-Oriented Error Handling
|
||||
|
||||
The codebase follows the "errors are just cases" framework from Ryan Fleury's
|
||||
[The Easiest Way To Handle Errors](https://www.dgtlgrove.com/p/the-easiest-way-to-handle-errors).
|
||||
The canonical reference (with code examples) is in
|
||||
`conductor/code_styleguides/error_handling.md`. Key principles:
|
||||
|
||||
- **Result dataclasses** instead of Optional[T] or exception-based control flow.
|
||||
- **Nil-sentinel dataclasses** instead of None.
|
||||
- **Zero-initialized fields** via @dataclass defaults.
|
||||
- **Fail early**: validation at the entry point, not deep in the call stack.
|
||||
- **AND over OR**: return a struct with data + side-channel errors, not a sum type.
|
||||
- **Exceptions reserved for the SDK boundary**: SDK errors are caught and converted
|
||||
to ErrorInfo dataclasses; the rest of the application works with data, not control flow.
|
||||
|
||||
This convention is established incrementally. The 2026-06-06 track applied it to
|
||||
mcp_client.py, ai_client.py, and rag_engine.py. Future tracks will apply it to
|
||||
the remaining src/ files.
|
||||
```
|
||||
|
||||
**`conductor/workflow.md`** (MODIFIED, +1 line in the Code Style section):
|
||||
|
||||
```markdown
|
||||
- For error handling, see [Data-Oriented Error Handling](./code_styleguides/error_handling.md).
|
||||
```
|
||||
|
||||
**`docs/guide_ai_client.md`** (MODIFIED, +1 section):
|
||||
|
||||
```markdown
|
||||
## Data-Oriented Error Handling (Fleury Pattern)
|
||||
|
||||
The provider layer uses `Result[str, ErrorInfo]` (returned by `_send_<vendor>_result()`)
|
||||
instead of raising `ProviderError`. SDK exceptions are caught at the boundary
|
||||
(see `send_openai_compatible` in `src/openai_compatible.py` and the DashScope
|
||||
adapter in `src/qwen_adapter.py`) and converted to `ErrorInfo` entries in the
|
||||
Result. The public `ai_client.send()` is deprecated; new code should use
|
||||
`ai_client.send_result()`. See `conductor/code_styleguides/error_handling.md`
|
||||
for the convention.
|
||||
```
|
||||
|
||||
## 5. Configuration / Dependencies
|
||||
|
||||
### 5.1 New dependency: `typing_extensions`
|
||||
|
||||
For the `@deprecated` decorator (Python 3.11+ has `@warnings.deprecated` but it's Python 3.13+; `typing_extensions` backports it).
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
...
|
||||
"typing_extensions>=4.5.0", # NEW
|
||||
]
|
||||
```
|
||||
|
||||
### 5.2 No new environment variables
|
||||
|
||||
All existing configs (`config.toml`, `credentials.toml`, per-project TOML) work unchanged.
|
||||
|
||||
## 6. Testing Strategy
|
||||
|
||||
| Test File | Purpose | Coverage Target |
|
||||
|---|---|---|
|
||||
| `tests/test_result_types.py` | `Result`, `ErrorInfo`, nil-sentinel singletons. | 100% |
|
||||
| `tests/test_mcp_client_paths.py` | Verify `_resolve_and_check` returns `Result` (not tuple); verify `read_file` returns `Result[str]`. | 90% (covers the new code paths; existing tests still pass) |
|
||||
| `tests/test_ai_client_result.py` | Verify `_send_<vendor>_result()` returns `Result`; verify `send_result()` is the new public API; verify `send()` emits `DeprecationWarning`. | 90% |
|
||||
| `tests/test_rag_engine_result.py` | Verify RAG methods return `Result`; verify `NilRAGState` is used. | 80% |
|
||||
| `tests/test_deprecation_warnings.py` | Verify `ai_client.send()` emits exactly one `DeprecationWarning` per call site (cached after first). | 100% |
|
||||
| `tests/test_mcp_client.py` (existing) | Verify no regressions; existing tests pass unchanged. | 100% (regression) |
|
||||
| `tests/test_ai_client.py` (existing) | Verify no regressions; existing tests pass unchanged. | 100% (regression) |
|
||||
| `tests/test_rag_engine.py` (existing) | Verify no regressions; existing tests pass unchanged. | 100% (regression) |
|
||||
|
||||
**Mocking strategy:** Existing tests use `unittest.mock.patch` on SDK calls; no changes needed. New tests use the same pattern.
|
||||
|
||||
**Integration verification:** Manual smoke test in the GUI: send a message that exercises the new patterns end-to-end. Document the smoke test in the Phase 5 checkpoint git note.
|
||||
|
||||
## 7. Migration / Rollout
|
||||
|
||||
| Phase | What | Risk |
|
||||
|---|---|---|
|
||||
| **Phase 1 — Foundation: patterns module + style guide** | Add `src/result_types.py`. Add `conductor/code_styleguides/error_handling.md`. Update `product-guidelines.md` and `workflow.md`. Add `typing_extensions` dep. | None. New files, no modifications. |
|
||||
| **Phase 2 — `mcp_client.py` refactor** | Refactor `_resolve_and_check` + the 9 tool functions. The 30+ `assert p is not None` become nil-sentinel usage. The `(p, err)` tuples become `Result`. | Medium. ~60 sites. Mitigated by existing `tests/test_mcp_client.py` coverage. |
|
||||
| **Phase 3 — `ai_client.py` refactor** | Refactor `_classify_*_error()` → return `ErrorInfo`. Refactor `_send_*` → `_send_*_result()` returning `Result`. Add `send_result()` public API. Mark `send()` `@deprecated`. | High. The provider layer is the most complex refactor. Mitigated by existing `tests/test_minimax_provider.py`, `tests/test_qwen_provider.py`, etc. |
|
||||
| **Phase 4 — `rag_engine.py` refactor** | Refactor RAG methods to return `Result`. Add `NilRAGState` sentinel. | Medium. ~20 sites. Mitigated by existing `tests/test_rag_engine.py`. |
|
||||
| **Phase 5 — Deprecation + docs + integration** | Wire deprecation warning. Update `docs/guide_ai_client.md` and `docs/guide_mcp_client.md`. Add the public_api_migration_20260606 placeholder to `conductor/tracks.md`. Manual smoke test. | Low. |
|
||||
|
||||
Each phase has its own checkpoint commit and git note.
|
||||
|
||||
## 8. Risks & Mitigations
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| `ProviderError` is currently raised from `_classify_*_error()`. The refactor changes these to return `ErrorInfo` instead. Any external caller that catches `ProviderError` will break. | Low | Medium | Search the codebase: `rg "except ProviderError"`. Per the grep above (line 1338 of `ai_client.py`), `ProviderError` is only caught in `ai_client.send()`. After the refactor, that catch becomes a `result.errors` check. No external code catches `ProviderError` directly. |
|
||||
| The 30+ `assert p is not None` in `mcp_client.py` are existing invariants that catch real bugs. If the refactor turns them into nil-sentinel paths, a real bug could manifest as a silent empty result. | Medium | High | The refactored code keeps the assertions as `assert resolved.ok` or `assert not isinstance(resolved.data, NilPath)` where the invariants matter. The `Result.errors` list captures the failure for the caller. |
|
||||
| Adding `@deprecated` to `send()` produces a lot of `DeprecationWarning` log spam in the test suite. | High | Low | The deprecation message is cached per call site (using `warnings.warn(..., stacklevel=2)` with a `DeprecationWarning` filter that doesn't propagate to the test failure). Tests can opt in to the warning check via `pytest.warns(DeprecationWarning)`. |
|
||||
| `result_types.py` introduces a circular import risk (if `models.py` or other core modules want to use `ErrorKind` early). | Low | Low | `result_types.py` is a leaf module with no imports from other src files except stdlib. |
|
||||
| The MCP dispatch internals (which call `read_file`, `list_directory`, etc.) currently expect a `str` return. The refactor returns `Result[str]`. | Medium | Medium | The dispatch layer is updated in Phase 2 alongside the tool functions. The dispatch unwraps `Result.data` and logs `Result.errors` via the comms log. The dispatch's public API (the `async_dispatch` function) still returns `str` to the AI model. |
|
||||
| The `RAGEngine.__init__` constructor currently raises if config is invalid. The refactor wants to defer errors to first use. | Medium | Low | Constructor still raises for "config missing" (fail early at init). "Config invalid" (e.g., bad embedding provider) defers to `_init_vector_store_result` (called explicitly or lazily). |
|
||||
|
||||
## 9. Open Questions
|
||||
|
||||
1. **The Result type generic syntax:** Python 3.11+ supports `Generic[T]` cleanly. The spec uses `Result[T]`. Should we also provide a non-generic `Result` for cases where the data is always `None` (e.g., `Result[None]` for operations that succeed/fail without data)? (Proposal: yes; provide `Ok = Result(data=None, errors=[])` as a constant for the trivial success case.)
|
||||
2. **Logging of errors:** When `_send_<vendor>_result()` returns a `Result` with errors, should the errors be auto-logged via `_append_comms`, or should the caller decide? (Proposal: auto-log errors as `WARN` entries in the comms log; this matches today's behavior where `ProviderError` was logged.)
|
||||
3. **Backwards-compat shim for the old `(p, err)` returns:** Some internal callers might still be unpacking `(p, err)`. Should the refactor break them or provide a shim? (Proposal: break them. The grep above shows the pattern is contained; the breakage is in tool functions, not in the public MCP API.)
|
||||
4. **Should the `Result` type be in a more general location?** E.g., `src/result_types.py` is fine for v1; if the patterns spread to other tracks, it could move to `src/result.py` or `src/datatypes/result.py`. (Proposal: keep `src/result_types.py` for v1; revisit if it becomes a multi-track import.)
|
||||
|
||||
## 10. Coordination with Pending Tracks (post-state baseline)
|
||||
|
||||
This track executes **after** three pending tracks have landed (or are far enough along that the codebase reflects their state). The spec assumes the following baseline when this track begins. Any drift from this baseline is a coordination issue that the implementer must resolve before Phase 1.
|
||||
|
||||
### 10.1 Post-`startup_speedup_20260606` State
|
||||
|
||||
- **`src/startup_profiler.py`** exists (new module with `StartupProfiler` context manager).
|
||||
- **`src/app_controller.py`** has `AppController._io_pool: ThreadPoolExecutor` (4 workers, prefix `controller-io-N`) for background work.
|
||||
- **`src/app_controller.py`** has a warmup mechanism: `_warmup_status`, `_warmup_done_event`, `on_warmup_complete`, `wait_for_warmup`.
|
||||
- **`src/ai_client.py`** has `import` statements restructured: heavy SDKs (`google.genai`, `anthropic`, `openai`, `fastapi`) are accessed via `_require_warmed(name)` at use sites, NOT top-level imports. `import src.ai_client` is < 50ms.
|
||||
- **`src/api_hooks.py`** has FastAPI imports deferred similarly. `import src.api_hooks` is < 100ms.
|
||||
- **`src/commands.py`, `src/command_palette.py`, `src/theme_2.py`, `src/theme_nerv.py`, `src/theme_nerv_fx.py`, `src/markdown_helper.py`** all have heavy imports moved to use-sites.
|
||||
- **No new `threading.Thread(...)` calls** anywhere in `src/` (per the track's invariant).
|
||||
- **Top-level `Optional[X]` in `src/ai_client.py`** is reduced (SDK clients now accessed via `_require_warmed`). But the function signatures still use `Optional[X]` for callbacks and config (e.g., `pre_tool_callback: Optional[Callable]`).
|
||||
- **`scripts/audit_main_thread_imports.py`** is a CI gate that fails if heavy imports appear at the top of main-thread-reachable files.
|
||||
|
||||
**Impact on this track:**
|
||||
- The new `src/result_types.py` is a leaf module with only stdlib imports. Safe to import at top of any file. **Verify** with the audit script in Phase 1.
|
||||
- The new `_send_<vendor>_result()` functions may need to be careful about the warmup mechanism: if the SDK isn't warmed, `_require_warmed(name)` is called inside `_ensure_<vendor>_client()`, which is itself called from `_send_<vendor>_result()`. The Result pattern's "fail at boundary, convert to ErrorInfo" applies: if `_require_warmed` raises, catch and convert.
|
||||
|
||||
### 10.2 Post-`test_batching_refactor_20260606` State
|
||||
|
||||
- **`scripts/run_tests_batched.py`** is the new categorized batcher with `--plan` and `--audit` modes.
|
||||
- **`scripts/test_categorizer.py`** + **`scripts/test_batcher.py`** + **`scripts/pytest_collection_order.py`** exist.
|
||||
- **`tests/test_categories.toml`** is populated with ~30 cross-cutting entries.
|
||||
- **`tests/conftest.py`** registers the `pytest_collection_order` plugin.
|
||||
- **All new tests** in this track will be auto-classified by the categorizer. Pure unit tests go to Tier 1; `live_gui` tests (if any) go to Tier 3. Most new tests for this track are Tier 1 (unit).
|
||||
|
||||
**Impact on this track:**
|
||||
- New test files (`test_result_types.py`, `test_mcp_client_paths.py`, `test_ai_client_result.py`, `test_rag_engine_result.py`, `test_deprecation_warnings.py`) should follow the standard naming convention. The categorizer will classify them automatically.
|
||||
- If any of these tests need `mock_app` or `app_instance` fixtures, they're Tier 2. If any need `live_gui`, they're Tier 3.
|
||||
- The `test_batching_refactor` track's registry may want a `test_ai_client_result.py` entry to ensure it goes to the right batch_group (likely `core` or `mma`).
|
||||
|
||||
### 10.3 Post-`qwen_llama_grok_integration_20260606` State (most impactful)
|
||||
|
||||
This is the track that most affects the data-oriented error handling refactor. The state:
|
||||
|
||||
#### 10.3.1 New modules in `src/`
|
||||
|
||||
- **`src/vendor_capabilities.py`**: `VendorCapabilities` dataclass, `_REGISTRY` populated for Qwen/Llama/Grok/MiniMax + Anthropic/Gemini/DeepSeek stubs, `get_capabilities(vendor, model)`, `list_models_for_vendor(vendor)`.
|
||||
- **`src/openai_compatible.py`**: `NormalizedResponse`, `OpenAICompatibleRequest`, `send_openai_compatible(client, request, capabilities)` that **raises** `ProviderError` via `_classify_openai_compatible_error()` on SDK errors.
|
||||
- **`src/qwen_adapter.py`**: `build_dashscope_tools()`, `classify_dashscope_error()` that **raises** `ProviderError`.
|
||||
|
||||
#### 10.3.2 Modified `src/ai_client.py`
|
||||
|
||||
- **All 5 providers** (`_send_gemini`, `_send_anthropic`, `_send_deepseek`, `_send_minimax`, `_send_gemini_cli`) plus 3 new vendors (`_send_qwen`, `_send_llama`, `_send_grok`) all exist. All return `str` (text content of the AI response).
|
||||
- **Per-vendor state**: state globals for all 5+3 providers; per-vendor history lists + locks; per-vendor client singletons.
|
||||
- **Per-vendor `list_models()`** dispatch exists.
|
||||
- **MiniMax is already refactored** to use `send_openai_compatible()` (the data-oriented refactor in that track reduced `_send_minimax` from ~250 lines to ~50).
|
||||
- **Anthropic and DeepSeek** still have their bespoke `_send_*()` implementations.
|
||||
- **Gemini** still has its SDK-specific caching logic (4-breakpoint system, explicit `genai.CachedContent`).
|
||||
- **Gemini CLI** still has its subprocess adapter (`GeminiCliAdapter`).
|
||||
|
||||
#### 10.3.3 Critical coordination questions for THIS track
|
||||
|
||||
**Q1: How to handle the existing `_send_<vendor>()` functions (which all return `str`)?**
|
||||
|
||||
Two options:
|
||||
|
||||
- **Option A (rename)**: Rename `_send_<vendor>()` to `_send_<vendor>_result()` and change the return type to `Result[str]`. The `send_result()` public API calls these directly. The deprecated `send()` public API calls these and unwraps. **Cleaner end state.** The internal callers (just `send()` and `send_result()`) update together.
|
||||
- **Option B (add new)**: Add NEW `_send_<vendor>_result()` functions alongside the existing `_send_<vendor>()`. Old functions stay; new functions do the Result conversion. `send_result()` calls the new ones. The deprecated `send()` calls the old ones. **Lower risk, more code.** Eventually the old functions get deleted in a follow-up track.
|
||||
|
||||
**This track uses Option A.** Rationale: the existing `_send_<vendor>()` functions are private (underscore prefix); only the `send()` and `send_result()` public APIs call them. Renaming + retuning the return type is contained. Test code that calls `_send_*()` directly is rare (the public `send()` is the test entry point) and easy to update.
|
||||
|
||||
**Q2: Does `send_openai_compatible` (in `src/openai_compatible.py`) need to change?**
|
||||
|
||||
**No.** Per Fleury: "exceptions are reserved for the SDK boundary." `send_openai_compatible` IS the SDK boundary for OpenAI-compatible vendors. It correctly catches `OpenAIError` and raises `_classify_openai_compatible_error(exc)`. The calling `_send_<vendor>_result()` (in `src/ai_client.py`) catches the raised `ProviderError` and converts it to an `ErrorInfo` inside a `Result[str]`. This is the **correct layering**: SDK raises → boundary catches → caller converts.
|
||||
|
||||
Similarly, `classify_dashscope_error` in `src/qwen_adapter.py` keeps raising. `_send_qwen_result()` catches and converts.
|
||||
|
||||
**Q3: Does the deprecated `send()` deprecation warning cause test spam?**
|
||||
|
||||
Yes. Most of the existing test files call `ai_client.send()`. Adding `@deprecated` to `send()` will produce a `DeprecationWarning` for each call. The deprecation warning is emitted at runtime via `warnings.warn(DeprecationWarning, stacklevel=2)`.
|
||||
|
||||
Mitigations:
|
||||
- `warnings.warn` only emits the warning once per call site by default (Python's `__warningregistry__`).
|
||||
- The conftest.py's `filterwarnings` setting can be configured to silence `DeprecationWarning` from specific modules.
|
||||
- The deprecation warning is **advisory**; the tests still pass. The agent implementing this track should add a `filterwarnings` entry to `tests/conftest.py` (or per-test) to silence the warning during the transition period.
|
||||
- The follow-up `public_api_migration_20260606` track (planned in §13.1) removes the deprecation entirely.
|
||||
|
||||
**Q4: Does the deprecation warning conflict with the existing `ProviderError` import?**
|
||||
|
||||
The deprecated `send()` no longer raises `ProviderError` (it returns `str` from the `Result.data` field, even if there were errors, matching today's behavior). The `except ProviderError` clauses in `src/ai_client.py` (e.g., line 1338) become dead code that can be removed in Phase 3 of this track.
|
||||
|
||||
**Q5: How do the new `_send_<vendor>_result()` functions interact with the existing `ProviderError`?**
|
||||
|
||||
Two options:
|
||||
- Keep `ProviderError` as the internal exception type that `_classify_*_error()` raises. `_send_<vendor>_result()` catches it and converts to `ErrorInfo`. `ProviderError` becomes a pure SDK-boundary exception.
|
||||
- Replace `ProviderError` entirely with `ErrorInfo` from `src/result_types.py`. `_classify_*_error()` returns `ErrorInfo` (a value, not an exception). `_send_<vendor>_result()` doesn't need to catch anything; the classifier returns the `ErrorInfo` directly.
|
||||
|
||||
**This track uses the second option (full replacement).** Rationale: keeping `ProviderError` as an internal exception defeats the purpose of the Fleury refactor. The whole point is "errors are data, not control flow." `ProviderError` is removed; `ErrorInfo` is its replacement.
|
||||
|
||||
**Q6: What about the `ProviderError.ui_message()` method?**
|
||||
|
||||
It moves to `ErrorInfo.ui_message()` (already in the design in §3.3). All call sites that used `exc.ui_message()` now use `err_info.ui_message()` (where `err_info: ErrorInfo` is from `result.errors[0]` or similar).
|
||||
|
||||
### 10.4 Baseline verification (Phase 1 task)
|
||||
|
||||
Before any refactor, the implementer runs:
|
||||
|
||||
```bash
|
||||
git log --oneline -1 conductor/tracks/qwen_llama_grok_integration_20260606/ # confirm qwen track merged
|
||||
git log --oneline -1 conductor/tracks/test_batching_refactor_20260606/ # confirm batching track merged
|
||||
git log --oneline -1 conductor/tracks/startup_speedup_20260606/ # confirm startup track merged
|
||||
ls src/result_types.py 2>/dev/null && echo "ALREADY EXISTS" || echo "OK to create"
|
||||
ls src/vendor_capabilities.py 2>/dev/null && echo "OK" || echo "MISSING — qwen track not merged?"
|
||||
ls src/openai_compatible.py 2>/dev/null && echo "OK" || echo "MISSING — qwen track not merged?"
|
||||
```
|
||||
|
||||
If any of the expected new files are missing, the implementer reports a coordination issue to the Tier 2 Tech Lead. **Do NOT proceed** with the data-oriented refactor until the post-state baseline is verified.
|
||||
|
||||
## 11. Out of Scope (Explicit)
|
||||
|
||||
- **Migrating the remaining `src/` files** (`app_controller.py`, `models.py`, `project_manager.py`, `commands.py`, `events.py`, `session_logger.py`, `multi_agent_conductor.py`, `hot_reloader.py`, etc.). The convention is established so these can be migrated one at a time in future tracks. See §12.2 for a prioritized list of follow-up migration tracks.
|
||||
- **Removing the deprecated public `ai_client.send()`.** The `@deprecated` marker is added; removal happens in the public_api_migration_20260606 track.
|
||||
- **Migrating the MMA worker interface** (`multi_agent_conductor.py` calls `ai_client.send()` for each worker). Deferred to the public_api_migration_20260606 track.
|
||||
- **Async / asyncio error propagation patterns.** Out of scope for this track.
|
||||
- **The `UserRequestEvent` and `Execution Clutch` HITL patterns** in `app_controller.py`. These are about user interaction, not error propagation. Deferred.
|
||||
- **The `EventEmitter` cross-thread event patterns** in `events.py`. Out of scope.
|
||||
|
||||
## 12. See Also
|
||||
|
||||
### 12.1 Follow-up Track (planned in §12.1 placeholder; detailed in conductor/tracks.md)
|
||||
|
||||
**"Public API Result Migration"** (`public_api_migration_20260606`) — Removes the deprecated `ai_client.send()`. Migrates all callers (`multi_agent_conductor.py`, `app_controller.py`, ~50+ test files) to `send_result()`. Adds any new public API surface needed (e.g., per-ticket `Result` returns in the MMA conductor). This is the **only** follow-up that this spec plans; the other future migrations are listed below for reference but not planned here.
|
||||
|
||||
### 12.2 Future Migration Tracks (prioritized; NOT planned in this spec)
|
||||
|
||||
1. **`app_controller.py` migration** — ~199 `Optional[X]` uses, ~30+ `except Exception` blocks. Highest priority because `app_controller.py` is the orchestrator and touches every subsystem.
|
||||
2. **`models.py` migration** — many `Optional[X]` fields in dataclasses. These can be migrated to default values (e.g., `script: str = ""` instead of `script: Optional[str] = None`).
|
||||
3. **`project_manager.py`, `session_logger.py`, `events.py`, `commands.py` migration** — smaller files, lower priority.
|
||||
4. **`multi_agent_conductor.py` migration** — once `app_controller.py` is done.
|
||||
5. **`hot_reloader.py`, `performance_monitor.py`, `summarize.py`, `outline_tool.py` migration** — utility modules, last priority.
|
||||
|
||||
### 12.3 Project References
|
||||
|
||||
- `docs/guide_ai_client.md` — current provider architecture; will be updated in Phase 5.
|
||||
- `docs/guide_mcp_client.md` — current MCP client architecture; will be updated in Phase 5.
|
||||
- `conductor/product-guidelines.md` "Modular Controller Pattern" — the convention this track extends (Data-Oriented Error Handling is a new top-level convention in the same family).
|
||||
- `conductor/tracks/qwen_llama_grok_integration_20260606/` — the previous track that introduced the "data-oriented" framing; this track extends that philosophy to error handling.
|
||||
- `conductor/tracks/test_batching_refactor_20260606/` — the previous track that established the "tier-based" pattern; this track uses the same convention format (spec + metadata + state + plan).
|
||||
|
||||
### 12.4 External References
|
||||
|
||||
- **Ryan Fleury, "The Easiest Way To Handle Errors Is To Not Have Them"** — the framework this track implements.
|
||||
- **Digital Grove codebase** — Fleury's reference C codebase where the patterns are most fully developed.
|
||||
- **Mike Acton on data-oriented design** — the "data is the API" framing that motivates the Result/nil-sentinel patterns.
|
||||
Reference in New Issue
Block a user