implement-service · diff
git:20260315.a07056d to git:20260410.125ee95
2 added, 0 removed. Audit A to A.
---
name: implement-service
description: Create a new Effect-TS service following the mandatory Reactive Agents patterns. Use when adding a service to any package.
argument-hint: <ServiceName> <package-name>
---
# Implement Service: $ARGUMENTS
## 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:
```typescript
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:
| Pattern | Required | Anti-Pattern |
| ------------------------------------------- | ------------------------------------- | --------------------- |
| `Context.Tag("Name")` | Service tag string matches class name | Different strings |
| `Layer.effect(Tag, Effect.gen(...))` | Layer creation | `new ServiceClass()` |
| `yield* DependencyService` | Dependency resolution | Constructor injection |
| `Ref.make()` / `Ref.get()` / `Ref.update()` | State management | `let mutableVar` |
| `Effect.Effect<T, E>` | Return types | `Promise<T>` |
| `Effect.sync(() => ...)` | Synchronous operations (bun:sqlite) | Raw synchronous calls |
| `Effect.tryPromise(...)` | Async operations (fetch, file I/O) | Raw `await` |
| `Data.TaggedError` | Error creation | `throw new Error()` |
## Step 4: Wire into the package layer
Add the service to the package's `createXxxLayer()` factory in `src/runtime.ts`:
```typescript
export const createMyPackageLayer = () =>
Layer.mergeAll(
ExistingServiceLive,
NewServiceLive.pipe(Layer.provide(DependencyServiceLive)),
);
```
## Step 5: Export from index.ts
Add to `src/index.ts`:
```typescript
export { ServiceName, ServiceNameLive } from "./services/service-name.js";
```
## Step 6: Write tests
Create `tests/service-name.test.ts`:
```typescript
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.