Actana

Streaming

NDJSON envelope spec — log, step, result, error lines from a sandbox run.

A sandbox run streams NDJSON — one JSON object per line, no SSE framing on the wire. The chat backend (stream-processor.ts + envelope-emitter.ts) parses each line and translates it to SSE for the browser. The Agents block forwards NDJSON straight into the workflow run stream.

The line types are defined as the discriminated union AssistantSandboxNDJSONLine in lib/assistant-sandbox/envelope.ts.

Envelope schema

Every line has a type and an optional ts (Unix epoch ms). Unknown type values are rejected defensively by the chat-backend transformer.

log

Free-form diagnostic line. Not surfaced in the chat thread by default.

{ "type": "log", "level": "info", "message": "Loaded 3 skills", "ts": 1700000000000 }

level is debug | info | warn | error. The chat backend filters logs out of the SSE stream; they appear in run traces only.

step

One iteration of the agent loop. Emitted at least twice per step:

  • once with status: 'running' when the call starts,
  • once with status: 'succeeded' | 'failed' when it ends.

The frontend correlates by id so the same row updates rather than appending duplicates.

{
  "type": "step",
  "id": "step_8f3...",
  "name": "recipes/add-node.sh",
  "status": "running",
  "args": { "nodeType": "inference" },
  "ts": 1700000000123
}
{
  "type": "step",
  "id": "step_8f3...",
  "name": "recipes/add-node.sh",
  "status": "succeeded",
  "result": { "nodeId": "node_..." },
  "durationMs": 412,
  "ts": 1700000000535
}

name is the tool / recipe / skill the step ran. args is the model-emitted call arguments; result is the tool's return value (truncated client-side for display). On failed, error carries the message.

result

The final assistant message body. Emitted exactly once per turn, after the last step.

{ "type": "result", "message": "Added an Inference node…", "ts": 1700000000999 }

The chat backend persists this onto the conversation as the assistant message and emits a terminating SSE event. The Agents block surfaces it on the block's response output.

error

Terminal failure. Emitted in place of result when the agent loop cannot produce a final message. Closes the stream.

{ "type": "error", "code": "model_timeout", "message": "Provider timed out after 60s", "ts": 1700000001000 }

Wire flow

sandbox container

  │  newline-delimited JSON lines on response body

POST {SANDBOX_URL}/stream

  │  raw NDJSON ReadableStream

caller (Assistant chat / Agents block)

  ├─ Assistant: NDJSON → SSE (envelope-emitter.ts) → browser EventSource
  └─ Agents block: NDJSON forwarded into the workflow run stream

Cancellation

Both callers wire AbortSignal into the upstream fetch:

  • Assistant: client EventSource close → server request.signal aborts → provisionAssistantSandbox cancels the upstream fetch → sandbox container terminates.
  • Agents block: workflow cancellation → ctx.abortSignalagents-handler.ts passes signal into fetch(...).

When the upstream fetch aborts mid-stream, the next NDJSON line read fails and the caller cleans up. Any in-progress sandbox work is lost — there is no checkpoint protocol.

Cancellation does not refund the upload OTP. If the run was already producing assets when cancelled, those assets are not written back; the OTP simply expires unused.

Replay limit

The Assistant sends the last 20 turns to the sandbox per dispatch (REPLAY_TURN_LIMIT in app/api/assistant/chat/stream/route.ts). Older history is summarized client-side or omitted; do not rely on the sandbox seeing the entire conversation.

Source

  • apps/actana/lib/assistant-sandbox/envelope.ts — line type definitions
  • apps/actana/lib/assistant-sandbox/stream-processor.ts — NDJSON → SSE transform
  • apps/actana/lib/assistant-sandbox/envelope-emitter.ts — SSE shaping helpers

On this page