remote/protocol

Use the remote protocol when Lash crosses a service boundary. The lash-remote-protocol crate defines versioned, serde-compatible DTOs for turns, process admin, LLM calls, activities, prompt layers, trigger occurrences and subscriptions, and remote tool grants; the facade reexports the DTO surface as lash::remote.

What This Is

A stable data contract for hosts that wrap Lash behind HTTP, queues, callbacks, workflow handlers, or another process. It is not a server implementation and it does not own auth, tenant routing, billing, rate limits, or storage.

Service Boundary

The DTOs are for host-owned service edges. If your code calls Lash in-process, use the normal facade in API basics instead.

remote edge

A web service, worker, workflow activity, queue consumer, remote provider, or host-owned tool service must exchange Lash-owned fields with another process.

workflow host

For Restate, Temporal, or another workflow runtime, key the handler by the stable turn_id or operation id and run the turn with .turn_id(...).effects(&controller). Do not add a second durable submitted/running work-item lifecycle around the turn; the workflow runtime owns in-flight replay/cancellation, Lash owns the session execution lease and final turn commit, and product storage owns user-visible rows.

local call

The caller already has a LashCore and LashSession in memory. Do not serialize and deserialize just to call local turns.

Minimal Examples

Wrap Lash's DTO in your host envelope. Service-specific fields belong in the wrapper or in metadata, not as local forks of Lash sub-objects.

use lash::remote::REMOTE_PROTOCOL_VERSION;
use lash::remote::turn_input::{RemoteInputItem, RemoteTurnInput, RemoteTurnRequest};

let request = RemoteTurnRequest {
    protocol_version: REMOTE_PROTOCOL_VERSION,
    session_id: chat_id.clone(),
    turn_id: turn_id.clone(),
    idempotency_key: Some(idempotency_key.clone()),
    input: RemoteTurnInput {
        protocol_version: REMOTE_PROTOCOL_VERSION,
        items: vec![RemoteInputItem::Text {
            text: "Summarize this task.".to_string(),
        }],
        image_blobs_base64: Default::default(),
        protocol_turn_options: None,
        trace_turn_id: Some(trace_turn_id.clone()),
        prompt_layer: None,
    },
    tool_grants: Vec::new(),
    model_intent: None,
    metadata: Default::default(),
};

request.validate()?;

Process starts use the same rule. The optional env_spec is a closed Lash DTO: plugin-owned options plus runtime policy. It is not a product metadata bag. A start also carries a required disposition (RemoteRecoveryDisposition) — the declared recovery contract that mirrors RecoveryDisposition across the wire (ADR 0019). It has no wire default, so an envelope that omits it does not deserialize; an external placeholder like the one below declares ExternallyOwned.

use std::collections::BTreeMap;

use lash::remote::REMOTE_PROTOCOL_VERSION;
use lash::remote::processes::{
    RemoteProcessExecutionEnvSpec, RemoteProcessExecutionPolicy, RemoteProcessInput,
    RemoteProcessModelLimits, RemoteProcessModelSpec, RemoteProcessOriginator,
    RemoteProcessPluginOptions, RemoteProcessStartRequest, RemoteRecoveryDisposition,
};
use serde_json::json;

let request = RemoteProcessStartRequest {
    protocol_version: REMOTE_PROTOCOL_VERSION,
    id: "process-01".to_string(),
    input: RemoteProcessInput::External {
        metadata: json!({ "source": "scheduler" }),
    },
    disposition: RemoteRecoveryDisposition::ExternallyOwned,
    env_spec: Some(RemoteProcessExecutionEnvSpec {
        plugin_options: RemoteProcessPluginOptions {
            plugins: BTreeMap::from([(
                "snapshot-tools".to_string(),
                json!({ "snapshot_ref": "tool-authority:sha256:abc123" }),
            )]),
        },
        policy: RemoteProcessExecutionPolicy {
            provider_id: "example-provider".to_string(),
            model: RemoteProcessModelSpec {
                id: "example-model".to_string(),
                variant: None,
                limits: RemoteProcessModelLimits {
                    context_window_tokens: 128_000,
                    output_token_capacity: Some(8_192),
                },
            },
            ..Default::default()
        },
    }),
    originator: RemoteProcessOriginator::Host,
    wake_target: None,
    grant: None,
    event_types: Vec::new(),
};

request.validate()?;

Contract Map

Use the smallest envelope that matches the boundary you are crossing.

turn ingress

RemoteTurnRequest carries session_id, turn_id, optional idempotency key, RemoteTurnInput, remote tool grants, optional model intent, and metadata.

turn result

RemoteTurnResult carries status, RemoteTurnOutcome, safe/raw assistant output, complete RemoteUsage buckets, execution summary, tool-call summaries, issues, collected activities, and metadata.

llm transport

RemoteLlmRequest and RemoteLlmResponse mirror the provider-facing request/response shape: messages, attachments, tools, tool choice, output spec, generation options, request metadata, complete usage buckets, diagnostics, terminal reason, and provider metadata.

activity stream

RemoteTurnActivity wraps semantic turn events with monotonically assigned per-stream sequence, event id, correlation id, and one flattened RemoteTurnEvent. The wire enum is a deliberately narrower mirror of the in-process TurnEvent: four host-internal variants collapse into a single RuntimeDiagnostic { kind, data } so the versioned contract does not churn on every runtime-internal signal. It is still a turn convenience item, not the reconnect cursor. See Streaming and reconnect for NDJSON and SSE framing guidance, and Reporting channels for how the mirror relates to the other reporting surfaces.

session observation

RemoteSessionCursor, RemoteSessionObservation, RemoteSessionObservationEvent, and RemoteLiveReplayGap are the remote reconnect surface. They carry opaque cursor strings, session revisions, bounded replay events, explicit remote observation snapshots, and gap recovery without serializing a full SessionReadView. See Streaming and reconnect for the host/frontend folding recipe.

prompt layer

RemotePromptLayer, prompt templates, slots, contributions, and gates let a service pass prompt overrides without inventing a parallel prompt schema.

trigger ingress

RemoteTriggerOccurrenceRequest, RemoteTriggerOccurrenceRecord, RemoteTriggerEmitReport, RemoteTriggerSubscriptionFilter, RemoteTriggerRegistration, and RemoteTriggerTargetSummary mirror trigger emission, stored occurrence replay, match reports, subscription listing, and target summaries. Subscription targets use RemoteProcessDefinitionIdentity, the same full module/host/process-ref/process-name identity used by process summaries and filters.

process admin

RemoteProcessStartRequest, process records, summaries, event DTOs, work snapshots, await/cancel/signal requests, and list filters mirror durable process admin. Process summaries and list filters use RemoteProcessDefinitionIdentity when they refer to a Lashlang process definition. RemoteProcessStartRequest.env_spec is typed as RemoteProcessExecutionEnvSpec, a closed contract containing only plugin_options and policy.

Remote Tools

Remote tool grants expose callable host operations through Lash's normal tool catalog while execution happens elsewhere.

grant

RemoteToolGrant defines the advertised name, schemas, projection overrides, output contract, examples, activation, scheduling, retry policy, and Lashlang call-path bindings. Host-owned bindings decide how granted tools execute.

executor

The remote protocol describes granted callable tools; it does not define a remote tool execution transport. Tool execution is host-owned: implement a normal ToolProvider, and call HTTP services, queues, or callbacks inside that provider when your deployment needs remote work.

call path

A binding in the grant's bindings map resolves to a RemoteCallPathBinding whose module_path plus operation forms the remote call path. Duplicate call paths are rejected by RemoteToolGrant::validate_all(...).

Validation Rules

The DTOs validate protocol and shape, not caller authorization. Keep auth and tenancy checks in your service layer.

version

Top-level envelopes reject unsupported protocol_version; nested turn input/activity versions must match the parent envelope where applicable.

required ids

Remote turn requests/results require non-empty session_id and turn_id. LLM requests/responses require non-empty request_id. Model intents require non-empty model.

messages

LLM messages must contain at least one content block. Tool-call blocks require call_id and tool_name; tool-result blocks require call_id.

attachments

Attachments require a non-empty MIME type. Attachment references require non-empty id and mime. Image blobs require non-empty ids and base64 payloads.

generation

RemoteGenerationOptions.output_token_cap must be greater than zero when present. Temperature and top-p are strings so provider-specific precision can round-trip.

triggers

Trigger occurrence requests require non-empty source_type, source_key, and idempotency_key. Trigger subscription filters validate any supplied RemoteProcessDefinitionIdentity target. Trigger filters and emit reports validate protocol version but leave authorization and tenant scoping to the boundary host.

process env

Process start environments are closed: env_spec accepts only plugin_options and policy. Unknown fields, including old product metadata such as tool_grants or resolved_tool_bindings under env_spec, fail deserialization. Model limits must be positive when supplied.

remote tools

Remote tool grants require a non-empty id and name, plus non-empty keys on any bindings present; schemas and bindings are optional. Duplicate module_path / operation call paths are rejected by RemoteToolGrant::validate_all(...).

Core Conversions

The standalone crate has an optional core-conversions feature for adapters that already depend on lash-core. The facade's lash::remote reexport is for DTOs; import lash-remote-protocol directly when you need conversion helpers.

turns

Conversions cover remote-safe TurnInput fields and RemoteTurnResult::from_core(...) for completed turns plus collected activities.

llm

RemoteLlmRequest::from_core(...), request conversion, and response conversion preserve messages, attachments, tools, output specs, terminal reasons, all usage buckets, replay metadata, and provider metadata where the core types carry them.

streaming

RemoteTurnActivitySink<W> serializes remote activities as newline-delimited JSON and records sink write errors for the host to inspect. It is a DTO adapter, not an HTTP route; the host still owns transport, auth, and reconnect policy.

triggers

Conversions cover trigger occurrence requests, stored occurrence records, emit reports, subscription filters, registrations, and target summaries when the standalone crate is built with core-conversions.

processes

Conversions cover process inputs, records, events, summaries, admin requests, and typed RemoteProcessExecutionEnvSpec when the standalone crate is built with core-conversions.

Where Next

This page owns the remote DTO contract. Use the facade guides for local embedding behavior, process architecture docs for durable execution, and the provider docs for transport normalization.

read on ·