Skip to main content

Semantic Action Grammar — a DSL for structured inter-agent communication, with parsing, validation, correlation, and fold-based context compression.

Project description

semantic-action-grammar

Semantic Action Grammar (SAG) — a typed, parseable DSL for structured inter-agent communication.

SAG is what JSON would look like if it were designed for agent-to-agent messaging instead of REST. It carries actions, queries, asserts, events, errors, control flow, knowledge facts, and a fold/unfold protocol for context compression — all in a wire format that's ~59% smaller than the equivalent JSON envelope and validatable without an LLM in the loop.

This package is the Python implementation. The canonical ANTLR4 grammar and the Java implementation live in the main repository.

Install

pip install semantic-action-grammar

Python 3.10+ required. The only runtime dependency is antlr4-python3-runtime. The import name is sag:

from sag import SAGMessageParser

Wire format at a glance

A SAG message is one header line followed by one or more typed statements:

H v 1 id=msg1 src=agentA dst=agentB ts=1700000000 corr=parent1 ttl=30
DO deploy("app1", version=42) P:security PRIO=HIGH BECAUSE balance>1000;
Q health.status WHERE cpu>80;
A state.ready = true;
IF ready==true THEN DO start() ELSE DO wait();
EVT deployComplete("app1");
ERR TIMEOUT "Connection timed out";
KNOW system.cpu = 85 v 3;
FOLD f1 "Completed onboarding" STATE {"users":42};
RECALL f1
Statement Syntax Purpose
Action DO verb(args) [P:policy] [PRIO=...] [BECAUSE expr] Execute commands
Query Q expr [WHERE constraint] Query state
Assert A path = value Set state
Control IF expr THEN stmt [ELSE stmt] Conditional
Event EVT name(args) Emit events
Error ERR code "message" Report errors
Fold FOLD id "summary" [STATE {...}] Compress context
Recall RECALL id Restore context
Subscribe SUB pattern [WHERE filter] Subscribe to topics
Unsubscribe UNSUB pattern Unsubscribe
Knowledge KNOW topic = value v N Share versioned facts

Quick start

from sag import SAGMessageParser, MessageMinifier, ActionStatement

text = (
    'H v 1 id=msg1 src=agentA dst=agentB ts=1700000000\n'
    'DO deploy("app1", version=42) BECAUSE balance>1000'
)

message = SAGMessageParser.parse(text)

# Header fields
print(message.header.message_id)  # "msg1"
print(message.header.source)      # "agentA"
print(message.header.destination) # "agentB"

# Typed statements
for stmt in message.statements:
    if isinstance(stmt, ActionStatement):
        print(stmt.verb)        # "deploy"
        print(stmt.args)        # ["app1"]
        print(stmt.named_args)  # {"version": 42}
        print(stmt.reason)      # "balance>1000"

# Re-serialize to the compact wire format
print(MessageMinifier.to_minified_string(message))

Validating without an LLM

Guardrails — BECAUSE clauses as preconditions

from sag import GuardrailValidator, MapContext, ActionStatement

context = MapContext({"balance": 1500})

action = ActionStatement(
    verb="transfer",
    args=[],
    named_args={"amt": 500},
    reason="balance > 1000",
)

result = GuardrailValidator.validate(action, context)
assert result.is_valid is True

# Same action, insufficient balance:
context = MapContext({"balance": 400})
result = GuardrailValidator.validate(action, context)
assert result.is_valid is False
assert result.error_code == "PRECONDITION_FAILED"

Schema validation — typed argument contracts

from sag import (
    ActionStatement,
    ArgType,
    SchemaRegistry,
    SchemaValidator,
    VerbSchema,
)

registry = SchemaRegistry()
registry.register(
    VerbSchema.Builder("deploy")
    .add_positional_arg("app", ArgType.STRING, True, "Application name")
    .add_named_arg("env", ArgType.STRING, False, "Environment",
                   allowed_values=["dev", "staging", "production"])
    .add_named_arg("replicas", ArgType.INTEGER, False, "Replica count",
                   min_value=1, max_value=100)
    .build()
)
validator = SchemaValidator(registry)

ok = ActionStatement(verb="deploy", args=["webapp"],
                     named_args={"env": "production", "replicas": 3})
assert validator.validate(ok).is_valid

bad = ActionStatement(verb="deploy", args=["webapp"],
                      named_args={"env": "qa"})  # not in enum
assert not validator.validate(bad).is_valid

Pre-built schema profile (CI/CD)

SoftwareDevProfile ships with 12 verbs configured for build, test, deploy, rollback, review, merge, lint, scan, release, provision, monitor, and migrate workflows — including value constraints (enums for environments, regex for SemVer, ranges for replicas/timeouts).

from sag import SoftwareDevProfile, SchemaValidator, ActionStatement

registry = SoftwareDevProfile.create_registry()
validator = SchemaValidator(registry)

action = ActionStatement(
    verb="deploy",
    args=["webapp"],
    named_args={"env": "production", "replicas": 3},
)
assert validator.validate(action).is_valid

Sanitizer — the four-layer firewall

The sanitizer chains grammar parse → routing guard → schema validation → guardrail check into one call. Anything that fails any layer is rejected before reaching your agents.

from sag import (
    AgentRegistry,
    MapContext,
    SAGSanitizer,
    SchemaRegistry,
    VerbSchema,
    ArgType,
)

schemas = SchemaRegistry()
schemas.register(
    VerbSchema.Builder("deploy")
    .add_positional_arg("app", ArgType.STRING, True, "Application name")
    .build()
)

agents = AgentRegistry()
agents.register("svc1")
agents.register("svc2")

sanitizer = SAGSanitizer(
    schema_registry=schemas,
    agent_registry=agents,
    default_context=MapContext({"balance": 1500}),
)

ok = 'H v 1 id=msg1 src=svc1 dst=svc2 ts=1700000000\nDO deploy("app1")'
assert sanitizer.sanitize(ok).valid

# Unknown source — caught at the routing layer
bad = 'H v 1 id=msg1 src=ghost dst=svc2 ts=1700000000\nDO deploy("app1")'
result = sanitizer.sanitize(bad)
assert not result.valid
assert result.errors[0].error_type.name == "ROUTING"

Compressing context — fold / unfold

The fold protocol collapses completed conversation segments into a single statement with optional preserved state. unfold recovers the original messages exactly — 100% roundtrip fidelity in the test suite.

from sag import FoldEngine, SAGMessageParser

engine = FoldEngine()

m1 = SAGMessageParser.parse(
    "H v 1 id=m1 src=a dst=b ts=1700000000\nDO start()"
)
m2 = SAGMessageParser.parse(
    "H v 1 id=m2 src=b dst=a ts=1700000001\nA status.ready = true"
)

fold = engine.fold([m1, m2], "Completed startup", state={"phase": "running"})
print(fold.fold_id, fold.summary, fold.state)

restored = engine.unfold(fold.fold_id)
assert restored == [m1, m2]   # exact equality

# Detect when context pressure should trigger a fold
big = [m1, m2] * 100
assert engine.detect_pressure(big, budget=200, threshold=0.5) is True

Token efficiency — SAG vs JSON

from sag import SAGMessageParser, MessageMinifier

msg = SAGMessageParser.parse(
    'H v 1 id=msg1 src=agentA dst=agentB ts=1700000000\n'
    'DO deploy("app1", version=42)'
)

cmp = MessageMinifier.compare_with_json(msg)
print(cmp)
# SAG: 78 chars (20 tokens) vs JSON: 209 chars (53 tokens) - Saved: 33 tokens (62.3%)

Threading conversations — CorrelationEngine

Each agent owns one engine, which mints monotonic message IDs and threads corr= headers through replies automatically.

from sag import CorrelationEngine, SAGMessageParser

agent_a = CorrelationEngine("agent-a")

# Build an outbound header
header = agent_a.create_response_header("agent-a", "agent-b")
print(header.message_id)  # "agent-a-1"

# When a reply comes in, build the response with corr= threaded through
incoming = SAGMessageParser.parse(
    "H v 1 id=msg42 src=agent-b dst=agent-a ts=1700000000\nQ status"
)
reply = agent_a.create_header_in_response_to("agent-a", "agent-b", incoming)
assert reply.correlation == "msg42"

CorrelationEngine.trace_thread, find_responses, and build_conversation_tree reconstruct cross-agent causal chains from collected message logs.

Sharing knowledge across agents

KnowledgeEngine is a per-agent, versioned fact store with topic-based subscriptions and delta propagation. Subscribers only receive facts that match their pattern and that have advanced past the version they last saw.

from sag import KnowledgeEngine

agent_a = KnowledgeEngine("agent-a")
agent_b = KnowledgeEngine("agent-b")

# Agent B subscribes to all of agent A's `system.*` facts
agent_a.add_subscriber("agent-b", "system.*")

agent_a.assert_fact("system.cpu", 85)
agent_a.assert_fact("system.mem", 70)
agent_a.assert_fact("app.errors", 3)   # not in subscription

delta = agent_a.compute_delta("agent-b")  # only system.* facts
agent_b.apply_incoming(delta, "agent-a")
# agent_b now knows system.cpu=85, system.mem=70 — nothing else

Topic patterns: system.* (single level), system.** (multi-level), ** (everything).

Multi-agent orchestration — Grove

Grove organizes agents into a tree and runs them bottom-up. Every inter-agent hop produces a real SAG message on the wire (proper header, KNOW statements). Plug in any backend via the AgentRunner protocol — EchoAgentRunner for tests, LLMAgentRunner for Claude/OpenAI, or a custom implementation.

from sag import TreeEngine, Grove, EchoAgentRunner

tree = TreeEngine()
tree.add_root("pm", "Project Manager",
              prompt="Synthesize reports", topics=["project.plan"])
tree.add_child("pm", "eng", "Engineer",
               prompt="Design architecture", topics=["eng.design"])
tree.add_child("pm", "qa", "QA Lead",
               prompt="Plan testing", topics=["qa.strategy"])

grove = Grove(tree, EchoAgentRunner())
result = grove.execute("Build a REST API")

print(f"Agents run: {result.agents_run}")
print(f"SAG messages exchanged: {len(result.messages)}")
for topic, (value, version) in result.facts.items():
    print(f"  {topic} (v{version}): {value}")

InteractiveGrove adds step-through execution, fact editing, and checkpoint/rollback. ChatSession opens a conversational loop with the root after the grove completes. Both are exported from the top-level sag package — see the main repository for full demos.

What's in the package

Module Exports
sag.parser SAGMessageParser
sag.model Message, Header, all statement types
sag.minifier MessageMinifier, TokenComparison
sag.expression, sag.context ExpressionEvaluator, Context, MapContext
sag.guardrail GuardrailValidator
sag.schema VerbSchema, SchemaRegistry, SchemaValidator, ArgType
sag.sanitizer SAGSanitizer, AgentRegistry
sag.fold FoldEngine
sag.correlation CorrelationEngine
sag.knowledge KnowledgeEngine, topic_matches
sag.tree, sag.grove TreeEngine, Grove, InteractiveGrove, ChatSession, runners
sag.checkpoint CheckpointManager
sag.profiles SoftwareDevProfile
sag.prompt PromptBuilder, SAGGenerator (LLM prompt + retry loop)

Benchmark headlines

(Numbers from the benchmark harness in the main repository.)

  • SAG wire format is 59% smaller than JSON for the same payload.
  • Fold protocol achieves 82–98% lossless compression at typical conversation sizes.
  • With a 10K-token budget, SAG + folding sustains ~8.8× more conversation turns than linear NL before context exhaustion.
  • Fold → unfold round-trips with 100% fidelity across the test suite.

See the main repository for the benchmarking harness, Java implementation, the live chatbot/grove demos, and the canonical SAG.g4 grammar.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

semantic_action_grammar-1.2.2.tar.gz (67.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

semantic_action_grammar-1.2.2-py3-none-any.whl (79.0 kB view details)

Uploaded Python 3

File details

Details for the file semantic_action_grammar-1.2.2.tar.gz.

File metadata

  • Download URL: semantic_action_grammar-1.2.2.tar.gz
  • Upload date:
  • Size: 67.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for semantic_action_grammar-1.2.2.tar.gz
Algorithm Hash digest
SHA256 9e2af44eb7736220b1c33c809687435f082287c8e9ff17d243e6047286ff0e59
MD5 c3b5d12105b804d43835464c55708fab
BLAKE2b-256 8415e9cefc77c255d2f4eff07e7f9f8027c323dcecae3979b9bdf4d63f3e8fbc

See more details on using hashes here.

File details

Details for the file semantic_action_grammar-1.2.2-py3-none-any.whl.

File metadata

File hashes

Hashes for semantic_action_grammar-1.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 c0e1bd5a2e23e1aa8a08c4e112c1aea125528f38fa9937c3369d054f1e44cc64
MD5 cfa94d1c377dec418e58f4f6c302a12e
BLAKE2b-256 f47cfcf68c6914ac31db669b56a8aa424c7ed3e96bb1228599e8ecbb04cb41c9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page