From f6b45507210e04e9884256b0132900640de4334b Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 10 Jun 2026 17:03:23 +0900 Subject: feat(skills): skill system + load_skill tool via per-turn tools filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skills are markdown in .skills/ dirs (~/.skills + /.skills, cwd shadows home; name = filename). Format: line1 summary, line2 ---, body line3+; load strips the first two lines; malformed = no summary but still loadable. Mechanism (first use of the context-assembly filter chain, ยง3.2): - kernel: expose HostAPI.applyFilters (delegates to bus.applyFilters) - session-orchestrator: define/export toolsFilter + ToolAssembly; apply once per turn before runTurn (cache-stable across steps), threading cwd + conversationId - skills (new ext): pure parse/merge/render + load_skill tool (live read, path-contained) + a toolsFilter filter rewriting load_skill's description + name enum per cwd - host-bin: register skills in CORE_EXTENSIONS - transport-http: fix HostAPI test stub for the new applyFilters method (fan-out) 734 vitest + 109 bun = 843 tests; tsc -b EXIT 0; biome clean; clean live boot. --- packages/kernel/src/host/host.test.ts | 52 ++++++++++++++++++++++++++++++++++- packages/kernel/src/host/host.ts | 7 +++++ 2 files changed, 58 insertions(+), 1 deletion(-) (limited to 'packages/kernel/src/host') 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("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("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("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(hook: FilterDescriptor, fn: FilterHandler) { return deps.bus.addFilter(hook, fn); }, + async applyFilters( + hook: FilterDescriptor, + value: TValue, + opts?: { readonly failClosed?: boolean }, + ): Promise { + return deps.bus.applyFilters(hook, value, opts); + }, provideService(handle: ServiceHandle, impl: T) { deps.bus.provideService(handle, impl); }, -- cgit v1.2.3