111 lines
5.6 KiB
TypeScript
111 lines
5.6 KiB
TypeScript
/**
|
|
* 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<string, ToolDef> = {
|
|
RequestTool: REQUEST_TOOL_DEF,
|
|
};
|
|
|
|
export async function executeTool(
|
|
name: string,
|
|
input: Record<string, unknown>,
|
|
ctx: ToolContext,
|
|
): Promise<ToolResult | null> {
|
|
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,
|
|
};
|
|
}
|