import { ToolDef } from '../../llm/openai-compat.js'; import { logger } from '../../logger.js'; import { ToolContext, ToolResult, ToolsConfig, ALL_TOOL_DEFS, executeCoreTools, runtimeLogsDir, } from './core.js'; import { saveRawData, logRawDownload, RAW_SAVE_TOOLS, RAW_LOG_ONLY_TOOLS } from './raw-save.js'; import { saveStructuredBlocks } from './structured-blocks.js'; import type { McpAggregator } from '../../mcp/aggregator.js'; import type { McpRuntimeConfig } from '../../mcp/config.js'; export type { ToolContext, ToolResult, ToolsConfig }; let _mcpAggregator: McpAggregator | null = null; export function setMcpAggregator(agg: McpAggregator | null): void { _mcpAggregator = agg; } type ExecuteCtxWithMcp = ToolContext & { ownerId?: string | null; jobId?: string | null; mcpConfig?: McpRuntimeConfig; mcpQuotaState?: { files: number; bytes: number }; }; // 外部モジュール(他チームが実装)のインターフェース型 interface ToolModule { TOOL_DEFS: Record; executeTool(name: string, input: Record, ctx: ToolContext): Promise; } // 外部モジュールを動的にロード(存在しない場合は null) async function tryLoadModule(specifier: string): Promise { try { const mod = await import(specifier) as ToolModule; return mod; } catch { return null; } } // 外部モジュールのキャッシュ let _webModule: ToolModule | null | undefined = undefined; let _imageModule: ToolModule | null | undefined = undefined; let _dataModule: ToolModule | null | undefined = undefined; let _officeModule: ToolModule | null | undefined = undefined; let _reviewModule: ToolModule | null | undefined = undefined; let _xModule: ToolModule | null | undefined = undefined; let _orchestrationModule: ToolModule | null | undefined = undefined; let _browserModule: ToolModule | null | undefined = undefined; let _mapsModule: ToolModule | null | undefined = undefined; let _youtubeModule: ToolModule | null | undefined = undefined; let _piecesModule: ToolModule | null | undefined = undefined; let _amazonModule: ToolModule | null | undefined = undefined; let _speechModule: ToolModule | null | undefined = undefined; let _checklistModule: ToolModule | null | undefined = undefined; let _msLearnModule: ToolModule | null | undefined = undefined; let _slideModule: ToolModule | null | undefined = undefined; let _userFolderModule: ToolModule | null | undefined = undefined; let _appTestModule: ToolModule | null | undefined = undefined; async function getWebModule(): Promise { if (_webModule === undefined) { _webModule = await tryLoadModule('./web.js'); if (_webModule) logger.debug('[tools/index] web module loaded'); } return _webModule; } async function getImageModule(): Promise { if (_imageModule === undefined) { _imageModule = await tryLoadModule('./image.js'); if (_imageModule) logger.debug('[tools/index] image module loaded'); } return _imageModule; } async function getDataModule(): Promise { if (_dataModule === undefined) { _dataModule = await tryLoadModule('./data.js'); if (_dataModule) logger.debug('[tools/index] data module loaded'); } return _dataModule; } async function getOfficeModule(): Promise { if (_officeModule === undefined) { _officeModule = await tryLoadModule('./office.js'); if (_officeModule) logger.debug('[tools/index] office module loaded'); } return _officeModule; } async function getReviewModule(): Promise { if (_reviewModule === undefined) { _reviewModule = await tryLoadModule('./review.js'); if (_reviewModule) logger.debug('[tools/index] review module loaded'); } return _reviewModule; } async function getXModule(): Promise { if (_xModule === undefined) { _xModule = await tryLoadModule('./x.js'); if (_xModule) logger.debug('[tools/index] x module loaded'); } return _xModule; } async function getOrchestrationModule(): Promise { if (_orchestrationModule === undefined) { _orchestrationModule = await tryLoadModule('./orchestration.js'); if (_orchestrationModule) logger.debug('[tools/index] orchestration module loaded'); } return _orchestrationModule; } async function getBrowserModule(): Promise { if (_browserModule === undefined) { _browserModule = await tryLoadModule('./browser.js'); if (_browserModule) logger.debug('[tools/index] browser module loaded'); } return _browserModule; } async function getMapsModule(): Promise { if (_mapsModule === undefined) { _mapsModule = await tryLoadModule('./maps.js'); if (_mapsModule) logger.debug('[tools/index] maps module loaded'); } return _mapsModule; } async function getYoutubeModule(): Promise { if (_youtubeModule === undefined) { _youtubeModule = await tryLoadModule('./youtube.js'); if (_youtubeModule) logger.debug('[tools/index] youtube module loaded'); } return _youtubeModule; } async function getPiecesModule(): Promise { if (_piecesModule === undefined) { _piecesModule = await tryLoadModule('./pieces.js'); if (_piecesModule) logger.debug('[tools/index] pieces module loaded'); } return _piecesModule; } async function getAmazonModule(): Promise { if (_amazonModule === undefined) { _amazonModule = await tryLoadModule('./amazon.js'); if (_amazonModule) logger.debug('[tools/index] amazon module loaded'); } return _amazonModule; } async function getSpeechModule(): Promise { if (_speechModule === undefined) { _speechModule = await tryLoadModule('./speech.js'); if (_speechModule) logger.debug('[tools/index] speech module loaded'); } return _speechModule; } async function getChecklistModule(): Promise { if (_checklistModule === undefined) { _checklistModule = await tryLoadModule('./checklist.js'); if (_checklistModule) logger.debug('[tools/index] checklist module loaded'); } return _checklistModule; } async function getSlideModule(): Promise { if (_slideModule === undefined) { _slideModule = await tryLoadModule('./slide.js'); if (_slideModule) logger.debug('[tools/index] slide module loaded'); } return _slideModule; } async function getMsLearnModule(): Promise { if (_msLearnModule === undefined) { _msLearnModule = await tryLoadModule('./ms-learn.js'); if (_msLearnModule) logger.debug('[tools/index] ms-learn module loaded'); } return _msLearnModule; } let _docsModule: ToolModule | null | undefined; async function getDocsModule(): Promise { if (_docsModule === undefined) { _docsModule = await tryLoadModule('./docs.js'); if (_docsModule) logger.debug('[tools/index] docs module loaded'); } return _docsModule; } let _missionModule: ToolModule | null | undefined; async function getMissionModule(): Promise { if (_missionModule === undefined) { _missionModule = await tryLoadModule('./mission.js'); if (_missionModule) logger.debug('[tools/index] mission module loaded'); } return _missionModule; } let _taskConversationModule: ToolModule | null | undefined; async function getTaskConversationModule(): Promise { if (_taskConversationModule === undefined) { _taskConversationModule = await tryLoadModule('./task-conversation.js'); if (_taskConversationModule) logger.debug('[tools/index] task-conversation module loaded'); } return _taskConversationModule; } let _workspaceTaskSearchModule: ToolModule | null | undefined; async function getWorkspaceTaskSearchModule(): Promise { if (_workspaceTaskSearchModule === undefined) { _workspaceTaskSearchModule = await tryLoadModule('./workspace-task-search.js'); if (_workspaceTaskSearchModule) logger.debug('[tools/index] workspace-task-search module loaded'); } return _workspaceTaskSearchModule; } let _fileProvenanceModule: ToolModule | null | undefined; async function getFileProvenanceModule(): Promise { if (_fileProvenanceModule === undefined) { _fileProvenanceModule = await tryLoadModule('./file-provenance.js'); if (_fileProvenanceModule) logger.debug('[tools/index] file-provenance module loaded'); } return _fileProvenanceModule; } let _toolRequestModule: ToolModule | null | undefined; async function getToolRequestModule(): Promise { if (_toolRequestModule === undefined) { _toolRequestModule = await tryLoadModule('./tool-request.js'); if (_toolRequestModule) logger.debug('[tools/index] tool-request module loaded'); } return _toolRequestModule; } /** * Snapshot of all known static tool names, refreshed every getToolDefs() call * (which runs at movement start, before any tool executes). Injected into * RequestTool's ctx so it can classify an unknown tool name vs one that exists * but isn't allowed in the current movement. MCP names are handled separately * by RequestTool via the `mcp__` prefix, so they need not be in this set. */ let _allKnownToolNames: string[] = []; async function getUserFolderModule(): Promise { if (_userFolderModule === undefined) { _userFolderModule = await tryLoadModule('./user-folder.js'); if (_userFolderModule) logger.debug('[tools/index] user-folder module loaded'); } return _userFolderModule; } let _brainstormModule: ToolModule | null | undefined; async function getBrainstormModule(): Promise { if (_brainstormModule === undefined) { _brainstormModule = await tryLoadModule('./brainstorm.js'); if (_brainstormModule) logger.debug('[tools/index] brainstorm module loaded'); } return _brainstormModule; } let _appDocsModule: ToolModule | null | undefined; async function getAppDocsModule(): Promise { if (_appDocsModule === undefined) { _appDocsModule = await tryLoadModule('./app-docs.js'); if (_appDocsModule) logger.debug('[tools/index] app-docs module loaded'); } return _appDocsModule; } let _sshModule: ToolModule | null | undefined; async function getSshModule(): Promise { if (_sshModule === undefined) { _sshModule = await tryLoadModule('./ssh.js'); if (_sshModule) logger.debug('[tools/index] ssh module loaded'); } return _sshModule; } let _sshConsoleModule: ToolModule | null | undefined; async function getSshConsoleModule(): Promise { if (_sshConsoleModule === undefined) { _sshConsoleModule = await tryLoadModule('./ssh-console.js'); if (_sshConsoleModule) logger.debug('[tools/index] ssh-console module loaded'); } return _sshConsoleModule; } let _calendarModule: ToolModule | null | undefined = undefined; async function getCalendarModule(): Promise { if (_calendarModule === undefined) { _calendarModule = await tryLoadModule('./calendar.js'); if (_calendarModule) logger.debug('[tools/index] calendar module loaded'); } return _calendarModule; } async function getAppTestModule(): Promise { if (_appTestModule === undefined) { _appTestModule = await tryLoadModule('./app-test.js'); if (_appTestModule) logger.debug('[tools/index] app-test module loaded'); } return _appTestModule; } /** * 全モジュールのツール定義を統合して返す。 * allowedTools と editAllowed に応じてフィルタリングする。 */ export async function getToolDefs( allowedTools: string[], editAllowed: boolean, options?: { vlmEnabled?: boolean; ownerId?: string | null; mcpDisabled?: boolean; spaceId?: string | null }, ): Promise { // 全ツール定義を収集 const allDefs: Record = { ...ALL_TOOL_DEFS }; const webMod = await getWebModule(); if (webMod) Object.assign(allDefs, webMod.TOOL_DEFS); const imageMod = await getImageModule(); if (imageMod) Object.assign(allDefs, imageMod.TOOL_DEFS); const dataMod = await getDataModule(); if (dataMod) Object.assign(allDefs, dataMod.TOOL_DEFS); const officeMod = await getOfficeModule(); if (officeMod) Object.assign(allDefs, officeMod.TOOL_DEFS); const reviewMod = await getReviewModule(); if (reviewMod) Object.assign(allDefs, reviewMod.TOOL_DEFS); const xMod = await getXModule(); if (xMod) Object.assign(allDefs, xMod.TOOL_DEFS); const orchestrationMod = await getOrchestrationModule(); if (orchestrationMod) Object.assign(allDefs, orchestrationMod.TOOL_DEFS); const browserMod = await getBrowserModule(); if (browserMod) Object.assign(allDefs, browserMod.TOOL_DEFS); const mapsMod = await getMapsModule(); if (mapsMod) Object.assign(allDefs, mapsMod.TOOL_DEFS); const youtubeMod = await getYoutubeModule(); if (youtubeMod) Object.assign(allDefs, youtubeMod.TOOL_DEFS); const piecesMod = await getPiecesModule(); if (piecesMod) Object.assign(allDefs, piecesMod.TOOL_DEFS); const amazonMod = await getAmazonModule(); if (amazonMod) Object.assign(allDefs, amazonMod.TOOL_DEFS); const speechMod = await getSpeechModule(); if (speechMod) Object.assign(allDefs, speechMod.TOOL_DEFS); const checklistMod = await getChecklistModule(); if (checklistMod) Object.assign(allDefs, checklistMod.TOOL_DEFS); const msLearnMod = await getMsLearnModule(); if (msLearnMod) Object.assign(allDefs, msLearnMod.TOOL_DEFS); const slideMod = await getSlideModule(); if (slideMod) Object.assign(allDefs, slideMod.TOOL_DEFS); const docsMod = await getDocsModule(); if (docsMod) Object.assign(allDefs, docsMod.TOOL_DEFS); const missionMod = await getMissionModule(); if (missionMod) Object.assign(allDefs, missionMod.TOOL_DEFS); const taskConversationMod = await getTaskConversationModule(); if (taskConversationMod) Object.assign(allDefs, taskConversationMod.TOOL_DEFS); const workspaceTaskSearchMod = await getWorkspaceTaskSearchModule(); if (workspaceTaskSearchMod) Object.assign(allDefs, workspaceTaskSearchMod.TOOL_DEFS); const fileProvenanceMod = await getFileProvenanceModule(); if (fileProvenanceMod) Object.assign(allDefs, fileProvenanceMod.TOOL_DEFS); const userFolderMod = await getUserFolderModule(); if (userFolderMod) Object.assign(allDefs, userFolderMod.TOOL_DEFS); const brainstormMod = await getBrainstormModule(); if (brainstormMod) Object.assign(allDefs, brainstormMod.TOOL_DEFS); const appDocsMod = await getAppDocsModule(); if (appDocsMod) Object.assign(allDefs, appDocsMod.TOOL_DEFS); const sshMod = await getSshModule(); if (sshMod) Object.assign(allDefs, sshMod.TOOL_DEFS); const sshConsoleMod = await getSshConsoleModule(); if (sshConsoleMod) Object.assign(allDefs, sshConsoleMod.TOOL_DEFS); const calendarMod = await getCalendarModule(); if (calendarMod) Object.assign(allDefs, calendarMod.TOOL_DEFS); const appTestMod = await getAppTestModule(); if (appTestMod) Object.assign(allDefs, appTestMod.TOOL_DEFS); const { TOOL_DEFS: skillToolDefs } = await import('./skills.js'); Object.assign(allDefs, skillToolDefs); const toolRequestMod = await getToolRequestModule(); if (toolRequestMod) Object.assign(allDefs, toolRequestMod.TOOL_DEFS); // Refresh the known-tool snapshot for RequestTool classification. // // CONCURRENCY INVARIANT (must hold): this is taken HERE — after every static // module is merged into `allDefs`, but BEFORE the per-job MCP defs are // appended below. The static catalog does NOT depend on movement / ownerId / // spaceId / mcpDisabled, so every concurrent getToolDefs() writer produces an // IDENTICAL array. That is why this module-global is safe to read from a // different movement's RequestTool dispatch. If you ever make a STATIC tool's // presence in `allDefs` conditional (feature flag, per-space, etc.), this // snapshot becomes movement-dependent and the global must move into ctx // instead — otherwise concurrent movements would misclassify each other's // tools as `unknown`. _allKnownToolNames = Object.keys(allDefs); // メタツール: ワークスペースのツール設定に関係なく常に利用可能 // - ReadToolDoc: 全ツールのドキュメント参照 // - CreateChecklist / CheckItem / GetChecklist: 進捗管理 (複数ステップタスクで使用) // - MissionUpdate: タスクの目標 / 進捗のピン止めメモを更新 (会話が長くなって // 最初の要件を見失わないため。常時上書き可能、未指定フィールドは保持) // - ListUserAssets / RunUserScript: ユーザーフォルダのスクリプト探索・実行 // - ReadUserAgents / UpdateUserAgents: per-user AGENTS.md (常時指示書) の読み書き // (UpdateUserMemory が事実断片、こちらは振る舞い方針そのもの) // - Brainstorm: 着手前 or 行き詰まり時の多アプローチ比較 (issue #247) // - ReadAppDoc / ListAppDocs / GetMyOrchestratorState: Help アシスタント用 (#help piece) // ただし他の piece からも参照できるようにメタ扱い // - RequestTool: 足りないツールの要求を記録(タスク詳細/ピース集計で可視化) const META_TOOLS = ['ReadToolDoc', 'CreateChecklist', 'CheckItem', 'GetChecklist', 'MissionUpdate', 'SearchTaskConversation', 'ReadTaskConversation', 'SearchWorkspaceTasks', 'GetFileProvenance', 'ListWorkspaceFiles', 'ListUserAssets', 'RunUserScript', 'UpdateUserMemory', 'ReadUserMemory', 'ReadUserAgents', 'UpdateUserAgents', 'WriteUserScript', 'Brainstorm', 'ReadAppDoc', 'ListAppDocs', 'GetMyOrchestratorState', 'ReadSkill', 'ListSkills', 'InstallSkill', 'RequestTool']; const effectiveAllowed = [...allowedTools]; for (const meta of META_TOOLS) { if (!effectiveAllowed.includes(meta) && meta in allDefs) { effectiveAllowed.push(meta); } } const staticDefs = effectiveAllowed .filter((name) => { if (!editAllowed && (name === 'Write' || name === 'Edit')) return false; if (!options?.vlmEnabled && name === 'ReadImage') return false; return name in allDefs; }) .map((name) => allDefs[name]!); // Per-space isolation: the MCP server set is scoped to the job's space. // options.spaceId === undefined → aggregator falls back to the legacy // NULL-space set (non-space jobs keep prior behaviour). const mcpDefs = _mcpAggregator && options?.ownerId && !options?.mcpDisabled ? await _mcpAggregator.getToolDefs(options.ownerId, allowedTools, options.spaceId) : []; return [...staticDefs, ...mcpDefs]; } /** * ツールを実行する内部ルーター。 * core → web → image → data の順で各モジュールに委譲し、 * 最初に null でない結果を返したモジュールの結果を使う。 */ async function executeToolInner( name: string, input: Record, ctx: ToolContext, ): Promise { if (name.startsWith('mcp__')) { if (ctx.mcpDisabled) { return { output: 'MCP tools are disabled for this task (options.mcpDisabled)', isError: true }; } if (!_mcpAggregator) { return { output: 'MCP サブシステムが初期化されていません', isError: true }; } const ctxWithMcp = ctx as ExecuteCtxWithMcp; if (!ctxWithMcp.mcpQuotaState) { ctxWithMcp.mcpQuotaState = { files: 0, bytes: 0 }; } if (!ctxWithMcp.mcpConfig) { return { output: 'MCP 設定が ToolContext に伝播していません', isError: true }; } const result = await _mcpAggregator.executeTool(name, input, { workspacePath: ctx.workspacePath, ownerId: ctxWithMcp.ownerId ?? '', jobId: ctxWithMcp.jobId ?? '', config: ctxWithMcp.mcpConfig, quotaState: ctxWithMcp.mcpQuotaState, // Job cancel/deadline signal so a slow/hung MCP server is interrupted // instead of pinning the worker slot past the deadline. abortSignal: ctx.abortSignal, // Per-space isolation guard (defense in depth). undefined = legacy // NULL-space job → only NULL-space servers are callable. spaceId: ctx.spaceId ?? null, }); if (result === null) { return { output: `MCP ツール dispatch が ${name} に対して null を返しました`, isError: true }; } return { output: result.output, isError: result.isError }; } // core ツール const coreResult = await executeCoreTools(name, input, ctx); if (coreResult !== null) return coreResult; // web ツール const webMod = await getWebModule(); if (webMod) { const webResult = await webMod.executeTool(name, input, ctx); if (webResult !== null) return webResult; } // image ツール const imageMod = await getImageModule(); if (imageMod) { const imageResult = await imageMod.executeTool(name, input, ctx); if (imageResult !== null) return imageResult; } // data ツール const dataMod = await getDataModule(); if (dataMod) { const dataResult = await dataMod.executeTool(name, input, ctx); if (dataResult !== null) return dataResult; } // office ツール const officeMod = await getOfficeModule(); if (officeMod) { const officeResult = await officeMod.executeTool(name, input, ctx); if (officeResult !== null) return officeResult; } // review ツール const reviewMod = await getReviewModule(); if (reviewMod) { const reviewResult = await reviewMod.executeTool(name, input, ctx); if (reviewResult !== null) return reviewResult; } // x tools const xMod = await getXModule(); if (xMod) { const xResult = await xMod.executeTool(name, input, ctx); if (xResult !== null) return xResult; } // orchestration ツール const orchestrationMod = await getOrchestrationModule(); if (orchestrationMod) { const orchestrationResult = await orchestrationMod.executeTool(name, input, ctx); if (orchestrationResult !== null) return orchestrationResult; } // browser ツール const browserMod = await getBrowserModule(); if (browserMod) { const browserResult = await browserMod.executeTool(name, input, ctx); if (browserResult !== null) return browserResult; } // maps ツール const mapsMod = await getMapsModule(); if (mapsMod) { const mapsResult = await mapsMod.executeTool(name, input, ctx); if (mapsResult !== null) return mapsResult; } // youtube ツール const youtubeMod = await getYoutubeModule(); if (youtubeMod) { const youtubeResult = await youtubeMod.executeTool(name, input, ctx); if (youtubeResult !== null) return youtubeResult; } // pieces ツール const piecesMod = await getPiecesModule(); if (piecesMod) { const piecesResult = await piecesMod.executeTool(name, input, ctx); if (piecesResult !== null) return piecesResult; } // amazon ツール const amazonMod = await getAmazonModule(); if (amazonMod) { const amazonResult = await amazonMod.executeTool(name, input, ctx); if (amazonResult !== null) return amazonResult; } // speech ツール const speechMod = await getSpeechModule(); if (speechMod) { const speechResult = await speechMod.executeTool(name, input, ctx); if (speechResult !== null) return speechResult; } // checklist ツール const checklistMod = await getChecklistModule(); if (checklistMod) { const checklistResult = await checklistMod.executeTool(name, input, ctx); if (checklistResult !== null) return checklistResult; } // ms-learn ツール const msLearnMod = await getMsLearnModule(); if (msLearnMod) { const msLearnResult = await msLearnMod.executeTool(name, input, ctx); if (msLearnResult !== null) return msLearnResult; } // slide ツール (SetTheme / AddSlide / BuildPptx / ResetSlides) const slideMod = await getSlideModule(); if (slideMod) { const slideResult = await slideMod.executeTool(name, input, ctx); if (slideResult !== null) return slideResult; } // docs ツール (ReadToolDoc) const docsMod = await getDocsModule(); if (docsMod) { const docsResult = await docsMod.executeTool(name, input, ctx); if (docsResult !== null) return docsResult; } // mission ツール (MissionUpdate) const missionMod = await getMissionModule(); if (missionMod) { const missionResult = await missionMod.executeTool(name, input, ctx); if (missionResult !== null) return missionResult; } // task-conversation ツール (SearchTaskConversation / ReadTaskConversation) const taskConversationMod = await getTaskConversationModule(); if (taskConversationMod) { const taskConversationResult = await taskConversationMod.executeTool(name, input, ctx); if (taskConversationResult !== null) return taskConversationResult; } // workspace-task-search ツール (SearchWorkspaceTasks) const workspaceTaskSearchMod2 = await getWorkspaceTaskSearchModule(); if (workspaceTaskSearchMod2) { const r = await workspaceTaskSearchMod2.executeTool(name, input, ctx); if (r !== null) return r; } // file-provenance ツール (GetFileProvenance / ListWorkspaceFiles) const fileProvenanceMod = await getFileProvenanceModule(); if (fileProvenanceMod) { const fileProvenanceResult = await fileProvenanceMod.executeTool(name, input, ctx); if (fileProvenanceResult !== null) return fileProvenanceResult; } // RequestTool: 足りないツールの要求を記録。分類用に既知ツール一覧を注入する // (getToolDefs が movement 開始時に更新した最新スナップショット)。 const toolRequestMod = await getToolRequestModule(); if (toolRequestMod) { if (name === 'RequestTool') ctx.knownToolNames = _allKnownToolNames; const toolRequestResult = await toolRequestMod.executeTool(name, input, ctx); if (toolRequestResult !== null) return toolRequestResult; } // user-folder ツール (ListUserAssets / RunUserScript) const userFolderMod = await getUserFolderModule(); if (userFolderMod) { const userFolderResult = await userFolderMod.executeTool(name, input, ctx); if (userFolderResult !== null) return userFolderResult; } // brainstorm ツール (Brainstorm) const brainstormMod = await getBrainstormModule(); if (brainstormMod) { const brainstormResult = await brainstormMod.executeTool(name, input, ctx); if (brainstormResult !== null) return brainstormResult; } // app-docs ツール (ReadAppDoc / ListAppDocs / GetMyOrchestratorState) const appDocsMod = await getAppDocsModule(); if (appDocsMod) { const appDocsResult = await appDocsMod.executeTool(name, input, ctx); if (appDocsResult !== null) return appDocsResult; } // ssh ツール (SshExec / SshUpload / SshDownload) const sshMod = await getSshModule(); if (sshMod) { const sshResult = await sshMod.executeTool(name, input, ctx); if (sshResult !== null) return sshResult; } // ssh-console ツール (SshConsoleEnsure / Send / Snapshot) const sshConsoleMod = await getSshConsoleModule(); if (sshConsoleMod) { const sshConsoleResult = await sshConsoleMod.executeTool(name, input, ctx); if (sshConsoleResult !== null) return sshConsoleResult; } // calendar ツール (AddCalendarEvent / ListCalendarEvents) const calendarMod = await getCalendarModule(); if (calendarMod) { const calendarResult = await calendarMod.executeTool(name, input, ctx); if (calendarResult !== null) return calendarResult; } // app-test ツール (TestWorkspaceApp) const appTestMod = await getAppTestModule(); if (appTestMod) { const appTestResult = await appTestMod.executeTool(name, input, ctx); if (appTestResult !== null) return appTestResult; } // skills ツール (ReadSkill) const { executeSkillTool } = await import('./skills.js'); const skillResult = executeSkillTool(name, input, ctx); if (skillResult !== null) return skillResult; return { output: `Unknown tool: ${name}`, isError: true }; } /** * ツールを実行するルーター(生データ自動保存ラッパー付き)。 * executeToolInner に委譲した後、成功した対象ツールの結果を logs/raw/ に保存する。 */ export async function executeTool( name: string, input: Record, ctx: ToolContext, ): Promise { logger.debug(`[tools/index] executing ${name}`); const result = await executeToolInner(name, input, ctx); // raw保存ラッパー: 成功した対象ツールの結果を {logsRoot}/raw/ に保存(計画5: runtimeDir 解決) if (ctx.workspacePath && !result.isError) { const logsRoot = runtimeLogsDir(ctx); if (RAW_SAVE_TOOLS.has(name)) { saveRawData(logsRoot, name, result.output); } else if (RAW_LOG_ONLY_TOOLS.has(name)) { const pathMatch = result.output.match(/-> (.+?) \(/); if (pathMatch?.[1]) { logRawDownload(logsRoot, name, pathMatch[1], result.output.length); } } // 構造化データ保存: structuredBlocks があれば {logsRoot}/structured/ に保存 if (result.structuredBlocks?.length) { saveStructuredBlocks(logsRoot, result.structuredBlocks); } } return result; }