vibelang is a Python-shaped interpreted language where user-defined function bodies are plain language. The interpreter is written in Go, and every function call is executed by a local or remote LLM running through Ollama, llama.cpp, or an OpenAI-compatible endpoint.
- Uses indentation-sensitive, Python-like syntax for variables, expressions, loops, and conditionals.
- Treats every
defbody as natural-language instructions instead of imperative code. - Supports AI macros with
macrodefinitions and@macro(...)expansion syntax, so the model can synthesize real vibelang expressions on the fly. - Supports module loading with
import "./module.vibe" as moduleandfrom "./module.vibe" import helper. - Supports Python-style default parameter values and keyword arguments for user-defined functions and builtins.
- Supports Python-style unpacking targets in assignments and
forloops. - Supports inline
* promptexpressions in assignments, conditions, loops, and standalone statements. - Supports leading AI directives such as
@temperature,@max_tokens,@max_steps,@cache,@system,@tools, and@deny_toolsinside function and macro bodies. - Evaluates
${...}prompt placeholders as real vibelang expressions, including indexing and prompt-safe builtins such aslen,basename, orjoin_path. - Supports Python-style list and dict comprehensions with optional trailing
iffilters. - Supports structural
match/casebranching with wildcard, list, dict, and capture patterns plus optionalifguards. - Supports Python-style
withblocks backed by native managed resources such as temporary files, temporary directories, mutex guards, and OpenTelemetry span scopes. - Supports structured AI return types such as
dict{city: string, alerts: optional[list[string]]}andtuple[string, int], and turns them into tighter JSON schemas for model backends. - Constrains helper calls with per-helper JSON schemas, so models see the exact argument names, required fields, and types for each callable tool.
- Supports deterministic
try/except/finallyblocks for builtin, tool, and model-call failures. - Supports Python-style
assertstatements for deterministic guards and self-checking programs. - Supports block-scoped
deferexpressions for LIFO cleanup on normal exit,break,continue, and errors. - Supports Python-like member access for imported modules and dict-shaped values, so
shared.helper()works naturally. - Supports Python-style negative indexing and slicing for lists and strings, plus operand-returning
and/orshort-circuit behavior. - Lets AI functions call other AI functions through a strict JSON tool-call loop, supports explicit JSON
call_manybatches, and also understands provider-nativetool_callsresponses from Ollama,llama.cpp, and OpenAI-compatible backends, including multi-call batches. - Rejects direct and indirect recursive AI helper re-entry before it spirals into repeated depth exhaustion, feeds the rejection back into the next model step, and fails fast if the model keeps retrying a rejected helper.
- Adds opt-in AI result caching, CSV/YAML/TOML helpers, first-class sets, richer dict and list helpers, Python-inspired collection helpers such as
all,any,reversed,flatten, andbatched, Markdown rendering, route construction, time parsing and formatting, UUID generation, numeric reducers, structured logging, and OpenTelemetry trace export. - Captures surrounding non-function values by value when an AI function is defined, so later mutations do not silently change prompt inputs.
- Exposes a broader standard library for AI execution, including filesystem, JSON, YAML, path, string, cookies, environment, globbing, HTTP, WebSocket and TCP clients, TCP listeners, time, math, local process helpers, async tasks, channels, channel selection, mutexes, wait groups, route matching, and runtime metrics.
- Lets one AI body route itself to a different backend with
@provider,@model,@endpoint,@api_key_env, and@timeout_ms, so local Gemma can coexist with remote OpenAI-compatible calls in one program. - Starts AI-backed HTTP servers, including ordered route tables, WebSocket upgrades, and Server-Sent Event streaming backed by native channel handles.
- Serves deterministic static frontend assets, including HTML, JS, CSS, JSON, SVG, and
.wasm, with correct HTTP content types throughhttp_static_responseandmime_type. - Exposes deterministic tool introspection so programs can inspect the live helper catalog through
tool_catalogandtool_describe. - Resolves modules from relative paths,
VIBE_PATH, working-directorystd/modules, direct URLs, andgithub.com/owner/repo/path@refimports. - Runs against local or remote model servers, with first-class support for Ollama,
llama.cpp, OpenAI, Groq, and other OpenAI-compatible gateways. - Sends chat-style structured JSON requests to local backends, which works better with modern Gemma 4 model servers.
- Caches parsed prompt templates so repeated
${...}interpolation work does not keep reparsing the same expressions. - Ships standard-library modules written in vibelang itself, including
std/web,std/react,std/config,std/telemetry,std/runtime, andstd/ai, withstd/webable to render wasm-oriented HTML shells that expose initial state and optional host model endpoints,std/reactable to generate React-like shells over the same host-model pattern, andstd/configable to summarize TOML-driven configuration with AI.
Build the interpreter:
go build -o bin/vibelang ./cmd/vibelangRun the included example with Ollama:
ollama serve
ollama pull gemma4
./bin/vibelang --provider ollama --model gemma4 examples/hello.vibeFor smaller local runs, Ollama also exposes lighter Gemma 4 tags such as gemma4:e4b.
Run the same program with llama.cpp:
llama-server -m /models/gemma4.gguf --port 8080
./bin/vibelang --provider llamacpp --endpoint http://127.0.0.1:8080 --model gemma4 examples/hello.vibeIf your local model tag or GGUF filename uses a different name, pass that exact value with --model.
Run against a remote OpenAI-compatible endpoint:
export OPENAI_API_KEY=...
./bin/vibelang --provider openai --model gpt-4.1-mini examples/hello.vibeexport GROQ_API_KEY=...
./bin/vibelang --provider groq --model openai/gpt-oss-20b examples/hello.vibeValidate a program without hitting the model:
./bin/vibelang --check examples/modules/main.vibePrint the interpreter version:
./bin/vibelang --versiondef summarize_weather(city: string, tone: string = "crisp") -> string:
Write one ${tone} sentence about the weather in ${city}.
city = "Berlin"
forecast = summarize_weather(city=city)
print(forecast)Inline prompts work anywhere a full expression makes sense in statement position:
workspace = join_path([cwd(), "tmp"])
make_dir(workspace)
path = join_path([workspace, "pi.txt"])
digits = * return the first 5 digits of pi as a string without explanation.
if * check whether ${path} already exists:
* delete the file at ${path}.
else:
* write ${digits} to the file at ${path}.Prompt interpolation is expression-aware, not just name-aware:
def explain_file(path: string, digits: string, tone: string = "matter-of-fact") -> string:
Write one ${tone} sentence about ${basename(path)} inside ${dirname(path)}.
Mention that ${digits} has ${len(digits)} characters.AI bodies can also declare execution controls up front:
def slugify(title: string) -> string:
@temperature 0
@max_steps 4
@cache true
@system You are a literal slugging assistant. Return only the slug text.
@tools lower, trim, replace, regex_replace
Convert ${title} into a lowercase URL slug.
Replace whitespace runs with "-".
Remove punctuation and collapse repeated "-" runs.Bodies can also pin themselves to a different model route when one program needs both local and remote execution:
def summarize_release(changes: list[string]) -> string:
@provider openai-compatible
@endpoint https://models.example.com/v1
@model hosted-gemma
@api_key_env VIBE_REMOTE_API_KEY
@timeout_ms 10000
Summarize ${json_pretty(changes)} in one crisp paragraph.Slices are first-class expressions:
digits = "31415926535"
items = ["alpha", "beta", "gamma", "delta"]
print(digits[:5])
print(digits[-3:])
print(items[1:3])
print(items[::-1])Comprehensions work the way Python users expect:
names = [upper(name) for name in ["ada", "grace", "linus"] if "a" in name]
lengths = {name: len(name) for name in names if len(name) > 3}
print(json(names))
print(json(lengths))Python-shaped collection helpers keep deterministic reshaping out of the model loop:
values = [1, 2, 3, 4, 5]
groups = batched(values, 2)
print(all([true, 1, "Ada"]))
print(any([false, "", "vibe"]))
print(json(reversed(values)))
print(json(flatten(groups)))
print(json(dict_delete({"name": "Ada", "role": "builder"}, "role")))
print(json(set_values(set_symmetric_difference(set([1, 2, 3]), set([3, 4])))))Unpacking works in plain assignments and loop headers:
first, second = ["Ada", "Lovelace"]
print(first)
print(second)
for index, label in zip([1, 2, 3], ["a", "b", "c"]):
print(index)
print(label)Assertions make deterministic invariants explicit:
snapshot = runtime_metrics()
assert snapshot["go.goroutine.count"] >= 1, "expected at least one goroutine"with blocks make temporary resources and scope-based cleanup explicit:
with temp_dir(prefix="vibelang-demo-") as workspace:
note = join_path([workspace, "note.txt"])
write_file(note, "hello")
print(file_exists(note))
print(file_exists(workspace))Pattern matching lets deterministic code branch on data shape before handing the rest to AI:
packet = {"type": "message", "payload": ["alpha", "beta"], "meta": {"city": "Berlin"}}
match packet:
case {"type": "ping"}:
print("pong")
case {"type": "message", "payload": [head, tail]} if head == tail:
print("duplicate payload")
case {"type": "message", "payload": [head, tail], "meta": {"city": city}}:
print(head)
print(tail)
print(city)
case _:
print("fallback")AI macros expand into real expressions before evaluation:
macro even_numbers(limit: int) -> list[int]:
Return one valid vibelang expression that builds the even numbers below ${limit} * 2.
Prefer using range with explicit named arguments.
numbers = @even_numbers(5)
print(numbers)Structured outputs can stay Python-shaped while still giving the model a precise target:
def describe_weather(city: string) -> dict{city: string, summary: string, alerts: optional[list[string]], stats: dict{temp_c: int, wind_kph: int}, focus: tuple[string, int]}:
Return a compact weather object for ${city}.
print(json_pretty(describe_weather("Berlin")))Modules are ordinary .vibe files:
# shared.vibe
prefix = "Dr."
def format_name(name: string) -> string:
Return exactly: ${prefix} ${name}# main.vibe
from "./shared.vibe" import prefix, format_name
import "./shared.vibe" as shared
print(prefix)
print(format_name("Ada"))
print(shared.format_name("Grace"))Concurrent work uses native Go-backed primitives:
ch = channel(1)
channel_send(ch, "tasks queued")
wg = wait_group()
wait_group_add(wg, 2)
first = spawn(str, args=[42], wait_group=wg)
second = spawn(join, args=[["vibe", "lang"], "-"], wait_group=wg)
notice = channel_recv(ch)
wait_group_wait(wg)
print(notice["value"])
print(await_task(first))
print(await_task(second))Channel selection adds a more Go-like coordination primitive:
fast = channel(1)
slow = channel(1)
channel_send(slow, "background")
packet = channel_select([fast, slow], timeout_ms=10)
print(packet["channel"] == slow)
print(packet["value"])Programs can inspect the helper surface deterministically:
json_tools = tool_catalog(prefix="json_")
print(json_tools[0]["name"])
print(tool_describe("http_request")["params"][0]["name"])Explicit AI caching is useful for expensive deterministic helpers:
def normalize_city(city: string) -> string:
@temperature 0
@cache true
Return ${city} in uppercase letters.
print(normalize_city("berlin"))
print(normalize_city("berlin"))
print(cache_stats()["entries"])HTTP handlers can also be AI-backed:
import "std/web" as web
def handle(request: dict) -> dict:
Call web.render_app_shell with the title "vibelang demo", the route ${request["path"]}, and initial state {"path": request["path"]}.
Return a dict with html set to that app shell.
server = http_serve("127.0.0.1:0", handle)
response = http_request("http://" + server["address"] + "/hello")
print(response["status"])
http_server_stop(server["handle"])Static frontend bundles can stay deterministic and still plug into the same HTTP surface:
site = join_path([cwd(), "site"])
make_dir(join_path([site, "pkg"]))
write_file(join_path([site, "index.html"]), "<h1>vibelang</h1>")
write_file(join_path([site, "pkg", "app.wasm"]), "\u0000asm")
home = http_static_response(site, {"path": "/"}, cache_control="public, max-age=60")
wasm = http_static_response(site, {"path": "/pkg/app.wasm"})
print(home["headers"]["Content-Type"])
print(wasm["headers"]["Content-Type"])std/web can also render wasm-first shells for AI-backed routes:
import "std/web" as web
def handle(request: dict) -> dict:
Call web.respond_wasm_shell with the title "vibelang wasm demo", the route ${request["path"]}, state {"path": request["path"]}, brief "A dashboard shell that boots a wasm bundle.", wasm_path "/pkg/app.wasm", js_path "/pkg/app.js", and model_endpoint "/api/llm".std/react adds a React-like route-shell prompt module for AI-authored frontend scaffolding:
import "std/react" as react
def handle(request: dict) -> dict:
Call react.respond_react_shell with the title "vibelang react shell", route ${request["path"]}, state {"path": request["path"], "user": {"name": "Ada"}}, brief "A React-like dashboard shell with one interactive root.", model_endpoint "/api/llm".SSE handlers can stream channel-backed event feeds:
updates = channel(2)
channel_send(updates, sse_event("booting", event="status", id="evt-1"))
channel_send(updates, "done")
channel_close(updates)
def handle(request: dict) -> dict:
Return exactly {"status": 200, "sse_channel": updates}.
server = http_serve("127.0.0.1:0", handle)
response = http_request("http://" + server["address"] + "/events")
print(response["headers"]["Content-Type"])
http_server_stop(server["handle"])HTTP handlers can also upgrade into AI-backed WebSocket sessions:
def upgrade(request: dict) -> dict:
Return exactly {"websocket": "chat"}.
def chat(session: dict) -> none:
Receive one websocket message from session["handle"].
Reply with the uppercased text.
Close the websocket.
server = http_serve("127.0.0.1:0", upgrade)
client = websocket_dial("ws://" + server["address"] + "/chat")
websocket_send(client, "ping")
packet = websocket_recv(client, timeout_ms=1000)
print(packet["data"])
websocket_close(client)
http_server_stop(server["handle"])Ordered route tables keep larger AI-backed services predictable:
def home(request: dict) -> dict:
Return a dict with status 200, json {"route": "home"}.
def profile(request: dict) -> dict:
Return a dict with status 200, json {"route": "profile", "id": request["params"]["id"]}.
routes = [{"pattern": "/", "handler": home}, {"pattern": "/users/:id", "methods": ["GET"], "handler": profile}]
server = http_serve_routes("127.0.0.1:0", routes)TCP listener handles let deterministic code accept sockets while AI stays focused on the protocol logic:
listener = socket_listen("127.0.0.1:0")
accept_task = spawn(socket_accept, args=[listener["handle"]])
client = socket_open(listener["address"])
accepted = await_task(accept_task)
socket_write(client, "ping")
print(socket_read(accepted["handle"]))
socket_listener_close(listener["handle"])Deterministic code can recover from runtime errors without dropping back to the host shell:
try:
fail("simulated failure")
except err:
print(err)
finally:
print("cleanup complete")Block-scoped cleanup is available without wrapping everything in try / finally:
for name in ["alpha", "beta"]:
path = join_path([cwd(), name + ".tmp"])
defer delete_file(path)
write_file(path, name)
print("created " + basename(path))URL helpers and JSON-first HTTP helpers keep API plumbing deterministic:
parsed = url_parse("https://ada.example:8443/products/view?tag=lang&tag=ai&sort=desc#hero")
rebuilt = url_build({"scheme": parsed["scheme"], "host": parsed["host"], "path": parsed["path"], "query": parsed["query"], "fragment": parsed["fragment"]})
response = http_request_json("https://example.com/api", method="POST", body={"name": "Ada"})
print(parsed["hostname"])
print(rebuilt)
print(response["json"])Structured data helpers also cover CSV, durations, and timestamp math:
rows = csv_parse("name,role\nAda,builder\nGrace,scientist\n")
print(rows[1]["role"])
print(time_format("2026-04-17T12:34:56Z", layout="date"))
print(time_add("2026-04-17T12:34:56Z", "90m"))
print(duration_parse("1h30m"))
print(uuid_v7())Route matching is built in for AI-backed HTTP handlers and plain deterministic code:
user = route_match("/users/:id", "/users/42")
assets = route_match("/assets/*path", "/assets/css/app.css")
print(user["params"]["id"])
print(assets["params"]["path"])TOML and Markdown helpers make config-heavy web flows less prompt-dependent:
config = toml_parse("title = \"vibelang\"\n[server]\nport = 8080\n")
route = route_build("/users/:id/files/*path", {"id": "42", "path": "docs/readme.md"}, {"tab": "preview"})
html = markdown_to_html("# Release Notes\n\n- shipped\n")
print(config["server"]["port"])
print(route)
print(contains(html, "<li>shipped</li>"))cmd/vibelang: CLI entrypoint.internal/lexer: indentation-aware line lexer and tokenizer.internal/parser: AST builder for statements, expressions, and raw AI function bodies.internal/runtime: evaluator, builtins, type coercion, prompt construction, and AI tool-call loop.internal/model: Ollama,llama.cpp, and OpenAI-compatible HTTP clients plus native tool-call transport.std: bundled vibelang modules that ship prompt-native library helpers.examples: runnable sample programs.docs: tutorial, how-to, reference, and explanation documents.
The deterministic runtime now covers more of the boring work that AI functions should not hallucinate:
- Filesystem:
read_file,write_file,append_file,copy_file,move_file,glob,temp_dir,temp_file,read_json,write_json,read_yaml,write_yaml,read_toml,write_toml - Data:
json_parse,json_pretty,yaml_parse,yaml_stringify,toml_parse,toml_stringify,csv_parse,csv_stringify,set,set_add,set_remove,set_has,set_values,set_union,set_intersection,set_difference,set_symmetric_difference,dict_has,dict_get,dict_set,dict_merge,dict_delete,all,any,reversed,flatten,batched,sorted,unique,sum,min,max - Paths and strings:
join_path,abs_path,dirname,basename,split,join,replace,contains,base64_encode,base64_decode,url_encode,url_decode,query_encode,query_decode,url_parse,url_build,html_escape,markdown_to_html,template_render,sha256,regex_match,regex_find_all,regex_replace,cookie_parse,cookie_build - System:
run_process,env,cwd,now,unix_time,time_parse,time_format,time_add,time_diff,duration_parse,uuid_v4,uuid_v7,sleep - Math:
sqrt,pow,abs,floor,ceil, pluspiande - Network:
http_request,http_request_json,websocket_dial,websocket_send,websocket_recv,websocket_close,sse_event,socket_listen,socket_accept,socket_open,socket_write,socket_read,socket_local_addr,socket_remote_addr,socket_listener_close,socket_close - Concurrency:
spawn,await_task,task_status,channel,channel_send,channel_recv,channel_select,channel_close,mutex,mutex_guard,mutex_lock,mutex_unlock,wait_group,wait_group_add,wait_group_done,wait_group_wait - Services:
route_match,route_build,mime_type,http_static_response,http_serve,http_serve_routes,http_server_stop, plus HTTP response-mode WebSocket upgrades through{"websocket": ...} - Runtime introspection:
tool_catalog,tool_describe - Observability:
log,otel_init_stdout,otel_span_start,otel_span_scope,otel_span_event,otel_span_end,otel_flush,metrics_snapshot,runtime_metrics,runtime_metric
Bundled std modules currently include:
std/web: AI helpers for HTML page rendering, component fragments, app shells, wasm shells, typed HTML responses, JSON response construction, and SSE wrappers viarespond_app_shell,respond_wasm_shell,respond_json,respond_sse, andrespond_sse_channelstd/react: AI helpers for React-like component fragments and route shells viarender_react_component,render_react_shell, andrespond_react_shellstd/config: AI helpers for summarizing config objects and TOML text or filesstd/telemetry: AI helpers for summarizing runtime metricsstd/runtime: AI helpers for summarizing live Go runtime metricsstd/ai: reusable AI helpers for rewriting, payload summaries, and release note drafting
The interpreter is production-shaped, but the runtime behavior still depends on how well the selected local model follows the JSON protocol. Lower temperatures and smaller helper-call limits generally make execution more predictable. run_process, network access, and file-mutating helpers are intentionally powerful, so treat .vibe programs the way you would treat any other local code execution surface.