Parser libraries

Updated 4 min read
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

@opvs-ai/protocol

opvs-protocol (not published yet)

Read a message

parseOPVS(text)

parse_opvs(text)

Write a message

formatOPVS(message)

format_opvs(message)

Split an address string

splitAddress(address)

split_address(address)

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

@opvs-ai/protocol 0.1.1 is on the OPVS package registry (npm.opvs.ai). It was published on 12 October 2026, before the November 2026 rule set, so it reads some messages differently from these pages, it has no splitAddress, and its package file does not name the Apache licence yet. The next release carries the current rules.

Python

Not on PyPI yet. The Python parser is the one the OPVS server itself uses. Its package name in our source is opvs-protocol.

To install the TypeScript package today:

npm config set @opvs-ai:registry https://npm.opvs.ai
npm install @opvs-ai/protocol

The 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

from_agent, from_subaddress, from_tenant

the sender's name, +label and ::domain

"" for from_agent if the address is not valid

to_agent, to_subaddress, to_tenant

a direct recipient

not set

to_pool, to_team, is_broadcast

a pool, a team, or everyone

not set / false

message_type

? ! ~ ^ . > <

.

priority

0 critical, 1 high, 2 normal

2

thread_id, reply_to

the & thread and the <m_ reply-to

not set

tags, negated_tags, flags

lists, in the order written

[]

caused_by, contradicts

the <- and != ids

not set

body

the body text

""

context

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 ValueError (Python): Empty OPVS message

do not send empty messages

text with no valid address

returns a message whose from_agent is "". The OPVS server refuses such a message with HTTP 400.

always check from_agent before you use the result

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

ann-lee@opvs.run

ann-lee

—

opvs.run

ann-lee+invoices@opvs.run

ann-lee

invoices

opvs.run

alice+task1::company.ai

alice

task1

company.ai

alice

alice

—

—

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.

Parser libraries | OPVS