From 7822340ea0e475883dcf8994b747183b29b0e119 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 16 Apr 2026 00:36:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(ai-agents):=20Template=20gallery=20?= =?UTF-8?q?=E2=80=94=203=20ready-to-use=20agent=20bundles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/lib/data/ai/agents/apply-template.ts | 154 ++++++ .../src/lib/modules/ai-agents/ListView.svelte | 75 ++- .../(app)/agents/templates/+page.svelte | 512 ++++++++++++++++++ packages/shared-ai/src/agents/index.ts | 9 + .../shared-ai/src/agents/templates/context.ts | 95 ++++ .../shared-ai/src/agents/templates/index.ts | 30 + .../src/agents/templates/research.ts | 87 +++ .../shared-ai/src/agents/templates/today.ts | 98 ++++ .../shared-ai/src/agents/templates/types.ts | 84 +++ packages/shared-ai/src/index.ts | 12 +- 10 files changed, 1150 insertions(+), 6 deletions(-) create mode 100644 apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts create mode 100644 apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte create mode 100644 packages/shared-ai/src/agents/templates/context.ts create mode 100644 packages/shared-ai/src/agents/templates/index.ts create mode 100644 packages/shared-ai/src/agents/templates/research.ts create mode 100644 packages/shared-ai/src/agents/templates/today.ts create mode 100644 packages/shared-ai/src/agents/templates/types.ts diff --git a/apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts b/apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts new file mode 100644 index 000000000..955d2f775 --- /dev/null +++ b/apps/mana/apps/web/src/lib/data/ai/agents/apply-template.ts @@ -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 { + 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, + }; +} diff --git a/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte b/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte index 0e3c522e9..5093a5c91 100644 --- a/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte @@ -17,8 +17,10 @@ handle the common "let the agent touch todo but not calendar" case. --> {#if mode === 'list'} + {@const onlyDefaultAgent = agents.value.length === 1 && agents.value[0].id === DEFAULT_AGENT_ID}
- +
+ + {#if onlyDefaultAgent} + + {/if} + {#if agents.value.length === 0}

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".

{:else}
    @@ -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)); diff --git a/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte b/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte new file mode 100644 index 000000000..833093ca2 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte @@ -0,0 +1,512 @@ + + + + + Agent-Templates — Mana + + +
    +
    + +

    Agent-Templates

    +

    + 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. +

    +
    + +
    + {#each ALL_TEMPLATES as t (t.id)} + + {/each} +
    + + {#if selected} +
    +
    + {selected.agent.avatar} +
    +

    {selected.label}

    +

    {selected.agent.role}

    +
    +
    + +
    {selected.description}
    + + {#if selected.scene} +
    +

    Scene-Layout

    +

    + {selected.scene.name} + {#if selected.scene.description} + — {selected.scene.description}{/if} +

    +
      + {#each selected.scene.openApps as app (app.appId)} +
    • + {app.appId} + {#if app.widthPx}{app.widthPx}px{/if} +
    • + {/each} +
    +
    + {/if} + + {#if selected.missions && selected.missions.length > 0} +
    +

    Starter-Missionen

    +
      + {#each selected.missions as m} +
    • + {m.title} +

      {m.objective}

      + + {#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} + +
    • + {/each} +
    +
    + {/if} + +
    +

    Optionen

    + {#if selected.scene} + + {/if} + {#if selected.missions && selected.missions.length > 0} + + + {/if} +
    + + {#if result} +
    + +
    + + {result.wasExisting + ? `„${result.agentName}" existierte schon — wiederverwendet.` + : `Agent „${result.agentName}" angelegt.`} + +

    + {#if result.sceneCreated}Scene angelegt + aktiviert.{/if} + {#if result.missionCount > 0} + {result.missionCount} Mission{result.missionCount !== 1 ? 'en' : ''} + {optStartActive ? 'aktiviert' : 'pausiert angelegt'}. + {/if} +

    + {#if result.warnings.length > 0} +
      + {#each result.warnings as w} +
    • âš  {w}
    • + {/each} +
    + {/if} +
    +
    +
    + + +
    + {:else} + {#if error} +
    + Konnte Template nicht anwenden +

    {error}

    +
    + {/if} +
    + +
    + {/if} +
    + {/if} +
    + + diff --git a/packages/shared-ai/src/agents/index.ts b/packages/shared-ai/src/agents/index.ts index a4719e619..28be15532 100644 --- a/packages/shared-ai/src/agents/index.ts +++ b/packages/shared-ai/src/agents/index.ts @@ -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'; diff --git a/packages/shared-ai/src/agents/templates/context.ts b/packages/shared-ai/src/agents/templates/context.ts new file mode 100644 index 000000000..66566c4f5 --- /dev/null +++ b/packages/shared-ai/src/agents/templates/context.ts @@ -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, + }, + ], +}; diff --git a/packages/shared-ai/src/agents/templates/index.ts b/packages/shared-ai/src/agents/templates/index.ts new file mode 100644 index 000000000..9a8b1eb0c --- /dev/null +++ b/packages/shared-ai/src/agents/templates/index.ts @@ -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); +} diff --git a/packages/shared-ai/src/agents/templates/research.ts b/packages/shared-ai/src/agents/templates/research.ts new file mode 100644 index 000000000..1669856c7 --- /dev/null +++ b/packages/shared-ai/src/agents/templates/research.ts @@ -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, + }, + ], +}; diff --git a/packages/shared-ai/src/agents/templates/today.ts b/packages/shared-ai/src/agents/templates/today.ts new file mode 100644 index 000000000..8bd94ccb8 --- /dev/null +++ b/packages/shared-ai/src/agents/templates/today.ts @@ -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, + }, + ], +}; diff --git a/packages/shared-ai/src/agents/templates/types.ts b/packages/shared-ai/src/agents/templates/types.ts new file mode 100644 index 000000000..5414f3fe4 --- /dev/null +++ b/packages/shared-ai/src/agents/templates/types.ts @@ -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[]; +} diff --git a/packages/shared-ai/src/index.ts b/packages/shared-ai/src/index.ts index 58d9cf58f..b1c291287 100644 --- a/packages/shared-ai/src/index.ts +++ b/packages/shared-ai/src/index.ts @@ -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';