mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 18:26:42 +02:00
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>
117 lines
4.2 KiB
TypeScript
117 lines
4.2 KiB
TypeScript
/**
|
||
* 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
|
||
* ~6000–8000 — 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');
|
||
}
|