Skip to content
中文

Task references and synchronous/asynchronous reads ​

The policy implementation follows.

ts
export function defineAnswerReaderCatalog<Input, Evidence, App, Data = unknown>(
  definitions: readonly AnswerReader<Input, Evidence, App, Data>[],
) {
  return readonlyCatalog(
    definitions.map((reader) => {
      if (typeof reader.read !== 'function') throw new TypeError('Invalid answer reader');
      return Object.freeze({ id: reader.id, read: reader.read.bind(reader) });
    }),
    (id) => new TypeError(`Unknown answer reader: ${id}`),
  );
}

export function defineAnswerTaskCatalog<Input, Evidence, App, Data = unknown>(
  definitions: readonly AnswerTaskDefinition<Input, Data>[],
  ports: AnswerTaskPorts<Input, Evidence, App, Data>,
) {
  const readers = defineAnswerReaderCatalog([...ports.readers.values()]);
  const checkIds = ports.checks.ids();
  const assertDefinition = ports.assertDefinition?.bind(ports);
  const outputs = new Set(ports.outputs.ids());
  const tasks = definitions.map((definition) => {
    if (
      !definition.id?.trim() ||
      !definition.description?.trim() ||
      !readers.has(definition.readerId) ||
      !outputs.has(definition.outputId) ||
      !Array.isArray(definition.goalIds) ||
      !definition.goalIds.length ||
      definition.goalIds.some((id) => typeof id !== 'string' || !id.trim()) ||
      new Set(definition.goalIds).size !== definition.goalIds.length ||
      !Array.isArray(definition.checkIds) ||
      new Set(definition.checkIds).size !== definition.checkIds.length ||
      definition.checkIds.some((id) => !checkIds.includes(id))
    )
      throw new TypeError(`Invalid answer task: ${definition.id}`);
    const metadata = structuredClone(definition.metadata);
    freezeSchema(metadata);
    const task = Object.freeze({
      ...definition,
      id: definition.id,
      description: definition.description,
      readerId: definition.readerId,
      outputId: definition.outputId,
      scopeId: definition.scopeId,
      goalIds: Object.freeze([...definition.goalIds]),
      checkIds: Object.freeze([...definition.checkIds]),
      inputSchema: snapshotSchemaAdapter(definition.inputSchema),
      metadata,
    });
    assertion(assertDefinition?.(task), 'Task assertions must return void synchronously');
    return task;
  });
  const catalog = readonlyCatalog(tasks, (id) => new TypeError(`Unknown answer task: ${id}`));
  const prepare = (request: { taskId: string; input: unknown }, context: OperationContext<App>) => {
    context.signal.throwIfAborted();
    const task = catalog.require(request.taskId);
    const input = synchronous(
      task.inputSchema.parse(request.input),
      'Answer task parsers must be synchronous',
    );
    context.signal.throwIfAborted();
    return { input, task, reader: readers.require(task.readerId) };
  };
  return Object.freeze({
    ...catalog,
    /** Task metadata with the JSON schema flattened for model or admin presentation. */
    describe: () =>
      tasks.map(({ inputSchema, ...task }) => ({
        ...task,
        inputJsonSchema: inputSchema.jsonSchema,
      })),
    /**
     * Read evidence through a task's registered reader, synchronously.
     *
     * @throws TypeError For an unknown task, a parse failure, or a reader returning a Promise.
     */
    read(request: { taskId: string; input: unknown }, context: OperationContext<App>): Evidence {
      return nativeSubFlowSync(context, 'task', request.taskId, () => {
        const { input, task, reader } = prepare(request, context);
        const value = synchronous(
          reader.read(input, {
            ...context,
            app: context.app,
            signal: context.signal,
            diagnostics: context.diagnostics,
            task,
          }),
          'Synchronous answer readers must not return a Promise',
        );
        context.signal.throwIfAborted();
        return value as Evidence;
      });
    },
    /**
     * Read evidence through a task's registered reader, awaiting an async reader.
     *
     * @throws TypeError For an unknown task or a parse failure; aborts with the caller's signal.
     */
    async readAsync(
      request: { taskId: string; input: unknown },
      context: OperationContext<App>,
    ): Promise<Evidence> {
      return nativeSubFlowAsync(context, 'task', request.taskId, async () => {
        const { input, task, reader } = prepare(request, context);
        const value = await awaitWithSignal(
          reader.read(input, {
            ...context,
            app: context.app,
            signal: context.signal,
            diagnostics: context.diagnostics,
            task,
          }),
          context.signal,
        );
        context.signal.throwIfAborted();
        return value as Evidence;
      });
    },
  });
}

API reference, usage examples and developer guides.