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

@ -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';