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;
});
},
});
}