@studio-foundation/runner 0.3.0-beta.1 → 0.3.0-beta.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.
- package/package.json +6 -3
- package/ARCHITECTURE.md +0 -53
- package/configs/agents/analyst.agent.yaml +0 -31
- package/configs/agents/code-generator.agent.yaml +0 -31
- package/configs/agents/generic.agent.yaml +0 -23
- package/src/__tests__/script-executor.test.ts +0 -180
- package/src/index.ts +0 -68
- package/src/integrations/integration-loader.test.ts +0 -88
- package/src/integrations/integration-loader.ts +0 -68
- package/src/middleware/anonymization.ts +0 -38
- package/src/plugins/index.ts +0 -4
- package/src/plugins/mcp-client.test.ts +0 -148
- package/src/plugins/mcp-client.ts +0 -128
- package/src/plugins/oauth-provider.test.ts +0 -167
- package/src/plugins/oauth-provider.ts +0 -175
- package/src/plugins/plugin-loader.test.ts +0 -114
- package/src/plugins/plugin-loader.ts +0 -90
- package/src/prompt-builder.test.ts +0 -167
- package/src/prompt-builder.ts +0 -332
- package/src/providers/anthropic.test.ts +0 -101
- package/src/providers/anthropic.ts +0 -135
- package/src/providers/mock.ts +0 -57
- package/src/providers/ollama.test.ts +0 -166
- package/src/providers/ollama.ts +0 -152
- package/src/providers/openai-responses.ts +0 -212
- package/src/providers/openai.test.ts +0 -67
- package/src/providers/openai.ts +0 -139
- package/src/providers/provider.ts +0 -54
- package/src/providers/registry.ts +0 -77
- package/src/runner.test.ts +0 -343
- package/src/runner.ts +0 -396
- package/src/script-executor.ts +0 -107
- package/src/tools/builtin/git.ts +0 -311
- package/src/tools/builtin/patch.ts +0 -257
- package/src/tools/builtin/repo-manager.ts +0 -142
- package/src/tools/builtin/search.ts +0 -108
- package/src/tools/builtin/shell.ts +0 -82
- package/src/tools/builtin/studio-run.ts +0 -73
- package/src/tools/builtin/web-search.test.ts +0 -122
- package/src/tools/builtin/web-search.ts +0 -101
- package/src/tools/errors.test.ts +0 -12
- package/src/tools/errors.ts +0 -6
- package/src/tools/plugin-loader.test.ts +0 -130
- package/src/tools/plugin-loader.ts +0 -203
- package/src/tools/skills/README.md +0 -49
- package/src/tools/skills/skill-loader.test.ts +0 -106
- package/src/tools/skills/skill-loader.ts +0 -62
- package/src/tools/tool-executor.test.ts +0 -88
- package/src/tools/tool-executor.ts +0 -84
- package/src/tools/tool-registry.ts +0 -130
- package/src/tools/yaml-executor.ts +0 -120
- package/src/utils/race-signal.test.ts +0 -50
- package/src/utils/race-signal.ts +0 -17
- package/templates/integrations/linear.integration.yaml +0 -35
- package/templates/integrations/slack.integration.yaml +0 -22
- package/templates/integrations/webhook.integration.yaml +0 -17
- package/templates/tools/git.tool.yaml +0 -80
- package/templates/tools/repo-manager.tool.yaml +0 -64
- package/templates/tools/search.tool.yaml +0 -22
- package/templates/tools/shell.tool.yaml +0 -19
- package/templates/tools/web-search.tool.yaml +0 -24
- package/tests/anonymization-middleware.test.ts +0 -61
- package/tests/anthropic.test.ts +0 -87
- package/tests/apply-patch.test.ts +0 -355
- package/tests/fixtures/tools/test-builtin.tool.yaml +0 -14
- package/tests/fixtures/tools/test-shell.tool.yaml +0 -19
- package/tests/mock-provider.test.ts +0 -104
- package/tests/openai.test.ts +0 -72
- package/tests/plugin-loader.test.ts +0 -54
- package/tests/prompt-builder.test.ts +0 -468
- package/tests/runner-anonymization.test.ts +0 -89
- package/tests/runner.test.ts +0 -885
- package/tests/studio-run.test.ts +0 -94
- package/tests/tool-executor.test.ts +0 -115
- package/tests/tool-registry.test.ts +0 -84
- package/tests/yaml-executor.test.ts +0 -76
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -7
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Search tool - codebase search using grep
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { exec } from 'child_process';
|
|
6
|
-
import { promisify } from 'util';
|
|
7
|
-
import type { Tool } from '../tool-registry.js';
|
|
8
|
-
|
|
9
|
-
const execAsync = promisify(exec);
|
|
10
|
-
|
|
11
|
-
export function createSearchTools(repoPath: string): Tool[] {
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
14
|
-
name: 'search-search_codebase',
|
|
15
|
-
description: 'Search for a pattern in the codebase (like grep)',
|
|
16
|
-
parameters: {
|
|
17
|
-
type: 'object',
|
|
18
|
-
properties: {
|
|
19
|
-
pattern: {
|
|
20
|
-
type: 'string',
|
|
21
|
-
description: 'Search pattern (string or regex)'
|
|
22
|
-
},
|
|
23
|
-
file_pattern: {
|
|
24
|
-
type: 'string',
|
|
25
|
-
description: 'File glob pattern to filter (e.g., "*.ts", "*.js")',
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
required: ['pattern']
|
|
29
|
-
},
|
|
30
|
-
execute: async ({ pattern, file_pattern }) => {
|
|
31
|
-
try {
|
|
32
|
-
const pat = pattern as string;
|
|
33
|
-
const filePat = file_pattern as string | undefined;
|
|
34
|
-
|
|
35
|
-
// Try ripgrep first, fallback to grep
|
|
36
|
-
let command: string;
|
|
37
|
-
|
|
38
|
-
// Check if ripgrep is available
|
|
39
|
-
try {
|
|
40
|
-
await execAsync('which rg', { cwd: repoPath });
|
|
41
|
-
// Use ripgrep
|
|
42
|
-
command = filePat
|
|
43
|
-
? `rg -n --glob "${filePat}" "${pat}" .`
|
|
44
|
-
: `rg -n "${pat}" .`;
|
|
45
|
-
} catch {
|
|
46
|
-
// Fallback to standard grep
|
|
47
|
-
command = filePat
|
|
48
|
-
? `grep -r -n --include="${filePat}" "${pat}" .`
|
|
49
|
-
: `grep -r -n "${pat}" .`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const { stdout } = await execAsync(command, {
|
|
53
|
-
cwd: repoPath,
|
|
54
|
-
maxBuffer: 1024 * 1024 * 10, // 10MB
|
|
55
|
-
timeout: 30000
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Parse results - format is "file:line:content"
|
|
59
|
-
const lines = stdout.trim().split('\n').filter(l => l.length > 0);
|
|
60
|
-
const matches = lines.slice(0, 50).map(line => {
|
|
61
|
-
const parts = line.split(':');
|
|
62
|
-
if (parts.length >= 3) {
|
|
63
|
-
const [file, lineNum, ...rest] = parts;
|
|
64
|
-
return {
|
|
65
|
-
file,
|
|
66
|
-
line: parseInt(lineNum, 10),
|
|
67
|
-
content: rest.join(':').trim()
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}).filter(m => m !== null);
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
success: true,
|
|
75
|
-
output: {
|
|
76
|
-
pattern,
|
|
77
|
-
matches,
|
|
78
|
-
count: matches.length,
|
|
79
|
-
total_found: lines.length,
|
|
80
|
-
truncated: lines.length > 50
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
} catch (error: unknown) {
|
|
84
|
-
// Exit code 1 typically means no matches found
|
|
85
|
-
if (error && typeof error === 'object' && 'code' in error && error.code === 1) {
|
|
86
|
-
return {
|
|
87
|
-
success: true,
|
|
88
|
-
output: {
|
|
89
|
-
pattern,
|
|
90
|
-
matches: [],
|
|
91
|
-
count: 0,
|
|
92
|
-
total_found: 0,
|
|
93
|
-
truncated: false
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
99
|
-
return {
|
|
100
|
-
success: false,
|
|
101
|
-
output: null,
|
|
102
|
-
error: `Search failed: ${errorMessage}`
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
];
|
|
108
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell tool - command execution with basic safety checks
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { exec } from 'child_process';
|
|
6
|
-
import { promisify } from 'util';
|
|
7
|
-
import type { Tool } from '../tool-registry.js';
|
|
8
|
-
|
|
9
|
-
const execAsync = promisify(exec);
|
|
10
|
-
|
|
11
|
-
export function createShellTools(workingDir: string): Tool[] {
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
14
|
-
name: 'shell-run_command',
|
|
15
|
-
description: 'Run a shell command in the repository directory',
|
|
16
|
-
parameters: {
|
|
17
|
-
type: 'object',
|
|
18
|
-
properties: {
|
|
19
|
-
command: {
|
|
20
|
-
type: 'string',
|
|
21
|
-
description: 'The shell command to run'
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
required: ['command']
|
|
25
|
-
},
|
|
26
|
-
execute: async ({ command }) => {
|
|
27
|
-
try {
|
|
28
|
-
const cmd = command as string;
|
|
29
|
-
|
|
30
|
-
// Basic sanitization - block obviously dangerous commands
|
|
31
|
-
const dangerous = ['rm -rf', 'sudo', 'mkfs', 'dd if=', '> /dev/', 'format'];
|
|
32
|
-
const isDangerous = dangerous.some(pattern => cmd.toLowerCase().includes(pattern));
|
|
33
|
-
|
|
34
|
-
if (isDangerous) {
|
|
35
|
-
return {
|
|
36
|
-
success: false,
|
|
37
|
-
output: null,
|
|
38
|
-
error: 'Dangerous command blocked for safety'
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Execute with timeout of 30 seconds
|
|
43
|
-
const { stdout, stderr } = await execAsync(cmd, {
|
|
44
|
-
cwd: workingDir,
|
|
45
|
-
timeout: 30000,
|
|
46
|
-
maxBuffer: 1024 * 1024 * 10 // 10MB max output
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
success: true,
|
|
51
|
-
output: {
|
|
52
|
-
stdout: stdout.trim(),
|
|
53
|
-
stderr: stderr.trim(),
|
|
54
|
-
exitCode: 0
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
} catch (error: unknown) {
|
|
58
|
-
// Execution error (non-zero exit code or timeout)
|
|
59
|
-
if (error && typeof error === 'object' && 'stdout' in error && 'stderr' in error) {
|
|
60
|
-
const execError = error as { stdout: string; stderr: string; code?: number };
|
|
61
|
-
return {
|
|
62
|
-
success: false,
|
|
63
|
-
output: {
|
|
64
|
-
stdout: execError.stdout?.trim() || '',
|
|
65
|
-
stderr: execError.stderr?.trim() || '',
|
|
66
|
-
exitCode: execError.code || 1
|
|
67
|
-
},
|
|
68
|
-
error: execError.stderr?.trim() || 'Command execution failed'
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
73
|
-
return {
|
|
74
|
-
success: false,
|
|
75
|
-
output: null,
|
|
76
|
-
error: `Shell execution error: ${errorMessage}`
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
];
|
|
82
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { RunSpawner } from '@studio-foundation/contracts';
|
|
2
|
-
import type { Tool } from '../tool-registry.js';
|
|
3
|
-
|
|
4
|
-
interface StudioRunContext {
|
|
5
|
-
spawner: RunSpawner;
|
|
6
|
-
currentRunId: string;
|
|
7
|
-
currentDepth: number;
|
|
8
|
-
maxDepth: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const STUDIO_RUN_PROMPT_SNIPPET = `
|
|
12
|
-
## studio_run tool
|
|
13
|
-
|
|
14
|
-
Use \`studio_run-run_pipeline\` to launch a Studio pipeline run and wait for its result.
|
|
15
|
-
The run executes asynchronously but this tool blocks until completion.
|
|
16
|
-
Use it to orchestrate sub-pipelines (e.g. generate N items by launching N runs).
|
|
17
|
-
`.trim();
|
|
18
|
-
|
|
19
|
-
export function createStudioRunTool(ctx: StudioRunContext): Tool[] {
|
|
20
|
-
return [
|
|
21
|
-
{
|
|
22
|
-
name: 'studio_run-run_pipeline',
|
|
23
|
-
description: 'Launch a Studio pipeline run and wait for completion. Returns the output of the last stage.',
|
|
24
|
-
parameters: {
|
|
25
|
-
type: 'object',
|
|
26
|
-
properties: {
|
|
27
|
-
pipeline: {
|
|
28
|
-
type: 'string',
|
|
29
|
-
description: 'Name of the pipeline to run (e.g. "recipe-developer")',
|
|
30
|
-
},
|
|
31
|
-
input: {
|
|
32
|
-
type: 'object',
|
|
33
|
-
description: 'Input data for the pipeline',
|
|
34
|
-
},
|
|
35
|
-
wait: {
|
|
36
|
-
type: 'boolean',
|
|
37
|
-
description: 'Whether to wait for completion before returning (default: true)',
|
|
38
|
-
default: true,
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
required: ['pipeline', 'input'],
|
|
42
|
-
},
|
|
43
|
-
async execute(args) {
|
|
44
|
-
const pipeline = args['pipeline'] as string;
|
|
45
|
-
const input = args['input'] as Record<string, unknown>;
|
|
46
|
-
const wait = args['wait'] !== false;
|
|
47
|
-
|
|
48
|
-
if (!wait) {
|
|
49
|
-
throw new Error('wait: false is not supported in v1. Use wait: true (default).');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (ctx.currentDepth + 1 > ctx.maxDepth) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`studio-run depth limit reached (max: ${ctx.maxDepth}). ` +
|
|
55
|
-
`Current depth: ${ctx.currentDepth}. Recursive pipeline spawning is not allowed at this level.`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const result = await ctx.spawner.spawnAndWait({
|
|
60
|
-
pipeline,
|
|
61
|
-
input,
|
|
62
|
-
parentRunId: ctx.currentRunId,
|
|
63
|
-
depth: ctx.currentDepth + 1,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
success: result.status === 'success',
|
|
68
|
-
output: result,
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
];
|
|
73
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the web_search builtin tool
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
6
|
-
import { createWebSearchTools, WEB_SEARCH_PROMPT_SNIPPET } from './web-search.js';
|
|
7
|
-
|
|
8
|
-
describe('createWebSearchTools', () => {
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
vi.restoreAllMocks();
|
|
11
|
-
delete process.env.TAVILY_API_KEY;
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('returns a single tool named web_search-search', () => {
|
|
15
|
-
const tools = createWebSearchTools();
|
|
16
|
-
expect(tools).toHaveLength(1);
|
|
17
|
-
expect(tools[0].name).toBe('web_search-search');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('requires query parameter', () => {
|
|
21
|
-
const [tool] = createWebSearchTools();
|
|
22
|
-
const schema = tool.parameters as { required: string[] };
|
|
23
|
-
expect(schema.required).toContain('query');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('returns error when TAVILY_API_KEY is not set', async () => {
|
|
27
|
-
const [tool] = createWebSearchTools();
|
|
28
|
-
const result = await tool.execute({ query: 'test' });
|
|
29
|
-
expect(result.success).toBe(false);
|
|
30
|
-
expect(result.error).toMatch(/TAVILY_API_KEY/);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('calls Tavily API and returns results', async () => {
|
|
34
|
-
process.env.TAVILY_API_KEY = 'test-key';
|
|
35
|
-
const mockResponse = {
|
|
36
|
-
results: [
|
|
37
|
-
{ title: 'Result 1', url: 'https://example.com/1', content: 'Content 1', score: 0.9 },
|
|
38
|
-
{ title: 'Result 2', url: 'https://example.com/2', content: 'Content 2', score: 0.8 },
|
|
39
|
-
],
|
|
40
|
-
answer: 'A direct answer'
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
44
|
-
new Response(JSON.stringify(mockResponse), { status: 200 })
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
const [tool] = createWebSearchTools();
|
|
48
|
-
const result = await tool.execute({ query: 'test query', max_results: 2 });
|
|
49
|
-
|
|
50
|
-
expect(result.success).toBe(true);
|
|
51
|
-
const output = result.output as {
|
|
52
|
-
query: string;
|
|
53
|
-
results: Array<{ title: string; url: string }>;
|
|
54
|
-
answer: string;
|
|
55
|
-
count: number;
|
|
56
|
-
};
|
|
57
|
-
expect(output.query).toBe('test query');
|
|
58
|
-
expect(output.results).toHaveLength(2);
|
|
59
|
-
expect(output.results[0].title).toBe('Result 1');
|
|
60
|
-
expect(output.answer).toBe('A direct answer');
|
|
61
|
-
expect(output.count).toBe(2);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('passes max_results to Tavily API', async () => {
|
|
65
|
-
process.env.TAVILY_API_KEY = 'test-key';
|
|
66
|
-
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
67
|
-
new Response(JSON.stringify({ results: [], answer: null }), { status: 200 })
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const [tool] = createWebSearchTools();
|
|
71
|
-
await tool.execute({ query: 'test', max_results: 3 });
|
|
72
|
-
|
|
73
|
-
const [, options] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
|
|
74
|
-
const body = JSON.parse(options.body as string);
|
|
75
|
-
expect(body.max_results).toBe(3);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('defaults max_results to 5 when not provided', async () => {
|
|
79
|
-
process.env.TAVILY_API_KEY = 'test-key';
|
|
80
|
-
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
81
|
-
new Response(JSON.stringify({ results: [] }), { status: 200 })
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const [tool] = createWebSearchTools();
|
|
85
|
-
await tool.execute({ query: 'test' });
|
|
86
|
-
|
|
87
|
-
const [, options] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
|
|
88
|
-
const body = JSON.parse(options.body as string);
|
|
89
|
-
expect(body.max_results).toBe(5);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('returns error on non-ok API response', async () => {
|
|
93
|
-
process.env.TAVILY_API_KEY = 'test-key';
|
|
94
|
-
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
95
|
-
new Response('Unauthorized', { status: 401 })
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const [tool] = createWebSearchTools();
|
|
99
|
-
const result = await tool.execute({ query: 'test' });
|
|
100
|
-
|
|
101
|
-
expect(result.success).toBe(false);
|
|
102
|
-
expect(result.error).toMatch(/401/);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('returns error on network failure', async () => {
|
|
106
|
-
process.env.TAVILY_API_KEY = 'test-key';
|
|
107
|
-
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
|
|
108
|
-
|
|
109
|
-
const [tool] = createWebSearchTools();
|
|
110
|
-
const result = await tool.execute({ query: 'test' });
|
|
111
|
-
|
|
112
|
-
expect(result.success).toBe(false);
|
|
113
|
-
expect(result.error).toMatch(/Network error/);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('WEB_SEARCH_PROMPT_SNIPPET', () => {
|
|
118
|
-
it('is a non-empty string', () => {
|
|
119
|
-
expect(typeof WEB_SEARCH_PROMPT_SNIPPET).toBe('string');
|
|
120
|
-
expect(WEB_SEARCH_PROMPT_SNIPPET.length).toBeGreaterThan(0);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web search tool - search the web using the Tavily API
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { Tool } from '../tool-registry.js';
|
|
6
|
-
|
|
7
|
-
const TAVILY_API_URL = 'https://api.tavily.com/search';
|
|
8
|
-
|
|
9
|
-
export const WEB_SEARCH_PROMPT_SNIPPET = `
|
|
10
|
-
## web_search tool
|
|
11
|
-
|
|
12
|
-
Use \`web_search-search\` to search the web for up-to-date information.
|
|
13
|
-
Search in the language most relevant to the query (e.g. English for technical docs, French for local content).
|
|
14
|
-
Prefer specific, targeted queries over broad ones for better results.
|
|
15
|
-
`.trim();
|
|
16
|
-
|
|
17
|
-
export function createWebSearchTools(): Tool[] {
|
|
18
|
-
return [
|
|
19
|
-
{
|
|
20
|
-
name: 'web_search-search',
|
|
21
|
-
description: 'Search the web using Tavily and return relevant results',
|
|
22
|
-
parameters: {
|
|
23
|
-
type: 'object',
|
|
24
|
-
properties: {
|
|
25
|
-
query: {
|
|
26
|
-
type: 'string',
|
|
27
|
-
description: 'The search query'
|
|
28
|
-
},
|
|
29
|
-
max_results: {
|
|
30
|
-
type: 'number',
|
|
31
|
-
description: 'Maximum number of results to return (default: 5)'
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
required: ['query']
|
|
35
|
-
},
|
|
36
|
-
execute: async ({ query, max_results }) => {
|
|
37
|
-
const apiKey = process.env.TAVILY_API_KEY;
|
|
38
|
-
if (!apiKey) {
|
|
39
|
-
return {
|
|
40
|
-
success: false,
|
|
41
|
-
output: null,
|
|
42
|
-
error: 'TAVILY_API_KEY environment variable is not set'
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const q = query as string;
|
|
47
|
-
const limit = (max_results as number | undefined) ?? 5;
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const response = await fetch(TAVILY_API_URL, {
|
|
51
|
-
method: 'POST',
|
|
52
|
-
headers: {
|
|
53
|
-
'Content-Type': 'application/json',
|
|
54
|
-
'Authorization': `Bearer ${apiKey}`
|
|
55
|
-
},
|
|
56
|
-
body: JSON.stringify({
|
|
57
|
-
query: q,
|
|
58
|
-
max_results: limit
|
|
59
|
-
})
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
const text = await response.text();
|
|
64
|
-
return {
|
|
65
|
-
success: false,
|
|
66
|
-
output: null,
|
|
67
|
-
error: `Tavily API error ${response.status}: ${text}`
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const data = await response.json() as {
|
|
72
|
-
results?: Array<{ title: string; url: string; content: string; score?: number }>;
|
|
73
|
-
answer?: string;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
success: true,
|
|
78
|
-
output: {
|
|
79
|
-
query: q,
|
|
80
|
-
results: (data.results ?? []).map(r => ({
|
|
81
|
-
title: r.title,
|
|
82
|
-
url: r.url,
|
|
83
|
-
content: r.content,
|
|
84
|
-
score: r.score
|
|
85
|
-
})),
|
|
86
|
-
answer: data.answer ?? null,
|
|
87
|
-
count: (data.results ?? []).length
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
} catch (error: unknown) {
|
|
91
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
92
|
-
return {
|
|
93
|
-
success: false,
|
|
94
|
-
output: null,
|
|
95
|
-
error: `Web search failed: ${errorMessage}`
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
];
|
|
101
|
-
}
|
package/src/tools/errors.test.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// runner/src/tools/errors.test.ts
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { ToolYamlError } from './errors.js';
|
|
4
|
-
|
|
5
|
-
describe('ToolYamlError', () => {
|
|
6
|
-
it('is an Error with name ToolYamlError', () => {
|
|
7
|
-
const err = new ToolYamlError('bad yaml');
|
|
8
|
-
expect(err).toBeInstanceOf(Error);
|
|
9
|
-
expect(err.name).toBe('ToolYamlError');
|
|
10
|
-
expect(err.message).toBe('bad yaml');
|
|
11
|
-
});
|
|
12
|
-
});
|
package/src/tools/errors.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { loadProjectTools } from './plugin-loader.js';
|
|
6
|
-
import { ToolYamlError } from './errors.js';
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
tmpDir = await mkdtemp(join(tmpdir(), 'studio-tool-loader-test-'));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterAll(async () => {
|
|
15
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
async function writeToolYaml(name: string, content: string): Promise<string> {
|
|
19
|
-
const dir = join(tmpDir, name);
|
|
20
|
-
await mkdir(dir, { recursive: true });
|
|
21
|
-
const toolsDir = join(dir, 'tools');
|
|
22
|
-
await mkdir(toolsDir, { recursive: true });
|
|
23
|
-
await writeFile(join(toolsDir, `${name}.tool.yaml`), content);
|
|
24
|
-
return toolsDir;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('loadProjectTools — shell template validation', () => {
|
|
28
|
-
it('throws ToolYamlError when template uses undeclared placeholder', async () => {
|
|
29
|
-
const toolsDir = await writeToolYaml('bad-search', `
|
|
30
|
-
name: bad_search
|
|
31
|
-
version: 1
|
|
32
|
-
commands:
|
|
33
|
-
- name: bad_search-search
|
|
34
|
-
description: Search
|
|
35
|
-
parameters:
|
|
36
|
-
query:
|
|
37
|
-
type: string
|
|
38
|
-
required: true
|
|
39
|
-
execute:
|
|
40
|
-
type: shell
|
|
41
|
-
command: 'curl "https://api.example.com?q={{search_query}}"'
|
|
42
|
-
`);
|
|
43
|
-
const promise = loadProjectTools(toolsDir, '/tmp');
|
|
44
|
-
await expect(promise).rejects.toThrow(ToolYamlError);
|
|
45
|
-
await expect(promise).rejects.toThrow(ToolYamlError);
|
|
46
|
-
await expect(promise).rejects.toThrow(
|
|
47
|
-
"template uses {{search_query}} but no such parameter is declared"
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('loads successfully when all template placeholders are declared', async () => {
|
|
52
|
-
const toolsDir = await writeToolYaml('good-search', `
|
|
53
|
-
name: good_search
|
|
54
|
-
version: 1
|
|
55
|
-
commands:
|
|
56
|
-
- name: good_search-search
|
|
57
|
-
description: Search
|
|
58
|
-
parameters:
|
|
59
|
-
query:
|
|
60
|
-
type: string
|
|
61
|
-
required: true
|
|
62
|
-
execute:
|
|
63
|
-
type: shell
|
|
64
|
-
command: 'curl "https://api.example.com?q={{query}}"'
|
|
65
|
-
`);
|
|
66
|
-
await expect(loadProjectTools(toolsDir, '/tmp')).resolves.toHaveLength(1);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('does not flag {{else}} as an undeclared placeholder', async () => {
|
|
70
|
-
const toolsDir = await writeToolYaml('conditional-tool', `
|
|
71
|
-
name: conditional_tool
|
|
72
|
-
version: 1
|
|
73
|
-
commands:
|
|
74
|
-
- name: conditional_tool-run
|
|
75
|
-
description: Run with optional flag
|
|
76
|
-
parameters:
|
|
77
|
-
verbose:
|
|
78
|
-
type: boolean
|
|
79
|
-
required: false
|
|
80
|
-
execute:
|
|
81
|
-
type: shell
|
|
82
|
-
command: 'echo {{#if verbose}}--verbose{{else}}--quiet{{/if}}'
|
|
83
|
-
`);
|
|
84
|
-
await expect(loadProjectTools(toolsDir, '/tmp')).resolves.toHaveLength(1);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('error message includes filename, command name, and declared parameters', async () => {
|
|
88
|
-
const toolsDir = await writeToolYaml('err-msg', `
|
|
89
|
-
name: err_msg
|
|
90
|
-
version: 1
|
|
91
|
-
commands:
|
|
92
|
-
- name: err_msg-run
|
|
93
|
-
description: Run
|
|
94
|
-
parameters:
|
|
95
|
-
query:
|
|
96
|
-
type: string
|
|
97
|
-
required: true
|
|
98
|
-
execute:
|
|
99
|
-
type: shell
|
|
100
|
-
command: 'curl {{typo}}'
|
|
101
|
-
`);
|
|
102
|
-
const promise = loadProjectTools(toolsDir, '/tmp');
|
|
103
|
-
await expect(promise).rejects.toThrow(ToolYamlError);
|
|
104
|
-
await expect(promise).rejects.toThrow(
|
|
105
|
-
"err-msg.tool.yaml › command 'err_msg-run'"
|
|
106
|
-
);
|
|
107
|
-
await expect(promise).rejects.toThrow(
|
|
108
|
-
"Declared parameters: query"
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('error message shows (none) when command has no parameters declared', async () => {
|
|
113
|
-
const toolsDir = await writeToolYaml('no-params', `
|
|
114
|
-
name: no_params
|
|
115
|
-
version: 1
|
|
116
|
-
commands:
|
|
117
|
-
- name: no_params-run
|
|
118
|
-
description: Run
|
|
119
|
-
parameters: {}
|
|
120
|
-
execute:
|
|
121
|
-
type: shell
|
|
122
|
-
command: 'curl {{typo}}'
|
|
123
|
-
`);
|
|
124
|
-
const promise = loadProjectTools(toolsDir, '/tmp');
|
|
125
|
-
await expect(promise).rejects.toThrow(ToolYamlError);
|
|
126
|
-
await expect(promise).rejects.toThrow(
|
|
127
|
-
"Declared parameters: (none)"
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
});
|