Downcity
ServicesExamples

CacheService Scenario

Use a lifecycle-based CacheService scenario to explain startup, state ownership, and idempotent design

CacheService Scenario

Scenario

You are building a local indexing assistant and want it to cache:

  • recently queried file summaries
  • recently parsed directory shapes
  • results from expensive external calls

And you want:

  • service startup to record a start timestamp
  • service shutdown to clear the cache

That is a strong fit for a lifecycle-aware service.

Example

import {
  Agent,
  BaseService,
  type AgentContext,
  type ServiceActions,
} from "@downcity/agent";

class CacheService extends BaseService {
  readonly name = "cache";

  private startedAt = 0;
  private readonly store = new Map<string, string>();

  readonly lifecycle = {
    start: async (_context: AgentContext) => {
      if (this.startedAt > 0) return;
      this.startedAt = Date.now();
    },
    stop: async () => {
      this.store.clear();
      this.startedAt = 0;
    },
  };

  setValue(key: string, value: string) {
    const cleanKey = String(key || "").trim();
    if (!cleanKey) {
      throw new Error("key is required");
    }
    this.store.set(cleanKey, String(value || ""));
  }

  getValue(key: string) {
    return this.store.get(String(key || "").trim()) || null;
  }

  readonly actions: ServiceActions = {
    status: {
      execute: async () => {
        return {
          success: true,
          data: {
            startedAt: this.startedAt || null,
            size: this.store.size,
          },
        };
      },
    },
    set: {
      execute: async ({ payload }) => {
        const body = payload as { key?: unknown; value?: unknown };
        const key = String(body.key || "").trim();
        const value = String(body.value || "");
        this.setValue(key, value);
        return {
          success: true,
          data: { key },
        };
      },
    },
    get: {
      execute: async ({ payload }) => {
        const body = payload as { key?: unknown };
        const key = String(body.key || "").trim();
        return {
          success: true,
          data: {
            key,
            value: this.getValue(key),
          },
        };
      },
    },
  };
}

const cacheService = new CacheService();

const agent = new Agent({
  id: "repo-helper",
  path: "/path/to/project",
  tools: {},
  services: [cacheService],
});

await agent.start({
  http: {
    host: "127.0.0.1",
    port: 15314,
  },
});

The design points that matter most

1. start should be idempotent

In the current SDK, the most stable automatic service startup points are:

  • agent.start({ http: { ... } })
  • agent.start({ rpc: true })

So repeated startup should not break the service.

2. stop should be idempotent too

Because the SDK still does not expose one full public service shutdown API.

3. status is often the first action worth designing

For runtime-like services, the most useful early action is often not business logic, but:

  • status

That reduces troubleshooting cost dramatically.

What this scenario is good for

It shows that service lifecycle is not about making every service heavy.
It is about giving startup and shutdown semantics a clean boundary when a capability really needs them.