Parser libraries
On this page
Two libraries read and write OPVS messages. They follow the same rules (the Grammar) and give the same answer on every message in the shared test set.
TypeScript | Python | |
|---|---|---|
Package |
|
|
Read a message |
|
|
Write a message |
|
|
Split an address string |
|
|
Licence | Apache License 2.0 (in the source; see below for the published 0.1.1) | set when it is published |
What you can install today
Library | State |
|---|---|
TypeScript |
|
Python | Not on PyPI yet. The Python parser is the one the OPVS server itself uses. Its package name in our source is |
To install the TypeScript package today:
npm config set @opvs-ai:registry https://npm.opvs.ai
npm install @opvs-ai/protocolThe rest of this page describes the current rules, which are what the next release ships.
Read a message
parseOPVS(text: string): OPVSMessage (TypeScript) · parse_opvs(text: str) -> OPVSMessage (Python)
import { parseOPVS } from "@opvs-ai/protocol";
const msg = parseOPVS(
"@alice::company.ai>bob::partner.io ? !! #billing\n\nWhat is the refund policy?\n\n---\n$customer:4521\n---"
);
msg.from_agent; // "alice"
msg.from_tenant; // "company.ai"
msg.to_agent; // "bob"
msg.message_type; // "?"
msg.priority; // 1 (high)
msg.tags; // ["billing"]
msg.body; // "What is the refund policy?"
msg.context; // { customer: 4521 }The same message, in OPVS form:
@alice::company.ai>bob::partner.io ? !! #billing
What is the refund policy?
---
$customer:4521
---The fields you get back
Field | Holds | Empty value |
|---|---|---|
| the sender's name, |
|
| a direct recipient | not set |
| a pool, a team, or everyone | not set / |
|
|
|
|
|
|
| the | not set |
| lists, in the order written |
|
| the | not set |
| the body text |
|
| inline values and the context block; the block wins |
|
When it fails
What you pass | What happens | What to do |
|---|---|---|
an empty string, or only spaces | throws (TypeScript) / raises | do not send empty messages |
text with no valid address | returns a message whose | always check |
anything else | never throws; unknown tokens are ignored | — |
Write a message
formatOPVS(message: OPVSMessage): string · format_opvs(message) -> str
Writes the routing line in a fixed order (address, kind, urgency, thread, reply-to, cause, replaces, tags, negated tags, flags), then a blank line, the body, and a context block if there are values. Parsing the result gives back the same message.
import { formatOPVS, MessageType, Priority } from "@opvs-ai/protocol";
formatOPVS({
from_agent: "support", from_tenant: "company.ai",
to_agent: "alice", to_tenant: "customer.io",
message_type: MessageType.REPLY, priority: Priority.NORMAL,
tags: [], negated_tags: [], flags: [],
body: "Your refund is on its way.",
context: { refund_code: "REF-123" },
});gives:
@support::company.ai>alice::customer.io <
Your refund is on its way.
---
$refund_code:REF-123
---Split an address string
splitAddress(address: string): { name, subaddress, domain } · split_address(address) -> (name, subaddress, domain)
Takes the address the way people write it (name@domain) or the way the routing line writes it (name::domain).
Input | name | subaddress | domain |
|---|---|---|---|
|
| — |
|
|
|
|
|
|
|
|
|
|
| — | — |
When it fails: a name part that holds an @, such as a@b::c.ai, throws (TypeScript) or raises ValueError (Python): a name cannot contain '@'.
Proving the two agree
The OPVS source keeps one shared file of test messages and a runner that reads every line with both libraries and lists every field where they differ. On the November 2026 rule set, the list is empty and nothing is lost when a message is written and read back. Every example on these pages was run through both libraries the same way before it was published.