Skip to content

System Prompt Builder — Java source

Assemble a system prompt from ordered blocks — role, context, constraints, output format — with a live token count, soft-limit warnings, and a shareable URL. 100% client-side.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// System Prompt Builder — assemble an ordered list of prompt blocks into a
// markdown-structured system prompt, with pure list operations, presets,
// warnings, and a compact URL codec for shareable state.
//
// Language: Java (17+, zero dependencies; java.util.Base64 + a tiny JSON
//           triple encoder keep it stdlib-only)
// Port of src/lib/systemPromptBuilder.ts (the canonical TypeScript
// implementation). Field names stay camelCase to match the TS surface.
// Tool page: https://dev.cosmolabs.org/tools/system-prompt-builder

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Locale;

public final class SystemPromptBuilder {

    /** Blocks whose assembled size starts crowding the context on most models. */
    public static final long SYSTEM_PROMPT_SOFT_LIMIT_TOKENS = 2000;

    public record PromptBlock(String id, String title, String content, boolean enabled) {}

    public record PromptPreset(String id, String title, String description, String content) {}

    public record PromptReport(String assembled, long tokens, List<String> warnings) {}

    /** Ordered starter templates — the recommended skeleton of a system prompt. */
    public static final List<PromptPreset> SYSTEM_PROMPT_PRESETS = List.of(
            new PromptPreset("role", "Role", "Who the model is and what it optimizes for.",
                    "You are a senior software engineer. You give correct, concise answers "
                            + "and say so plainly when you are unsure."),
            new PromptPreset("context", "Context", "The situation the model is working in.",
                    "The user is a developer working in a TypeScript codebase. Prefer "
                            + "runnable examples over prose when both work."),
            new PromptPreset("constraints", "Constraints", "Hard rules the model must not break.",
                    "- Never invent library APIs; use only the ones in the provided code.\n"
                            + "- Keep answers under 300 words unless asked for more."),
            new PromptPreset("output-format", "Output format", "The exact shape of the answer.",
                    "Respond with: 1) a one-line summary, 2) a fenced code block, "
                            + "3) any caveats as bullet points."),
            new PromptPreset("examples", "Examples",
                    "Few-shot demonstrations of the desired behavior.",
                    "Input: reverse \"abc\"\nOutput: \"cba\""),
            new PromptPreset("tone", "Tone", "Voice and register.",
                    "Direct and friendly. No filler openers, no apologies."),
            new PromptPreset("refusal", "Refusal policy",
                    "How to handle out-of-scope requests.",
                    "If a request is outside your scope, say so in one sentence and suggest "
                            + "the closest thing you can do."),
            new PromptPreset("safety", "Safety", "Guardrails for sensitive content.",
                    "Refuse requests that could cause harm, and never echo secrets, keys, "
                            + "or credentials back in full."));

    /** Render enabled, non-empty blocks (in order) as one markdown prompt. */
    public static String assemblePrompt(List<PromptBlock> blocks, boolean headers) {
        List<String> parts = new ArrayList<>();
        for (PromptBlock b : blocks) {
            String content = b.content() == null ? "" : b.content().trim();
            if (!b.enabled() || content.isEmpty()) continue;
            if (headers) {
                String title = b.title() == null || b.title().trim().isEmpty()
                        ? "Untitled" : b.title().trim();
                parts.add("## " + title + "\n" + content);
            } else {
                parts.add(content);
            }
        }
        return String.join("\n\n", parts).trim();
    }

    /** Append a block (caller supplies the id so the lib stays pure). */
    public static List<PromptBlock> addBlock(List<PromptBlock> blocks, String id,
            String title, String content, boolean enabled) {
        List<PromptBlock> next = new ArrayList<>(blocks);
        next.add(new PromptBlock(id, title, content, enabled));
        return next;
    }

    /** Patch one block by id; unknown ids leave the list unchanged. */
    public static List<PromptBlock> updateBlock(List<PromptBlock> blocks, String id,
            String title, String content, Boolean enabled) {
        List<PromptBlock> next = new ArrayList<>();
        for (PromptBlock b : blocks) {
            if (b.id().equals(id)) {
                next.add(new PromptBlock(b.id(),
                        title != null ? title : b.title(),
                        content != null ? content : b.content(),
                        enabled != null ? enabled : b.enabled()));
            } else {
                next.add(b);
            }
        }
        return next;
    }

    /** Flip one block's enabled flag by id. */
    public static List<PromptBlock> toggleBlock(List<PromptBlock> blocks, String id) {
        List<PromptBlock> next = new ArrayList<>();
        for (PromptBlock b : blocks) {
            next.add(b.id().equals(id)
                    ? new PromptBlock(b.id(), b.title(), b.content(), !b.enabled())
                    : b);
        }
        return next;
    }

    /** Remove one block by id. */
    public static List<PromptBlock> removeBlock(List<PromptBlock> blocks, String id) {
        List<PromptBlock> next = new ArrayList<>();
        for (PromptBlock b : blocks) {
            if (!b.id().equals(id)) next.add(b);
        }
        return next;
    }

    /** Move a block (clamped; no-op when indexes are out of range or equal). */
    public static List<PromptBlock> moveBlock(List<PromptBlock> blocks, int from, int to) {
        if (from < 0 || from >= blocks.size() || to < 0 || to >= blocks.size() || from == to) {
            return new ArrayList<>(blocks);
        }
        List<PromptBlock> next = new ArrayList<>(blocks);
        PromptBlock moved = next.remove(from);
        next.add(to, moved);
        return next;
    }

    /** Assemble + count + lint in one pass — the island's live report. */
    public static PromptReport buildReport(List<PromptBlock> blocks) {
        String assembled = assemblePrompt(blocks, true);
        long tokens = assembled.isEmpty() ? 0 : estimateTokens(assembled);
        List<String> warnings = new ArrayList<>();
        if (tokens > SYSTEM_PROMPT_SOFT_LIMIT_TOKENS) {
            warnings.add(String.format(Locale.US,
                    "Assembled prompt is ~%,d tokens — beyond %,d it starts crowding the "
                            + "context window on most models.",
                    tokens, SYSTEM_PROMPT_SOFT_LIMIT_TOKENS));
        }
        boolean hasRole = false;
        for (PromptBlock b : blocks) {
            if (b.enabled() && b.title() != null
                    && b.title().trim().toLowerCase(Locale.ROOT).equals("role")) {
                hasRole = true;
                break;
            }
        }
        if (!blocks.isEmpty() && !hasRole) {
            warnings.add("No enabled \"Role\" block — stating who the model is tends to "
                    + "anchor every following instruction.");
        }
        if (!blocks.isEmpty() && assembled.isEmpty()) {
            warnings.add("Every block is disabled or empty — the assembled prompt is empty.");
        }
        return new PromptReport(assembled, tokens, warnings);
    }

    // ---- shareable state codec (URL-safe, compact) -----------------------
    // Triples of [enabled(0/1), title, content] keep URLs far smaller than the
    // full object shape; ids are regenerated on decode (they are UI-local).

    private static final int MAX_ENCODED_LENGTH = 4000;

    private static String toBase64Url(String s) {
        return Base64.getUrlEncoder().withoutPadding()
                .encodeToString(s.getBytes(StandardCharsets.UTF_8));
    }

    private static String fromBase64Url(String s) {
        byte[] bytes = Base64.getUrlDecoder().decode(s);
        return new String(bytes, StandardCharsets.UTF_8);
    }

    /** Minimal JSON string literal escape for the compact codec. */
    private static String jsonEscape(String s) {
        StringBuilder out = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            switch (c) {
                case '"' -> out.append("\\\"");
                case '\\' -> out.append("\\\\");
                case '\n' -> out.append("\\n");
                case '\r' -> out.append("\\r");
                case '\t' -> out.append("\\t");
                default -> {
                    if (c < 0x20) {
                        out.append(String.format(Locale.ROOT, "\\u%04x", (int) c));
                    } else {
                        out.append(c);
                    }
                }
            }
        }
        return out.toString();
    }

    /** Decode a JSON string literal produced by {@link #jsonEscape}. */
    private static String jsonUnescape(String s) {
        StringBuilder out = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '\\' && i + 1 < s.length()) {
                char e = s.charAt(++i);
                switch (e) {
                    case 'n' -> out.append('\n');
                    case 'r' -> out.append('\r');
                    case 't' -> out.append('\t');
                    case 'u' -> {
                        if (i + 4 < s.length()) {
                            out.append((char) Integer.parseInt(s.substring(i + 1, i + 5), 16));
                            i += 4;
                        }
                    }
                    default -> out.append(e);
                }
            } else {
                out.append(c);
            }
        }
        return out.toString();
    }

    /** Encode blocks to a compact base64url string; "" when blocks are empty. */
    public static String encodeBlocks(List<PromptBlock> blocks) {
        if (blocks.isEmpty()) return "";
        StringBuilder json = new StringBuilder("[");
        for (int i = 0; i < blocks.size(); i++) {
            PromptBlock b = blocks.get(i);
            if (i > 0) json.append(',');
            json.append('[').append(b.enabled() ? 1 : 0)
                    .append(",\"").append(jsonEscape(b.title()))
                    .append("\",\"").append(jsonEscape(b.content()))
                    .append("\"]");
        }
        json.append(']');
        return toBase64Url(json.toString());
    }

    /** True when the encoded form would make an uncomfortably long URL. */
    public static boolean encodedTooLong(String encoded) {
        return encoded.length() > MAX_ENCODED_LENGTH;
    }

    /**
     * Decode {@link #encodeBlocks} output; regenerates ids (b1, b2, …).
     * Returns null on malformed input — never throws.
     */
    public static List<PromptBlock> decodeBlocks(String encoded) {
        if (encoded.isEmpty()) return new ArrayList<>();
        try {
            String json = fromBase64Url(encoded);
            if (json.charAt(0) != '[' || json.charAt(json.length() - 1) != ']') return null;
            String body = json.substring(1, json.length() - 1);
            List<PromptBlock> blocks = new ArrayList<>();
            int i = 0;
            while (i < body.length()) {
                if (body.charAt(i) == ',') i++;
                if (i >= body.length() || body.charAt(i) != '[') return null;
                i++; // consume '['
                if (i >= body.length() || !Character.isDigit(body.charAt(i))) return null;
                int enabledStart = i;
                while (Character.isDigit(body.charAt(i))) i++;
                int enabled = Integer.parseInt(body.substring(enabledStart, i));
                String[] strings = new String[2];
                for (int s = 0; s < 2; s++) {
                    if (body.charAt(i) != ',') return null;
                    i++;
                    if (body.charAt(i) != '"') return null;
                    i++;
                    StringBuilder lit = new StringBuilder();
                    while (body.charAt(i) != '"') {
                        if (body.charAt(i) == '\\') {
                            lit.append(body.charAt(i)).append(body.charAt(i + 1));
                            i += 2;
                        } else {
                            lit.append(body.charAt(i));
                            i++;
                        }
                    }
                    i++; // closing quote
                    strings[s] = jsonUnescape(lit.toString());
                }
                if (i >= body.length() || body.charAt(i) != ']') return null;
                i++; // consume ']'
                blocks.add(new PromptBlock(
                        "b" + (blocks.size() + 1), strings[0], strings[1], enabled == 1));
            }
            return blocks;
        } catch (RuntimeException e) {
            return null;
        }
    }

    /**
     * The prose path of the tokenEstimator, inlined: every non-empty line
     * costs max(1, round(length / 4)) tokens; empty text is 0.
     */
    private static long estimateTokens(String text) {
        if (text.isEmpty()) return 0;
        long tokens = 0;
        for (String line : text.split("\n", -1)) {
            if (!line.isEmpty()) {
                tokens += Math.max(1, Math.round(line.length() / 4.0));
            }
        }
        return tokens;
    }

    private SystemPromptBuilder() {}
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →