monoloop 0.1.1

Monoloop: Connector + Interpreter + transaction-composing Loop (AGPL-3.0-or-later; commercial at frogfish.io)
Documentation

monoloop

Product façade for Monoloop: Connector + Interpreter + transaction-composing Loop.

Hosts should depend on this crate for plug-and-play assembly (cargo add monoloop). Profile connectors (Grok, Cursor, …) are optional Cargo features.

License: AGPL-3.0-or-later. Commercial: https://frogfish.io

Resource URL
docs.rs https://docs.rs/monoloop
Repository / normative doc/ https://github.com/frogfishio/monoloop
Homepage https://frogfish.io

What this crate is / is not

Is Is not
Re-exports of the three product components + contracts A fourth runtime component
Thin CLI (monoloop --version / --copyright) A chat UI or agent framework
Assembly entry for hosts Test kit (see monoloop-testkit)

Host integration (product hosts)

1. Smoke assembly (FakeConnector, no network)

cargo run -p monoloop --example fake_echo

Shape: ChannelBindingChannelRegistryRuntimeBootstrapsubmit.

2. Grok Build wiring (no testkit)

cargo run -p monoloop --example host_grok_wiring --features grok

Public binding signature (also on docs.rs / monoloop-connector-grok):

pub fn grok_channel_binding(
    id: impl AsRef<str>,
    endpoint_ref: impl Into<String>,     // "ws://127.0.0.1:2419"
    credential_ref: impl Into<String>,   // SecretResolver key name
    secrets: Arc<dyn SecretResolver>,    // host keychain adapter
    encoder: Arc<dyn OutboundDialectEncoder>, // AcpPromptEncoder::grok()
    interpreter: Arc<dyn InterpreterFactory>, // DefaultInterpreterFactory::new()
) -> ChannelBinding

Façade imports with --features grok:

  • monoloop::connector_grok::{grok_channel_binding, SecretResolver, SecretRef, …}
  • monoloop::loop_runtime::AcpPromptEncoder
  • monoloop::interpreter::DefaultInterpreterFactory

Live Driver/Console qualification stays in monoloop-testkit (live_grok_*). Product hosts must not depend on testkit.

3. Multi-turn history (host-built)

user_text_input("…") is a one-line helper. For chat journals, build CanonicalInput yourself:

use monoloop::contracts::{CanonicalInput, CanonicalMessage, InputLimits, TextPart};

let limits = InputLimits::default();
let input = CanonicalInput::try_new(
    vec![
        CanonicalMessage::User { content: vec![TextPart::try_new("Hi", limits.max_text_part_bytes)?], name: None },
        CanonicalMessage::Assistant { content: vec![TextPart::try_new("Hello!", limits.max_text_part_bytes)?], tool_calls: vec![] },
        CanonicalMessage::User { content: vec![TextPart::try_new("Continue.", limits.max_text_part_bytes)?], name: None },
    ],
    &limits,
)?;

Monoloop does not own durable history. The host maps journal → CanonicalMessage::{System,User,Assistant,Tool} and submits one transaction at a time. Resume Grok with explicit session_id: Some(SessionId::from_external(&ExternalSessionId::try_new(grok_session_id)?)) (never ambient “last session”).

4. Live text path = complete canonical units only

There is no token / delta stream API. UI should render from push events:

TransactionEventPayload::CanonicalUnit(CanonicalUnitEvent) — complete sentences / structures / tool lifecycle units. InterpretationEnd / EOF alone ≠ turn success; wait for the completion callback / Ended payload.

5. Tokio Handle in Tauri / desktop hosts

RuntimeBootstrap.executor is a tokio::runtime::Handle. Supported pattern:

  1. At app startup, build a dedicated multi-thread Tokio runtime and keep it for process life.
  2. Pass runtime.handle().clone() (or Handle::current() inside that runtime) into RuntimeBootstrap.
  3. Drive submit / async work on that runtime.

#[tokio::main] is fine for CLI samples; embedded hosts (Tauri) should own the runtime explicitly. Set RuntimeConfig { enable_mcp_listener: false, ..Default::default() } unless you want the MCP shell (Default enables it).

Hard rules

  • Do not depend on monoloop-testkit from product crates.
  • Do not invent ambient “current session”.
  • Empty tools (HostToolRegistry::empty(), tools: vec![]) → tool_unavailable, zero effects.
  • Canonical completeness ≠ authorization.

Agent assembly recipe (copy this shape)

1. ChannelBinding { connector_factory, encoder, interpreter, capabilities, … }
2. ChannelRegistry::build(vec![binding])
3. DefaultTransactionRuntime::start(RuntimeBootstrap {
     config: RuntimeConfig { enable_mcp_listener: false, ..Default::default() },
     channels, tools: HostToolRegistry::empty(), executor
   })
4. TransactionRuntime::submit(TransactionRequest { … push events + completion … })
5. Await the one completion callback

Module map

Use From
monoloop::contracts::* identities, ports, CanonicalInput, sinks
monoloop::connector::* FakeConnector / abstract Connector
monoloop::interpreter::* DefaultInterpreterFactory
monoloop::loop_runtime::* runtime, registry, encoders
monoloop::connector_grok::* Grok profile (features = ["grok"])

TestTextEncoder (smoke only)

Loop-owned deterministic encoder for FakeConnector + test_raw. Not a production Channel encoder. Live hosts use profile *_channel_binding.

Optional Channel profiles

monoloop = { version = "0.1", features = ["grok"] }
# also: cursor, codex, agy, zai, claude

Version / license helpers

use monoloop::{version_string, copyright_notice};
println!("{}", version_string());
println!("{}", copyright_notice());

Normative specs (public repo): doc/README.md, doc/MONOLOOP.md, doc/TRANSACTION_RUNTIME_IMPLEMENTATION.md, doc/GROK_BUILD_CONNECTOR.md.