managarten/packages/shared-ai/src/planner/prompt.ts
Till JS efc7641a60 chore(ai): P2 batch — prompt sync, perf, dedup, scope unification
Six P2 items from the AI Workbench audit:

#7 Prompt ↔ loop budget sync:
  System prompt now says "1 bis 5 Schritte pro Planungsrunde, bis zu 5
  Planungsrunden" — matches MAX_REASONING_LOOP_ITERATIONS. Cross-ref
  comment added to runner.ts.

#9 SceneHeader: useAgents() → useAgent(id):
  Only loads the single bound agent instead of the full agent list.
  Eliminates unnecessary Dexie churn on every scene header render.

#10 Unified scope filter:
  New scope-filter.ts with filterByScopeTagMap() (batch, sync) and
  filterByScopeAsync() (per-record). Both scope-context.ts (AI) and
  scene-scope.svelte.ts (UI) now import from the shared module —
  zero duplicated filter logic.

#11 Research dedup:
  Research input ID changed from `news-research-${Date.now()}` to
  `news-research-${mission.id}` — re-runs overwrite instead of
  appending duplicates.

#12 Kontext injection policy clarified:
  loadAgentKontextAsResolvedInput no longer falls back to the global
  singleton. Comment + code aligned: kontext injection is explicit
  (via input picker), not auto. Dead loadKontextAsResolvedInput
  kept for potential future opt-in auto-inject feature.

Audit doc updated with all items marked DONE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:33:52 +02:00

113 lines
3.9 KiB
TypeScript

/**
* Prompt builder for the Mission Planner.
*
* Produces a system + user message pair. The grammar we ask the model to
* return is deliberately small (JSON in a fenced block, one shape) — the
* parser is strict, and we'd rather the LLM mess up in a detectable way
* than produce half-valid output.
*
* Pure function: no ambient state. Used identically from the browser and
* the mana-ai Bun service.
*/
import type { AiPlanInput } from './types';
export interface PlannerMessages {
readonly system: string;
readonly user: string;
}
export function buildPlannerPrompt(input: AiPlanInput): PlannerMessages {
return {
system: buildSystemPrompt(input),
user: buildUserPrompt(input),
};
}
function buildSystemPrompt(input: AiPlanInput): string {
const toolBlock = input.availableTools
.map((t) => {
const params = t.parameters
.map((p) => {
const req = p.required ? ' (required)' : '';
const enumeration = p.enum ? ` [${p.enum.join('|')}]` : '';
return ` - ${p.name}: ${p.type}${enumeration}${req}${p.description}`;
})
.join('\n');
return `${t.name} (${t.module}) — ${t.description}\n${params || ' (no parameters)'}`;
})
.join('\n');
return `Du bist eine KI, die im Auftrag des Nutzers an einer langlebigen Mission arbeitet.
Dein Job: aus dem aktuellen Mission-Kontext einen konkreten Plan ableiten — 1 bis 5 Schritte pro Planungsrunde, jeder ein Tool-Aufruf auf Nutzerdaten. Es gibt bis zu 5 Planungsrunden pro Iteration. Jeder Schritt MUSS eine Begründung haben (rationale), die der Nutzer in der Review-UI sieht.
Wichtige Regeln:
1. Nutze NUR Tools aus der Liste unten. Unbekannte Tools → Plan invalide.
2. Read-only Tools (z.B. list_*, get_*) laufen automatisch — ihre Ausgabe siehst du in der nächsten Planungsrunde als "Zwischenergebnisse" und kannst dann darauf aufbauend schreibende Tools vorschlagen. Write-Tools (create_*, update_*, add_tag_to_note, etc.) werden dem Nutzer zur Approval vorgelegt.
3. Wenn ein Batch-Job ansteht (z.B. "tagge alle Notizen"), gib alle Einzel-Calls in EINEM plan zurück — du kriegst nach propose-Tools keinen weiteren Turn.
4. Berücksichtige das Feedback aus vorherigen Iterationen (unten im User-Prompt). Wenn ein Vorschlag rejected wurde, wiederhole ihn nicht ohne Änderung.
5. Antworte AUSSCHLIESSLICH mit einem JSON-Block in folgendem Format, keine Prosa davor/danach:
\`\`\`json
{
"summary": "Ein Satz was du in dieser Iteration tust.",
"steps": [
{
"summary": "Kurzer Schritt-Titel",
"toolName": "create_task",
"params": { "title": "…" },
"rationale": "Warum genau jetzt, auf Basis welchen Inputs."
}
]
}
\`\`\`
Verfügbare Tools:
${toolBlock || ' (keine Tools verfügbar — gib leeren steps zurück)'}`;
}
function buildUserPrompt(input: AiPlanInput): string {
const { mission, resolvedInputs } = input;
const inputsBlock =
resolvedInputs.length === 0
? '_(keine verlinkten Inputs)_'
: resolvedInputs
.map((r) => {
const header = `### ${r.module}/${r.table}: ${r.title ?? r.id}`;
return `${header}\n${r.content}`;
})
.join('\n\n');
const iterationHistory =
mission.iterations.length === 0
? '_(erste Iteration)_'
: mission.iterations
.slice(-3)
.map((it) => {
const steps = it.plan.map((s) => ` - [${s.status}] ${s.summary}`).join('\n');
const feedback = it.userFeedback ? `\n Nutzer-Feedback: ${it.userFeedback}` : '';
const summary = it.summary ? `\n Summary: ${it.summary}` : '';
return `**${it.startedAt}** (${it.overallStatus}):${summary}\n${steps}${feedback}`;
})
.join('\n\n');
return `# Mission: ${mission.title}
## Konzept
${mission.conceptMarkdown || '_(leer)_'}
## Konkretes Ziel
${mission.objective}
## Verlinkte Inputs
${inputsBlock}
## Letzte Iterationen (max. 3)
${iterationHistory}
---
Erzeuge jetzt einen Plan für die nächste Iteration.`;
}