Introduction

Lonis is an AI-native tool harness for the Anima ecosystem — a local-first, machine-readable alternative to ambient server protocols for exposing sharply bounded tool surfaces to agents.

"MCP exposes servers to models; Lonis exposes tools to agents."

Lonis is built on one conviction, inherited from its namesake's first life as a vision analyzer: never let the model guess. Where the original Lonis measured pixels so a vision model couldn't hallucinate colors, this Lonis binds tools to contracts so an agent can't misread what it can do. In both incarnations, the LLM is a pure reasoner suspended between an unreliable sensorium and an unreliable effector system — and Lonis is the well-formed I/O membrane around it.

What it is

  • A contract (lonis-schema): the Block — a structured domain object every tool emits through, uniformly versioned, attributed, bounded, replayable, and render-parity (human and machine render from the same typed value).
  • A runtime (lonis-core): the Tool trait, registries, per-mode rendering, and bounded adapters that host any composable CLI as a tool — with hard timeouts, byte caps, cleared environments, and kill-and-reap discipline.
  • A CLI (lonis): the harness binary — discover tools, invoke them, emit their schemas. lonis is itself a conforming provider: it can host itself.
  • Macros (lonis-derive): derives that make the contract's sharp edges compile-time guarantees instead of runtime failures.
  • A facade (lonis): one crate re-exporting all of it, serde-style.

Where it sits

In the Anima doctrine's planes, Lonis is the Tools plane — the piece that turns the other planes' domain objects into invocable verbs with bounded surfaces. amari-discovery is the reference vertical whose machinery Lonis generalizes (and which will eventually delete its own protocol layer into lonis-schema). karpal-discovery is the first external consumer.

Why not MCP?

MCP is the diplomacy layer — how Anima talks to external harnesses. Lonis is domestic tooling: Unix, not RPC. Fixed argv, JSON on stdin, blocks on stdout, structured errors on stderr, exit codes, cleared environments, byte caps, kill-and-reap on timeout. For internal tools, a bounded subprocess with a sharp contract beats a long-lived server: less context pollution, no connection lifecycle, crash isolation by construction.

Getting Started

Install

cargo install lonis-cli   # the `lonis` binary

Or from source:

git clone https://github.com/Industrial-Algebra/Lonis
cd Lonis && cargo install --path crates/lonis-cli

Five minutes with the harness

# What tools are registered?
lonis tools list

# Describe one (its ToolContract)
lonis tools describe lonis:builtin:echo

# Invoke it — inline JSON, a @file, or stdin
lonis call lonis:builtin:echo '{"hello": "world"}' --mode json

Every invocation follows the same split: blocks on stdout, structured errors on stderr with a stable exit code.

[
  {
    "schema_version": "lonis.block/v1",
    "provenance": { "tool_version": "0.1.0", "input_hash": "…" },
    "attribution": {
      "identity": "lonis:builtin:echo",
      "provenance": { "when": "2026-08-10T12:00:00Z", "producer": "lonis:builtin:echo" }
    },
    "payload": { "kind": "result", "data": { "output": { "hello": "world" } } }
  }
]

The normative contract

lonis schema              # list all 16 schema families
lonis schema block        # the envelope (all kinds + the extension seam)
lonis schema message      # one kind's payload schema

Validate anything you emit against these — they're the same documents the golden fixtures validate against in CI.

As a library

[dependencies]
lonis = "0.1"   # the facade: schema + derive + core
#![allow(unused)]
fn main() {
use lonis::{Block, Capabilities, Tool, ToolRegistry};
}

See The Block Contract next.

The Block Contract

A block is the canonical structured domain object — what every Lonis tool emits through. Defined by the Anima doctrine §2.7; decided in ADR-0001.

Six cross-cutting properties

Every block carries all six:

PropertyWhereMeaning
Envelopeschema_version, provenance, warningsVersioned, self-describing frame
AttributionattributionWho (identity), under what lens (viewpoint), and when/where/producer
BoundedboundsResource limits (max_items/max_bytes/max_length/timeout_millis) are first-class
Versionedschema_version + per-kind $idlonis.block/v1 + lonis.block/<kind>/v1
Replayableprovenance hashes + seedCanonical content hashes → deterministic replay
Render-parityrender_human()Human and machine render from the same typed value — drift is structurally impossible

The wire shape

Flat — the envelope's data is the payload, not a wrapper:

{
  "schema_version": "lonis.block/v1",
  "provenance": { "replay": { "replayable": true }, "input_hash": "…", "seed": 7 },
  "warnings": [],
  "attribution": {
    "identity": "dominic",
    "viewpoint": "reviewer",
    "provenance": { "when": "…", "where": "session:abc", "producer": "lonis:test" }
  },
  "bounds": { "max_items": 64 },
  "payload": { "kind": "message", "data": { "content": "hello" } }
}

Unknown top-level fields are rejected (deny_unknown_fields everywhere).

The 14 seed kinds

Three categories, plus the open seam:

  • Participant-stream (7): message, question, answer, decision, action, assumption, summary — what a transcript decomposes into.
  • Knowledge/definition (4): evidence, definition, capability, intent — the "what is" set.
  • Process (3): plan, result, outcome — the "how / what-happened" set. outcome covers both structured domain results and structured errors (kind/message/details/exit_code).
  • Extension — any other kind tag, carried losslessly (Extension { kind, data }). This is how verticals and future kinds cross the wire without breaking older consumers.

The normative form

The contract's machine-checkable form is 16 curated draft-2020-12 JSON Schemas (lonis schema), validated both directions against golden fixtures in CI — see Schema Reference and Validation.

Typed Payloads and the Erased Seam

Decided in ADR-0002, from the karpal-discovery session's feedback.

The principle

Typing matters in-process; across a process/JSON boundary it's always JSON anyway. So maximize in-process typing, and erase only where erasure is unavoidable — a subprocess.

What it means concretely

Block is generic over its payload:

#![allow(unused)]
fn main() {
pub trait BlockPayload: Serialize + DeserializeOwned + Send + Sync + 'static {
    fn kind_name(&self) -> &str;
    fn schema_id(&self) -> String;
    fn render_human(&self) -> String;
}
}

A vertical defines its own payload enum — karpal-discovery's KarpalPayload — and gets a fully-typed Block<KarpalPayload>, Tool<KarpalPayload>, and ToolRegistry<KarpalPayload>. Zero erasure anywhere the vertical reaches.

Erasure reappears exactly once: at the umbrella host, where blocks arrive across a subprocess JSON channel and are parsed as SeedBlock (Block<BlockKind>). Unknown kinds land in BlockKind::Extension losslessly — the erased seam. Erasure is topological: on the boundary of the system, nowhere in the interior.

The derive

#[derive(BlockPayload)] makes the seam's two easy-to-get-wrong rules compile-time guarantees (ADR-0004):

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, lonis_schema::BlockPayload)]
#[lonis_payload(namespace = "karpal", render_fn = "render_search")]
enum KarpalPayload {
    Search { query: String, results: Vec<ItemSummary> },
    Ready,
}
// serde tag == kind_name() == "karpal.search" — from one declaration.
}
  • Payloads serialize adjacently tagged ({"kind", "data"}) — an internally-tagged enum hard-fails at the seam, and the derive makes that shape automatic.
  • Kind tags are namespaced (<vertical>.<kind>) so kinds never collide across verticals.
  • render_fn keeps a custom human render without giving up the derived wire safety.

Subprocess Tools: Bounded and Isolated

SubprocessTool hosts an arbitrary external CLI as a Lonis Tool — the thesis made concrete. Decided in ADR-0003.

The wire protocol

DirectionChannelShape
InstdinOne JSON value, then EOF
SuccessstdoutBlocks: JSON array, ndjson lines, or a single block object
Failurestderr + nonzero exitStructured ToolError JSON, propagated verbatim

Anything else on stderr maps to kind tool_failed with the child's exit code. Non-block stdout maps to invalid_output (exit 9).

Bounds are first-class

Every invocation runs under:

  • a hard wall-clock timeout (default 5 s),
  • byte caps on stdout (default 1 MiB) and stderr (default 256 KiB).

Exceeding either kills and reaps the child and reports LIMIT_EXCEEDED (exit 7).

Isolation

Following the amari-discovery probe blueprint: direct exec (no shell), a cleared environment (only PATH inherited, plus explicit additions), and a neutral working directory (the system temp dir, configurable).

Consequence for tool authors: targets arrive via stdin input or argv — never env or cwd.

Availability

A tri-state probe (Ready / Missing / NotExecutable + reason) — a tool the harness knows about may still be absent on this host. invoke on an unavailable tool fails without spawning.

Legacy CLIs

StdoutMapping::Text wraps raw stdout in an attributed result block with a pinned input_hash — grep/jq-style tools work today; prefer real blocks for anything you control.

Legacy text wrap

See Authoring Tools for the Subprocess Seam for the full protocol guide, including streaming (ndjson + flush) and the provider surface.

The Provider Model

One executable hosting many tools. Decided in ADR-0006.

The problem it solves

SubprocessTool binds one argv prefix to one tool. A vertical with a full operation set (search / detail / inspect / recommend / …) shouldn't register each one individually — the executable itself should describe its surface.

The four subcommands

mytool --mode json manifest              # {"name", "version", "tools": [...], ...}
mytool --mode json tools list            # {"provider", "tools": [{"name", "description"}]}
mytool --mode json tools describe <name> # a ToolContract JSON
mytool --mode json call <name>           # ADR-0003 invocation (stdin JSON → blocks)

SubprocessProvider discovers that surface and constructs a SubprocessTool per operation — with provider-wide bounds (timeout, byte caps, env, cwd) inherited by every tool.

Details that matter

  • Forward-compatible: manifests tolerate unknown fields — a host never rejects a newer provider for new metadata.
  • Name mangling: dotted v0 names (figma.get_document) become namespaced ToolIds (figma:get_document).
  • The same bounded, isolated execution core as SubprocessTool runs discovery and invocation.

lonis is a conforming provider

lonis manifest
lonis --mode json tools list

The harness hosting itself through the seam is the dogfood — and the integration test.

Stream Mode

Decided in ADR-0009.

The principle

Async is a property of the session orchestrator, not of the tool boundary. A subprocess is bytes on a pipe — synchronous by physics. Wallace-class hosts are async by nature, but that's the host layer. So Lonis streams synchronously and bridges at the seam.

The shape

  • BlockStream<P> — a pull iterator of Result<Block<P>, ToolError>. Tool::invoke_stream defaults to collect-then-stream (object-safe: no async fn in the trait). A terminal failure is the stream's final item; blocks delivered before it are kept.
  • SubprocessTool streams for real: ndjson stdout lines become blocks as they arrive; the supervisor enforces timeout and byte caps concurrently with delivery; stderr drains bounded for the terminal error mapping.
  • Backpressure is real: a bounded channel means a slow consumer backs up the pipe, which throttles the child.
  • Dropping the stream kills the child — an abandoned stream never leaves a running process.

Async hosts

BlockStream::into_async() (feature futures) yields a runtime-agnostic futures_core::Stream. The library never names a runtime; tokio is the host's choice.

For tool authors

Emit one block per line (ndjson) and flush after each line — language runtimes block-buffer stdout on pipes (Rust's println! included), so without an explicit flush your blocks arrive in one burst at exit.

CLI

lonis call <tool> '<input>' --stream --mode ndjson

ndjson/human render incrementally; --mode json buffers to one array (a valid JSON document can't be emitted incrementally).

Replay and Content Hashing

Decided in ADR-0001 §5 (content hashing), ADR-0007 (canonicalization policy), and ADR-0008 (verification).

Pins

Blocks carry replay provenance: typed hash slots (project_hash, input_hash, plan_hash, result_hash), an optional seed, and a replay { replayable, required_hashes, reasons } declaration.

Block::content_hash() is SHA-256 over the canonical payload JSON: object keys recursively sorted, and numbers normalized — integral floats collapse to integers (100.0, 1e2100), negative zero to zero. Semantically equal payloads hash identically across producers and languages.

Residual limitation: exotic float spellings (0.30000000000000004 vs 0.3) still hash differently. Hash-critical values should be integers or strings when cross-producer equality matters.

Verification

verify_replay(block, observed) -> ReplayStatus adjudicates a block's pins against the hashes observed now:

  • NotReplayable { reasons } — the producer declared it (e.g. environment-specific state);
  • Replayable — every required hash is present and equal;
  • Failed { missing, mismatches } — one combined report; unknown required field names fail closed (a v2 hash field can't silently pass an older consumer).

Recomputing the observed hashes (re-inspecting a project, re-canonicalizing an input with json_content_hash) is the vertical's business; the horizontal contract adjudicates equality.

Golden pins

The wire shape and the canonicalization are pinned by checked-in golden instances and hashes (tests/golden/blocks/) — a drift breaks CI, not a consumer. See Schema Reference and Validation.

The lonis CLI

lonis [--mode human|json|ndjson] <command>

Commands

CommandPurpose
lonis tools listRegistered tool ids and versions (--mode json → provider-list shape)
lonis tools describe <id>A tool's ToolContract (JSON)
lonis call <id> [input] [--stream]Invoke a tool
lonis schema [kind]Emit the curated JSON Schemas (lonis schema lists all 16)
lonis manifestThe provider manifest — lonis is a conforming provider

Input

call accepts three input forms, all explicit:

  • inline JSON: lonis call lonis:builtin:echo '{"a": 1}'
  • a file: lonis call lonis:builtin:echo @input.json
  • stdin (when the argument is omitted)

The amari split

  • stdout: blocks (a JSON array, one block per ndjson line, or human render) — always parseable.
  • stderr: a structured ToolError ({"kind", "message", "details?", "exit_code"}) — with the process exit code set from it.

Exit-code baseline: 0 ok, 1 generic, 2 invalid input, 3 not found, 4 confirmation required, 5 rate limited, 6 tool failed, 7 limit exceeded, 8 io, 9 serialization, 69 not implemented, 70 internal. Tools extend this map via Capabilities::exit_code_map.

Streaming

lonis call <id> <input> --stream --mode ndjson renders blocks as the tool produces them (see Stream Mode).

Authoring Tools for the Subprocess Seam

The full guide lives in the repository at docs/guides/subprocess-tool-authoring.md — this page is the summary.

The wire protocol

  • in: one JSON value on stdin, then EOF
  • out (success): blocks on stdout (array / ndjson / single object)
  • out (failure): structured ToolError on stderr + nonzero exit

The rules

  1. Payloads serialize adjacently tagged ({"kind", "data"}). Internal tagging hard-fails at the seam. In Rust, use #[derive(BlockPayload)]; in other languages, emit the two-key object.
  2. Kind tags are namespaced (<vertical>.<kind>) and equal to kind_name() — one declaration, no divergence.
  3. The minimal block requires schema_version, attribution (with provenance.when RFC 3339 + provenance.producer), and payload. Unknown top-level fields are rejected. Validate against lonis schema block.
  4. Targets arrive via stdin or argv — never env or cwd. The environment is cleared and the cwd neutral by design.
  5. You are bounded — hard timeout, byte caps; exceeding either kills your process. Keep payloads small; stream incrementally.
  6. Streaming: one block per line (ndjson), and flush after each line — runtimes block-buffer stdout on pipes.

Hosting many tools

Ship one executable with the provider surface (manifest / tools list / tools describe / call) — the host discovers your whole operation set. See The Provider Model.

Depending on Lonis pre-1.0-era

Since v0.1.0 the crates are on crates.io: lonis-schema = "0.1". Before publication, the discipline was git deps pinned to a rev, optional behind a feature — the pattern is preserved in the full guide for pre-release branches.

Feature Flags

Features are additive only — enabling a feature never removes API.

lonis-schema

FeatureEffect
deriveRe-exports the LonisCapabilities and BlockPayload derives from lonis-derive (serde-style)

lonis-core

FeatureEffect
futuresBlockStream::into_async() — a runtime-agnostic futures_core::Stream bridge (adds futures-core + futures-channel only; never a runtime)

lonis (facade)

FeatureDefaultEffect
coreRe-export the harness runtime (Tool, ToolRegistry, SubprocessTool, …)
deriveRe-export the derives
futuresPropagates lonis-core/futures

Schema Reference and Validation

The block contract's normative, machine-checkable form is 16 curated draft-2020-12 JSON Schemas, checked into crates/lonis-schema/schemas/ and emitted by the CLI.

lonis schema            # catalog: 16 families with their $ids
lonis schema block      # the envelope (payload = oneOf 14 kinds + extension)
lonis schema message    # one kind's payload schema
lonis schema extension  # the erased seam itself

Conventions

  • $ids mirror BlockKind::schema_id(): https://industrialalgebra.com/schemas/lonis.block/<kind>/v1
  • additionalProperties: false throughout — mirrors serde's deny_unknown_fields
  • maxItems/maxLength bounds on every collection and string — the doctrine's "bounded" property expressed in the wire contract itself
  • The extension branch admits any kind tag not in the seed enum, so vertical payloads validate against the envelope (structure here; data shape against the vertical's own schema)

The two-way pin

Schemas and golden fixtures validate each other in CI (crates/lonis-schema/tests/schemas.rs):

  • golden block instances (one per kind, produced from real Rust values) parse as SeedBlock and re-serialize identically — the wire-shape pin;
  • the same instances validate against the envelope and their kind schema — serde↔schema consistency;
  • each block's content_hash is pinned in hashes.json — the canonicalization pin.

Regenerating

# After an intentional per-kind schema change:
cargo run -p lonis-schema --example compose_block_schema

# After an intentional wire-shape change:
cargo run -p lonis-schema --example dump_golden_blocks

Both leave an auditable diff that a reviewer can judge.

Blocks and Payloads

From lonis-schema (re-exported by the lonis facade).

Block<P: BlockPayload>

The canonical structured domain object. Flat wire shape: schema_version, provenance, warnings, attribution, bounds, payload. Key methods:

  • Block::new(attribution, payload) — at the v1 contract
  • with_provenance / with_warnings / with_bounds — builders
  • schema_id() — the payload kind's stable $id
  • content_hash() — SHA-256 over canonical payload JSON
  • render_human() — render-parity human form

BlockPayload

The trait verticals implement (usually via the derive):

#![allow(unused)]
fn main() {
fn kind_name(&self) -> &str;
fn schema_id(&self) -> String;
fn render_human(&self) -> String;
}

BlockKind / SeedBlock

The 14-kind seed payload enum (implementing BlockPayload) plus the Extension { kind, data } catch-all. SeedBlock = Block<BlockKind> is the umbrella host's type.

Supporting types

  • Attribution { identity, viewpoint, provenance: AttributionSource { when, where, producer } }Attribution::new(identity, producer) stamps RFC 3339 UTC.
  • BlockBounds { max_items, max_bytes, max_length, timeout_millis } — all optional; a default (unbounded) set is omitted from the wire.
  • ReplayProvenance — tool version, compatibility, replay metadata, the four typed hash slots, seed.
  • verify_replay(block, observed) -> ReplayStatus — replay verification (see Replay and Content Hashing).
  • json_content_hash(&Value) — canonical hashing for arbitrary JSON (e.g. pinning input_hash).

Errors and identifiers

ToolError { kind, message, details?, exit_code } · ToolId(<tool>:<namespace>:<item>) · SchemaVersion(<name>/v<N>) · exit_code baseline constants.

Tool and ToolRegistry

From lonis-core.

Capabilities

Self-description — every tool implements it:

#![allow(unused)]
fn main() {
fn schema_version(&self) -> SchemaVersion;
fn tool_version(&self) -> &str;
fn output_formats(&self) -> &'static [OutputMode];
fn exit_code_map(&self) -> &'static [(&'static str, u8)];
fn tool_id(&self) -> ToolId;
}

Usually derived: #[derive(LonisCapabilities)] with #[lonis(tool_id = "...")].

Tool<P: BlockPayload>

#![allow(unused)]
fn main() {
fn invoke(&self, input: Value) -> Result<Vec<Block<P>>, ToolError>;
fn invoke_stream(&self, input: Value) -> Result<BlockStream<P>, ToolError> { /* default */ }
fn contract(&self) -> Option<ToolContract> { None }
}

invoke_stream defaults to collect-then-stream; tools with genuine incremental output override it.

ToolRegistry<P>

In-process registry, homogeneous in the payload type (a vertical's registry is fully typed). Deterministic order (sorted by id).

  • register(Box<dyn Tool<P>>)already_registered on duplicates
  • get(id) / iter() / len() / is_empty()
  • invoke(id, input) / invoke_stream(id, input)not_found (exit 3) on unknown ids

Rendering

  • render(&[Block<P>], mode, writer) — json array / ndjson lines / human
  • render_error(&ToolError, mode, writer)
  • run_tool(registry, id, input, mode) -> u8 — the amari split: blocks to stdout, structured error to stderr with its exit code
  • run_stream(registry, id, input, mode) -> u8 — the streaming variant

SubprocessTool and SubprocessProvider

From lonis-core.

SubprocessTool

Hosts one external CLI as one Tool<BlockKind>.

#![allow(unused)]
fn main() {
SubprocessTool::new(tool_id, command)
    .with_args(args)
    .with_timeout_millis(5_000)
    .with_max_stdout_bytes(1_048_576)
    .with_max_stderr_bytes(262_144)
    .with_cwd(path)
    .with_env(vars)
    .with_mapping(StdoutMapping::Blocks /* or Text */)
    .with_description(..).with_version(..)
}
  • availability() -> AvailabilityReady / Missing / NotExecutable
  • invoke — stdin JSON in; blocks or ToolError out; bounded and isolated (see Subprocess Tools)
  • invoke_stream — ndjson lines become blocks as they arrive (see Stream Mode)
  • contract() — a ToolContract (determinism Nondeterministic, side effects MutatesExternal by default)

SubprocessProvider

Discovers and hosts a whole surface from one executable.

#![allow(unused)]
fn main() {
let provider = SubprocessProvider::new("mytool");
let manifest = provider.manifest()?;      // ProviderManifest
let tools = provider.tools()?;            // Vec<ProviderToolSummary>
let contract = provider.describe("op")?;  // ToolContract
let tool = provider.tool("op");           // SubprocessTool (argv: call op)
}

Discovery args default to the v0 surface with --mode json and are overridable (with_manifest_args, with_tools_list_args); bounds set on the provider are inherited by constructed tools. Dotted names mangle to namespaced ToolIds.

Derive Macros

From lonis-derive, re-exported by lonis-schema behind its derive feature (and by the facade behind derive).

#[derive(LonisCapabilities)]

Generates the five-method Capabilities impl from one attribute:

#![allow(unused)]
fn main() {
#[derive(LonisCapabilities)]
#[lonis(tool_id = "amari:discovery:search")]
struct SearchTool;
}

Defaults: current protocol SchemaVersion, all three output modes, the baseline exit-code map, CARGO_PKG_VERSION.

#[derive(BlockPayload)]

Generates, from one enum declaration (ADR-0004):

  • adjacently-tagged {"kind", "data"} serde impls,
  • kind_name() — snake_case variant names, dot-namespaced via #[lonis_payload(namespace = "karpal")]karpal.search,
  • schema_id()lonis.block/<kind>/v1,
  • render_human() — a <kind>: <Debug> default, or a hook: render_fn = "path::to::render" (fn(&Self) -> String).

Supports struct variants (named fields) and unit variants (data: null). Tuple variants and generics are compile errors. Unknown kinds are rejected on the vertical's own enum (closed universe); cross-vertical tolerance lives host-side in BlockKind::Extension.

Consumers need serde and serde_json as dependencies; the enum must implement Debug when using the default render.

Architecture Decision Records

The ADRs are the case law of this workspace — every structural decision is recorded with its context and consequences. They live in docs/adr/:

ADRDecision
0001The block contract; invoke → Vec<Block>; extraction direction
0002Block<P: BlockPayload>; erasure is topological
0003The subprocess wire protocol (bounded, isolated)
0004#[derive(BlockPayload)] + the authoring guide
0005Curated JSON Schemas + golden fixtures (with the issue #10 erratum)
0006The provider surface; lonis-as-provider
0007Content-hash canonicalization (number normalization)
0008verify_replay — replay verification helper
0009Stream mode: sync pull core, async at the host

The upstream constitution is the Anima Ecosystem Doctrine §2.7 (block taxonomy) and §3 (Lonis as the Tools plane).

Changelog

All notable changes to the Lonis workspace are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[0.1.0] — Unreleased

First public release of the Lonis workspace: an AI-native tool harness for the Anima ecosystem. "MCP exposes servers to models; Lonis exposes tools to agents."

The contract (lonis-schema)

  • Block<P: BlockPayload> — the doctrine §2.7 structured domain object: flat wire shape (schema_version, provenance, warnings, attribution, bounds, payload), deny_unknown_fields throughout (ADR-0001/0002)
  • The 14-kind seed corpus (BlockKind) in three categories with a lossless Extension seam; verticals implement BlockPayload for fully-typed in-process contracts
  • Full Attribution (identity / viewpoint / when / where / producer), first-class BlockBounds, ReplayProvenance (a superset of amari-discovery's provenance, for the future extraction), and verify_replay (ADR-0008)
  • Canonical content hashing (SHA-256, key-sorted + number-normalized per ADR-0007) and render-parity (render_human from the same typed value)
  • 16 curated draft-2020-12 JSON Schemas (14 kinds + envelope + extension seam) with golden wire fixtures and pinned hashes (ADR-0005)
  • Namespaced-string SchemaVersion (<name>/v<N>) and the amari-aligned exit-code vocabulary

The runtime (lonis-core)

  • Tool<P> / ToolRegistry<P> — generic over the typed payload; a vertical's registry is homogeneous and fully typed (ADR-0002)
  • SubprocessTool — bounded, isolated external CLI adapter: stdin JSON in, blocks out, structured ToolError on stderr; hard timeout + byte caps kill and reap; availability tri-state (ADR-0003)
  • SubprocessProvider — one executable hosting many tools via manifest / tools list / tools describe / call; lonis itself is a conforming provider (ADR-0006)
  • Stream mode: BlockStream<P> pull iterator with real backpressure (bounded channel), drop-kills-child, and a runtime-agnostic futures_core::Stream bridge behind the futures feature (ADR-0009)

The macros (lonis-derive)

  • #[derive(LonisCapabilities)] from #[lonis(tool_id = "...")]
  • #[derive(BlockPayload)] — adjacently-tagged wire serde, namespaced kind tags, schema_id, and a render_fn hook, from one declaration (ADR-0004)

The CLI (lonis-cli)

  • lonis tools list|describe, lonis call <id> [input | @file] [--stream], lonis schema [kind], lonis manifest; --mode human|json|ndjson

The facade (lonis)

  • One crate re-exporting schema + derive + core, serde-style (core default; derive, futures opt-in)