mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 07:41:09 +02:00
feat(templates): generalise to WorkbenchTemplate + ship Calmness pilot (T1)
First pass of the workbench-templates plan (docs/plans/workbench-
templates.md) — templates are no longer agent-centric but a general
"starter kit" bundle: optional agent + optional scene + optional
missions + optional per-module seeds. Pilot non-AI template "Calmness"
ships alongside.
Shape generalisation (packages/shared-ai/src/agents/templates/types.ts):
- AgentTemplate renamed to WorkbenchTemplate; all fields now optional
(agent, scene, missions, seeds). Back-compat AgentTemplate alias
kept so research/context/today keep compiling.
- Added `category: 'ai'|'wellness'|'work'|'lifeEvent'|'delight'` +
`icon` (for non-agent templates that have no avatar) + `version`
field (for future update-detection).
- New WorkbenchTemplateSeedItem shape: `{stableId?, data: unknown}`.
Module-specific seed payloads are typed at the handler side.
- Existing three AI templates nachgezogen: category='ai' (or
'delight' for today-agent), icon, version='1'.
Seed infrastructure:
- apps/mana/apps/web/src/lib/data/ai/agents/seed-registry.ts — in-
memory handler map keyed by module name; module-local seed.ts files
register themselves at import time.
- apps/mana/apps/web/src/lib/modules/meditate/seed.ts — first handler:
createPreset-based, idempotent via stableId embedded as HTML
comment in the preset description (T1 pragmatism; T2 adds a proper
column on the preset schema).
- data/ai/missions/setup.ts pulls `import '$lib/modules/meditate/seed'`
so the handler is registered before any template is applied.
Applicator upgrades (data/ai/agents/apply-template.ts):
- Agent step now optional — skipped cleanly when template has no
agent part.
- New step 4: seeds. Walks template.seeds, looks up the handler for
each module, aggregates per-item outcomes (created/skipped-exists/
failed) into result.seedOutcomes. Missing handler = warning, not
fatal. Crypto/encryption unchanged — seeds go through the same
module stores that module code already uses.
- Result shape gains `seedOutcomes: Record<string, SeedOutcome[]>`
so the gallery can show "3 new, 1 already there".
Calmness pilot (packages/shared-ai/src/agents/templates/calmness.ts):
- category='wellness', NO agent, scene with meditate/mood/journal/
sleep apps, two meditate preset seeds:
* 4-7-8 Atmung (breathing preset)
* Body-Scan 10min (bodyscan preset with 9 scan steps)
- Each seed has a stableId so re-apply is idempotent.
Gallery updates (routes/(app)/agents/templates/+page.svelte):
- Card avatar falls back to t.icon when no agent. "Agent" chip shows
only for agent-templates; "N Seeds" chip shows for templates with
seeds.
- Detail header shows "Workbench-Setup ohne AI-Agent" when no agent.
- New "Seeds" preview section: lists per-module counts + item names.
- Options section gains a "Seed-Daten in Module einpflegen" checkbox.
- Success panel shows seed summary: "3 Seeds neu, 1 bereits
vorhanden".
Tests: shared-ai 26/26, webapp svelte-check 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a524997a2f
commit
a08e45ca16
14 changed files with 887 additions and 119 deletions
|
|
@ -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<Record<string, readonly SeedOutcome[]>>;
|
||||
/** 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<ApplyTemplateResult> {
|
||||
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<string, readonly SeedOutcome[]> = {};
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
50
apps/mana/apps/web/src/lib/data/ai/agents/seed-registry.ts
Normal file
50
apps/mana/apps/web/src/lib/data/ai/agents/seed-registry.ts
Normal file
|
|
@ -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<readonly SeedOutcome[]>;
|
||||
}
|
||||
|
||||
const handlers = new Map<string, SeedHandler>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
78
apps/mana/apps/web/src/lib/modules/meditate/seed.ts
Normal file
78
apps/mana/apps/web/src/lib/modules/meditate/seed.ts
Normal file
|
|
@ -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<!-- seed:${stableId} -->` : '';
|
||||
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<LocalMeditatePreset>('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;
|
||||
},
|
||||
});
|
||||
|
|
@ -17,10 +17,13 @@
|
|||
let selected = $state<AgentTemplate | null>(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<string | null>(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<Record<string, readonly { outcome: string }[]>>): {
|
||||
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)}
|
||||
>
|
||||
<span class="avatar">{t.agent.avatar}</span>
|
||||
<span class="avatar">{t.agent?.avatar ?? t.icon}</span>
|
||||
<span class="label">{t.label}</span>
|
||||
<span class="tagline">{t.tagline}</span>
|
||||
<span class="meta">
|
||||
{#if t.agent}<span class="chip">Agent</span>{/if}
|
||||
{#if t.scene}<span class="chip">Scene</span>{/if}
|
||||
{#if t.missions && t.missions.length > 0}
|
||||
<span class="chip"
|
||||
>{t.missions.length} Mission{t.missions.length !== 1 ? 'en' : ''}</span
|
||||
>
|
||||
{/if}
|
||||
{#if t.seeds}
|
||||
{@const total = Object.values(t.seeds).reduce((s, items) => s + items.length, 0)}
|
||||
<span class="chip">{total} Seed{total !== 1 ? 's' : ''}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -108,10 +141,14 @@
|
|||
{#if selected}
|
||||
<section class="detail" style="--accent: {selected.color}">
|
||||
<header class="detail-head">
|
||||
<span class="detail-avatar">{selected.agent.avatar}</span>
|
||||
<span class="detail-avatar">{selected.agent?.avatar ?? selected.icon}</span>
|
||||
<div>
|
||||
<h2>{selected.label}</h2>
|
||||
<p class="detail-role">{selected.agent.role}</p>
|
||||
{#if selected.agent}
|
||||
<p class="detail-role">{selected.agent.role}</p>
|
||||
{:else}
|
||||
<p class="detail-role">Workbench-Setup ohne AI-Agent</p>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -161,6 +198,33 @@
|
|||
</section>
|
||||
{/if}
|
||||
|
||||
{#if selected.seeds && Object.keys(selected.seeds).length > 0}
|
||||
<section class="preview">
|
||||
<h3>Seeds</h3>
|
||||
<p class="seed-hint">
|
||||
Vorgefüllte Einträge in deinen Modulen. Werden als neue Records angelegt; bestehende
|
||||
Einträge mit gleicher Seed-ID werden übersprungen (idempotent).
|
||||
</p>
|
||||
<ul class="seeds-preview">
|
||||
{#each Object.entries(selected.seeds) as [moduleName, items]}
|
||||
<li>
|
||||
<strong>{moduleName}</strong>
|
||||
<span class="seed-count">
|
||||
{items.length} Eintr{items.length !== 1 ? 'äge' : 'ag'}
|
||||
</span>
|
||||
<ul class="seed-items">
|
||||
{#each items as item}
|
||||
<li>
|
||||
<code>{(item.data as { name?: string }).name ?? '(unbenannt)'}</code>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="options">
|
||||
<h3>Optionen</h3>
|
||||
{#if selected.scene}
|
||||
|
|
@ -179,6 +243,12 @@
|
|||
<span>Mission(en) sofort aktivieren (Standard: pausiert)</span>
|
||||
</label>
|
||||
{/if}
|
||||
{#if selected.seeds && Object.keys(selected.seeds).length > 0}
|
||||
<label class="opt">
|
||||
<input type="checkbox" bind:checked={optApplySeeds} />
|
||||
<span>Seed-Daten in Module einpflegen</span>
|
||||
</label>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if result}
|
||||
|
|
@ -186,9 +256,13 @@
|
|||
<Check size={16} />
|
||||
<div>
|
||||
<strong>
|
||||
{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}
|
||||
</strong>
|
||||
<p>
|
||||
{#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}
|
||||
</p>
|
||||
{#if result.warnings.length > 0}
|
||||
<ul class="warnings">
|
||||
|
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
338
docs/plans/workbench-templates.md
Normal file
338
docs/plans/workbench-templates.md
Normal file
|
|
@ -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<Record<string, readonly WorkbenchTemplateSeedItem[]>>;
|
||||
}
|
||||
|
||||
/** 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<SeedOutcome[]>;
|
||||
}
|
||||
|
||||
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<LocalMeditatePreset>('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.
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
72
packages/shared-ai/src/agents/templates/calmness.ts
Normal file
72
packages/shared-ai/src/agents/templates/calmness.ts
Normal file
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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`. */
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<Record<string, readonly WorkbenchTemplateSeedItem[]>>;
|
||||
}
|
||||
|
||||
// ─── 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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue