chore(config): 🔧 Update IDE/build configuration settings in project config file
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
72338f8c16
commit
21fb00766c
2 changed files with 684 additions and 0 deletions
191
.project/README.md
Normal file
191
.project/README.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# @ai Project Management
|
||||
|
||||
## Why @ai Exists
|
||||
|
||||
Every AI-enabled application in this ecosystem independently re-implemented (or skipped) the
|
||||
same four concerns: identity, memory, personality, and context assembly. @chobit had `miku.json`
|
||||
and a 10-message ephemeral history. @life had `memory.service.ts` siloed per-platform, inline
|
||||
agent personas, and its own ambient companion service (AmbientCompanionService). @kthulu had
|
||||
no persistent memory or identity at all. Each app assembled LLM prompts differently with no
|
||||
shared contract.
|
||||
|
||||
@ai consolidates those four concerns into a single runtime. It is the *mind* of the assistant.
|
||||
Applications are the *body* (@chobit), *hands* (@kthulu), and *world* (@life, @education, @career).
|
||||
|
||||
## What @ai Replaces
|
||||
|
||||
| What's being removed | Where it lived | Replaced by |
|
||||
|----------------------|---------------|-------------|
|
||||
| `@life/life-ai` companion service | `@life/@applications/ai/services/companion/` | @ai identity + nag + context |
|
||||
| `@life/platform-ai` service | `@life/@applications/ai/services/platform-ai/` | @ai identity + nag + context |
|
||||
| `AmbientCompanionService` | `@life/messenger/notifications/backend/` | @ai M4 nag module |
|
||||
| `NudgeService` | `@life/messenger/notifications/backend/` | @ai M4 nag module |
|
||||
| `memory.service.ts` | `@life/platform-ai/features/assistant/` | @ai M2 memory module |
|
||||
| `miku.json` (local) | `@chobit/config/personalities/` | @ai M3 personality module |
|
||||
| `.quinn` CronCreate nag | `~/.claude/commands/nag-start.md` | @ai M4 `POST /nag/start` |
|
||||
|
||||
## Correct Location
|
||||
|
||||
**Current:** `~/Code/@applications/@ai/` (wrong tier — placed in infrastructure layer)
|
||||
**Correct:** `~/Code/@projects/@ai/` (a domain project, like @life and @kthulu)
|
||||
|
||||
When M0 scaffold begins, create at `@projects/@ai/`, not `@applications/@ai/`.
|
||||
Exported packages go in `@projects/@ai/@packages/` (not global `~/Code/@packages/`).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
.project/
|
||||
├── README.md # This file
|
||||
├── streams/ # Active feature workstreams
|
||||
│ └── <stream-name>/
|
||||
│ ├── README.md # Feature overview and architecture
|
||||
│ ├── STATUS.md # Current progress and blockers
|
||||
│ ├── HANDOFF.md # Session handoff context
|
||||
│ └── NOTES.md # Technical decisions and learnings
|
||||
├── history/ # Completed work records
|
||||
│ └── YYYYMMDD_description.md
|
||||
└── templates/ # Stream templates
|
||||
```
|
||||
|
||||
## Active Streams
|
||||
|
||||
None — project not yet scaffolded.
|
||||
|
||||
## Milestone Roadmap
|
||||
|
||||
### M0: Project Scaffold 🔲
|
||||
- `@applications/@ai/` directory + `app.manifest.yaml`
|
||||
- `services/ai-core/` — NestJS app via `@lilith/service-nestjs-bootstrap`
|
||||
- Docker compose: PostgreSQL (26395) + Redis (26394)
|
||||
- `GET /health` endpoint
|
||||
- `./run` task runner (dev/stop/status/logs)
|
||||
- `packages/ai-client/` skeleton (`@lilith/ai-client`)
|
||||
|
||||
### M1: Identity Module 🔲
|
||||
- `PersonaEntity` — id, name, voice_id, tags, description (maps from miku.json)
|
||||
- `UserIdentityEntity` — id, display_name, bound_persona_id, metadata JSONB
|
||||
- CRUD endpoints: `GET/POST/PATCH /identity`, `GET/POST /identity/:id/personas`
|
||||
- Seed: "quinn" identity + "miku" persona from existing `godot-desktop/config/personalities/miku.json`
|
||||
- Client: `ai-client/identity.ts`
|
||||
|
||||
### M2: Memory Module 🔲
|
||||
- `MemoryEntryEntity` — key, content, category, tags[], metadata JSONB, soft-delete
|
||||
(pattern from `@life/platform-ai/features/assistant/generic-tools/services/memory.service.ts`)
|
||||
- Redis cache layer with PG fallback
|
||||
(pattern from `@ml/knowledge-platform/features/api/service/src/cache/subject-cache.ts`)
|
||||
- Endpoints: `GET/POST/PATCH/DELETE /memory`, `GET /memory/search?q=&tags=&category=`
|
||||
- TTL-based cache with subject invalidation
|
||||
- Client: `ai-client/memory.ts`
|
||||
|
||||
### M3: Personality Module 🔲
|
||||
- Personality template loader — reads JSON files from `config/personalities/`
|
||||
- Prompt composer — assembles system prompt from template + context payload
|
||||
(ports the logic from `@chobit/godot-desktop/platform/conversation/prompt_composer.gd`)
|
||||
- Composition order: identity → voice_constraint → traits → negatives → emotion_tags → depth_tier → context_modifiers → situation_overrides
|
||||
- Endpoints:
|
||||
- `GET /personality` — list available personalities
|
||||
- `GET /personality/:id` — personality definition
|
||||
- `POST /personality/:id/compose` — compose system prompt from context payload
|
||||
- Migrate `miku.json` from `@chobit` to `@ai` as the source of truth
|
||||
- Client: `ai-client/personality.ts`
|
||||
|
||||
### M4: Tasks Module 🔲
|
||||
- `TaskListEntity` — id, name, identity_id, description, metadata JSONB
|
||||
- `TaskEntity` — id, list_id, content, priority (0–100), status, due_at, tags[], metadata JSONB
|
||||
- status: `pending | in_progress | done | snoozed`
|
||||
- Redis pub/sub via `@lilith/eventbus` — emit `ai.task.created`, `ai.task.updated`, `ai.task.completed`
|
||||
- Endpoints:
|
||||
- `GET/POST /tasks` — list management
|
||||
- `GET/POST /tasks/:list_id/items` — task CRUD
|
||||
- `PATCH /tasks/:list_id/items/:id` — update status/priority
|
||||
- Seed: "quinn-platforms" task list from `.quinn/business/registrations.md`
|
||||
- Client: `ai-client/tasks.ts`
|
||||
|
||||
**Full stream spec:** `.project/streams/m4-nag-loop/README.md`
|
||||
|
||||
Two working reference implementations inform M4's design:
|
||||
- **`.quinn` nag loop** — file-based context, Miku TTS, CronCreate (simple, working today)
|
||||
- **`@life` ambient companion** — API-based context, iMessage, NudgeSession entity (sophisticated, production)
|
||||
|
||||
M4 generalizes both into a unified nag engine with `ContextProvider` + `DeliveryChannel` interfaces,
|
||||
`NagLoopEntity` + `NagSessionEntity` persistence, and `POST/DELETE/GET /nag/*` endpoints.
|
||||
|
||||
### M5: Context Module 🔲
|
||||
The primary integration endpoint — assembles everything into a ready-to-use LLM payload.
|
||||
|
||||
- `POST /context/compose` — accepts identity_id, personality_id, recent_messages[], context{}
|
||||
- Assembly pipeline:
|
||||
1. Load identity → user binding
|
||||
2. Compose personality system prompt (→ M3 endpoint)
|
||||
3. Query memory for relevant entries (semantic search on recent_messages)
|
||||
4. Fetch active tasks for identity → optional task_summary string
|
||||
5. Return: `{ system_prompt, memory_injections[], task_summary }`
|
||||
- Replaces: `@chobit` direct model-boss calls, `@life` memory.service.ts inline assembly
|
||||
- Client: `ai-client/context.ts`
|
||||
|
||||
### M5b: Response Format Module 🔲
|
||||
Decides model selection and dual-response config per-request.
|
||||
|
||||
- `ResponseFormat` returned alongside `system_prompt` from `/context/compose`
|
||||
- Model selection logic: conversation → `qwen3-4b`, complex → `qwen3-32b`, TTS always → `qwen3-4b`
|
||||
- Dual-response modes: `text_only | tts_only | dual`
|
||||
- Depth tier → TTS max_tokens mapping (from personality module)
|
||||
- Consumer capability registration: declare `tts_capable: true/false` on identity
|
||||
- `tts` config includes: model, max_tokens, voice_id, personality_id
|
||||
- Injected TTS system constraint: "Respond in 1–3 short spoken sentences. No lists, no markdown."
|
||||
- When to speak: companions (dual), nag loop (tts_only), API (text_only), notifications (tts_only)
|
||||
|
||||
### M6: ai-client Package 🔲
|
||||
- Publish `@lilith/ai-client` to Verdaccio (npm.nasty.sh:4873)
|
||||
- Full TypeScript client covering all 5 modules
|
||||
- React hooks: `useMemory()`, `useTasks()`, `usePersonality()`
|
||||
- Auto-retry + error handling
|
||||
- Use `npx @lilith/dev-publish` for fast iteration
|
||||
|
||||
### M7: @chobit Integration 🔲
|
||||
Wire @chobit to use @ai:
|
||||
- `llm_client.gd` → HTTP `POST /context/compose` (replaces raw model-boss endpoint)
|
||||
- `conversation_store.gd` → async sync to `POST /memory` after each turn
|
||||
- Remove `MAX_HISTORY = 10` cap — full history lives in @ai
|
||||
- `prompt_composer.gd` → becomes thin HTTP client to `POST /personality/miku/compose`
|
||||
- Extend Redis eventbus namespace: `chobit.task.*` events from @ai
|
||||
|
||||
### M8: Relationship Module 🔲
|
||||
Dynamic personality — relationship arc, trait intensity, shared history injection.
|
||||
|
||||
- `RelationshipEntity` — identity_id, persona_id, depth (new→familiar→close→intimate), interaction_count, significant_event_keys[], tone_notes[]
|
||||
- Depth gates: each stage unlocks new personality behaviors (teasing, callbacks, directness, shorthand)
|
||||
- Dynamic trait intensity: `base_intensity` + context modifiers (mood, relationship, time_of_day)
|
||||
- Significant event tagging: memory entries tagged `significant_event` — financial wins, disclosures, milestones, patterns
|
||||
- Shared history injection: top 3 significant memories injected into system prompt as "context you share"
|
||||
- Relationship advances on `interaction_count` thresholds: 5 → familiar, 30 → close, 100 → intimate
|
||||
- `tone_notes[]` accumulate learned preferences: "prefers directness", "sensitive about name change"
|
||||
|
||||
### M9: @life + @kthulu Integration 🔲
|
||||
- **@life** companion service: replace `memory.service.ts` with `@lilith/ai-client` calls
|
||||
- **@kthulu** context-builder: add identity layer — `@ai /context/compose` wraps code context
|
||||
- Both consume same `@lilith/ai-client` package
|
||||
|
||||
---
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Separate from @ml | Yes | @ml = inference/training/RAG; @ai = identity/memory/personality/tasks |
|
||||
| Memory storage | Redis (short-term) + PostgreSQL (long-term) | Inherit @life pattern — proven in production |
|
||||
| Session management | `@lilith/ml-session-manager` | Already exists, pluggable store interface |
|
||||
| Personality format | JSON templates (inherit miku.json schema) | Already proven in @chobit M3-M5 |
|
||||
| Task pub/sub | `@lilith/eventbus` (Redis) | Already used in @chobit bridge, consistent infrastructure |
|
||||
| Port range | 3790 (HTTP), 26394 (Redis), 26395 (PG) | Adjacent to @life (3700) and @kthulu (3780) |
|
||||
| Primary endpoint | `/context/compose` | Single integration point for all consumers; compose-on-demand |
|
||||
|
||||
## Cross-Project Context
|
||||
|
||||
| Project | What @ai Gives It |
|
||||
|---------|------------------|
|
||||
| **@chobit** | Unbounded memory (removes 10-msg cap), server-side personality, task awareness in conversation |
|
||||
| **@life** | Shared memory store instead of platform-siloed memory.service.ts |
|
||||
| **@kthulu** | User identity layer on top of code context (who is the developer, what do they care about) |
|
||||
| **Claude Code** | Nag loop → proper task system; MCP access to memory and personality |
|
||||
493
.project/streams/m4-nag-loop/README.md
Normal file
493
.project/streams/m4-nag-loop/README.md
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
# M4: Nag Loop — Stream Spec
|
||||
|
||||
**Status:** Pre-implementation (requires M0 scaffold first)
|
||||
**Depends on:** M0 (NestJS skeleton), M3 (personality module, for voice dispatch)
|
||||
**Reference implementations:** `.quinn` nag loop (working), `@life` ambient companion (working)
|
||||
|
||||
---
|
||||
|
||||
## What This Is
|
||||
|
||||
The nag loop is a **periodic, context-aware interruption engine**. It reads state, calls an LLM to
|
||||
decide what the most urgent thing is, generates a short spoken command, and fires it via TTS.
|
||||
|
||||
Two working implementations already exist in this ecosystem. `@ai`'s M4 must **generalize both**
|
||||
rather than duplicating either.
|
||||
|
||||
---
|
||||
|
||||
## Two Working Reference Implementations
|
||||
|
||||
### 1. `.quinn` Nag Loop (simple, file-based)
|
||||
|
||||
**Source:** `~/.claude/commands/nag-start.md`, `nag-stop.md`
|
||||
**Transport:** Miku TTS via `mcp__speech-synthesis__synthesize`
|
||||
**Delivery:** Spoken audio on local machine
|
||||
|
||||
**How it works:**
|
||||
1. `CronCreate` fires every 5 minutes
|
||||
2. Reads 5 markdown files:
|
||||
- `.quinn/context.md` — live session state (Claude updates this during chat)
|
||||
- `.quinn/todos.md` — ordered task list (`- [ ]` / `- [x]`)
|
||||
- `.quinn/curricula/bimbo.md`, `beauty-body.md`, `influencer.md`
|
||||
3. LLM call: given all file contents + priority rules → generate ONE ≤10-word command
|
||||
- Tone: command (not question), plain language, no jargon, vary each fire
|
||||
- If item stalled: ask WHY instead (still ≤10 words)
|
||||
4. `mcp__speech-synthesis__synthesize(message, personality='miku')`
|
||||
5. Stop: `CronDelete` matching jobs + Miku goodbye
|
||||
|
||||
**Priority rules (hardcoded in prompt):**
|
||||
```
|
||||
1. Call name change office before 5pm (BLOCKING)
|
||||
2. Shower + full-body lotion (2x today)
|
||||
3. Photo shoot looks (unblocks 8 platforms)
|
||||
4. Platform ad copy / registrations
|
||||
```
|
||||
|
||||
**Strengths:** Dead simple. Works today. Pure file-based — no DB, no infra.
|
||||
**Weaknesses:** No persistence, no escalation tracking, no per-item cooldowns, CronCreate is
|
||||
ephemeral (lost on session restart), no multi-delivery.
|
||||
|
||||
---
|
||||
|
||||
### 2. `@life` Ambient Companion (sophisticated, API-based)
|
||||
|
||||
**Source:** `~/Code/@projects/@life/@projects/messenger/notifications/backend/`
|
||||
**Transport:** iMessage / SMS via `MessagingChannel` (black:3100)
|
||||
**Delivery:** Message sent to user's phone
|
||||
|
||||
**How it works:**
|
||||
1. 1-minute cron tick checks `SettingKey.UserAwake` (boolean in settings DB)
|
||||
2. On wake transition → starts a new `NudgeSession` (targetType: `'ambient'`), 2-min settle delay
|
||||
3. On each tick, if session active and `nextPingAt` ≤ now → `processDuePings()`
|
||||
4. `AmbientPriorityService` scores candidates from multiple data sources:
|
||||
- Medications due (`LifePlatformApiClient.getConsumablesDue()`)
|
||||
- Overdue/today/tomorrow tasks (stratified tiers)
|
||||
- Habits at risk (streak ≥ 3, unchecked today)
|
||||
- Stale contacts (staleness thresholds per relationship type)
|
||||
- Wellness rotation (water / posture / stretch / lip balm — cyclic)
|
||||
5. Picks highest-scoring candidate not on cooldown
|
||||
6. `AiMessageService.generateNudgeMessage(factualContext, tone, itemPingCount)` → message
|
||||
7. Sends via `MessagingChannel`
|
||||
8. Updates session metadata (cooldowns, itemPings, pingCount, lastItem)
|
||||
9. Schedules next ping (10–60 min, randomized within speed tier)
|
||||
10. Auto-stops at sleep hour or explicit stop event
|
||||
|
||||
**Escalation:** per-item ping count → tone: `gentle` (0–2) → `pointed` (3–5) → `relentless` (6+)
|
||||
|
||||
**Tier / cooldown system:**
|
||||
```
|
||||
Critical (25 min): medications
|
||||
High (120 min): overdue tasks, habits streak ≥7
|
||||
Medium (240 min): today's tasks, habits streak 3–6
|
||||
Low (480 min): tomorrow's tasks, stale contacts
|
||||
Wellness (120 min): water/posture/stretch rotation
|
||||
```
|
||||
|
||||
**Strengths:** Production-grade. Per-item cooldowns. Escalation. Wake/sleep awareness.
|
||||
Multi-source priority scoring. Persisted session state.
|
||||
**Weaknesses:** Coupled to `@life` data model. iMessage-only delivery. Not generalized.
|
||||
|
||||
---
|
||||
|
||||
## Similarities and Differences
|
||||
|
||||
| Dimension | `.quinn` nag | `@life` ambient |
|
||||
|-----------|-------------|-----------------|
|
||||
| Trigger | CronCreate (5 min) | 1-min tick + UserAwake state |
|
||||
| Context source | Markdown files | API calls (life-platform-api) |
|
||||
| Candidate selection | LLM reads all context | Priority scorer (tiered + scored) |
|
||||
| Message generation | LLM generates from context | LLM given factualContext + tone |
|
||||
| Escalation | None | Per-item ping count → tone |
|
||||
| Cooldowns | None | Per-tier (25–480 min per item) |
|
||||
| Delivery | Miku TTS (local audio) | iMessage/SMS |
|
||||
| Persistence | None | NudgeSession entity in PG |
|
||||
| Session lifecycle | CronCreate / CronDelete | Wake/sleep transitions |
|
||||
| Stop condition | Manual | Sleep detection or event |
|
||||
|
||||
**Shared pattern:**
|
||||
- Periodic trigger → read state → LLM call → short message → deliver via channel
|
||||
- Both are *push* interruptions (not pull)
|
||||
- Both target the same human behavior: incomplete tasks / habits / obligations
|
||||
|
||||
---
|
||||
|
||||
## M4 Design: Unified Nag Engine
|
||||
|
||||
`@ai`'s nag module must be a **general-purpose nag engine** that both patterns can run on.
|
||||
The key abstractions:
|
||||
|
||||
### 1. `NagLoop` (config, persisted)
|
||||
|
||||
```typescript
|
||||
@Entity('ai_nag_loops')
|
||||
@Unique(['identity_id', 'source'])
|
||||
class NagLoopEntity {
|
||||
id: string; // uuid
|
||||
identity_id: string; // e.g. "quinn"
|
||||
source: string; // e.g. "quinn-todos", "life-ambient"
|
||||
interval_cron: string; // e.g. "*/5 * * * *"
|
||||
personality_id: string; // e.g. "miku"
|
||||
context_provider: string; // "file" | "api" | "hybrid"
|
||||
context_config: Record<string,unknown>; // provider-specific config (file paths, API URL, etc)
|
||||
priority_rules: string[]; // ordered instructions for the LLM
|
||||
delivery_channel: string; // "tts" | "imessage" | "sms"
|
||||
delivery_config: Record<string,unknown>; // channel-specific config
|
||||
active: boolean;
|
||||
last_fired_at: Date | null;
|
||||
last_message: string | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `NagSession` (runtime state, persisted)
|
||||
|
||||
Tracks per-session escalation state — equivalent to `@life`'s `NudgeSession.metadata`:
|
||||
|
||||
```typescript
|
||||
@Entity('ai_nag_sessions')
|
||||
class NagSessionEntity {
|
||||
id: string;
|
||||
loop_id: string; // FK → NagLoopEntity
|
||||
status: 'active' | 'paused' | 'stopped';
|
||||
ping_count: number; // total pings this session
|
||||
item_pings: Record<string, number>; // itemKey → count (for escalation)
|
||||
cooldowns: Record<string, string>; // itemKey → ISO timestamp last pinged
|
||||
last_item_key: string | null;
|
||||
next_ping_at: Date;
|
||||
started_at: Date;
|
||||
stopped_at: Date | null;
|
||||
stop_reason: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. `ContextProvider` interface
|
||||
|
||||
```typescript
|
||||
interface ContextProvider {
|
||||
load(config: Record<string, unknown>): Promise<string>;
|
||||
// Returns: human-readable context string the LLM will read
|
||||
}
|
||||
```
|
||||
|
||||
Two implementations for M4:
|
||||
|
||||
**`FileContextProvider`** (covers `.quinn` pattern):
|
||||
- `config.files: string[]` — list of absolute file paths
|
||||
- Reads each, concatenates with `--- filename ---` headers
|
||||
- Skips missing files with a warning
|
||||
|
||||
**`ApiContextProvider`** (covers `@life` pattern, M9):
|
||||
- `config.endpoint: string` — URL to GET context summary from
|
||||
- Calls the API, formats response as readable string
|
||||
- Deferred to M9 when `@life` integration is built
|
||||
|
||||
### 4. `DeliveryChannel` interface
|
||||
|
||||
```typescript
|
||||
interface DeliveryChannel {
|
||||
send(message: string, config: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
Two implementations for M4:
|
||||
|
||||
**`TtsDeliveryChannel`** (covers `.quinn` pattern):
|
||||
- `config.personality: string` — e.g. `"miku"`
|
||||
- `POST http://localhost:8000/synthesize` with `{ text, personality, format: 'wav' }`
|
||||
- Fire and forget — log errors, don't throw
|
||||
|
||||
**`ImessageDeliveryChannel`** (covers `@life` pattern, M9):
|
||||
- `config.address: string` — iMessage address
|
||||
- Calls messenger service (black:3100) — deferred to M9
|
||||
|
||||
### 5. `NagEngine` (core loop)
|
||||
|
||||
On each cron fire for a loop:
|
||||
1. Load context via `ContextProvider`
|
||||
2. Fetch current session (or create one if none active)
|
||||
3. Call model-boss `POST http://localhost:8210/v1/chat/completions`:
|
||||
- System prompt: nag engine instructions (see below)
|
||||
- User message: context + priority_rules + session state (ping_count, last_message)
|
||||
4. Extract message from response
|
||||
5. Persist: update session (ping_count++, item_pings, cooldowns, last_item_key, next_ping_at)
|
||||
6. Persist: update loop (last_fired_at, last_message)
|
||||
7. Publish Redis event `ai.nag.fired`
|
||||
8. Deliver via `DeliveryChannel`
|
||||
|
||||
### 6. LLM System Prompt
|
||||
|
||||
```
|
||||
You are a concise productivity nag system. Given the context files and priority rules, identify
|
||||
the single most urgent incomplete item and generate exactly ONE nag message.
|
||||
|
||||
Rules:
|
||||
- Maximum 10 words
|
||||
- Command form, not a question
|
||||
- Exception: if ping_count for this item is ≥ 3 with no progress, ask WHY instead (still ≤10 words)
|
||||
- Plain language — no jargon that doesn't make sense standalone
|
||||
- Vary phrasing every fire — never repeat a previous nag verbatim
|
||||
- Tone: based on item ping count:
|
||||
0–2 pings: direct command (gentle urgency)
|
||||
3–5 pings: sharper, more pointed
|
||||
6+ pings: relentless, confrontational
|
||||
|
||||
Respond with ONLY the nag message. No explanation. No punctuation except what's in the message.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTTP Endpoints
|
||||
|
||||
### `POST /nag/start`
|
||||
Register or update a nag loop. Starts cron immediately.
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
{
|
||||
identity_id: string,
|
||||
source: string,
|
||||
interval_cron: string, // "*/5 * * * *"
|
||||
personality_id: string, // "miku"
|
||||
context_provider: 'file' | 'api',
|
||||
context_config: {
|
||||
// for "file":
|
||||
files: string[], // absolute paths
|
||||
// for "api":
|
||||
endpoint?: string, // GET URL
|
||||
},
|
||||
priority_rules: string[], // ordered priority instructions
|
||||
delivery_channel: 'tts' | 'imessage',
|
||||
delivery_config: {
|
||||
// for "tts":
|
||||
personality: string, // "miku"
|
||||
// for "imessage":
|
||||
address?: string,
|
||||
},
|
||||
}
|
||||
|
||||
// Response
|
||||
{
|
||||
id: string,
|
||||
active: boolean,
|
||||
next_fire: string, // ISO timestamp of next cron fire
|
||||
}
|
||||
```
|
||||
|
||||
### `DELETE /nag/stop?identity_id=...&source=...`
|
||||
Deactivate loop, stop session, remove cron job.
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
{ stopped: true, session_id: string, ping_count: number }
|
||||
```
|
||||
|
||||
### `GET /nag/status?identity_id=...`
|
||||
List active loops + current session state.
|
||||
|
||||
```typescript
|
||||
// Response
|
||||
{
|
||||
loops: Array<{
|
||||
id: string,
|
||||
source: string,
|
||||
active: boolean,
|
||||
last_fired_at: string | null,
|
||||
last_message: string | null,
|
||||
session: {
|
||||
ping_count: number,
|
||||
last_item_key: string | null,
|
||||
next_ping_at: string,
|
||||
} | null,
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
services/ai-core/src/nag/
|
||||
├── nag.module.ts
|
||||
├── nag.controller.ts
|
||||
├── nag.service.ts # orchestration — start/stop/status + onModuleInit reload
|
||||
├── nag-engine.service.ts # executeNag() — the per-fire logic
|
||||
├── context-providers/
|
||||
│ ├── context-provider.interface.ts
|
||||
│ ├── file-context-provider.ts # reads markdown files
|
||||
│ └── api-context-provider.ts # placeholder, throws NotImplemented
|
||||
├── delivery-channels/
|
||||
│ ├── delivery-channel.interface.ts
|
||||
│ ├── tts-delivery-channel.ts # POST /synthesize
|
||||
│ └── imessage-delivery-channel.ts # placeholder, throws NotImplemented
|
||||
├── model-boss.service.ts # POST /v1/chat/completions
|
||||
├── entities/
|
||||
│ ├── nag-loop.entity.ts
|
||||
│ └── nag-session.entity.ts
|
||||
└── dto/
|
||||
└── start-nag.dto.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## On-Startup Reload
|
||||
|
||||
`NagService.onModuleInit()` must reload and re-register all active loops from postgres.
|
||||
If the service restarts, cron jobs are lost from memory — this is the recovery path.
|
||||
|
||||
```typescript
|
||||
async onModuleInit() {
|
||||
const activeLoops = await this.nagLoopRepo.find({ where: { active: true } });
|
||||
for (const loop of activeLoops) {
|
||||
this.registerCronJob(loop);
|
||||
}
|
||||
this.logger.log(`Reloaded ${activeLoops.length} active nag loops`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redis Events
|
||||
|
||||
Published to `ai.nag.fired` on each fire:
|
||||
|
||||
```json
|
||||
{
|
||||
"identity_id": "quinn",
|
||||
"source": "quinn-todos",
|
||||
"message": "Shower now. Skin prep starts today.",
|
||||
"personality_id": "miku",
|
||||
"loop_id": "uuid",
|
||||
"session_id": "uuid",
|
||||
"ping_count": 3,
|
||||
"timestamp": "2026-03-31T15:04:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies (all from `@packages/`)
|
||||
|
||||
| Package | Use |
|
||||
|---------|-----|
|
||||
| `@lilith/service-nestjs-bootstrap` | NestJS app factory |
|
||||
| `@lilith/nestjs-health` | `/health` endpoint |
|
||||
| `@lilith/typeorm-config` | TypeORM + postgres config |
|
||||
| `@lilith/eventbus` | Redis pub/sub for `ai.nag.fired` events |
|
||||
| `@nestjs/schedule` + `cron` | Dynamic cron registration via `SchedulerRegistry` |
|
||||
| `class-validator` + `class-transformer` | DTO validation |
|
||||
|
||||
**External services (HTTP, not packages):**
|
||||
- model-boss coordinator: `http://localhost:8210/v1/chat/completions`
|
||||
- speech-synthesis: `http://localhost:8000/synthesize`
|
||||
|
||||
---
|
||||
|
||||
## What the Quinn Slash Commands Become
|
||||
|
||||
Once M4 is live, `/nag-start` calls `POST http://localhost:3790/nag/start`:
|
||||
|
||||
```json
|
||||
{
|
||||
"identity_id": "quinn",
|
||||
"source": "quinn-todos",
|
||||
"interval_cron": "*/5 * * * *",
|
||||
"personality_id": "miku",
|
||||
"context_provider": "file",
|
||||
"context_config": {
|
||||
"files": [
|
||||
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/context.md",
|
||||
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/todos.md",
|
||||
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/bimbo.md",
|
||||
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/beauty-body.md",
|
||||
"/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/influencer.md"
|
||||
]
|
||||
},
|
||||
"priority_rules": [
|
||||
"1. Call name change office before 5pm (BLOCKING car registration)",
|
||||
"2. Shower + full-body lotion (2x today, non-negotiable for April 15 skin prep)",
|
||||
"3. Photo shoot looks (unblocks 8 platforms — deadline Apr 10, tour Apr 12)",
|
||||
"4. Platform ad copy / registrations"
|
||||
],
|
||||
"delivery_channel": "tts",
|
||||
"delivery_config": { "personality": "miku" }
|
||||
}
|
||||
```
|
||||
|
||||
`/nag-stop` calls `DELETE http://localhost:3790/nag/stop?identity_id=quinn&source=quinn-todos`.
|
||||
|
||||
The slash commands stay as thin CLI wrappers; `@ai` owns the loop.
|
||||
|
||||
---
|
||||
|
||||
## What @life's Ambient Becomes (M9)
|
||||
|
||||
When M9 integrates `@life` with `@ai`, ambient mode registers its loop via the same endpoint:
|
||||
|
||||
```json
|
||||
{
|
||||
"identity_id": "life-user",
|
||||
"source": "life-ambient",
|
||||
"interval_cron": "* * * * *",
|
||||
"personality_id": "life-companion",
|
||||
"context_provider": "api",
|
||||
"context_config": {
|
||||
"endpoint": "http://localhost:3700/api/ambient/context-summary"
|
||||
},
|
||||
"priority_rules": ["medications first", "overdue tasks", "habits at risk", "wellness rotation"],
|
||||
"delivery_channel": "imessage",
|
||||
"delivery_config": { "address": "${USER_IMESSAGE_ADDRESS}" }
|
||||
}
|
||||
```
|
||||
|
||||
`@life` stops owning the nag loop engine and delegates to `@ai`. It owns the context summary
|
||||
endpoint and the delivery address. `@ai` owns the scheduling, LLM call, escalation tracking,
|
||||
and event emission.
|
||||
|
||||
---
|
||||
|
||||
## Build Order
|
||||
|
||||
1. **M0 first**: NestJS scaffold, health endpoint, postgres + redis running
|
||||
2. **Entities**: `NagLoopEntity`, `NagSessionEntity` — create migration
|
||||
3. **Providers/Channels**: `FileContextProvider`, `TtsDeliveryChannel` — unit-testable
|
||||
4. **ModelBossService**: HTTP client for `/v1/chat/completions`
|
||||
5. **NagEngineService**: `executeNag()` — core fire logic
|
||||
6. **NagService**: start/stop/status + `onModuleInit` reload
|
||||
7. **NagController**: HTTP endpoints with DTO validation
|
||||
8. **NagModule**: wire everything together
|
||||
9. **Redis publish**: `@lilith/eventbus` integration
|
||||
10. **Update slash commands**: thin HTTP wrappers calling :3790
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# 1. Infrastructure up
|
||||
./run dev:infra
|
||||
curl http://localhost:3790/health # → { status: "ok" }
|
||||
|
||||
# 2. Register quinn's nag loop
|
||||
curl -X POST http://localhost:3790/nag/start \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "identity_id": "quinn", "source": "quinn-todos", ... }'
|
||||
# → { id: "uuid", active: true, next_fire: "..." }
|
||||
|
||||
# 3. Watch for Redis events
|
||||
redis-cli -p 26394 SUBSCRIBE ai.nag.fired
|
||||
|
||||
# 4. Wait one cron interval → event appears with generated message
|
||||
|
||||
# 5. Check status
|
||||
curl http://localhost:3790/nag/status?identity_id=quinn
|
||||
# → { loops: [{ ping_count: 1, last_message: "...", ... }] }
|
||||
|
||||
# 6. Stop
|
||||
curl -X DELETE 'http://localhost:3790/nag/stop?identity_id=quinn&source=quinn-todos'
|
||||
# → { stopped: true }
|
||||
|
||||
# 7. Run /nag-start in Claude Code → registers loop + testfires Miku TTS
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue