Skip to content

Commit 2c755ff

Browse files
authored
feat(js/plugins/middleware): implemented skills middleware (#5049)
1 parent 9f5c562 commit 2c755ff

20 files changed

Lines changed: 2088 additions & 16 deletions

js/plugins/middleware/README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Genkit Middleware
2+
3+
This package provides a collection of useful middlewares for the Genkit JS SDK to enhance model execution, tool usage, and agentic workflows.
4+
5+
## Installation
6+
7+
```bash
8+
npm install @genkit-ai/middleware
9+
# or
10+
pnpm add @genkit-ai/middleware
11+
```
12+
13+
## Available Middlewares
14+
15+
### 1. FileSystem Middleware (`filesystem`)
16+
17+
Grants the model access to the local filesystem by injecting standard file manipulation tools (`list_files`, `read_file`, `write_file`, `search_and_replace`). All operations are safely restricted to a specified root directory.
18+
19+
```typescript
20+
import { genkit } from 'genkit';
21+
import { filesystem } from '@genkit-ai/middleware';
22+
23+
const ai = genkit({ ... });
24+
25+
const response = await ai.generate({
26+
model: 'gemini-2.5-flash',
27+
prompt: 'Create a hello world node app in the workspace',
28+
use: [
29+
filesystem({ rootDirectory: './workspace' })
30+
]
31+
});
32+
```
33+
34+
### 2. Skills Middleware (`skills`)
35+
36+
Automatically scans a directory for `SKILL.md` files (and their YAML frontmatter) and injects them into the system prompt. It also provides a `use_skill` tool the model can use to retrieve more specific skills on demand.
37+
38+
```typescript
39+
import { genkit } from 'genkit';
40+
import { skills } from '@genkit-ai/middleware';
41+
42+
const ai = genkit({ ... });
43+
44+
const response = await ai.generate({
45+
prompt: 'How do I run tests in this repo?',
46+
use: [
47+
skills({ skillPaths: ['./skills'] })
48+
]
49+
});
50+
```
51+
52+
### 3. Tool Approval Middleware (`toolApproval`)
53+
54+
Restricts execution of tools to an approved list. If the model attempts to call an unapproved tool, it throws a `ToolInterruptError` allowing you to prompt the user for manual confirmation before resuming.
55+
56+
```typescript
57+
import { genkit, restartTool } from 'genkit';
58+
import { toolApproval } from '@genkit-ai/middleware';
59+
60+
const ai = genkit({ ... });
61+
62+
// 1. Initial attempt
63+
const response = await ai.generate({
64+
prompt: 'write a file',
65+
tools: [writeFileTool],
66+
use: [
67+
toolApproval({ approved: [] }) // Empty list means call triggers interrupt
68+
]
69+
});
70+
71+
if (response.finishReason === 'interrupted') {
72+
const interrupt = response.interrupts[0];
73+
74+
// 2. Ask user for approval, then recreate the tool request with approval
75+
const approvedPart = restartTool(interrupt, { toolApproved: true });
76+
77+
// 3. Resume execution
78+
const resumedResponse = await ai.generate({
79+
messages: response.messages,
80+
resume: { restart: [approvedPart] },
81+
use: [
82+
toolApproval({ approved: [] })
83+
]
84+
});
85+
}
86+
```
87+
88+
### 4. Retry Middleware (`retry`)
89+
90+
Automatically retries failed model generations on transient error codes (like `RESOURCE_EXHAUSTED`, `UNAVAILABLE`) using exponential backoff with jitter.
91+
92+
```typescript
93+
import { genkit } from 'genkit';
94+
import { retry } from '@genkit-ai/middleware';
95+
96+
const ai = genkit({ ... });
97+
98+
const response = await ai.generate({
99+
model: googleAI.model('gemini-pro-latest'),
100+
prompt: 'Heavy reasoning task...',
101+
use: [
102+
retry({
103+
maxRetries: 3,
104+
initialDelayMs: 1000,
105+
backoffFactor: 2
106+
})
107+
]
108+
});
109+
```
110+
111+
### 5. Fallback Middleware (`fallback`)
112+
113+
Automatically switches to a different model if the primary model fails on a specific set of error codes. Useful for falling back to a smaller/faster model when a large model exceeds quota limits.
114+
115+
```typescript
116+
import { genkit } from 'genkit';
117+
import { fallback } from '@genkit-ai/middleware';
118+
119+
const ai = genkit({ ... });
120+
121+
const response = await ai.generate({
122+
model: googleAI.model('gemini-pro-latest'),
123+
prompt: 'Try the pro model first...',
124+
use: [
125+
fallback({
126+
models: [googleAI.model('gemini-flash-latest')], // try flash if pro fails
127+
statuses: ['RESOURCE_EXHAUSTED']
128+
})
129+
]
130+
});
131+
```
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { googleAI } from '@genkit-ai/google-genai';
18+
import * as fs from 'fs';
19+
import {
20+
genkit,
21+
restartTool,
22+
type GenerateResponse,
23+
type MessageData,
24+
type ToolRequestPart,
25+
} from 'genkit';
26+
import * as path from 'path';
27+
import * as readline from 'readline';
28+
import { filesystem, retry, skills, toolApproval } from '../src/index.js';
29+
30+
const rl = readline.createInterface({
31+
input: process.stdin,
32+
output: process.stdout,
33+
});
34+
35+
function askQuestion(query: string): Promise<string> {
36+
return new Promise((resolve) => rl.question(query, resolve));
37+
}
38+
39+
async function main() {
40+
const ai = genkit({
41+
plugins: [
42+
googleAI(),
43+
filesystem.plugin(),
44+
skills.plugin(),
45+
toolApproval.plugin(),
46+
retry.plugin(),
47+
],
48+
});
49+
50+
const currentDir = process.cwd();
51+
const fsRoot = path.join(currentDir, 'workspace');
52+
const skillsRoot = path.join(currentDir, 'skills');
53+
54+
// Ensure workspace exists
55+
if (!fs.existsSync(fsRoot)) {
56+
fs.mkdirSync(fsRoot, { recursive: true });
57+
}
58+
59+
console.log('--- Coding Agent ---');
60+
console.log('Type your request. To exit, type "exit".');
61+
62+
let messages: MessageData[] = [
63+
{
64+
role: 'system',
65+
content: [
66+
{
67+
text:
68+
`You are a helpful coding agent. Very terse but thoughtful and careful.\n` +
69+
`Your working directory is in ${fsRoot}, you are not allowed to access anything outside it.\n` +
70+
`Use skills. ALWAYS start by analyzing the current state of the workspace, ` +
71+
`there might be something already there.`,
72+
},
73+
],
74+
},
75+
];
76+
77+
while (true) {
78+
const input = await askQuestion('\n> ');
79+
if (input.trim().toLowerCase() === 'exit') {
80+
break;
81+
}
82+
83+
try {
84+
let interruptRestart: ToolRequestPart[] | undefined;
85+
let response: GenerateResponse;
86+
87+
while (true) {
88+
response = await ai.generate({
89+
model: 'googleai/gemini-flash-latest',
90+
prompt: interruptRestart ? undefined : input,
91+
messages: messages,
92+
resume: interruptRestart ? { restart: interruptRestart } : undefined,
93+
use: [
94+
toolApproval({
95+
approved: ['read_file', 'list_files', 'use_skill'],
96+
}),
97+
skills({ skillPaths: [skillsRoot] }),
98+
filesystem({ rootDirectory: fsRoot }),
99+
],
100+
maxTurns: 20,
101+
});
102+
103+
if (response.finishReason !== 'interrupted') {
104+
break;
105+
}
106+
107+
const interrupts = response.interrupts;
108+
if (!interrupts || interrupts.length === 0) {
109+
console.log('Interrupted but no interrupt record found.');
110+
break;
111+
}
112+
113+
const approvedInterrupts: ToolRequestPart[] = [];
114+
for (const interrupt of interrupts) {
115+
console.log('\n*** Tool Approval Required ***');
116+
console.log(`Tool: ${interrupt.toolRequest.name}`);
117+
console.log(`Input: ${JSON.stringify(interrupt.toolRequest.input)}`);
118+
119+
const approval = await askQuestion('Approve? (y/N): ');
120+
if (approval.trim().toLowerCase() === 'y') {
121+
approvedInterrupts.push(
122+
restartTool(interrupt, { toolApproved: true })
123+
);
124+
}
125+
}
126+
127+
if (approvedInterrupts.length > 0) {
128+
console.log('Resuming...');
129+
interruptRestart = approvedInterrupts;
130+
messages = response.messages;
131+
} else {
132+
console.log('Tool denied.');
133+
break;
134+
}
135+
}
136+
137+
console.log(`\nAI Response:\n${response.text}`);
138+
messages = response.messages;
139+
} catch (e) {
140+
console.error('Error during generation:', e);
141+
}
142+
}
143+
rl.close();
144+
}
145+
146+
main();

js/plugins/middleware/examples/fallback.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { genkit } from 'genkit';
1919
import { fallback } from '../src/index.js';
2020

2121
const ai = genkit({
22-
plugins: [googleAI()],
22+
plugins: [googleAI(), fallback.plugin()],
2323
});
2424

2525
async function main() {

js/plugins/middleware/examples/retry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { genkit } from 'genkit';
1919
import { retry } from '../src/index.js'; // @genkit-ai/middleware
2020

2121
const ai = genkit({
22-
plugins: [googleAI()],
22+
plugins: [googleAI(), retry.plugin()],
2323
});
2424

2525
async function main() {

js/plugins/middleware/src/fallback.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,29 @@ export const FallbackOptionsSchema = z
4040
/**
4141
* An array of models to try in order.
4242
*/
43-
models: z.array(ModelReferenceSchema),
43+
models: z
44+
.array(ModelReferenceSchema)
45+
.describe('An array of models to try in order.'),
4446
/**
4547
* An array of `StatusName` values that should trigger a fallback.
4648
* @default ['UNAVAILABLE', 'DEADLINE_EXCEEDED', 'RESOURCE_EXHAUSTED', 'ABORTED', 'INTERNAL', 'NOT_FOUND', 'UNIMPLEMENTED']
4749
*/
48-
statuses: z.array(z.string()).optional(),
50+
statuses: z
51+
.array(z.string())
52+
.optional()
53+
.describe(
54+
'An array of StatusName values that should trigger a fallback.'
55+
),
4956
/**
5057
* If true, the fallback model will not inherit the original request's configuration.
5158
* @default false
5259
*/
53-
isolateConfig: z.boolean().optional(),
60+
isolateConfig: z
61+
.boolean()
62+
.optional()
63+
.describe(
64+
"If true, the fallback model will not inherit the original request's configuration."
65+
),
5466
})
5567
.passthrough();
5668

@@ -76,6 +88,7 @@ export const fallback: GenerateMiddleware<typeof FallbackOptionsSchema> =
7688
generateMiddleware(
7789
{
7890
name: 'fallback',
91+
description: 'Fallback to a different model on specific error statuses.',
7992
configSchema: FallbackOptionsSchema,
8093
},
8194
(options) => {

0 commit comments

Comments
 (0)