@outputai/llm 0.11.0 → 0.11.1-next.2223fa5.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/package.json +2 -2
- package/src/agent.js +105 -83
- package/src/agent.spec.js +312 -225
- package/src/ai_provider.js +2 -2
- package/src/ai_sdk_options.js +93 -39
- package/src/ai_sdk_options.spec.js +314 -154
- package/src/consts.js +6 -0
- package/src/generate.js +81 -0
- package/src/generate.spec.js +466 -0
- package/src/index.d.ts +222 -242
- package/src/index.js +3 -4
- package/src/prompt/content.js +70 -0
- package/src/prompt/content.spec.js +208 -0
- package/src/prompt/{escape.js → interpolations.js} +11 -18
- package/src/prompt/interpolations.spec.js +109 -0
- package/src/prompt/loader.full.spec.js +293 -0
- package/src/prompt/loader.js +45 -39
- package/src/prompt/loader.spec.js +186 -289
- package/src/prompt/markup/attributes.js +52 -0
- package/src/prompt/markup/attributes.spec.js +132 -0
- package/src/prompt/markup/nodes.js +71 -0
- package/src/prompt/markup/nodes.spec.js +333 -0
- package/src/prompt/markup/tokenizer.js +3 -0
- package/src/prompt/markup/tokenizer.spec.js +149 -0
- package/src/prompt/markup/tokens.js +26 -0
- package/src/prompt/markup/tokens.spec.js +250 -0
- package/src/prompt/validations.js +98 -67
- package/src/prompt/validations.spec.js +204 -47
- package/src/{prompt/load_content.js → utils/file.js} +12 -15
- package/src/utils/file.spec.js +89 -0
- package/src/utils/models.js +15 -0
- package/src/utils/models.spec.js +119 -0
- package/src/utils/skills.js +74 -0
- package/src/utils/skills.spec.js +168 -0
- package/src/utils/sources.js +48 -0
- package/src/utils/sources.spec.js +122 -0
- package/src/utils/stream.js +22 -0
- package/src/utils/stream.spec.js +55 -0
- package/src/utils/tools.js +47 -0
- package/src/utils/tools.spec.js +167 -0
- package/src/utils/wrap.js +148 -0
- package/src/utils/wrap.spec.js +359 -0
- package/src/validations.js +149 -27
- package/src/validations.spec.js +451 -53
- package/src/ai_model.js +0 -60
- package/src/ai_model.spec.js +0 -259
- package/src/ai_sdk.js +0 -92
- package/src/ai_sdk.spec.js +0 -564
- package/src/prompt/block_options.js +0 -58
- package/src/prompt/block_options.spec.js +0 -71
- package/src/prompt/blocks.js +0 -47
- package/src/prompt/blocks.spec.js +0 -63
- package/src/prompt/escape.spec.js +0 -159
- package/src/prompt/load_content.spec.js +0 -83
- package/src/prompt/loader_validation.spec.js +0 -128
- package/src/prompt/parser.js +0 -16
- package/src/prompt/parser.spec.js +0 -186
- package/src/prompt/prepare_text.js +0 -27
- package/src/prompt/prepare_text.spec.js +0 -141
- package/src/prompt/skill.js +0 -128
- package/src/prompt/skill.spec.js +0 -172
- package/src/utils/message.js +0 -3
- package/src/utils/message.spec.js +0 -29
- package/src/utils/response_wrappers.js +0 -100
- package/src/utils/response_wrappers.spec.js +0 -240
- package/src/utils/source_extraction.js +0 -53
- package/src/utils/source_extraction.spec.js +0 -194
- package/src/utils/trace.js +0 -19
- package/src/utils/trace.spec.js +0 -112
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { loadPrompt } from './loader.js';
|
|
2
|
-
import { buildSystemSkillsVar, buildLoadSkillTool, resolvePromptSkills } from './skill.js';
|
|
3
|
-
|
|
4
|
-
export const prepareTextPrompt = ( { prompt, variables, promptDir, skills, tools } ) => {
|
|
5
|
-
const loadedPrompt = loadPrompt( prompt, variables, promptDir );
|
|
6
|
-
|
|
7
|
-
const resolvedSkills = resolvePromptSkills( loadedPrompt, skills );
|
|
8
|
-
|
|
9
|
-
const result = { loadedPrompt, tools: tools ? { ...tools } : null };
|
|
10
|
-
|
|
11
|
-
if ( resolvedSkills.length > 0 ) {
|
|
12
|
-
result.tools = {
|
|
13
|
-
load_skill: buildLoadSkillTool( resolvedSkills ),
|
|
14
|
-
...( result.tools ?? {} )
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const skillsMessage = { role: 'system', content: buildSystemSkillsVar( resolvedSkills ) };
|
|
18
|
-
const systemMessage = loadedPrompt.messages.find( m => m.role === 'system' );
|
|
19
|
-
if ( systemMessage ) {
|
|
20
|
-
systemMessage.content = `${systemMessage.content}\n\n${skillsMessage.content}`;
|
|
21
|
-
} else {
|
|
22
|
-
loadedPrompt.messages.unshift( skillsMessage );
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return result;
|
|
27
|
-
};
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const loadPromptImpl = vi.fn();
|
|
4
|
-
const resolvePromptSkillsImpl = vi.fn();
|
|
5
|
-
const buildLoadSkillToolImpl = vi.fn();
|
|
6
|
-
const buildSystemSkillsVarImpl = vi.fn();
|
|
7
|
-
|
|
8
|
-
vi.mock( './loader.js', () => ( {
|
|
9
|
-
loadPrompt: ( ...args ) => loadPromptImpl( ...args )
|
|
10
|
-
} ) );
|
|
11
|
-
|
|
12
|
-
vi.mock( './skill.js', () => ( {
|
|
13
|
-
resolvePromptSkills: ( ...args ) => resolvePromptSkillsImpl( ...args ),
|
|
14
|
-
buildLoadSkillTool: ( ...args ) => buildLoadSkillToolImpl( ...args ),
|
|
15
|
-
buildSystemSkillsVar: ( ...args ) => buildSystemSkillsVarImpl( ...args )
|
|
16
|
-
} ) );
|
|
17
|
-
|
|
18
|
-
const importSut = async () => import( './prepare_text.js' );
|
|
19
|
-
|
|
20
|
-
const makePrompt = messages => ( {
|
|
21
|
-
name: 'test@v1',
|
|
22
|
-
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
23
|
-
messages,
|
|
24
|
-
promptFileDir: '/prompts'
|
|
25
|
-
} );
|
|
26
|
-
|
|
27
|
-
describe( 'prepareTextPrompt', () => {
|
|
28
|
-
beforeEach( () => {
|
|
29
|
-
vi.resetModules();
|
|
30
|
-
vi.clearAllMocks();
|
|
31
|
-
resolvePromptSkillsImpl.mockReturnValue( [] );
|
|
32
|
-
buildLoadSkillToolImpl.mockReturnValue( { type: 'load-skill-tool' } );
|
|
33
|
-
buildSystemSkillsVarImpl.mockReturnValue( 'Available skills:\n- copy: Copy guidance' );
|
|
34
|
-
} );
|
|
35
|
-
|
|
36
|
-
it( 'loads and returns a prompt with caller tools when no skills resolve', async () => {
|
|
37
|
-
const loadedPrompt = makePrompt( [ { role: 'user', content: 'Hello' } ] );
|
|
38
|
-
const tools = { search: { type: 'search-tool' } };
|
|
39
|
-
loadPromptImpl.mockReturnValue( loadedPrompt );
|
|
40
|
-
|
|
41
|
-
const { prepareTextPrompt } = await importSut();
|
|
42
|
-
const result = prepareTextPrompt( {
|
|
43
|
-
prompt: 'test@v1',
|
|
44
|
-
variables: { name: 'Ada' },
|
|
45
|
-
promptDir: '/workflow',
|
|
46
|
-
skills: [],
|
|
47
|
-
tools
|
|
48
|
-
} );
|
|
49
|
-
|
|
50
|
-
expect( loadPromptImpl ).toHaveBeenCalledWith( 'test@v1', { name: 'Ada' }, '/workflow' );
|
|
51
|
-
expect( resolvePromptSkillsImpl ).toHaveBeenCalledWith( loadedPrompt, [] );
|
|
52
|
-
expect( result ).toEqual( {
|
|
53
|
-
loadedPrompt,
|
|
54
|
-
tools
|
|
55
|
-
} );
|
|
56
|
-
expect( result.tools ).not.toBe( tools );
|
|
57
|
-
expect( buildLoadSkillToolImpl ).not.toHaveBeenCalled();
|
|
58
|
-
expect( buildSystemSkillsVarImpl ).not.toHaveBeenCalled();
|
|
59
|
-
} );
|
|
60
|
-
|
|
61
|
-
it( 'returns null tools when no caller tools or skills resolve', async () => {
|
|
62
|
-
const loadedPrompt = makePrompt( [ { role: 'user', content: 'Hello' } ] );
|
|
63
|
-
loadPromptImpl.mockReturnValue( loadedPrompt );
|
|
64
|
-
|
|
65
|
-
const { prepareTextPrompt } = await importSut();
|
|
66
|
-
const result = prepareTextPrompt( {
|
|
67
|
-
prompt: 'test@v1',
|
|
68
|
-
variables: {},
|
|
69
|
-
skills: []
|
|
70
|
-
} );
|
|
71
|
-
|
|
72
|
-
expect( result ).toEqual( {
|
|
73
|
-
loadedPrompt,
|
|
74
|
-
tools: null
|
|
75
|
-
} );
|
|
76
|
-
expect( buildLoadSkillToolImpl ).not.toHaveBeenCalled();
|
|
77
|
-
expect( buildSystemSkillsVarImpl ).not.toHaveBeenCalled();
|
|
78
|
-
} );
|
|
79
|
-
|
|
80
|
-
it( 'adds load_skill and prepends a system message when skills resolve without an existing system message', async () => {
|
|
81
|
-
const loadedPrompt = makePrompt( [ { role: 'user', content: 'Hello' } ] );
|
|
82
|
-
const resolvedSkills = [ { name: 'copy', description: 'Copy guidance', instructions: '# Copy' } ];
|
|
83
|
-
loadPromptImpl.mockReturnValue( loadedPrompt );
|
|
84
|
-
resolvePromptSkillsImpl.mockReturnValue( resolvedSkills );
|
|
85
|
-
|
|
86
|
-
const { prepareTextPrompt } = await importSut();
|
|
87
|
-
const result = prepareTextPrompt( {
|
|
88
|
-
prompt: 'test@v1',
|
|
89
|
-
variables: {},
|
|
90
|
-
skills: resolvedSkills
|
|
91
|
-
} );
|
|
92
|
-
|
|
93
|
-
expect( buildLoadSkillToolImpl ).toHaveBeenCalledWith( resolvedSkills );
|
|
94
|
-
expect( buildSystemSkillsVarImpl ).toHaveBeenCalledWith( resolvedSkills );
|
|
95
|
-
expect( result.tools.load_skill ).toEqual( { type: 'load-skill-tool' } );
|
|
96
|
-
expect( loadedPrompt.messages ).toEqual( [
|
|
97
|
-
{ role: 'system', content: 'Available skills:\n- copy: Copy guidance' },
|
|
98
|
-
{ role: 'user', content: 'Hello' }
|
|
99
|
-
] );
|
|
100
|
-
} );
|
|
101
|
-
|
|
102
|
-
it( 'merges skill instructions into an existing system message', async () => {
|
|
103
|
-
const loadedPrompt = makePrompt( [
|
|
104
|
-
{ role: 'system', content: 'You are concise.' },
|
|
105
|
-
{ role: 'user', content: 'Hello' }
|
|
106
|
-
] );
|
|
107
|
-
const resolvedSkills = [ { name: 'copy', description: 'Copy guidance', instructions: '# Copy' } ];
|
|
108
|
-
loadPromptImpl.mockReturnValue( loadedPrompt );
|
|
109
|
-
resolvePromptSkillsImpl.mockReturnValue( resolvedSkills );
|
|
110
|
-
|
|
111
|
-
const { prepareTextPrompt } = await importSut();
|
|
112
|
-
prepareTextPrompt( {
|
|
113
|
-
prompt: 'test@v1',
|
|
114
|
-
variables: {},
|
|
115
|
-
skills: resolvedSkills
|
|
116
|
-
} );
|
|
117
|
-
|
|
118
|
-
expect( loadedPrompt.messages ).toEqual( [
|
|
119
|
-
{ role: 'system', content: 'You are concise.\n\nAvailable skills:\n- copy: Copy guidance' },
|
|
120
|
-
{ role: 'user', content: 'Hello' }
|
|
121
|
-
] );
|
|
122
|
-
} );
|
|
123
|
-
|
|
124
|
-
it( 'allows caller tools to override the generated load_skill tool', async () => {
|
|
125
|
-
const loadedPrompt = makePrompt( [ { role: 'user', content: 'Hello' } ] );
|
|
126
|
-
const resolvedSkills = [ { name: 'copy', description: 'Copy guidance', instructions: '# Copy' } ];
|
|
127
|
-
const callerLoadSkill = { type: 'caller-load-skill-tool' };
|
|
128
|
-
loadPromptImpl.mockReturnValue( loadedPrompt );
|
|
129
|
-
resolvePromptSkillsImpl.mockReturnValue( resolvedSkills );
|
|
130
|
-
|
|
131
|
-
const { prepareTextPrompt } = await importSut();
|
|
132
|
-
const result = prepareTextPrompt( {
|
|
133
|
-
prompt: 'test@v1',
|
|
134
|
-
variables: {},
|
|
135
|
-
skills: resolvedSkills,
|
|
136
|
-
tools: { load_skill: callerLoadSkill }
|
|
137
|
-
} );
|
|
138
|
-
|
|
139
|
-
expect( result.tools.load_skill ).toBe( callerLoadSkill );
|
|
140
|
-
} );
|
|
141
|
-
} );
|
package/src/prompt/skill.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { join, resolve, basename } from 'node:path';
|
|
3
|
-
import matter from 'gray-matter';
|
|
4
|
-
import { tool } from 'ai';
|
|
5
|
-
import { z, ValidationError, FatalError } from '@outputai/core';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Create an inline skill instruction package.
|
|
9
|
-
*
|
|
10
|
-
* @param {object} params
|
|
11
|
-
* @param {string} params.name - Skill identifier
|
|
12
|
-
* @param {string} [params.description] - When to use this skill (defaults to name)
|
|
13
|
-
* @param {string} params.instructions - Full instructions returned when LLM calls load_skill
|
|
14
|
-
* @returns {{ name: string, description: string, instructions: string }}
|
|
15
|
-
*/
|
|
16
|
-
export function skill( { name, description, instructions } ) {
|
|
17
|
-
if ( !name ) {
|
|
18
|
-
throw new ValidationError( 'skill() requires a name' );
|
|
19
|
-
}
|
|
20
|
-
if ( !instructions ) {
|
|
21
|
-
throw new ValidationError( 'skill() requires instructions' );
|
|
22
|
-
}
|
|
23
|
-
return { name, description: description ?? name, instructions };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Load a single skill from a markdown file.
|
|
28
|
-
* Frontmatter may provide `name` and `description`; body becomes the instructions.
|
|
29
|
-
*
|
|
30
|
-
* @param {string} filePath - Absolute path to the .md skill file
|
|
31
|
-
* @returns {{ name: string, description: string, instructions: string }}
|
|
32
|
-
*/
|
|
33
|
-
export const loadSkillFile = filePath => {
|
|
34
|
-
const raw = readFileSync( filePath, 'utf-8' );
|
|
35
|
-
const { data, content } = matter( raw );
|
|
36
|
-
return {
|
|
37
|
-
name: data.name ?? basename( filePath, '.md' ),
|
|
38
|
-
description: data.description ?? basename( filePath, '.md' ),
|
|
39
|
-
instructions: content.trim()
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Load skills from an array of paths (files or directories of .md files).
|
|
45
|
-
* Paths are resolved relative to `promptDir`.
|
|
46
|
-
*
|
|
47
|
-
* @param {string[]} skillPaths - Paths from prompt frontmatter (may be files or directories)
|
|
48
|
-
* @param {string} promptDir - Base directory for resolving relative paths
|
|
49
|
-
* @returns {{ name: string, description: string, instructions: string }[]}
|
|
50
|
-
*/
|
|
51
|
-
export const loadPromptSkills = ( skillPaths, promptDir ) => {
|
|
52
|
-
const paths = Array.isArray( skillPaths ) ? skillPaths : [ skillPaths ];
|
|
53
|
-
return paths.flatMap( skillPath => {
|
|
54
|
-
const resolved = resolve( promptDir, skillPath );
|
|
55
|
-
if ( !existsSync( resolved ) ) {
|
|
56
|
-
throw new FatalError( `Skill path not found: "${skillPath}" (resolved to "${resolved}")` );
|
|
57
|
-
}
|
|
58
|
-
if ( statSync( resolved ).isDirectory() ) {
|
|
59
|
-
return readdirSync( resolved )
|
|
60
|
-
.filter( f => f.endsWith( '.md' ) )
|
|
61
|
-
.sort()
|
|
62
|
-
.map( f => loadSkillFile( join( resolved, f ) ) );
|
|
63
|
-
}
|
|
64
|
-
return [ loadSkillFile( resolved ) ];
|
|
65
|
-
} );
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Load skills from a colocated `skills/` directory next to the prompt file.
|
|
70
|
-
* Returns empty array if the directory doesn't exist or has no .md files.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} promptDir - Directory containing the prompt file
|
|
73
|
-
* @returns {{ name: string, description: string, instructions: string }[]}
|
|
74
|
-
*/
|
|
75
|
-
export const loadColocatedSkills = promptDir => {
|
|
76
|
-
const skillsDir = resolve( promptDir, 'skills' );
|
|
77
|
-
if ( !existsSync( skillsDir ) || !statSync( skillsDir ).isDirectory() ) {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
return loadPromptSkills( [ './skills/' ], promptDir );
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Resolve all skills available to a prompt.
|
|
85
|
-
*
|
|
86
|
-
* @param {object} prompt - Loaded prompt object
|
|
87
|
-
* @param {{ name: string, description: string, instructions: string }[]} [callerSkills] - Inline caller-provided skills
|
|
88
|
-
* @returns {{ name: string, description: string, instructions: string }[]}
|
|
89
|
-
*/
|
|
90
|
-
export const resolvePromptSkills = ( prompt, callerSkills = [] ) => {
|
|
91
|
-
const hasExplicitSkills = prompt.config.skills && prompt.promptFileDir;
|
|
92
|
-
const frontmatterSkills = hasExplicitSkills ?
|
|
93
|
-
loadPromptSkills( prompt.config.skills, prompt.promptFileDir ) :
|
|
94
|
-
[];
|
|
95
|
-
const autoSkills = !hasExplicitSkills && prompt.promptFileDir ?
|
|
96
|
-
loadColocatedSkills( prompt.promptFileDir ) :
|
|
97
|
-
[];
|
|
98
|
-
|
|
99
|
-
return [ ...frontmatterSkills, ...autoSkills, ...callerSkills ];
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Build the skills system message content listing available skills.
|
|
104
|
-
*
|
|
105
|
-
* @param {{ name: string, description: string }[]} skills
|
|
106
|
-
* @returns {string}
|
|
107
|
-
*/
|
|
108
|
-
export const buildSystemSkillsVar = skills =>
|
|
109
|
-
'Available skills (use load_skill to get full instructions):\n' +
|
|
110
|
-
skills.map( s => `- ${s.name}: ${s.description}` ).join( '\n' );
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Build the `load_skill` AI SDK tool that the LLM calls to retrieve full skill instructions.
|
|
114
|
-
*
|
|
115
|
-
* @param {{ name: string, instructions: string }[]} skills
|
|
116
|
-
* @returns {import('ai').Tool}
|
|
117
|
-
*/
|
|
118
|
-
export const buildLoadSkillTool = skills => tool( {
|
|
119
|
-
description: 'Get detailed instructions for a named skill',
|
|
120
|
-
inputSchema: z.object( { name: z.string().describe( 'Name of the skill to load' ) } ),
|
|
121
|
-
execute: ( { name } ) => {
|
|
122
|
-
const sk = skills.find( s => s.name === name );
|
|
123
|
-
if ( !sk ) {
|
|
124
|
-
return `Skill "${name}" not found. Available: ${skills.map( s => s.name ).join( ', ' )}`;
|
|
125
|
-
}
|
|
126
|
-
return sk.instructions;
|
|
127
|
-
}
|
|
128
|
-
} );
|
package/src/prompt/skill.spec.js
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { FatalError, ValidationError } from '@outputai/core';
|
|
6
|
-
|
|
7
|
-
const aiMocks = vi.hoisted( () => ( {
|
|
8
|
-
tool: vi.fn( def => ( { ...def, _tool: true } ) )
|
|
9
|
-
} ) );
|
|
10
|
-
|
|
11
|
-
vi.mock( 'ai', () => ( {
|
|
12
|
-
tool: ( ...args ) => aiMocks.tool( ...args )
|
|
13
|
-
} ) );
|
|
14
|
-
|
|
15
|
-
const importSut = async () => import( './skill.js' );
|
|
16
|
-
|
|
17
|
-
const makeTempDir = () => mkdtempSync( join( tmpdir(), 'skill-test-' ) );
|
|
18
|
-
|
|
19
|
-
describe( 'skill', () => {
|
|
20
|
-
it( 'creates an inline skill with a default description', async () => {
|
|
21
|
-
const { skill } = await importSut();
|
|
22
|
-
|
|
23
|
-
expect( skill( { name: 'writer', instructions: '# Writer' } ) ).toEqual( {
|
|
24
|
-
name: 'writer',
|
|
25
|
-
description: 'writer',
|
|
26
|
-
instructions: '# Writer'
|
|
27
|
-
} );
|
|
28
|
-
} );
|
|
29
|
-
|
|
30
|
-
it( 'throws when name or instructions are missing', async () => {
|
|
31
|
-
const { skill } = await importSut();
|
|
32
|
-
|
|
33
|
-
expect( () => skill( { instructions: '# Missing name' } ) ).toThrow( ValidationError );
|
|
34
|
-
expect( () => skill( { name: 'missing_instructions' } ) ).toThrow( ValidationError );
|
|
35
|
-
} );
|
|
36
|
-
} );
|
|
37
|
-
|
|
38
|
-
describe( 'skill file loading', () => {
|
|
39
|
-
it( 'loads a skill file using frontmatter metadata', async () => {
|
|
40
|
-
const dir = makeTempDir();
|
|
41
|
-
const filePath = join( dir, 'copy.md' );
|
|
42
|
-
writeFileSync( filePath, `---
|
|
43
|
-
name: copywriter
|
|
44
|
-
description: Writes concise copy
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
# Copy
|
|
48
|
-
Write clearly.
|
|
49
|
-
` );
|
|
50
|
-
|
|
51
|
-
const { loadSkillFile } = await importSut();
|
|
52
|
-
|
|
53
|
-
expect( loadSkillFile( filePath ) ).toEqual( {
|
|
54
|
-
name: 'copywriter',
|
|
55
|
-
description: 'Writes concise copy',
|
|
56
|
-
instructions: '# Copy\nWrite clearly.'
|
|
57
|
-
} );
|
|
58
|
-
} );
|
|
59
|
-
|
|
60
|
-
it( 'uses the markdown filename when frontmatter omits name and description', async () => {
|
|
61
|
-
const dir = makeTempDir();
|
|
62
|
-
const filePath = join( dir, 'research.md' );
|
|
63
|
-
writeFileSync( filePath, '# Research\nSearch carefully.\n' );
|
|
64
|
-
|
|
65
|
-
const { loadSkillFile } = await importSut();
|
|
66
|
-
|
|
67
|
-
expect( loadSkillFile( filePath ) ).toEqual( {
|
|
68
|
-
name: 'research',
|
|
69
|
-
description: 'research',
|
|
70
|
-
instructions: '# Research\nSearch carefully.'
|
|
71
|
-
} );
|
|
72
|
-
} );
|
|
73
|
-
|
|
74
|
-
it( 'loads skills from a file path and a directory path, sorting directory markdown files', async () => {
|
|
75
|
-
const promptDir = makeTempDir();
|
|
76
|
-
const skillsDir = join( promptDir, 'skills' );
|
|
77
|
-
mkdirSync( skillsDir );
|
|
78
|
-
writeFileSync( join( promptDir, 'single.md' ), '# Single' );
|
|
79
|
-
writeFileSync( join( skillsDir, 'b.md' ), '# B' );
|
|
80
|
-
writeFileSync( join( skillsDir, 'a.md' ), '# A' );
|
|
81
|
-
writeFileSync( join( skillsDir, 'ignore.txt' ), '# Ignored' );
|
|
82
|
-
|
|
83
|
-
const { loadPromptSkills } = await importSut();
|
|
84
|
-
const result = loadPromptSkills( [ './single.md', './skills' ], promptDir );
|
|
85
|
-
|
|
86
|
-
expect( result.map( s => s.name ) ).toEqual( [ 'single', 'a', 'b' ] );
|
|
87
|
-
} );
|
|
88
|
-
|
|
89
|
-
it( 'throws FatalError for a missing skill path', async () => {
|
|
90
|
-
const { loadPromptSkills } = await importSut();
|
|
91
|
-
|
|
92
|
-
expect( () => loadPromptSkills( './missing.md', makeTempDir() ) ).toThrow( FatalError );
|
|
93
|
-
} );
|
|
94
|
-
|
|
95
|
-
it( 'loads colocated skills and returns an empty array when no skills directory exists', async () => {
|
|
96
|
-
const promptDir = makeTempDir();
|
|
97
|
-
const skillsDir = join( promptDir, 'skills' );
|
|
98
|
-
mkdirSync( skillsDir );
|
|
99
|
-
writeFileSync( join( skillsDir, 'style.md' ), '# Style' );
|
|
100
|
-
|
|
101
|
-
const { loadColocatedSkills } = await importSut();
|
|
102
|
-
|
|
103
|
-
expect( loadColocatedSkills( promptDir ).map( s => s.name ) ).toEqual( [ 'style' ] );
|
|
104
|
-
expect( loadColocatedSkills( makeTempDir() ) ).toEqual( [] );
|
|
105
|
-
} );
|
|
106
|
-
} );
|
|
107
|
-
|
|
108
|
-
describe( 'resolvePromptSkills', () => {
|
|
109
|
-
it( 'uses explicit prompt skills and skips colocated auto-discovery', async () => {
|
|
110
|
-
const promptDir = makeTempDir();
|
|
111
|
-
const explicitDir = join( promptDir, 'explicit' );
|
|
112
|
-
const autoDir = join( promptDir, 'skills' );
|
|
113
|
-
mkdirSync( explicitDir );
|
|
114
|
-
mkdirSync( autoDir );
|
|
115
|
-
writeFileSync( join( explicitDir, 'explicit.md' ), '# Explicit' );
|
|
116
|
-
writeFileSync( join( autoDir, 'auto.md' ), '# Auto' );
|
|
117
|
-
const callerSkill = { name: 'caller', description: 'Caller', instructions: '# Caller' };
|
|
118
|
-
|
|
119
|
-
const { resolvePromptSkills } = await importSut();
|
|
120
|
-
const result = resolvePromptSkills( {
|
|
121
|
-
config: { skills: [ './explicit' ] },
|
|
122
|
-
promptFileDir: promptDir
|
|
123
|
-
}, [ callerSkill ] );
|
|
124
|
-
|
|
125
|
-
expect( result.map( s => s.name ) ).toEqual( [ 'explicit', 'caller' ] );
|
|
126
|
-
} );
|
|
127
|
-
|
|
128
|
-
it( 'auto-discovers colocated skills when prompt config has no explicit skills', async () => {
|
|
129
|
-
const promptDir = makeTempDir();
|
|
130
|
-
const skillsDir = join( promptDir, 'skills' );
|
|
131
|
-
mkdirSync( skillsDir );
|
|
132
|
-
writeFileSync( join( skillsDir, 'auto.md' ), '# Auto' );
|
|
133
|
-
|
|
134
|
-
const { resolvePromptSkills } = await importSut();
|
|
135
|
-
const result = resolvePromptSkills( { config: {}, promptFileDir: promptDir } );
|
|
136
|
-
|
|
137
|
-
expect( result.map( s => s.name ) ).toEqual( [ 'auto' ] );
|
|
138
|
-
} );
|
|
139
|
-
} );
|
|
140
|
-
|
|
141
|
-
describe( 'skill prompt helpers', () => {
|
|
142
|
-
it( 'builds the system skills message', async () => {
|
|
143
|
-
const { buildSystemSkillsVar } = await importSut();
|
|
144
|
-
|
|
145
|
-
expect( buildSystemSkillsVar( [
|
|
146
|
-
{ name: 'copy', description: 'Copywriting' },
|
|
147
|
-
{ name: 'research', description: 'Research' }
|
|
148
|
-
] ) ).toBe(
|
|
149
|
-
'Available skills (use load_skill to get full instructions):\n' +
|
|
150
|
-
'- copy: Copywriting\n' +
|
|
151
|
-
'- research: Research'
|
|
152
|
-
);
|
|
153
|
-
} );
|
|
154
|
-
|
|
155
|
-
it( 'builds a load_skill tool that returns instructions or an available-skills message', async () => {
|
|
156
|
-
const skills = [
|
|
157
|
-
{ name: 'copy', description: 'Copywriting', instructions: '# Copy' },
|
|
158
|
-
{ name: 'research', description: 'Research', instructions: '# Research' }
|
|
159
|
-
];
|
|
160
|
-
|
|
161
|
-
const { buildLoadSkillTool } = await importSut();
|
|
162
|
-
const result = buildLoadSkillTool( skills );
|
|
163
|
-
|
|
164
|
-
expect( aiMocks.tool ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
165
|
-
description: 'Get detailed instructions for a named skill',
|
|
166
|
-
inputSchema: expect.any( Object ),
|
|
167
|
-
execute: expect.any( Function )
|
|
168
|
-
} ) );
|
|
169
|
-
expect( result.execute( { name: 'copy' } ) ).toBe( '# Copy' );
|
|
170
|
-
expect( result.execute( { name: 'missing' } ) ).toBe( 'Skill "missing" not found. Available: copy, research' );
|
|
171
|
-
} );
|
|
172
|
-
} );
|
package/src/utils/message.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ROLE, getContent, isRole } from './message.js';
|
|
3
|
-
|
|
4
|
-
describe( 'message utils', () => {
|
|
5
|
-
describe( 'ROLE', () => {
|
|
6
|
-
it( 'exposes expected role string constants', () => {
|
|
7
|
-
expect( ROLE ).toEqual( {
|
|
8
|
-
SYSTEM: 'system',
|
|
9
|
-
USER: 'user',
|
|
10
|
-
ASSISTANT: 'assistant',
|
|
11
|
-
TOOL: 'tool'
|
|
12
|
-
} );
|
|
13
|
-
} );
|
|
14
|
-
} );
|
|
15
|
-
|
|
16
|
-
describe( 'isRole', () => {
|
|
17
|
-
it( 'returns a predicate that matches messages by role', () => {
|
|
18
|
-
const isUser = isRole( ROLE.USER );
|
|
19
|
-
expect( isUser( { role: 'user', content: 'hi' } ) ).toBe( true );
|
|
20
|
-
expect( isUser( { role: 'assistant', content: 'bye' } ) ).toBe( false );
|
|
21
|
-
} );
|
|
22
|
-
} );
|
|
23
|
-
|
|
24
|
-
describe( 'getContent', () => {
|
|
25
|
-
it( 'returns message content', () => {
|
|
26
|
-
expect( getContent( { role: 'user', content: 'hello' } ) ).toBe( 'hello' );
|
|
27
|
-
} );
|
|
28
|
-
} );
|
|
29
|
-
} );
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { combineSources, extractSourcesFromSteps } from './source_extraction.js';
|
|
2
|
-
import { calculateLLMCallCost } from '../cost/index.js';
|
|
3
|
-
import { endTraceWithSuccess } from './trace.js';
|
|
4
|
-
import { calculateBase64FileSize } from './image.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Calculates the cost and wraps an AI SDK text response in a Proxy with shortcut for 'result' and 'cost'
|
|
8
|
-
*
|
|
9
|
-
* Emits the `cost:llm:request` event.
|
|
10
|
-
*
|
|
11
|
-
* Also finishes the trace events.
|
|
12
|
-
*
|
|
13
|
-
* @param {object} args
|
|
14
|
-
* @param {string} args.traceId - id created by the startTrace
|
|
15
|
-
* @param {string} args.providerId - id of the provider used
|
|
16
|
-
* @param {string} args.modelId - id of the model used
|
|
17
|
-
* @param {object} args.response - AI SDK's text response
|
|
18
|
-
* @returns {object} Proxied response
|
|
19
|
-
*/
|
|
20
|
-
export const wrapTextResponse = async ( { traceId, providerId, modelId, response } ) => {
|
|
21
|
-
const { totalUsage: usage, providerMetadata, text: result, steps, sources } = response;
|
|
22
|
-
|
|
23
|
-
const cost = await calculateLLMCallCost( { usage, modelId, providerId } );
|
|
24
|
-
const sourcesFromTools = extractSourcesFromSteps( steps );
|
|
25
|
-
|
|
26
|
-
endTraceWithSuccess( { traceId, usage, cost, result, providerMetadata, sourcesFromTools } );
|
|
27
|
-
|
|
28
|
-
return new Proxy( response, {
|
|
29
|
-
get( target, prop, receiver ) {
|
|
30
|
-
if ( prop === 'result' ) {
|
|
31
|
-
return target.text;
|
|
32
|
-
}
|
|
33
|
-
if ( prop === 'cost' ) {
|
|
34
|
-
return cost;
|
|
35
|
-
}
|
|
36
|
-
if ( prop === 'sources' && sourcesFromTools.length > 0 ) {
|
|
37
|
-
return combineSources( { sourcesFromTools, sourcesFromResponse: sources } );
|
|
38
|
-
}
|
|
39
|
-
return Reflect.get( target, prop, receiver );
|
|
40
|
-
}
|
|
41
|
-
} );
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Wraps the response returned by the onFinish callback from the stream.
|
|
46
|
-
*
|
|
47
|
-
* When the onFinish is triggered, concludes the trace event, calculates cost and emits `cost:llm:request`.
|
|
48
|
-
* Returns a proxy around the response with `cost` property.
|
|
49
|
-
*
|
|
50
|
-
* @param {object} args
|
|
51
|
-
* @param {string} args.traceId - id created by the startTrace
|
|
52
|
-
* @param {string} args.providerId - id of the provider used
|
|
53
|
-
* @param {string} args.modelId - id of the model used
|
|
54
|
-
* @param {Function} args.onFinish - Original callback to call with the proxied response
|
|
55
|
-
* @returns {object} Proxied response
|
|
56
|
-
*/
|
|
57
|
-
export const wrapStreamOnFinishResponse = ( { traceId, providerId, modelId, onFinish: _onFinish } ) => ( {
|
|
58
|
-
async onFinish( response ) {
|
|
59
|
-
const proxiedResponse = await wrapTextResponse( { traceId, providerId, modelId, response } );
|
|
60
|
-
_onFinish?.( proxiedResponse );
|
|
61
|
-
}
|
|
62
|
-
} );
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Calculates the cost and wraps an AI SDK image response in a Proxy with shortcut for 'result' and 'cost'
|
|
66
|
-
*
|
|
67
|
-
* Emits the `cost:llm:request` event.
|
|
68
|
-
*
|
|
69
|
-
* Also finishes the trace events.
|
|
70
|
-
*
|
|
71
|
-
* @param {object} args
|
|
72
|
-
* @param {string} args.traceId - id created by the startTrace
|
|
73
|
-
* @param {string} args.providerId - id of the provider used
|
|
74
|
-
* @param {string} args.modelId - id of the model used
|
|
75
|
-
* @param {object} args.response - AI SDK's image response
|
|
76
|
-
* @returns {object} Proxied response
|
|
77
|
-
*/
|
|
78
|
-
export const wrapImageResponse = async ( { traceId, providerId, modelId, response } ) => {
|
|
79
|
-
const { usage, providerMetadata } = response;
|
|
80
|
-
const cost = await calculateLLMCallCost( { usage, providerId, modelId } );
|
|
81
|
-
|
|
82
|
-
const result = response.images.map( ( { mediaType, base64 } ) => ( {
|
|
83
|
-
size: calculateBase64FileSize( base64 ),
|
|
84
|
-
mediaType
|
|
85
|
-
} ) );
|
|
86
|
-
|
|
87
|
-
endTraceWithSuccess( { traceId, usage, cost, result, providerMetadata } );
|
|
88
|
-
|
|
89
|
-
return new Proxy( response, {
|
|
90
|
-
get( target, prop, receiver ) {
|
|
91
|
-
if ( prop === 'result' ) {
|
|
92
|
-
return target.image;
|
|
93
|
-
}
|
|
94
|
-
if ( prop === 'cost' ) {
|
|
95
|
-
return cost;
|
|
96
|
-
}
|
|
97
|
-
return Reflect.get( target, prop, receiver );
|
|
98
|
-
}
|
|
99
|
-
} );
|
|
100
|
-
};
|