cleaner
Clean and structure raw user prompts by normalizing tone, removing sensitive data, and preserving intent. Outputs JSON with retouched text, redactions, notes, and risks for safer pre-processing and planning.
Instructions
Pre-reasoning prompt normalizer and PII redactor. Use when: you receive raw/free-form user text and need it cleaned before planning, tool selection, or code execution. Does: normalize tone, structure the ask, and redact secrets; preserves user intent. Safe: read-only, idempotent, no side effects (good default to run automatically). Input: { prompt, mode?, temperature? } — defaults mode='general', temperature=0.2; mode='code' only for code-related prompts. Output: JSON { retouched, notes?, openQuestions?, risks?, redactions? }. Keywords: clean, sanitize, normalize, redact, structure, preprocess, guardrails
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| mode | No | Retouching mode; default 'general'. Use 'code' only for code-related prompts. | |
| prompt | Yes | Raw user prompt | |
| temperature | No | Sampling temperature (0-2); default 0.2 |
Implementation Reference
- src/cleaner.ts:83-180 (handler)Primary handler function that implements the 'cleaner' tool logic: calls LLM with system prompt to retouch input, parses JSON output with robust extraction and retries, applies PII redaction, and structures the result.export async function retouchPrompt(input: RetouchInputT): Promise<RetouchOutputT> { const start = Date.now(); const system = await loadCleanerSystemPrompt(); const baseTemperature = input.temperature ?? 0; const userBody = `MODE: ${input.mode || "general"}\nRAW_PROMPT:\n${input.prompt}`; const sys = system; class CleanerNonJsonError extends Error { constructor(message = "Cleaner returned non-JSON") { super(message); this.name = "CleanerNonJsonError"; } } // Retry loop for content-level non-JSON responses const maxAttempts = Math.max(1, 1 + (config.contentMaxRetries ?? 0)); let lastErr: Error | undefined; for (let attempt = 1; attempt <= maxAttempts; attempt++) { // Escalate on retries: enforce temperature 0 and stricter system instructions const strictSuffix = attempt > 1 ? "\n\nSTRICT OUTPUT MODE: Respond with EXACTLY ONE JSON object and nothing else. No prose. No code fences. No prefix/suffix." : ""; const sysAttempt = sys + strictSuffix; const tempAttempt = attempt > 1 ? 0 : baseTemperature; const response = await chatCompletions( { model: config.model, temperature: tempAttempt, max_tokens: 600, messages: [ { role: "system", content: sysAttempt }, { role: "user", content: userBody }, ], }, { requestId: input.requestId }, ); const content = response.choices?.[0]?.message?.content ?? ""; const initial = redactSecrets(content); const redactedText = initial.text; try { const obj = extractFirstJsonObject(redactedText); const parsed = RetouchOutput.safeParse(obj); if (!parsed.success) { throw new Error("shape-error"); } const { value, redactions } = ensureNoSecretsInObject(parsed.data); const totalRedactions = initial.count + redactions; const result: RetouchOutputT = { ...value, redactions: totalRedactions > 0 ? Array(totalRedactions).fill("[REDACTED]") : value.redactions, }; logger.info("retouch.prompt", { elapsed_ms: Date.now() - start, input_len: input.prompt.length, preview: logger.preview(input.prompt), request_id: input.requestId, attempts: attempt, outcome: "ok", }); return result; } catch (e: any) { lastErr = new CleanerNonJsonError("Cleaner returned non-JSON"); if (attempt < maxAttempts) { const base = config.backoffMs ?? 250; const jitter = config.backoffJitter ?? 0.2; const exp = Math.pow(2, attempt - 1); const rand = 1 + (Math.random() * 2 - 1) * jitter; // 1 +/- jitter const delay = Math.max(0, Math.floor(base * exp * rand)); logger.warn("retouch.retry", { request_id: input.requestId, attempt, delay_ms: delay, reason: "non-json", }); await new Promise((r) => setTimeout(r, delay)); continue; } } } logger.info("retouch.prompt", { elapsed_ms: Date.now() - start, input_len: input.prompt.length, preview: logger.preview(input.prompt), request_id: input.requestId, attempts: maxAttempts, outcome: "error", reason: "non-json", }); throw lastErr ?? new CleanerNonJsonError("Cleaner returned non-JSON"); }
- src/shapes.ts:5-22 (schema)Zod schemas for input (RetouchInput) and output (RetouchOutput) validation used by the cleaner tool.export const RetouchInput = z.object({ prompt: z.string().min(1), mode: z.enum(["code", "general"]).optional(), temperature: z.number().min(0).max(2).optional(), requestId: z.string().uuid().optional(), }); export const RetouchOutput = z.object({ retouched: z.string().min(1), notes: z.array(z.string()).optional(), openQuestions: z.array(z.string()).optional(), risks: z.array(z.string()).optional(), redactions: z.array(z.literal("[REDACTED]")).optional(), }); export type HealthOutputT = z.infer<typeof HealthOutput>; export type RetouchInputT = z.infer<typeof RetouchInput>; export type RetouchOutputT = z.infer<typeof RetouchOutput>;
- src/tools.ts:11-36 (registration)Registration of the 'cleaner' tool in listTools(), including MCP-compatible description and JSON input schema.{ name: "cleaner", description: [ "Pre-reasoning prompt normalizer and PII redactor.", "Use when: you receive raw/free-form user text and need it cleaned before planning, tool selection, or code execution.", "Does: normalize tone, structure the ask, and redact secrets; preserves user intent.", "Safe: read-only, idempotent, no side effects (good default to run automatically).", "Input: { prompt, mode?, temperature? } — defaults mode='general', temperature=0.2; mode='code' only for code-related prompts.", "Output: JSON { retouched, notes?, openQuestions?, risks?, redactions? }.", "Keywords: clean, sanitize, normalize, redact, structure, preprocess, guardrails", ].join("\n"), inputSchema: { type: "object", properties: { prompt: { type: "string", description: "Raw user prompt" }, mode: { type: "string", enum: ["code", "general"], description: "Retouching mode; default 'general'. Use 'code' only for code-related prompts.", }, temperature: { type: "number", description: "Sampling temperature (0-2); default 0.2" }, }, required: ["prompt"], }, },
- src/tools.ts:92-105 (handler)Dispatch handler in callTool() that invokes the retouchPrompt for 'cleaner' and its aliases, with input/output parsing.case "cleaner": case "sanitize-text": case "normalize-prompt": { const parsed = RetouchInput.parse(args); const result = await retouchPrompt(parsed); const safe = RetouchOutput.parse(result); logger.info("retouch.prompt", { elapsed_ms: Date.now() - start, input_len: parsed.prompt.length, preview: logger.preview(parsed.prompt), request_id: parsed.requestId, }); return jsonContent(safe); }
- src/cleaner.ts:22-81 (helper)Helper function to robustly extract the first valid JSON object from LLM response text, handling fences and partial outputs.function extractFirstJsonObject(text: string): any { // Remove common code fences (```json ... ``` or ``` ... ```) const unfenced = text .replace(/```[a-zA-Z]*\n?/g, "") .replace(/```/g, "") .trim(); // Fast path: try parsing the whole string try { return JSON.parse(unfenced); } catch {} // Scan for the first balanced JSON object while ignoring braces inside strings const s = unfenced; let start = -1; let depth = 0; let inString = false; let escape = false; for (let i = 0; i < s.length; i++) { const ch = s[i]; if (inString) { if (escape) { escape = false; continue; } if (ch === "\\") { escape = true; continue; } if (ch === '"') { inString = false; } continue; } if (ch === '"') { inString = true; continue; } if (ch === "{") { if (depth === 0) start = i; depth++; continue; } if (ch === "}") { if (depth > 0) { depth--; if (depth === 0 && start !== -1) { const candidate = s.slice(start, i + 1); try { return JSON.parse(candidate); } catch { // keep scanning; there may be another valid object ahead start = -1; } } } } } throw new Error("Cleaner returned non-JSON"); }