Skip to content

System Prompt Builder — Kotlin 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 Kotlin 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: Kotlin (JVM 17+, zero dependencies)
// 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.util.Base64
import kotlin.math.max
import kotlin.math.roundToInt

/** Blocks whose assembled size starts crowding the context on most models. */
const val SYSTEM_PROMPT_SOFT_LIMIT_TOKENS: Long = 2000L

data class PromptBlock(
    val id: String,
    val title: String,
    val content: String,
    val enabled: Boolean = true,
)

data class PromptPreset(
    val id: String,
    val title: String,
    val description: String,
    val content: String,
)

data class PromptReport(
    val assembled: String,
    val tokens: Long,
    val warnings: List<String>,
)

/** Ordered starter templates — the recommended skeleton of a system prompt. */
val SYSTEM_PROMPT_PRESETS: List<PromptPreset> = listOf(
    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.",
    ),
    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.",
    ),
    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.",
    ),
    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.",
    ),
    PromptPreset(
        "examples", "Examples", "Few-shot demonstrations of the desired behavior.",
        "Input: reverse \"abc\"\nOutput: \"cba\"",
    ),
    PromptPreset(
        "tone", "Tone", "Voice and register.",
        "Direct and friendly. No filler openers, no apologies.",
    ),
    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.",
    ),
    PromptPreset(
        "safety", "Safety", "Guardrails for sensitive content.",
        "Refuse requests that could cause harm, and never echo secrets, keys, or " +
            "credentials back in full.",
    ),
)

private fun fmt(v: Long): String = String.format("%,d", v)

/** Render enabled, non-empty blocks (in order) as one markdown prompt. */
fun assemblePrompt(blocks: List<PromptBlock>, headers: Boolean = true): String =
    blocks
        .filter { it.enabled && it.content.trim().isNotEmpty() }
        .joinToString("\n\n") { b ->
            if (headers) {
                "## ${b.title.trim().ifEmpty { "Untitled" }}\n${b.content.trim()}"
            } else {
                b.content.trim()
            }
        }
        .trim()

/** Append a block (caller supplies the id so the lib stays pure). */
fun addBlock(
    blocks: List<PromptBlock>, id: String, title: String,
    content: String = "", enabled: Boolean = true,
): List<PromptBlock> = blocks + PromptBlock(id, title, content, enabled)

/** Patch one block by id; unknown ids leave the list unchanged. */
fun updateBlock(
    blocks: List<PromptBlock>, id: String,
    title: String? = null, content: String? = null, enabled: Boolean? = null,
): List<PromptBlock> = blocks.map { b ->
    if (b.id == id) b.copy(
        title = title ?: b.title,
        content = content ?: b.content,
        enabled = enabled ?: b.enabled,
    ) else b
}

/** Flip one block's enabled flag by id. */
fun toggleBlock(blocks: List<PromptBlock>, id: String): List<PromptBlock> =
    blocks.map { if (it.id == id) it.copy(enabled = !it.enabled) else it }

/** Remove one block by id. */
fun removeBlock(blocks: List<PromptBlock>, id: String): List<PromptBlock> =
    blocks.filter { it.id != id }

/** Move a block (clamped; no-op when indexes are out of range or equal). */
fun moveBlock(blocks: List<PromptBlock>, from: Int, to: Int): List<PromptBlock> {
    if (from !in blocks.indices || to !in blocks.indices || from == to) {
        return blocks.toList()
    }
    val next = blocks.toMutableList()
    val moved = next.removeAt(from)
    next.add(to, moved)
    return next
}

/** Assemble + count + lint in one pass — the island's live report. */
fun buildReport(blocks: List<PromptBlock>): PromptReport {
    val assembled = assemblePrompt(blocks)
    val tokens = if (assembled.isEmpty()) 0L else estimateTokens(assembled)
    val warnings = mutableListOf<String>()
    if (tokens > SYSTEM_PROMPT_SOFT_LIMIT_TOKENS) {
        warnings.add(
            "Assembled prompt is ~${fmt(tokens)} tokens — beyond " +
                "${fmt(SYSTEM_PROMPT_SOFT_LIMIT_TOKENS)} it starts crowding the context " +
                "window on most models."
        )
    }
    val hasRole = blocks.any {
        it.enabled && it.title.trim().lowercase() == "role"
    }
    if (blocks.isNotEmpty() && !hasRole) {
        warnings.add(
            "No enabled \"Role\" block — stating who the model is tends to anchor every " +
                "following instruction."
        )
    }
    if (blocks.isNotEmpty() && assembled.isEmpty()) {
        warnings.add("Every block is disabled or empty — the assembled prompt is empty.")
    }
    return 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 const val MAX_ENCODED_LENGTH = 4000

private fun toBase64Url(s: String): String =
    Base64.getUrlEncoder().withoutPadding().encodeToString(s.toByteArray())

private fun fromBase64Url(s: String): String =
    String(Base64.getUrlDecoder().decode(s))

/** Minimal JSON string literal escape for the compact codec. */
private fun jsonEscape(s: String): String {
    val out = StringBuilder()
    for (c in s) {
        when (c) {
            '"' -> out.append("\\\"")
            '\\' -> out.append("\\\\")
            '\n' -> out.append("\\n")
            '\r' -> out.append("\\r")
            '\t' -> out.append("\\t")
            else -> if (c.code < 0x20) {
                out.append("\\u%04x".format(c.code))
            } else {
                out.append(c)
            }
        }
    }
    return out.toString()
}

private fun jsonUnescape(s: String): String {
    val out = StringBuilder()
    var i = 0
    while (i < s.length) {
        val c = s[i]
        if (c == '\\' && i + 1 < s.length) {
            when (val e = s[++i]) {
                'n' -> out.append('\n')
                'r' -> out.append('\r')
                't' -> out.append('\t')
                'u' -> if (i + 4 < s.length) {
                    out.append(s.substring(i + 1, i + 5).toInt(16).toChar())
                    i += 4
                }
                else -> out.append(e)
            }
        } else {
            out.append(c)
        }
        i++
    }
    return out.toString()
}

/** Encode blocks to a compact base64url string; "" when blocks are empty. */
fun encodeBlocks(blocks: List<PromptBlock>): String {
    if (blocks.isEmpty()) return ""
    val json = buildString {
        append('[')
        blocks.forEachIndexed { i, b ->
            if (i > 0) append(',')
            append('[').append(if (b.enabled) 1 else 0)
            append(",\"").append(jsonEscape(b.title))
            append("\",\"").append(jsonEscape(b.content)).append("\"]")
        }
        append(']')
    }
    return toBase64Url(json)
}

/** True when the encoded form would make an uncomfortably long URL. */
fun encodedTooLong(encoded: String): Boolean = encoded.length > MAX_ENCODED_LENGTH

/**
 * Decode [encodeBlocks] output; regenerates ids (b1, b2, …).
 * Returns null on malformed input — never throws.
 */
fun decodeBlocks(encoded: String): List<PromptBlock>? {
    if (encoded.isEmpty()) return emptyList()
    return try {
        val json = fromBase64Url(encoded)
        if (!json.startsWith("[") || !json.endsWith("]")) return null
        val body = json.substring(1, json.length - 1)
        val blocks = mutableListOf<PromptBlock>()
        var i = 0
        while (i < body.length) {
            if (body[i] == ',') i++
            if (i >= body.length || body[i] != '[') return null
            i++
            if (i >= body.length || !body[i].isDigit()) return null
            val enabledStart = i
            while (i < body.length && body[i].isDigit()) i++
            val enabled = body.substring(enabledStart, i).toInt()
            val strings = arrayOfNulls<String>(2)
            for (s in 0 until 2) {
                if (body[i] != ',') return null
                i++
                if (body[i] != '"') return null
                i++
                val lit = StringBuilder()
                while (body[i] != '"') {
                    if (body[i] == '\\') {
                        lit.append(body[i]).append(body[i + 1])
                        i += 2
                    } else {
                        lit.append(body[i])
                        i++
                    }
                }
                i++
                strings[s] = jsonUnescape(lit.toString())
            }
            if (i >= body.length || body[i] != ']') return null
            i++
            blocks.add(
                PromptBlock(
                    id = "b${blocks.size + 1}",
                    title = strings[0]!!,
                    content = strings[1]!!,
                    enabled = enabled == 1,
                )
            )
        }
        blocks
    } catch (e: RuntimeException) {
        null
    }
}

/**
 * The prose path of the tokenEstimator, inlined: every non-empty line costs
 * max(1, round(length / 4)) tokens; empty text is 0.
 */
private fun estimateTokens(text: String): Long {
    if (text.isEmpty()) return 0
    return text.split('\n').sumOf { line ->
        if (line.isEmpty()) 0L else max(1L, Math.round(line.length / 4.0))
    }
}

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 →