jev-recipes 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![Node.js version](https://img.shields.io/node/v/jev-recipes)](https://nodejs.org/)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/agencyenterprise/jev-recipes/blob/main/LICENSE)
9
9
 
10
- [Quickstart](#use-a-recipe) | [Live music app](https://jev-ai-music.com/) | [Coding assistants](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/coding-assistants.md) | [Recipe catalog](https://github.com/agencyenterprise/jev-recipes/blob/main/recipes/README.md) | [API vs. SDK vs. recipes](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/api-sdk-recipes.md) | [Contributing](https://github.com/agencyenterprise/jev-recipes/blob/main/CONTRIBUTING.md)
10
+ [Quickstart](#use-a-recipe) | [Live music app](https://jev-ai-music.com/) | [Coding assistants](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/coding-assistants.md) | [Recipe catalog](https://github.com/agencyenterprise/jev-recipes/blob/main/recipes/README.md) | [API vs. SDK vs. recipes](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/api-sdk-recipes.md) | [Agent frameworks](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/framework-adapters.md) | [Contributing](https://github.com/agencyenterprise/jev-recipes/blob/main/CONTRIBUTING.md)
11
11
 
12
12
  <!-- BEGIN GENERATED: summary -->
13
13
 
@@ -31,9 +31,21 @@ For example, give `route` a support message and descriptions of your teams. It r
31
31
 
32
32
  Evaluate your own cases with the [installed evaluator](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/evaluation.md), retain model responses, and replay confidence policies offline. The [agent workflow](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/agent-loop/README.md) and [customer queue](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/customer-queue/README.md) show how decisions fit into application code. [Direct and Gateway integrations](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/integrations.md) use the same recipe interface.
33
33
 
34
- Try the [support-routing starter](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/support-routing/README.md) for a small web app with ready, review, and optional fallback paths. Its [rules/Jev/fallback comparison](https://github.com/agencyenterprise/jev-recipes/blob/main/evals/support-routing/README.md) retains responses and labels experimental evidence explicitly.
34
+ ## Build a complete support flow
35
35
 
36
- Build the searchable static catalog with `npm run site:build`, then preview it with `npm run site:preview`. It includes fixture exploration, related-recipe comparisons, and saved evaluation evidence. Current measurements and missing or older evidence are labeled explicitly.
36
+ The [support conversation starter](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/support-routing/README.md) checks for missing details, asks a configured question, and resumes with the customer's answer. It proposes a queue or requests human review when the conversation remains unresolved.
37
+
38
+ ```sh
39
+ cd examples/support-routing
40
+ npm ci --ignore-scripts
41
+ npm run dev
42
+ ```
43
+
44
+ Open `http://localhost:3000` and choose **Ask and continue**. This folder is portable and needs no repository build. Edit `config.mjs` to supply your queues, required information, questions, and review policy. Run `npm run demo` for the command-line version. Saved scenarios work without a key; the starter guide explains live calls and application integration.
45
+
46
+ The existing [rules/Jev/fallback comparison](https://github.com/agencyenterprise/jev-recipes/blob/main/evals/support-routing/README.md) covers single-turn routing, not this clarification loop. Its evidence remains experimental.
47
+
48
+ Build the searchable static catalog with `npm run site:build`, then preview it with `npm run site:preview`. It includes fixture exploration, related-recipe comparisons, and saved evaluation evidence. Current measurements and missing, unknown-origin, or older evidence are labeled explicitly. Use `npm run eval:audit` after building to check retained evidence offline; see [evaluation and archive format](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/evaluation.md#archive-format-and-evidence-origin).
37
49
 
38
50
  ## Hear a real application: Jevthoven
39
51
 
@@ -231,6 +243,26 @@ console.log(`${result.requestsMade} calls for ${result.requestCount} tickets`);
231
243
 
232
244
  Accuracy can drop as batches grow. Measure on your own labeled data and tune `batchSize`; the [eval harness](https://github.com/agencyenterprise/jev-recipes/blob/main/CONTRIBUTING.md#model-evaluation) reports calibration per threshold.
233
245
 
246
+ ## Use inside an agent framework
247
+
248
+ `jev-recipes/ai-sdk` and `jev-recipes/langchain` plug the harness recipes into the loop you already run. With the Vercel AI SDK, `guardTools` reviews every tool call with `tool-call-gate`, `routeModelStep` picks the model per call with `model-route`, and `completionCheck` accepts a completion claim only when `completion-gate` sees evidence:
249
+
250
+ ```js
251
+ import { generateText, stepCountIs } from 'ai';
252
+ import { completionCheck, guardTools, routeModelStep } from 'jev-recipes/ai-sdk';
253
+
254
+ const done = completionCheck();
255
+ const result = await generateText({
256
+ model: careful,
257
+ prompt: 'Run the account tests and publish release 1.2.0.',
258
+ tools: { ...guardTools(tools, { policy: 'Ask before publishing.' }), ...done.tools },
259
+ prepareStep: routeModelStep({ candidates: [{ id: 'fast', text: 'Small edits.', model: fast }] }),
260
+ stopWhen: [stepCountIs(8), done.stopWhen],
261
+ });
262
+ ```
263
+
264
+ Any recipe becomes a tool with `recipeTools(['route', 'verify'])`. The LangChain adapter offers the same guard, completion, and recipe tools for `bindTools`, `ToolNode`, and `createAgent`. Install `ai` or `@langchain/core` yourself; both are optional peers. See the [framework adapter guide](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/framework-adapters.md) and the offline [AI SDK](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/ai-sdk-agent/README.md) and [LangChain](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/langchain-tools/README.md) examples.
265
+
234
266
  ## Use a local or alternative decision model
235
267
 
236
268
  Recipes talk to any server that implements the TypeSafe `systemOne` wire format. Point the SDK client at it with `baseURL`, or set `TYPESAFE_BASE_URL` in the environment, and pass the client to any recipe:
@@ -0,0 +1,38 @@
1
+ import type { LanguageModel, PrepareStepFunction, StopCondition, Tool, ToolSet } from 'ai';
2
+ import type { RecipeName } from '../../catalog/schema.js';
3
+ import type { ModelRouteResult } from '../../recipes/model-route/index.js';
4
+ import type { RecipeOptions } from '../../src/schema.js';
5
+ import type { CompletionOptions, CompletionToolOutput, GuardOptions, RecipeToolOptions } from '../shared.js';
6
+ export type { BlockedToolOutput, CompletionEvent, CompletionOptions, CompletionToolOutput, GuardEvent, GuardOptions, RecipeToolOptions, } from '../shared.js';
7
+ export type RecipeTool = Tool<Record<string, unknown>, Record<string, unknown>>;
8
+ export declare function recipeTool(name: RecipeName, options?: RecipeToolOptions): RecipeTool;
9
+ export declare function recipeTools(names: readonly RecipeName[], options?: RecipeToolOptions): Record<string, RecipeTool>;
10
+ export type GuardToolsOptions = GuardOptions & {
11
+ request?: string;
12
+ };
13
+ export declare function guardTools<TOOLS extends ToolSet>(tools: TOOLS, options?: GuardToolsOptions): TOOLS;
14
+ export type ModelCandidate = {
15
+ id: string;
16
+ text: string;
17
+ model: LanguageModel;
18
+ };
19
+ export type RouteModelStepOptions = RecipeOptions & {
20
+ candidates: readonly ModelCandidate[];
21
+ request?: string;
22
+ context?: string;
23
+ minConfidence?: number;
24
+ fallback?: LanguageModel;
25
+ onDecision?: (event: {
26
+ decision: ModelRouteResult;
27
+ model: LanguageModel | undefined;
28
+ }) => void;
29
+ };
30
+ export declare function routeModelStep(options: RouteModelStepOptions): PrepareStepFunction<any, any>;
31
+ export type CompletionCheck = {
32
+ tools: Record<string, Tool<{
33
+ report: string;
34
+ evidence?: string;
35
+ }, CompletionToolOutput>>;
36
+ stopWhen: StopCondition<any, any>;
37
+ };
38
+ export declare function completionCheck(options?: CompletionOptions): CompletionCheck;
@@ -0,0 +1,88 @@
1
+ import { jsonSchema, tool } from 'ai';
2
+ import { modelRoute } from '../../recipes/model-route/index.js';
3
+ import { COMPLETION_TOOL_DESCRIPTION, COMPLETION_TOOL_INPUT_SCHEMA, DEFAULT_COMPLETION_TOOL_NAME, blockedOutput, defineRecipeTool, messageText, requireText, reviewCompletion, reviewToolCall, } from '../shared.js';
4
+ export function recipeTool(name, options = {}) {
5
+ const definition = defineRecipeTool(name, options);
6
+ return tool({
7
+ description: definition.description,
8
+ inputSchema: jsonSchema(definition.inputSchema),
9
+ execute: async (input, executionOptions) => definition.execute(input, executionOptions.abortSignal),
10
+ });
11
+ }
12
+ export function recipeTools(names, options = {}) {
13
+ return Object.fromEntries(names.map((name) => [name, recipeTool(name, options)]));
14
+ }
15
+ export function guardTools(tools, options = {}) {
16
+ const { request, ...guardOptions } = options;
17
+ const guarded = Object.entries(tools).map(([toolName, definition]) => {
18
+ if (!('execute' in definition) || typeof definition.execute !== 'function')
19
+ return [toolName, definition];
20
+ const execute = definition.execute;
21
+ const wrapped = {
22
+ ...definition,
23
+ execute: async (input, executionOptions) => {
24
+ const decision = await reviewToolCall({
25
+ toolName,
26
+ input,
27
+ request: request ?? requireText(lastUserText(executionOptions.messages), 'request'),
28
+ }, guardOptions, executionOptions.abortSignal);
29
+ if (decision.action !== 'allow')
30
+ return blockedOutput(toolName, decision);
31
+ return execute(input, executionOptions);
32
+ },
33
+ };
34
+ return [toolName, wrapped];
35
+ });
36
+ return Object.fromEntries(guarded);
37
+ }
38
+ export function routeModelStep(options) {
39
+ const { candidates, request, context, minConfidence, fallback, onDecision, ...recipeOptions } = options;
40
+ if (!candidates.length)
41
+ throw new Error('Supply at least one model candidate.');
42
+ const decisions = new WeakMap();
43
+ return async ({ initialMessages }) => {
44
+ if (!decisions.has(initialMessages)) {
45
+ const decision = await modelRoute({
46
+ request: request ?? requireText(firstUserText(initialMessages), 'request'),
47
+ models: candidates.map(({ id, text }) => ({ id, text })),
48
+ ...(context === undefined ? {} : { context }),
49
+ ...(minConfidence === undefined ? {} : { minConfidence }),
50
+ }, recipeOptions);
51
+ const selected = decision.status === 'ready' && decision.selection !== null
52
+ ? candidates.find((candidate) => candidate.id === decision.selection)?.model
53
+ : fallback;
54
+ onDecision?.({ decision, model: selected });
55
+ decisions.set(initialMessages, selected);
56
+ }
57
+ const model = decisions.get(initialMessages);
58
+ return model === undefined ? {} : { model };
59
+ };
60
+ }
61
+ export function completionCheck(options = {}) {
62
+ const toolName = options.toolName ?? DEFAULT_COMPLETION_TOOL_NAME;
63
+ const report = tool({
64
+ description: COMPLETION_TOOL_DESCRIPTION,
65
+ inputSchema: jsonSchema(COMPLETION_TOOL_INPUT_SCHEMA),
66
+ execute: async (input, executionOptions) => reviewCompletion({
67
+ task: options.task ?? requireText(firstUserText(executionOptions.messages), 'task'),
68
+ report: input.report,
69
+ evidence: input.evidence,
70
+ }, options, executionOptions.abortSignal),
71
+ });
72
+ return {
73
+ tools: { [toolName]: report },
74
+ stopWhen: ({ steps }) => {
75
+ const last = steps.at(-1);
76
+ return (last?.toolResults.some((result) => result.toolName === toolName &&
77
+ typeof result.output === 'object' &&
78
+ result.output !== null &&
79
+ result.output.accepted === true) ?? false);
80
+ },
81
+ };
82
+ }
83
+ function lastUserText(messages) {
84
+ return messageText(messages, 'user', 'last');
85
+ }
86
+ function firstUserText(messages) {
87
+ return messageText(messages, 'user', 'first');
88
+ }
@@ -0,0 +1,24 @@
1
+ import type { DynamicStructuredTool } from '@langchain/core/tools';
2
+ export type GuardableTool = {
3
+ name: string;
4
+ description: string;
5
+ schema: unknown;
6
+ returnDirect?: boolean;
7
+ invoke(input: any, config?: any): Promise<unknown>;
8
+ };
9
+ import type { RecipeName } from '../../catalog/schema.js';
10
+ import { COMPLETION_TOOL_INPUT_SCHEMA } from '../shared.js';
11
+ import type { CompletionOptions, GuardOptions, RecipeToolOptions } from '../shared.js';
12
+ export type { BlockedToolOutput, CompletionEvent, CompletionOptions, CompletionToolOutput, GuardEvent, GuardOptions, RecipeToolOptions, } from '../shared.js';
13
+ type JsonSchema = Record<string, unknown>;
14
+ export type RecipeTool = DynamicStructuredTool<JsonSchema>;
15
+ export declare function recipeTool(name: RecipeName, options?: RecipeToolOptions): RecipeTool;
16
+ export declare function recipeTools(names: readonly RecipeName[], options?: RecipeToolOptions): RecipeTool[];
17
+ export type GuardRequest = string | ((input: unknown, config: unknown) => string);
18
+ export type GuardToolsOptions = GuardOptions & {
19
+ request: GuardRequest;
20
+ };
21
+ export declare function guardTools(tools: readonly GuardableTool[], options: GuardToolsOptions): DynamicStructuredTool[];
22
+ export declare function completionTool(options: CompletionOptions & {
23
+ task: string;
24
+ }): DynamicStructuredTool<typeof COMPLETION_TOOL_INPUT_SCHEMA>;
@@ -0,0 +1,42 @@
1
+ import { tool } from '@langchain/core/tools';
2
+ import { COMPLETION_TOOL_DESCRIPTION, COMPLETION_TOOL_INPUT_SCHEMA, DEFAULT_COMPLETION_TOOL_NAME, blockedOutput, defineRecipeTool, requireText, reviewCompletion, reviewToolCall, } from '../shared.js';
3
+ export function recipeTool(name, options = {}) {
4
+ const definition = defineRecipeTool(name, options);
5
+ return tool((input, config) => definition.execute(input, config?.signal), {
6
+ name: definition.name,
7
+ description: definition.description,
8
+ schema: definition.inputSchema,
9
+ });
10
+ }
11
+ export function recipeTools(names, options = {}) {
12
+ return names.map((name) => recipeTool(name, options));
13
+ }
14
+ export function guardTools(tools, options) {
15
+ const { request, ...guardOptions } = options;
16
+ return tools.map((original) => {
17
+ const schema = original.schema;
18
+ return tool(async (input, config) => {
19
+ const decision = await reviewToolCall({
20
+ toolName: original.name,
21
+ input,
22
+ request: typeof request === 'function' ? request(input, config) : request,
23
+ }, guardOptions, config?.signal);
24
+ if (decision.action !== 'allow')
25
+ return blockedOutput(original.name, decision);
26
+ const { toolCall: _toolCall, ...delegatedConfig } = config ?? {};
27
+ return original.invoke(input, delegatedConfig);
28
+ }, {
29
+ name: original.name,
30
+ description: original.description,
31
+ schema,
32
+ ...(original.returnDirect === undefined ? {} : { returnDirect: original.returnDirect }),
33
+ });
34
+ });
35
+ }
36
+ export function completionTool(options) {
37
+ return tool((input, config) => reviewCompletion({ task: requireText(options.task, 'task'), report: input.report, evidence: input.evidence }, options, config?.signal), {
38
+ name: options.toolName ?? DEFAULT_COMPLETION_TOOL_NAME,
39
+ description: COMPLETION_TOOL_DESCRIPTION,
40
+ schema: COMPLETION_TOOL_INPUT_SCHEMA,
41
+ });
42
+ }
@@ -0,0 +1,88 @@
1
+ import type { RecipeName } from '../catalog/schema.js';
2
+ import type { CompletionGateResult } from '../recipes/completion-gate/index.js';
3
+ import type { ToolCallGateResult } from '../recipes/tool-call-gate/index.js';
4
+ import type { RecipeOptions } from '../src/schema.js';
5
+ export type RecipeToolOptions = RecipeOptions & {
6
+ description?: string;
7
+ };
8
+ export type RecipeToolDefinition = {
9
+ name: RecipeName;
10
+ description: string;
11
+ inputSchema: Record<string, unknown>;
12
+ execute: (input: unknown, signal?: AbortSignal) => Promise<Record<string, unknown>>;
13
+ };
14
+ export type GuardOptions = RecipeOptions & {
15
+ policy?: string;
16
+ context?: string;
17
+ minConfidence?: number;
18
+ onDecision?: (event: GuardEvent) => void;
19
+ };
20
+ export type GuardEvent = {
21
+ toolName: string;
22
+ input: unknown;
23
+ decision: ToolCallGateResult;
24
+ };
25
+ export type BlockedToolOutput = {
26
+ blocked: true;
27
+ action: 'ask' | 'deny';
28
+ verdict: ToolCallGateResult['verdict'];
29
+ status: ToolCallGateResult['status'];
30
+ detected: ToolCallGateResult['detected'];
31
+ confidence: number;
32
+ message: string;
33
+ };
34
+ export type CompletionOptions = RecipeOptions & {
35
+ task?: string;
36
+ evidence?: string;
37
+ toolName?: string;
38
+ minConfidence?: number;
39
+ onDecision?: (event: CompletionEvent) => void;
40
+ };
41
+ export type CompletionEvent = {
42
+ report: string;
43
+ evidence: string | undefined;
44
+ decision: CompletionGateResult;
45
+ };
46
+ export type CompletionToolOutput = {
47
+ accepted: boolean;
48
+ verdict: CompletionGateResult['verdict'];
49
+ status: CompletionGateResult['status'];
50
+ detected: CompletionGateResult['detected'];
51
+ confidence: number;
52
+ message: string;
53
+ };
54
+ export declare const DEFAULT_COMPLETION_TOOL_NAME = "report_completion";
55
+ export declare const COMPLETION_TOOL_DESCRIPTION = "Report that the task is complete. State what was done in report, and put the evidence that demonstrates it, such as test output, command results, or observed behavior, in evidence. A claim without evidence is not accepted.";
56
+ export declare const COMPLETION_TOOL_INPUT_SCHEMA: {
57
+ readonly type: "object";
58
+ readonly properties: {
59
+ readonly report: {
60
+ readonly type: "string";
61
+ readonly description: "What was done and how the task was completed.";
62
+ };
63
+ readonly evidence: {
64
+ readonly type: "string";
65
+ readonly description: "Concrete evidence that demonstrates completion, such as test output.";
66
+ };
67
+ };
68
+ readonly required: readonly ["report"];
69
+ readonly additionalProperties: false;
70
+ };
71
+ export declare function defineRecipeTool(name: RecipeName, options?: RecipeToolOptions): RecipeToolDefinition;
72
+ export declare function renderToolCall(toolName: string, input: unknown): string;
73
+ export declare function reviewToolCall(call: {
74
+ toolName: string;
75
+ input: unknown;
76
+ request: string;
77
+ }, options: GuardOptions, signal?: AbortSignal): Promise<ToolCallGateResult>;
78
+ export declare function blockedOutput(toolName: string, decision: ToolCallGateResult): BlockedToolOutput;
79
+ export declare function reviewCompletion(claim: {
80
+ task: string;
81
+ report: string;
82
+ evidence: string | undefined;
83
+ }, options: CompletionOptions, signal?: AbortSignal): Promise<CompletionToolOutput>;
84
+ export declare function messageText(messages: readonly {
85
+ role: string;
86
+ content: unknown;
87
+ }[], role: 'user' | 'assistant', position: 'first' | 'last'): string | undefined;
88
+ export declare function requireText(value: string | undefined, description: string): string;
@@ -0,0 +1,132 @@
1
+ import { describeRecipe } from '../catalog/index.js';
2
+ import { loadRecipe } from '../catalog/runner.js';
3
+ import { completionGate } from '../recipes/completion-gate/index.js';
4
+ import { toolCallGate } from '../recipes/tool-call-gate/index.js';
5
+ export const DEFAULT_COMPLETION_TOOL_NAME = 'report_completion';
6
+ export const COMPLETION_TOOL_DESCRIPTION = 'Report that the task is complete. State what was done in report, and put the evidence that demonstrates it, such as test output, command results, or observed behavior, in evidence. A claim without evidence is not accepted.';
7
+ export const COMPLETION_TOOL_INPUT_SCHEMA = {
8
+ type: 'object',
9
+ properties: {
10
+ report: { type: 'string', description: 'What was done and how the task was completed.' },
11
+ evidence: {
12
+ type: 'string',
13
+ description: 'Concrete evidence that demonstrates completion, such as test output.',
14
+ },
15
+ },
16
+ required: ['report'],
17
+ additionalProperties: false,
18
+ };
19
+ export function defineRecipeTool(name, options = {}) {
20
+ const { description, ...recipeOptions } = options;
21
+ const recipe = describeRecipe(name);
22
+ const summary = description ??
23
+ (recipe.useWhen ? `${recipe.description} Use when: ${recipe.useWhen}` : recipe.description);
24
+ return {
25
+ name: recipe.id,
26
+ description: summary,
27
+ inputSchema: recipe.inputSchema,
28
+ execute: async (input, signal) => {
29
+ const run = await loadRecipe(name);
30
+ const result = await run(input, withSignal(recipeOptions, signal));
31
+ return result;
32
+ },
33
+ };
34
+ }
35
+ export function renderToolCall(toolName, input) {
36
+ return input === undefined ? `${toolName}()` : `${toolName}(${JSON.stringify(input)})`;
37
+ }
38
+ export async function reviewToolCall(call, options, signal) {
39
+ const { policy, context, minConfidence, onDecision, ...recipeOptions } = options;
40
+ const decision = await toolCallGate({
41
+ request: call.request,
42
+ toolCall: renderToolCall(call.toolName, call.input),
43
+ ...(context === undefined ? {} : { context }),
44
+ ...(policy === undefined ? {} : { policy }),
45
+ ...(minConfidence === undefined ? {} : { minConfidence }),
46
+ }, withSignal(recipeOptions, signal));
47
+ onDecision?.({ toolName: call.toolName, input: call.input, decision });
48
+ return decision;
49
+ }
50
+ export function blockedOutput(toolName, decision) {
51
+ const action = decision.action === 'deny' ? 'deny' : 'ask';
52
+ const risks = decision.detected.length ? ` Detected risks: ${decision.detected.join(', ')}.` : '';
53
+ const message = action === 'deny'
54
+ ? `The call to ${toolName} was refused by policy and did not run.${risks} Do not retry it. Explain what you intended and continue with permitted work.`
55
+ : `The call to ${toolName} did not run because a person must confirm it first.${risks} Do not retry it unattended. Explain what you intended and continue with other permitted work.`;
56
+ return {
57
+ blocked: true,
58
+ action,
59
+ verdict: decision.verdict,
60
+ status: decision.status,
61
+ detected: decision.detected,
62
+ confidence: decision.confidence,
63
+ message,
64
+ };
65
+ }
66
+ export async function reviewCompletion(claim, options, signal) {
67
+ const { evidence: fixedEvidence, minConfidence, onDecision, ...rest } = options;
68
+ const { task: _task, toolName: _toolName, ...recipeOptions } = rest;
69
+ const evidence = [fixedEvidence, claim.evidence]
70
+ .filter((text) => Boolean(text?.trim()))
71
+ .join('\n\n');
72
+ const decision = await completionGate({
73
+ task: claim.task,
74
+ report: claim.report,
75
+ ...(evidence ? { evidence } : {}),
76
+ ...(minConfidence === undefined ? {} : { minConfidence }),
77
+ }, withSignal(recipeOptions, signal));
78
+ onDecision?.({ report: claim.report, evidence: evidence || undefined, decision });
79
+ const accepted = decision.status === 'ready' && decision.verdict === 'complete';
80
+ const signals = decision.detected.length ? ` Signals: ${decision.detected.join(', ')}.` : '';
81
+ const message = accepted
82
+ ? 'The completion report was accepted.'
83
+ : decision.status === 'review'
84
+ ? `The completion report could not be judged confidently (verdict: ${decision.verdict}).${signals} Supply clearer evidence or continue working.`
85
+ : `The completion report was not accepted (verdict: ${decision.verdict}).${signals} Finish the remaining work or supply evidence that demonstrates completion.`;
86
+ return {
87
+ accepted,
88
+ verdict: decision.verdict,
89
+ status: decision.status,
90
+ detected: decision.detected,
91
+ confidence: decision.confidence,
92
+ message,
93
+ };
94
+ }
95
+ export function messageText(messages, role, position) {
96
+ const ordered = position === 'first' ? messages : [...messages].reverse();
97
+ for (const message of ordered) {
98
+ if (message.role !== role)
99
+ continue;
100
+ const text = contentText(message.content);
101
+ if (text)
102
+ return text;
103
+ }
104
+ return undefined;
105
+ }
106
+ function contentText(content) {
107
+ if (typeof content === 'string')
108
+ return content.trim();
109
+ if (!Array.isArray(content))
110
+ return '';
111
+ return content
112
+ .map((part) => typeof part === 'object' && part !== null && 'text' in part && typeof part.text === 'string'
113
+ ? part.text
114
+ : '')
115
+ .filter((text) => text.trim())
116
+ .join('\n')
117
+ .trim();
118
+ }
119
+ export function requireText(value, description) {
120
+ if (!value)
121
+ throw new Error(`Cannot derive ${description}; supply it explicitly.`);
122
+ return value;
123
+ }
124
+ function withSignal(options, signal) {
125
+ const { client, model } = options;
126
+ const chosen = options.signal ?? signal;
127
+ return {
128
+ ...(client === undefined ? {} : { client }),
129
+ ...(model === undefined ? {} : { model }),
130
+ ...(chosen === undefined ? {} : { signal: chosen }),
131
+ };
132
+ }
@@ -1,10 +1,13 @@
1
1
  export interface RecipeEvidence {
2
- kind: 'fixture' | 'earlier' | 'synthetic' | 'public-dataset' | 'human-reviewed' | 'mixed' | 'unspecified';
2
+ kind: 'fixture' | 'unknown' | 'earlier' | 'synthetic' | 'public-dataset' | 'human-reviewed' | 'mixed' | 'unspecified';
3
3
  label: string;
4
4
  experimental: boolean;
5
5
  measurement: {
6
6
  model: string;
7
7
  date: string | null;
8
+ operation?: string | null;
9
+ replayedAt?: string | null;
10
+ scoringRevision?: number;
8
11
  split: string | null;
9
12
  cases: number;
10
13
  ready: number | null;