mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 07:33:39 +02:00
feat(shared-ai): canonical proposable-tool list + drift guard on mana-ai
Makes the webapp's AI policy and the server's tool allow-list physically impossible to drift. Adds the missing entries the guard caught on first run: `complete_tasks_by_title`, `visit_place`, `undo_drink` now have parameter schemas server-side too. - `packages/shared-ai/src/policy/proposable-tools.ts` - `AI_PROPOSABLE_TOOL_NAMES` as `const` array + literal union type - `AI_PROPOSABLE_TOOL_SET` for set-membership checks - Webapp `DEFAULT_AI_POLICY` derives its `propose` entries from the shared list via `Object.fromEntries(...)` — adding a tool there is now a one-line change in `@mana/shared-ai` - mana-ai `AI_AVAILABLE_TOOLS`: module-load assertion compares its hardcoded names against `AI_PROPOSABLE_TOOL_SET` and throws with a pointed error on drift (extras in one direction, missing in the other). Service refuses to start on mismatch — better than silent degradation. - Bun test (`tools.test.ts`) runs the same contract plus sanity checks (non-empty description, required params carry docs). Vitest policy test adds the symmetric check on the webapp side. All three runtimes now green: webapp 66/66, shared-ai 2/2, mana-ai 9/9 Bun tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dccd9c5c4e
commit
4be5e29bd3
7 changed files with 164 additions and 37 deletions
41
services/mana-ai/src/planner/tools.test.ts
Normal file
41
services/mana-ai/src/planner/tools.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { AI_PROPOSABLE_TOOL_SET } from '@mana/shared-ai';
|
||||
import { AI_AVAILABLE_TOOLS, AI_AVAILABLE_TOOL_NAMES } from './tools';
|
||||
|
||||
describe('AI_AVAILABLE_TOOLS contract', () => {
|
||||
it('every AvailableTool name is in the shared proposable set', () => {
|
||||
for (const tool of AI_AVAILABLE_TOOLS) {
|
||||
expect(
|
||||
AI_PROPOSABLE_TOOL_SET.has(tool.name),
|
||||
`"${tool.name}" missing from @mana/shared-ai AI_PROPOSABLE_TOOL_NAMES`
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('every shared proposable name has an AvailableTool entry', () => {
|
||||
for (const name of AI_PROPOSABLE_TOOL_SET) {
|
||||
expect(
|
||||
AI_AVAILABLE_TOOL_NAMES.has(name),
|
||||
`"${name}" missing from services/mana-ai AI_AVAILABLE_TOOLS — add the tool definition`
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('every tool has at least a name + description + module', () => {
|
||||
for (const tool of AI_AVAILABLE_TOOLS) {
|
||||
expect(tool.name.length).toBeGreaterThan(0);
|
||||
expect(tool.module.length).toBeGreaterThan(0);
|
||||
expect(tool.description.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('required params carry a non-empty description', () => {
|
||||
for (const tool of AI_AVAILABLE_TOOLS) {
|
||||
for (const p of tool.parameters) {
|
||||
if (p.required) {
|
||||
expect(p.description.length, `${tool.name}.${p.name}.description`).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,19 +1,14 @@
|
|||
/**
|
||||
* Hardcoded allow-list of tools the server-side Planner may propose.
|
||||
*
|
||||
* The webapp owns the full tool registry (in
|
||||
* `apps/mana/apps/web/src/lib/data/tools/registry.ts`) and the policy
|
||||
* (`DEFAULT_AI_POLICY` in `data/ai/policy.ts`). This file mirrors the
|
||||
* subset where policy === 'propose' so the mana-ai Bun service can
|
||||
* build a valid prompt without importing Dexie-bound code.
|
||||
*
|
||||
* Drift risk: if the webapp adds a new proposable tool and this file
|
||||
* isn't updated, the mana-ai Planner simply won't suggest it — graceful
|
||||
* degradation. A contract test that compares both lists would be a
|
||||
* sensible follow-up.
|
||||
* 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 type { AvailableTool } from '@mana/shared-ai';
|
||||
import { AI_PROPOSABLE_TOOL_SET, type AvailableTool } from '@mana/shared-ai';
|
||||
|
||||
export const AI_AVAILABLE_TOOLS: readonly AvailableTool[] = [
|
||||
{
|
||||
|
|
@ -44,6 +39,19 @@ export const AI_AVAILABLE_TOOLS: readonly AvailableTool[] = [
|
|||
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',
|
||||
|
|
@ -63,6 +71,33 @@ export const AI_AVAILABLE_TOOLS: readonly AvailableTool[] = [
|
|||
{ 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: [],
|
||||
},
|
||||
];
|
||||
|
||||
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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue