summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-04 22:26:46 +0900
committerAdam Malczewski <[email protected]>2026-06-04 22:26:46 +0900
commitfd855ffb335e72a94b6f992ede5a859237460a8b (patch)
tree3833674b0957ddec1ed3b3e6140c89360e675037 /packages/kernel
parenta6119e0434597399c773da6f0b31363003f6aa09 (diff)
downloaddispatch-fd855ffb335e72a94b6f992ede5a859237460a8b.tar.gz
dispatch-fd855ffb335e72a94b6f992ede5a859237460a8b.zip
feat(kernel): define the ABI contracts (conversation, tool, provider, auth, dispatch, hooks, extension/HostAPI, runtime, events)
Diffstat (limited to 'packages/kernel')
-rw-r--r--packages/kernel/src/contracts/auth.ts48
-rw-r--r--packages/kernel/src/contracts/conversation.ts91
-rw-r--r--packages/kernel/src/contracts/dispatch.ts31
-rw-r--r--packages/kernel/src/contracts/events.ts122
-rw-r--r--packages/kernel/src/contracts/extension.ts255
-rw-r--r--packages/kernel/src/contracts/hooks.ts92
-rw-r--r--packages/kernel/src/contracts/index.ts95
-rw-r--r--packages/kernel/src/contracts/provider.ts116
-rw-r--r--packages/kernel/src/contracts/runtime.ts60
-rw-r--r--packages/kernel/src/contracts/tool.ts107
-rw-r--r--packages/kernel/src/index.ts8
11 files changed, 1024 insertions, 1 deletions
diff --git a/packages/kernel/src/contracts/auth.ts b/packages/kernel/src/contracts/auth.ts
new file mode 100644
index 0000000..6058156
--- /dev/null
+++ b/packages/kernel/src/contracts/auth.ts
@@ -0,0 +1,48 @@
+/**
+ * Auth contract — how a provider obtains credentials.
+ *
+ * Kept minimal and general. The common case is API-key + base-URL; richer
+ * flows (OAuth, token refresh) are handled by specific auth extensions that
+ * still resolve to credentials the provider can consume.
+ */
+
+/**
+ * The simplest credential set: an API key and optional base URL.
+ * This is the common case for OpenAI-compatible and most provider extensions.
+ */
+export interface ApiKeyCredentials {
+ readonly type: "api-key";
+ readonly apiKey: string;
+ readonly baseURL?: string;
+}
+
+/**
+ * Bearer token credentials (e.g. OAuth access tokens).
+ * The auth extension is responsible for refresh logic; the provider
+ * receives a currently-valid token.
+ */
+export interface BearerTokenCredentials {
+ readonly type: "bearer-token";
+ readonly token: string;
+ readonly baseURL?: string;
+}
+
+/** Union of credential shapes the kernel recognizes. */
+export type Credentials = ApiKeyCredentials | BearerTokenCredentials;
+
+/**
+ * What an auth extension registers with the kernel. A provider resolves its
+ * credentials through this contract — the kernel never touches secrets
+ * directly (the concrete vault is a core extension).
+ */
+export interface AuthContract {
+ /** Unique identifier for this auth provider (e.g. "apikey", "claude-oauth"). */
+ readonly id: string;
+
+ /**
+ * Resolve currently-valid credentials. May involve reading from the
+ * secret vault (injected via Host API) or performing a token refresh.
+ * Returns `null` if credentials are unavailable (e.g. not yet configured).
+ */
+ readonly resolve: () => Promise<Credentials | null>;
+}
diff --git a/packages/kernel/src/contracts/conversation.ts b/packages/kernel/src/contracts/conversation.ts
new file mode 100644
index 0000000..c9ad0eb
--- /dev/null
+++ b/packages/kernel/src/contracts/conversation.ts
@@ -0,0 +1,91 @@
+/**
+ * Conversation model — the kernel's representation of a dialogue.
+ *
+ * The kernel owns only the types and pure transforms. Persistence is a core
+ * extension (conversation-store). A turn is one user→assistant cycle; a step
+ * is one LLM round-trip within a turn. Chunks are append-only.
+ */
+
+/** Who produced a message. */
+export type Role = "system" | "user" | "assistant" | "tool";
+
+/** Opaque identifier for a turn (one user→assistant cycle). */
+export type TurnId = string & { readonly __brand: "TurnId" };
+
+/** Opaque identifier for a step (one LLM round-trip within a turn). */
+export type StepId = string & { readonly __brand: "StepId" };
+
+/**
+ * A chunk is one ordered piece of a message — the atomic unit of the
+ * append-only conversation log. Discriminated by `type`.
+ */
+export type Chunk =
+ | TextChunk
+ | ThinkingChunk
+ | ToolCallChunk
+ | ToolResultChunk
+ | ErrorChunk
+ | SystemChunk;
+
+/** A piece of plain text content from the assistant or user. */
+export interface TextChunk {
+ readonly type: "text";
+ readonly text: string;
+}
+
+/** A piece of model reasoning / thinking content (e.g. extended thinking). */
+export interface ThinkingChunk {
+ readonly type: "thinking";
+ readonly text: string;
+}
+
+/**
+ * A model's request to run a tool. The kernel routes by `name`; the tool
+ * implementation never sees this directly — it receives parsed `input` via
+ * `ToolContract.execute`.
+ */
+export interface ToolCallChunk {
+ readonly type: "tool-call";
+ readonly toolCallId: string;
+ readonly toolName: string;
+ readonly input: unknown;
+}
+
+/**
+ * The result of a tool execution, attributed to the originating tool-call id.
+ * The kernel guarantees every tool-call chunk gets exactly one result chunk
+ * (synthesized if interrupted — see reconcile).
+ */
+export interface ToolResultChunk {
+ readonly type: "tool-result";
+ readonly toolCallId: string;
+ readonly toolName: string;
+ readonly content: string;
+ readonly isError: boolean;
+}
+
+/** An error that occurred during generation or tool dispatch. */
+export interface ErrorChunk {
+ readonly type: "error";
+ readonly message: string;
+ readonly code?: string;
+}
+
+/**
+ * A system-injected message (e.g. system prompt, context assembly output).
+ * Kept distinct from text so the log records provenance.
+ */
+export interface SystemChunk {
+ readonly type: "system";
+ readonly text: string;
+}
+
+/**
+ * A chat message: a role plus an ordered sequence of chunks. Messages are the
+ * unit passed to and from the provider; chunks are the unit persisted and
+ * rendered.
+ */
+export interface ChatMessage {
+ readonly role: Role;
+ readonly chunks: readonly Chunk[];
+}
diff --git a/packages/kernel/src/contracts/dispatch.ts b/packages/kernel/src/contracts/dispatch.ts
new file mode 100644
index 0000000..c2914cf
--- /dev/null
+++ b/packages/kernel/src/contracts/dispatch.ts
@@ -0,0 +1,31 @@
+/**
+ * Dispatch policy — controls how the kernel runs a step's tool calls.
+ *
+ * Two orthogonal axes, split so every combination is coherent:
+ * - `maxConcurrent`: how many tools run at once (0=unlimited, 1=sequential, 2+=cap)
+ * - `eager`: when execution starts (true=on arrival, false=after step finishes)
+ *
+ * The policy is a kernel input, never ambient — the session-orchestrator
+ * resolves it and hands the final value to `runTurn`.
+ */
+
+/**
+ * Controls how the kernel dispatches tool calls within a step.
+ *
+ * **`maxConcurrent`:**
+ * - `0` — unlimited parallelism (deliberate opt-in; reopens the dedup footgun)
+ * - `1` — sequential execution (a concurrency limit of 1 is exactly serial)
+ * - `2+` — at most N tools run concurrently
+ *
+ * **`eager`:**
+ * - `true` — launch each tool-call the instant it streams in from the provider
+ * (overlaps tool execution with the rest of generation)
+ * - `false` — wait until the step's finish event, then dispatch the batch
+ *
+ * **Default:** `{ maxConcurrent: 1, eager: true }` — never two tools at once
+ * (safe for any tool), yet the first tool starts during generation.
+ */
+export interface ToolDispatchPolicy {
+ readonly maxConcurrent: number;
+ readonly eager: boolean;
+}
diff --git a/packages/kernel/src/contracts/events.ts b/packages/kernel/src/contracts/events.ts
new file mode 100644
index 0000000..ce1dae4
--- /dev/null
+++ b/packages/kernel/src/contracts/events.ts
@@ -0,0 +1,122 @@
+/**
+ * Outward events — the event type the runtime emits to the outside world.
+ *
+ * These are the events transport extensions push to clients, notification
+ * extensions react to, and conversation-store uses for persistence.
+ * Discriminated by `type`.
+ */
+
+import type { Usage } from "./provider.js";
+
+/**
+ * The union of all events the runtime emits outward during a turn.
+ * Consumers (transport, persistence, notifications) pattern-match on `type`.
+ */
+export type AgentEvent =
+ | StatusEvent
+ | TurnStartEvent
+ | TurnTextDeltaEvent
+ | TurnReasoningDeltaEvent
+ | TurnToolCallEvent
+ | TurnToolResultEvent
+ | TurnToolOutputEvent
+ | TurnUsageEvent
+ | TurnErrorEvent
+ | TurnDoneEvent
+ | TurnSealedEvent;
+
+/** Status change for a tab / session (e.g. idle → running). */
+export interface StatusEvent {
+ readonly type: "status";
+ readonly tabId: string;
+ readonly status: string;
+}
+
+/** A turn has begun. */
+export interface TurnStartEvent {
+ readonly type: "turn-start";
+ readonly tabId: string;
+ readonly turnId: string;
+}
+
+/** Incremental text content from the model during a turn. */
+export interface TurnTextDeltaEvent {
+ readonly type: "text-delta";
+ readonly tabId: string;
+ readonly turnId: string;
+ readonly delta: string;
+}
+
+/** Incremental reasoning / thinking content during a turn. */
+export interface TurnReasoningDeltaEvent {
+ readonly type: "reasoning-delta";
+ readonly tabId: string;
+ readonly turnId: string;
+ readonly delta: string;
+}
+
+/** The model has requested a tool to be run. */
+export interface TurnToolCallEvent {
+ readonly type: "tool-call";
+ readonly tabId: string;
+ readonly turnId: string;
+ readonly toolCallId: string;
+ readonly toolName: string;
+ readonly input: unknown;
+}
+
+/** A tool has completed execution. */
+export interface TurnToolResultEvent {
+ readonly type: "tool-result";
+ readonly tabId: string;
+ readonly turnId: string;
+ readonly toolCallId: string;
+ readonly toolName: string;
+ readonly content: string;
+ readonly isError: boolean;
+}
+
+/** Streaming output from a tool execution (e.g. shell stdout/stderr). */
+export interface TurnToolOutputEvent {
+ readonly type: "tool-output";
+ readonly tabId: string;
+ readonly turnId: string;
+ readonly toolCallId: string;
+ readonly data: string;
+ readonly stream: "stdout" | "stderr";
+}
+
+/** Token usage for the current step or turn. */
+export interface TurnUsageEvent {
+ readonly type: "usage";
+ readonly tabId: string;
+ readonly turnId: string;
+ readonly usage: Usage;
+}
+
+/** An error occurred during the turn. */
+export interface TurnErrorEvent {
+ readonly type: "error";
+ readonly tabId: string;
+ readonly turnId: string;
+ readonly message: string;
+ readonly code?: string;
+}
+
+/** The turn has completed (model finished generating). */
+export interface TurnDoneEvent {
+ readonly type: "done";
+ readonly tabId: string;
+ readonly turnId: string;
+ readonly reason: string;
+}
+
+/**
+ * The turn has been sealed — all chunks persisted, history is final.
+ * This is the hook point for post-turn extensions (compaction, cache-warm).
+ */
+export interface TurnSealedEvent {
+ readonly type: "turn-sealed";
+ readonly tabId: string;
+ readonly turnId: string;
+}
diff --git a/packages/kernel/src/contracts/extension.ts b/packages/kernel/src/contracts/extension.ts
new file mode 100644
index 0000000..ac252b4
--- /dev/null
+++ b/packages/kernel/src/contracts/extension.ts
@@ -0,0 +1,255 @@
+/**
+ * Extension model — manifest, extension lifecycle, and the Host API.
+ *
+ * An extension is a unit that contributes capabilities via the Host API.
+ * The manifest declares what it provides and what it requires. The Host API
+ * is the object an extension receives in `activate(host)` — it is the
+ * §2.3 surface through which all registration and service access flows.
+ */
+
+import type { AuthContract } from "./auth.js";
+import type {
+ EventHandler,
+ EventHookDescriptor,
+ FilterDescriptor,
+ FilterHandler,
+ ServiceHandle,
+} from "./hooks.js";
+import type { ProviderContract } from "./provider.js";
+import type { ToolContract } from "./tool.js";
+
+// --- Manifest ---
+
+/** Trust level of an extension, recorded for future policy differentiation. */
+export type TrustLevel = "bundled" | "local" | "external";
+
+/**
+ * What an extension contributes to the system. Used by the host for
+ * discovery, dependency resolution, and the capability gate.
+ */
+export interface ManifestContributions {
+ readonly tools?: readonly string[];
+ readonly providers?: readonly string[];
+ readonly auth?: readonly string[];
+ readonly hooks?: readonly string[];
+ readonly routes?: readonly string[];
+ readonly commands?: readonly string[];
+ readonly services?: readonly string[];
+ readonly migrations?: readonly string[];
+ readonly scheduledJobs?: readonly string[];
+ readonly settings?: readonly string[];
+}
+
+/**
+ * Capabilities an extension declares it needs. The host uses these for the
+ * capability gate — an extension can only access Host API surfaces it has
+ * declared capability for.
+ */
+export interface ManifestCapabilities {
+ readonly fs?: boolean;
+ readonly shell?: boolean;
+ readonly network?: boolean;
+ readonly secrets?: boolean;
+ readonly db?: boolean;
+ readonly spawn?: boolean;
+}
+
+/**
+ * An extension's declaration — what it is, what it provides, and what it
+ * requires. The host uses this to resolve dependency DAG, check apiVersion
+ * compatibility, and enforce the capability gate.
+ */
+export interface Manifest {
+ /** Unique extension identifier (e.g. "tools-fs", "provider-anthropic"). */
+ readonly id: string;
+
+ /** Human-readable display name. */
+ readonly name: string;
+
+ /** Extension's own version (semver). */
+ readonly version: string;
+
+ /** Semver range of kernel API versions this extension is compatible with. */
+ readonly apiVersion: string;
+
+ /** Ids of extensions this one depends on (resolved topologically). */
+ readonly dependsOn?: readonly string[];
+
+ /** Activation strategy: "eager" (on boot) or lazy event triggers. */
+ readonly activation?: "eager" | string;
+
+ /** What this extension contributes to the system. */
+ readonly contributes?: ManifestContributions;
+
+ /** Capabilities this extension requires from the host. */
+ readonly capabilities?: ManifestCapabilities;
+
+ /** Trust level — bundled (first-party), local (project), or external. */
+ readonly trust: TrustLevel;
+}
+
+// --- Storage interface ---
+
+/**
+ * Namespaced storage interface exposed through the Host API.
+ * The concrete backend (SQLite) is a core extension; the kernel defines
+ * only the contract. Supports key-value and simple query operations.
+ */
+export interface StorageNamespace {
+ readonly get: (key: string) => Promise<string | null>;
+ readonly set: (key: string, value: string) => Promise<void>;
+ readonly delete: (key: string) => Promise<void>;
+ readonly has: (key: string) => Promise<boolean>;
+ readonly keys: (prefix?: string) => Promise<readonly string[]>;
+}
+
+// --- Permission ---
+
+/** The outcome of a permission check. */
+export interface PermissionDecision {
+ readonly allowed: boolean;
+ readonly reason?: string;
+}
+
+/** A request to check whether an action is permitted. */
+export interface PermissionRequest {
+ readonly tool: string;
+ readonly action: string;
+ readonly context?: Readonly<Record<string, unknown>>;
+}
+
+/** Permission gate exposed through the Host API. */
+export interface PermissionGate {
+ readonly check: (request: PermissionRequest) => Promise<PermissionDecision>;
+}
+
+// --- Scheduler ---
+
+/** A scheduled job definition an extension can register with the host. */
+export interface ScheduledJob {
+ readonly id: string;
+ readonly cron: string;
+ readonly execute: () => void | Promise<void>;
+}
+
+// --- Logger ---
+
+/** Logger interface available to every extension via the Host API. */
+export interface Logger {
+ readonly debug: (message: string, ...args: unknown[]) => void;
+ readonly info: (message: string, ...args: unknown[]) => void;
+ readonly warn: (message: string, ...args: unknown[]) => void;
+ readonly error: (message: string, ...args: unknown[]) => void;
+}
+
+// --- Config ---
+
+/** Read-only config access for an extension's own settings namespace. */
+export interface ConfigAccess {
+ readonly get: <T = unknown>(key: string) => T | undefined;
+ readonly getAll: () => Readonly<Record<string, unknown>>;
+}
+
+// --- Secrets ---
+
+/** Capability-gated access to the secret/credential vault. */
+export interface SecretsAccess {
+ readonly get: (key: string) => Promise<string | null>;
+ readonly set: (key: string, value: string) => Promise<void>;
+ readonly delete: (key: string) => Promise<void>;
+}
+
+// --- Events emitter ---
+
+/** Outward event emitter available to extensions via the Host API. */
+export interface EventsEmitter {
+ readonly emit: (event: { readonly type: string; readonly [key: string]: unknown }) => void;
+}
+
+// --- Host API ---
+
+/**
+ * The Host API — what every extension receives in `activate(host)`.
+ *
+ * This is the §2.3 surface: the single object through which an extension
+ * registers tools, providers, auth, hooks, services, and accesses kernel
+ * services (storage, config, secrets, permissions, logging, scheduling).
+ *
+ * Method signatures are typed contracts; implementations live in the host
+ * module (not the kernel contracts).
+ */
+export interface HostAPI {
+ /** Register a tool with the kernel's tool registry. */
+ readonly defineTool: (tool: ToolContract) => void;
+
+ /** Register a provider with the kernel's provider registry. */
+ readonly defineProvider: (provider: ProviderContract) => void;
+
+ /** Register an auth provider with the kernel's auth registry. */
+ readonly defineAuth: (auth: AuthContract) => void;
+
+ /** Subscribe to an event hook. Handlers are error-isolated per call. */
+ readonly on: <TPayload>(
+ hook: EventHookDescriptor<TPayload>,
+ handler: EventHandler<TPayload>,
+ ) => () => void;
+
+ /** Add a filter to a filter hook chain. Filters are awaited in-band. */
+ readonly addFilter: <TValue>(
+ hook: FilterDescriptor<TValue>,
+ fn: FilterHandler<TValue>,
+ ) => () => void;
+
+ /** Provide an implementation for a typed service handle. */
+ readonly provideService: <T>(handle: ServiceHandle<T>, impl: T) => void;
+
+ /** Retrieve the implementation for a typed service handle. */
+ readonly getService: <T>(handle: ServiceHandle<T>) => T;
+
+ /** Get a namespaced storage interface for this extension. */
+ readonly storage: (namespace: string) => StorageNamespace;
+
+ /** Read-only access to merged config (global → project → extension). */
+ readonly config: ConfigAccess;
+
+ /** Capability-gated access to the secret/credential vault. */
+ readonly secrets: SecretsAccess;
+
+ /** Permission gate — check whether an action is allowed. */
+ readonly permissions: PermissionGate;
+
+ /** Emit outward events (transport pushes these to clients). */
+ readonly events: EventsEmitter;
+
+ /** Logger — always available, even before other extensions activate. */
+ readonly logger: Logger;
+
+ /** Register a scheduled job with the host's scheduler. */
+ readonly scheduler: {
+ readonly register: (job: ScheduledJob) => void;
+ };
+}
+
+// --- Extension lifecycle ---
+
+/**
+ * An extension — the unit that contributes capabilities to Dispatch.
+ * `activate` is called by the host during boot (or lazy activation);
+ * `deactivate` is optional and called on shutdown or reload.
+ */
+export interface Extension {
+ /** The extension's manifest — its declaration of identity and capabilities. */
+ readonly manifest: Manifest;
+
+ /**
+ * Called by the host to activate the extension. The extension registers
+ * its contributions (tools, providers, hooks, services) through the Host API.
+ */
+ readonly activate: (host: HostAPI) => void | Promise<void>;
+
+ /**
+ * Optional cleanup called when the extension is deactivated (shutdown,
+ * reload, or auto-disable). Should dispose resources the extension owns.
+ */
+ readonly deactivate?: () => void | Promise<void>;
+}
diff --git a/packages/kernel/src/contracts/hooks.ts b/packages/kernel/src/contracts/hooks.ts
new file mode 100644
index 0000000..eb94465
--- /dev/null
+++ b/packages/kernel/src/contracts/hooks.ts
@@ -0,0 +1,92 @@
+/**
+ * Hooks and services — typed cross-extension coupling anchors.
+ *
+ * Every cross-extension reference is anchored to an exported typed symbol
+ * (not a raw string), so `lsp references` can compute the true blast radius
+ * of a change. The id string lives in exactly one place: the owner's
+ * `defineHook` / `defineFilter` / `defineService` declaration.
+ *
+ * Two hook kinds:
+ * - **Event**: fire-and-forget, N listeners, error-isolated per handler.
+ * - **Filter**: ordered value-in→value-out chain, in-band, awaited.
+ *
+ * Services are NOT hooks — they are single-responder request/response.
+ */
+
+/**
+ * A typed descriptor for an event hook. The id string is the single source
+ * of truth; consumers import this symbol, never the raw string.
+ *
+ * Event hooks are fire-and-forget: N listeners, errors isolated per handler
+ * (a thrown handler is caught and logged — it never breaks the turn).
+ */
+export interface EventHookDescriptor<TPayload> {
+ readonly kind: "event";
+ readonly id: string;
+ readonly _payload?: TPayload;
+}
+
+/**
+ * A typed descriptor for a filter hook. Filters are an ordered chain:
+ * each receives the current value and returns the (possibly transformed)
+ * next value. Awaited in-band — a slow filter slows the turn, by design.
+ *
+ * Fail-open by default (a thrown filter logs and passes the value through);
+ * the owner may mark a chain fail-closed.
+ */
+export interface FilterDescriptor<TValue> {
+ readonly kind: "filter";
+ readonly id: string;
+ readonly _value?: TValue;
+}
+
+/** Union of hook descriptor kinds the kernel mechanism supports. */
+export type HookDescriptor<TPayload> = EventHookDescriptor<TPayload> | FilterDescriptor<TPayload>;
+
+/**
+ * A typed service handle. Services are single-responder request/response —
+ * NOT hooks. Modeling "ask the human for permission" as a hook invites
+ * "which of N handlers wins?" ambiguity; a service has exactly one provider.
+ */
+export interface ServiceHandle<T> {
+ readonly kind: "service";
+ readonly id: string;
+ readonly _type?: T;
+}
+
+/**
+ * Create a typed event hook descriptor. Pure function, no side effects.
+ * The returned object is the symbol consumers import and pass to `host.on`.
+ *
+ * @param id - Namespaced hook id in `owner/name` form (e.g. "kernel/turn.sealed").
+ */
+export function defineEventHook<TPayload>(id: string): EventHookDescriptor<TPayload> {
+ return { kind: "event", id };
+}
+
+/**
+ * Create a typed filter hook descriptor. Pure function, no side effects.
+ * The returned object is the symbol consumers import and pass to `host.addFilter`.
+ *
+ * @param id - Namespaced filter id in `owner/name` form.
+ */
+export function defineFilter<TValue>(id: string): FilterDescriptor<TValue> {
+ return { kind: "filter", id };
+}
+
+/**
+ * Create a typed service handle. Pure function, no side effects.
+ * The returned object is the symbol a provider passes to `host.provideService`
+ * and consumers pass to `host.getService`.
+ *
+ * @param id - Namespaced service id in `owner/name` form.
+ */
+export function defineService<T>(id: string): ServiceHandle<T> {
+ return { kind: "service", id };
+}
+
+/** Handler function for an event hook subscription. */
+export type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
+
+/** Transform function for a filter hook in the chain. */
+export type FilterHandler<TValue> = (value: TValue) => TValue | Promise<TValue>;
diff --git a/packages/kernel/src/contracts/index.ts b/packages/kernel/src/contracts/index.ts
new file mode 100644
index 0000000..1c3393f
--- /dev/null
+++ b/packages/kernel/src/contracts/index.ts
@@ -0,0 +1,95 @@
+/**
+ * Kernel contracts barrel — the stable ABI every extension compiles against.
+ *
+ * Re-exports all contract types and pure helpers. No implementations, no I/O,
+ * no concrete feature names. This is the only surface extensions depend on
+ * from the kernel.
+ */
+
+export type {
+ ApiKeyCredentials,
+ AuthContract,
+ BearerTokenCredentials,
+ Credentials,
+} from "./auth.js";
+export type {
+ ChatMessage,
+ Chunk,
+ ErrorChunk,
+ Role,
+ StepId,
+ SystemChunk,
+ TextChunk,
+ ThinkingChunk,
+ ToolCallChunk,
+ ToolResultChunk,
+ TurnId,
+} from "./conversation.js";
+export type { ToolDispatchPolicy } from "./dispatch.js";
+export type {
+ AgentEvent,
+ StatusEvent,
+ TurnDoneEvent,
+ TurnErrorEvent,
+ TurnReasoningDeltaEvent,
+ TurnSealedEvent,
+ TurnStartEvent,
+ TurnTextDeltaEvent,
+ TurnToolCallEvent,
+ TurnToolOutputEvent,
+ TurnToolResultEvent,
+ TurnUsageEvent,
+} from "./events.js";
+export type {
+ ConfigAccess,
+ EventsEmitter,
+ Extension,
+ HostAPI,
+ Logger,
+ Manifest,
+ ManifestCapabilities,
+ ManifestContributions,
+ PermissionDecision,
+ PermissionGate,
+ PermissionRequest,
+ ScheduledJob,
+ SecretsAccess,
+ StorageNamespace,
+ TrustLevel,
+} from "./extension.js";
+
+export type {
+ EventHandler,
+ EventHookDescriptor,
+ FilterDescriptor,
+ FilterHandler,
+ HookDescriptor,
+ ServiceHandle,
+} from "./hooks.js";
+
+export { defineEventHook, defineFilter, defineService } from "./hooks.js";
+export type {
+ FinishEvent,
+ ProviderContract,
+ ProviderErrorEvent,
+ ProviderEvent,
+ ProviderStreamOptions,
+ ProviderToolCallEvent,
+ ReasoningDeltaEvent,
+ TextDeltaEvent,
+ Usage,
+ UsageEvent,
+} from "./provider.js";
+export type {
+ EventEmitter,
+ RunTurnInput,
+ RunTurnResult,
+} from "./runtime.js";
+export type {
+ JsonSchemaProperty,
+ ToolCall,
+ ToolContract,
+ ToolExecuteContext,
+ ToolParameterSchema,
+ ToolResult,
+} from "./tool.js";
diff --git a/packages/kernel/src/contracts/provider.ts b/packages/kernel/src/contracts/provider.ts
new file mode 100644
index 0000000..1d0fd75
--- /dev/null
+++ b/packages/kernel/src/contracts/provider.ts
@@ -0,0 +1,116 @@
+/**
+ * Provider contract — how an LLM backend plugs into the kernel.
+ *
+ * The kernel is provider-agnostic: it knows only this streaming interface and
+ * the event taxonomy. A provider extension wraps a concrete LLM API and
+ * translates its responses into `ProviderEvent`s.
+ */
+
+import type { ChatMessage } from "./conversation.js";
+import type { ToolContract } from "./tool.js";
+
+/**
+ * Token usage counters for a single step. All fields are counts of tokens.
+ * Cache fields are optional because not all providers expose cache metrics.
+ */
+export interface Usage {
+ readonly inputTokens: number;
+ readonly outputTokens: number;
+ readonly cacheReadTokens?: number;
+ readonly cacheWriteTokens?: number;
+}
+
+/**
+ * Events a provider yields during a single `stream` call. The kernel consumes
+ * these to drive tool dispatch, build chunks, and emit outward `AgentEvent`s.
+ * Discriminated by `type`.
+ */
+export type ProviderEvent =
+ | TextDeltaEvent
+ | ReasoningDeltaEvent
+ | ProviderToolCallEvent
+ | UsageEvent
+ | FinishEvent
+ | ProviderErrorEvent;
+
+/** Incremental text content from the model. */
+export interface TextDeltaEvent {
+ readonly type: "text-delta";
+ readonly delta: string;
+}
+
+/** Incremental reasoning / thinking content from the model. */
+export interface ReasoningDeltaEvent {
+ readonly type: "reasoning-delta";
+ readonly delta: string;
+}
+
+/**
+ * A complete tool-call parsed by the provider. The kernel uses `name` to
+ * dispatch to the matching `ToolContract`.
+ */
+export interface ProviderToolCallEvent {
+ readonly type: "tool-call";
+ readonly toolCallId: string;
+ readonly toolName: string;
+ readonly input: unknown;
+}
+
+/** Token usage report, typically emitted at step end. */
+export interface UsageEvent {
+ readonly type: "usage";
+ readonly usage: Usage;
+}
+
+/**
+ * Signals the end of a step. `reason` indicates why the model stopped
+ * generating (e.g. "stop", "tool-calls", "length", "content-filter").
+ */
+export interface FinishEvent {
+ readonly type: "finish";
+ readonly reason: string;
+}
+
+/** An error from the provider (network, rate-limit, model error, etc.). */
+export interface ProviderErrorEvent {
+ readonly type: "error";
+ readonly message: string;
+ readonly code?: string;
+ readonly retryable?: boolean;
+}
+
+/**
+ * Options passed to a provider's `stream` method beyond messages and tools.
+ * Kept minimal — providers may ignore fields they don't support.
+ */
+export interface ProviderStreamOptions {
+ /** Model identifier to use. */
+ readonly model?: string;
+ /** Sampling temperature override. */
+ readonly temperature?: number;
+ /** Maximum output tokens override. */
+ readonly maxTokens?: number;
+ /** System prompt to prepend. */
+ readonly systemPrompt?: string;
+}
+
+/**
+ * What a provider extension registers with the kernel. The kernel calls
+ * `stream` and consumes the async iterable of events — it never knows which
+ * concrete LLM API is behind it.
+ */
+export interface ProviderContract {
+ /** Unique identifier for this provider (e.g. "anthropic", "openai-compat"). */
+ readonly id: string;
+
+ /**
+ * Stream a response for the given messages and available tools.
+ * The provider yields `ProviderEvent`s incrementally; the kernel drives
+ * tool dispatch and chunk assembly from them.
+ */
+ readonly stream: (
+ messages: readonly ChatMessage[],
+ tools: readonly ToolContract[],
+ opts?: ProviderStreamOptions,
+ ) => AsyncIterable<ProviderEvent>;
+}
diff --git a/packages/kernel/src/contracts/runtime.ts b/packages/kernel/src/contracts/runtime.ts
new file mode 100644
index 0000000..15f4869
--- /dev/null
+++ b/packages/kernel/src/contracts/runtime.ts
@@ -0,0 +1,60 @@
+/**
+ * Runtime contracts — the input/output types for `runTurn`.
+ *
+ * The kernel's turn loop is a pure function of these inputs. It takes
+ * messages as a plain input, returns result messages, and touches no DB.
+ * The implementation lives in the kernel runtime module; these types are
+ * the contract the session-orchestrator (core) programs against.
+ */
+
+import type { ChatMessage } from "./conversation.js";
+import type { ToolDispatchPolicy } from "./dispatch.js";
+import type { AgentEvent } from "./events.js";
+import type { ProviderContract, Usage } from "./provider.js";
+import type { ToolContract } from "./tool.js";
+
+/**
+ * The emitter function the kernel calls to push events outward.
+ * The session-orchestrator provides this, wiring it to transport + persistence.
+ */
+export type EventEmitter = (event: AgentEvent) => void;
+
+/**
+ * Input to `runTurn` — everything the kernel needs to execute one turn.
+ * All fields are resolved by the session-orchestrator before calling;
+ * the kernel never reads config or resolves providers/tools itself.
+ */
+export interface RunTurnInput {
+ /** The resolved provider to stream from. */
+ readonly provider: ProviderContract;
+
+ /** The conversation history (including system prompt as first message). */
+ readonly messages: readonly ChatMessage[];
+
+ /** The tool set available for this turn (may be empty). */
+ readonly tools: readonly ToolContract[];
+
+ /** How to dispatch tool calls within each step. */
+ readonly dispatch: ToolDispatchPolicy;
+
+ /** The emitter the kernel calls for each outward event. */
+ readonly emit: EventEmitter;
+
+ /** Cancellation signal for the entire turn. */
+ readonly signal?: AbortSignal;
+}
+
+/**
+ * The result of a completed turn. The session-orchestrator uses this to
+ * persist the new messages and report usage.
+ */
+export interface RunTurnResult {
+ /** The assistant messages produced by this turn (appended to history). */
+ readonly messages: readonly ChatMessage[];
+
+ /** Aggregated token usage across all steps in the turn. */
+ readonly usage: Usage;
+
+ /** Why the turn ended (e.g. "stop", "max-steps", "error", "aborted"). */
+ readonly finishReason: string;
+}
diff --git a/packages/kernel/src/contracts/tool.ts b/packages/kernel/src/contracts/tool.ts
new file mode 100644
index 0000000..f74ce77
--- /dev/null
+++ b/packages/kernel/src/contracts/tool.ts
@@ -0,0 +1,107 @@
+/**
+ * Tool contract — what a tool conforms to and what the kernel calls.
+ *
+ * The kernel never finds or names a concrete tool; it receives them via
+ * `runTurn` and dispatches by shape. A tool's `parameters` uses a structural
+ * JSON-Schema-like type so the kernel stays dependency-light (no zod).
+ * Extensions may use zod internally and convert to this shape.
+ */
+
+/**
+ * Structural JSON Schema subset for tool parameter declarations.
+ * The kernel does not validate against this — the provider serializes it for
+ * the model, and the tool implementation validates its own input.
+ * Using a structural type (not a library) keeps the kernel dependency-free.
+ */
+export interface ToolParameterSchema {
+ readonly type: "object";
+ readonly properties?: Readonly<Record<string, JsonSchemaProperty>>;
+ readonly required?: readonly string[];
+ readonly additionalProperties?: boolean;
+ readonly description?: string;
+}
+
+/** A single property within a tool's parameter schema. */
+export interface JsonSchemaProperty {
+ readonly type?: string;
+ readonly description?: string;
+ readonly enum?: readonly string[];
+ readonly items?: JsonSchemaProperty;
+ readonly properties?: Readonly<Record<string, JsonSchemaProperty>>;
+ readonly required?: readonly string[];
+ readonly default?: unknown;
+}
+
+/**
+ * Context passed to a tool's `execute` method. The kernel constructs this per
+ * call, attributing streaming output to the specific tool-call id so
+ * concurrent tool output is never interleaved ambiguously.
+ */
+export interface ToolExecuteContext {
+ /** Unique id of the tool-call this execution serves. */
+ readonly toolCallId: string;
+
+ /**
+ * Stream output from the tool. The kernel attributes every call to the
+ * tool-call id, so concurrent shell output from different tools is
+ * correctly separated.
+ */
+ readonly onOutput: (data: string, stream: "stdout" | "stderr") => void;
+
+ /**
+ * Cancellation signal. An aborted turn sets this so in-flight tool work
+ * can clean up rather than leak.
+ */
+ readonly signal: AbortSignal;
+}
+
+/**
+ * The value a tool returns from execution. Content is a string for
+ * provider-agnostic transport; `isError` flags failure so the model can
+ * react without the kernel interpreting the content.
+ */
+export interface ToolResult {
+ readonly content: string;
+ readonly isError?: boolean;
+}
+
+/**
+ * A tool-call as emitted by the provider and dispatched by the kernel.
+ * The kernel matches `name` against registered tools and passes `input`
+ * to the matched tool's `execute`.
+ */
+export interface ToolCall {
+ readonly id: string;
+ readonly name: string;
+ readonly input: unknown;
+}
+
+/**
+ * What a tool extension registers with the kernel via `host.defineTool`.
+ * The kernel calls `execute` blindly by shape — it never knows which
+ * concrete tools exist.
+ */
+export interface ToolContract {
+ /** Unique name the model uses to invoke this tool. */
+ readonly name: string;
+
+ /** Human-readable description shown to the model. */
+ readonly description: string;
+
+ /** JSON-Schema-ish parameter declaration (structural, no library dep). */
+ readonly parameters: ToolParameterSchema;
+
+ /**
+ * Execute the tool with parsed input. The kernel provides a per-call
+ * context (cancellation, output streaming, attribution).
+ */
+ readonly execute: (args: unknown, ctx: ToolExecuteContext) => Promise<ToolResult>;
+
+ /**
+ * Whether this tool is safe to run concurrently with other tools.
+ * When `false`, the kernel serializes this tool's calls even when the
+ * dispatch policy allows parallelism. Defaults to `true` if omitted.
+ * This overrides the global setting downward only (never widens parallelism).
+ */
+ readonly concurrencySafe?: boolean;
+}
diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts
index 3abab71..a9ff31f 100644
--- a/packages/kernel/src/index.ts
+++ b/packages/kernel/src/index.ts
@@ -1 +1,7 @@
-export const KERNEL_PLACEHOLDER = true;
+// @dispatch/kernel — the minimal runtime core.
+//
+// Exposes the ABI (contracts) that every extension and the runtime compile
+// against. Host, runtime, and bus implementations are added by their own
+// owner-agents and re-exported here as they land.
+
+export * from "./contracts/index.js";