Глава 20

Соберите общий runtime вокруг registry

Registry - это то, что связывает публичное определение tool с его Zod-валидацией, классом риска и server-only handler в одном месте. Provider adapter получает только определения (то, что видит модель), а runtime владеет исполнением (то, что видит домен). Одна запись - одна ясная граница.

TypeScript
type ToolEntry = {
  definition: ToolDefinition;
  input: z.ZodType;
  risk: "read" | "preview" | "terminal";
  execute?: (value: unknown, context: TrustedToolContext) => Promise<unknown>;
};

export const toolRegistry = new Map<string, ToolEntry>([
  ["policy_search", policySearchEntry],
  ["order_read", orderReadEntry],
  ["refund_preview", refundPreviewEntry],
  ["handoff_prepare", handoffPrepareEntry],
  ["answer_user", answerUserTerminal],
  ["ask_clarification", clarificationTerminal],
  ["request_refund_approval", approvalTerminal],
  ["request_handoff", handoffTerminal]
]);

export const providerTools = Array.from(toolRegistry.values()).map(
  function getDefinition(entry) {
    return entry.definition;
  }
);

Server route для turn собирает всё вместе: проверяет сессию, применяет rate limit, строит доверенный контекст и запускает agent loop с выбранным провайдером.

TypeScript
const turnInput = z.object({
  conversation_id: z.string().uuid(),
  message: z.string().min(1).max(4_000),
  provider: z.enum(["openai", "anthropic", "google"])
}).strict();

export async function POST(request: Request) {
  const session = await requireSession(request);
  await rateLimit.consume(`support:${session.tenantId}:${session.userId}`);
  const body = turnInput.parse(await request.json());
  const traceId = crypto.randomUUID();

  const context: TrustedToolContext = {
    actorId: session.userId,
    tenantId: session.tenantId,
    locale: session.locale,
    traceId
  };

  const provider = providerFactory.get(body.provider);
  const outcome = await runAgent(provider, {
    message: body.message,
    instructions: buildPrompt(session.locale),
    tools: providerTools,
    traceId
  }, context);

  return Response.json({ trace_id: traceId, outcome });
}

Заметьте поле provider во входе - оно нужно для лаборатории и cross-provider eval, но в проде так делать нельзя.

Provider выбирается конфигурацией. В реальном продукте не позволяйте произвольному пользователю переключать провайдера. Поле показано для лаборатории и cross-provider eval; production routing определяется серверной политикой и tenant configuration.

Единый runtime собран, и он умеет работать с любым провайдером - потому что вся провайдерская специфика вынесена в адаптеры. Разберём их по одному, начиная с OpenAI.

Ссылки