glm-5.3-flash 62f04c6856 docs(agents): spawned subagents never commit; coordinator commits once
A subagent mid-dedash sweep read the 'commit and push when reasonable'
rule and pushed its own subset before the coordinator verified the
aggregate. State the assembly rule explicitly so spawned tasks report
instead.
2026-11-01 14:30:23 +00:00
2026-08-14 12:41:38 +00:00
2026-08-14 12:41:38 +00:00

alktty

Terminal session protocol for the alk/tty ALPN: a JSON negotiation frame followed by raw chunks ([stream_type: u8][length: u32 be][payload]), a backend-agnostic TtyBackend trait, a backend-agnostic producer adapter, and a typed consumer client.

A producer/consumer protocol crate on top of alkcall channels: no networking, no transport dependencies. Downstream crates (a alknet-docker or alknet-ssh backend, a sandboxed TS/Python adapter) compose on top of it: implement TtyBackend for your execution environment, and the adapter handles the wire.

Quick start

Producer: register backends and run the adapter

use std::collections::HashMap;
use std::sync::Arc;

// `LocalTtyBackend` needs the `local` feature; swap in any `TtyBackend`
// implementation (docker, ssh, ...) compiled for your target.
let mut backends: HashMap<String, Arc<dyn alktty::TtyBackend>> = HashMap::new();
backends.insert("local".into(), Arc::new(alktty::LocalTtyBackend::new()));

let tty_adapter = alktty::TtyAdapter::new(backends);
// tty_adapter implements alkcall::core::ProtocolHandler on the `alk/tty`
// ALPN; hand it to your transport accept loop:
//   tty_adapter.handle(connection, &auth).await

Producer via alk/channels instead of a dedicated ALPN

use std::collections::HashMap;
use std::sync::Arc;

let backends: HashMap<String, Arc<dyn alktty::TtyBackend>> =
    std::iter::once(("local".to_string(), Arc::new(alktty::LocalTtyBackend::new()) as _))
        .collect();

// In your channels acceptor's `install_channel_zero` hook (per-connection):
//   alktty::register_openable(&channel_core, Arc::new(backends), None, &mut registry, auth)
// Consumers open `channels/tty/sub`; the open op's params ARE the
// negotiation (ADR-009); no second frame on the channel stream.

Consumer: open a session over the direct ALPN

use alkcall::core::Connection;
use alktty::{NegotiateRequest, TtySession};

let connection: Connection = /* dial the transport, negotiate `alk/tty` */;
let session = TtySession::connect_direct(
    connection,
    NegotiateRequest {
        carriage: "raw".into(),
        backend: "local".into(),
        tty: None,
        cmd: vec!["bash".into()],
        cwd: None,
        env: Default::default(),
        backend_params: Default::default(),
    },
).await?;

session.resize(120, 40, 0, 0).await?;
session.send_stdin("ls -l\n".into()).await?;
session.close_stdin().await?;

let mut stdout = session.recv_stdout().await;
while let Some(bytes) = futures::StreamExt::next(&mut stdout).await {
    // The stream ends on the zero-length stdout sentinel ("drained");
    // it is never yielded as an item.
    // ... render bytes ...
}
let code = session.wait().await?;

Consumer: open a session via alk/channels

use alkcall::channels::client::ChannelClient;
use alktty::TtySession;

let client: ChannelClient = /* from_connection on an `alk/channels` connection */;
let session = TtySession::open_via_channels(
    &client,
    serde_json::json!({
        "carriage": "raw",
        "backend": "local",
        "cmd": ["bash"],
        "tty": {"term": "xterm-256color", "cols": 120, "rows": 40,
                "pixel_width": 0, "pixel_height": 0}
    }),
).await?;

Architecture

Two roles, symmetric per the alk convention (avoid "server"/"client": both sides of a connection can be either):

Role Direct alk/tty Via alk/channels
Producer (runs backends, emits terminal bytes) TtyAdapter as a ProtocolHandler: negotiation frame → access control → backend allocation → three-pump session driver register_openable: the registry's ACL gates the open op; the handler spawns the same session driver pre-negotiated
Consumer (types input, renders output) TtySession::connect_direct, which writes the negotiation frame and exposes typed stdin/stdout/stderr/control/exit methods TtySession::open_via_channels, where the open op's params are the negotiation

One connection hosts many sessions (one per bidi stream on the direct path, one per channel on the channels path); sessions are independent.

The wire format (ADR-001) is two carriages: a 4-byte length-prefixed JSON negotiation frame, then raw chunks of the form [stream_type: u8][length: u32 be][payload] with five stream types (STREAM_STDIN=0 … STREAM_CTRL_OUT=4; see alktty::wire). Zero-length data chunks are sentinels (stdin EOF from the client, stdout drained from the server); the exit code rides as the final ctrl_out control chunk (ADR-005). The codec is alktty::wire's ChunkReader/ChunkWriter; the machine-readable contract is the BAST document at docs/architecture/tty-bast.md.

WASM target

The default crate (no features) compiles to wasm32-unknown-unknown; this is what makes the downstream TS/Python adapter story work: a wasm-compiled alktty is the protocol layer for a sandboxed adapter. The local feature (the LocalTtyBackend reference implementation over portable_pty + tokio::process) needs a real OS and is non-wasm by design.

Documentation

  • Architecture docs, the authoritative spec: ADRs (001–009), the wire format, the backend trait contract, and the local backend's cancel-cleanup semantics.
  • API docs: full crate documentation on docs.rs.

License

MIT OR Apache-2.0

S
Description
No description provided
Readme
2.3 MiB
0 Stars 6 Watchers 0 Forks
Languages
Rust 96.3%
Python 3%
Shell 0.7%