Skip to content
中文

Generation, checks, review, local repair, and delivery ​

The policy implementation follows.

ts
export function defineAnswerFlow<Data, Value, App = undefined>(
  definition: AnswerFlowDefinition<Data, Value, App>,
) {
  const { maxItems, maxRepairs } = definition,
    reviewPolicy = definition.reviewPolicy ?? 'optional';
  if (
    !Number.isSafeInteger(maxItems) ||
    maxItems < 1 ||
    !Number.isSafeInteger(maxRepairs) ||
    maxRepairs < 0 ||
    !['optional', 'required', 'off'].includes(reviewPolicy)
  )
    throw new TypeError('Invalid answer flow limits or policy');
  for (const handler of [
    definition.generate,
    definition.check,
    definition.fallback,
    definition.canInvoke,
  ]) {
    if (typeof handler !== 'function') throw new TypeError('Missing answer flow handler');
  }
  if (definition.review !== undefined && typeof definition.review !== 'function')
    throw new TypeError('Invalid answer review handler');
  const generateValues = definition.generate.bind(definition),
    checkValue = definition.check.bind(definition),
    reviewValues = definition.review?.bind(definition),
    fallbackValue = definition.fallback.bind(definition),
    canInvoke = definition.canInvoke.bind(definition),
    observer = definition.observe?.bind(definition);
  const allowed = (context: OperationContext<App>) => {
    context.signal.throwIfAborted();
    const value = booleanResult(
      canInvoke(context),
      'Answer budget checks must return boolean synchronously',
    );
    context.signal.throwIfAborted();
    return value;
  };
  const issues = (cell: Cell<Data, Value>): AnswerIssue[] => {
    const result = [
      ...cell.local.issues,
      ...(cell.review.status === 'failed' ? cell.review.issues : []),
    ];
    if (!cell.hasValue || cell.local.status !== 'passed')
      result.push({
        code: !cell.hasValue ? 'ANSWER_MISSING' : 'ANSWER_LOCAL_CHECK',
        message: 'No locally accepted value',
      });
    if ((reviewPolicy === 'required' || cell.knownReviewFailure) && cell.review.status !== 'passed')
      result.push({ code: 'ANSWER_REVIEW_REQUIRED', message: 'Required review is unresolved' });
    return result;
  };
  function observe(
    stageId: string,
    cells: Cell<Data, Value>[],
    context: OperationContext<App>,
  ): void {
    if (!observer) return;
    const results = cells.map((cell) => ({
      item: cell.item,
      value: cell.value,
      status: 'blocked' as const,
      issues: issues(cell),
      checks: { local: cell.local, review: cell.review },
    }));
    observeSafely((input) => observer(input, context), { stageId, results });
  }
  async function generate(
    cells: Cell<Data, Value>[],
    repair: boolean,
    context: OperationContext<App>,
  ): Promise<void> {
    context.signal.throwIfAborted();
    if (!cells.length || !allowed(context)) return;
    const previous = new Map(
      cells.flatMap((cell) =>
        !cell.hasValue ? [] : [[cell.item.id, cell.value as Value] as const],
      ),
    );
    const repairs = repair ? new Map(cells.map((cell) => [cell.item.id, issues(cell)])) : undefined;
    for (const cell of cells) cell.generated = false;
    let values: ReadonlyMap<string, Value>;
    try {
      values = await awaitWithSignal(
        generateValues({ items: cells.map((cell) => cell.item), repairs, previous }, context),
        context.signal,
      );
    } catch {
      context.signal.throwIfAborted();
      observe(repair ? 'repair-unavailable' : 'generation-unavailable', cells, context);
      return;
    }
    context.signal.throwIfAborted();
    if (
      !values ||
      typeof values.get !== 'function' ||
      typeof values.keys !== 'function' ||
      [...values.keys()].some((id) => !cells.some((cell) => cell.item.id === id))
    )
      throw new TypeError('Invalid generated answer ownership');
    for (const cell of cells) {
      cell.hasValue = values.has(cell.item.id);
      cell.value = values.get(cell.item.id);
      cell.generated = cell.hasValue;
      cell.local = { status: 'not-run', issues: [] };
      cell.review = { status: 'not-run', issues: [] };
    }
  }
  function check(cells: Cell<Data, Value>[], context: OperationContext<App>): void {
    for (const cell of cells) {
      context.signal.throwIfAborted();
      if (!cell.hasValue) {
        cell.local = unavailable();
        continue;
      }
      const result = snapshotAnswerCheck(
        synchronous(
          checkValue({ item: cell.item, value: cell.value as Value }, context),
          'Answer checkers must be synchronous',
        ),
      );
      context.signal.throwIfAborted();
      if (Object.hasOwn(result, 'value')) cell.value = result.value;
      cell.local = result;
    }
  }
  async function review(cells: Cell<Data, Value>[], context: OperationContext<App>): Promise<void> {
    context.signal.throwIfAborted();
    const selected = cells.filter((cell) => cell.local.status === 'passed' && cell.hasValue);
    if (!selected.length) return;
    if (reviewPolicy === 'off') {
      for (const cell of selected) cell.review = { status: 'not-run', issues: [] };
      return;
    }
    if (!reviewValues || !allowed(context)) {
      for (const cell of selected) cell.review = unavailable();
      return;
    }
    let checks: ReadonlyMap<string, AnswerCheck<Value>>;
    try {
      checks = await awaitWithSignal(
        reviewValues(
          { items: selected.map((cell) => ({ item: cell.item, value: cell.value! })) },
          context,
        ),
        context.signal,
      );
    } catch {
      context.signal.throwIfAborted();
      for (const cell of selected) cell.review = unavailable();
      return;
    }
    context.signal.throwIfAborted();
    if (
      !checks ||
      typeof checks.get !== 'function' ||
      typeof checks.keys !== 'function' ||
      [...checks.keys()].some((id) => !selected.some((cell) => cell.item.id === id))
    )
      throw new TypeError('Invalid answer review ownership');
    const checked = new Map([...checks].map(([id, result]) => [id, snapshotAnswerCheck(result)]));
    for (const cell of selected) {
      cell.review = checked.get(cell.item.id) ?? unavailable();
      if (cell.review.status === 'failed') cell.knownReviewFailure = true;
      if (cell.review.status === 'passed') cell.knownReviewFailure = false;
    }
  }
  const workflow = defineWorkflow<State<Data, Value>, AnswerResult<Data, Value>[], App>({
    id: 'answer-flow',
    entryNodeId: 'generate',
    nodes: [
      {
        id: 'generate',
        description: 'Generate available items',
        startsRound: true,
        run: async (state, context) => {
          await generate(
            state.cells.filter((cell) => cell.item.sourceStatus === 'ready'),
            false,
            context,
          );
        },
        routes: [{ id: 'to-validate', targetNodeId: 'validate' }],
      },
      {
        id: 'validate',
        description: 'Check values and retain sanitized output',
        run: (state, context) => {
          check(
            state.cells.filter((cell) => cell.item.sourceStatus === 'ready'),
            context,
          );
        },
        routes: [{ id: 'to-review', targetNodeId: 'review' }],
      },
      {
        id: 'review',
        description: 'Review locally accepted values',
        run: async (state, context) => {
          await review(
            state.cells.filter((cell) => cell.item.sourceStatus === 'ready'),
            context,
          );
          observe('checked', state.cells, context);
        },
        routes: [{ id: 'to-select-repair', targetNodeId: 'select-repair' }],
      },
      {
        id: 'select-repair',
        description: 'Select only failed available items',
        run: (state) => ({
          pending: state.cells.filter(
            (cell) => cell.item.sourceStatus === 'ready' && issues(cell).length > 0,
          ),
        }),
        routes: [
          {
            id: 'to-repair',
            conditionId: '{nid}.{rid}',
            when: (state, context) =>
              state.pending.length > 0 && state.repairBudget.available && allowed(context),
            targetNodeId: 'repair',
          },
          { id: 'to-deliver', targetNodeId: 'deliver' },
        ],
      },
      {
        id: 'repair',
        description: 'Use one bounded repair attempt',
        startsRound: true,
        run: async (state, context) => {
          if (!state.repairBudget.take()) throw new TypeError('Answer repair budget exhausted');
          await generate(state.pending, true, context);
        },
        routes: [{ id: 'to-recheck', targetNodeId: 'recheck' }],
      },
      {
        id: 'recheck',
        description: 'Recheck only repaired items',
        run: async (state, context) => {
          const generated = state.pending.filter((cell) => cell.generated);
          check(generated, context);
          await review(generated, context);
          observe('repaired', state.pending, context);
        },
        routes: [{ id: 'to-select-repair', targetNodeId: 'select-repair' }],
      },
      {
        id: 'deliver',
        description: 'Return accepted values or explicit fallback states',
        run: (state, context) => ({
          result: state.cells.map((cell) => {
            context.signal.throwIfAborted();
            const errors = issues(cell);
            if (cell.item.sourceStatus === 'ready' && !errors.length && cell.hasValue)
              return {
                item: cell.item,
                value: cell.value,
                status: 'accepted' as const,
                issues: errors,
                checks: { local: cell.local, review: cell.review },
              };
            const outcome = synchronous(
              fallbackValue({ item: cell.item, issues: errors }, context),
              'Answer fallback must be synchronous',
            );
            if (
              !outcome ||
              typeof outcome.handled !== 'boolean' ||
              (outcome.handled && !('value' in outcome))
            )
              throw new TypeError('Invalid answer fallback outcome');
            return {
              item: cell.item,
              value: outcome.handled ? outcome.value : undefined,
              status: outcome.handled ? ('fallback' as const) : ('blocked' as const),
              issues: errors,
              checks: { local: cell.local, review: cell.review },
            };
          }),
        }),
        routes: [{ id: 'complete', complete: (state) => state.result! }],
      },
    ],
  });
  return Object.freeze({
    /**
     * Run one answer batch.
     *
     * @param request - Input ids the batch must cover and the items to answer; validated before
     *   any handler runs.
     * @param context - Operation context supplying app, signal and diagnostics.
     * @returns One accepted, fallback or blocked result per item, in request order.
     * @throws TypeError For an invalid batch or handler output, or when the repair budget is
     *   exhausted without a final result.
     */
    async run(
      request: { inputIds: readonly string[]; items: readonly AnswerItem<Data>[] },
      context: OperationContext<App>,
    ): Promise<AnswerResult<Data, Value>[]> {
      return nativeSubFlowAsync(context, 'answer', 'answer-flow', async () => {
        context.signal.throwIfAborted();
        validateAnswerBatch(request.inputIds, request.items, maxItems);
        const cells = request.items.map((item) => ({
          item: Object.freeze({
            id: item.id,
            data: item.data,
            sourceStatus: item.sourceStatus,
            inputIds: Object.freeze([...item.inputIds]),
          }),
          hasValue: false,
          local: { status: 'not-run' as const, issues: [] },
          review: { status: 'not-run' as const, issues: [] },
          knownReviewFailure: false,
        }));
        return new AgentCoreEngine<AnswerResult<Data, Value>[]>({
          maxRounds: maxRepairs + 1,
          errors: { exhausted: () => new TypeError('Answer flow exhausted') },
          execution: { signal: context.signal, diagnostics: context.diagnostics },
        })
          .bindWorkflow(workflow, context.app as App)
          .run({ cells, pending: [], repairBudget: new AttemptBudget(maxRepairs) });
      });
    },
  });
}

API reference, usage examples and developer guides.