Implement Service: $ARGUMENTS

SkillAI & models

Create a new Effect-TS service following the mandatory Reactive Agents patterns. Use when adding a service to any package.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Implement Service: $ARGUMENTS skill

What this skill tells your AI

The instructions your AI receives, as published by tylerjrbuell/reactive-agents-ts in .agents/skills/implement-service/SKILL.md and read by ahel’s review.

Step 1: Check the spec

Before writing any code, read the spec for the target package to confirm:

  • The service name and interface are defined in the spec
  • The service's dependencies are identified
  • The service's methods and return types are specified

Step 2: Create the service file

Create the service file at the path specified in the package's spec (usually src/services/<name>.ts or src/<name>.ts).

Follow this exact template:

import { Effect, Context, Layer, Ref } from "effect";
// Import error types from this package
import { MyError } from "../errors.js";
// Import dependency services
import { DependencyService } from "@reactive-agents/some-package";

// ─── Service Tag ─────────────────────────────────────────────────────────────

/**
 * Brief description of what this service does.
 * Which layers/phases use it.
 */
export class ServiceName extends Context.Tag("ServiceName")<
  ServiceName,
  {
    /** Method description */
    readonly methodA: (input: InputType) => Effect.Effect<OutputType, ServiceError>;
    /** Method description */
    readonly methodB: () => Effect.Effect<readonly ResultType[], ServiceError>;
  }
>() {}

// ─── Live Implementation ─────────────────────────────────────────────────────

export const ServiceNameLive = Layer.effect(
  ServiceName,
  Effect.gen(function* () {
    // 1. Resolve dependencies
    const dep = yield* DependencyService;

    // 2. Initialize state (if needed)
    const state = yield* Ref.make(initialState);

    // 3. Return implementation
    return {
      methodA: (input) =>
        Effect.gen(function* () {
          // Implementation using Effect patterns
          const result = yield* dep.someMethod(input);
          yield* Ref.update(state, (s) => /* update state */);
          return result;
        }),

      methodB: () =>
        Ref.get(state).pipe(
          Effect.map((s) => Array.from(s.values())),
        ),
    };
  }),
);

Step 3: Verify patterns

Check every line against these rules:

PatternRequiredAnti-Pattern
Context.Tag("Name")Service tag string matches class nameDifferent strings
Layer.effect(Tag, Effect.gen(...))Layer creationnew ServiceClass()
yield* DependencyServiceDependency resolutionConstructor injection
Ref.make() / Ref.get() / Ref.update()State managementlet mutableVar
Effect.Effect<T, E>Return typesPromise<T>
Effect.sync(() => ...)Synchronous operations (bun:sqlite)Raw synchronous calls
Effect.tryPromise(...)Async operations (fetch, file I/O)Raw await
Data.TaggedErrorError creationthrow new Error()

Step 4: Wire into the package layer

Add the service to the package's createXxxLayer() factory in src/runtime.ts:

export const createMyPackageLayer = () =>
  Layer.mergeAll(
    ExistingServiceLive,
    NewServiceLive.pipe(Layer.provide(DependencyServiceLive)),
  );

Step 5: Export from index.ts

Add to src/index.ts:

export { ServiceName, ServiceNameLive } from "./services/service-name.js";

Step 6: Write tests

Create tests/service-name.test.ts:

import { Effect, Layer } from "effect";
import { describe, it, expect } from "bun:test";
import { ServiceName, ServiceNameLive } from "../src/services/service-name.js";

describe("ServiceName", () => {
  // Compose test layer with mock dependencies
  const testLayer = ServiceNameLive.pipe(Layer.provide(MockDependencyLive));

  it("should handle methodA correctly", async () => {
    const result = await Effect.gen(function* () {
      const svc = yield* ServiceName;
      return yield* svc.methodA(testInput);
    }).pipe(Effect.provide(testLayer), Effect.runPromise);

    expect(result).toEqual(expectedOutput);
  });

  it("should return tagged error on failure", async () => {
    const result = await Effect.gen(function* () {
      const svc = yield* ServiceName;
      return yield* svc.methodA(badInput);
    }).pipe(
      Effect.provide(testLayer),
      Effect.flip, // Flip to get the error
      Effect.runPromise,
    );

    expect(result._tag).toBe("ServiceError");
  });
});

Common Mistakes

  1. Forgetting .js extension in imports — Bun ESM requires explicit .js extensions for relative imports
  2. Using interface instead of Schema.Struct for data types passed between services
  3. Wrapping LLMService.complete() in Effect.tryPromise — it already returns Effect
  4. Using let for state — always use Ref
  5. Missing readonly on service method types — all methods must be readonly
  6. Calling LLMService.complete() inside Effect.tryPromise — LLMService methods already return Effect. Wrapping them in Effect.tryPromise creates a double-wrapped Effect that will never resolve correctly.
  7. Not adding kernel-specific services as phases — if your new service needs to intercept per-turn reasoning, implement it as a Phase in the composable kernel rather than a standalone service. See kernel-extension skill.

Signals

GitHub stars
27
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
implement-service
Source
github.com/tylerjrbuell/reactive-agents-ts