This is the reader-facing version of
docs/ROADMAP.md in the repository,
which is the actual working design log (denser, more cross-referenced to
goals/). Both are updated together — this page exists so the same
information reads well outside the codebase.Phase 0 — Language Research
Done. The problem statement, computational model, and the central
safety law all predate any code — see Philosophy.
Phase 1 — Minimal Language
Done. Lexer, parser, AST, a shallow type-annotation checker, functions,
variables, control flow, arrays, modules — the deterministic core covered
in the Language Tour. Not built: real static
type inference (today’s checking is a runtime check, not a type system in
the PL-theory sense), user-defined structs/product types, a bytecode VM
(the reference implementation is a tree-walking interpreter, which is fine
for design iteration and revisited once semantics stabilize).
Phase 2 — AI Primitives
Done.
model, set delegation, ask, Answer — see
Model, Ask & Answer — plus, once the
policy/capability layer they depended on existed (Phase 4), the three
primitives originally deferred here: tool (a
capability-checked function with no prepare/verify/commit lifecycle),
memory (a session-scoped key/value store, distinct
from a variable — plus a persistent
form backed by a durable, versioned SQLite log, scoped down from “Delta
Lake” to the mechanism worth keeping: append-only writes give versioning
and auditing as the same log, for free). tool also comes in an
async form — see Phase 3. A
real model provider now exists too — two, in fact, behind one function,
inference(provider, ...)
— the identical ModelProvider seam kept ask()’s semantics from
ever needing to change, and each is capability-gated (policy: deny model.anthropic) since a real call costs money and leaves the process,
closing a gap this page used to name as “tabled for Phase 4.” A real
model can now also
choose which tool to invoke
via ask_with_tools(prompt, tools) — the first time a model’s own
decision, not human-written Sense control flow, reaches the same
_call_tool enforcement path a hand-written call goes through: a
policy-denied tool the model asks for is never executed, and the denial
is fed back to the model rather than crashing the call. tool gained an
optional description for this, required only once a tool is actually
handed to ask_with_tools.
Memory is now wired into reasoning
too, and it split cleanly on investigation: write-back needed no new
interpreter feature — a tool wrapping <memory>.remember(...) handed
to ask_with_tools already lets a model manage its own memory, gated
and audited like anything else; only retrieval
(ask_with_memory(prompt, memory), injecting a memory’s contents
ahead of the prompt) was genuinely new, and it works with
inference("mock", ...) too.
skill(name, description, tools)
rounds out four of the original five pieces — no dedicated keyword,
same reasoning as model: a plain builtin returning a Skill value that
ask_with_tools flattens into its component tools, prepending the
skill’s description to the prompt (real effect, not just documentation).
Not built: async tool support inside the tool-calling loop, a
provider-agnostic tool-calling abstraction, other providers (OpenAI, a
local model), automatic write-back from free text (deliberate — see the
page above), nested skills, and a per-skill capability.
connect_mcp(name, command, args, capability?)
closes the fifth and last piece — and the one that’s genuinely different
in kind from the other four: an MCP tool has no Sense body at all, so
invoking one is a JSON-RPC call to an external process over the official
mcp SDK, bridged into the synchronous interpreter through a background
thread running one persistent asyncio event loop for the connection’s
whole lifetime (necessary because Sense has no closures to make an
async-native callback shape work, and because an MCP session has to be
reused across many calls, not re-opened per call). Tested against a real
subprocess MCP server, not a mock — which is exactly how a real bug in
the bridge design got caught and fixed (anyio’s cancel scopes needing
__aenter__/__aexit__ in the same asyncio task). All five pieces from
the original “real LLM integration” ask are done, each scoped to the
smallest honest slice that still does the real thing. Not built: MCP
HTTP/SSE transport (stdio only), MCP resources/prompts (tools only), and
reconnection on a dead session.Phase 3 — Agent Runtime
In progress — session, agent, pause/resume, and opt-in async scheduling
done. Sessions and Agents are
real: an agent is a first-class runtime entity with identity, persistent
state, and a genuine pause/resume lifecycle, not
sugar over a function call.
async agent
(and its async tool counterpart) makes start()/resume() skip their
blocking wait, so an agent’s body and its caller — or two async agents —
can genuinely execute at the same time; a plain (sync) agent/tool is
completely unaffected. This was chosen deliberately over making the whole
language async-by-default (every call non-blocking unless marked sync),
which forces function coloring through every plain call — see
Design Order for why that conflicts with
Sense’s simplicity-first goal. Not built: inter-agent delegation/
communication/spawning (an agent can’t start or message another agent
yet), a true scheduler (each async call is just a fresh daemon thread,
not fairness/priority-managed), any synchronization primitive (lock,
channel, atomic — two async bodies sharing state can race),
serialization/persistence across process restarts (a paused agent’s state
lives in a live thread’s call stack), budget, evaluation, and Process as
a concept distinct from Agent (not needed yet — nothing requires “has a
lifecycle but doesn’t pursue a goal” separately from Agent).Phase 4 — Safety and Control
Core done, including human-in-the-loop, declared approval, and audit
logs.
reversible/irreversible action with a real
prepare → verify → commit law, plus capability-gated policy: allow/deny
— see Actions and Policy. This closes
what was, for a while, the single biggest gap between what was built and
what actually makes Sense Sense: every function call used to execute
immediately, with no notion of a staged, verifiable, revocable effect.
ask_human(question) — see Pause & Resume —
is the human-intervention primitive, built directly on
pause()/resume() once those were generalized to carry a value across
the suspend boundary. An action can now also mandate approval
(requires x.y, approval) rather than relying on calling code to add it by
hand — <action>.approve() is a separate method, and .commit() checks it
the same independent way it re-checks capability, raising the new
SenseApprovalError if skipped. Every prepare/verify/commit/deny/fail
event is recorded in Interpreter.audit_log, with a simplified
audit_log() view for Sense programs themselves.
A reversible action must now also declare a rollback block, run by
<action>.rollback() after commit — see Rollback
— and wrapping what an action’s body calls in a tool
closes what used to be a gap (“only the action itself is gated, not what it
calls”). --dry-run
(on both sense run and sense inspect) reuses the prepare → verify →
commit law directly: every capability/approval check still runs for real,
only the body performing the actual effect is skipped, with the
simulation marked on the existing audit log rather than a new state or a
second log. Not built: resource budgets, capability dimensions beyond a
name (cost, risk, rate limits, for both action and tool), default-deny
sandboxing (today’s default is allow — see Policy),
durable/persisted audit logging (today’s is in-memory only, per
Interpreter instance), and a recovery story for a rollback block that
itself fails mid-rollback.Python Interoperability
Core done. Not one of the original 8 phases — its own plan, sequenced
right after capability enforcement and before broader ecosystem work.
import python "module" reaches directly
into any installed Python package, and it’s simpler than a typical
cross-language bridge for one specific reason: Sense’s own reference
interpreter is itself written in Python, running in the same process — no
FFI, no serialization boundary. Most Sense values already are Python
values (Int is int, Array is list), so a Python function returning
a list is immediately a usable Sense Array; anything else (a dict, a
class instance) comes back as an opaque, still-usable wrapper. The safety
model extends to cover this rather than being bypassed by it: a bare
import and call is exactly as unrestricted as a plain Sense fn always
was, and wrapping a Python call in an action gets full
prepare → verify → commit enforcement regardless of what the body’s
implementation happens to be.
Not built: automatic safety classification of foreign functions (declaring
requires is still something you do by hand), sandboxing or conformance
testing of imported code, interop with any language other than Python,
importing a specific name rather than a whole module, and pre-built
integration packages (sense-pandas, sense-numpy, etc.).Phase 5 — Developer Experience
Testing framework, formatter, and inspector console core done.
test "description": ... — an isolated,
individually-reported assertion block — plus sense test <path>, which
discovers test_*.sns/*_test.sns files under a file or directory path
and reports a PASS/FAIL summary; a failure inside one test block
doesn’t stop its siblings, the rest of the file, or the rest of a run.
sense fmt is a canonical pretty-printer over the
existing AST, with two guarantees checked against every real example:
formatting never changes program behavior, and it’s idempotent
(fmt(fmt(x)) == fmt(x)) — comments are re-attached rather than dropped,
and blank lines are preserved (never imposed).
sense inspect <file> serves
a live, local, browser-based console over a program’s declared surface —
every tool/action/agent/memory/skill/MCP server, plus policy state
and a live audit log — closer to Swagger UI’s “try it out” than to static
docs, except every panel calls straight into an existing Interpreter
method rather than a new enforcement path: a policy denial clicked from
the browser is the exact same SensePolicyError a hand-written call
would raise. Watching a real agent hit pause() and resuming it from the
browser is the one panel with no REST/Swagger analog at all. Stdlib-only,
no CDN — the same offline-by-design principle inference("mock", ...)/
ask() established, extended to tooling.
Not built: package manager, formatter line-width wrapping/alignment/
configuration, language server, debugger, test fixtures/mocking/coverage,
agent simulation environment, observability tools, “try it out” for MCP
tools (needs something only Sense code can honestly supply), WebSocket
push for the console (short polling instead).Phase 6 — Real Applications
Not started. Reference apps (a coding agent, a research agent, an
enterprise analyst) meant to prove Sense earns its keep over
Python + SDK + framework — see Non-Goals for the
exact test this phase is meant to pass.Phase 7 — Ecosystem
Not started. Package/capability registries, model/tool providers, a
hosted runtime.
The pattern, if you’re skimming
Every phase above follows the same shape: ship the piece with no unmet dependencies, write down what’s missing and why, and treat “not built yet” as different from “forgotten.” If you’re deciding whether to build on Sense today, the “not built” lines in each phase are the actual scope of what you’d be signing up to work around — take them as seriously as the “done” lines.Continue
Design order
Why phases are sequenced this way instead of built in parallel.
FAQ
Common questions this roadmap raises.

