mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-20 09:01:19 +00:00
* feat(backend): add LLM-powered feed enhancement Add enhancement harness that fills feed item slots and generates synthetic items via OpenRouter. - LLM client with 30s timeout, reusable SDK instance - Prompt builder with mini calendar and week overview - arktype schema validation + JSON Schema for structured output - Pure merge function with clock injection - Defensive fallback in feed endpoint on enhancement failure - Skips LLM call when no unfilled slots or no API key Co-authored-by: Ona <no-reply@ona.com> * refactor: move feed enhancement into UserSession Move enhancement logic from HTTP handler into UserSession so the transport layer has no knowledge of enhancement. UserSession.feed() handles refresh, enhancement, and caching in one place. - UserSession subscribes to engine updates and re-enhances eagerly - Enhancement cache tracks source identity to prevent stale results - UserSessionManager accepts config object with optional enhancer - HTTP handler simplified to just call session.feed() Co-authored-by: Ona <no-reply@ona.com> * test: add schema sync tests for arktype/JSON Schema drift Validates reference payloads against both the arktype schema (parseEnhancementResult) and the OpenRouter JSON Schema structure. Catches field additions/removals or type changes in either schema. Co-authored-by: Ona <no-reply@ona.com> * refactor: rename arktype schemas to match types Co-authored-by: Ona <no-reply@ona.com> --------- Co-authored-by: Ona <no-reply@ona.com>
@aris/core
Core orchestration layer for ARIS feed reconciliation.
Overview
flowchart TB
subgraph Sources["Feed Sources (Graph)"]
LS[Location Source]
WS[Weather Source]
TS[TFL Source]
CS[Calendar Source]
end
LS --> WS
LS --> TS
subgraph Controller["FeedController"]
direction TB
C1[Holds context]
C2[Manages source graph]
C3[Reconciles on update]
C4[Notifies subscribers]
end
Sources --> Controller
Controller --> Sub[Subscribers]
Concepts
FeedSource
A unified interface for sources that provide context and/or feed items. Sources form a dependency graph.
interface FeedSource<TItem extends FeedItem = FeedItem> {
readonly id: string
readonly dependencies?: readonly string[]
// Context production (optional)
onContextUpdate?(
callback: (update: Partial<Context>) => void,
getContext: () => Context,
): () => void
fetchContext?(context: Context): Promise<Partial<Context>>
// Feed item production (optional)
onItemsUpdate?(callback: (items: TItem[]) => void, getContext: () => Context): () => void
fetchItems?(context: Context): Promise<TItem[]>
}
A source may:
- Provide context for other sources (implement
fetchContext/onContextUpdate) - Produce feed items (implement
fetchItems/onItemsUpdate) - Both
Context Keys
Each package exports typed context keys for type-safe access:
import { contextKey, type ContextKey } from "@aris/core"
interface Location {
lat: number
lng: number
}
export const LocationKey: ContextKey<Location> = contextKey("location")
Usage
Define a Context-Only Source
import type { FeedSource } from "@aris/core"
const locationSource: FeedSource = {
id: "location",
onContextUpdate(callback, _getContext) {
const watchId = navigator.geolocation.watchPosition((pos) => {
callback({
[LocationKey]: { lat: pos.coords.latitude, lng: pos.coords.longitude },
})
})
return () => navigator.geolocation.clearWatch(watchId)
},
async fetchContext() {
const pos = await getCurrentPosition()
return {
[LocationKey]: { lat: pos.coords.latitude, lng: pos.coords.longitude },
}
},
}
Define a Source with Dependencies
import type { FeedSource, FeedItem } from "@aris/core"
import { contextValue } from "@aris/core"
type WeatherItem = FeedItem<"weather", { temp: number; condition: string }>
const weatherSource: FeedSource<WeatherItem> = {
id: "weather",
dependencies: ["location"],
async fetchContext(context) {
const location = contextValue(context, LocationKey)
if (!location) return {}
const weather = await fetchWeatherApi(location)
return { [WeatherKey]: weather }
},
async fetchItems(context) {
const weather = contextValue(context, WeatherKey)
if (!weather) return []
return [
{
id: `weather-${Date.now()}`,
type: "weather",
priority: 0.5,
timestamp: new Date(),
data: { temp: weather.temp, condition: weather.condition },
},
]
},
}
Graph Behavior
The source graph:
- Validates all dependencies exist
- Detects circular dependencies
- Topologically sorts sources
On refresh:
fetchContextruns in dependency orderfetchItemsruns on all sources- Combined items returned to subscribers
On reactive update:
- Source pushes context update via
onContextUpdatecallback - Dependent sources re-run
fetchContext - Affected sources re-run
fetchItems - Subscribers notified
API
Context
| Export | Description |
|---|---|
ContextKey<T> |
Branded type for type-safe context keys |
contextKey<T>(key) |
Creates a typed context key |
contextValue(context, key) |
Type-safe context value accessor |
Context |
Time + arbitrary key-value bag |
Feed
| Export | Description |
|---|---|
FeedSource<TItem> |
Unified source interface |
FeedItem<TType, TData> |
Single item in the feed |
Legacy (deprecated)
| Export | Description |
|---|---|
DataSource<TItem, TConfig> |
Use FeedSource instead |
ContextProvider<T> |
Use FeedSource instead |
ContextBridge |
Use source graph instead |