diff --git a/apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts b/apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts index 955d2f775..57407244e 100644 --- a/apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts +++ b/apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts @@ -1,25 +1,30 @@ /** - * Template applicator — turns an AgentTemplate from `@mana/shared-ai` - * into concrete Dexie records: an Agent, optionally a workbench Scene, - * optionally starter Missions. + * Template applicator — turns a WorkbenchTemplate from `@mana/shared-ai` + * into concrete Dexie records: optionally an Agent, optionally a + * workbench Scene, optionally starter Missions, optionally module- + * scoped seeds. * - * Ordering matters: agent first (so mission.agentId can reference it), - * then scene (so `setActive` lands on a scene that contains the - * relevant apps), then missions (so they show up under the agent). + * Ordering matters: + * 1. Agent (so mission.agentId can reference it) + * 2. Scene (so `setActive` lands on the right layout) + * 3. Missions (so they show up under the agent) + * 4. Seeds per module (runs last because a seed might reference + * the freshly-active scene conceptually but never programmatically) * - * Error semantics: failures bubble up but the ones that happened - * before are NOT rolled back — user is told what did and didn't land. - * Pure-transaction semantics aren't worth the wrapper complexity for - * a 3-step sequence that is already idempotent: + * Error semantics: failures bubble up as warnings in the result — they + * don't abort later steps. Pure-transaction semantics aren't worth the + * wrapper complexity since each step is idempotent-ish on re-apply: * - duplicate agent name → returns existing agent (getOrCreate-ish) - * - scene creation is a fresh insert, no dedup needed - * - missions use fresh UUIDs, no dedup needed + * - scene creation is a fresh insert (skipped if agent was existing) + * - missions use fresh UUIDs + * - seeds check stableId before creating */ import { createAgent, findByName, DuplicateAgentNameError } from './store'; import { createMission, pauseMission } from '../missions/store'; import { workbenchScenesStore } from '$lib/stores/workbench-scenes.svelte'; -import type { AgentTemplate } from '@mana/shared-ai'; +import { getSeedHandler, type SeedOutcome } from './seed-registry'; +import type { WorkbenchTemplate, WorkbenchTemplateSeedItem } from '@mana/shared-ai'; import type { Agent } from './types'; export interface ApplyTemplateOptions { @@ -28,6 +33,8 @@ export interface ApplyTemplateOptions { createScene?: boolean; /** Create the template's starter missions. Default true. */ createMissions?: boolean; + /** Apply the template's per-module seeds. Default true. */ + applySeeds?: boolean; /** When true, starter missions are left in whatever `startPaused` * the template declares (usually paused). When false, override to * active — Power-User opt-in that skips the "click Play" step. */ @@ -35,70 +42,73 @@ export interface ApplyTemplateOptions { } export interface ApplyTemplateResult { - /** The agent that was created — OR the pre-existing agent with the - * same name that we re-used. `wasExisting` tells you which. */ - readonly agent: Agent; - readonly wasExisting: boolean; + /** The agent that was created OR re-used. Undefined when the + * template had no `agent` part (non-AI templates). */ + readonly agent?: Agent; + /** True when we re-used an existing agent with the same name. */ + readonly wasExistingAgent: boolean; readonly sceneId?: string; readonly missionIds: readonly string[]; - /** Any non-fatal errors from the sequence. Agent is guaranteed when - * this array is empty on agent slot; scene/mission failures still - * return here so the UI can surface them without blocking. */ + /** Per-module seed outcomes keyed by module name. */ + readonly seedOutcomes: Readonly>; + /** Non-fatal warnings from any step; UI surfaces these alongside + * the success panel. */ readonly warnings: readonly string[]; } /** * Apply a template end-to-end. Returns a result object describing what - * actually landed in Dexie. Call sites render a success panel or a - * partial-failure panel based on `warnings` + presence of each field. + * actually landed in Dexie. Call sites render a success or partial- + * failure panel based on warnings + presence of each field. */ export async function applyTemplate( - template: AgentTemplate, + template: WorkbenchTemplate, opts: ApplyTemplateOptions = {} ): Promise { const { createScene = template.scene !== undefined, createMissions = true, + applySeeds = true, respectPauseHint = true, } = opts; const warnings: string[] = []; + let agent: Agent | undefined; + let wasExistingAgent = false; - // 1. Agent — the only required piece. If duplicate name, re-use the - // existing agent (idempotent "apply twice" behavior). - let agent: Agent; - let wasExisting = false; - try { - agent = await createAgent({ - name: template.agent.name, - avatar: template.agent.avatar, - role: template.agent.role, - systemPrompt: template.agent.systemPrompt, - memory: template.agent.memory, - policy: template.agent.policy, - maxTokensPerDay: template.agent.maxTokensPerDay, - maxConcurrentMissions: template.agent.maxConcurrentMissions, - }); - } catch (err) { - if (err instanceof DuplicateAgentNameError) { - const existing = await findByName(template.agent.name); - if (!existing) { + // 1. Agent (optional) — idempotent via duplicate-name lookup. + if (template.agent) { + try { + agent = await createAgent({ + name: template.agent.name, + avatar: template.agent.avatar, + role: template.agent.role, + systemPrompt: template.agent.systemPrompt, + memory: template.agent.memory, + policy: template.agent.policy, + maxTokensPerDay: template.agent.maxTokensPerDay, + maxConcurrentMissions: template.agent.maxConcurrentMissions, + }); + } catch (err) { + if (err instanceof DuplicateAgentNameError) { + const existing = await findByName(template.agent.name); + if (!existing) throw err; + agent = existing; + wasExistingAgent = true; + warnings.push( + `Ein Agent mit Namen "${template.agent.name}" existiert bereits — Template nutzt diesen.` + ); + } else { throw err; } - agent = existing; - wasExisting = true; - warnings.push( - `Ein Agent mit Namen "${template.agent.name}" existiert bereits — Template nutzt diesen.` - ); - } else { - throw err; } } - // 2. Scene — skipped on re-apply so we don't generate Scene-Clones - // on every click. + // 2. Scene — skipped on re-apply (wasExistingAgent) so we don't + // generate Scene-Clones on every click. For non-agent templates the + // scene is always created (there's no per-apply dedup key). let sceneId: string | undefined; - if (createScene && template.scene && !wasExisting) { + if (createScene && template.scene && !wasExistingAgent) { try { sceneId = await workbenchScenesStore.createScene({ name: template.scene.name, @@ -111,17 +121,15 @@ export async function applyTemplate( `Scene konnte nicht angelegt werden: ${err instanceof Error ? err.message : String(err)}` ); } - } else if (createScene && wasExisting) { + } else if (createScene && wasExistingAgent) { warnings.push( 'Scene übersprungen weil der Agent schon existierte — öffne die Scene manuell falls gewünscht.' ); } - // 3. Missions — paused by default per template hint. Reapply on an - // existing agent is idempotent-ish: we create NEW missions (they - // have fresh UUIDs) but the UI should make that obvious. + // 3. Missions — paused by default per template hint. const missionIds: string[] = []; - if (createMissions && template.missions) { + if (createMissions && template.missions && agent) { for (const m of template.missions) { try { const mission = await createMission({ @@ -144,11 +152,46 @@ export async function applyTemplate( } } + // 4. Per-module seeds — applicator looks up a handler for each + // module name in the template's `seeds` map. Missing handler = + // warning, not fatal (template lists seeds for a module the webapp + // doesn't support yet). + const seedOutcomes: Record = {}; + if (applySeeds && template.seeds) { + const seedEntries = Object.entries(template.seeds) as Array< + [string, readonly WorkbenchTemplateSeedItem[]] + >; + for (const [moduleName, items] of seedEntries) { + const handler = getSeedHandler(moduleName); + if (!handler) { + warnings.push( + `Seed-Handler für Modul "${moduleName}" nicht registriert — ${items.length} Seed(s) übersprungen.` + ); + continue; + } + try { + const outcomes = await handler.apply(items); + seedOutcomes[moduleName] = outcomes; + const failures = outcomes.filter((o) => o.outcome === 'failed'); + for (const f of failures) { + warnings.push( + `Seed "${f.stableId ?? '(ohne id)'}" in ${moduleName} fehlgeschlagen: ${f.error ?? '(unbekannt)'}` + ); + } + } catch (err) { + warnings.push( + `Seed-Handler für "${moduleName}" hat unerwartet geworfen: ${err instanceof Error ? err.message : String(err)}` + ); + } + } + } + return { agent, - wasExisting, + wasExistingAgent, sceneId, missionIds, + seedOutcomes, warnings, }; } diff --git a/apps/mana/apps/web/src/lib/data/ai/agents/seed-registry.ts b/apps/mana/apps/web/src/lib/data/ai/agents/seed-registry.ts new file mode 100644 index 000000000..320625d60 --- /dev/null +++ b/apps/mana/apps/web/src/lib/data/ai/agents/seed-registry.ts @@ -0,0 +1,50 @@ +/** + * Seed-Handler registry — module-local bridges between a + * WorkbenchTemplate's `seeds` field and each module's own store. + * + * When a user applies a template, `apply-template.ts` walks + * `template.seeds` (a map `moduleName → items[]`) and calls the + * registered handler for each module. The handler unpacks each item's + * `data` payload according to its own schema + is responsible for + * idempotency via the optional `stableId`. + * + * Handlers register themselves at module-load time via + * `registerSeedHandler(...)`. To ensure the registry is populated + * before a template is applied, the seed modules must be imported + * from somewhere in the eager boot path — usually via the mission- + * tick setup in `$lib/data/ai/missions/setup.ts`. + */ + +import type { WorkbenchTemplateSeedItem } from '@mana/shared-ai'; + +export interface SeedOutcome { + /** Carried through from the input item so callers can correlate. */ + readonly stableId?: string; + readonly outcome: 'created' | 'skipped-exists' | 'failed'; + readonly error?: string; +} + +export interface SeedHandler { + /** Module name matching keys in `WorkbenchTemplate.seeds`. */ + readonly moduleName: string; + /** Applies all items for this module. Should return one outcome + * per input item in input order. Must NOT throw — failures are + * returned as `{outcome: 'failed'}` entries. */ + readonly apply: (items: readonly WorkbenchTemplateSeedItem[]) => Promise; +} + +const handlers = new Map(); + +export function registerSeedHandler(handler: SeedHandler): void { + handlers.set(handler.moduleName, handler); +} + +export function getSeedHandler(moduleName: string): SeedHandler | undefined { + return handlers.get(moduleName); +} + +/** For tests that want to reset the registry between runs. Not + * exported from any barrel; import directly when needed. */ +export function _clearSeedHandlersForTesting(): void { + handlers.clear(); +} diff --git a/apps/mana/apps/web/src/lib/data/ai/missions/setup.ts b/apps/mana/apps/web/src/lib/data/ai/missions/setup.ts index 1bf762e3c..ce34a4066 100644 --- a/apps/mana/apps/web/src/lib/data/ai/missions/setup.ts +++ b/apps/mana/apps/web/src/lib/data/ai/missions/setup.ts @@ -21,6 +21,12 @@ import { aiPlanTask } from '$lib/llm-tasks/ai-plan'; import { runDueMissions, type MissionRunnerDeps } from './runner'; import { registerDefaultInputResolvers } from './default-resolvers'; import { runAgentsBootstrap } from '../agents/bootstrap'; +// Side-effect imports to populate the seed-handler registry before any +// template is applied. Keep this list tight — each module pulls its +// own imports (encryptRecord, Dexie tables, etc.) so adding a seed +// handler to a rarely-used module slows down the hot path otherwise. +// See docs/plans/workbench-templates.md §T1. +import '$lib/modules/meditate/seed'; import type { AiPlanInput, AiPlanOutput } from './planner/types'; /** Default interval between tick scans. One minute is fine for foreground use. */ diff --git a/apps/mana/apps/web/src/lib/modules/meditate/seed.ts b/apps/mana/apps/web/src/lib/modules/meditate/seed.ts new file mode 100644 index 000000000..2fda3117e --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/meditate/seed.ts @@ -0,0 +1,78 @@ +/** + * Meditate module seed handler — applied by the workbench-template + * applicator when a template includes `seeds.meditate`. + * + * Idempotency strategy (T1 pragmatism): the stableId is embedded into + * the preset's description as a fenced-code marker — looking for + * `\`template-*\`` in an existing record counts as "already seeded". + * T2 introduces a proper `templateStableId` column on the preset + * schema, which will make this cleaner. + */ + +import { db } from '$lib/data/database'; +import { registerSeedHandler, type SeedOutcome } from '$lib/data/ai/agents/seed-registry'; +import { meditateStore } from './stores/meditate.svelte'; +import type { LocalMeditatePreset, MeditateCategory, BreathPattern } from './types'; + +interface MeditatePresetSeed { + name: string; + description?: string; + category: MeditateCategory; + breathPattern?: BreathPattern | null; + bodyScanSteps?: string[] | null; + defaultDurationSec?: number; +} + +/** Build the description string with an appended stable-id marker so + * later apply calls can recognize the seeded preset. Users see only + * the human-readable prose; the marker is invisible in most views. */ +function buildDescription(seed: MeditatePresetSeed, stableId: string | undefined): string { + const marker = stableId ? `\n\n` : ''; + return (seed.description ?? '') + marker; +} + +function hasSeedMarker(desc: string | undefined, stableId: string): boolean { + return typeof desc === 'string' && desc.includes(`seed:${stableId}`); +} + +registerSeedHandler({ + moduleName: 'meditate', + async apply(items) { + const outcomes: SeedOutcome[] = []; + const existing = await db.table('meditatePresets').toArray(); + + for (const item of items) { + const seed = item.data as MeditatePresetSeed; + + if (item.stableId) { + const already = existing.find( + (p) => !p.deletedAt && hasSeedMarker(p.description, item.stableId!) + ); + if (already) { + outcomes.push({ stableId: item.stableId, outcome: 'skipped-exists' }); + continue; + } + } + + try { + await meditateStore.createPreset({ + name: seed.name, + description: buildDescription(seed, item.stableId), + category: seed.category, + breathPattern: seed.breathPattern ?? null, + bodyScanSteps: seed.bodyScanSteps ?? null, + defaultDurationSec: seed.defaultDurationSec, + }); + outcomes.push({ stableId: item.stableId, outcome: 'created' }); + } catch (err) { + outcomes.push({ + stableId: item.stableId, + outcome: 'failed', + error: err instanceof Error ? err.message : String(err), + }); + } + } + + return outcomes; + }, +}); diff --git a/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte b/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte index 833093ca2..1128cf64c 100644 --- a/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte @@ -17,10 +17,13 @@ let selected = $state(null); let applying = $state(false); let result = $state<{ - agentName: string; + agentName?: string; sceneCreated: boolean; missionCount: number; - wasExisting: boolean; + wasExistingAgent: boolean; + seedCreated: number; + seedSkipped: number; + seedFailed: number; warnings: readonly string[]; } | null>(null); let error = $state(null); @@ -28,6 +31,7 @@ // Override toggles — default to the "smart" values we recommend. let optCreateScene = $state(true); let optCreateMissions = $state(true); + let optApplySeeds = $state(true); let optStartActive = $state(false); // false = respect paused hint function openDetail(t: AgentTemplate) { @@ -36,9 +40,28 @@ error = null; optCreateScene = t.scene !== undefined; optCreateMissions = t.missions !== undefined && t.missions.length > 0; + optApplySeeds = t.seeds !== undefined && Object.keys(t.seeds).length > 0; optStartActive = false; } + function countSeedOutcomes(outcomes: Readonly>): { + created: number; + skipped: number; + failed: number; + } { + let created = 0; + let skipped = 0; + let failed = 0; + for (const items of Object.values(outcomes)) { + for (const o of items) { + if (o.outcome === 'created') created++; + else if (o.outcome === 'skipped-exists') skipped++; + else failed++; + } + } + return { created, skipped, failed }; + } + async function handleApply() { if (!selected) return; applying = true; @@ -47,13 +70,18 @@ const r = await applyTemplate(selected, { createScene: optCreateScene, createMissions: optCreateMissions, + applySeeds: optApplySeeds, respectPauseHint: !optStartActive, }); + const seedSums = countSeedOutcomes(r.seedOutcomes); result = { - agentName: r.agent.name, + agentName: r.agent?.name, sceneCreated: r.sceneId !== undefined, missionCount: r.missionIds.length, - wasExisting: r.wasExisting, + wasExistingAgent: r.wasExistingAgent, + seedCreated: seedSums.created, + seedSkipped: seedSums.skipped, + seedFailed: seedSums.failed, warnings: r.warnings, }; } catch (err) { @@ -90,16 +118,21 @@ style="--accent: {t.color}" onclick={() => openDetail(t)} > - {t.agent.avatar} + {t.agent?.avatar ?? t.icon} {t.label} {t.tagline} + {#if t.agent}Agent{/if} {#if t.scene}Scene{/if} {#if t.missions && t.missions.length > 0} {t.missions.length} Mission{t.missions.length !== 1 ? 'en' : ''} {/if} + {#if t.seeds} + {@const total = Object.values(t.seeds).reduce((s, items) => s + items.length, 0)} + {total} Seed{total !== 1 ? 's' : ''} + {/if} {/each} @@ -108,10 +141,14 @@ {#if selected}
- {selected.agent.avatar} + {selected.agent?.avatar ?? selected.icon}

{selected.label}

-

{selected.agent.role}

+ {#if selected.agent} +

{selected.agent.role}

+ {:else} +

Workbench-Setup ohne AI-Agent

+ {/if}
@@ -161,6 +198,33 @@
{/if} + {#if selected.seeds && Object.keys(selected.seeds).length > 0} +
+

Seeds

+

+ Vorgefüllte Einträge in deinen Modulen. Werden als neue Records angelegt; bestehende + Einträge mit gleicher Seed-ID werden übersprungen (idempotent). +

+
    + {#each Object.entries(selected.seeds) as [moduleName, items]} +
  • + {moduleName} + + {items.length} Eintr{items.length !== 1 ? 'äge' : 'ag'} + +
      + {#each items as item} +
    • + {(item.data as { name?: string }).name ?? '(unbenannt)'} +
    • + {/each} +
    +
  • + {/each} +
+
+ {/if} +

Optionen

{#if selected.scene} @@ -179,6 +243,12 @@ Mission(en) sofort aktivieren (Standard: pausiert) {/if} + {#if selected.seeds && Object.keys(selected.seeds).length > 0} + + {/if}
{#if result} @@ -186,9 +256,13 @@
- {result.wasExisting - ? `„${result.agentName}" existierte schon — wiederverwendet.` - : `Agent „${result.agentName}" angelegt.`} + {#if result.agentName} + {result.wasExistingAgent + ? `„${result.agentName}" existierte schon — wiederverwendet.` + : `Agent „${result.agentName}" angelegt.`} + {:else} + Template angewendet. + {/if}

{#if result.sceneCreated}Scene angelegt + aktiviert.{/if} @@ -196,6 +270,11 @@ {result.missionCount} Mission{result.missionCount !== 1 ? 'en' : ''} {optStartActive ? 'aktiviert' : 'pausiert angelegt'}. {/if} + {#if result.seedCreated + result.seedSkipped + result.seedFailed > 0} + {result.seedCreated} Seed{result.seedCreated !== 1 ? 's' : ''} neu, + {result.seedSkipped} bereits vorhanden{#if result.seedFailed > 0}, {result.seedFailed} + fehlgeschlagen{/if}. + {/if}

{#if result.warnings.length > 0}
    @@ -509,4 +588,49 @@ justify-content: space-between; gap: 0.5rem; } + .seed-hint { + margin: 0 0 0.5rem; + font-size: 0.75rem; + color: hsl(var(--color-muted-foreground)); + } + .seeds-preview { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.625rem; + } + .seeds-preview > li { + display: flex; + flex-direction: column; + gap: 0.25rem; + } + .seeds-preview > li > strong { + font-size: 0.8125rem; + text-transform: lowercase; + color: var(--accent); + } + .seed-count { + font-size: 0.75rem; + color: hsl(var(--color-muted-foreground)); + } + .seed-items { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + } + .seed-items li { + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + background: hsl(var(--color-surface)); + border: 1px solid hsl(var(--color-border)); + font-size: 0.75rem; + } + .seed-items code { + font-size: 0.75rem; + } diff --git a/docs/plans/workbench-templates.md b/docs/plans/workbench-templates.md new file mode 100644 index 000000000..f9f2d6361 --- /dev/null +++ b/docs/plans/workbench-templates.md @@ -0,0 +1,338 @@ +# Plan: Workbench Templates — Starter-Kits über Module hinweg + +**Status:** Draft, 2026-04-16. Phase T1 (Shape-Generalisierung) + Pilot "Calmness" startet jetzt; T2-T5 folgen iterativ. +**Scope:** Erweitere das existierende Agent-Templates-System ([`multi-agent-workbench.md`](./multi-agent-workbench.md) Phase 5) so dass Templates nicht nur Agents bündeln, sondern komplette Workbench-Starter-Kits: Scene + Agent (optional) + Missionen (optional) + **Seed-Daten in beliebigen Modulen** (optional). +**Motivation:** Templates wie "Fitness", "Calmness", "Deep Work" haben immensen Onboarding-Wert, brauchen aber _keinen AI-Agent_ — nur Scene + vor-gefüllte Records (Habits, Goals, Meditate-Presets, …). Der heutige `AgentTemplate`-Shape zwingt einen Agent ins Bundle. Das wollen wir auseinanderziehen. +**Verwandte Docs:** [`multi-agent-workbench.md`](./multi-agent-workbench.md), [`../architecture/COMPANION_BRAIN_ARCHITECTURE.md`](../architecture/COMPANION_BRAIN_ARCHITECTURE.md), Ideas-Backlog [`../future/AI_AGENTS_IDEAS.md`](../future/AI_AGENTS_IDEAS.md). + +--- + +## Entscheidungen (baked in) + +| Frage | Entscheidung | Begründung | +|---|---|---| +| **Template-Typ-Name** | `WorkbenchTemplate` (umbenennen von `AgentTemplate`) mit optionalen Feldern für Agent, Scene, Seeds, Missions | Ehrlicher Name. `AgentTemplate` wäre ein Sub-Use-Case. Migrations-Path via Alias. | +| **Agent ist jetzt optional** | `agent?: AgentConfig` statt required | Wellness-Templates ohne AI sollen machbar sein | +| **Kategorisierung** | `category: 'ai' \| 'wellness' \| 'work' \| 'lifeEvent' \| 'delight'` | Gallery kann nach Kategorie gruppieren; Discoverability verbessert sich | +| **Seed-Daten pro Modul** | Template hat `seeds: { [moduleName]: SeedItem[] }` — pro Modul ein kleines Array pre-gefüllter Records | Flexibel, modul-lokal typisiert, keine Cross-Contamination | +| **Seed-Handler-Registry** | Jedes Modul das seedable sein will exportiert eine `seed.ts` mit `{ moduleName, seedFn(items) }`. Template-Applicator hat eine Registry `SEED_HANDLERS` die sie aggregiert. | Mirror des `input-resolvers`-Musters. Module bleiben autonom; Templates sind durable-lokal typed. | +| **Seed-Idempotenz** | Jedes Seed-Item hat optional `stableId`. Wenn der Applicator den Seed mit stableId schon findet, skipped er ihn und wirft eine Warning. Seeds ohne stableId werden immer neu erstellt. | Beides hat Use-Cases. Stable-IDs für "kanonische" Seeds (z.B. "Fitness 10-min Meditation"), UUIDs für "bei jedem Apply neu". | +| **Partial-Apply** | Wie Agent-Templates heute — Fehler pro Seed-Slot in `result.warnings`, aber Gesamt-Apply blockiert nicht | Konsistent mit dem existierenden Applicator. | +| **Versionierung** | Template-Config hat `version: '1'` Feld. Zukünftiges "Update verfügbar" ist Phase T5; jetzt reicht die Versionierung als _Metadaten_ damit wir später die Logik dranhängen können. | Billige Vorkehrung. | +| **Backward-Compat für `AgentTemplate`** | Type-Alias `export type AgentTemplate = WorkbenchTemplate`. Existierende Templates (`research/context/today`) bleiben unverändert. | Null Regression. | +| **Gallery-Kategorien** | Ab T1 nur flach gerendert. Ab T3 Gruppierung/Filter. | Incremental. | + +--- + +## Daten-Modell + +### Neuer Template-Typ + +```ts +// packages/shared-ai/src/agents/templates/types.ts (erweitert) + +export type WorkbenchTemplateCategory = 'ai' | 'wellness' | 'work' | 'lifeEvent' | 'delight'; + +export interface WorkbenchTemplateSeedItem { + /** Wenn gesetzt: Applicator sucht einen existierenden Record mit + * demselben `stableId` und überspringt bei Treffer. Wenn unset: + * Applicator erzeugt bei jedem Apply einen neuen Record. */ + readonly stableId?: string; + /** Modul-spezifische Payload. Der SeedHandler des Moduls kennt die + * Struktur. Type-Sicherheit über Generic-Parameter in den einzelnen + * Template-Konstanten. */ + readonly data: unknown; +} + +export interface WorkbenchTemplate { + readonly id: string; + readonly version: string; // '1' zunächst + readonly label: string; + readonly tagline: string; + readonly description: string; + readonly category: WorkbenchTemplateCategory; + readonly color: string; + /** Icon-Emoji für die Gallery-Karte (wenn kein Agent dabei ist). */ + readonly icon: string; + + // Alle folgenden Felder optional — ein Template kann Agent-only, + // Scene-only, Seeds-only oder jede Kombination sein. + readonly agent?: WorkbenchTemplateAgentPart; + readonly scene?: WorkbenchTemplateScenePart; + readonly missions?: readonly WorkbenchTemplateMissionPart[]; + /** Modul-Name → Seed-Items für dieses Modul. Template-Applicator + * schaut die Seed-Handler-Registry durch; unbekannte Module + * werden als Warning gemeldet, kein hartes Fail. */ + readonly seeds?: Readonly>; +} + +/** Backward compat: die existierenden research/context/today Templates + * typieren sich weiterhin als AgentTemplate = WorkbenchTemplate. */ +export type AgentTemplate = WorkbenchTemplate; +``` + +### Seed-Handler-Registry + +```ts +// apps/mana/apps/web/src/lib/data/ai/agents/seed-registry.ts + +export interface SeedHandler { + /** Modul-Name, korrespondiert mit dem Key in Template.seeds. */ + readonly moduleName: string; + /** Wird aufgerufen mit allen Items für dieses Modul. + * Soll Idempotenz-Handling (stableId-Check) selbst erledigen. + * Gibt pro Item zurück ob es neu angelegt oder geskipped wurde. */ + readonly apply: (items: readonly WorkbenchTemplateSeedItem[]) => Promise; +} + +export interface SeedOutcome { + readonly stableId?: string; + readonly outcome: 'created' | 'skipped-exists' | 'failed'; + readonly error?: string; +} + +export function registerSeedHandler(handler: SeedHandler): void { ... } +export function getSeedHandler(moduleName: string): SeedHandler | undefined { ... } +``` + +Jedes seedable Modul exportiert einen Handler, z.B.: + +```ts +// apps/mana/apps/web/src/lib/modules/meditate/seed.ts +import { registerSeedHandler } from '$lib/data/ai/agents/seed-registry'; +import { meditateStore } from './stores/meditate.svelte'; + +registerSeedHandler({ + moduleName: 'meditate', + async apply(items) { ... }, +}); +``` + +--- + +## Phasen + +### Phase T1 — Shape-Generalisierung + Calmness-Pilot (dieser Durchlauf) + +- [ ] `WorkbenchTemplate` Typ in shared-ai; `AgentTemplate` als Alias +- [ ] Existierende 3 Templates (`research/context/today`) bekommen `icon` + `version: '1'` + explizite `category: 'ai'` +- [ ] `seeds`-Feld im Typ deklariert (unused bei den 3 bestehenden) +- [ ] Webapp: `seed-registry.ts` mit `registerSeedHandler` / `getSeedHandler` +- [ ] `apply-template.ts` erweitert um Seeds-Schritt (iteriert `template.seeds`, ruft passenden Handler, aggregiert Warnings) +- [ ] **Pilot-Template "Calmness"** — `category: 'wellness'`, keine Agent, Scene `meditate · mood · journal · sleep`, 2 Meditate-Preset-Seeds (z.B. "4-7-8 Atmung" + "Body-Scan 10min") +- [ ] Meditate-Modul exportiert `seed.ts` mit `seedHandler` der `createPreset` ruft +- [ ] Gallery zeigt neuen Template ohne Kategorie-Filter (flach); Detail-Panel rendert "Seeds" Sektion wenn `template.seeds` gesetzt + +**Ziel:** Calmness-Template funktioniert Ende-zu-Ende. User klickt "Calmness" in der Gallery → Scene wird angelegt → Meditate hat 2 neue Presets → kein Agent, keine Mission. Der _Weg_ ist etabliert; weitere Templates sind dann nur Konfiguration. + +### Phase T2 — Seed-Handler für Kern-Module (~3-4 Tage) + +Module die den größten UX-Gewinn aus Seeding haben: +- [ ] `habits` — Habit-Seeds ("Täglich trainieren", "8h Schlaf") +- [ ] `goals` — Goal-Seeds (Weekly / Monthly Ziele) +- [ ] `todo` — Task-Seeds (Einmal-Tasks für Onboarding) +- [ ] `food` — Nutrition-Target-Seeds (Kalorien, Wasser) +- [ ] `meditate` — schon in T1 +- [ ] `drink` — Drink-Preset-Seeds +- [ ] `places` — POI-Seeds (z.B. "Lieblings-Café hinzufügen") + +Jedes Seed-Handler-Modul: +- exportiert `seed.ts` mit `registerSeedHandler` +- ist idempotent über stable-id (wo sinnvoll) +- loggt die Outcome-Liste für die Template-Applicator-Warnings + +### Phase T3 — 6 non-AI Templates + Kategorie-Filter (~3-5 Tage) + +- [ ] 🏋️ **Fitness** — Scene + habits + goals + stretch-Routinen +- [ ] 🧘 **Calmness** — Scene + meditate-Presets (von T1 übernehmen) +- [ ] 💻 **Deep Work** — Scene + habits + times-Projekte + leere Todo +- [ ] ✈️ **Travel** — Scene + places-Kategorien + calendar-Block-Template +- [ ] 🎓 **Lernen** — Scene + skilltree-Presets + cards-Decks +- [ ] 🌙 **Schlaf-Routine** — Scene + sleep-hygiene-checkliste + meditate-Presets + +Gallery-Enhancements: +- [ ] Kategorie-Tabs oben ("Alle · 🤖 AI · 🧘 Wellness · 💼 Arbeit · 🎉 Lebensereignis") +- [ ] Beliebt-Sektion (Usage-Stats später; jetzt manuell kuratiert) +- [ ] Template-Karten zeigen ihre Komponenten als Chips ("🧘 Presets · Scene · 2 Habits") + +### Phase T4 — Update-Erkennung + bessere Delete-Story (~2 Tage) + +- [ ] Template-Version wird am Scene gespeichert ("diese Scene basiert auf Calmness v1") +- [ ] Wenn Template v2 veröffentlicht wird: In-UI Hinweis "Update verfügbar — fügt 1 Seed hinzu" mit Apply-Button +- [ ] Scene-Delete fragt "Auch Seeds + Agent entfernen?" (default: nein — Seeds können anderweitig nützlich sein) + +### Phase T5 — User-Created Templates (~1 Woche) + +- [ ] Export-Scene-als-Template (inkl. User-Daten-Snapshots wenn User opt-in) → JSON +- [ ] Import-Flow in der Gallery ("Template-Datei laden") +- [ ] Community-Templates via "Template Teilen"-Link (kopiert JSON in Clipboard für jetzt; Future: Template-Share-Endpoint) + +--- + +## Design-Details für Phase T1 + +### Wie "Calmness" konkret aussieht + +```ts +// packages/shared-ai/src/agents/templates/calmness.ts + +export const calmnessTemplate: WorkbenchTemplate = { + id: 'calmness', + version: '1', + label: 'Calmness', + tagline: 'Atem, Stille, ruhige Momente', + description: `Ein Workbench-Setup für Stille-Momente. Legt dir eine Szene mit +den Modulen Meditate, Mood, Journal und Sleep an und seed-ed zwei Einstiegs- +Meditationen — mehr brauchst du nicht um anzufangen. + +Kein AI-Agent. Du meditierst, nicht dein Computer.`, + category: 'wellness', + color: '#8B5CF6', + icon: '🧘', + scene: { + name: 'Calmness', + description: 'Ruhe, Atem, Stille', + openApps: [ + { appId: 'meditate', widthPx: 540 }, + { appId: 'mood', widthPx: 340 }, + { appId: 'journal', widthPx: 440 }, + { appId: 'sleep', widthPx: 340 }, + ], + }, + seeds: { + meditate: [ + { + stableId: 'template-calmness:preset:4-7-8', + data: { + name: '4-7-8 Atmung', + description: 'Beruhigende Atemtechnik. Einatmen 4s, halten 7s, ausatmen 8s.', + category: 'breathing', + breathPattern: { inhale: 4, hold: 7, exhale: 8, rest: 0 }, + defaultDurationSec: 300, + }, + }, + { + stableId: 'template-calmness:preset:bodyscan-10', + data: { + name: 'Body-Scan 10min', + description: 'Sanfte Aufmerksamkeits-Wanderung durch den Körper.', + category: 'bodyscan', + bodyScanSteps: [ + 'Spüre deine Füße', 'Spüre deine Beine', 'Spüre dein Becken', + 'Spüre deinen Bauch', 'Spüre deine Brust', 'Spüre deine Arme', + 'Spüre deinen Nacken', 'Spüre deinen Kopf', 'Spüre deinen ganzen Körper', + ], + defaultDurationSec: 600, + }, + }, + ], + }, +}; +``` + +### Seed-Handler für Meditate (konkret) + +```ts +// apps/mana/apps/web/src/lib/modules/meditate/seed.ts +import { meditateStore } from './stores/meditate.svelte'; +import { db } from '$lib/data/database'; +import { registerSeedHandler } from '$lib/data/ai/agents/seed-registry'; +import type { LocalMeditatePreset } from './types'; + +interface MeditatePresetSeed { + name: string; + description?: string; + category: 'silence' | 'breathing' | 'bodyscan'; + breathPattern?: { inhale: number; hold: number; exhale: number; rest: number }; + bodyScanSteps?: string[]; + defaultDurationSec?: number; +} + +registerSeedHandler({ + moduleName: 'meditate', + async apply(items) { + const outcomes = []; + for (const item of items) { + const seed = item.data as MeditatePresetSeed; + // Stable-id idempotency: search Dexie for an existing preset + // with `templateStableId = item.stableId`. Preset schema doesn't + // have that column today; we stash it in `description` for T1. + // T2 adds a proper column. + if (item.stableId) { + const existing = await db + .table('meditatePresets') + .filter((p) => !p.deletedAt && p.description?.includes(`\`${item.stableId}\``)) + .first(); + if (existing) { + outcomes.push({ stableId: item.stableId, outcome: 'skipped-exists' }); + continue; + } + } + try { + await meditateStore.createPreset({ + ...seed, + description: seed.description + ? `${seed.description}\n\n\`${item.stableId ?? ''}\`` + : `\`${item.stableId ?? ''}\``, + }); + outcomes.push({ stableId: item.stableId, outcome: 'created' }); + } catch (err) { + outcomes.push({ + stableId: item.stableId, + outcome: 'failed', + error: err instanceof Error ? err.message : String(err), + }); + } + } + return outcomes; + }, +}); +``` + +Note: der "stableId im description einbetten"-Trick ist Pragmatik für T1. T2 führt eine proper `templateStableId` Spalte ein wenn wir mehr Module seeden. + +--- + +## Dateien (neu / modifiziert in T1) + +**Neu:** +- `packages/shared-ai/src/agents/templates/calmness.ts` +- `apps/mana/apps/web/src/lib/data/ai/agents/seed-registry.ts` +- `apps/mana/apps/web/src/lib/modules/meditate/seed.ts` + +**Modifiziert:** +- `packages/shared-ai/src/agents/templates/types.ts` — generalisierter Shape +- `packages/shared-ai/src/agents/templates/index.ts` — Calmness exportieren +- `packages/shared-ai/src/agents/templates/research.ts`, `context.ts`, `today.ts` — `category: 'ai'`, `icon`, `version: '1'` nachreichen +- `apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts` — Seeds-Schritt +- `apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte` — Detail-Panel zeigt "Seeds" Sektion +- `apps/mana/apps/web/src/lib/data/ai/missions/setup.ts` — lädt `meditate/seed` beim startMissionTick damit der Handler registriert ist + +--- + +## Risiken + Mitigation + +| Risiko | Mitigation | +|---|---| +| Seed-Handler-Registrierung zur Laufzeit nicht garantiert (z.B. Lazy-Loading bricht Registry) | T1: Handler werden in `startMissionTick` explizit eingebunden (`import '$lib/modules/meditate/seed'`). T2: Module-Registry aufbohren. | +| Seed schreibt in encrypted-Tabelle ohne User-Master-Key | Seed-Handler ruft die Modul-Stores (die `encryptRecord` schon nutzen) — Crypto-Pfad unverändert. Funktioniert nur bei unlocked vault (wie alle Writes). | +| Stable-ID-Hack via description ist hässlich | T1 akzeptiert das als pragmatisch. T2 führt echte Spalte + Dexie-Migration ein. | +| Template-Versionierung ohne Update-Logik | Version wird gespeichert, aber keine Update-UI in T1. Vorbereitung für T4. | + +--- + +## Nicht-Ziele T1 + +- **Keine Kategorie-UI** (T3) +- **Keine Update-Detection** (T4) +- **Keine User-Created Templates** (T5) +- **Keine echte Dexie-Spalte für stableId** (T2) — T1 nutzt description-Trick +- **Kein "Template anwenden v2" Upgrade-Pfad** (T4) + +--- + +## Offene Fragen + +1. **Seed-Schemas typisieren?** Heute sind Seeds `data: unknown`. Jeder Handler casted intern. Alternative: jedes Modul exportiert seinen Seed-Typ, Template referenziert ihn. Tight-coupling vs. Flexibilität. → T2 Entscheidung. +2. **Scene-Apps validieren?** Wenn ein Template eine `appId: 'foo'` referenziert die es nicht gibt, erscheint ein broken Tab. T2: Warnings auswerfen statt silently leere Tab. → T2. +3. **Delete-Cascade?** Wenn User die Calmness-Scene löscht, sollen die 2 Presets weg sein? Meine Tendenz: nein — Presets sind "Vorschläge", User bewertet selbst ob sie die behalten will. Scene ist Layout, Seed ist Inhalt. diff --git a/packages/shared-ai/src/agents/index.ts b/packages/shared-ai/src/agents/index.ts index 28be15532..62780f9c8 100644 --- a/packages/shared-ai/src/agents/index.ts +++ b/packages/shared-ai/src/agents/index.ts @@ -2,6 +2,13 @@ export type { Agent, AgentState } from './types'; export { DEFAULT_AGENT_ID, DEFAULT_AGENT_NAME } from './types'; export type { + WorkbenchTemplate, + WorkbenchTemplateAgentPart, + WorkbenchTemplateScenePart, + WorkbenchTemplateSceneApp, + WorkbenchTemplateMissionPart, + WorkbenchTemplateSeedItem, + WorkbenchTemplateCategory, AgentTemplate, AgentTemplateAgentPart, AgentTemplateScenePart, diff --git a/packages/shared-ai/src/agents/templates/calmness.ts b/packages/shared-ai/src/agents/templates/calmness.ts new file mode 100644 index 000000000..b9dc51ba2 --- /dev/null +++ b/packages/shared-ai/src/agents/templates/calmness.ts @@ -0,0 +1,72 @@ +import type { WorkbenchTemplate } from './types'; + +/** + * Calmness — first non-AI workbench template. + * + * Legt eine Scene fürs Runterkommen an und seed-ed zwei Einstiegs- + * Meditationen ins Meditate-Modul. Kein Agent, keine Mission — dieses + * Template ist "Infrastruktur für Ruhe", keine Automation. + * + * Dient in Phase T1 des Workbench-Templates-Plans auch als Pilot- + * Template dafür wie nicht-AI-Templates aussehen. Folgende Templates + * (Fitness, Deep Work, …) nutzen denselben Shape. + */ + +export const calmnessTemplate: WorkbenchTemplate = { + id: 'calmness', + version: '1', + label: 'Calmness', + icon: '🧘', + tagline: 'Atem, Stille, ruhige Momente', + description: `Ein Workbench-Setup für Stille-Momente. Legt dir eine Scene mit den Modulen Meditate, Mood, Journal und Sleep an und seed-et zwei Einstiegs-Meditationen — mehr brauchst du nicht um anzufangen. + +Kein AI-Agent. Du meditierst, nicht dein Computer.`, + category: 'wellness', + color: '#8B5CF6', + scene: { + name: 'Calmness', + description: 'Ruhe, Atem, Stille', + openApps: [ + { appId: 'meditate', widthPx: 540 }, + { appId: 'mood', widthPx: 340 }, + { appId: 'journal', widthPx: 440 }, + { appId: 'sleep', widthPx: 340 }, + ], + }, + seeds: { + meditate: [ + { + stableId: 'template-calmness:preset:4-7-8', + data: { + name: '4-7-8 Atmung', + description: + 'Beruhigende Atemtechnik. Einatmen 4 Sekunden, halten 7 Sekunden, ausatmen 8 Sekunden. Gut für abends oder bei Unruhe.', + category: 'breathing', + breathPattern: { inhale: 4, hold: 7, exhale: 8, rest: 0 }, + defaultDurationSec: 300, + }, + }, + { + stableId: 'template-calmness:preset:bodyscan-10', + data: { + name: 'Body-Scan 10min', + description: + 'Sanfte Aufmerksamkeits-Wanderung durch den Körper — von den Füßen bis zum Scheitel.', + category: 'bodyscan', + bodyScanSteps: [ + 'Spüre deine Füße', + 'Spüre deine Beine', + 'Spüre dein Becken', + 'Spüre deinen Bauch', + 'Spüre deine Brust', + 'Spüre deine Arme', + 'Spüre deinen Nacken', + 'Spüre deinen Kopf', + 'Spüre deinen ganzen Körper', + ], + defaultDurationSec: 600, + }, + }, + ], + }, +}; diff --git a/packages/shared-ai/src/agents/templates/context.ts b/packages/shared-ai/src/agents/templates/context.ts index 66566c4f5..320ad7ae3 100644 --- a/packages/shared-ai/src/agents/templates/context.ts +++ b/packages/shared-ai/src/agents/templates/context.ts @@ -21,6 +21,8 @@ const CONTEXT_POLICY: AiPolicy = { export const contextTemplate: AgentTemplate = { id: 'context', + version: '1', + icon: '🧭', label: 'Kontext-Agent', tagline: 'Lernt dich kennen, damit andere Agents besser arbeiten', description: `Der Agent fragt dich gezielt Fragen und destilliert die Antworten @@ -35,7 +37,7 @@ Was er tut: 3. Verdichtet deine Antworten zu einem strukturierten Kontext-Update (als Vorschlag) Alles läuft als Vorschlag — du bestätigst welche Version deines Profils gespeichert wird.`, - category: 'context', + category: 'ai', color: '#D946EF', agent: { name: 'Kontext-Agent', diff --git a/packages/shared-ai/src/agents/templates/index.ts b/packages/shared-ai/src/agents/templates/index.ts index 9a8b1eb0c..f4a266165 100644 --- a/packages/shared-ai/src/agents/templates/index.ts +++ b/packages/shared-ai/src/agents/templates/index.ts @@ -10,8 +10,18 @@ import { researchTemplate } from './research'; import { contextTemplate } from './context'; import { todayTemplate } from './today'; +import { calmnessTemplate } from './calmness'; export type { + // Generalised names (T1 of workbench-templates plan): + WorkbenchTemplate, + WorkbenchTemplateAgentPart, + WorkbenchTemplateScenePart, + WorkbenchTemplateSceneApp, + WorkbenchTemplateMissionPart, + WorkbenchTemplateSeedItem, + WorkbenchTemplateCategory, + // Pre-T1 aliases: AgentTemplate, AgentTemplateAgentPart, AgentTemplateScenePart, @@ -19,9 +29,14 @@ export type { AgentTemplateMissionPart, } from './types'; -export const ALL_TEMPLATES = [researchTemplate, contextTemplate, todayTemplate] as const; +export const ALL_TEMPLATES = [ + researchTemplate, + contextTemplate, + todayTemplate, + calmnessTemplate, +] as const; -export { researchTemplate, contextTemplate, todayTemplate }; +export { researchTemplate, contextTemplate, todayTemplate, calmnessTemplate }; /** Lookup helper — returns the template matching the given id, or * undefined. Useful for deep-links `/agents/templates?pick=research`. */ diff --git a/packages/shared-ai/src/agents/templates/research.ts b/packages/shared-ai/src/agents/templates/research.ts index 1669856c7..960ab3d66 100644 --- a/packages/shared-ai/src/agents/templates/research.ts +++ b/packages/shared-ai/src/agents/templates/research.ts @@ -22,6 +22,8 @@ const RESEARCH_POLICY: AiPolicy = { export const researchTemplate: AgentTemplate = { id: 'research', + version: '1', + icon: '🔍', label: 'Recherche-Agent', tagline: 'Liest Quellen, schreibt Notizen, destilliert einen Bericht', description: `Gib dem Agent ein Thema und eine Liste von Quellen-URLs. Er: @@ -32,7 +34,7 @@ export const researchTemplate: AgentTemplate = { 4. Verlinkt im Bericht zurück auf die Quellen-Notizen Jede Notiz wird als Vorschlag angelegt — du bestätigst was wirklich gespeichert wird.`, - category: 'research', + category: 'ai', color: '#0EA5E9', agent: { name: 'Recherche-Agent', diff --git a/packages/shared-ai/src/agents/templates/today.ts b/packages/shared-ai/src/agents/templates/today.ts index 8bd94ccb8..342958caf 100644 --- a/packages/shared-ai/src/agents/templates/today.ts +++ b/packages/shared-ai/src/agents/templates/today.ts @@ -23,6 +23,8 @@ const TODAY_POLICY: AiPolicy = { export const todayTemplate: AgentTemplate = { id: 'today', + version: '1', + icon: '🌅', label: 'Today-Agent', tagline: 'Jeden Tag ein Gedicht über das was heute besonderes passierte', description: `Der Agent recherchiert täglich (morgens um 7 Uhr) was an diesem @@ -39,7 +41,7 @@ Was er tut: 3. Wählt ein Thema das ihn (oder dich) inspiriert 4. Schreibt ein kurzes Gedicht (4-8 Zeilen, deutsch) 5. Schlägt eine Journal-Notiz vor mit Titel "Heute — [Datum]"`, - category: 'today', + category: 'delight', color: '#F97316', agent: { name: 'Today-Agent', diff --git a/packages/shared-ai/src/agents/templates/types.ts b/packages/shared-ai/src/agents/templates/types.ts index 5414f3fe4..6b0386d90 100644 --- a/packages/shared-ai/src/agents/templates/types.ts +++ b/packages/shared-ai/src/agents/templates/types.ts @@ -1,84 +1,106 @@ /** - * Agent-Template shape — a bundle of (agent config, optional scene - * layout, optional starter missions) that the webapp applies as a - * single unit when the user picks it from the template gallery. + * Workbench-Template shape — a bundle of (optional agent config, + * optional scene layout, optional starter missions, optional + * per-module seed data) that the webapp applies as a single unit + * when the user picks it from the /agents/templates gallery. * * Templates are pure data: no runtime imports, no side effects, no * references to Dexie / Svelte. The webapp's `apply-template.ts` * orchestrator is the only code that turns a template into concrete - * records. This keeps the templates trivial to author (drop a file - * next to this one) and keeps shared-ai dependency-free. + * records. This keeps templates trivial to author (drop a file next + * to the existing ones) and keeps shared-ai dependency-free. + * + * Originally named `AgentTemplate` (shipped in Phase 5 of the + * Multi-Agent Workbench). Generalised in T1 of the Workbench-Templates + * plan (docs/plans/workbench-templates.md) so templates can exist + * WITHOUT an agent (e.g. a Calmness wellness starter-kit). */ import type { AiPolicy } from '../../policy/types'; import type { MissionCadence, MissionInputRef } from '../../missions/types'; -export interface AgentTemplateAgentPart { - /** Display name — the user can rename after creation. Must be unique - * at apply-time; the orchestrator deduplicates via `createAgent`. */ +export type WorkbenchTemplateCategory = 'ai' | 'wellness' | 'work' | 'lifeEvent' | 'delight'; + +export interface WorkbenchTemplateAgentPart { name: string; - /** Emoji or short string. Shown on the card + everywhere the agent - * appears in UI. */ avatar: string; - /** Short user-facing description ("what this agent does for you"). */ role: string; - /** Optional pre-filled systemPrompt. Encrypted at rest. */ systemPrompt?: string; - /** Optional pre-filled memory. Encrypted at rest. */ memory?: string; - /** Per-tool + per-module decisions. Templates reuse the DEFAULT_AI_POLICY - * and layer tweaks on top. Undefined → DEFAULT_AI_POLICY. */ policy?: AiPolicy; - /** Optional budget; undefined = no daily cap. */ maxTokensPerDay?: number; - /** Default 1 (serial). */ maxConcurrentMissions?: number; } -export interface AgentTemplateSceneApp { +export interface WorkbenchTemplateSceneApp { readonly appId: string; readonly widthPx?: number; readonly maximized?: boolean; } -export interface AgentTemplateScenePart { - /** Display name for the scene tab. */ +export interface WorkbenchTemplateScenePart { name: string; description?: string; - openApps: readonly AgentTemplateSceneApp[]; + openApps: readonly WorkbenchTemplateSceneApp[]; } -export interface AgentTemplateMissionPart { +export interface WorkbenchTemplateMissionPart { title: string; objective: string; conceptMarkdown: string; cadence: MissionCadence; inputs?: readonly MissionInputRef[]; - /** Whether the mission should be immediately active. Templates - * default to `paused` so the user has to hit Play — avoids - * surprise autonomous work on first use. */ startPaused?: boolean; } -export interface AgentTemplate { - /** Stable id, used for analytics + "this template was applied" detection. */ - id: string; - /** Short display label for the gallery card. */ - label: string; - /** One-line tagline under the label. */ - tagline: string; - /** Longer body for the card's detail pane. Can be markdown. */ - description: string; - /** Category / tag for grouping in the gallery. */ - category: 'research' | 'context' | 'today' | 'custom'; - /** Accent color for the card. */ - color: string; - - agent: AgentTemplateAgentPart; - /** Optional — when absent, no scene is created. When present, the - * orchestrator creates a scene pre-populated with these apps. */ - scene?: AgentTemplateScenePart; - /** Optional starter missions. Each defaults to `startPaused: true` - * so autonomous work doesn't start without explicit user consent. */ - missions?: readonly AgentTemplateMissionPart[]; +/** + * One seeded record in a module. The Applicator passes these to the + * module's SeedHandler by name; the handler unpacks `data` according + * to its own schema. + */ +export interface WorkbenchTemplateSeedItem { + /** Stable identifier for idempotent re-apply. When set, the handler + * looks up an existing record with the same stableId and skips + * re-creation. When unset, a fresh record is created on every + * apply (UUID-random). */ + readonly stableId?: string; + /** Module-specific payload. The seed handler for the matching + * module name knows the shape. Typed as `unknown` here so shared-ai + * doesn't import the webapp's module types. */ + readonly data: unknown; } + +export interface WorkbenchTemplate { + readonly id: string; + /** Schema/content version stamped on the scene when applied — used + * later for update-detection. Start at '1'. */ + readonly version: string; + readonly label: string; + readonly tagline: string; + readonly description: string; + readonly category: WorkbenchTemplateCategory; + readonly color: string; + /** Icon emoji for the gallery card when no agent avatar is present. */ + readonly icon: string; + + // All component parts are optional — a template can be agent-only, + // scene-only, seeds-only, or any combination. + readonly agent?: WorkbenchTemplateAgentPart; + readonly scene?: WorkbenchTemplateScenePart; + readonly missions?: readonly WorkbenchTemplateMissionPart[]; + /** Module-name → seed items for that module. Keys match module + * names from apps/mana/apps/web/src/lib/modules/*. Unknown keys + * are reported as warnings by the applicator, not fatal. */ + readonly seeds?: Readonly>; +} + +// ─── Backward-compat aliases ───────────────────────────────── +// Pre-T1 name. Existing templates (research/context/today) typed +// themselves as `AgentTemplate`; keep the aliases so nothing breaks. +// New templates should use `WorkbenchTemplate` directly. + +export type AgentTemplate = WorkbenchTemplate; +export type AgentTemplateAgentPart = WorkbenchTemplateAgentPart; +export type AgentTemplateScenePart = WorkbenchTemplateScenePart; +export type AgentTemplateSceneApp = WorkbenchTemplateSceneApp; +export type AgentTemplateMissionPart = WorkbenchTemplateMissionPart; diff --git a/packages/shared-ai/src/index.ts b/packages/shared-ai/src/index.ts index b1c291287..7244c019e 100644 --- a/packages/shared-ai/src/index.ts +++ b/packages/shared-ai/src/index.ts @@ -83,5 +83,12 @@ export type { AgentTemplateScenePart, AgentTemplateSceneApp, AgentTemplateMissionPart, + WorkbenchTemplate, + WorkbenchTemplateAgentPart, + WorkbenchTemplateScenePart, + WorkbenchTemplateSceneApp, + WorkbenchTemplateMissionPart, + WorkbenchTemplateSeedItem, + WorkbenchTemplateCategory, } from './agents'; export { DEFAULT_AGENT_ID, DEFAULT_AGENT_NAME, ALL_TEMPLATES, getTemplateById } from './agents';