The shape of it
1
A Model is a value, but naming one needs model
inference(...) returns a Model — and model NAME = inference(...)
is required to bind it to a name; a plain reasoning = inference(...)
is a SenseSyntaxError.2
ask() asks whichever Model is delegated to
Never a vendor directly. Swap the model,
ask() doesn’t change.
(Named ask, not reason — a real LLM “extended thinking” mode is a
different, specific capability; this is just “get an answer.”)3
The result is an Answer, never a plain value
.value, .confidence, .source — nothing else. No implicit
unwrapping: you can’t pass an Answer<String> where a plain String
is expected without writing .value.Switch which model answers: set delegation
set delegation is scoped like a variable — inside a
session or agent, it
reverts once that block ends.
Bypass delegation: <model>.ask(prompt)
answer<T> as a type annotation
array<T> isn’t
a thing, but the convention here matches belief<T> in the original
design notes — the type itself is Answer, capitalized everywhere else).
Real providers: inference(provider, model_id?, ...)
One function, every vendor — provider is the only thing that changes:
Model/ask()/Answer surface as inference("mock", ...) —
nothing above changes. What’s actually different:
1
The API key is never a literal in Sense source
Read from an environment variable only. Missing it raises immediately
at
inference(...) call time, not on the first ask().2
Capability-gated
A real call costs money and leaves the process — Also recorded in
AnthropicProvider
declares capability = "model.anthropic", OpenAIProvider declares
"model.openai"; inference("mock", ...) declares neither, so mock
calls are never gated. Default allow, so this only bites once you write
policy: deny model.anthropic:audit_log(); a mock
call isn’t.3
No fabricated confidence
inference("mock", ...) always reports 0.5. A real provider reports
nil — stringify() renders that as answer<?>(...), not a fake
number.pip install sense-lang[anthropic] / [openai] — both optional, both
imported lazily, so plain sense_lang never requires either. Any
provider exception is wrapped in SenseRuntimeError, never a raw Python
traceback.Name a model: model NAME = expr
claude = inference("anthropic") is a
SenseSyntaxError telling you to add model. Without a name to override
it, .name would just default to the model id ("claude-sonnet-5") —
that fallback still exists, but the only way to observe it now is a value
that’s never assigned at all, e.g. print(inference("anthropic").name).
model x = 5 raises SenseTypeError before x is bound; it only ever
accepts a Model.
Configure a call: labeled arguments
temperature/max_tokens/api_key_env are optional, forwarded to the
real API call — max_tokens as OpenAI’s own max_completion_tokens on
the wire, since OpenAI’s newer models reject the older name outright;
inference(..., max_tokens: ...) is the one label to write regardless of
provider. This is the only builtin that takes labeled arguments — a
plain function/tool/action call stays positional-only.
inference("mock", ...) takes its own pair instead —
response_template:/confidence: — since there’s no real API call to
configure; passing temperature: to "mock" (or response_template: to
a real provider) raises SenseRuntimeError.
Switching providers based on whichever key is set is one branch:
Not built yet
Streaming, multi-turn conversation history, system prompts, a local model (Ollama/llama.cpp). See Roadmap.Continue
Tool-Calling
Let a real model choose which
tool to call.Sessions
Scope which model is active to one block.
Agents
Runtime entities that hold state and can pause mid-task.
Actions & policy
The same capability mechanism, for side effects instead of model calls.

