> This is a working draft from initial architecture discussions. Not final documentation.
## Overview
ARIS is an AI-powered personal assistant. The core aggregates data from various sources and compiles a feed of contextually relevant items - similar to Google Now. The feed shows users useful information based on their current context (date, time, location).
Examples of feed items:
- Upcoming calendar events
- Nearby locations
- Current weather
- Alerts
## Design Principles
1.**Extensibility**: The core must support different data sources, including third-party sources.
2.**Separation of concerns**: Core handles data only. UI rendering is a separate system.
3.**Parallel execution**: Sources run in parallel; no inter-source dependencies.
4.**Graceful degradation**: Failed sources are skipped; partial results are returned.
- Providing a reconciler that orchestrates data sources
- Returning a flat list of prioritized feed items
### Key Concepts
- **Context**: Time and location (with accuracy) passed to all sources
- **FeedItem**: Has an ID (source-generated, stable), type, priority, timestamp, and JSON-serializable data
- **DataSource**: Interface that third parties implement to provide feed items
- **Reconciler**: Orchestrates sources, runs them in parallel, returns items and any errors
## Data Sources
Key decisions:
- Sources receive the full context and decide internally what to use
- Each source returns a single item type (e.g., separate "Calendar Source" and "Location Suggestion Source" rather than a combined "Google Source")
- Sources live in separate packages, not in the core
- Sources are responsible for:
- Transforming their domain data into feed items
- Assigning priority based on domain logic (e.g., "event starting in 10 minutes" = high priority)
- Returning empty arrays when nothing is relevant
### Configuration
Configuration is passed at source registration time, not per reconcile call. Sources can use config for filtering/limiting (e.g., "max 3 calendar events").
## Feed Output
- Flat list of `FeedItem` objects
- No UI information (no icons, card types, etc.)
- Items are a discriminated union by `type` field
- Reconciler sorts by priority; can act as tiebreaker
## UI Rendering (Separate from Core)
The core does not handle UI. For extensible third-party UI:
1. Third-party apps register their UI schemas through the backend (UI Registry)
2. Frontend fetches UI schemas from the backend
3. Frontend matches feed items to schemas by `type` and renders accordingly
This approach:
- Keeps the core focused on data
- Works across platforms (web, React Native)
- Avoids the need for third parties to inject code into the app
- Uses a json-render style approach for declarative UI from JSON schemas
> Note: the codebase has evolved since the sections above. The engine now uses a dependency graph with topological ordering (`FeedEngine`, `FeedSource`), not the parallel reconciler described above. The `priority` field is being replaced by post-processing (see the ideas doc). This section describes the UI and enhancement architecture going forward.
Feed items carry an optional `ui` field containing a json-render tree, and an optional `slots` field for LLM-fillable content.
```typescript
interface FeedItem<TType,TData> {
id: string
type: TType
timestamp: Date
data: TData
ui?: JsonRenderNode
slots?: Record<string,Slot>
}
interface Slot {
/** Tells the LLM what this slot wants — the source writes this */
description: string
/** LLM-filled text content, null until enhanced */
content: string | null
}
```
### How it works
The source produces the item with a UI tree and empty slots:
description: "A short contextual insight about the current weather and how it affects the user's day",
content: null
},
"cross-source": {
description: "Connection between weather and the user's calendar events or plans",
content: null
}
}
}
```
The LLM enhancement harness fills `content`:
```typescript
slots: {
"insight": {
description: "...",
content: "Rain after 3pm — grab a jacket before your walk"
},
"cross-source": {
description: "...",
content: "Should be dry by 7pm for your dinner at The Ivy"
}
}
```
The client renders the `ui` tree. When it hits a `Slot` node, it looks up `slots[name].content`. If non-null, render the text. If null, render nothing.
### Separation of concerns
- **Sources** own the UI layout and declare what slots exist with descriptions.
- **The LLM** fills slot content. It doesn't know about layout or positioning.
- **The client** renders the UI tree and resolves slots to their content.
Sources define the prompt for each slot via the `description` field. The harness doesn't need to know what slots any source type has — it reads them dynamically from the items.
Each source defines its own slots. The harness handles them automatically — no central registry needed.
## Enhancement Harness
The LLM enhancement harness fills slots and produces synthetic feed items. It runs reactively — triggered by context changes, not by a timer.
### Execution model
```
FeedEngine.refresh()
→ sources produce items with ui + empty slots
↓
Fast path (rule-based post-processors, <10ms)
→ group, dedup, affinity, time-adjust
→ merge LAST cached slot fills + synthetic items
→ return feed to UI immediately
↓
Background: has context changed since last LLM run?
(hash of: item IDs + data + slot descriptions + user memory)
↓
No → done, cache is still valid
Yes → run LLM harness async
→ fill slots + generate synthetic items
→ cache result
→ push updated feed to UI via WebSocket
```
The user never waits for the LLM. They see the feed instantly with the previous enhancement applied. If the LLM produces new slot content or synthetic items, the feed updates in place.
### LLM input
The harness serializes items with their unfilled slots into a single prompt. Items without slots are excluded. The LLM sees everything at once and fills whatever slots are relevant.