jev-recipes 0.9.5 → 0.9.6
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.
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { LanguageModel, PrepareStepFunction, StopCondition, Tool, ToolSet } from 'ai';
|
|
1
|
+
import type { FlexibleSchema, LanguageModel, PrepareStepFunction, StopCondition, Tool, ToolSet } from 'ai';
|
|
2
2
|
import type { RecipeName } from '../../catalog/schema.js';
|
|
3
3
|
import type { ModelRouteResult } from '../../recipes/model-route/index.js';
|
|
4
4
|
import type { RecipeOptions } from '../../src/schema.js';
|
|
5
|
-
import type { CompletionOptions, CompletionToolOutput, GuardOptions, RecipeToolOptions } from '../shared.js';
|
|
5
|
+
import type { BlockedToolOutput, CompletionOptions, CompletionToolOutput, GuardOptions, RecipeToolOptions } from '../shared.js';
|
|
6
6
|
export type { BlockedToolOutput, CompletionEvent, CompletionOptions, CompletionToolOutput, GuardEvent, GuardOptions, RecipeToolOptions, } from '../shared.js';
|
|
7
7
|
export type RecipeTool = Tool<Record<string, unknown>, Record<string, unknown>>;
|
|
8
8
|
export declare function recipeTool(name: RecipeName, options?: RecipeToolOptions): RecipeTool;
|
|
@@ -10,7 +10,20 @@ export declare function recipeTools(names: readonly RecipeName[], options?: Reci
|
|
|
10
10
|
export type GuardToolsOptions = GuardOptions & {
|
|
11
11
|
request?: string;
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
type ToolExecution<DEFINITION extends ToolSet[string]> = NonNullable<DEFINITION['execute']>;
|
|
14
|
+
type ToolOutput<RESULT> = RESULT extends AsyncIterable<infer OUTPUT> ? OUTPUT : Awaited<RESULT>;
|
|
15
|
+
type GuardedOutput<DEFINITION extends ToolSet[string]> = ToolOutput<ReturnType<ToolExecution<DEFINITION>>> | BlockedToolOutput;
|
|
16
|
+
type GuardedExecution<DEFINITION extends ToolSet[string]> = {
|
|
17
|
+
[KEY in keyof Pick<DEFINITION, 'execute'>]: (...args: Parameters<ToolExecution<DEFINITION>>) => AsyncGenerator<GuardedOutput<DEFINITION>>;
|
|
18
|
+
};
|
|
19
|
+
type GuardedTool<DEFINITION extends ToolSet[string]> = DEFINITION extends unknown ? Omit<DEFINITION, 'execute' | 'toModelOutput' | 'outputSchema'> & GuardedExecution<DEFINITION> & {
|
|
20
|
+
outputSchema?: FlexibleSchema<GuardedOutput<DEFINITION>>;
|
|
21
|
+
toModelOutput?: NonNullable<Tool<Parameters<ToolExecution<DEFINITION>>[0], GuardedOutput<DEFINITION>>['toModelOutput']>;
|
|
22
|
+
} : never;
|
|
23
|
+
export type GuardedTools<TOOLS extends ToolSet> = {
|
|
24
|
+
[NAME in keyof TOOLS]: 'execute' extends keyof TOOLS[NAME] ? ToolExecution<TOOLS[NAME]> extends never ? TOOLS[NAME] : GuardedTool<TOOLS[NAME]> : TOOLS[NAME];
|
|
25
|
+
};
|
|
26
|
+
export declare function guardTools<TOOLS extends ToolSet>(tools: TOOLS, options?: GuardToolsOptions): GuardedTools<TOOLS>;
|
|
14
27
|
export type ModelCandidate = {
|
|
15
28
|
id: string;
|
|
16
29
|
text: string;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { jsonSchema, tool } from 'ai';
|
|
1
|
+
import { asSchema, jsonSchema, tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { modelRoute } from '../../recipes/model-route/index.js';
|
|
3
|
-
import {
|
|
4
|
+
import { toolCallGateResultSchema } from '../../recipes/tool-call-gate/index.js';
|
|
5
|
+
import { BLOCKED_TOOL_OUTPUT_KIND, COMPLETION_TOOL_DESCRIPTION, COMPLETION_TOOL_INPUT_SCHEMA, DEFAULT_COMPLETION_TOOL_NAME, blockedOutput, defineRecipeTool, messageText, requireText, reviewCompletion, reviewToolCall, } from '../shared.js';
|
|
4
6
|
export function recipeTool(name, options = {}) {
|
|
5
7
|
const definition = defineRecipeTool(name, options);
|
|
6
8
|
return tool({
|
|
@@ -13,27 +15,75 @@ export function recipeTools(names, options = {}) {
|
|
|
13
15
|
return Object.fromEntries(names.map((name) => [name, recipeTool(name, options)]));
|
|
14
16
|
}
|
|
15
17
|
export function guardTools(tools, options = {}) {
|
|
18
|
+
return Object.fromEntries(Object.entries(tools).map(([name, definition]) => [name, guardTool(name, definition, options)]));
|
|
19
|
+
}
|
|
20
|
+
function guardTool(toolName, definition, options) {
|
|
21
|
+
const execute = definition.execute;
|
|
22
|
+
if (typeof execute !== 'function')
|
|
23
|
+
return definition;
|
|
16
24
|
const { request, ...guardOptions } = options;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
const toModelOutput = definition.toModelOutput;
|
|
26
|
+
return {
|
|
27
|
+
...definition,
|
|
28
|
+
execute: async function* (input, executionOptions) {
|
|
29
|
+
guardOptions.signal?.throwIfAborted();
|
|
30
|
+
executionOptions.abortSignal?.throwIfAborted();
|
|
31
|
+
const decision = await reviewToolCall({
|
|
32
|
+
toolName,
|
|
33
|
+
input,
|
|
34
|
+
request: request ?? requireText(lastUserText(executionOptions.messages), 'request'),
|
|
35
|
+
}, guardOptions, executionOptions.abortSignal);
|
|
36
|
+
guardOptions.signal?.throwIfAborted();
|
|
37
|
+
executionOptions.abortSignal?.throwIfAborted();
|
|
38
|
+
if (decision.action !== 'allow') {
|
|
39
|
+
yield blockedOutput(toolName, decision);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const output = execute(input, executionOptions);
|
|
43
|
+
if (isAsyncIterable(output))
|
|
44
|
+
yield* output;
|
|
45
|
+
else
|
|
46
|
+
yield await output;
|
|
47
|
+
},
|
|
48
|
+
...(toModelOutput === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: {
|
|
51
|
+
toModelOutput: (result) => blockedToolOutputSchema.safeParse(result.output).success
|
|
52
|
+
? { type: 'json', value: result.output }
|
|
53
|
+
: toModelOutput(result),
|
|
54
|
+
}),
|
|
55
|
+
...(definition.outputSchema === undefined
|
|
56
|
+
? {}
|
|
57
|
+
: { outputSchema: includeBlockedOutput(definition.outputSchema) }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function isAsyncIterable(value) {
|
|
61
|
+
return (typeof value?.[Symbol.asyncIterator] ===
|
|
62
|
+
'function');
|
|
63
|
+
}
|
|
64
|
+
const blockedToolOutputSchema = toolCallGateResultSchema
|
|
65
|
+
.pick({ verdict: true, status: true, detected: true, confidence: true })
|
|
66
|
+
.extend({
|
|
67
|
+
kind: z.literal(BLOCKED_TOOL_OUTPUT_KIND),
|
|
68
|
+
blocked: z.literal(true),
|
|
69
|
+
action: z.enum(['ask', 'deny']),
|
|
70
|
+
message: z.string(),
|
|
71
|
+
});
|
|
72
|
+
function includeBlockedOutput(outputSchema) {
|
|
73
|
+
const original = asSchema(outputSchema);
|
|
74
|
+
return jsonSchema(async () => ({
|
|
75
|
+
anyOf: [
|
|
76
|
+
{ $id: 'urn:jev-recipes:tool-output', ...(await original.jsonSchema) },
|
|
77
|
+
z.toJSONSchema(blockedToolOutputSchema),
|
|
78
|
+
],
|
|
79
|
+
}), {
|
|
80
|
+
validate: async (value) => {
|
|
81
|
+
const blocked = blockedToolOutputSchema.safeParse(value);
|
|
82
|
+
if (blocked.success)
|
|
83
|
+
return { success: true, value: blocked.data };
|
|
84
|
+
return original.validate ? original.validate(value) : { success: true, value };
|
|
85
|
+
},
|
|
35
86
|
});
|
|
36
|
-
return Object.fromEntries(guarded);
|
|
37
87
|
}
|
|
38
88
|
export function routeModelStep(options) {
|
|
39
89
|
const { candidates, request, context, minConfidence, fallback, onDecision, ...recipeOptions } = options;
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
};
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
9
2
|
import type { RecipeName } from '../../catalog/schema.js';
|
|
10
3
|
import { COMPLETION_TOOL_INPUT_SCHEMA } from '../shared.js';
|
|
11
4
|
import type { CompletionOptions, GuardOptions, RecipeToolOptions } from '../shared.js';
|
|
@@ -18,6 +11,13 @@ export type GuardRequest = string | ((input: unknown, config: unknown) => string
|
|
|
18
11
|
export type GuardToolsOptions = GuardOptions & {
|
|
19
12
|
request: GuardRequest;
|
|
20
13
|
};
|
|
14
|
+
export type GuardableTool = {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
schema: unknown;
|
|
18
|
+
returnDirect?: boolean;
|
|
19
|
+
invoke(input: any, config?: any): Promise<unknown>;
|
|
20
|
+
};
|
|
21
21
|
export declare function guardTools(tools: readonly GuardableTool[], options: GuardToolsOptions): DynamicStructuredTool[];
|
|
22
22
|
export declare function completionTool(options: CompletionOptions & {
|
|
23
23
|
task: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { tool } from '@langchain/core/tools';
|
|
1
|
+
import { DynamicStructuredTool, tool } from '@langchain/core/tools';
|
|
2
|
+
import { ToolMessage } from '@langchain/core/messages';
|
|
2
3
|
import { COMPLETION_TOOL_DESCRIPTION, COMPLETION_TOOL_INPUT_SCHEMA, DEFAULT_COMPLETION_TOOL_NAME, blockedOutput, defineRecipeTool, requireText, reviewCompletion, reviewToolCall, } from '../shared.js';
|
|
3
4
|
export function recipeTool(name, options = {}) {
|
|
4
5
|
const definition = defineRecipeTool(name, options);
|
|
@@ -12,26 +13,53 @@ export function recipeTools(names, options = {}) {
|
|
|
12
13
|
return names.map((name) => recipeTool(name, options));
|
|
13
14
|
}
|
|
14
15
|
export function guardTools(tools, options) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
}, {
|
|
16
|
+
return tools.map((original) => new GuardedTool(original, options));
|
|
17
|
+
}
|
|
18
|
+
class GuardedTool extends DynamicStructuredTool {
|
|
19
|
+
original;
|
|
20
|
+
guardOptions;
|
|
21
|
+
constructor(original, guardOptions) {
|
|
22
|
+
super({
|
|
29
23
|
name: original.name,
|
|
30
24
|
description: original.description,
|
|
31
|
-
schema,
|
|
32
|
-
|
|
25
|
+
schema: original.schema,
|
|
26
|
+
returnDirect: original.returnDirect ?? false,
|
|
27
|
+
func: (input, _runManager, config) => this.invoke(input, config),
|
|
33
28
|
});
|
|
34
|
-
|
|
29
|
+
this.original = original;
|
|
30
|
+
this.guardOptions = guardOptions;
|
|
31
|
+
}
|
|
32
|
+
async invoke(input, config) {
|
|
33
|
+
const { request, ...options } = this.guardOptions;
|
|
34
|
+
const args = isToolCall(input) ? input.args : input;
|
|
35
|
+
const requestConfig = isToolCall(input) ? { ...config, toolCall: input } : config;
|
|
36
|
+
options.signal?.throwIfAborted();
|
|
37
|
+
config?.signal?.throwIfAborted();
|
|
38
|
+
const decision = await reviewToolCall({
|
|
39
|
+
toolName: this.name,
|
|
40
|
+
input: args,
|
|
41
|
+
request: typeof request === 'function' ? request(args, requestConfig) : request,
|
|
42
|
+
}, options, config?.signal);
|
|
43
|
+
options.signal?.throwIfAborted();
|
|
44
|
+
config?.signal?.throwIfAborted();
|
|
45
|
+
if (decision.action === 'allow')
|
|
46
|
+
return this.original.invoke(input, config);
|
|
47
|
+
const output = blockedOutput(this.name, decision);
|
|
48
|
+
const toolCallId = requestConfig?.toolCall?.id;
|
|
49
|
+
return toolCallId === undefined
|
|
50
|
+
? output
|
|
51
|
+
: new ToolMessage({
|
|
52
|
+
name: this.name,
|
|
53
|
+
tool_call_id: toolCallId,
|
|
54
|
+
content: JSON.stringify(output),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
call(input, config, tags) {
|
|
58
|
+
return this.invoke(input, tags === undefined ? config : { ...config, tags: config?.tags ?? tags });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function isToolCall(input) {
|
|
62
|
+
return (typeof input === 'object' && input !== null && 'type' in input && input.type === 'tool_call');
|
|
35
63
|
}
|
|
36
64
|
export function completionTool(options) {
|
|
37
65
|
return tool((input, config) => reviewCompletion({ task: requireText(options.task, 'task'), report: input.report, evidence: input.evidence }, options, config?.signal), {
|
|
@@ -22,7 +22,9 @@ export type GuardEvent = {
|
|
|
22
22
|
input: unknown;
|
|
23
23
|
decision: ToolCallGateResult;
|
|
24
24
|
};
|
|
25
|
+
export declare const BLOCKED_TOOL_OUTPUT_KIND = "jev-recipes/blocked-tool-output";
|
|
25
26
|
export type BlockedToolOutput = {
|
|
27
|
+
kind: typeof BLOCKED_TOOL_OUTPUT_KIND;
|
|
26
28
|
blocked: true;
|
|
27
29
|
action: 'ask' | 'deny';
|
|
28
30
|
verdict: ToolCallGateResult['verdict'];
|
package/dist/adapters/shared.js
CHANGED
|
@@ -2,6 +2,7 @@ import { describeRecipe } from '../catalog/index.js';
|
|
|
2
2
|
import { loadRecipe } from '../catalog/runner.js';
|
|
3
3
|
import { completionGate } from '../recipes/completion-gate/index.js';
|
|
4
4
|
import { toolCallGate } from '../recipes/tool-call-gate/index.js';
|
|
5
|
+
export const BLOCKED_TOOL_OUTPUT_KIND = 'jev-recipes/blocked-tool-output';
|
|
5
6
|
export const DEFAULT_COMPLETION_TOOL_NAME = 'report_completion';
|
|
6
7
|
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
8
|
export const COMPLETION_TOOL_INPUT_SCHEMA = {
|
|
@@ -54,6 +55,7 @@ export function blockedOutput(toolName, decision) {
|
|
|
54
55
|
? `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
56
|
: `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
57
|
return {
|
|
58
|
+
kind: BLOCKED_TOOL_OUTPUT_KIND,
|
|
57
59
|
blocked: true,
|
|
58
60
|
action,
|
|
59
61
|
verdict: decision.verdict,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-recipes",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "248 small Jev decisions for JavaScript and TypeScript: route, rerank, gate, grade, compare, and label text for agents, RAG, support, code review, and music. Each returns a typed result with explicit uncertainty. Plug into Vercel AI SDK or LangChain agents, import one recipe, or run the CLI with JSON from any language.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|