Files
moltbot/src/agents/pi-tools.before-tool-call.ts

97 lines
2.6 KiB
TypeScript

import type { AnyAgentTool } from "./tools/common.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { normalizeToolName } from "./tool-policy.js";
type HookContext = {
agentId?: string;
sessionKey?: string;
};
type HookOutcome = { blocked: true; reason: string } | { blocked: false; params: unknown };
const log = createSubsystemLogger("agents/tools");
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export async function runBeforeToolCallHook(args: {
toolName: string;
params: unknown;
toolCallId?: string;
ctx?: HookContext;
}): Promise<HookOutcome> {
const hookRunner = getGlobalHookRunner();
if (!hookRunner?.hasHooks("before_tool_call")) {
return { blocked: false, params: args.params };
}
const toolName = normalizeToolName(args.toolName || "tool");
const params = args.params;
try {
const normalizedParams = isPlainObject(params) ? params : {};
const hookResult = await hookRunner.runBeforeToolCall(
{
toolName,
params: normalizedParams,
},
{
toolName,
agentId: args.ctx?.agentId,
sessionKey: args.ctx?.sessionKey,
},
);
if (hookResult?.block) {
return {
blocked: true,
reason: hookResult.blockReason || "Tool call blocked by plugin hook",
};
}
if (hookResult?.params && isPlainObject(hookResult.params)) {
if (isPlainObject(params)) {
return { blocked: false, params: { ...params, ...hookResult.params } };
}
return { blocked: false, params: hookResult.params };
}
} catch (err) {
const toolCallId = args.toolCallId ? ` toolCallId=${args.toolCallId}` : "";
log.warn(`before_tool_call hook failed: tool=${toolName}${toolCallId} error=${String(err)}`);
}
return { blocked: false, params };
}
export function wrapToolWithBeforeToolCallHook(
tool: AnyAgentTool,
ctx?: HookContext,
): AnyAgentTool {
const execute = tool.execute;
if (!execute) {
return tool;
}
const toolName = tool.name || "tool";
return {
...tool,
execute: async (toolCallId, params, signal, onUpdate) => {
const outcome = await runBeforeToolCallHook({
toolName,
params,
toolCallId,
ctx,
});
if (outcome.blocked) {
throw new Error(outcome.reason);
}
return await execute(toolCallId, outcome.params, signal, onUpdate);
},
};
}
export const __testing = {
runBeforeToolCallHook,
isPlainObject,
};