mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 03:56:45 +00:00
* fix(gateway): avoid premature agent.wait completion on transient errors * fix(agent): preemptively guard tool results against context overflow * fix: harden tool-result context guard and add message_id metadata * fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID The run.skill-filter test was mocking ../../routing/session-key.js with only buildAgentMainSessionKey and normalizeAgentId, but the module also exports DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts. Switch to importOriginal pattern so all real exports are preserved alongside the mocked functions. * pi-runner: guard accumulated tool-result overflow in transformContext * PI runner: compact overflowing tool-result context * Subagent: harden tool-result context recovery * Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios. * Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies. * Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior. * Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features. * Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios. * Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels. * fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)
502 lines
18 KiB
TypeScript
502 lines
18 KiB
TypeScript
import {
|
|
codingTools,
|
|
createEditTool,
|
|
createReadTool,
|
|
createWriteTool,
|
|
readTool,
|
|
} from "@mariozechner/pi-coding-agent";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import type { ToolLoopDetectionConfig } from "../config/types.tools.js";
|
|
import { logWarn } from "../logger.js";
|
|
import { getPluginToolMeta } from "../plugins/tools.js";
|
|
import { isSubagentSessionKey } from "../routing/session-key.js";
|
|
import { resolveGatewayMessageChannel } from "../utils/message-channel.js";
|
|
import { resolveAgentConfig } from "./agent-scope.js";
|
|
import { createApplyPatchTool } from "./apply-patch.js";
|
|
import {
|
|
createExecTool,
|
|
createProcessTool,
|
|
type ExecToolDefaults,
|
|
type ProcessToolDefaults,
|
|
} from "./bash-tools.js";
|
|
import { listChannelAgentTools } from "./channel-tools.js";
|
|
import type { ModelAuthMode } from "./model-auth.js";
|
|
import { createOpenClawTools } from "./openclaw-tools.js";
|
|
import { wrapToolWithAbortSignal } from "./pi-tools.abort.js";
|
|
import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
|
|
import {
|
|
isToolAllowedByPolicies,
|
|
resolveEffectiveToolPolicy,
|
|
resolveGroupToolPolicy,
|
|
resolveSubagentToolPolicy,
|
|
} from "./pi-tools.policy.js";
|
|
import {
|
|
assertRequiredParams,
|
|
CLAUDE_PARAM_GROUPS,
|
|
createOpenClawReadTool,
|
|
createSandboxedEditTool,
|
|
createSandboxedReadTool,
|
|
createSandboxedWriteTool,
|
|
normalizeToolParams,
|
|
patchToolSchemaForClaudeCompatibility,
|
|
wrapToolWorkspaceRootGuard,
|
|
wrapToolParamNormalization,
|
|
} from "./pi-tools.read.js";
|
|
import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js";
|
|
import type { AnyAgentTool } from "./pi-tools.types.js";
|
|
import type { SandboxContext } from "./sandbox.js";
|
|
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
|
import {
|
|
applyToolPolicyPipeline,
|
|
buildDefaultToolPolicyPipelineSteps,
|
|
} from "./tool-policy-pipeline.js";
|
|
import {
|
|
applyOwnerOnlyToolPolicy,
|
|
collectExplicitAllowlist,
|
|
mergeAlsoAllowPolicy,
|
|
resolveToolProfilePolicy,
|
|
} from "./tool-policy.js";
|
|
import { resolveWorkspaceRoot } from "./workspace-dir.js";
|
|
|
|
function isOpenAIProvider(provider?: string) {
|
|
const normalized = provider?.trim().toLowerCase();
|
|
return normalized === "openai" || normalized === "openai-codex";
|
|
}
|
|
|
|
function isApplyPatchAllowedForModel(params: {
|
|
modelProvider?: string;
|
|
modelId?: string;
|
|
allowModels?: string[];
|
|
}) {
|
|
const allowModels = Array.isArray(params.allowModels) ? params.allowModels : [];
|
|
if (allowModels.length === 0) {
|
|
return true;
|
|
}
|
|
const modelId = params.modelId?.trim();
|
|
if (!modelId) {
|
|
return false;
|
|
}
|
|
const normalizedModelId = modelId.toLowerCase();
|
|
const provider = params.modelProvider?.trim().toLowerCase();
|
|
const normalizedFull =
|
|
provider && !normalizedModelId.includes("/")
|
|
? `${provider}/${normalizedModelId}`
|
|
: normalizedModelId;
|
|
return allowModels.some((entry) => {
|
|
const normalized = entry.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
return normalized === normalizedModelId || normalized === normalizedFull;
|
|
});
|
|
}
|
|
|
|
function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
|
|
const cfg = params.cfg;
|
|
const globalExec = cfg?.tools?.exec;
|
|
const agentExec =
|
|
cfg && params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools?.exec : undefined;
|
|
return {
|
|
host: agentExec?.host ?? globalExec?.host,
|
|
security: agentExec?.security ?? globalExec?.security,
|
|
ask: agentExec?.ask ?? globalExec?.ask,
|
|
node: agentExec?.node ?? globalExec?.node,
|
|
pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend,
|
|
safeBins: agentExec?.safeBins ?? globalExec?.safeBins,
|
|
backgroundMs: agentExec?.backgroundMs ?? globalExec?.backgroundMs,
|
|
timeoutSec: agentExec?.timeoutSec ?? globalExec?.timeoutSec,
|
|
approvalRunningNoticeMs:
|
|
agentExec?.approvalRunningNoticeMs ?? globalExec?.approvalRunningNoticeMs,
|
|
cleanupMs: agentExec?.cleanupMs ?? globalExec?.cleanupMs,
|
|
notifyOnExit: agentExec?.notifyOnExit ?? globalExec?.notifyOnExit,
|
|
notifyOnExitEmptySuccess:
|
|
agentExec?.notifyOnExitEmptySuccess ?? globalExec?.notifyOnExitEmptySuccess,
|
|
applyPatch: agentExec?.applyPatch ?? globalExec?.applyPatch,
|
|
};
|
|
}
|
|
|
|
function resolveFsConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
|
|
const cfg = params.cfg;
|
|
const globalFs = cfg?.tools?.fs;
|
|
const agentFs =
|
|
cfg && params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools?.fs : undefined;
|
|
return {
|
|
workspaceOnly: agentFs?.workspaceOnly ?? globalFs?.workspaceOnly,
|
|
};
|
|
}
|
|
|
|
export function resolveToolLoopDetectionConfig(params: {
|
|
cfg?: OpenClawConfig;
|
|
agentId?: string;
|
|
}): ToolLoopDetectionConfig | undefined {
|
|
const global = params.cfg?.tools?.loopDetection;
|
|
const agent =
|
|
params.agentId && params.cfg
|
|
? resolveAgentConfig(params.cfg, params.agentId)?.tools?.loopDetection
|
|
: undefined;
|
|
|
|
if (!agent) {
|
|
return global;
|
|
}
|
|
if (!global) {
|
|
return agent;
|
|
}
|
|
|
|
return {
|
|
...global,
|
|
...agent,
|
|
detectors: {
|
|
...global.detectors,
|
|
...agent.detectors,
|
|
},
|
|
};
|
|
}
|
|
|
|
export const __testing = {
|
|
cleanToolSchemaForGemini,
|
|
normalizeToolParams,
|
|
patchToolSchemaForClaudeCompatibility,
|
|
wrapToolParamNormalization,
|
|
assertRequiredParams,
|
|
} as const;
|
|
|
|
export function createOpenClawCodingTools(options?: {
|
|
exec?: ExecToolDefaults & ProcessToolDefaults;
|
|
messageProvider?: string;
|
|
agentAccountId?: string;
|
|
messageTo?: string;
|
|
messageThreadId?: string | number;
|
|
sandbox?: SandboxContext | null;
|
|
sessionKey?: string;
|
|
agentDir?: string;
|
|
workspaceDir?: string;
|
|
config?: OpenClawConfig;
|
|
abortSignal?: AbortSignal;
|
|
/**
|
|
* Provider of the currently selected model (used for provider-specific tool quirks).
|
|
* Example: "anthropic", "openai", "google", "openai-codex".
|
|
*/
|
|
modelProvider?: string;
|
|
/** Model id for the current provider (used for model-specific tool gating). */
|
|
modelId?: string;
|
|
/** Model context window in tokens (used to scale read-tool output budget). */
|
|
modelContextWindowTokens?: number;
|
|
/**
|
|
* Auth mode for the current provider. We only need this for Anthropic OAuth
|
|
* tool-name blocking quirks.
|
|
*/
|
|
modelAuthMode?: ModelAuthMode;
|
|
/** Current channel ID for auto-threading (Slack). */
|
|
currentChannelId?: string;
|
|
/** Current thread timestamp for auto-threading (Slack). */
|
|
currentThreadTs?: string;
|
|
/** Group id for channel-level tool policy resolution. */
|
|
groupId?: string | null;
|
|
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
groupChannel?: string | null;
|
|
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
groupSpace?: string | null;
|
|
/** Parent session key for subagent group policy inheritance. */
|
|
spawnedBy?: string | null;
|
|
senderId?: string | null;
|
|
senderName?: string | null;
|
|
senderUsername?: string | null;
|
|
senderE164?: string | null;
|
|
/** Reply-to mode for Slack auto-threading. */
|
|
replyToMode?: "off" | "first" | "all";
|
|
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
|
hasRepliedRef?: { value: boolean };
|
|
/** If true, the model has native vision capability */
|
|
modelHasVision?: boolean;
|
|
/** Require explicit message targets (no implicit last-route sends). */
|
|
requireExplicitMessageTarget?: boolean;
|
|
/** If true, omit the message tool from the tool list. */
|
|
disableMessageTool?: boolean;
|
|
/** Whether the sender is an owner (required for owner-only tools). */
|
|
senderIsOwner?: boolean;
|
|
}): AnyAgentTool[] {
|
|
const execToolName = "exec";
|
|
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
|
const {
|
|
agentId,
|
|
globalPolicy,
|
|
globalProviderPolicy,
|
|
agentPolicy,
|
|
agentProviderPolicy,
|
|
profile,
|
|
providerProfile,
|
|
profileAlsoAllow,
|
|
providerProfileAlsoAllow,
|
|
} = resolveEffectiveToolPolicy({
|
|
config: options?.config,
|
|
sessionKey: options?.sessionKey,
|
|
modelProvider: options?.modelProvider,
|
|
modelId: options?.modelId,
|
|
});
|
|
const groupPolicy = resolveGroupToolPolicy({
|
|
config: options?.config,
|
|
sessionKey: options?.sessionKey,
|
|
spawnedBy: options?.spawnedBy,
|
|
messageProvider: options?.messageProvider,
|
|
groupId: options?.groupId,
|
|
groupChannel: options?.groupChannel,
|
|
groupSpace: options?.groupSpace,
|
|
accountId: options?.agentAccountId,
|
|
senderId: options?.senderId,
|
|
senderName: options?.senderName,
|
|
senderUsername: options?.senderUsername,
|
|
senderE164: options?.senderE164,
|
|
});
|
|
const profilePolicy = resolveToolProfilePolicy(profile);
|
|
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
|
|
|
const profilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(profilePolicy, profileAlsoAllow);
|
|
const providerProfilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(
|
|
providerProfilePolicy,
|
|
providerProfileAlsoAllow,
|
|
);
|
|
// Prefer sessionKey for process isolation scope to prevent cross-session process visibility/killing.
|
|
// Fallback to agentId if no sessionKey is available (e.g. legacy or global contexts).
|
|
const scopeKey =
|
|
options?.exec?.scopeKey ?? options?.sessionKey ?? (agentId ? `agent:${agentId}` : undefined);
|
|
const subagentPolicy =
|
|
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
|
? resolveSubagentToolPolicy(
|
|
options.config,
|
|
getSubagentDepthFromSessionStore(options.sessionKey, { cfg: options.config }),
|
|
)
|
|
: undefined;
|
|
const allowBackground = isToolAllowedByPolicies("process", [
|
|
profilePolicyWithAlsoAllow,
|
|
providerProfilePolicyWithAlsoAllow,
|
|
globalPolicy,
|
|
globalProviderPolicy,
|
|
agentPolicy,
|
|
agentProviderPolicy,
|
|
groupPolicy,
|
|
sandbox?.tools,
|
|
subagentPolicy,
|
|
]);
|
|
const execConfig = resolveExecConfig({ cfg: options?.config, agentId });
|
|
const fsConfig = resolveFsConfig({ cfg: options?.config, agentId });
|
|
const sandboxRoot = sandbox?.workspaceDir;
|
|
const sandboxFsBridge = sandbox?.fsBridge;
|
|
const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";
|
|
const workspaceRoot = resolveWorkspaceRoot(options?.workspaceDir);
|
|
const workspaceOnly = fsConfig.workspaceOnly === true;
|
|
const applyPatchConfig = execConfig.applyPatch;
|
|
// Secure by default: apply_patch is workspace-contained unless explicitly disabled.
|
|
// (tools.fs.workspaceOnly is a separate umbrella flag for read/write/edit/apply_patch.)
|
|
const applyPatchWorkspaceOnly = workspaceOnly || applyPatchConfig?.workspaceOnly !== false;
|
|
const applyPatchEnabled =
|
|
!!applyPatchConfig?.enabled &&
|
|
isOpenAIProvider(options?.modelProvider) &&
|
|
isApplyPatchAllowedForModel({
|
|
modelProvider: options?.modelProvider,
|
|
modelId: options?.modelId,
|
|
allowModels: applyPatchConfig?.allowModels,
|
|
});
|
|
|
|
if (sandboxRoot && !sandboxFsBridge) {
|
|
throw new Error("Sandbox filesystem bridge is unavailable.");
|
|
}
|
|
|
|
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
|
if (tool.name === readTool.name) {
|
|
if (sandboxRoot) {
|
|
const sandboxed = createSandboxedReadTool({
|
|
root: sandboxRoot,
|
|
bridge: sandboxFsBridge!,
|
|
modelContextWindowTokens: options?.modelContextWindowTokens,
|
|
});
|
|
return [workspaceOnly ? wrapToolWorkspaceRootGuard(sandboxed, sandboxRoot) : sandboxed];
|
|
}
|
|
const freshReadTool = createReadTool(workspaceRoot);
|
|
const wrapped = createOpenClawReadTool(freshReadTool, {
|
|
modelContextWindowTokens: options?.modelContextWindowTokens,
|
|
});
|
|
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
|
|
}
|
|
if (tool.name === "bash" || tool.name === execToolName) {
|
|
return [];
|
|
}
|
|
if (tool.name === "write") {
|
|
if (sandboxRoot) {
|
|
return [];
|
|
}
|
|
// Wrap with param normalization for Claude Code compatibility
|
|
const wrapped = wrapToolParamNormalization(
|
|
createWriteTool(workspaceRoot),
|
|
CLAUDE_PARAM_GROUPS.write,
|
|
);
|
|
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
|
|
}
|
|
if (tool.name === "edit") {
|
|
if (sandboxRoot) {
|
|
return [];
|
|
}
|
|
// Wrap with param normalization for Claude Code compatibility
|
|
const wrapped = wrapToolParamNormalization(
|
|
createEditTool(workspaceRoot),
|
|
CLAUDE_PARAM_GROUPS.edit,
|
|
);
|
|
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
|
|
}
|
|
return [tool];
|
|
});
|
|
const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {};
|
|
const execTool = createExecTool({
|
|
...execDefaults,
|
|
host: options?.exec?.host ?? execConfig.host,
|
|
security: options?.exec?.security ?? execConfig.security,
|
|
ask: options?.exec?.ask ?? execConfig.ask,
|
|
node: options?.exec?.node ?? execConfig.node,
|
|
pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend,
|
|
safeBins: options?.exec?.safeBins ?? execConfig.safeBins,
|
|
agentId,
|
|
cwd: workspaceRoot,
|
|
allowBackground,
|
|
scopeKey,
|
|
sessionKey: options?.sessionKey,
|
|
messageProvider: options?.messageProvider,
|
|
backgroundMs: options?.exec?.backgroundMs ?? execConfig.backgroundMs,
|
|
timeoutSec: options?.exec?.timeoutSec ?? execConfig.timeoutSec,
|
|
approvalRunningNoticeMs:
|
|
options?.exec?.approvalRunningNoticeMs ?? execConfig.approvalRunningNoticeMs,
|
|
notifyOnExit: options?.exec?.notifyOnExit ?? execConfig.notifyOnExit,
|
|
notifyOnExitEmptySuccess:
|
|
options?.exec?.notifyOnExitEmptySuccess ?? execConfig.notifyOnExitEmptySuccess,
|
|
sandbox: sandbox
|
|
? {
|
|
containerName: sandbox.containerName,
|
|
workspaceDir: sandbox.workspaceDir,
|
|
containerWorkdir: sandbox.containerWorkdir,
|
|
env: sandbox.docker.env,
|
|
}
|
|
: undefined,
|
|
});
|
|
const processTool = createProcessTool({
|
|
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
|
|
scopeKey,
|
|
});
|
|
const applyPatchTool =
|
|
!applyPatchEnabled || (sandboxRoot && !allowWorkspaceWrites)
|
|
? null
|
|
: createApplyPatchTool({
|
|
cwd: sandboxRoot ?? workspaceRoot,
|
|
sandbox:
|
|
sandboxRoot && allowWorkspaceWrites
|
|
? { root: sandboxRoot, bridge: sandboxFsBridge! }
|
|
: undefined,
|
|
workspaceOnly: applyPatchWorkspaceOnly,
|
|
});
|
|
const tools: AnyAgentTool[] = [
|
|
...base,
|
|
...(sandboxRoot
|
|
? allowWorkspaceWrites
|
|
? [
|
|
workspaceOnly
|
|
? wrapToolWorkspaceRootGuard(
|
|
createSandboxedEditTool({ root: sandboxRoot, bridge: sandboxFsBridge! }),
|
|
sandboxRoot,
|
|
)
|
|
: createSandboxedEditTool({ root: sandboxRoot, bridge: sandboxFsBridge! }),
|
|
workspaceOnly
|
|
? wrapToolWorkspaceRootGuard(
|
|
createSandboxedWriteTool({ root: sandboxRoot, bridge: sandboxFsBridge! }),
|
|
sandboxRoot,
|
|
)
|
|
: createSandboxedWriteTool({ root: sandboxRoot, bridge: sandboxFsBridge! }),
|
|
]
|
|
: []
|
|
: []),
|
|
...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
|
|
execTool as unknown as AnyAgentTool,
|
|
processTool as unknown as AnyAgentTool,
|
|
// Channel docking: include channel-defined agent tools (login, etc.).
|
|
...listChannelAgentTools({ cfg: options?.config }),
|
|
...createOpenClawTools({
|
|
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
|
|
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
|
|
agentSessionKey: options?.sessionKey,
|
|
agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
|
|
agentAccountId: options?.agentAccountId,
|
|
agentTo: options?.messageTo,
|
|
agentThreadId: options?.messageThreadId,
|
|
agentGroupId: options?.groupId ?? null,
|
|
agentGroupChannel: options?.groupChannel ?? null,
|
|
agentGroupSpace: options?.groupSpace ?? null,
|
|
agentDir: options?.agentDir,
|
|
sandboxRoot,
|
|
sandboxFsBridge,
|
|
workspaceDir: workspaceRoot,
|
|
sandboxed: !!sandbox,
|
|
config: options?.config,
|
|
pluginToolAllowlist: collectExplicitAllowlist([
|
|
profilePolicy,
|
|
providerProfilePolicy,
|
|
globalPolicy,
|
|
globalProviderPolicy,
|
|
agentPolicy,
|
|
agentProviderPolicy,
|
|
groupPolicy,
|
|
sandbox?.tools,
|
|
subagentPolicy,
|
|
]),
|
|
currentChannelId: options?.currentChannelId,
|
|
currentThreadTs: options?.currentThreadTs,
|
|
replyToMode: options?.replyToMode,
|
|
hasRepliedRef: options?.hasRepliedRef,
|
|
modelHasVision: options?.modelHasVision,
|
|
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
|
|
disableMessageTool: options?.disableMessageTool,
|
|
requesterAgentIdOverride: agentId,
|
|
}),
|
|
];
|
|
// Security: treat unknown/undefined as unauthorized (opt-in, not opt-out)
|
|
const senderIsOwner = options?.senderIsOwner === true;
|
|
const toolsByAuthorization = applyOwnerOnlyToolPolicy(tools, senderIsOwner);
|
|
const subagentFiltered = applyToolPolicyPipeline({
|
|
tools: toolsByAuthorization,
|
|
toolMeta: (tool) => getPluginToolMeta(tool),
|
|
warn: logWarn,
|
|
steps: [
|
|
...buildDefaultToolPolicyPipelineSteps({
|
|
profilePolicy: profilePolicyWithAlsoAllow,
|
|
profile,
|
|
providerProfilePolicy: providerProfilePolicyWithAlsoAllow,
|
|
providerProfile,
|
|
globalPolicy,
|
|
globalProviderPolicy,
|
|
agentPolicy,
|
|
agentProviderPolicy,
|
|
groupPolicy,
|
|
agentId,
|
|
}),
|
|
{ policy: sandbox?.tools, label: "sandbox tools.allow" },
|
|
{ policy: subagentPolicy, label: "subagent tools.allow" },
|
|
],
|
|
});
|
|
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
|
|
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
|
|
// Provider-specific cleaning: Gemini needs constraint keywords stripped, but Anthropic expects them.
|
|
const normalized = subagentFiltered.map((tool) =>
|
|
normalizeToolParameters(tool, { modelProvider: options?.modelProvider }),
|
|
);
|
|
const withHooks = normalized.map((tool) =>
|
|
wrapToolWithBeforeToolCallHook(tool, {
|
|
agentId,
|
|
sessionKey: options?.sessionKey,
|
|
loopDetection: resolveToolLoopDetectionConfig({ cfg: options?.config, agentId }),
|
|
}),
|
|
);
|
|
const withAbort = options?.abortSignal
|
|
? withHooks.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal))
|
|
: withHooks;
|
|
|
|
// NOTE: Keep canonical (lowercase) tool names here.
|
|
// pi-ai's Anthropic OAuth transport remaps tool names to Claude Code-style names
|
|
// on the wire and maps them back for tool dispatch.
|
|
return withAbort;
|
|
}
|