feat(ai-agents): Template gallery — 3 ready-to-use agent bundles

First pass of the Multi-Agent discoverability UX. A new /agents/
templates route showcases pre-configured agents; clicking one creates
agent + scene + starter mission(s) as a single bundle. Addresses the
"blank form anxiety" + "user doesn't know what agents are for"
observations from the UX brainstorm.

Three templates for v1 (shared-ai/src/agents/templates/):
- 🔍 Recherche-Agent — reads sources one by one, writes a note per
  source, summarizes into a report. Manual-cadence mission; all
  writes propose so user curates.
- 🧭 Kontext-Agent — learns about the user via a weekly check-in.
  Reads kontext/notes/goals, asks 2-3 questions, proposes a diff-
  style context update. Weekly Sunday cadence.
- 🌅 Today-Agent — researches "on this day" history each morning,
  writes a 4-8 line German poem, proposes a journal note. Daily 7am
  cadence. A "delight" agent, not a productive one.

Each template packs (agent config, scene layout, starter mission):
- AgentTemplate type lives in @mana/shared-ai — pure data, no runtime
  imports. Adding a new template = drop a file in templates/ and
  extend ALL_TEMPLATES.
- Template-specific policies derive from the proposable-tool list so
  drift-guard catches divergence from the canonical set.
- Starter missions default to startPaused=true — user sees the
  mission ready-to-go and hits Play when ready. Prevents surprise
  autonomous work on first apply.

Applicator (data/ai/agents/apply-template.ts):
- Creates agent → scene (if template defines one) → missions in
  order. Agent failure = abort; scene/mission failures surface as
  warnings in the result without blocking.
- Duplicate-name handling: falls through to findByName, returns
  existing agent with wasExisting=true; scene is skipped in that
  case to avoid clone-proliferation.

Gallery page /(app)/agents/templates/+page.svelte:
- Three large cards side-by-side (stacks on mobile) with avatar /
  label / tagline / meta chips (Scene, N Missionen).
- Click opens detail panel with full description, scene preview
  (app-ids + widths), mission preview (title / objective / cadence),
  and override checkboxes (create scene, create missions, start
  active vs paused).
- Success panel shows what landed with warnings inline; CTA back to
  workbench.

Discoverability in /ai-agents module:
- Bar now has two buttons: "Aus Template" (primary, goto templates
  route) + "Eigener Agent" (secondary, opens the existing blank-form
  create mode).
- When only the default "Mana" agent exists, render a dashed promo
  banner at the top linking to the template gallery. Disappears as
  soon as the user has a second agent.

Tests: webapp svelte-check 0 errors, 0 warnings. shared-ai 26/26.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-16 00:36:39 +02:00
parent 4d9b16a683
commit 7822340ea0
10 changed files with 1150 additions and 6 deletions

View file

@ -0,0 +1,154 @@
/**
* Template applicator turns an AgentTemplate from `@mana/shared-ai`
* into concrete Dexie records: an Agent, optionally a workbench Scene,
* optionally starter Missions.
*
* 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).
*
* 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:
* - duplicate agent name returns existing agent (getOrCreate-ish)
* - scene creation is a fresh insert, no dedup needed
* - missions use fresh UUIDs, no dedup needed
*/
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 type { Agent } from './types';
export interface ApplyTemplateOptions {
/** Create the template's scene + set it active. Default true when the
* template defines a scene; false when it doesn't. */
createScene?: boolean;
/** Create the template's starter missions. Default true. */
createMissions?: 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. */
respectPauseHint?: boolean;
}
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;
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. */
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.
*/
export async function applyTemplate(
template: AgentTemplate,
opts: ApplyTemplateOptions = {}
): Promise<ApplyTemplateResult> {
const {
createScene = template.scene !== undefined,
createMissions = true,
respectPauseHint = true,
} = opts;
const warnings: string[] = [];
// 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) {
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.
let sceneId: string | undefined;
if (createScene && template.scene && !wasExisting) {
try {
sceneId = await workbenchScenesStore.createScene({
name: template.scene.name,
description: template.scene.description ?? null,
seedApps: [...template.scene.openApps],
setActive: true,
});
} catch (err) {
warnings.push(
`Scene konnte nicht angelegt werden: ${err instanceof Error ? err.message : String(err)}`
);
}
} else if (createScene && wasExisting) {
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.
const missionIds: string[] = [];
if (createMissions && template.missions) {
for (const m of template.missions) {
try {
const mission = await createMission({
title: m.title,
objective: m.objective,
conceptMarkdown: m.conceptMarkdown,
cadence: m.cadence,
inputs: m.inputs ? [...m.inputs] : undefined,
agentId: agent.id,
});
if (respectPauseHint && m.startPaused !== false) {
await pauseMission(mission.id);
}
missionIds.push(mission.id);
} catch (err) {
warnings.push(
`Starter-Mission "${m.title}" fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`
);
}
}
}
return {
agent,
wasExisting,
sceneId,
missionIds,
warnings,
};
}

View file

@ -17,8 +17,10 @@
handle the common "let the agent touch todo but not calendar" case.
-->
<script lang="ts">
import { ArrowLeft, Plus, Pause, Play, Archive, Trash } from '@mana/shared-icons';
import { ArrowLeft, Plus, Pause, Play, Archive, Trash, Sparkle } from '@mana/shared-icons';
import { goto } from '$app/navigation';
import { useAgents } from '$lib/data/ai/agents/queries';
import { DEFAULT_AGENT_ID } from '@mana/shared-ai';
import {
createAgent,
updateAgent,
@ -215,16 +217,34 @@
</script>
{#if mode === 'list'}
{@const onlyDefaultAgent = agents.value.length === 1 && agents.value[0].id === DEFAULT_AGENT_ID}
<div class="pane">
<header class="bar">
<button type="button" class="primary" onclick={() => (mode = 'create')}>
<Plus size={14} /><span>Neuer Agent</span>
<button type="button" class="primary" onclick={() => goto('/agents/templates')}>
<Sparkle size={14} /><span>Aus Template</span>
</button>
<button type="button" class="secondary" onclick={() => (mode = 'create')}>
<Plus size={14} /><span>Eigener Agent</span>
</button>
</header>
{#if onlyDefaultAgent}
<button type="button" class="promo" onclick={() => goto('/agents/templates')}>
<span class="promo-icon"><Sparkle size={16} weight="fill" /></span>
<span class="promo-body">
<strong>Starte mit einem Template</strong>
<span class="promo-sub">
Recherche · Kontext · Today — vorgefertigte Agenten mit passender Scene und
Starter-Mission.
</span>
</span>
</button>
{/if}
{#if agents.value.length === 0}
<p class="empty">
Noch keine Agenten. Ein Default-Agent „Mana" wird beim ersten Login automatisch angelegt;
für weitere persona-basierte Agenten klicke auf „Neuer Agent".
für weitere persona-basierte Agenten klicke auf „Aus Template" oder „Eigener Agent".
</p>
{:else}
<ul class="m-list">
@ -446,6 +466,7 @@
.bar {
display: flex;
justify-content: flex-end;
gap: 0.375rem;
}
.primary {
display: inline-flex;
@ -463,6 +484,52 @@
.primary:disabled {
opacity: 0.5;
}
.secondary {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border: 1px solid hsl(var(--color-border));
border-radius: 0.375rem;
background: hsl(var(--color-surface));
color: hsl(var(--color-muted-foreground));
cursor: pointer;
font: inherit;
font-size: 0.8125rem;
}
.promo {
display: flex;
gap: 0.625rem;
align-items: center;
width: 100%;
padding: 0.75rem 0.875rem;
border: 1px dashed color-mix(in oklab, hsl(var(--color-primary)) 50%, transparent);
border-radius: 0.5rem;
background: color-mix(in oklab, hsl(var(--color-primary)) 6%, hsl(var(--color-surface)));
color: hsl(var(--color-foreground));
text-align: left;
cursor: pointer;
font: inherit;
}
.promo:hover {
background: color-mix(in oklab, hsl(var(--color-primary)) 10%, hsl(var(--color-surface)));
}
.promo-icon {
color: hsl(var(--color-primary));
flex-shrink: 0;
}
.promo-body {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.promo-body strong {
font-size: 0.875rem;
}
.promo-sub {
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
}
.empty {
padding: 1.5rem 1rem;
color: hsl(var(--color-muted-foreground));

View file

@ -0,0 +1,512 @@
<!--
Agent-Templates Gallery — /agents/templates
Landing page for picking a pre-configured AI agent. Three cards
side-by-side on desktop, stacked on mobile. Clicking a card opens a
detail panel with description + "apply" options.
Designed as a standalone route (not an AppPage) so it can be deep-
linked from /welcome, from in-module banners, or shared directly.
-->
<script lang="ts">
import { ArrowLeft, Check, Play } from '@mana/shared-icons';
import { goto } from '$app/navigation';
import { ALL_TEMPLATES, type AgentTemplate } from '@mana/shared-ai';
import { applyTemplate } from '$lib/data/ai/agents/apply-template';
let selected = $state<AgentTemplate | null>(null);
let applying = $state(false);
let result = $state<{
agentName: string;
sceneCreated: boolean;
missionCount: number;
wasExisting: boolean;
warnings: readonly string[];
} | null>(null);
let error = $state<string | null>(null);
// Override toggles — default to the "smart" values we recommend.
let optCreateScene = $state(true);
let optCreateMissions = $state(true);
let optStartActive = $state(false); // false = respect paused hint
function openDetail(t: AgentTemplate) {
selected = t;
result = null;
error = null;
optCreateScene = t.scene !== undefined;
optCreateMissions = t.missions !== undefined && t.missions.length > 0;
optStartActive = false;
}
async function handleApply() {
if (!selected) return;
applying = true;
error = null;
try {
const r = await applyTemplate(selected, {
createScene: optCreateScene,
createMissions: optCreateMissions,
respectPauseHint: !optStartActive,
});
result = {
agentName: r.agent.name,
sceneCreated: r.sceneId !== undefined,
missionCount: r.missionIds.length,
wasExisting: r.wasExisting,
warnings: r.warnings,
};
} catch (err) {
error = err instanceof Error ? err.message : String(err);
} finally {
applying = false;
}
}
</script>
<svelte:head>
<title>Agent-Templates — Mana</title>
</svelte:head>
<div class="page">
<header class="header">
<button type="button" class="back" onclick={() => goto('/')}>
<ArrowLeft size={14} /><span>Zurück zum Workbench</span>
</button>
<h1>Agent-Templates</h1>
<p class="sub">
Vorgefertigte AI-Agenten, die sofort loslaufen. Jedes Template legt einen Agent, eine passende
Scene und eine Starter-Mission an — die Mission ist standardmäßig pausiert, damit du bewusst
Play drückst.
</p>
</header>
<div class="grid">
{#each ALL_TEMPLATES as t (t.id)}
<button
type="button"
class="card"
class:selected={selected?.id === t.id}
style="--accent: {t.color}"
onclick={() => openDetail(t)}
>
<span class="avatar">{t.agent.avatar}</span>
<span class="label">{t.label}</span>
<span class="tagline">{t.tagline}</span>
<span class="meta">
{#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}
</span>
</button>
{/each}
</div>
{#if selected}
<section class="detail" style="--accent: {selected.color}">
<header class="detail-head">
<span class="detail-avatar">{selected.agent.avatar}</span>
<div>
<h2>{selected.label}</h2>
<p class="detail-role">{selected.agent.role}</p>
</div>
</header>
<div class="detail-desc">{selected.description}</div>
{#if selected.scene}
<section class="preview">
<h3>Scene-Layout</h3>
<p class="preview-name">
<strong>{selected.scene.name}</strong>
{#if selected.scene.description}
{selected.scene.description}{/if}
</p>
<ul class="apps-preview">
{#each selected.scene.openApps as app (app.appId)}
<li>
<code>{app.appId}</code>
{#if app.widthPx}<span class="app-w">{app.widthPx}px</span>{/if}
</li>
{/each}
</ul>
</section>
{/if}
{#if selected.missions && selected.missions.length > 0}
<section class="preview">
<h3>Starter-Missionen</h3>
<ul class="missions-preview">
{#each selected.missions as m}
<li>
<strong>{m.title}</strong>
<p>{m.objective}</p>
<span class="cadence">
{#if m.cadence.kind === 'manual'}manuell auslösen
{:else if m.cadence.kind === 'daily'}täglich {m.cadence.atHour}:{String(
m.cadence.atMinute
).padStart(2, '0')}
{:else if m.cadence.kind === 'weekly'}wöchentlich, Tag {m.cadence.dayOfWeek} um {m
.cadence.atHour}:00
{:else if m.cadence.kind === 'interval'}alle {m.cadence.everyMinutes} Minuten
{:else}cron: {m.cadence.expression}
{/if}
</span>
</li>
{/each}
</ul>
</section>
{/if}
<section class="options">
<h3>Optionen</h3>
{#if selected.scene}
<label class="opt">
<input type="checkbox" bind:checked={optCreateScene} />
<span>Scene „{selected.scene.name}" anlegen und direkt öffnen</span>
</label>
{/if}
{#if selected.missions && selected.missions.length > 0}
<label class="opt">
<input type="checkbox" bind:checked={optCreateMissions} />
<span>Starter-Mission(en) mit anlegen</span>
</label>
<label class="opt" class:disabled={!optCreateMissions}>
<input type="checkbox" bind:checked={optStartActive} disabled={!optCreateMissions} />
<span>Mission(en) sofort aktivieren (Standard: pausiert)</span>
</label>
{/if}
</section>
{#if result}
<div class="result success">
<Check size={16} />
<div>
<strong>
{result.wasExisting
? `„${result.agentName}" existierte schon — wiederverwendet.`
: `Agent „${result.agentName}" angelegt.`}
</strong>
<p>
{#if result.sceneCreated}Scene angelegt + aktiviert.{/if}
{#if result.missionCount > 0}
{result.missionCount} Mission{result.missionCount !== 1 ? 'en' : ''}
{optStartActive ? 'aktiviert' : 'pausiert angelegt'}.
{/if}
</p>
{#if result.warnings.length > 0}
<ul class="warnings">
{#each result.warnings as w}
<li>{w}</li>
{/each}
</ul>
{/if}
</div>
</div>
<div class="result-actions">
<button type="button" class="btn-ghost" onclick={() => (selected = null)}>
Weiteres Template auswählen
</button>
<button type="button" class="btn-primary" onclick={() => goto('/')}>
Zum Workbench
</button>
</div>
{:else}
{#if error}
<div class="result error">
<strong>Konnte Template nicht anwenden</strong>
<p>{error}</p>
</div>
{/if}
<div class="apply-row">
<button type="button" class="btn-primary" onclick={handleApply} disabled={applying}>
<Play size={14} />
<span>{applying ? 'Lege an…' : `Template „${selected.label}" anwenden`}</span>
</button>
</div>
{/if}
</section>
{/if}
</div>
<style>
.page {
max-width: 1080px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.header {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.back {
align-self: flex-start;
display: inline-flex;
gap: 0.375rem;
align-items: center;
padding: 0.375rem 0.625rem;
border: 1px solid hsl(var(--color-border));
border-radius: 0.375rem;
background: hsl(var(--color-surface));
color: hsl(var(--color-muted-foreground));
cursor: pointer;
font: inherit;
font-size: 0.8125rem;
}
h1 {
margin: 0;
font-size: 1.75rem;
}
.sub {
margin: 0;
font-size: 0.9375rem;
color: hsl(var(--color-muted-foreground));
max-width: 60ch;
line-height: 1.5;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.card {
display: flex;
flex-direction: column;
gap: 0.375rem;
padding: 1.25rem;
border: 2px solid hsl(var(--color-border));
border-radius: 0.75rem;
background: hsl(var(--color-surface));
color: hsl(var(--color-foreground));
text-align: left;
cursor: pointer;
font: inherit;
transition: border-color 0.15s;
}
.card:hover {
border-color: var(--accent);
}
.card.selected {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 25%, transparent);
}
.avatar {
font-size: 2.5rem;
line-height: 1;
}
.label {
font-size: 1.125rem;
font-weight: 600;
}
.tagline {
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
line-height: 1.4;
}
.meta {
margin-top: 0.5rem;
display: flex;
gap: 0.375rem;
}
.chip {
padding: 0.125rem 0.5rem;
border-radius: 999px;
background: color-mix(in oklab, var(--accent) 15%, transparent);
color: var(--accent);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.detail {
border: 1px solid hsl(var(--color-border));
border-radius: 0.75rem;
padding: 1.5rem;
background: hsl(var(--color-surface));
display: flex;
flex-direction: column;
gap: 1rem;
border-top: 3px solid var(--accent);
}
.detail-head {
display: flex;
gap: 1rem;
align-items: center;
}
.detail-avatar {
font-size: 2.5rem;
line-height: 1;
}
.detail-head h2 {
margin: 0;
font-size: 1.25rem;
}
.detail-role {
margin: 0.125rem 0 0;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
.detail-desc {
white-space: pre-wrap;
font-size: 0.9375rem;
line-height: 1.55;
}
.preview {
padding: 0.875rem 1rem;
border: 1px solid hsl(var(--color-border));
border-radius: 0.5rem;
background: hsl(var(--color-background));
}
.preview h3 {
margin: 0 0 0.5rem;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: hsl(var(--color-muted-foreground));
}
.preview-name {
margin: 0 0 0.5rem;
font-size: 0.875rem;
}
.apps-preview {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.apps-preview 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;
display: inline-flex;
gap: 0.25rem;
align-items: center;
}
.apps-preview code {
font-size: 0.75rem;
}
.app-w {
color: hsl(var(--color-muted-foreground));
font-variant-numeric: tabular-nums;
}
.missions-preview {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.missions-preview li strong {
font-size: 0.875rem;
}
.missions-preview li p {
margin: 0.125rem 0;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
.cadence {
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
font-style: italic;
}
.options {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.options h3 {
margin: 0 0 0.25rem;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: hsl(var(--color-muted-foreground));
}
.opt {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.opt.disabled {
opacity: 0.5;
}
.apply-row {
display: flex;
justify-content: flex-end;
}
.btn-primary {
display: inline-flex;
gap: 0.375rem;
align-items: center;
padding: 0.625rem 1rem;
border: 1px solid var(--accent);
border-radius: 0.5rem;
background: var(--accent);
color: white;
cursor: pointer;
font: inherit;
font-size: 0.9375rem;
font-weight: 600;
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-ghost {
padding: 0.625rem 1rem;
border: 1px solid hsl(var(--color-border));
border-radius: 0.5rem;
background: hsl(var(--color-surface));
color: hsl(var(--color-foreground));
cursor: pointer;
font: inherit;
font-size: 0.9375rem;
}
.result {
display: flex;
gap: 0.75rem;
padding: 0.875rem 1rem;
border-radius: 0.5rem;
border: 1px solid;
}
.result.success {
border-color: #1b7a3a;
background: #d7f7e3;
color: #0f3f1d;
}
.result.error {
border-color: #8a1b1b;
background: #f7d7d7;
color: #3f0f0f;
}
.result p {
margin: 0.25rem 0 0;
font-size: 0.875rem;
}
.result strong {
font-size: 0.9375rem;
}
.warnings {
margin: 0.375rem 0 0;
padding-left: 1rem;
font-size: 0.8125rem;
}
.result-actions {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
</style>

View file

@ -1,2 +1,11 @@
export type { Agent, AgentState } from './types';
export { DEFAULT_AGENT_ID, DEFAULT_AGENT_NAME } from './types';
export type {
AgentTemplate,
AgentTemplateAgentPart,
AgentTemplateScenePart,
AgentTemplateSceneApp,
AgentTemplateMissionPart,
} from './templates';
export { ALL_TEMPLATES, getTemplateById } from './templates';

View file

@ -0,0 +1,95 @@
import { AI_PROPOSABLE_TOOL_NAMES } from '../../policy/proposable-tools';
import type { AgentTemplate } from './types';
import type { AiPolicy } from '../../policy/types';
/**
* Context agent tries to learn as much as possible about the user by
* asking questions + reading available context, then consolidates into
* the Kontext-document. Everything is propose so the user curates their
* own profile.
*/
const CONTEXT_POLICY: AiPolicy = {
tools: Object.fromEntries(AI_PROPOSABLE_TOOL_NAMES.map((n) => [n, 'propose'])),
defaultsByModule: {
kontext: 'propose',
notes: 'propose',
goals: 'auto',
},
defaultForAi: 'propose',
};
export const contextTemplate: AgentTemplate = {
id: 'context',
label: 'Kontext-Agent',
tagline: 'Lernt dich kennen, damit andere Agents besser arbeiten',
description: `Der Agent fragt dich gezielt Fragen und destilliert die Antworten
in dein Kontext-Dokument. Andere Agents (Recherche, Today, ) lesen dieses
Dokument als Prompt-Zusatz je besser es gepflegt ist, desto relevanter werden
ihre Vorschläge.
Was er tut:
1. Liest was schon in deinem Kontext + Notizen + Goals steht
2. Stellt gezielt Fragen zu Lücken ("Was treibt dich aktuell um?", "Welche Projekte liegen an?")
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',
color: '#D946EF',
agent: {
name: 'Kontext-Agent',
avatar: '🧭',
role: 'Lernt dich kennen und pflegt dein Kontext-Dokument',
systemPrompt: `Du bist ein neugieriger aber respektvoller Kontext-Agent. Ziel: verdichte was der User von sich selbst preisgibt zu einem gut strukturierten Kontext-Dokument, das andere AI-Agents als Prompt-Input nutzen können.
Vorgehen:
1. Lies immer zuerst das existierende kontextDoc + die letzten 5 Notizen + Goals, bevor du Fragen stellst.
2. Frage pro Iteration höchstens 2-3 konkrete Fragen. Keine Massenbefragung.
3. Schlage beim Update des Kontext-Dokuments immer eine Diff-Ansicht vor nie Full-Replace.
4. Respektiere Lücken: wenn der User etwas nicht teilen will, nimm das auf ("Thema nicht relevant für den Agent").
5. Schreibe das Kontext-Dokument auf Deutsch, in Ich-Form ("Ich bin…", "Mir ist wichtig…").
Struktur im Kontext-Dokument:
- # Wer ich bin (Rolle, Hintergrund)
- # Was mich umtreibt (aktuelle Projekte, Themen)
- # Wie ich arbeite (Arbeitsstil, Präferenzen)
- # Was ich lieber nicht teile (Opt-outs)`,
memory: `# Kontext-Ziele
(Hier kannst du festhalten welche Aspekte von dir der Agent priorisieren soll
z.B. "fokus auf berufliche Projekte, privat ist mir egal" oder "frag mich zu
meinen Hobbys" etc.)
`,
policy: CONTEXT_POLICY,
maxConcurrentMissions: 1,
},
scene: {
name: 'Kontext',
description: 'Dein Profil für alle anderen Agents',
openApps: [
{ appId: 'kontext', widthPx: 720 },
{ appId: 'ai-missions', widthPx: 440 },
{ appId: 'ai-workbench', widthPx: 440 },
],
},
missions: [
{
title: 'Kontext verdichten',
objective:
'Lies was schon da ist, identifiziere Lücken, stelle 2-3 Fragen und schlage ein Kontext-Update vor.',
conceptMarkdown: `# Kontext-Erkundung
Der Agent tickt wöchentlich und macht einen "Kontext-Check":
1. Was hat sich seit dem letzten Update geändert?
2. Welche Lücken sind noch im Profil?
3. 2-3 neue Fragen die der User beantworten kann (via Proposal-Inbox)
**Tipp:** Beantworte die Fragen einfach als Note-Antwort der Agent liest sie
beim nächsten Tick.`,
cadence: { kind: 'weekly', dayOfWeek: 0, atHour: 10 },
startPaused: true,
},
],
};

View file

@ -0,0 +1,30 @@
/**
* Agent-Templates canonical set of pre-configured agents the user can
* apply from the gallery in `/agents/templates`.
*
* Adding a new template: drop a new file next to `research.ts`,
* `context.ts`, `today.ts`, export the `AgentTemplate` constant, and
* add it to the `ALL_TEMPLATES` array below.
*/
import { researchTemplate } from './research';
import { contextTemplate } from './context';
import { todayTemplate } from './today';
export type {
AgentTemplate,
AgentTemplateAgentPart,
AgentTemplateScenePart,
AgentTemplateSceneApp,
AgentTemplateMissionPart,
} from './types';
export const ALL_TEMPLATES = [researchTemplate, contextTemplate, todayTemplate] as const;
export { researchTemplate, contextTemplate, todayTemplate };
/** Lookup helper returns the template matching the given id, or
* undefined. Useful for deep-links `/agents/templates?pick=research`. */
export function getTemplateById(id: string): (typeof ALL_TEMPLATES)[number] | undefined {
return ALL_TEMPLATES.find((t) => t.id === id);
}

View file

@ -0,0 +1,87 @@
import { AI_PROPOSABLE_TOOL_NAMES } from '../../policy/proposable-tools';
import type { AgentTemplate } from './types';
import type { AiPolicy } from '../../policy/types';
/**
* Research agent gets a topic + sources, writes a note per source,
* then summarizes into a report note. Biased toward propose for every
* note write so the user reviews what gets stored.
*/
const RESEARCH_POLICY: AiPolicy = {
tools: Object.fromEntries(AI_PROPOSABLE_TOOL_NAMES.map((n) => [n, 'propose'])),
defaultsByModule: {
notes: 'propose',
// Read-only modules default to auto — the agent is allowed to
// peek into kontext / goals without nagging the user.
goals: 'auto',
kontext: 'auto',
},
defaultForAi: 'propose',
};
export const researchTemplate: AgentTemplate = {
id: 'research',
label: 'Recherche-Agent',
tagline: 'Liest Quellen, schreibt Notizen, destilliert einen Bericht',
description: `Gib dem Agent ein Thema und eine Liste von Quellen-URLs. Er:
1. Liest jede Quelle einzeln
2. Schreibt pro Quelle eine strukturierte Notiz mit Kernaussagen + Zitaten
3. Fasst am Ende alle Notizen zu einem Gesamt-Bericht zusammen
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',
color: '#0EA5E9',
agent: {
name: 'Recherche-Agent',
avatar: '🔍',
role: 'Liest Quellen, schreibt Notizen, erstellt Gesamtberichte',
systemPrompt: `Du bist ein systematischer Recherche-Agent. Deine Aufgabe ist Quellen in strukturierte Notizen zu verwandeln und diese dann zu einem Gesamtbericht zu destillieren.
Vorgehen:
1. Pro Quelle: schreibe eine Notiz mit Titel "Q: [Quelle]", Kerninhalt als 3-7 Bullet-Points, direkte Zitate in Blockquotes.
2. Verweise auf keine Quelle die du nicht wirklich gelesen hast. Erfinde nichts.
3. Für den Gesamtbericht: fasse die Notizen unter 3-5 Thesen zusammen, mit Cross-Links zurück auf die Quellen-Notizen.
Schreib deutsch, klar, ohne Marketing-Sprache.`,
memory: `# Recherche-Richtlinien
(Hier kannst du festhalten wie du recherchiert haben willst z.B. bevorzugte Sprache,
Zitier-Stil, Themengebiete die dich besonders interessieren, Quellen denen du vertraust.)
`,
policy: RESEARCH_POLICY,
maxConcurrentMissions: 1,
},
scene: {
name: 'Recherche',
description: 'Quellen lesen, Notizen schreiben, Berichte erstellen',
openApps: [
{ appId: 'notes', widthPx: 540 },
{ appId: 'ai-missions', widthPx: 440 },
{ appId: 'ai-workbench', widthPx: 440 },
{ appId: 'news-research', widthPx: 540 },
],
},
missions: [
{
title: 'Quellen-Recherche zu einem Thema',
objective:
'Lies die verlinkten Quellen, schreibe pro Quelle eine Notiz, erstelle am Ende einen Gesamt-Bericht.',
conceptMarkdown: `# Recherche-Auftrag
Ersetze diesen Block mit:
- **Thema:** _worum geht es?_
- **Quellen:** _Liste von URLs oder Input-Notizen_
- **Fragestellung:** _was willst du am Ende wissen?_
Der Agent liest die Quellen sequentiell und schreibt pro Quelle eine Notiz (als
Vorschlag). Am Ende erstellt er einen Gesamtbericht der die Notizen zusammenfasst
und die Fragestellung beantwortet.`,
cadence: { kind: 'manual' },
startPaused: true,
},
],
};

View file

@ -0,0 +1,98 @@
import { AI_PROPOSABLE_TOOL_NAMES } from '../../policy/proposable-tools';
import type { AgentTemplate } from './types';
import type { AiPolicy } from '../../policy/types';
/**
* Today agent daily poem about what happened on this calendar date in
* history. Researches via the news-research module and saves the result
* as a note. Designed as a lightweight "delight" agent; shows off the
* autonomous-creative side of the system.
*/
const TODAY_POLICY: AiPolicy = {
tools: Object.fromEntries(AI_PROPOSABLE_TOOL_NAMES.map((n) => [n, 'propose'])),
defaultsByModule: {
notes: 'propose',
// The agent does plenty of reads; those are auto anyway under the
// policy helper, but explicit here to make the intent clear.
news: 'auto',
kontext: 'auto',
},
defaultForAi: 'propose',
};
export const todayTemplate: AgentTemplate = {
id: 'today',
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
Kalendertag in der Geschichte Besonderes passiert ist, und destilliert das zu
einem kleinen Gedicht als Notiz in deinem Journal.
Ein "Delight-Agent" hat keinen produktiven Zweck. Gedacht als tägliches Moment
der Reflexion und als Beispiel dafür dass AI nicht nur effizient sein muss.
Was er tut:
1. Ermittelt das heutige Datum
2. Recherchiert 3-5 historische Ereignisse dieses Tages (via Web-Research)
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',
color: '#F97316',
agent: {
name: 'Today-Agent',
avatar: '🌅',
role: 'Tägliches Gedicht über historische Ereignisse dieses Tages',
systemPrompt: `Du bist der Today-Agent. Einziger Job: jeden Morgen ein kurzes Gedicht über etwas das an diesem Tag in der Geschichte passiert ist.
Regeln:
1. **Immer** zuerst per Web-Research "on this day [Datum]" oder "historische Ereignisse [Datum]" recherchieren.
2. Wähle EIN Ereignis nicht fünf. Lieber ein kleines poetisches Detail als eine Liste.
3. Gedicht: 4-8 Zeilen, deutsch, **kein Reim-Zwang** (Reim nur wenn er natürlich kommt), freier Rhythmus ok.
4. Speichere als Proposal für eine Note Titel "Heute — [YYYY-MM-DD] — [kurzes Thema]".
5. Kein Content-Warning nötig für historisch bekannte Themen (Kriege, Tode). Aber: behandle sie würdevoll, nicht ironisch.
6. **Kein Meta-Kommentar im Gedicht selbst** kein "An diesem Tag vor 50 Jahren…". Direkt ins Bild.
Beispiel-Qualität: lieber ein kurzes, klares Bild als eine überladene Reim-Konstruktion.`,
memory: `# Stilvorlieben
(Hier kannst du dem Agent sagen welchen Ton du magst z.B. "eher melancholisch"
oder "mit Humor" oder "klassisch und streng".)
`,
policy: TODAY_POLICY,
maxConcurrentMissions: 1,
},
scene: {
name: 'Today',
description: 'Dein tägliches Gedicht',
openApps: [
{ appId: 'journal', widthPx: 540 },
{ appId: 'ai-missions', widthPx: 440 },
{ appId: 'ai-workbench', widthPx: 440 },
],
},
missions: [
{
title: 'Tägliches Gedicht über heute',
objective:
'Recherchiere was an diesem Datum in der Geschichte passiert ist, wähle ein Thema, schreibe ein kurzes deutsches Gedicht, schlage eine Journal-Notiz vor.',
conceptMarkdown: `# Today-Poem
**Cadence:** jeden Morgen um 7 Uhr.
**Ablauf jedes Runs:**
1. Web-Research: "on this day [Datum]" + deutschsprachige Quellen bevorzugt
2. Pick: ein Ereignis, ein Detail, ein Bild
3. Write: 4-8 Zeilen freies Gedicht
4. Propose: neue Journal-Notiz mit Titel "Heute — [Datum] — [Thema]"
Der Agent soll **nicht** jeden Tag eine Zusammenfassung produzieren sondern einen
kleinen poetischen Moment. Wenn er nichts findet das ihn inspiriert, darf er das
auch sagen ("heute fiel mir nichts ein").`,
cadence: { kind: 'daily', atHour: 7, atMinute: 0 },
startPaused: true,
},
],
};

View file

@ -0,0 +1,84 @@
/**
* 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.
*
* 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.
*/
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`. */
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 {
readonly appId: string;
readonly widthPx?: number;
readonly maximized?: boolean;
}
export interface AgentTemplateScenePart {
/** Display name for the scene tab. */
name: string;
description?: string;
openApps: readonly AgentTemplateSceneApp[];
}
export interface AgentTemplateMissionPart {
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[];
}

View file

@ -75,5 +75,13 @@ export {
type PolicyDecision,
} from './policy';
export type { Agent, AgentState } from './agents';
export { DEFAULT_AGENT_ID, DEFAULT_AGENT_NAME } from './agents';
export type {
Agent,
AgentState,
AgentTemplate,
AgentTemplateAgentPart,
AgentTemplateScenePart,
AgentTemplateSceneApp,
AgentTemplateMissionPart,
} from './agents';
export { DEFAULT_AGENT_ID, DEFAULT_AGENT_NAME, ALL_TEMPLATES, getTemplateById } from './agents';