feat(cya): T01-T07 core console-native MVP slice (CYA-WP-0001)

- T01: Python + Typer/rich + pyproject.toml + full src/ layout + working `cya` CLI entrypoint
- T02: Bounded transparent context collector (top-level only, provenance, ignores) + --explain-context
- T03: Genuine rule-based risk classifier (primary) + mandatory terminal confirmation, no auto-execute
- T04: LLMAdapter Protocol + deterministic FakeLLMAdapter seam (llm-connect boundary, zero bypass)
- T05: Strictly minimal phase-memory no-op ports (loud markers, per operator direction 2026-05-26)
- T06: Orchestrator coordinating the full flow; CLI is thin delegation
- T07: pytest harness + safety-focused tests (risk invariants + collector)

All changes verified by running the installed `cya` binary and `pytest tests/`.

Workplan updated with status. State Hub progress event logged (workstream 0a1233fd...).

Refs: CYA-WP-0001, Decision a644364b-11c4-49a9-bf17-99063382e27b
This commit is contained in:
2026-05-26 02:19:13 +02:00
parent da6c7acfc9
commit 637919dd8c
16 changed files with 1308 additions and 8 deletions

27
src/cya/llm/__init__.py Normal file
View File

@@ -0,0 +1,27 @@
"""llm-connect adapter boundary — the integration seam (T04).
can-you-assist owns orchestration + CLI experience.
llm-connect owns provider access, config, token counting, and structured I/O.
This package defines the small stable Protocol / interface that all model
interaction must flow through. A deterministic fake lives here for tests.
Real delegation to llm-connect is a small localized change once the contract
is stable.
See workplan CYA-WP-0001-T04 for the full contract and acceptance criteria.
"""
from .adapter import (
AssistanceRequest,
AssistanceResponse,
LLMAdapter,
FakeLLMAdapter,
)
__all__ = [
"AssistanceRequest",
"AssistanceResponse",
"LLMAdapter",
"FakeLLMAdapter",
]

139
src/cya/llm/adapter.py Normal file
View File

@@ -0,0 +1,139 @@
"""llm-connect adapter boundary (T04 — the integration seam).
Per SCOPE.md and INTENT.md:
- `can-you-assist` owns orchestration + CLI experience.
- `llm-connect` owns provider access, config, token counting, and structured I/O.
This module defines the single stable contract that *all* model interaction
in this repository must flow through. There must never be a production code
path that talks to an LLM (or a mock) while bypassing this boundary.
Design goals for the MVP slice:
- Tiny, stable surface (Protocol + two simple data containers).
- A deterministic, fully reproducible FakeLLMAdapter for tests and early demos.
- Easy future replacement: swapping the fake for a real (or stubbed)
llm-connect client must be a small, localized change.
See workplan CYA-WP-0001-T04 for the full acceptance criteria and the
"Integration Guide for llm-connect" expectations.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol
# ---------------------------------------------------------------------------
# Request / Response shapes (kept minimal for T04)
# These will evolve slightly when the real orchestrator (T06) and
# llm-connect types are known, but the boundary contract stays stable.
# ---------------------------------------------------------------------------
@dataclass
class AssistanceRequest:
"""What we send to the LLM adapter.
Contains the framed user intent, the packed context envelope (or its
serialised form), and any hints the caller wants to pass (model prefs,
token budget, etc.). The adapter is allowed to ignore hints it does not
understand.
"""
user_request: str
context: dict[str, Any] | None = None # usually a ContextEnvelope.to_dict()
hints: dict[str, Any] = field(default_factory=dict)
@dataclass
class AssistanceResponse:
"""What comes back from the LLM adapter.
The orchestrator / CLI is responsible for turning this into the final
user-facing output. The raw fields are intentionally rich so that
different front-ends (terminal, future voice) can render appropriately.
"""
suggestion: str
explanation: str = ""
rationale: str = ""
risks: list[str] = field(default_factory=list)
raw_model_output: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
# ---------------------------------------------------------------------------
# The stable boundary
# ---------------------------------------------------------------------------
class LLMAdapter(Protocol):
"""The single seam for all model interaction.
Any real implementation (llm-connect or otherwise) must satisfy this
protocol. All production call sites must go through an instance of
something obeying this interface.
"""
def complete(self, request: AssistanceRequest) -> AssistanceResponse:
"""Turn a framed request + context into a structured assistant response."""
...
# ---------------------------------------------------------------------------
# Deterministic fake (used by tests, early demos, and T06 development)
# ---------------------------------------------------------------------------
class FakeLLMAdapter:
"""A fully deterministic, side-effect-free fake adapter.
Returns canned but useful responses that are stable across runs.
The response content is derived only from the request text so that
tests can assert on it without any network or real model.
This is the implementation that must be used by all unit and safety
tests until a real adapter is explicitly swapped in.
"""
def complete(self, request: AssistanceRequest) -> AssistanceResponse:
user_text = request.user_request.strip()
risks: list[str] = []
# Very simple deterministic logic for the MVP slice.
# In a real adapter this would be the call to llm-connect.
if "delete" in user_text.lower() or "remove" in user_text.lower():
suggestion = "I cannot recommend executing that directly. Consider a more targeted command or review the exact files first."
explanation = "Your request contained destructive language. The safety layer already required confirmation; the model echoes caution."
rationale = "Rule-based safety + conservative model policy."
risks = ["Destructive intent detected by rules", "Broad scope in request"]
elif "git" in user_text.lower() and ("log" in user_text.lower() or "history" in user_text.lower()):
suggestion = "Run: git log --oneline -10 --graph --decorate"
explanation = "Standard, safe way to view recent history with a compact graph."
rationale = "Common informational request; safe read-only operation."
else:
suggestion = f"Understood: {user_text[:80]}...\n\nSuggested next step: explore the current directory with `ls -la` or `git status` and share more specific intent."
explanation = "This is a placeholder response from the FakeLLMAdapter (T04)."
rationale = "No high-risk patterns; generic helpful reply."
return AssistanceResponse(
suggestion=suggestion,
explanation=explanation,
rationale=rationale,
risks=risks,
raw_model_output=f"[FAKE] echo of request: {user_text[:200]}",
metadata={
"adapter": "FakeLLMAdapter",
"version": "t04-mvp",
"deterministic": True,
},
)
__all__ = [
"AssistanceRequest",
"AssistanceResponse",
"LLMAdapter",
"FakeLLMAdapter",
]