src / toolsProvider.ts
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics } from "./config.js";
type Character = { name: string; role: string; personality: string; expertise: string; speakingStyle: string };
const characters = new Map<string, Character>();
function systemPrompt(character: Character, context?: string) {
return [
`あなたは「${character.name}」です。役割は${character.role}です。`,
`性格: ${character.personality}`, `専門性: ${character.expertise}`, `話し方: ${character.speakingStyle}`,
"キャラクター性を保ち、根拠のある実用的な回答を日本語で返してください。",
context ? `共有コンテキスト:\n${context}` : "",
].filter(Boolean).join("\n");
}
async function callLocalModel(messages: Array<{ role: string; content: string }>, settings: { model: string; temperature: number; maxTokens: number }) {
const response = await fetch("http://127.0.0.1:1234/v1/chat/completions", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ model: settings.model, messages, temperature: settings.temperature, max_tokens: settings.maxTokens }),
});
if (!response.ok) throw new Error(`LM Studio API error ${response.status}: ${await response.text()}`);
const json = await response.json() as { choices?: Array<{ message?: { content?: string } }> };
const answer = json.choices?.[0]?.message?.content;
if (!answer) throw new Error("LM Studio returned no response content.");
return answer;
}
async function mapLimited<T, R>(items: T[], limit: number, worker: (item: T) => Promise<R>) {
const output = new Array<R>(items.length);
let cursor = 0;
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
for (;;) { const index = cursor++; if (index >= items.length) return; output[index] = await worker(items[index]); }
}));
return output;
}
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const createCast = tool({
name: "create_cast",
description: "マスターエージェント用。目的に合わせ、サブエージェントの配役を保存する。",
parameters: { characters: z.array(z.object({ name: z.string(), role: z.string(), personality: z.string(), expertise: z.string(), speakingStyle: z.string() })).min(1).max(12) },
implementation: async ({ characters: cast }) => {
for (const character of cast) characters.set(character.name, character);
return { saved: cast.map(c => ({ name: c.name, role: c.role })), next: "run_roundtable で保存済みの名前を participants に指定してください。" };
},
});
const roundtable = tool({
name: "run_roundtable",
description: "保存済みの複数キャラクターに同じ議題を並行して検討させる。温度・並行人数・各発言の長さはLM Studioのプラグイン設定スライダーから変更する。",
parameters: { topic: z.string(), participants: z.array(z.string()).min(1).max(12), context: z.string().optional() },
implementation: async ({ topic, participants, context }) => {
const config = ctl.getPluginConfig(configSchematics);
const selected = participants.map(name => characters.get(name)).filter((c): c is Character => Boolean(c));
const missing = participants.filter(name => !characters.has(name));
if (!selected.length) return { error: "有効な参加者がいません。create_cast を先に呼んでください。" };
const settings = { model: config.get("modelIdentifier"), temperature: config.get("roundtableTemperature"), maxTokens: config.get("maxTokensPerCharacter") };
const messages = await mapLimited(selected, config.get("parallelAgents"), async character => {
try {
const content = await callLocalModel([{ role: "system", content: systemPrompt(character, context) }, { role: "user", content: `議題: ${topic}\nあなたの担当から、見解・懸念・提案を述べてください。` }], settings);
return { character: character.name, role: character.role, content };
} catch (error) { return { character: character.name, role: character.role, error: error instanceof Error ? error.message : String(error) }; }
});
return { topic, settingsUsed: { temperature: settings.temperature, parallelAgents: config.get("parallelAgents"), maxTokensPerCharacter: settings.maxTokens }, missingParticipants: missing, messages };
},
});
const listCharacters = tool({
name: "list_characters",
description: "保存済みのキャラクターと役割を一覧する。",
parameters: {},
implementation: async () => ({
characters: [...characters.values()].map(({ name, role, expertise }) => ({ name, role, expertise })),
}),
});
const askCharacter = tool({
name: "ask_character",
description: "保存済みの一人のキャラクターへ個別タスクを依頼する。温度と出力長はプラグイン設定スライダーを使用する。",
parameters: { name: z.string(), task: z.string(), context: z.string().optional() },
implementation: async ({ name, task, context }) => {
const character = characters.get(name);
if (!character) return { error: `「${name}」は未登録です。create_cast を先に呼んでください。` };
const config = ctl.getPluginConfig(configSchematics);
try {
const content = await callLocalModel([{ role: "system", content: systemPrompt(character, context) }, { role: "user", content: task }], {
model: config.get("modelIdentifier"), temperature: config.get("roundtableTemperature"), maxTokens: config.get("maxTokensPerCharacter"),
});
return { character: name, role: character.role, content };
} catch (error) { return { character: name, error: error instanceof Error ? error.message : String(error) }; }
},
});
const removeCharacter = tool({
name: "remove_character",
description: "保存済みのキャラクターを削除する。",
parameters: { name: z.string() },
implementation: async ({ name }) => ({ removed: characters.delete(name), name }),
});
return [createCast, listCharacters, askCharacter, roundtable, removeCharacter];
}
src / toolsProvider.ts
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics } from "./config.js";
type Character = { name: string; role: string; personality: string; expertise: string; speakingStyle: string };
const characters = new Map<string, Character>();
function systemPrompt(character: Character, context?: string) {
return [
`あなたは「${character.name}」です。役割は${character.role}です。`,
`性格: ${character.personality}`, `専門性: ${character.expertise}`, `話し方: ${character.speakingStyle}`,
"キャラクター性を保ち、根拠のある実用的な回答を日本語で返してください。",
context ? `共有コンテキスト:\n${context}` : "",
].filter(Boolean).join("\n");
}
async function callLocalModel(messages: Array<{ role: string; content: string }>, settings: { model: string; temperature: number; maxTokens: number }) {
const response = await fetch("http://127.0.0.1:1234/v1/chat/completions", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ model: settings.model, messages, temperature: settings.temperature, max_tokens: settings.maxTokens }),
});
if (!response.ok) throw new Error(`LM Studio API error ${response.status}: ${await response.text()}`);
const json = await response.json() as { choices?: Array<{ message?: { content?: string } }> };
const answer = json.choices?.[0]?.message?.content;
if (!answer) throw new Error("LM Studio returned no response content.");
return answer;
}
async function mapLimited<T, R>(items: T[], limit: number, worker: (item: T) => Promise<R>) {
const output = new Array<R>(items.length);
let cursor = 0;
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
for (;;) { const index = cursor++; if (index >= items.length) return; output[index] = await worker(items[index]); }
}));
return output;
}
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const createCast = tool({
name: "create_cast",
description: "マスターエージェント用。目的に合わせ、サブエージェントの配役を保存する。",
parameters: { characters: z.array(z.object({ name: z.string(), role: z.string(), personality: z.string(), expertise: z.string(), speakingStyle: z.string() })).min(1).max(12) },
implementation: async ({ characters: cast }) => {
for (const character of cast) characters.set(character.name, character);
return { saved: cast.map(c => ({ name: c.name, role: c.role })), next: "run_roundtable で保存済みの名前を participants に指定してください。" };
},
});
const roundtable = tool({
name: "run_roundtable",
description: "保存済みの複数キャラクターに同じ議題を並行して検討させる。温度・並行人数・各発言の長さはLM Studioのプラグイン設定スライダーから変更する。",
parameters: { topic: z.string(), participants: z.array(z.string()).min(1).max(12), context: z.string().optional() },
implementation: async ({ topic, participants, context }) => {
const config = ctl.getPluginConfig(configSchematics);
const selected = participants.map(name => characters.get(name)).filter((c): c is Character => Boolean(c));
const missing = participants.filter(name => !characters.has(name));
if (!selected.length) return { error: "有効な参加者がいません。create_cast を先に呼んでください。" };
const settings = { model: config.get("modelIdentifier"), temperature: config.get("roundtableTemperature"), maxTokens: config.get("maxTokensPerCharacter") };
const messages = await mapLimited(selected, config.get("parallelAgents"), async character => {
try {
const content = await callLocalModel([{ role: "system", content: systemPrompt(character, context) }, { role: "user", content: `議題: ${topic}\nあなたの担当から、見解・懸念・提案を述べてください。` }], settings);
return { character: character.name, role: character.role, content };
} catch (error) { return { character: character.name, role: character.role, error: error instanceof Error ? error.message : String(error) }; }
});
return { topic, settingsUsed: { temperature: settings.temperature, parallelAgents: config.get("parallelAgents"), maxTokensPerCharacter: settings.maxTokens }, missingParticipants: missing, messages };
},
});
const listCharacters = tool({
name: "list_characters",
description: "保存済みのキャラクターと役割を一覧する。",
parameters: {},
implementation: async () => ({
characters: [...characters.values()].map(({ name, role, expertise }) => ({ name, role, expertise })),
}),
});
const askCharacter = tool({
name: "ask_character",
description: "保存済みの一人のキャラクターへ個別タスクを依頼する。温度と出力長はプラグイン設定スライダーを使用する。",
parameters: { name: z.string(), task: z.string(), context: z.string().optional() },
implementation: async ({ name, task, context }) => {
const character = characters.get(name);
if (!character) return { error: `「${name}」は未登録です。create_cast を先に呼んでください。` };
const config = ctl.getPluginConfig(configSchematics);
try {
const content = await callLocalModel([{ role: "system", content: systemPrompt(character, context) }, { role: "user", content: task }], {
model: config.get("modelIdentifier"), temperature: config.get("roundtableTemperature"), maxTokens: config.get("maxTokensPerCharacter"),
});
return { character: name, role: character.role, content };
} catch (error) { return { character: name, error: error instanceof Error ? error.message : String(error) }; }
},
});
const removeCharacter = tool({
name: "remove_character",
description: "保存済みのキャラクターを削除する。",
parameters: { name: z.string() },
implementation: async ({ name }) => ({ removed: characters.delete(name), name }),
});
return [createCast, listCharacters, askCharacter, roundtable, removeCharacter];
}