# Lattice SDK API Reference

This document provides the complete API reference for the Lattice Python SDK.

## Installation

```bash
pip install lattice
```

## Quick Start

```python
from lattice import Client

# Initialize client for current project
client = Client()

# Or specify paths explicitly
client = Client(project_path="/path/to/project", global_path="/path/to/global")
```

---

## Client

The `Client` class is the main entry point for interacting with Lattice memory.

### Constructor

```python
Client(
    project_path: Path | str | None = None,
    global_path: Path | str | None = None,
) -> None
```

Initializes a Lattice SDK client.

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `project_path` | `Path \| str \| None` | `None` (current directory) | Path to the project directory. Uses current working directory if not specified. |
| `global_path` | `Path \| str \| None` | `None` (`~/.config/lattice/`) | Path to global Lattice directory for shared rules and global memory. |

**Returns:** `None`

**Example:**

```python
from lattice import Client

# Use defaults (current directory, ~/.config/lattice/)
client = Client()

# Specify project path only
client = Client(project_path="/home/user/my-project")

# Specify both paths
client = Client(
    project_path="/home/user/my-project",
    global_path="/home/user/.config/lattice"
)
```

---

## Methods

### search

```python
def search(
    self,
    query: str,
    limit: int = 10,
    scope: str = "project",
) -> Result[list[SearchResult], str]
```

Search project or global memory using hybrid search (FTS5 + vector embeddings + Reciprocal Rank Fusion).

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | `str` | Required | Search query string. |
| `limit` | `int` | `10` | Maximum number of results to return. |
| `scope` | `str` | `"project"` | Search scope: `"project"` or `"global"`. |

**Returns:** `Result[list[SearchResult], str]`

- `Success[list[SearchResult]]`: List of search results on success.
- `Failure[str]`: Error message on failure.

**Possible Errors:**
- `"Project not initialized. Run 'lattice ingest' first."` — No `.lattice/store.db` found.
- `"Global store not found. Run 'lattice evolve --global' first."` — Global scope requested but not initialized.

**Example:**

```python
from lattice import Client

client = Client(project_path=".")

# Search project memory
result = client.search("authentication bug")
if result:
    for hit in result.unwrap():
        print(f"[{hit.rrf_score:.3f}] {hit.role}: {hit.content[:100]}")
else:
    print(f"Search failed: {result.failure()}")

# Search global memory with custom limit
result = client.search("error handling patterns", limit=20, scope="global")
```

---

### log

```python
def log(
    self,
    type: str,
    content: str,
    *,
    input: str = "",
    status: str = "",
    error: str = "",
    session_id: str | None = None,
) -> Result[int, str]
```

Record an event to Lattice memory. This is the unified event logging method for session compression.

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `type` | `str` | Required | Event type: `"user"`, `"assistant"`, `"reasoning"`, or `"tool"`. |
| `content` | `str` | Required | Event content. For `type="tool"`, this is the tool name. |
| `input` | `str` | `""` | Tool input summary (only for `type="tool"`). |
| `status` | `str` | `""` | Tool status: `"success"` or `"error"` (only for `type="tool"`). |
| `error` | `str` | `""` | Error message if `status="error"` (max 500 chars). |
| `session_id` | `str \| None` | `None` | Optional session ID. Auto-generated if not provided. |

**Returns:** `Result[int, str]`

- `Success[int]`: Event ID on success.
- `Failure[str]`: Error message on failure.

**Event Type Guidelines:**

| Type | Content | Input | Status | Error |
|------|---------|-------|--------|-------|
| `"user"` | Full user message | Ignored | Ignored | Ignored |
| `"assistant"` | Full assistant response | Ignored | Ignored | Ignored |
| `"reasoning"` | Full reasoning text | Ignored | Ignored | Ignored |
| `"tool"` | Tool name (e.g., `"read"`) | Brief input summary | `"success"` or `"error"` | Error message if failed |

**Example:**

```python
from lattice import Client

client = Client(project_path=".")
session = "ses_abc123"

# Log user message
result = client.log("user", "How do I fix the auth error?", session_id=session)

# Log assistant reasoning
result = client.log(
    "reasoning",
    "Need to check UserValidator class for token validation logic...",
    session_id=session
)

# Log successful tool call
result = client.log(
    "tool", "read",
    input="src/auth/validator.py",
    status="success",
    session_id=session
)

# Log failed tool call
result = client.log(
    "tool", "edit",
    input="src/auth/validator.py",
    status="error",
    error="SyntaxError: invalid syntax at line 42",
    session_id=session
)

# Check result
if result:
    print(f"Logged event with ID: {result.unwrap()}")
else:
    print(f"Failed to log: {result.failure()}")
```

---

### log_turn

```python
def log_turn(
    self,
    user: str,
    assistant: str,
    tools_called: list[str] | None = None,
    session_id: str | None = None,
) -> Result[int, str]
```

Store a complete conversation turn (legacy method). Automatically sanitizes secrets from both messages.

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `user` | `str` | Required | User message content. |
| `assistant` | `str` | Required | Assistant response content. |
| `tools_called` | `list[str] \| None` | `None` | Optional list of tool names called during this turn. |
| `session_id` | `str \| None` | `None` | Optional session ID. Auto-generated if not provided. |

**Returns:** `Result[int, str]`

- `Success[int]`: Log ID of the user message on success.
- `Failure[str]`: Error message on failure.

**Note:** For new code, prefer `log()` which provides more granular event tracking with session compression support.

**Example:**

```python
from lattice import Client

client = Client(project_path=".")

# Log a conversation turn
result = client.log_turn(
    user="What's the authentication flow?",
    assistant="The auth flow uses JWT tokens...",
    tools_called=["read", "search"],
    session_id="ses_abc123"
)

if result:
    print(f"Logged turn with ID: {result.unwrap()}")
```

---

### get_instincts

```python
def get_instincts(self) -> str
```

Get merged Global + Project rules. These represent "System 1" (Instinct) — always-on rules loaded from rule directories.

**Returns:** `str` — Merged rules content from global and project directories.

**Rule Loading Order:**
1. Global rules from `<global_path>/rules/*.md`
2. Project rules from `<project_path>/.lattice/rules/*.md` (excluding promoted rules)

**Example:**

```python
from lattice import Client

client = Client(project_path=".")

# Get all active rules
rules = client.get_instincts()
if rules:
    print("Active rules:")
    print(rules)
else:
    print("No rules configured")
```

---

### propose_rule

```python
def propose_rule(
    self,
    pattern: PatternType | str | RuleProposal,
    observation: str = "",
    evidence: list[str] | None = None,
    suggested_action: str = "",
) -> Result[str, str]
```

Submit a rule proposal to `.lattice/drift/proposals/`. Accepts either individual parameters or a `RuleProposal` object.

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `pattern` | `PatternType \| str \| RuleProposal` | Required | Pattern type OR a `RuleProposal` object. See patterns below. |
| `observation` | `str` | `""` | What was observed (max 200 chars). Required if `pattern` is not a `RuleProposal`. |
| `evidence` | `list[str] \| None` | `None` | List of session IDs as evidence. |
| `suggested_action` | `str` | `""` | Short action slug (max 100 chars). Required if `pattern` is not a `RuleProposal`. |

**Pattern Types:**

| PatternType | Value | Description |
|-------------|-------|-------------|
| `CONVENTION` | `"convention"` | A coding convention or style guideline. |
| `BUG_PATTERN` | `"bug-pattern"` | A recurring bug or error pattern. |
| `PREFERENCE` | `"preference"` | A user preference or choice. |
| `ARCHITECTURE` | `"architecture"` | An architectural decision or pattern. |

**Returns:** `Result[str, str]`

- `Success[str]`: Relative path to the created proposal file.
- `Failure[str]`: Error message on failure.

**Possible Errors:**
- `"Observation must be <= 200 characters"`
- `"Suggested action must be <= 100 characters"`
- `"Invalid pattern type. Must be one of: [...]"` — Invalid pattern string.
- `"Project not initialized. Run 'lattice ingest' first."`

**Example:**

```python
from lattice import Client, PatternType, RuleProposal

client = Client(project_path=".")

# Method 1: Using individual parameters
result = client.propose_rule(
    PatternType.BUG_PATTERN,
    "Expired JWT tokens caused auth failures in 3 sessions",
    ["ses_abc123", "ses_def456", "ses_ghi789"],
    "validate-jwt-expiry"
)

# Method 2: Using PatternType enum
result = client.propose_rule(
    PatternType.CONVENTION,
    "Always use explicit imports instead of wildcards",
    ["ses_session1"],
    "explicit-imports"
)

# Method 3: Using string for pattern type
result = client.propose_rule(
    "preference",  # String works too
    "Prefer dataclasses over Pydantic for simple DTOs",
    ["ses_xyz"],
    "prefer-dataclasses"
)

# Method 4: Using RuleProposal object
proposal = RuleProposal(
    pattern=PatternType.ARCHITECTURE,
    observation="Separate read and write models for scalability",
    evidence=["ses_arch_review"],
    suggested_action="cqrs-pattern"
)
result = client.propose_rule(proposal)

# Check result
if result:
    print(f"Proposal created: {result.unwrap()}")
else:
    print(f"Failed to create proposal: {result.failure()}")
```

---

## Types

### SearchResult

A search result from hybrid search.

```python
@dataclass(frozen=True)
class SearchResult:
    session_id: str      # Session ID of the matching log
    role: str            # Role of the message ("user", "assistant", "tool")
    content: str         # Content of the matching log
    rrf_score: float     # Reciprocal Rank Fusion score (higher = better match)
    timestamp: str       # ISO timestamp of the log
```

**Example:**

```python
result = SearchResult(
    session_id="ses_abc123",
    role="user",
    content="Fix the authentication bug in the login flow",
    rrf_score=0.85,
    timestamp="2026-01-01T00:00:00Z"
)
```

### RuleProposal

A structured rule proposal.

```python
@dataclass(frozen=True)
class RuleProposal:
    pattern: PatternType          # Pattern type enum
    observation: str              # What was observed (max 200 chars)
    evidence: list[str]           # Session IDs as evidence
    suggested_action: str         # Action slug (max 100 chars)
```

**Example:**

```python
from lattice import RuleProposal, PatternType

proposal = RuleProposal(
    pattern=PatternType.BUG_PATTERN,
    observation="Expired JWT tokens caused auth failures",
    evidence=["ses_abc123", "ses_def456"],
    suggested_action="validate-jwt-expiry"
)
```

### PatternType

Enum for pattern types identified by the Compiler.

```python
class PatternType(str, Enum):
    CONVENTION = "convention"      # Coding convention or style
    BUG_PATTERN = "bug-pattern"    # Recurring bug pattern
    PREFERENCE = "preference"      # User preference
    ARCHITECTURE = "architecture"  # Architectural decision
```

---

## Error Handling

The SDK uses the `returns` library's `Result[T, E]` type for explicit error handling. All methods that can fail return `Result` types.

### Pattern: Check Success

```python
result = client.search("query")

# Check if successful
if result:
    # Success branch - unwrap the value
    hits = result.unwrap()
    for hit in hits:
        print(hit.content)
else:
    # Failure branch - get the error
    error = result.failure()
    print(f"Error: {error}")
```

### Pattern: Explicit Matching

```python
from returns.result import Success, Failure

result = client.search("query")

match result:
    case Success(hits):
        for hit in hits:
            print(hit.content)
    case Failure(error):
        print(f"Error: {error}")
```

### Pattern: Unwrap with Default

```python
# Get results or empty list on error
hits = client.search("query").unwrap_or([])

# Get first result or None
first = client.search("query").map(lambda r: r[0] if r else None).unwrap_or(None)
```

### Common Error Messages

| Error Message | Cause | Solution |
|--------------|-------|----------|
| `"Project not initialized. Run 'lattice ingest' first."` | No `.lattice/` directory or `store.db` | Run `lattice ingest` in project root |
| `"Global store not found. Run 'lattice evolve --global' first."` | Global scope requested but no global.db | Run `lattice evolve --global` |
| `"Observation must be <= 200 characters"` | `propose_rule()` observation too long | Shorten the observation |
| `"Suggested action must be <= 100 characters"` | `propose_rule()` action too long | Shorten the action slug |
| `"Invalid pattern type..."` | Unknown pattern string | Use valid `PatternType` value |

---

## Complete Example

```python
from lattice import Client, PatternType, RuleProposal

# Initialize
client = Client(project_path=".")

# Search for relevant context
results = client.search("authentication error").unwrap_or([])
print(f"Found {len(results)} relevant messages")

# Log a session
session_id = "ses_" + "abc123"
client.log("user", "How do I fix JWT validation?", session_id=session_id)
client.log("reasoning", "Need to check token expiry logic...", session_id=session_id)
client.log("tool", "read", input="src/auth/jwt.py", status="success", session_id=session_id)

# Propose a rule based on observations
proposal = client.propose_rule(
    PatternType.BUG_PATTERN,
    "JWT tokens not validated for expiry in auth middleware",
    ["ses_abc123"],
    "add-jwt-expiry-check"
)

if proposal:
    print(f"Rule proposed: {proposal.unwrap()}")

# Get all active rules for context injection
rules = client.get_instincts()
print(f"Active rules: {len(rules)} chars")
```

---

## See Also

- [RFC-002: Lattice Bicameral Memory](RFC-002-Lattice-Bicameral-Memory.md) — Architecture specification
- [ARCHITECTURE.md](ARCHITECTURE.md) — Technical architecture details