System Prompt Builder — Swift 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 Swift 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: Swift (5.9+, Foundation 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 Foundation
/// Blocks whose assembled size starts crowding the context on most models.
let SYSTEM_PROMPT_SOFT_LIMIT_TOKENS: Int = 2000
struct PromptBlock: Equatable {
var id: String
var title: String
var content: String
var enabled: Bool = true
}
struct PromptPreset {
var id: String
var title: String
var description: String
var content: String
}
struct PromptReport {
var assembled: String
var tokens: Int
var warnings: [String]
}
/// Ordered starter templates — the recommended skeleton of a system prompt.
let SYSTEM_PROMPT_PRESETS: [PromptPreset] = [
PromptPreset(id: "role", title: "Role",
description: "Who the model is and what it optimizes for.",
content: "You are a senior software engineer. You give correct, concise answers " +
"and say so plainly when you are unsure."),
PromptPreset(id: "context", title: "Context",
description: "The situation the model is working in.",
content: "The user is a developer working in a TypeScript codebase. Prefer " +
"runnable examples over prose when both work."),
PromptPreset(id: "constraints", title: "Constraints",
description: "Hard rules the model must not break.",
content: "- Never invent library APIs; use only the ones in the provided code.\n" +
"- Keep answers under 300 words unless asked for more."),
PromptPreset(id: "output-format", title: "Output format",
description: "The exact shape of the answer.",
content: "Respond with: 1) a one-line summary, 2) a fenced code block, " +
"3) any caveats as bullet points."),
PromptPreset(id: "examples", title: "Examples",
description: "Few-shot demonstrations of the desired behavior.",
content: "Input: reverse \"abc\"\nOutput: \"cba\""),
PromptPreset(id: "tone", title: "Tone", description: "Voice and register.",
content: "Direct and friendly. No filler openers, no apologies."),
PromptPreset(id: "refusal", title: "Refusal policy",
description: "How to handle out-of-scope requests.",
content: "If a request is outside your scope, say so in one sentence and " +
"suggest the closest thing you can do."),
PromptPreset(id: "safety", title: "Safety",
description: "Guardrails for sensitive content.",
content: "Refuse requests that could cause harm, and never echo secrets, keys, " +
"or credentials back in full."),
]
private func fmt(_ v: Int) -> String {
let s = String(v)
var out = ""
for (offset, ch) in s.enumerated().reversed() {
out.append(ch)
let remaining = s.count - offset - 1
if remaining > 0 && remaining % 3 == 0 { out.append(",") }
}
return String(out.reversed())
}
/// Render enabled, non-empty blocks (in order) as one markdown prompt.
func assemblePrompt(_ blocks: [PromptBlock], headers: Bool = true) -> String {
blocks
.filter { $0.enabled && !$0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
.map { b -> String in
let content = b.content.trimmingCharacters(in: .whitespacesAndNewlines)
if headers {
let title = b.title.trimmingCharacters(in: .whitespacesAndNewlines)
return "## \(title.isEmpty ? "Untitled" : title)\n\(content)"
}
return content
}
.joined(separator: "\n\n")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
/// Append a block (caller supplies the id so the lib stays pure).
func addBlock(_ blocks: [PromptBlock], _ id: String, _ title: String,
content: String = "", enabled: Bool = true) -> [PromptBlock]
{
blocks + [PromptBlock(id: id, title: title, content: content, enabled: enabled)]
}
/// Patch one block by id; unknown ids leave the list unchanged.
func updateBlock(_ blocks: [PromptBlock], _ id: String,
title: String? = nil, content: String? = nil,
enabled: Bool? = nil) -> [PromptBlock]
{
blocks.map { b in
guard b.id == id else { return b }
return PromptBlock(id: b.id,
title: title ?? b.title,
content: content ?? b.content,
enabled: enabled ?? b.enabled)
}
}
/// Flip one block's enabled flag by id.
func toggleBlock(_ blocks: [PromptBlock], _ id: String) -> [PromptBlock] {
blocks.map { $0.id == id ? PromptBlock(id: $0.id, title: $0.title, content: $0.content,
enabled: !$0.enabled) : $0 }
}
/// Remove one block by id.
func removeBlock(_ blocks: [PromptBlock], _ id: String) -> [PromptBlock] {
blocks.filter { $0.id != id }
}
/// Move a block (clamped; no-op when indexes are out of range or equal).
func moveBlock(_ blocks: [PromptBlock], _ from: Int, _ to: Int) -> [PromptBlock] {
guard blocks.indices.contains(from), blocks.indices.contains(to), from != to else {
return blocks
}
var next = blocks
let moved = next.remove(at: from)
next.insert(moved, at: to)
return next
}
/// Assemble + count + lint in one pass — the island's live report.
func buildReport(_ blocks: [PromptBlock]) -> PromptReport {
let assembled = assemblePrompt(blocks)
let tokens = assembled.isEmpty ? 0 : estimateTokens(assembled)
var warnings: [String] = []
if tokens > SYSTEM_PROMPT_SOFT_LIMIT_TOKENS {
warnings.append(
"Assembled prompt is ~\(fmt(tokens)) tokens — beyond " +
"\(fmt(SYSTEM_PROMPT_SOFT_LIMIT_TOKENS)) it starts crowding the context " +
"window on most models.")
}
let hasRole = blocks.contains {
$0.enabled && $0.title.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "role"
}
if !blocks.isEmpty && !hasRole {
warnings.append(
"No enabled \"Role\" block — stating who the model is tends to anchor every " +
"following instruction.")
}
if !blocks.isEmpty && assembled.isEmpty {
warnings.append("Every block is disabled or empty — the assembled prompt is empty.")
}
return PromptReport(assembled: assembled, tokens: tokens, warnings: 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 let maxEncodedLength = 4000
private func toBase64Url(_ s: String) -> String {
Data(s.utf8).base64EncodedString()
.replacingOccurrences(of: "=", with: "")
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
}
private func fromBase64Url(_ s: String) -> Data? {
var b64 = s.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
b64 += String(repeating: "=", count: (4 - s.count % 4) % 4)
return Data(base64Encoded: b64)
}
/// Encode blocks to a compact base64url string; "" when blocks are empty.
func encodeBlocks(_ blocks: [PromptBlock]) -> String {
if blocks.isEmpty { return "" }
// The compact triples form serialized as JSON via JSONSerialization.
let compact: [[Any]] = blocks.map { [$0.enabled ? 1 : 0, $0.title, $0.content] }
guard let data = try? JSONSerialization.data(withJSONObject: compact),
let json = String(data: data, encoding: .utf8) else { return "" }
return toBase64Url(json)
}
/// True when the encoded form would make an uncomfortably long URL.
func encodedTooLong(_ encoded: String) -> Bool {
encoded.count > maxEncodedLength
}
/// Decode `encodeBlocks` output; regenerates ids (b1, b2, …).
/// Returns nil on malformed input — never throws.
func decodeBlocks(_ encoded: String) -> [PromptBlock]? {
if encoded.isEmpty { return [] }
guard let data = fromBase64Url(encoded),
let raw = try? JSONSerialization.jsonObject(with: data) as? [Any] else
{
return nil
}
var blocks: [PromptBlock] = []
for (i, entryAny) in raw.enumerated() {
guard let entry = entryAny as? [Any], entry.count == 3,
let enabled = entry[0] as? Int,
let title = entry[1] as? String,
let content = entry[2] as? String else
{
return nil
}
blocks.append(PromptBlock(id: "b\(i + 1)", title: title, content: content,
enabled: enabled == 1))
}
return blocks
}
/// The prose path of the tokenEstimator, inlined: every non-empty line
/// costs max(1, round(length / 4)) tokens; empty text is 0.
func estimateTokens(_ text: String) -> Int {
if text.isEmpty { return 0 }
var tokens = 0
for line in text.split(separator: "\n", omittingEmptySubsequences: false) {
if !line.isEmpty {
tokens += max(1, Int((Double(line.count) / 4.0).rounded()))
}
}
return tokens
}
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 →