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()KitRuntime
Section titled “KitRuntime”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}emit(event)
Section titled “emit(event)”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.emitteddiagnostic.
const event = runtime.emit({ type: 'note.saved', context: { surface: 'editor' }, payload: { id: 'n_1' },})// event.id, event.timestamp are filled inon(eventType, handler)
Section titled “on(eventType, handler)”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()command(command)
Section titled “command(command)”Dispatches a command. Returns a promise that resolves to a CommandResult<T>.
- The handler registered for
command.typeruns. - If no handler, returns
{ ok: false, error: Error }and emitscommand.unhandled. - If the handler throws, returns
{ ok: false, error }and emitscommand.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)}handleCommand(commandType, handler)
Section titled “handleCommand(commandType, handler)”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 }})install(module)
Section titled “install(module)”Installs a module:
- Runs
module.setup?.(runtime). - Subscribes any
events. - Registers any
commands. - Emits
module.installed.
Returns when setup completes.
start()
Section titled “start()”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>.
stop()
Section titled “stop()”Calls module.stop?.(runtime) for every installed module, in reverse order. Emits runtime.stopped.
The shell calls this on disconnectedCallback.
provide(token, value)
Section titled “provide(token, value)”Registers a provider. If the token already has a value, emits a provider.overwritten diagnostic before replacing.
inject(token)
Section titled “inject(token)”Returns the value registered for the token, or undefined. No throw — the caller decides how to handle missing providers.
createBoundary(options)
Section titled “createBoundary(options)”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.
onDiagnostic(handler)
Section titled “onDiagnostic(handler)”Subscribes to the diagnostic stream. Returns an unsubscribe function.
runtime.onDiagnostic((d) => { console.debug('[kit]', d.type, d.detail)})KitEventInput / KitEvent
Section titled “KitEventInput / KitEvent”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()}KitCommandInput / CommandResult
Section titled “KitCommandInput / CommandResult”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 }KitContext
Section titled “KitContext”type KitContext = { surface?: string surfaces?: string[] feature?: string entity?: Entity [key: string]: unknown // arbitrary additional fields are preserved}
type Entity = { type: string; id: string }KitModule
Section titled “KitModule”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>}ProviderToken
Section titled “ProviderToken”type ProviderToken<T> = { id: symbol description: string readonly type?: T // for type inference; never set at runtime}KitDiagnostic
Section titled “KitDiagnostic”type KitDiagnostic = { type: string timestamp: number detail?: Record<string, unknown>}Handler signatures
Section titled “Handler signatures”type EventHandler = (event: KitEvent) => void | Promise<void>type CommandHandler<T = unknown> = (command: KitCommandInput) => T | Promise<T>type DiagnosticHandler = (diagnostic: KitDiagnostic) => voidtype Unsubscribe = () => voidHelpers
Section titled “Helpers”defineKitModule(module)
Section titled “defineKitModule(module)”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.
createProviderToken(description)
Section titled “createProviderToken(description)”const StorageToken = createProviderToken<StorageService>('storage')Creates a unique token. The description shows up in diagnostics.
normalizeMetadata(input)
Section titled “normalizeMetadata(input)”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.
Reading the source
Section titled “Reading the source”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.