mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 12:06:42 +02:00
Closes the three biggest tool-coverage gaps so the shipped agent templates can actually do their job end-to-end. Before this, the Recherche-Agent couldn't create notes (only edit), the Today-Agent couldn't create journal entries, and no habit-related tool was server-proposable at all. shared-ai (proposable-tools.ts): - create_note (notes) — key unlock: Recherche-Agent now creates per-source notes and the summary report. - create_journal_entry (journal) — key unlock: Today-Agent proposes a poem as a journal entry with optional mood. - create_habit (habits) — agent can suggest new habits. - log_habit (habits) — agent can log a habit completion for today. Organized the list with per-module section comments for readability now that we're at 15 proposable tools. mana-ai (planner/tools.ts): - 5 new tool definitions with full parameter schemas: * create_note (title, content?) * create_journal_entry (content, title?, mood? enum) * create_habit (title, icon, color) * log_habit (habitId, note?) - Drift-guard contract test passes (41/41) — confirms the mana-ai tool list is in sync with the shared-ai canonical set. Webapp (policy.ts): - get_habits added to AUTO_TOOLS (read-only; agent can inspect which habits exist without nagging the user for approval). - list_notes added to AUTO_TOOLS (was already used in the reasoning loop but missing from the explicit auto-list; the planner default fell through to 'propose' which was wasteful for a read op). Module coverage after this change: ✅ todo (5 tools) ✅ calendar (2) ✅ notes (5 incl. create) ✅ places (4) ✅ drink (3) ✅ food (2) ✅ news (1) ✅ journal (1) ✅ habits (3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
228 lines
7.4 KiB
TypeScript
228 lines
7.4 KiB
TypeScript
/**
|
|
* Hardcoded allow-list of tools the server-side Planner may propose.
|
|
*
|
|
* Parameter shapes live here (the webapp owns the full Dexie-bound
|
|
* registry); the set of NAMES is shared via `@mana/shared-ai`'s
|
|
* `AI_PROPOSABLE_TOOL_NAMES`. The module-load assertion at the bottom
|
|
* guards against drift in either direction — if this file or the shared
|
|
* list falls out of sync, the service refuses to start.
|
|
*/
|
|
|
|
import { AI_PROPOSABLE_TOOL_SET, type AvailableTool } from '@mana/shared-ai';
|
|
|
|
export const AI_AVAILABLE_TOOLS: readonly AvailableTool[] = [
|
|
{
|
|
name: 'create_task',
|
|
module: 'todo',
|
|
description: 'Erstellt einen neuen Task mit optionalem Faelligkeitsdatum und Prioritaet',
|
|
parameters: [
|
|
{ name: 'title', type: 'string', description: 'Titel des Tasks', required: true },
|
|
{
|
|
name: 'dueDate',
|
|
type: 'string',
|
|
description: 'Faelligkeitsdatum (YYYY-MM-DD)',
|
|
required: false,
|
|
},
|
|
{
|
|
name: 'priority',
|
|
type: 'string',
|
|
description: 'Prioritaet',
|
|
required: false,
|
|
enum: ['low', 'medium', 'high'],
|
|
},
|
|
{ name: 'description', type: 'string', description: 'Beschreibung', required: false },
|
|
],
|
|
},
|
|
{
|
|
name: 'complete_task',
|
|
module: 'todo',
|
|
description: 'Markiert einen Task als erledigt',
|
|
parameters: [{ name: 'taskId', type: 'string', description: 'ID des Tasks', required: true }],
|
|
},
|
|
{
|
|
name: 'complete_tasks_by_title',
|
|
module: 'todo',
|
|
description: 'Markiert alle Tasks deren Titel den Substring enthält (case-insensitive)',
|
|
parameters: [
|
|
{
|
|
name: 'titleSubstring',
|
|
type: 'string',
|
|
description: 'Teil des Task-Titels',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'create_event',
|
|
module: 'calendar',
|
|
description: 'Erstellt einen Kalender-Event',
|
|
parameters: [
|
|
{ name: 'title', type: 'string', description: 'Event-Titel', required: true },
|
|
{ name: 'startIso', type: 'string', description: 'Start (ISO)', required: true },
|
|
{ name: 'endIso', type: 'string', description: 'Ende (ISO)', required: false },
|
|
],
|
|
},
|
|
{
|
|
name: 'create_place',
|
|
module: 'places',
|
|
description: 'Fügt einen neuen Ort hinzu',
|
|
parameters: [
|
|
{ name: 'name', type: 'string', description: 'Name des Ortes', required: true },
|
|
{ name: 'category', type: 'string', description: 'Kategorie', required: false },
|
|
],
|
|
},
|
|
{
|
|
name: 'visit_place',
|
|
module: 'places',
|
|
description: 'Vermerkt einen Besuch an einem bereits erfassten Ort',
|
|
parameters: [{ name: 'placeId', type: 'string', description: 'ID des Ortes', required: true }],
|
|
},
|
|
{
|
|
name: 'undo_drink',
|
|
module: 'drink',
|
|
description: 'Macht den letzten Drink-Eintrag rückgängig',
|
|
parameters: [],
|
|
},
|
|
{
|
|
name: 'save_news_article',
|
|
module: 'news',
|
|
description:
|
|
'Speichert einen Artikel von einer URL in die Leseliste. URL wird serverseitig per Readability extrahiert.',
|
|
parameters: [
|
|
{ name: 'url', type: 'string', description: 'Die Artikel-URL', required: true },
|
|
{
|
|
name: 'title',
|
|
type: 'string',
|
|
description: 'Anzeigetitel für den Approval-Dialog (informativ)',
|
|
required: false,
|
|
},
|
|
{
|
|
name: 'summary',
|
|
type: 'string',
|
|
description: 'Kurze Begründung warum dieser Artikel relevant ist',
|
|
required: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'update_note',
|
|
module: 'notes',
|
|
description:
|
|
'Überschreibt Titel und/oder Inhalt einer bestehenden Notiz. Destruktiv — bevorzuge append_to_note oder add_tag_to_note wenn du nur ergänzen willst.',
|
|
parameters: [
|
|
{ name: 'noteId', type: 'string', description: 'ID der Notiz', required: true },
|
|
{ name: 'title', type: 'string', description: 'Neuer Titel', required: false },
|
|
{
|
|
name: 'content',
|
|
type: 'string',
|
|
description: 'Neuer Inhalt (überschreibt vollständig)',
|
|
required: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'append_to_note',
|
|
module: 'notes',
|
|
description:
|
|
'Hängt Text ans Ende des Inhalts einer bestehenden Notiz an (neue Zeile getrennt). Nicht-destruktiv.',
|
|
parameters: [
|
|
{ name: 'noteId', type: 'string', description: 'ID der Notiz', required: true },
|
|
{ name: 'content', type: 'string', description: 'Text zum Anhängen', required: true },
|
|
],
|
|
},
|
|
{
|
|
name: 'add_tag_to_note',
|
|
module: 'notes',
|
|
description:
|
|
'Fügt einen Hashtag (z.B. "#Natur") an eine bestehende Notiz an. Idempotent — wenn der Tag schon vorhanden ist, passiert nichts.',
|
|
parameters: [
|
|
{ name: 'noteId', type: 'string', description: 'ID der Notiz', required: true },
|
|
{
|
|
name: 'tag',
|
|
type: 'string',
|
|
description:
|
|
'Tag-Name (ohne #; z.B. "Natur", "Arbeit"). Leerzeichen werden durch _ ersetzt.',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
|
|
// ── Notes: create ────────────────────────────────────────
|
|
{
|
|
name: 'create_note',
|
|
module: 'notes',
|
|
description: 'Erstellt eine neue Notiz. Gibt die ID der angelegten Notiz zurueck.',
|
|
parameters: [
|
|
{ name: 'title', type: 'string', description: 'Titel der Notiz', required: true },
|
|
{
|
|
name: 'content',
|
|
type: 'string',
|
|
description: 'Inhalt der Notiz (Markdown)',
|
|
required: false,
|
|
},
|
|
],
|
|
},
|
|
|
|
// ── Journal ──────────────────────────────────────────────
|
|
{
|
|
name: 'create_journal_entry',
|
|
module: 'journal',
|
|
description:
|
|
'Erstellt einen neuen Tagebuch-Eintrag fuer den heutigen Tag. Gibt die ID zurueck.',
|
|
parameters: [
|
|
{
|
|
name: 'content',
|
|
type: 'string',
|
|
description: 'Inhalt des Eintrags (Markdown)',
|
|
required: true,
|
|
},
|
|
{ name: 'title', type: 'string', description: 'Optionaler Titel', required: false },
|
|
{
|
|
name: 'mood',
|
|
type: 'string',
|
|
description: 'Stimmung',
|
|
required: false,
|
|
enum: ['great', 'good', 'neutral', 'bad', 'terrible'],
|
|
},
|
|
],
|
|
},
|
|
|
|
// ── Habits ───────────────────────────────────────────────
|
|
{
|
|
name: 'create_habit',
|
|
module: 'habits',
|
|
description: 'Erstellt einen neuen Habit-Tracker. Gibt die ID des neuen Habits zurueck.',
|
|
parameters: [
|
|
{ name: 'title', type: 'string', description: 'Titel des Habits', required: true },
|
|
{ name: 'icon', type: 'string', description: 'Emoji-Icon', required: true },
|
|
{ name: 'color', type: 'string', description: 'Hex-Farbe (z.B. #EF4444)', required: true },
|
|
],
|
|
},
|
|
{
|
|
name: 'log_habit',
|
|
module: 'habits',
|
|
description:
|
|
'Loggt eine Ausfuehrung eines existierenden Habits fuer heute. Optional mit Notiz.',
|
|
parameters: [
|
|
{ name: 'habitId', type: 'string', description: 'ID des Habits', required: true },
|
|
{ name: 'note', type: 'string', description: 'Optionale Notiz zum Log', required: false },
|
|
],
|
|
},
|
|
];
|
|
|
|
export const AI_AVAILABLE_TOOL_NAMES = new Set<string>(AI_AVAILABLE_TOOLS.map((t) => t.name));
|
|
|
|
// ── Contract check — runs on module load ───────────────────
|
|
// Catches drift between this file and @mana/shared-ai's canonical
|
|
// proposable list. A mismatch means the webapp's policy + mana-ai are
|
|
// about to disagree; better fail fast than ship a silently-degraded AI.
|
|
{
|
|
const extra = [...AI_AVAILABLE_TOOL_NAMES].filter((n) => !AI_PROPOSABLE_TOOL_SET.has(n));
|
|
const missing = [...AI_PROPOSABLE_TOOL_SET].filter((n) => !AI_AVAILABLE_TOOL_NAMES.has(n));
|
|
if (extra.length || missing.length) {
|
|
throw new Error(
|
|
`[mana-ai] AI_AVAILABLE_TOOLS drift vs AI_PROPOSABLE_TOOL_NAMES. ` +
|
|
`extra=${JSON.stringify(extra)} missing=${JSON.stringify(missing)}`
|
|
);
|
|
}
|
|
}
|