projects/omnince/README.md
01Project · 2023
02

Omnince

03
AI assistants you build in Python
05

Omnince is an AI assistant you build by writing Python. Decorate a function and it becomes a tool the assistant can use. Name the inputs with the type hints you would write anyway, describe them in the comments you would write anyway, and that is the whole contract. The model reads the shape and the words, and knows how to call it.

06

All of it runs in your browser. The assistant streams from a language model, and the moment it reaches for one of your tools the Python executes right there on the page, inside a WebAssembly sandbox. The result flows back, the model keeps going, and the loop closes itself. No server, no plugin manifest, no deploy step. You write a function and refresh.

07

I built it in 2023, just as the first custom assistants were appearing and no one had settled what one should be, to chase a hunch: that the model, the tools, and the memory would all collapse onto the device in your hand. Omnince is a sketch of that world. The name means the quality of being universal.

08
TypeScriptReactPyodide · WASMOpenAIMonacoReduxPWA
experiment · 2023github.com/chcaru/omnince
notein-context.md

A function is the whole contract

Every other way to give a model a new skill in 2023 asked for ceremony. A plugin manifest. A hand-written JSON schema. A server standing by to answer when the model called. I wanted the smallest possible gesture between having an idea for a capability and watching an assistant use it.

So in Omnince the gesture is a Python function. You decorate it, name the arguments with the type hints you would write anyway, and describe them in the comments you would write anyway. Omnince reads the shape and the words and hands the model a tool. The thing you wrote to be correct is also the thing that explains itself.

And it all runs where you are. The assistant streams from a model, and when it decides to use your function the Python executes in the page through a WebAssembly build of CPython. No backend, no deploy, no round trip to a server you keep alive. The browser became the runtime.

The timing is the part I am fondest of. I started Omnince in December 2023, weeks after OpenAI shipped its first custom GPTs, when the idea of an assistant you could shape yourself was brand new. The pattern at its center, a tool being nothing more than a typed function with its schema read straight from the signature, is the same one OpenAI would later ship as its Agents SDK, in March 2025, more than a year after I started. I was not following a standard; there was not one yet. And it was written entirely by hand, in the last stretch before AI could write code worth keeping.

The name means the quality of being universal. The wager underneath it is that the model, the tools, and the memory are all sliding onto the device in your hand, and that the distance between describing a capability and a machine using it is collapsing toward zero. Omnince is what that looks like built a couple of years early: you write down what you mean, in the language you already speak, and the machine does it.

  • Build a tool witha decorated Python function
  • Tool schemainferred from type hints
  • Tool docsyour inline comments
  • Runs inPyodide · WebAssembly
  • Packagesmicropip · live from PyPI
  • Backendnone
architectureomnince.loop
your browser · no backendanswercalls a toolresulttools offeredthe functionsone hop outagent.pyPython · you write thisParse in the browsertype hints · commentsToolsschema + descriptionsYoua messageModelreasons · streams · calls toolsRun the toolPyodide · WebAssemblyAnswerLLMa single API call
you write · you asktoolsin-browser runtimethe modelanswer

The Python you write becomes tools: type hints turn into a schema, comments into descriptions. The model is offered those tools and, when it reaches for one, the real function runs in a WebAssembly sandbox right here in the page. The result loops back, and it keeps going until it answers. The whole agentic loop is client-side. The only thing that ever leaves is a single call to the model, and the bet is that even that moves on-device.

walkthroughhow-a-turn-runs
  1. 01
    write

    Decorate a function

    Write an ordinary Python function and mark it with @omni.tool. Its parameters become the tool's inputs, their type hints become the schema, and the comment beside each one becomes the description the model reads. A class makes a nested object; optionals, unions, and literals all carry through.

  2. 02
    read

    Turn it into a tool

    Omnince parses your code in the browser and walks its syntax tree, lowering every signature into a JSON schema and gathering the comments as documentation. No registry, no manifest. The function you wrote is the entire definition.

  3. 03
    offer

    Hand it to the model

    Your message and the available tool schemas stream to a language model. It answers in prose, or it decides a tool would help and streams back a call with the arguments filled in. Tools can even gate themselves with a predicate that runs each turn, appearing only when they apply.

  4. 04
    run

    Run it in the page

    A tool call executes the real Python in a WebAssembly sandbox, right here on the page. If the function imports a package, micropip fetches it from PyPI on the spot; if it calls the network, a proxy clears the way. The return value comes back as the result.

  5. 05
    loop

    Close the loop

    That result returns to the model, which keeps going, calling more tools or writing the final answer. Calls chain, memory persists between chats through scoped storage, and the whole loop runs without a server you own.

notabledesign-choices

The decisions worth keeping

None of the parts are exotic. A language model does the reasoning, Pyodide runs the Python, the type system describes the shapes. What I like is the seam where they meet: the decision to let an ordinary function be the entire interface between you, the model, and the machine.

  1. 01

    The type hints are the schema

    You describe a tool's inputs with the same type hints you would write for any Python function. Omnince turns them into the JSON schema the model fills in, and turns the comment beside each parameter into its description. A plain class becomes a nested object, an optional becomes an optional field, a union of string literals becomes an enum.

    in contextEvery other approach wanted a second description of the data: a schema file, a soup of decorators, a config block kept painfully in sync with the code. Omnince noticed the description already existed in the signature. The function that runs and the contract the model reads are the same few lines, so they can never drift apart.

  2. 02

    A language runtime inside the page

    The tools are real Python, executed by Pyodide, a build of CPython compiled to WebAssembly. micropip installs pure-wheel packages straight from PyPI the moment a tool imports them, and a proxy lets a tool reach out to the open web. requests, numpy, and pandas are simply there.

    in contextThis is the load-bearing bet of the whole project. If the runtime lives in the browser, the assistant needs nothing behind it: no function-as-a-service to host the tools, no server to keep warm, no deploy between writing a function and using it. The page is the backend, which means there isn't one.

  3. 03

    Assistants you program, not configure

    An assistant is more than a prompt. A predicate decides, every turn, whether a tool is even offered, so capabilities appear and vanish with context. A modify_messages hook lets Python rewrite the conversation before it reaches the model, which is how the memory assistant slips in what it remembered. Scoped storage carries state across chats.

    in contextMost assistant builders give you a text box for instructions and a checklist of tools. Omnince gives you a program. Everything that happens between your message and the model's reply is yours to shape in Python, which turns a static persona into something that can hold state, change its own abilities, and edit its own context.

  4. 04

    No backend, on purpose

    Stream, call, execute, feed back, repeat: every step of the agentic loop happens in the client. The one thing that crosses the network is the model call itself, made straight from the browser with your own key.

    in contextThe premise was simple: language models are moving onto the device, and the backend is becoming optional. Omnince is built as if that future already arrived. Today the model is still a call away; the architecture is a wager that soon it won't even be that.

resultstools-it-runs

Three tools, each a few lines of Python. One trivial, one that pulls a package out of thin air, one that gives an assistant a memory. The decorator is the only thing they have in common.

dice.pypython
import random

@omni.tool(description="Roll some dice")
def roll_dice(
    count: int,  # how many dice
    sides: int,  # sides per die
):
    return [random.randint(1, sides) for _ in range(count)]
The smallest possible tool. Two typed inputs, a comment each, and the model knows exactly what to ask for.
papers.pypython
import requests, xmltodict

@omni.tool(description="Search arXiv for papers")
def arxiv_search(
    query: str,        # what to search for
    max_results: int,  # how many to return
):
    res = requests.get("http://export.arxiv.org/api/query",
        params={"search_query": query, "max_results": max_results})
    return xmltodict.parse(res.text)
xmltodict ships with nothing. The moment this tool runs, micropip pulls it from PyPI in the browser and the import just works.
memory.pypython
@omni.modify_messages()
async def remember(messages):
    notes = await omni.storage.shared.get("memory")
    if notes:
        messages.insert(1, {"role": "system",
            "content": "Remember: " + str(notes)})
    return messages

@omni.tool(description="Save something to remember later")
async def bio(
    key: str,    # what this fact is about
    value: str,  # the fact to remember
):
    notes = (await omni.storage.shared.get("memory")) or {}
    notes[key] = value
    await omni.storage.shared.set("memory", notes)
Two halves of a memory: a tool that writes a fact to scoped storage, and a hook that quietly threads those facts back into every future chat.
try itopen-it-yourself

There is no embedded sandbox here, because Omnince wants a model and a key of its own. The live app runs the whole loop in your browser: bring an OpenAI key, write a tool, and watch the assistant reach for it. Your key and your code never leave the page. Here is the shape of every tool you would write.

anatomy-of-a-tool.pypython
import omni
from typing import Optional, Literal

class Place:                        # a reusable shape for the model
    name: str                       # explained by this very comment
    kind: Optional[Literal['city', 'park']]

@omni.tool(
    displayName='Find a place',     # what the user sees
    description='Look a place up',   # what the model sees
    predicate=lambda: True,         # offer the tool only when true
)
def find_place(
    place: Place,   # typed inputs become the schema
    limit: int,     # comments become the descriptions
):
    return ...      # return a string, or anything JSON-serializable