diff options
Diffstat (limited to 'packages/kernel/src')
| -rw-r--r-- | packages/kernel/src/contracts/extension.ts | 20 | ||||
| -rw-r--r-- | packages/kernel/src/host/host.test.ts | 52 | ||||
| -rw-r--r-- | packages/kernel/src/host/host.ts | 7 |
3 files changed, 78 insertions, 1 deletions
diff --git a/packages/kernel/src/contracts/extension.ts b/packages/kernel/src/contracts/extension.ts index 1760cf9..5a7821b 100644 --- a/packages/kernel/src/contracts/extension.ts +++ b/packages/kernel/src/contracts/extension.ts @@ -196,6 +196,26 @@ export interface HostAPI { fn: FilterHandler<TValue>, ) => () => void; + /** + * Run a filter chain: thread `value` through every filter registered for + * `hook` in priority/registration order and return the final value. The + * single-value-in/value-out counterpart to `addFilter`. Awaited in-band. + * + * Fail-open by default (a thrown filter is logged and the value passes + * through unchanged); pass `{ failClosed: true }` to make a thrown filter + * reject. With no registered filters the input value is returned as-is. + * + * This is what lets a core extension expose a contribution point (e.g. the + * session-orchestrator running a per-turn tool/context-assembly chain) that + * standard extensions plug into via `addFilter` — the kernel owns the + * mechanism, the owner declares the typed `FilterDescriptor`. + */ + readonly applyFilters: <TValue>( + hook: FilterDescriptor<TValue>, + value: TValue, + opts?: { readonly failClosed?: boolean }, + ) => Promise<TValue>; + /** Provide an implementation for a typed service handle. */ readonly provideService: <T>(handle: ServiceHandle<T>, impl: T) => void; diff --git a/packages/kernel/src/host/host.test.ts b/packages/kernel/src/host/host.test.ts index 106dd56..669d093 100644 --- a/packages/kernel/src/host/host.test.ts +++ b/packages/kernel/src/host/host.test.ts @@ -15,7 +15,7 @@ import type { SecretsAccess, StorageNamespace, } from "../contracts/extension.js"; -import { defineEventHook, defineService } from "../contracts/hooks.js"; +import { defineEventHook, defineFilter, defineService } from "../contracts/hooks.js"; import type { Attributes, ErrorAttributes, @@ -617,6 +617,39 @@ describe("createHost", () => { expect(received).toEqual(["hello"]); }); + it("applyFilters threads a value through registered filters in order", async () => { + const hook = defineFilter<string>("test/text-transform"); + + const ext = createExtension("filter-ext", { + activate: (host) => { + host.addFilter(hook, (value) => `${value}-first`); + host.addFilter(hook, (value) => `${value}-second`); + }, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const api = host.getHostAPI(); + const result = await api.applyFilters(hook, "start"); + expect(result).toBe("start-first-second"); + }); + + it("applyFilters returns the input unchanged when no filters are registered", async () => { + const hook = defineFilter<string>("test/unused-filter"); + + const ext = createExtension("no-filter-ext", { + activate: () => {}, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const api = host.getHostAPI(); + const result = await api.applyFilters(hook, "unchanged"); + expect(result).toBe("unchanged"); + }); + it("storage delegates to the factory", async () => { let storageResult: StorageNamespace | undefined; @@ -925,6 +958,23 @@ describe("createHost", () => { "Registration not available after activation", ); }); + + it("applyFilters is available on registration-closed HostAPI", async () => { + const hook = defineFilter<string>("test/closed-filter"); + + const ext = createExtension("filter-ext", { + activate: (host) => { + host.addFilter(hook, (value) => `${value}-filtered`); + }, + }); + + const host = createHost([ext], deps); + await host.activate(); + + const api = host.getHostAPI(); + const result = await api.applyFilters(hook, "input"); + expect(result).toBe("input-filtered"); + }); }); describe("auto-scoped logger (D6)", () => { diff --git a/packages/kernel/src/host/host.ts b/packages/kernel/src/host/host.ts index 8aa4f78..a6396a9 100644 --- a/packages/kernel/src/host/host.ts +++ b/packages/kernel/src/host/host.ts @@ -125,6 +125,13 @@ export function createHost(extensions: readonly Extension[], deps: HostDeps): Ho addFilter<TValue>(hook: FilterDescriptor<TValue>, fn: FilterHandler<TValue>) { return deps.bus.addFilter(hook, fn); }, + async applyFilters<TValue>( + hook: FilterDescriptor<TValue>, + value: TValue, + opts?: { readonly failClosed?: boolean }, + ): Promise<TValue> { + return deps.bus.applyFilters(hook, value, opts); + }, provideService<T>(handle: ServiceHandle<T>, impl: T) { deps.bus.provideService(handle, impl); }, |
