DeepSeek Harness servicios y dependencias: clase base Service y declaración de tipos

En los capítulos anteriores hemos estado usando los servicios integrados ctx.tools, ctx.llm y ctx.agents.

En este capítulo, desde la perspectiva del proveedor de servicios, veremos cómo usarla clase base Servicepara proporcionar un servicio con nombre y permitir que el consumidor obtenga sugerencias de tipo completas.


Qué es un servicio

En Harness, tools, llm y agents son servicios.

Un servicio es una capacidad con nombre montada en ctx; cualquier complemento puede proporcionar servicios para que otros complementos los utilicen.

Servicio: una capacidad que un complemento expone a otros complementos.

Ocupa un ctx.<key> estable (por ejemplo, ctx.tools, ctx.llm, ctx.sessions). Otros complementos buscan servicios por key, en lugar de importar una implementación concreta.

Por ejemplo, ctx.tools es el servicio de tiempo de ejecución de herramientas, ctx.llm es el servicio de modelos grandes y ctx.agents es el servicio de agentes.

Los nombres de los servicios integrados, los métodos públicos y la ubicación del código fuente se generan automáticamente desde el repositorio hacia las páginas de subsistemas de cada servicio. Al desarrollar complementos, debes basarte en estos bloques generados y en las interfaces TypeScript de los servicios, y no depender de ninguna lista estática escrita a mano.


Usar servicios: declaración inject

Para usar un servicio existente, declara inject en el complemento.

El framework garantiza que, cuando se ejecuta apply, todos los servicios declarados en inject ya están listos.

Ejemplo

// Ruta del archivo: scratch-plugin/src/use-tools.ts
// Declara dependencia del servicio tools
export const inject = ['tools']

export function apply(ctx: Context) {
  // ctx.tools existe y está listo
  ctx.tools.register(/* ... */)
}

Si el servicio aún no está listo, tu complemento esperará y no ejecutará apply.


Proveer servicios: clase base Service

Deriva una subclase de la clase base Service y llama en el constructor asuper(ctx, 'nombre del servicio')Registra el servicio con nombre.

Ejemplo

// Ruta del archivo: scratch-plugin/src/metrics-service.ts
import { Service, type Context } from '@deepseek-ai/cordis'

// Los servicios también pueden depender de otros servicios; decláralo con inject estático
export default class MetricsService extends Service {
  static inject = ['llm']  // Este servicio requiere que el servicio llm esté listo primero

  constructor(ctx: Context) {
    // El segundo parámetro 'metrics' es el nombre del servicio; se monta en ctx.metrics
    super(ctx, 'metrics')
  }

  // Método de servicio público
  record(event: string, value: number) {
    // Aquí se registra una métrica, por ejemplo, el número de llamadas a herramientas
    // La implementación real escribiría en el backend de métricas; se omite aquí
  }
}

Después de cargar este complemento, el consumidor puede acceder a él a través de ctx.metrics.

Ejemplo

// Ruta del archivo: scratch-plugin/src/use-metrics.ts
// El consumidor declara dependencia del servicio metrics
export const inject = ['metrics']

export function apply(ctx: Context) {
  // Llama al método del servicio: registra una llamada a herramienta
  ctx.metrics.record('ejemplo_tool_call', 1)
}

Declaración de tipos: fusión de declare module

Usa la fusión de declaraciones de TypeScript para que ctx.metrics tenga el tipo correcto.

De esta manera, al escribir código habrá autocompletado y, en tiempo de compilación, también se detectarán los errores por escribir mal el nombre del servicio.

Ejemplo

// Ruta del archivo: scratch-plugin/src/metrics-service.ts
import { Service, type Context } from '@deepseek-ai/cordis'

// Fusión de declaraciones: le dice a TypeScript que Context tiene un campo metrics
declare module '@deepseek-ai/cordis' {
  interface Context {
    metrics: MetricsService
  }
}

export default class MetricsService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'metrics')
  }

  record(event: string, value: number) {
    /* implementación omitida */
  }
}

La fusión de declaraciones con declare module es la práctica estándar de TypeScript para agregar tipos a un módulo existente.

Aquí el tipo y la implementación de ctx.metrics se escriben en el mismo archivo para garantizar que no diverjan.


Dependencias obligatorias y opcionales

Las dependencias de servicios se dividen en obligatorias y opcionales.

Las dependencias obligatorias se declaran con inject; si el servicio está ausente, el complemento no se carga en absoluto.

En las dependencias opcionales se omite inject y en el punto de uso se usactx.get()para consultar, y es posible obtener undefined.

Ejemplo

// Ruta del archivo: scratch-plugin/src/optional-dep.ts
// Dependencia obligatoria: si el servicio está ausente, el complemento no se carga
export const inject = ['tools']

// Dependencia opcional: se omite inject; en el punto de uso se consulta con ctx.get()
export function apply(ctx: Context) {
  // ctx.get('metrics') puede devolver undefined; usa encadenamiento opcional para llamarlo de forma segura
  const metrics = ctx.get('metrics')
  metrics?.record('ejemplo_plugin_loaded', 1)
}
Tipo de dependenciaSintaxisComportamiento cuando el servicio está ausenteCaso de uso
Dependencia obligatoriaexport const inject = ['tools']El complemento no se carga y permanece en PENDINGNo puede funcionar sin ese servicio
Dependencia opcionalSe omite inject y se consulta con ctx.get('metrics')El complemento se carga normalmente; ctx.get() devuelve undefinedEl servicio es prescindible; debe funcionar normalmente también cuando está ausente

¿Cuándo usar dependencias opcionales? Cuando el servicio es prescindible y tu complemento debe funcionar normalmente incluso en su ausencia.


Comportamiento cuando los servicios desaparecen

Si durante la ejecución de la aplicación un servicio obligatorio desaparece (por ejemplo, se desinstala su proveedor), ocurren dos cosas:

  1. Los complementos que dependen de él se eliminan (dispose) automáticamente (liberan recursos).
  2. Cuando el servicio reaparece, el complemento se recarga automáticamente.

Esto evita que los complementos llamen a servicios que ya no existen.


Resumen y autoevaluación

Las subclases de Service proporcionan servicios con nombre mediante super(ctx, 'nombre'); la fusión de declaraciones con declare module completa los tipos; inject declara dependencias obligatorias y ctx.get() consulta dependencias opcionales.

Ponte a prueba:

  1. ¿En qué forma existe un servicio en ctx? ¿Qué parámetro determina el nombre del servicio?
  2. ¿Cómo hacer que ctx.metrics tenga sugerencias de tipo?
  3. ¿En qué se diferencia la sintaxis de código entre dependencias obligatorias y opcionales?
Otras extensiones