Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 65 additions & 52 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ src/
ports/ # Abstract interfaces (AgentRunner, ThreadRepository, AgentConfigLoader)
exceptions.py # Domain-specific exception hierarchy
application/ # Use cases that orchestrate domain logic. Depends only on domain.
use_cases/ # SendMessage, StreamMessage, HITL decisions, thread management
use_cases/ # SendMessage, StreamMessage, HITL decisions, thread management, store file management
requests/ # Pydantic request models for the API layer
routes/ # FastAPI route handlers (thin layer: validate input, call use case, return response)
routes/ # FastAPI route handlers (health, threads, chat, trace, agents, store, websocket)
infrastructure/ # Concrete implementations of domain ports
deepagent/ # LangGraph Deep Agent adapter + factory
yaml_config/ # YAML config file loader
Expand Down Expand Up @@ -95,8 +95,8 @@ Key points:
- Validation includes:
- `name` must be 1-100 characters.
- `system_prompt` and `system_prompt_file` are mutually exclusive (enforced by `@model_validator`).
- `middleware` values must match the `MiddlewareType` enum.
- `backend.type` must match the `BackendType` enum.
- `backend.type` must match the `BackendType` enum (`state` or `store`).
- `backend.store_backend` and `backend.checkpoint_backend` must be `"memory"` or `"postgres"`.
- `hitl.rules` values are either `bool` or `InterruptRule` objects.
- `subagents` entries require `name` and `description`.

Expand All @@ -108,45 +108,6 @@ uv run python -m src schema > agent-config-schema.json

---

## How to Add a New Middleware

### 1. Add a value to the `MiddlewareType` enum

In `src/domain/entities/agent_config.py`:

```python
class MiddlewareType(StrEnum):
TODO_LIST = "todo_list"
FILESYSTEM = "filesystem"
SUB_AGENT = "sub_agent"
MY_MIDDLEWARE = "my_middleware" # Add your new type
```

### 2. Register it in the factory

In `src/infrastructure/deepagent/factory.py`, add the mapping:

```python
from my_package import MyMiddleware

MIDDLEWARE_MAP: dict[MiddlewareType, type] = {
MiddlewareType.TODO_LIST: FilesystemMiddleware,
MiddlewareType.FILESYSTEM: FilesystemMiddleware,
MiddlewareType.SUB_AGENT: SubAgentMiddleware,
MiddlewareType.MY_MIDDLEWARE: MyMiddleware, # Register here
}
```

### 3. Use it in YAML

```yaml
name: my-agent
middleware:
- my_middleware
```

---

## How to Add a New Backend

### 1. Add a value to the `BackendType` enum
Expand All @@ -157,8 +118,6 @@ In `src/domain/entities/agent_config.py`:
class BackendType(StrEnum):
STATE = "state"
STORE = "store"
FILESYSTEM = "filesystem"
COMPOSITE = "composite"
MY_BACKEND = "my_backend" # Add your new type
```

Expand All @@ -171,14 +130,10 @@ def _resolve_backend(config: AgentConfig):
match config.backend.type:
case BackendType.STATE:
return None
case BackendType.FILESYSTEM:
return FilesystemBackend(root_dir=config.backend.root_dir or "./workspace")
case BackendType.STORE:
return lambda rt: StoreBackend(rt)
return lambda rt: StoreBackend(store=store, namespace=lambda r: ("filesystem",))
case BackendType.MY_BACKEND:
return MyBackend(config.backend.root_dir) # Your implementation
case BackendType.COMPOSITE:
return None
return MyBackend(config.backend) # Your implementation
```

### 3. Use it in YAML
Expand All @@ -187,11 +142,69 @@ def _resolve_backend(config: AgentConfig):
name: my-agent
backend:
type: my_backend
root_dir: "./data"
store_backend: memory
checkpoint_backend: memory
```

---

## Store File API

Files in the LangGraph store (skills, memories, any text blob) are managed through a dedicated set of use cases, a domain port, and an infrastructure adapter, following the same hexagonal pattern as the rest of the codebase.

### Routes (`src/application/routes/store.py`)

| Method | Path | Handler |
|---|---|---|
| `GET` | `/api/v1/store/files` | `list_store_files` (optional `prefix` query param) |
| `GET` | `/api/v1/store/files/{path:path}` | `get_store_file` |
| `PUT` | `/api/v1/store/files/{path:path}` | `put_store_file` (body: `StoreFilePutRequest`) |
| `DELETE` | `/api/v1/store/files/{path:path}` | `delete_store_file` |

Response DTOs: `StoreFileResponse` (`path`, `content`) and `StoreFilePutRequest` (`content`). The `{path:path}` converter allows slashes in the path segment. A missing file on `GET` raises `StoreFileNotFoundError` (`src/domain/errors/store_file.py`), resulting in a `404`.

### Use Cases (`src/application/use_cases/manage_store_file.py`)

Each use case is a thin pass-through to the repository (SRP — one class per action):

| Use Case | Method | Description |
|---|---|---|
| `ListStoreFilesUseCase` | `execute(prefix="/") -> list[str]` | List file paths matching the prefix. |
| `GetStoreFileUseCase` | `execute(path) -> str \| None` | Retrieve a single file's content; `None` if not found. |
| `PutStoreFileUseCase` | `execute(path, content) -> str` | Create or replace a file; returns the stored content. |
| `DeleteStoreFileUseCase` | `execute(path) -> None` | Delete a file (idempotent). |

All use cases are `async` and accept a `StoreFileRepository` via constructor injection.

### Port — `StoreFileRepository` (`src/domain/ports/store_file_repository.py`)

Abstract interface for file CRUD on a namespace-scoped key-value store:

| Method | Signature |
|---|---|
| `list_files` | `(prefix: str) -> list[str]` |
| `get_file` | `(path: str) -> str \| None` |
| `put_file` | `(path: str, content: str) -> None` |
| `delete_file` | `(path: str) -> None` |

`delete_file` is idempotent — implementations must not raise if the path does not exist.

### Adapter — `LangGraphStoreFileRepository` (`src/infrastructure/store_file/adapter.py`)

Implements `StoreFileRepository` on top of a LangGraph `BaseStore` (`InMemoryStore` or `AsyncPostgresStore`):

- Files are stored as `{"content": str, "encoding": "utf-8"}` values keyed by path under the `("filesystem",)` namespace by default (configurable via the `namespace` constructor arg).
- `list_files` uses `asearch(namespace, limit=100)` and filters client-side by `str.startswith(prefix)`.
- `get_file` returns `item.value.get("content")` (or `None` if the item is missing or malformed).
- `put_file` uses `aput` with the content dict.
- `delete_file` uses `adelete` (idempotent).

### Dependency injection

The four use cases are wired in `src/dependencies.py` via `get_list_store_files_use_case`, `get_get_store_file_use_case`, `get_put_store_file_use_case`, and `get_delete_store_file_use_case`. They share a single `LangGraphStoreFileRepository` instance built from the same store used by agent backends.

---

## Running Tests

The project uses **pytest** with **pytest-asyncio** for async test support. All tests are pure unit tests with no external dependencies (LLM calls are fully faked).
Expand Down
Loading
Loading