/** * RequestTool — META_TOOL for the tool-request mechanism. * * When an agent realises it needs a tool that this movement does not expose, * it calls RequestTool({ name, reason }). The request is recorded so the gap * becomes visible in the task detail and per-piece aggregation, so an operator * can enable the tool for the workspace (Settings → Tools). When the run can * reach a user, the interactive approval path (`toolApprovalInteractive`) * pauses for consent and lets the run continue with the tool once approved; * otherwise the request is recorded and the agent proceeds without it. * * Classification (recorded as `category`): * - already_available → the tool IS exposed here; just call it (not recorded). * - available_not_allowed (`requested`) → exists in the catalog but not in * this movement → recorded as a genuine omission candidate. * - unknown → not in the catalog (possible hallucination / capability gap). * * Plumbing: ToolContext carries `effectiveToolNames` (this movement's set), * `knownToolNames` (full catalog, injected by the dispatcher), and * `recordToolRequest` (bound to task/job/space by the worker). */ import { ToolDef } from '../../llm/openai-compat.js'; import type { ToolContext, ToolResult } from './tool-context.js'; const REQUEST_TOOL_DEF: ToolDef = { type: 'function', function: { name: 'RequestTool', description: 'この movement に無いツールが必要なとき name と理由(reason)を渡して要求する。ユーザーが応答できる実行では承認を求め、承認されればそのまま使える。それ以外は記録のみで即時には使えないので、無しで進めるか complete({status:"needs_user_input"}) でユーザーに依頼する(恒久的に許可するにはワークスペースの設定 → ツールで有効化)。詳細は ReadToolDoc({ name: "RequestTool" })。', parameters: { type: 'object', properties: { name: { type: 'string', description: '必要なツール名(例: WebSearch, Bash)。' }, reason: { type: 'string', description: 'なぜそのツールが必要か(具体的に)。' }, }, required: ['name', 'reason'], }, }, }; export const TOOL_DEFS: Record = { RequestTool: REQUEST_TOOL_DEF, }; export async function executeTool( name: string, input: Record, ctx: ToolContext, ): Promise { if (name !== 'RequestTool') return null; const toolName = typeof input['name'] === 'string' ? input['name'].trim() : ''; const reason = typeof input['reason'] === 'string' ? input['reason'].trim() : ''; if (!toolName) return { output: 'RequestTool: name は必須です。', isError: true }; if (!reason) return { output: 'RequestTool: reason(なぜ必要か)は必須です。', isError: true }; // Already available in THIS movement → nothing to record, just tell it to call. const effective = ctx.effectiveToolNames ?? []; if (effective.includes(toolName)) { return { output: `"${toolName}" はこの movement で既に利用可能です。そのまま呼び出してください。`, isError: false, }; } // Classify against the full catalog. MCP tools (mcp__*) can't be cheaply // enumerated, so treat them as "exists but not allowed here". const known = ctx.knownToolNames ?? []; const isMcp = toolName.startsWith('mcp__'); const category: 'requested' | 'unknown' = known.includes(toolName) || isMcp ? 'requested' : 'unknown'; if (category === 'unknown') { // The requested tool does not exist in the catalog (likely a hallucinated // name). Return a HARD ERROR so the agent does not mistake it for a pending // approval or a successful call. Still recorded for diagnostics (only when a // recorder is wired — `?.` is a silent no-op otherwise, so the message must // NOT claim it was recorded). ctx.recordToolRequest?.({ toolName, reason, category: 'unknown' }); return { output: `"${toolName}" というツールは存在しません。RequestTool で要求できるのは実在するツール名だけです。` + ` これは承認待ちではありません — 利用可能なツールで進めてください(一覧は ReadToolDoc 等で確認できます)。`, isError: true, }; } // available_not_allowed. If this run can reach a user, pause for inline // approval (blocking permission-prompt). Otherwise record auto_denied and // let the agent proceed without it (the user finds the request later). if (ctx.toolApprovalInteractive) { ctx.recordToolRequest?.({ toolName, reason, category: 'requested', status: 'pending' }); ctx.pendingToolApproval = { toolName }; return { output: `"${toolName}" の利用にはユーザーの承認が必要です。承認/拒否を待っています。` + ` 承認されればこのまま続行でき、拒否されればそのツール無しで進めることになります。`, isError: false, }; } ctx.recordToolRequest?.({ toolName, reason, category: 'requested', status: 'auto_denied' }); return { output: `"${toolName}" はこの movement では許可されていないため使えません(要求を記録しました)。` + ` このツール無しで進めるか、達成できない場合は complete({status:"needs_user_input", missing_info:"..."}) でユーザーに依頼してください。` + ` ワークスペースの設定(設定 → ツール)で許可すると次回から使えます。`, isError: false, }; }