Downcity
ServicesExamples

Service and Tool Composition

Use a realistic composition scenario to show the common pattern where a service owns state and a tool exposes the model-facing call surface

Service and Tool Composition

Scenario

You have a SearchIndexService that owns:

  • built search index state
  • cached recent search results

But you also want the model to trigger "search project documents" through a tool.

That leads to a very common shape:

  • the service owns long-lived state
  • the tool is a thin facade

Example shape

class SearchIndexService extends BaseService {
  readonly name = "search_index";
  private readonly documents = new Map<string, string>();

  indexDocument(id: string, text: string) {
    this.documents.set(id, text);
  }

  search(query: string) {
    const clean = String(query || "").trim().toLowerCase();
    return [...this.documents.entries()]
      .filter(([, text]) => text.toLowerCase().includes(clean))
      .map(([id, text]) => ({ id, text }));
  }

  readonly actions = {};
}

Then the tool becomes:

const searchTool = {
  description: "Search indexed project documents",
  inputSchema: {},
  execute: async ({ query }: { query: string }) => {
    return searchIndexService.search(query);
  },
};

Why this split is so common

Because the two layers solve different problems:

  • the service answers "who owns the state?"
  • the tool answers "how does the model call into it?"

When you do not need this split

If the capability:

  • has no long-lived state
  • is just one one-off query

then a pure tool is usually enough.