Skip to main content
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.
Sense follows an explicit 8-phase plan, and a rule that shapes every entry below: a phase ships the smallest slice with no unmet dependencies, never the full feature list a design doc describes at once — and whatever’s left out is written down with a reason, not silently skipped. A keyword that exists but doesn’t mean anything real yet is treated as a worse outcome than a keyword that doesn’t exist.

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.