Kernel Extension — Composable Phase Architecture
SkillAI & modelsAdd new behavior to the composable kernel — new Phase, Guard, MetaTool, or custom kernel variant. Use when extending agent reasoning, adding tool call filtering, or building a custom kernel for a new strategy.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Kernel Extension — Composable Phase Architecture skill
What this skill tells your AI
The instructions your AI receives, as published by tylerjrbuell/reactive-agents-ts in .agents/skills/kernel-extension/SKILL.md and read by ahel’s review.
Decision Tree: What Are You Adding?
Does it need to READ the LLM response and TRANSFORM kernel state per-turn?
YES → Phase
Does it need to BLOCK or MODIFY a specific tool call before execution?
YES → Guard
Does it need to INTERCEPT a specific named tool call and return a synthetic result?
YES → MetaTool entry in metaToolRegistry
Do you need a completely DIFFERENT phase pipeline for a new strategy?
YES → Custom Kernel via makeKernel({ phases: [...] })
When in doubt: Guards are simpler than Phases. Phases are simpler than custom kernels.
Adding a Phase
File location
packages/reasoning/src/kernel/capabilities/<cap>/<name>.ts (pick the capability that
fits: reason/, act/, reflect/, verify/, attend/, etc. The old
strategies/kernel/phases/ tree was removed in the Stage-5 capability re-layout.)
Exact type signature (no deviations)
import { Effect } from "effect";
import { LLMService } from "@reactive-agents/llm-provider";
import { KernelState, KernelContext } from "../../state/kernel-state.js";
export const myPhase = (
state: KernelState,
context: KernelContext,
): Effect.Effect<KernelState, never, LLMService> =>
Effect.gen(function* () {
// Read state — pure access, no mutation
const lastStep = state.steps.at(-1);
// Do work — yield* LLMService only if this is a think-equivalent phase
// For non-LLM phases, use Effect.sync(() => ...) for pure transformations
// Return FULL state — spread and override only what changed
return {
...state,
myNewField: "computed value",
};
});
Wire into the kernel
// In your strategy file or kernel/loop/react-kernel.ts:
import { makeKernel } from "../loop/react-kernel.js";
import { handleThinking } from "./reason/think.js";
import { handleActing } from "./act/act.js";
import { myPhase } from "./reason/my-phase.js";
// The DEFAULT pipeline is two phases: makeKernel() === [handleThinking, handleActing].
// Context assembly + guards run INSIDE handleThinking/handleActing — they are not
// separate top-level phases. Insert your phase relative to those two:
// - Before handleThinking: pre-processing, context enrichment
// - Between thinking and acting: post-LLM analysis / pre-execution enrichment
// - After handleActing: post-execution reflection
const kernel = makeKernel({
phases: [handleThinking, myPhase, handleActing],
});
Rules
- Phases are pure functions of
(state, context)→Effect<state> - NEVER mutate
statedirectly — always return a new object via spread - NEVER add per-turn logic to
kernel/loop/runner.ts— that's what phases are for - A phase that calls LLMService should be placed where
think.tsis or alongside it
Adding a Guard
Location
packages/reasoning/src/kernel/capabilities/act/guard.ts
Exact type signature
import { Guard, GuardOutcome } from "../kernel-state.js";
export const myGuard: Guard = (
toolCall: { name: string; input: unknown },
state: KernelState,
input: unknown,
): GuardOutcome =>
// GuardOutcome MUST be exactly one of:
// { allow: true }
// { block: true; reason: string }
toolCall.name === "forbidden-tool"
? { block: true, reason: "This tool is blocked by myGuard." }
: { allow: true };
Register for all strategies (default guards)
// In guard.ts — add to defaultGuards array:
export const defaultGuards: Guard[] = [
existingGuard1,
deduplicationGuard,
myGuard, // ← add here
];
Register for a single strategy only
// In your strategy file — pass a custom guards array:
const kernel = makeKernel({
phases: [contextBuilder, think, guard, act],
// custom guards passed via context — see KernelContext.guards
});
Rules
- Guards are SYNCHRONOUS — no
Effect, noasync, noyield* - Return exactly
{ allow: true }or{ block: true; reason: string }— nothing else - Guards run in array order; first
blockwins - A blocked tool call is logged but does NOT end the run — the LLM gets the block reason and continues
Adding a MetaTool
Location
packages/reasoning/src/kernel/capabilities/act/act.ts
Pattern
// In act.ts, inside metaToolRegistry:
const metaToolRegistry: Record<string, MetaToolHandler> = {
"pulse": pulseHandler, // existing
"brief": briefHandler, // existing
"my-meta-tool": async (args, state, context) => {
// Receives the parsed tool call arguments
// Returns a synthetic ToolResult — no real ToolService call
const result = computeResult(args);
return {
content: JSON.stringify(result),
success: true,
};
},
};
When MetaTool vs real Tool
| Use | When |
|---|---|
| MetaTool | Intercepts a known tool name, synthesizes result from in-memory state, no external I/O |
| Real Tool | Needs ToolService registration, may do HTTP/file/process I/O, follows ToolDefinition schema |
Custom Kernel
Use when a strategy needs a fundamentally different phase sequence:
import { makeKernel } from "../loop/react-kernel.js";
import { handleActing } from "./act/act.js";
// Compose only the phases you need:
export const myCustomKernel = makeKernel({
phases: [myThink, handleActing],
// Phases are executed in order, left to right, each turn
});
// Register as a ReasoningStrategy:
export const myStrategy: ReasoningStrategy = {
name: "my-strategy",
execute: (input) =>
Effect.gen(function* () {
const result = yield* myCustomKernel(input);
return result;
}),
};
Testing a Phase
Every phase test needs a timeout. Use a mock LLMService layer.
// tests/kernel/capabilities/reason/my-phase.test.ts
// Run: bun test packages/reasoning/tests/kernel/capabilities/reason/my-phase.test.ts --timeout 15000
import { Effect, Layer } from "effect";
import { describe, it, expect } from "bun:test";
import { myPhase } from "../../../../src/kernel/capabilities/reason/my-phase.js";
import { LLMService } from "@reactive-agents/llm-provider";
import { makeMockLLM } from "@reactive-agents/testing";
describe("myPhase", () => {
const mockLLMLayer = Layer.succeed(LLMService, makeMockLLM({
defaultResponse: "mock response",
}));
const makeState = (overrides = {}) => ({
messages: [],
steps: [],
iteration: 0,
status: "running" as const,
...overrides,
});
it("should transform state correctly", async () => {
const state = makeState({ iteration: 1 });
const context = { task: "test task", agentId: "agent-1" };
const result = await myPhase(state, context).pipe(
Effect.provide(mockLLMLayer),
Effect.runPromise,
);
expect(result.myNewField).toBe("expected value");
}, 15000);
it("should not modify unrelated state fields", async () => {
const state = makeState({ messages: [{ role: "user", content: "hi" }] });
const context = { task: "test", agentId: "agent-1" };
const result = await myPhase(state, context).pipe(
Effect.provide(mockLLMLayer),
Effect.runPromise,
);
// Phase should not touch fields it doesn't own
expect(result.messages).toEqual(state.messages);
}, 15000);
});
Critical: Do NOT Touch
kernel/loop/runner.tsmain loop — extend via phases, not inline logiccontext-engine.ts:buildStaticContextis LIVE (the static system-prompt builder, called fromcontext/prompt-sections-default.ts) — do NOT treat it as dead. OnlybuildDynamicContextwas removed (Apr 2026). Earlier versions of this skill wrongly listedbuildStaticContextas disabled.state.messages[]via direct mutation — return new state object from phases
Signals
- GitHub stars
- 27
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
kernel-extension- Source
- github.com/tylerjrbuell/reactive-agents-ts