@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.
Files changed (69) hide show
  1. package/package.json +2 -2
  2. package/src/agent.js +105 -83
  3. package/src/agent.spec.js +312 -225
  4. package/src/ai_provider.js +2 -2
  5. package/src/ai_sdk_options.js +93 -39
  6. package/src/ai_sdk_options.spec.js +314 -154
  7. package/src/consts.js +6 -0
  8. package/src/generate.js +81 -0
  9. package/src/generate.spec.js +466 -0
  10. package/src/index.d.ts +222 -242
  11. package/src/index.js +3 -4
  12. package/src/prompt/content.js +70 -0
  13. package/src/prompt/content.spec.js +208 -0
  14. package/src/prompt/{escape.js → interpolations.js} +11 -18
  15. package/src/prompt/interpolations.spec.js +109 -0
  16. package/src/prompt/loader.full.spec.js +293 -0
  17. package/src/prompt/loader.js +45 -39
  18. package/src/prompt/loader.spec.js +186 -289
  19. package/src/prompt/markup/attributes.js +52 -0
  20. package/src/prompt/markup/attributes.spec.js +132 -0
  21. package/src/prompt/markup/nodes.js +71 -0
  22. package/src/prompt/markup/nodes.spec.js +333 -0
  23. package/src/prompt/markup/tokenizer.js +3 -0
  24. package/src/prompt/markup/tokenizer.spec.js +149 -0
  25. package/src/prompt/markup/tokens.js +26 -0
  26. package/src/prompt/markup/tokens.spec.js +250 -0
  27. package/src/prompt/validations.js +98 -67
  28. package/src/prompt/validations.spec.js +204 -47
  29. package/src/{prompt/load_content.js → utils/file.js} +12 -15
  30. package/src/utils/file.spec.js +89 -0
  31. package/src/utils/models.js +15 -0
  32. package/src/utils/models.spec.js +119 -0
  33. package/src/utils/skills.js +74 -0
  34. package/src/utils/skills.spec.js +168 -0
  35. package/src/utils/sources.js +48 -0
  36. package/src/utils/sources.spec.js +122 -0
  37. package/src/utils/stream.js +22 -0
  38. package/src/utils/stream.spec.js +55 -0
  39. package/src/utils/tools.js +47 -0
  40. package/src/utils/tools.spec.js +167 -0
  41. package/src/utils/wrap.js +148 -0
  42. package/src/utils/wrap.spec.js +359 -0
  43. package/src/validations.js +149 -27
  44. package/src/validations.spec.js +451 -53
  45. package/src/ai_model.js +0 -60
  46. package/src/ai_model.spec.js +0 -259
  47. package/src/ai_sdk.js +0 -92
  48. package/src/ai_sdk.spec.js +0 -564
  49. package/src/prompt/block_options.js +0 -58
  50. package/src/prompt/block_options.spec.js +0 -71
  51. package/src/prompt/blocks.js +0 -47
  52. package/src/prompt/blocks.spec.js +0 -63
  53. package/src/prompt/escape.spec.js +0 -159
  54. package/src/prompt/load_content.spec.js +0 -83
  55. package/src/prompt/loader_validation.spec.js +0 -128
  56. package/src/prompt/parser.js +0 -16
  57. package/src/prompt/parser.spec.js +0 -186
  58. package/src/prompt/prepare_text.js +0 -27
  59. package/src/prompt/prepare_text.spec.js +0 -141
  60. package/src/prompt/skill.js +0 -128
  61. package/src/prompt/skill.spec.js +0 -172
  62. package/src/utils/message.js +0 -3
  63. package/src/utils/message.spec.js +0 -29
  64. package/src/utils/response_wrappers.js +0 -100
  65. package/src/utils/response_wrappers.spec.js +0 -240
  66. package/src/utils/source_extraction.js +0 -53
  67. package/src/utils/source_extraction.spec.js +0 -194
  68. package/src/utils/trace.js +0 -19
  69. 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
- } );
@@ -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
- } );
@@ -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
- } );
@@ -1,3 +0,0 @@
1
- export const ROLE = { SYSTEM: 'system', USER: 'user', ASSISTANT: 'assistant', TOOL: 'tool' };
2
- export const isRole = role => msg => msg.role === role;
3
- export const getContent = msg => msg.content;
@@ -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
- };