# Runtime Contract

This document describes the execution contract of `sandlet-sdk`.

It is intentionally narrow. The runtime is a one-shot executor for the `main` entrypoint of a Roto script, returning a single string output.

## Overview

A run consists of:

1. choosing script source
2. passing zero or more string arguments
3. executing `main` once
4. returning a single string output value
5. providing an `ExecutionContext` to the running script

## Request Model

The runtime accepts a `RunRequest` with these fields:

- `script`
  The script source to compile.
- `entrypoint`
  Must be `"main"`. Any other value (including empty or whitespace-padded strings) is rejected.
- `args`
  Zero or more string arguments passed to the entrypoint.
- `limits`
  Runtime limits used for script-size validation, output capping, and context construction.

Over gRPC, the entrypoint is always `"main"` and is not configurable by the caller.

### Script Source

`script` is one of:

- `ScriptSource::File { path }`
- `ScriptSource::Inline { source }`

For file-based scripts, the SDK reads metadata from the target path and checks the file size against
`limits.max_script_bytes`.

For inline scripts, the SDK writes the source into a temporary directory before compiling it and removes that temporary
directory after the run completes.

### Entrypoint

The entrypoint is fixed to `main`. The supported entrypoint shapes are:

```text
fn main() -> String
fn main(args: List<String>) -> String
```

If `main` does not exist or does not match one of the supported callable shapes, the run fails with a structured diagnostic with category `RuntimeContract` and code `entrypoint-signature-mismatch`.

If the runtime resolves a zero-argument `main` and the request contains arguments, the run fails with a `RuntimeContract` diagnostic with code `entrypoint-arguments-mismatch`.

### Limits

`RunLimits` contains:

| field | default |
|-------|---------|
| `max_script_bytes` | `64 * 1024` |
| `max_args` | `64` |
| `max_arg_bytes` | `64 * 1024` |
| `max_output_bytes` | `64 * 1024` |
| `timeout_s` | `0` (no timeout) |
| `max_temp_bytes` | `64 * 1024` |
| `metadata` | `None` |

- `max_script_bytes` — enforced for both file-backed and inline script sources.
- `max_output_bytes` — enforced after the entrypoint returns.
- `timeout_s` — enforced by the SDK on a cooperative, best-effort basis. `0` means no timeout.
- `max_temp_bytes` — enforced during inline-script temp-directory creation. File-backed scripts are not affected.
- `metadata` — opaque per-run string forwarded into `ExecutionContext`. `None` means unset; `Some("")` means explicitly empty. Over gRPC, the proto field is `optional string`, preserving that distinction.

## Result Model

`RunResult` contains:

- `output`
  The returned string value.
- `diagnostics`
  Optional `RunDiagnostics` with structured `Diagnostic`s and `LogEntry`s collected during the run.

## Failure Semantics

All errors are represented by a single `Error` type with a `kind: ErrorKind` field that identifies the failure domain, plus an optional list of structured `Diagnostic`s.

Over gRPC, the same failure domain is serialized twice on purpose:

- `RunFailure.kind` carries the machine-readable top-level `ErrorKind`
- each attached `Diagnostic` keeps its own machine-readable `Category`

### Error kinds

| `ErrorKind` | Meaning | Typical codes |
|---|---|---|
| `InvalidRequest` | Bad arguments, missing required fields, naming violations | `empty-entrypoint`, `whitespace-entrypoint`, `invalid-entrypoint`, `too-many-args`, `oversized-argument` |
| `Compile` | Roto compilation failure | `roto-compile-error` |
| `RuntimeContract` | Signature mismatch, spec registration mismatch | `entrypoint-signature-mismatch`, `entrypoint-arguments-mismatch`, `spec-registration-mismatch` |
| `RuntimeExecution` | Unexpected failure during script execution | — |
| `Transport` | gRPC server or network failure | — |
| `ResourceExhausted` | Limits exceeded (script bytes, output, temp space) | `script-too-large`, `output-too-large`, `temp-space-exhausted` |
| `Cancelled` | Operation cancelled by caller, system, or timeout | `execution-cancelled`, `timeout-exceeded` |
| `Internal` | SDK bug or unexpected state | `context-type-registration-failed`, `unexpected-registration-error` |
| `Panic` | Unrecoverable panic from a script or module | — |

### Diagnostic categories

When a diagnostic is derived from the top-level error, its `Category` matches the error's `ErrorKind` directly:

| `ErrorKind` | `Category` |
|---|---|
| `InvalidRequest` | `InvalidRequest` |
| `Compile` | `Compile` |
| `RuntimeContract` | `RuntimeContract` |
| `RuntimeExecution` | `RuntimeExecution` |
| `Transport` | `Transport` |
| `ResourceExhausted` | `ResourceExhausted` |
| `Cancelled` | `Cancelled` |
| `Internal` | `Internal` |
| `Panic` | `Panic` |

### Specific failure paths

Failure cases by source:

- **Request validation**: invalid entrypoint, too many args, oversized arguments — `InvalidRequest`
- **Script loading**: script-size or temp-space limit violations — `ResourceExhausted`; file I/O failures — `Internal`
- **Roto compile**: compilation errors — `Compile` with a `roto-compile-error` diagnostic
- **Entrypoint lookup**: missing or wrong-signature entrypoints — `RuntimeContract`
- **Entrypoint argument mismatch**: zero-arg entrypoint called with args — `RuntimeContract`
- **Output limit exceeded**: `max_output_bytes` exceeded — `ResourceExhausted` with `output-too-large` diagnostic
- **Module registration**: duplicate registration or spec mismatch — `RuntimeContract`
- **Script panic**: unrecoverable panic — `Panic`
- **Timeout exceeded**: cooperative runtime deadline reached — `Cancelled` with `timeout-exceeded` diagnostic
- **Execution cancelled**: caller-provided cancellation token was cancelled — `Cancelled` with `execution-cancelled` diagnostic
- **gRPC transport**: server bind/startup failures — `Transport`

Each failure may produce one or more structured diagnostics with a machine-readable `code` field. Over gRPC, callers should branch on `RunFailure.kind` for the primary failure domain and use diagnostic categories and codes for more detail.

## Execution Guarantees

The runtime guarantees:

- one-shot execution only
- fresh runtime registration for each run
- module registration before compilation
- cleanup of temporary inline-script files after execution
- reliable execution-local cleanup via module `close()` (configured via `.on_close()`) called after execution terminates (even on panics)
- module validation before agent construction (duplicate names, naming contract violations, and empty names are rejected at build time)
- agent metadata derives from validated module specs

The runtime does not guarantee:

- hard interruption of pure CPU-bound Roto code that never reaches an SDK or host-function checkpoint
- session reuse

## Cancellation And Timeout

Timeout and cancellation are cooperative. The SDK creates a per-run cancellation token when execution starts and checks it at SDK-controlled boundaries:

- before and after script loading
- during module registration
- before and after compilation
- before and after the entrypoint call

The SDK represents cancellation as `ErrorKind::Cancelled` with either `timeout-exceeded` or `execution-cancelled`.

The timeout clock starts at the beginning of `Engine::run_once`, so request validation, script loading, module registration, compilation, execution, and output validation are all part of the same operation budget.

Pure Roto code or registered host functions that run for a long time may only observe cancellation after the entrypoint returns, because the current in-process runtime path has no safe hard-stop mechanism.

gRPC client disconnects or request cancellation now map to the same `RunCancellation` token used by the runtime. Cancellation remains cooperative: pure Roto code or long host-function calls still stop at the next SDK checkpoint, not via hard interruption.

### `ExecutionContext`

`ExecutionContext` is constructed from `RunLimits` and passed to the Roto script via `roto::Context`.

Public fields (module authors should treat them as **read-only**):

- `timeout_s: u32` — run timeout hint in seconds (`0` = no limit).
- `max_output_bytes: u64` — cap on the entrypoint output string.
- `max_temp_bytes: u64` — cap on temporary file space.
- `metadata: Arc<str>` — opaque per-run metadata. Empty when unset by the caller.

Helper methods:

- `deadline() -> Option<Instant>` — returns the runtime deadline when one exists.
- `is_cancelled() -> bool` — reports explicit cancellation or an expired deadline.
- `check_cancelled() -> Result<()>` — returns a structured cancellation error when cancellation has been requested or the deadline has expired.

A fresh `ExecutionContext` is created for every run, so there is no cross-run leakage.

The current `register_fn` host-function API does not pass `ExecutionContext` into registered functions. A future context-aware host-function API should use `check_cancelled()` as the standard cooperative checkpoint.

## Describe Surface

The SDK exposes agent metadata through `Agent::metadata()`:

- `agent_name` — the name assigned at build time
- `sdk_version` — SDK crate/runtime build version (from `CARGO_PKG_VERSION`)
- `modules` — registered module metadata, including per-module capability labels
- `declared_limits` — default runtime limits the SDK enforces (string-keyed map)
- `metadata` — operator/runtime inspection metadata (string-keyed map)

These fields are part of the gRPC `DescribeResponse` wire format. `declared_limits` and `metadata` use `map<string, string>` on the wire so values are transport-safe and deterministic.

## Execution Structure

The execution path is split into layers:

- `Engine` — compiles and executes scripts.
- `AgentService` — owns transport-agnostic `Describe` and `RunOnce` behavior.
- gRPC transport — adapts requests into the service layer.

This keeps the runtime core separate from transport concerns.

## Intentionally Unsupported

The runtime contract does not support:

- non-string return values
- streaming input or output
- long-lived sessions
