kernel-extension

star 16

Add 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.

tylerjrbuell By tylerjrbuell schedule Updated 6/2/2026

name: kernel-extension description: Add 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. user-invocable: false

Kernel Extension — Composable Phase Architecture

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 state directly — 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.ts is 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, no async, no yield*
  • Return exactly { allow: true } or { block: true; reason: string } — nothing else
  • Guards run in array order; first block wins
  • 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.ts main loop — extend via phases, not inline logic
  • context-engine.ts: buildStaticContext is LIVE (the static system-prompt builder, called from context/prompt-sections-default.ts) — do NOT treat it as dead. Only buildDynamicContext was removed (Apr 2026). Earlier versions of this skill wrongly listed buildStaticContext as disabled.
  • state.messages[] via direct mutation — return new state object from phases
Install via CLI
npx skills add https://github.com/tylerjrbuell/reactive-agents-ts --skill kernel-extension
Repository Details
star Stars 16
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator
tylerjrbuell
tylerjrbuell Explore all skills →