diff options
| author | Adam Malczewski <[email protected]> | 2026-06-04 22:26:46 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-04 22:26:46 +0900 |
| commit | fd855ffb335e72a94b6f992ede5a859237460a8b (patch) | |
| tree | 3833674b0957ddec1ed3b3e6140c89360e675037 /packages/kernel | |
| parent | a6119e0434597399c773da6f0b31363003f6aa09 (diff) | |
| download | dispatch-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.ts | 48 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/conversation.ts | 91 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/dispatch.ts | 31 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/events.ts | 122 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/extension.ts | 255 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/hooks.ts | 92 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/index.ts | 95 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/provider.ts | 116 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/runtime.ts | 60 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/tool.ts | 107 | ||||
| -rw-r--r-- | packages/kernel/src/index.ts | 8 |
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"; |
