AGPL-3.0-or-later · forever.

observability · live

Read the run while it happens.

Consume the engine NDJSON journal as an AsyncIterable, then settle on one typed outcome and keep the trace as proof.

The event stream is the run journal in motion. Your UI can react to it, while the engine remains the sole owner of task state, terminal verdicts and the trace on disk.

Keep the handle whole.

The handle is both an AsyncIterable and a promise-backed result. Consume the journal, then await the final exit contract.

const handle = nika.run('pipeline.nika.yaml')

for await (const event of handle) {
  renderEvent(event)
}

const result = await handle.outcome

Treat the vocabulary as additive.

The local event type guarantees kind and leaves the rest open. Handle the kinds your product needs and keep an unknown branch so a newer engine does not break an older client.

function renderEvent(event: { kind: string; [key: string]: unknown }) {
  switch (event.kind) {
    case 'task_started':
    case 'task_completed':
    case 'workflow_completed':
      console.log(event)
      break
    default:
      console.debug('new engine event', event.kind)
  }
}

Let the caller own cancellation.

Pass an AbortSignal into run. The child process receives the cancellation through the SDK process seam, while the outcome still records how the run settled.

const controller = new AbortController()
const handle = nika.run('pipeline.nika.yaml', {
  signal: controller.signal,
})

setTimeout(() => controller.abort(), 30_000)

Use the trace for history.

A UI stream is transient presentation. The hash-chained trace is the durable receipt for review, resume and replay after the process is gone.

const verdict = await nika.traceVerify()
if (!verdict.intact) {
  throw new Error(verdict.output)
}