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 →