Skip to content
English

绑定、校验、期限与真实回执 ​

策略的具体实现如下。

ts
export class ModelCalls<
  Input = unknown,
  Output = unknown,
  const Bindings extends readonly ModelBinding<any, any>[] = readonly ModelBinding<Input, Output>[],
> {
  private sequence = 0;
  private readonly bindings = new ProtocolRegistry<ModelBinding<any, any>>();
  /**
   * @param bindings - Bindings to register; each needs a non-blank `identity` and an `invoke`.
   * @throws TypeError When a binding is malformed or two bindings share an id.
   */
  constructor(bindings: Bindings) {
    for (const binding of bindings) {
      if (!binding.identity?.trim() || typeof binding.invoke !== 'function')
        throw new TypeError('Invalid model binding');
      this.bindings.set(
        binding.id,
        Object.freeze({
          id: binding.id,
          identity: binding.identity,
          invoke: binding.invoke.bind(binding),
          assertInput: binding.assertInput?.bind(binding),
          assertOutput: binding.assertOutput?.bind(binding),
        }),
      );
    }
  }
  /** Registered bindings in declaration order, with methods already bound to their owner. */
  get definitions(): readonly RegisteredBinding<Bindings[number]>[] {
    return Object.freeze([...this.bindings.values()]) as readonly RegisteredBinding<
      Bindings[number]
    >[];
  }
  /** Registered binding ids in declaration order. */
  ids(): readonly string[] {
    return this.bindings.ids();
  }
  /** Whether a binding id is registered. */
  has(id: string): boolean {
    return this.bindings.has(id);
  }
  /** Registered binding for `id`, or `undefined` when it is absent. */
  get(id: string): RegisteredBinding<Bindings[number]> | undefined {
    return this.bindings.get(id) as RegisteredBinding<Bindings[number]> | undefined;
  }
  /**
   * Registered binding for `id`.
   *
   * @throws TypeError When the id is not registered.
   */
  require(id: string): RegisteredBinding<Bindings[number]> {
    return this.bindings.require(id) as RegisteredBinding<Bindings[number]>;
  }
  /** Iterate the registered bindings. */
  values(): IterableIterator<RegisteredBinding<Bindings[number]>> {
    return this.bindings.values() as IterableIterator<RegisteredBinding<Bindings[number]>>;
  }
  /** Iterate `[id, binding]` pairs. */
  entries(): IterableIterator<[string, RegisteredBinding<Bindings[number]>]> {
    return this.bindings.entries() as IterableIterator<
      [string, RegisteredBinding<Bindings[number]>]
    >;
  }
  /**
   * Implementation identity of one binding.
   *
   * @throws TypeError When the id is not registered.
   */
  identity(id: string): string {
    return this.require(id).identity;
  }
  /**
   * Invoke one registered binding under the caller's signal, timeout and attempt budget.
   *
   * Order matters: the binding id is resolved, input is asserted, the budget take happens
   * before the provider call, and the output assertion runs before a result is reported. Any
   * failure emits a diagnostic and rethrows unchanged, so callers see the provider's own error.
   *
   * @param request - Binding id, input and per-call timeout.
   * @param context - Execution context; `signal` is combined with a timeout signal.
   * @returns The provider output wrapped with invocation metadata.
   * @throws TypeError For a malformed request, unknown id, missing signal/context, invalid
   *   timeout or failed input/output assertion; `BudgetError` when the attempt budget refuses;
   *   the provider's own error otherwise, including abort and timeout.
   */
  invoke<const Id extends string>(
    request: ModelInvocationRequest<InputFor<Bindings, Id>> & { readonly modelId: Id },
    context: ModelInvocationContext,
  ): Promise<ModelResult<OutputFor<Bindings, Id>>>;
  async invoke(
    request: ModelInvocationRequest<unknown>,
    context: ModelInvocationContext,
  ): Promise<ModelResult<unknown>> {
    if (!request || typeof request !== 'object' || !('input' in request))
      throw new TypeError('Use one model request and execution context');
    const { modelId: id, input, timeoutMs } = request;
    if (
      !context?.signal ||
      typeof id !== 'string' ||
      !id.trim() ||
      !Number.isSafeInteger(timeoutMs) ||
      timeoutMs < 1 ||
      // 2147483647 is the largest delay `setTimeout`/`AbortSignal.timeout` accept reliably.
      timeoutMs > 2147483647
    )
      throw new TypeError('A modelId, signal and positive supported timeoutMs are required');
    const sourceBudget = context.attemptBudget;
    const take = sourceBudget?.take.bind(sourceBudget);
    const callId = invocationId(),
      startedAtMs = Date.now();
    const diagnostics = context.diagnostics?.child(callId, {
      invocationId: callId,
      modelId: id,
      nodeRole: 'invocation',
      definitionKind: 'model-binding',
      definitionId: id,
      nodeId: id,
      nodeName: id,
      nodeKind: 'model-binding',
    });
    const signal = combineSignals(context.signal, AbortSignal.timeout(timeoutMs));
    let phase: 'input' | 'invoke' | 'output' = 'input';
    try {
      const binding = this.bindings.require(id);
      signal.throwIfAborted();
      assertion(binding.assertInput?.(input), 'Model input assertions must be synchronous');
      signal.throwIfAborted();
      if (take) {
        const accepted = synchronous(take(), 'Model attempt budget must be synchronous');
        breakWhile(typeof accepted !== 'boolean', 'Model attempt budget must return boolean');
        breakWhile(!accepted, () => new BudgetError('External model attempt budget exhausted'));
      }
      const callSequence = ++this.sequence;
      phase = 'invoke';
      diagnostics?.emit('model.binding', () => ({
        modelId: id,
        invocationId: callId,
        status: 'running',
        phase,
      }));
      // Hand off checked inputs before yielding. Arbitrary App inputs may be
      // class instances or services, so Core must not silently clone them.
      signal.throwIfAborted();
      const output = await awaitWithSignal(
        binding.invoke(input, { ...context, signal, diagnostics, attemptBudget: sourceBudget }),
        signal,
      );
      signal.throwIfAborted();
      phase = 'output';
      assertion(binding.assertOutput?.(output), 'Model output assertions must be synchronous');
      signal.throwIfAborted();
      diagnostics?.emit('model.binding', () => ({
        modelId: id,
        invocationId: callId,
        status: 'success',
        phase,
        durationMs: Date.now() - startedAtMs,
      }));
      return invocationResult(output, {
        ...diagnostics?.identity,
        invocationId: callId,
        modelId: id,
        startedAtMs,
        durationMs: Date.now() - startedAtMs,
        callSequence,
        status: 'success' as const,
      });
    } catch (error) {
      diagnostics?.emit('model.binding', () => ({
        modelId: id,
        invocationId: callId,
        status: signal.aborted ? 'cancelled' : 'error',
        phase,
        durationMs: Date.now() - startedAtMs,
        errorCode: getErrorInfo(error).code,
      }));
      throw error;
    }
  }
}

接口参考、使用示例与开发指南。