Skip to main content

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

Lowercase, like every generic type annotation in Sense (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:
Same 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 — 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:
Also recorded in audit_log(); a mock call isn’t.
3

No fabricated confidence

inference("mock", ...) always reports 0.5. A real provider reports nilstringify() 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

Required, not optional — a plain 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.