managarten/packages/shared-ai/src/planner/system-prompt.ts
Till JS 4daca8970b feat(shared-ai): runPlannerLoop + compact system prompt for function calling
Introduces the new planner pipeline both the webapp runner and the
mana-ai tick will swap onto in the next commits. Additive for now —
the legacy buildPlannerPrompt + parsePlannerResponse stay exported so
callers can migrate one at a time; they get removed once the last
consumer is gone.

- planner/loop.ts — runPlannerLoop orchestrates a multi-turn chat
  against a caller-supplied LlmClient. Tool-calls from the LLM are
  handed to an onToolCall callback and their results fed back as
  tool-messages. Parallel tool-calls in one turn execute sequentially
  to keep the message log linear for debugging. Stops on assistant
  stop, empty tool_calls, or a hard max-rounds ceiling (default 5).
- planner/system-prompt.ts — new buildSystemPrompt. ~40-line German
  system frame, no tool listing (the SDK-level tools field carries
  the schemas now), no JSON format example, no "please return JSON"
  plea. User frame renders mission + linked inputs + last 3
  iteration summaries, same as before.
- Five test cases covering the loop: immediate stop, single tool
  call with result feedback, parallel calls execute in order, tool
  failures propagate as tool-messages the LLM can react to, and
  maxRounds ceiling fires with the right stopReason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:31:01 +02:00

117 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* System-prompt builder for the function-calling planner.
*
* Radically smaller than the pre-migration text-JSON prompt: no tool
* listing (the LLM gets schemas via the native ``tools`` request
* field), no format example (the SDK enforces structured tool_calls),
* no "please return JSON" plea. We just tell the LLM what its job is,
* how to behave in a reasoning loop, and hand over control.
*
* The rendered prompt is ~400 tokens compared to the previous
* ~60008000 — big savings on cost and, more importantly, on the
* signal-to-noise ratio the model has to filter.
*/
import type { Mission } from '../missions/types';
import type { ResolvedInput } from './types';
export interface SystemPromptInput {
readonly mission: Mission;
readonly resolvedInputs: readonly ResolvedInput[];
/** When set, included verbatim as the agent's persona frame. */
readonly agentSystemPrompt?: string | null;
/** When set, appended as the agent's persistent memory. */
readonly agentMemory?: string | null;
}
export interface SystemPromptOutput {
readonly systemPrompt: string;
readonly userPrompt: string;
}
export function buildSystemPrompt(input: SystemPromptInput): SystemPromptOutput {
const systemPrompt = buildSystemFrame(input);
const userPrompt = buildUserFrame(input);
return { systemPrompt, userPrompt };
}
function buildSystemFrame(input: SystemPromptInput): string {
const agentBlock = renderAgentContext(input);
return [
'Du arbeitest im Auftrag des Nutzers an einer langlebigen Mission.',
'',
'Dein Vorgehen:',
'1. Lies zuerst (Read-Tools liefern dir sofort Ergebnisse) — verstehe den Zustand, bevor du schreibst.',
'2. Führe anschließend die notwendigen Schreib-Tools aus, um das konkrete Ziel umzusetzen.',
'3. Wiederhole bis zu 5 Planungsrunden: nach jedem Tool-Aufruf bekommst du das Ergebnis zurück und kannst daraus den nächsten Schritt ableiten.',
'4. Stoppe, wenn das Ziel erreicht ist oder kein sinnvoller nächster Schritt bleibt.',
'5. Berücksichtige Feedback aus früheren Iterationen — wiederhole keinen Schritt, der zuvor fehlgeschlagen ist, ohne ihn zu ändern.',
'',
'Wichtig:',
'- Nutze ausschließlich die Tools, die dir als Function-Calls bereitgestellt werden. Nennungen in Prosa werden ignoriert.',
'- Wenn mehrere unabhängige Aktionen anstehen (z. B. "erstelle 8 Fragen"), gib sie in einem einzigen Turn als parallele Tool-Calls aus — das spart Runden.',
'- Wenn ein Tool einen Fehler zurückgibt, reagiere darauf (anderes Tool probieren oder stoppen) — ignoriere Fehler nicht.',
agentBlock,
]
.filter(Boolean)
.join('\n');
}
function renderAgentContext(input: SystemPromptInput): string {
const parts: string[] = [];
if (input.agentSystemPrompt?.trim()) {
parts.push('\n<agent_persona>');
parts.push(input.agentSystemPrompt.trim());
parts.push('</agent_persona>');
}
if (input.agentMemory?.trim()) {
parts.push('\n<agent_memory>');
parts.push(input.agentMemory.trim());
parts.push('</agent_memory>');
}
return parts.join('\n');
}
function buildUserFrame(input: SystemPromptInput): string {
const { mission, resolvedInputs } = input;
const inputsBlock =
resolvedInputs.length === 0
? '_(keine verlinkten Inputs)_'
: resolvedInputs
.map((r) => `### ${r.module}/${r.table}: ${r.title ?? r.id}\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 || '_(nicht gesetzt)_',
'',
'## Verlinkte Inputs',
inputsBlock,
'',
'## Letzte Iterationen (max. 3)',
iterationHistory,
'',
'---',
'',
'Beginne jetzt mit der nächsten Iteration. Rufe die nötigen Tools auf.',
].join('\n');
}