Files
maestro/src/engine/tools/tool-request.ts
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

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,
};
}