feat: add instrument listing, track creation and update tools to AI agent

This commit is contained in:
Xiaohan-Tian
2026-06-04 23:14:15 -07:00
parent 8ee7ddac77
commit 7d53a9cd82
10 changed files with 836 additions and 9 deletions
+20 -1
View File
@@ -253,6 +253,21 @@ describe('AgentCore todo integration', () => {
expect(provider.systemPrompts[0]).toBe('system prompt:prompts/system.md');
});
it('exposes the new track management tools in regular mode', async () => {
configState.set('general.agent_mode', 'regular');
const provider = new ScriptedProvider([
[{ type: 'done', content: '', finishReason: 'stop' }],
]);
AgentCore.instance().setLLMProvider(provider);
await collectChunks('Inspect available tools.');
const toolNames = provider.tools[0].map(tool => tool.function.name);
expect(toolNames).toContain('list_all_available_instruments');
expect(toolNames).toContain('create_new_track');
expect(toolNames).toContain('update_track');
});
it('uses the compact system prompt in efficient mode', async () => {
configState.set('general.agent_mode', 'efficient');
const provider = new ScriptedProvider([
@@ -290,7 +305,11 @@ describe('AgentCore todo integration', () => {
await collectChunks('Read the current region.');
expect(provider.tools[0].map(tool => tool.function.name)).not.toContain('read_music');
const toolNames = provider.tools[0].map(tool => tool.function.name);
expect(toolNames).not.toContain('read_music');
expect(toolNames).not.toContain('list_all_available_instruments');
expect(toolNames).not.toContain('create_new_track');
expect(toolNames).not.toContain('update_track');
spy.mockRestore();
});
@@ -0,0 +1,77 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CreateNewTrackTool } from './CreateNewTrackTool';
import { KGCore } from '../../core/KGCore';
import { KGProject } from '../../core/KGProject';
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
import { KGMidiTrack } from '../../core/track/KGMidiTrack';
vi.mock('../../stores/projectStore', () => ({
useProjectStore: {
getState: () => ({
activeRegionId: null,
selectedRegionIds: [],
selectedTrackId: null,
}),
},
}));
function mockCore(project: KGProject) {
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
executeCommand: (command: { execute(): void }) => command.execute(),
} as unknown as KGCore);
}
describe('CreateNewTrackTool', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(KGAudioInterface, 'instance').mockReturnValue({
createTrackSynth: vi.fn(),
} as unknown as KGAudioInterface);
});
it('creates a new track and returns the exact output shape', async () => {
const project = new KGProject('create-track-project');
project.setTracks([new KGMidiTrack('Lead', 1, 'trumpet')]);
mockCore(project);
const tool = new CreateNewTrackTool();
const result = await tool.execute({
track_name: 'Bass',
instrument: 'Electric Bass (finger)',
});
expect(result).toEqual({
success: true,
result: 'New track created:\ntrack_id: 2\ntrack_name: Bass\ninstrument: Electric Bass (finger)',
});
expect(project.getTracks()).toHaveLength(2);
expect(project.getTracks()[1]).toBeInstanceOf(KGMidiTrack);
expect((project.getTracks()[1] as KGMidiTrack).getInstrument()).toBe('electric_bass_finger');
expect(tool.isReadOnlyTool()).toBe(false);
expect(tool.isAvailableInEfficientMode()).toBe(false);
expect(tool.buildToolResultDisplayContent({
track_name: 'Bass',
instrument: 'Electric Bass (finger)',
}, result)).toBe(
'New track created:\n- track_id: 2\n- track_name: Bass\n- instrument: Electric Bass (finger)',
);
});
it('rejects an invalid instrument name', async () => {
const project = new KGProject('invalid-instrument-project');
mockCore(project);
const tool = new CreateNewTrackTool();
const result = await tool.execute({
track_name: 'Bass',
instrument: 'electric_bass_finger',
});
expect(result).toEqual({
success: false,
result: 'Invalid instrument "electric_bass_finger". Use the exact English name from list_all_available_instruments.',
});
expect(project.getTracks()).toHaveLength(0);
});
});
+97
View File
@@ -0,0 +1,97 @@
import { AddTrackCommand } from '../../core/commands/track/AddTrackCommand';
import { BaseTool } from './BaseTool';
import type { ToolParameter, ToolResult } from './BaseTool';
import {
getEnglishInstrumentName,
resolveInstrumentKeyByEnglishName,
} from './toolTargeting';
export class CreateNewTrackTool extends BaseTool {
readonly name = 'create_new_track';
readonly description =
'Create a new MIDI track with a given track name and exact English instrument name. Use list_all_available_instruments first to discover valid instrument names.';
readonly parameters: Record<string, ToolParameter> = {
track_name: {
type: 'string',
description: 'Name of the new MIDI track to create.',
required: true,
},
instrument: {
type: 'string',
description: 'Exact English instrument name from list_all_available_instruments.',
required: true,
},
};
override isReadOnlyTool(): boolean {
return false;
}
override isAvailableInEfficientMode(): boolean {
return false;
}
override buildConfirmationContent(args: Record<string, unknown> | null): string | undefined {
if (!args) {
return undefined;
}
const trackName = typeof args.track_name === 'string' ? args.track_name : null;
const instrument = typeof args.instrument === 'string' ? args.instrument : null;
if (!trackName || !instrument) {
return undefined;
}
return `Allow creating track **${trackName}** with instrument **${instrument}**?`;
}
override buildToolResultDisplayContent(args: Record<string, unknown> | null, toolResult: ToolResult): string | undefined {
if (!args || !toolResult.success) {
return undefined;
}
const trackName = typeof args.track_name === 'string' ? args.track_name : null;
const instrument = typeof args.instrument === 'string' ? args.instrument : null;
if (!trackName || !instrument) {
return undefined;
}
const trackIdMatch = toolResult.result.match(/track_id:\s*(\d+)/);
if (!trackIdMatch) {
return undefined;
}
return [
'New track created:',
`- track_id: ${trackIdMatch[1]}`,
`- track_name: ${trackName}`,
`- instrument: ${instrument}`,
].join('\n');
}
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
this.validateParameters(params);
const trackName = params.track_name as string;
const instrumentName = params.instrument as string;
const instrumentKey = resolveInstrumentKeyByEnglishName(instrumentName);
if (!instrumentKey) {
return this.createErrorResult(`Invalid instrument "${instrumentName}". Use the exact English name from list_all_available_instruments.`);
}
const command = new AddTrackCommand(undefined, trackName, instrumentKey);
await this.executeCommand(command);
return this.createSuccessResult([
'New track created:',
`track_id: ${command.getTrackId().toString()}`,
`track_name: ${trackName}`,
`instrument: ${getEnglishInstrumentName(instrumentKey)}`,
].join('\n'));
} catch (error) {
return this.createErrorResult(`Failed to create track: ${error}`);
}
}
}
@@ -0,0 +1,44 @@
import { describe, expect, it, vi } from 'vitest';
import { ListAllAvailableInstrumentsTool } from './ListAllAvailableInstrumentsTool';
import { listAvailableInstrumentsByGroup } from './toolTargeting';
vi.mock('../../stores/projectStore', () => ({
useProjectStore: {
getState: () => ({
activeRegionId: null,
selectedRegionIds: [],
selectedTrackId: null,
}),
},
}));
describe('ListAllAvailableInstrumentsTool', () => {
it('lists instruments grouped by English group name with blank lines between groups', async () => {
const tool = new ListAllAvailableInstrumentsTool();
const result = await tool.execute({});
expect(result.success).toBe(true);
expect(result.result.startsWith(
'Group: Piano and Keyboards\n- Acoustic Grand Piano\n- Bright Acoustic Piano',
)).toBe(true);
expect(result.result).toContain('\n\nGroup: Guitar\n- Acoustic Guitar (nylon)');
expect(result.result).toContain('\n\nGroup: Bass\n- Acoustic Bass');
expect(result.result).toContain('\n\nGroup: Percussion Kit\n- Standard Drum Kit');
expect(result.result).toContain('\n\nGroup: Synthesizer\n- Lead 1 (square)');
});
it('is unavailable in efficient mode', () => {
const tool = new ListAllAvailableInstrumentsTool();
expect(tool.isAvailableInEfficientMode()).toBe(false);
});
it('returns a simplified UI display message', async () => {
const tool = new ListAllAvailableInstrumentsTool();
const result = await tool.execute({});
const totalInstruments = listAvailableInstrumentsByGroup()
.reduce((count, group) => count + group.instruments.length, 0);
expect(tool.buildToolResultDisplayContent({}, result)).toBe(`Listed ${totalInstruments} available instruments.`);
});
});
@@ -0,0 +1,40 @@
import { BaseTool } from './BaseTool';
import type { ToolParameter, ToolResult } from './BaseTool';
import { listAvailableInstrumentsByGroup } from './toolTargeting';
export class ListAllAvailableInstrumentsTool extends BaseTool {
readonly name = 'list_all_available_instruments';
readonly description =
'List all available instruments grouped by English instrument family names. Use this before creating a new MIDI track or changing a track instrument, because write tools require the exact English instrument name from this list.';
readonly parameters: Record<string, ToolParameter> = {};
override isAvailableInEfficientMode(): boolean {
return false;
}
override buildToolResultDisplayContent(_args: Record<string, unknown> | null, toolResult: ToolResult): string | undefined {
if (!toolResult.success) {
return undefined;
}
const totalInstruments = listAvailableInstrumentsByGroup()
.reduce((count, group) => count + group.instruments.length, 0);
return `Listed ${totalInstruments} available instruments.`;
}
async execute(_params: Record<string, unknown>): Promise<ToolResult> {
try {
const result = listAvailableInstrumentsByGroup()
.map(({ groupName, instruments }) => [
`Group: ${groupName}`,
...instruments.map(instrument => `- ${instrument}`),
].join('\n'))
.join('\n\n');
return this.createSuccessResult(result);
} catch (error) {
return this.createErrorResult(`Failed to list available instruments: ${error}`);
}
}
}
+309
View File
@@ -0,0 +1,309 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { UpdateTrackTool } from './UpdateTrackTool';
import { KGCore } from '../../core/KGCore';
import { KGProject } from '../../core/KGProject';
import { KGMidiTrack } from '../../core/track/KGMidiTrack';
import { KGAudioTrack } from '../../core/track/KGAudioTrack';
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
vi.mock('../../stores/projectStore', () => ({
useProjectStore: {
getState: () => ({
activeRegionId: null,
selectedRegionIds: [],
selectedTrackId: null,
}),
},
}));
function mockCore(project: KGProject) {
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
executeCommand: (command: { execute(): void }) => command.execute(),
} as unknown as KGCore);
}
describe('UpdateTrackTool', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(KGAudioInterface, 'instance').mockReturnValue({
setTrackInstrument: vi.fn(),
} as unknown as KGAudioInterface);
});
it('renames a track by track_id', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('rename-track-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
new_track_name: 'Lead 2',
});
expect(result).toEqual({
success: true,
result: 'Track updated:\ntrack_id: 1\ntrack_name: Lead 2\ninstrument: Trumpet',
});
expect(track.getName()).toBe('Lead 2');
expect(tool.buildToolResultDisplayContent({
track_id: '1',
new_track_name: 'Lead 2',
}, result)).toBe(
'Track updated:\n- track_id: 1\n- track_name: Lead 2\n- instrument: Trumpet',
);
});
it('updates a track instrument by track_name', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('instrument-track-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_name: 'Lead',
instrument: 'Flute',
});
expect(result).toEqual({
success: true,
result: 'Track updated:\ntrack_id: 1\ntrack_name: Lead\ninstrument: Flute',
});
expect(track.getInstrument()).toBe('flute');
});
it('updates both track name and instrument', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('rename-and-instrument-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
new_track_name: 'Flute Lead',
instrument: 'Flute',
});
expect(result).toEqual({
success: true,
result: 'Track updated:\ntrack_id: 1\ntrack_name: Flute Lead\ninstrument: Flute',
});
expect(track.getName()).toBe('Flute Lead');
expect(track.getInstrument()).toBe('flute');
});
it('uses track_id when both track_id and track_name are provided', async () => {
const leadTrack = new KGMidiTrack('Lead', 1, 'trumpet');
const bassTrack = new KGMidiTrack('Bass', 2, 'acoustic_bass');
const project = new KGProject('track-id-precedence-project');
project.setTracks([leadTrack, bassTrack]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '2',
track_name: 'Lead',
new_track_name: 'Bass 2',
});
expect(result.success).toBe(true);
expect(leadTrack.getName()).toBe('Lead');
expect(bassTrack.getName()).toBe('Bass 2');
});
it('rejects duplicate track names when track_id is omitted', async () => {
const firstLead = new KGMidiTrack('Lead', 1, 'trumpet');
const secondLead = new KGMidiTrack('Lead', 2, 'flute');
const project = new KGProject('duplicate-name-project');
project.setTracks([firstLead, secondLead]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_name: 'Lead',
new_track_name: 'Lead 2',
});
expect(result).toEqual({
success: false,
result: 'Multiple MIDI tracks share the name "Lead". Provide track_id instead.',
});
});
it('returns an error when neither identifier is provided', async () => {
const project = new KGProject('missing-id-project');
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
new_track_name: 'Lead 2',
});
expect(result).toEqual({
success: false,
result: 'Either track_id or track_name must be provided.',
});
});
it('returns an error when no update fields are provided', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('missing-fields-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
});
expect(result).toEqual({
success: false,
result: 'At least one of instrument or new_track_name must be provided.',
});
});
it('treats empty string optional fields as not provided', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('empty-string-fields-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
new_track_name: '',
instrument: '',
});
expect(result).toEqual({
success: false,
result: 'At least one of instrument or new_track_name must be provided.',
});
expect(tool.buildConfirmationContent({
track_id: '1',
new_track_name: '',
instrument: '',
})).toBeUndefined();
});
it('treats null optional fields as not provided', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('null-fields-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
new_track_name: null,
instrument: null,
});
expect(result).toEqual({
success: false,
result: 'At least one of instrument or new_track_name must be provided.',
});
});
it('applies a valid change when the other optional field is empty', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('mixed-empty-field-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
new_track_name: '',
instrument: 'Flute',
});
expect(result).toEqual({
success: true,
result: 'Track updated:\ntrack_id: 1\ntrack_name: Lead\ninstrument: Flute',
});
expect(track.getInstrument()).toBe('flute');
expect(tool.buildConfirmationContent({
track_id: '1',
new_track_name: '',
instrument: 'Flute',
})).toBe('Allow updating track ID **1** to set instrument to **Flute**?');
});
it('returns an error when provided values do not change the track', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('unchanged-values-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
new_track_name: 'Lead',
instrument: 'Trumpet',
});
expect(result).toEqual({
success: false,
result: 'No changes to apply to the target track.',
});
});
it('returns an error for an invalid instrument', async () => {
const track = new KGMidiTrack('Lead', 1, 'trumpet');
const project = new KGProject('invalid-instrument-project');
project.setTracks([track]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
instrument: 'trumpet',
});
expect(result).toEqual({
success: false,
result: 'Invalid instrument "trumpet". Use the exact English name from list_all_available_instruments.',
});
});
it('returns an error when the track does not exist', async () => {
const project = new KGProject('missing-track-project');
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '99',
new_track_name: 'Lead 2',
});
expect(result).toEqual({
success: false,
result: 'Track with ID "99" not found or is not a MIDI track.',
});
});
it('returns an error when the target is not a MIDI track', async () => {
const audioTrack = new KGAudioTrack('Vocal', 1);
const project = new KGProject('audio-track-project');
project.setTracks([audioTrack]);
mockCore(project);
const tool = new UpdateTrackTool();
const result = await tool.execute({
track_id: '1',
new_track_name: 'Vocal 2',
});
expect(result).toEqual({
success: false,
result: 'Track with ID "1" not found or is not a MIDI track.',
});
expect(tool.isReadOnlyTool()).toBe(false);
expect(tool.isAvailableInEfficientMode()).toBe(false);
});
});
+177
View File
@@ -0,0 +1,177 @@
import { UpdateTrackCommand } from '../../core/commands/track/UpdateTrackCommand';
import { KGMidiTrack, type InstrumentType } from '../../core/track/KGMidiTrack';
import { BaseTool } from './BaseTool';
import type { ToolParameter, ToolResult } from './BaseTool';
import {
getEnglishInstrumentName,
getTrackDisplayName,
resolveInstrumentKeyByEnglishName,
resolveMidiTrackByExactName,
resolveMidiTrackByIdOrName,
} from './toolTargeting';
export class UpdateTrackTool extends BaseTool {
readonly name = 'update_track';
readonly description =
'Update an existing MIDI track by track_id or track_name. Supports renaming the track and/or changing its instrument to an exact English instrument name from list_all_available_instruments.';
readonly parameters: Record<string, ToolParameter> = {
track_id: {
type: 'string',
description: 'Target MIDI track ID. Preferred when available.',
required: false,
},
track_name: {
type: 'string',
description: 'Target MIDI track name. Used only when track_id is omitted.',
required: false,
},
instrument: {
type: 'string',
description: 'Optional exact English instrument name from list_all_available_instruments.',
required: false,
},
new_track_name: {
type: 'string',
description: 'Optional new name for the track.',
required: false,
},
};
override isReadOnlyTool(): boolean {
return false;
}
override isAvailableInEfficientMode(): boolean {
return false;
}
override buildConfirmationContent(args: Record<string, unknown> | null): string | undefined {
if (!args) {
return undefined;
}
const normalizedInstrumentName = this.normalizeOptionalString(args.instrument);
const normalizedNewTrackName = this.normalizeOptionalString(args.new_track_name);
const targetLabel = typeof args.track_id === 'string'
? `track ID **${args.track_id}**`
: typeof args.track_name === 'string'
? `track **${args.track_name}**`
: null;
if (!targetLabel) {
return undefined;
}
const changes: string[] = [];
if (normalizedNewTrackName !== undefined) {
changes.push(`rename to **${normalizedNewTrackName}**`);
}
if (normalizedInstrumentName !== undefined) {
changes.push(`set instrument to **${normalizedInstrumentName}**`);
}
if (changes.length === 0) {
return undefined;
}
return `Allow updating ${targetLabel} to ${changes.join(' and ')}?`;
}
override buildToolResultDisplayContent(args: Record<string, unknown> | null, toolResult: ToolResult): string | undefined {
if (!args || !toolResult.success) {
return undefined;
}
const trackIdMatch = toolResult.result.match(/track_id:\s*(\d+)/);
const trackNameMatch = toolResult.result.match(/track_name:\s*(.+)/);
const instrumentMatch = toolResult.result.match(/instrument:\s*(.+)/);
if (!trackIdMatch || !trackNameMatch || !instrumentMatch) {
return undefined;
}
return [
'Track updated:',
`- track_id: ${trackIdMatch[1]}`,
`- track_name: ${trackNameMatch[1]}`,
`- instrument: ${instrumentMatch[1]}`,
].join('\n');
}
async execute(params: Record<string, unknown>): Promise<ToolResult> {
try {
this.validateParameters(params);
const trackId = params.track_id as string | undefined;
const trackName = params.track_name as string | undefined;
const instrumentName = this.normalizeOptionalString(params.instrument);
const newTrackName = this.normalizeOptionalString(params.new_track_name);
if (!trackId && !trackName) {
return this.createErrorResult('Either track_id or track_name must be provided.');
}
if (instrumentName === undefined && newTrackName === undefined) {
return this.createErrorResult('At least one of instrument or new_track_name must be provided.');
}
if (!trackId && trackName) {
const matchingTracks = resolveMidiTrackByExactName(trackName);
if (matchingTracks.length > 1) {
return this.createErrorResult(`Multiple MIDI tracks share the name "${trackName}". Provide track_id instead.`);
}
}
const resolvedTrack = resolveMidiTrackByIdOrName(trackId, trackName);
if (!resolvedTrack) {
return this.createErrorResult(
trackId
? `Track with ID "${trackId}" not found or is not a MIDI track.`
: `Track with name "${trackName}" not found or is not a MIDI track.`,
);
}
if (!(resolvedTrack instanceof KGMidiTrack)) {
return this.createErrorResult(
`Track "${getTrackDisplayName(resolvedTrack)}" is not a MIDI track.`,
);
}
let instrumentKey: InstrumentType | undefined;
if (instrumentName !== undefined) {
instrumentKey = resolveInstrumentKeyByEnglishName(instrumentName);
if (!instrumentKey) {
return this.createErrorResult(`Invalid instrument "${instrumentName}". Use the exact English name from list_all_available_instruments.`);
}
}
const trackNameChanged = newTrackName !== undefined && newTrackName !== resolvedTrack.getName();
const instrumentChanged = instrumentKey !== undefined && instrumentKey !== resolvedTrack.getInstrument();
if (!trackNameChanged && !instrumentChanged) {
return this.createErrorResult('No changes to apply to the target track.');
}
const command = new UpdateTrackCommand(resolvedTrack.getId(), {
...(trackNameChanged ? { name: newTrackName } : {}),
...(instrumentChanged && instrumentKey !== undefined ? { instrument: instrumentKey } : {}),
});
await this.executeCommand(command);
return this.createSuccessResult([
'Track updated:',
`track_id: ${resolvedTrack.getId().toString()}`,
`track_name: ${resolvedTrack.getName()}`,
`instrument: ${getEnglishInstrumentName(resolvedTrack.getInstrument())}`,
].join('\n'));
} catch (error) {
return this.createErrorResult(`Failed to update track: ${error}`);
}
}
private normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
return value === '' ? undefined : value;
}
}
+9
View File
@@ -11,6 +11,9 @@ import { ReadChordProgressionTool } from './ReadChordProgressionTool';
import { UpdateTodoListTool } from './UpdateTodoListTool';
import { GetUserSelectedMusicRangeAndTrackTool } from './GetUserSelectedMusicRangeAndTrackTool';
import { ListAllTracksTool } from './ListAllTracksTool';
import { ListAllAvailableInstrumentsTool } from './ListAllAvailableInstrumentsTool';
import { CreateNewTrackTool } from './CreateNewTrackTool';
import { UpdateTrackTool } from './UpdateTrackTool';
export {
AddNotesTool,
@@ -20,6 +23,9 @@ export {
UpdateTodoListTool,
GetUserSelectedMusicRangeAndTrackTool,
ListAllTracksTool,
ListAllAvailableInstrumentsTool,
CreateNewTrackTool,
UpdateTrackTool,
};
// Tool registry for easy access
@@ -31,6 +37,9 @@ export const AVAILABLE_TOOLS = {
read_chord_progression: ReadChordProgressionTool,
get_user_selected_music_range_and_track: GetUserSelectedMusicRangeAndTrackTool,
list_all_tracks: ListAllTracksTool,
list_all_available_instruments: ListAllAvailableInstrumentsTool,
create_new_track: CreateNewTrackTool,
update_track: UpdateTrackTool,
} as const;
export type ToolName = keyof typeof AVAILABLE_TOOLS;
+44
View File
@@ -4,7 +4,9 @@ import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import { KGGlobalRegion } from '../../core/region/KGGlobalRegion';
import { KGMidiTrack } from '../../core/track/KGMidiTrack';
import { KGTrack } from '../../core/track/KGTrack';
import type { InstrumentType } from '../../core/track/KGMidiTrack';
import { useProjectStore } from '../../stores/projectStore';
import { FLUIDR3_INSTRUMENT_MAP, INSTRUMENT_GROUPS } from '../../constants/generalMidiConstants';
export const NO_MIDI_TARGET_RAW_MESSAGE =
'No MIDI target could be resolved. Select the MIDI region you want me to edit and retry, or tell me which MIDI track to operate on by providing its track_id.';
@@ -57,6 +59,48 @@ export function resolveMidiTrackByIdOrName(
return null;
}
export function resolveMidiTrackByExactName(trackName: string): KGMidiTrack[] {
const project = KGCore.instance().getCurrentProject();
return project.getTracks().filter((track): track is KGMidiTrack => (
track instanceof KGMidiTrack && track.getName() === trackName
));
}
export function resolveInstrumentKeyByEnglishName(instrumentName: string): InstrumentType | null {
for (const [instrumentKey, instrumentInfo] of Object.entries(FLUIDR3_INSTRUMENT_MAP)) {
if (instrumentInfo.displayName === instrumentName) {
return instrumentKey as InstrumentType;
}
}
return null;
}
export function getEnglishInstrumentName(instrumentKey: InstrumentType): string {
return FLUIDR3_INSTRUMENT_MAP[instrumentKey]?.displayName ?? String(instrumentKey);
}
export function listAvailableInstrumentsByGroup(): Array<{ groupName: string; instruments: string[] }> {
const groupedInstruments = new Map<string, string[]>();
for (const groupName of Object.values(INSTRUMENT_GROUPS)) {
groupedInstruments.set(groupName, []);
}
for (const instrumentInfo of Object.values(FLUIDR3_INSTRUMENT_MAP)) {
const groupName = INSTRUMENT_GROUPS[instrumentInfo.group as keyof typeof INSTRUMENT_GROUPS];
if (!groupedInstruments.has(groupName)) {
groupedInstruments.set(groupName, []);
}
groupedInstruments.get(groupName)!.push(instrumentInfo.displayName);
}
return Array.from(groupedInstruments.entries()).map(([groupName, instruments]) => ({
groupName,
instruments,
}));
}
export function findRegionById(regionId: string): KGRegion | null {
const project = KGCore.instance().getCurrentProject();