System Prompt Builder — Zig 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 Zig 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: Zig (0.13+, stdlib only — std.json + std.base64)
// Port of src/lib/systemPromptBuilder.ts (the canonical TypeScript
// implementation). All returned strings/slices are allocated from the
// caller's allocator.
//
// Tool page: https://dev.cosmolabs.org/tools/system-prompt-builder
const std = @import("std");
/// Blocks whose assembled size starts crowding the context on most models.
pub const SYSTEM_PROMPT_SOFT_LIMIT_TOKENS: i64 = 2000;
pub const Block = struct {
id: []const u8,
title: []const u8,
content: []const u8,
enabled: bool = true,
};
pub const Preset = struct {
id: []const u8,
title: []const u8,
description: []const u8,
content: []const u8,
};
pub const Report = struct {
assembled: []const u8, // allocated
tokens: i64,
warnings: [][]const u8, // each allocated
pub fn free(self: *Report, alloc: std.mem.Allocator) void {
alloc.free(self.assembled);
for (self.warnings) |w| alloc.free(w);
alloc.free(self.warnings);
}
};
/// Ordered starter templates — the recommended skeleton of a system prompt.
pub const SYSTEM_PROMPT_PRESETS = [_]Preset{
.{ .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." },
.{ .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." },
.{ .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." },
.{ .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." },
.{ .id = "examples", .title = "Examples",
.description = "Few-shot demonstrations of the desired behavior.",
.content = "Input: reverse \"abc\"\nOutput: \"cba\"" },
.{ .id = "tone", .title = "Tone",
.description = "Voice and register.",
.content = "Direct and friendly. No filler openers, no apologies." },
.{ .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." },
.{ .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." },
};
/// The prose path of the tokenEstimator, inlined: every non-empty line
/// costs max(1, round(length / 4)) tokens; empty text is 0.
pub fn estimateTokens(text: []const u8) i64 {
if (text.len == 0) return 0;
var tokens: i64 = 0;
var it = std.mem.splitScalar(u8, text, '\n');
while (it.next()) |line| {
if (line.len > 0) {
const per: i64 = @intFromFloat(@as(f64, @floatFromInt(line.len)) / 4.0 + 0.5);
tokens += @max(1, per);
}
}
return tokens;
}
fn trim(s: []const u8) []const u8 {
return std.mem.trim(u8, s, " \t\r\n");
}
/// Render enabled, non-empty blocks (in order) as one markdown prompt.
/// Caller owns the returned string.
pub fn assemblePrompt(alloc: std.mem.Allocator, blocks: []const Block, headers: bool) ![]const u8 {
var out: std.ArrayList(u8) = .init(alloc);
errdefer out.deinit();
var first = true;
for (blocks) |b| {
const content = trim(b.content);
if (!b.enabled or content.len == 0) continue;
if (!first) try out.appendSlice("\n\n");
if (headers) {
const title = trim(b.title);
try out.writer().print("## {s}\n{s}", .{
if (title.len == 0) "Untitled" else title, content,
});
} else {
try out.appendSlice(content);
}
first = false;
}
return out.toOwnedSlice();
}
/// Append a block (caller supplies the id so the lib stays pure).
/// Caller owns the returned slice.
pub fn addBlock(alloc: std.mem.Allocator, blocks: []const Block, id: []const u8,
title: []const u8, content: []const u8, enabled: bool) ![]Block
{
const next = try alloc.alloc(Block, blocks.len + 1);
@memcpy(next[0..blocks.len], blocks);
next[blocks.len] = .{
.id = try alloc.dupe(u8, id),
.title = try alloc.dupe(u8, title),
.content = try alloc.dupe(u8, content),
.enabled = enabled,
};
return next;
}
/// Move a block (clamped; no-op when indexes are out of range or equal).
/// Caller owns the returned slice (ids/titles/contents are duplicated).
pub fn moveBlock(alloc: std.mem.Allocator, blocks: []const Block, from: usize, to: usize) ![]Block {
if (from >= blocks.len or to >= blocks.len or from == to) {
return dupeBlocks(alloc, blocks);
}
const next = try alloc.alloc(Block, blocks.len);
@memcpy(next, blocks);
const moved = next[from];
if (to > from) {
var i = from;
while (i < to) : (i += 1) next[i] = next[i + 1];
} else {
var i = from;
while (i > to) : (i -= 1) next[i] = next[i - 1];
}
next[to] = moved;
return next;
}
fn dupeBlocks(alloc: std.mem.Allocator, blocks: []const Block) ![]Block {
const out = try alloc.alloc(Block, blocks.len);
for (blocks, 0..) |b, i| {
out[i] = .{
.id = try alloc.dupe(u8, b.id),
.title = try alloc.dupe(u8, b.title),
.content = try alloc.dupe(u8, b.content),
.enabled = b.enabled,
};
}
return out;
}
/// Assemble + count + lint in one pass — the island's live report.
pub fn buildReport(alloc: std.mem.Allocator, blocks: []const Block) !Report {
const assembled = try assemblePrompt(alloc, blocks, true);
errdefer alloc.free(assembled);
const tokens: i64 = if (assembled.len > 0) estimateTokens(assembled) else 0;
var warnings: std.ArrayList([]const u8) = .init(alloc);
errdefer {
for (warnings.items) |w| alloc.free(w);
warnings.deinit();
}
if (tokens > SYSTEM_PROMPT_SOFT_LIMIT_TOKENS) {
try warnings.append(try std.fmt.allocPrint(alloc,
"Assembled prompt is ~{d} tokens — beyond {d} it starts crowding the context window on most models.",
.{ tokens, SYSTEM_PROMPT_SOFT_LIMIT_TOKENS }));
}
var has_role = false;
for (blocks) |b| {
if (b.enabled and std.ascii.eqlIgnoreCase(trim(b.title), "role")) {
has_role = true;
break;
}
}
if (blocks.len > 0 and !has_role) {
try warnings.append(try alloc.dupe(u8,
"No enabled \"Role\" block — stating who the model is tends to anchor every following instruction."));
}
if (blocks.len > 0 and assembled.len == 0) {
try warnings.append(try alloc.dupe(u8,
"Every block is disabled or empty — the assembled prompt is empty."));
}
return .{
.assembled = assembled,
.tokens = tokens,
.warnings = try warnings.toOwnedSlice(),
};
}
// ---- 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).
pub const MAX_ENCODED_LENGTH: usize = 4000;
/// JSON string escaping for the compact codec.
const enc = struct {
fn writeString(w: anytype, s: []const u8) !void {
try w.writeByte('"');
for (s) |c| {
switch (c) {
'"' => try w.writeAll("\\\""),
'\\' => try w.writeAll("\\\\"),
'\n' => try w.writeAll("\\n"),
'\r' => try w.writeAll("\\r"),
'\t' => try w.writeAll("\\t"),
else => {
if (c < 0x20) {
try w.print("\\u{x:0>4}", .{c});
} else {
try w.writeByte(c);
}
},
}
}
try w.writeByte('"');
}
};
/// Encode blocks to a compact base64url string; "" when blocks are empty.
/// Caller owns the returned string.
pub fn encodeBlocks(alloc: std.mem.Allocator, blocks: []const Block) ![]const u8 {
if (blocks.len == 0) return try alloc.dupe(u8, "");
var json: std.ArrayList(u8) = .init(alloc);
defer json.deinit();
try json.append('[');
for (blocks, 0..) |b, i| {
if (i > 0) try json.append(',');
try json.writer().print("[{d},", .{@as(u8, if (b.enabled) 1 else 0)});
try enc.writeString(json.writer(), b.title);
try json.append(',');
try enc.writeString(json.writer(), b.content);
try json.append(']');
}
try json.append(']');
const enc64 = std.base64.url_safe_no_pad.Encoder;
const size = enc64.calcSize(json.items.len);
const out = try alloc.alloc(u8, size);
_ = enc64.encode(out, json.items);
return out;
}
/// True when the encoded form would make an uncomfortably long URL.
pub fn encodedTooLong(encoded: []const u8) bool {
return encoded.len > MAX_ENCODED_LENGTH;
}
/// Decode `encodeBlocksReal` output; regenerates ids ("b1", "b2", …).
/// Returns null on malformed input — never throws. Caller owns the result
/// (free each block's fields + the slice with freeBlocks).
pub fn decodeBlocks(alloc: std.mem.Allocator, encoded: []const u8) !?[]Block {
if (encoded.len == 0) return try alloc.alloc(Block, 0);
const dec64 = std.base64.url_safe_no_pad.Decoder;
const size = dec64.calcSizeForSlice(encoded) catch return null;
const json_buf = try alloc.alloc(u8, size);
defer alloc.free(json_buf);
dec64.decode(json_buf, encoded) catch return null;
const parsed = std.json.parseFromSlice(std.json.Value, alloc, json_buf, .{}) catch return null;
defer parsed.deinit();
if (parsed.value != .array) return null;
var blocks: std.ArrayList(Block) = .init(alloc);
errdefer {
for (blocks.items) |b| freeBlock(alloc, b);
blocks.deinit();
}
for (parsed.value.array.items, 0..) |entry, i| {
if (entry != .array or entry.array.items.len != 3) return null;
const enabled_v = entry.array.items[0];
const title_v = entry.array.items[1];
const content_v = entry.array.items[2];
if (enabled_v != .integer or title_v != .string or content_v != .string) return null;
try blocks.append(.{
.id = try std.fmt.allocPrint(alloc, "b{d}", .{i + 1}),
.title = try alloc.dupe(u8, title_v.string),
.content = try alloc.dupe(u8, content_v.string),
.enabled = enabled_v.integer == 1,
});
}
return try blocks.toOwnedSlice();
}
fn freeBlock(alloc: std.mem.Allocator, b: Block) void {
alloc.free(b.id);
alloc.free(b.title);
alloc.free(b.content);
}
/// Free a decoded block list.
pub fn freeBlocks(alloc: std.mem.Allocator, blocks: []Block) void {
for (blocks) |b| freeBlock(alloc, b);
alloc.free(blocks);
}
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 →