Skip to content

Runtime API

The runtime is the kernel of Kitsune. It’s framework-neutral and dependency-free.

import { createKitRuntime } from '@atheory-ai/kitsune-core'
const runtime = createKitRuntime()
type KitRuntime = {
// events
emit(event: KitEventInput): KitEvent
on(eventType: string, handler: EventHandler): Unsubscribe
// commands
command<T>(command: KitCommandInput): Promise<CommandResult<T>>
handleCommand<T>(commandType: string, handler: CommandHandler<T>): Unsubscribe
// module lifecycle
install(module: KitModule): Promise<void>
start(): Promise<void>
stop(): Promise<void>
// providers
provide<T>(token: ProviderToken<T>, value: T): void
inject<T>(token: ProviderToken<T>): T | undefined
// boundaries
createBoundary(options: BoundaryOptions): BoundaryHandle
// diagnostics
onDiagnostic(handler: DiagnosticHandler): Unsubscribe
}

Emits an event synchronously. Returns the stamped event (with id and timestamp).

  • Calls every handler subscribed to event.type, then every handler subscribed to '*'.
  • Errors thrown by one handler do not stop others.
  • Emits a event.emitted diagnostic.
const event = runtime.emit({
type: 'note.saved',
context: { surface: 'editor' },
payload: { id: 'n_1' },
})
// event.id, event.timestamp are filled in

Subscribes a handler to an event type. Pass '*' to observe every event.

Returns an unsubscribe function.

const unsubscribe = runtime.on('note.saved', (event) => {
console.log(event)
})
unsubscribe()

Dispatches a command. Returns a promise that resolves to a CommandResult<T>.

  • The handler registered for command.type runs.
  • If no handler, returns { ok: false, error: Error } and emits command.unhandled.
  • If the handler throws, returns { ok: false, error } and emits command.failed.
  • On success, returns { ok: true, value }.
const result = await runtime.command<{ id: string }>({
type: 'draft.save',
payload: { content: '...' },
})
if (result.ok) {
console.log('saved with id', result.value?.id)
}

Registers a command handler. The latest registered handler wins (overrides previous ones).

Returns an unsubscribe function.

runtime.handleCommand('draft.save', async (command) => {
await fetch('/api/drafts', { method: 'POST', body: JSON.stringify(command.payload) })
return { saved: true }
})

Installs a module:

  1. Runs module.setup?.(runtime).
  2. Subscribes any events.
  3. Registers any commands.
  4. Emits module.installed.

Returns when setup completes.

Calls module.start?.(runtime) for every installed module, in order. Emits runtime.started.

The shell calls this on connectedCallback. Call manually if you create a runtime outside <kit-shell>.

Calls module.stop?.(runtime) for every installed module, in reverse order. Emits runtime.stopped.

The shell calls this on disconnectedCallback.

Registers a provider. If the token already has a value, emits a provider.overwritten diagnostic before replacing.

Returns the value registered for the token, or undefined. No throw — the caller decides how to handle missing providers.

Creates a runtime-level boundary handle (separate from <kit-boundary> DOM elements). Returns a BoundaryHandle with a destroy() method.

Useful when integrating with frameworks that want to manage boundary lifetimes manually. Most apps use the DOM boundary instead.

Subscribes to the diagnostic stream. Returns an unsubscribe function.

runtime.onDiagnostic((d) => {
console.debug('[kit]', d.type, d.detail)
})

Full diagnostic catalog →

type KitEventInput = {
type: string
context?: KitContext
source?: Record<string, unknown>
entity?: Entity
interaction?: Record<string, unknown>
payload?: Record<string, unknown>
}
type KitEvent = KitEventInput & {
id: string // crypto.randomUUID()
timestamp: number // Date.now()
}
type KitCommandInput<TPayload = Record<string, unknown>> = {
type: string
context?: KitContext
source?: Record<string, unknown>
interaction?: Record<string, unknown>
payload?: TPayload
}
type CommandResult<T> =
| { ok: true; value?: T }
| { ok: false; error: Error }
type KitContext = {
surface?: string
surfaces?: string[]
feature?: string
entity?: Entity
[key: string]: unknown // arbitrary additional fields are preserved
}
type Entity = { type: string; id: string }
type KitModule = {
name: string
setup?: (runtime: KitRuntime) => void | Promise<void>
start?: (runtime: KitRuntime) => void | Promise<void>
stop?: (runtime: KitRuntime) => void | Promise<void>
events?: Record<string, EventHandler>
commands?: Record<string, CommandHandler>
}
type ProviderToken<T> = {
id: symbol
description: string
readonly type?: T // for type inference; never set at runtime
}
type KitDiagnostic = {
type: string
timestamp: number
detail?: Record<string, unknown>
}
type EventHandler = (event: KitEvent) => void | Promise<void>
type CommandHandler<T = unknown> = (command: KitCommandInput) => T | Promise<T>
type DiagnosticHandler = (diagnostic: KitDiagnostic) => void
type Unsubscribe = () => void
import { defineKitModule } from '@atheory-ai/kitsune-core'
const module = defineKitModule({
name: 'my-module',
events: { 'note.saved': (event) => {} },
})

Pure type helper — returns the input unchanged. Use it so editors infer module types correctly.

const StorageToken = createProviderToken<StorageService>('storage')

Creates a unique token. The description shows up in diagnostics.

const { event, command } = normalizeMetadata({
eventType: 'note.saved',
commandType: 'dialog.close',
context: { surface: 'editor' },
payload: { target: 'confirm' },
})

Used internally by the boundary; exported for adapters that build their own metadata bridges.

The whole runtime is one file: packages/core/src/index.ts. It’s around 250 lines and is meant to be readable in one sitting.